前言
首先聲明一下,這個(gè)是一個(gè)技術(shù)文章,中間可能會(huì)涉及到一些公司,名稱均已經(jīng)做脫敏處理,請(qǐng)勿對(duì)號(hào)入座。標(biāo)題純屬是做一下標(biāo)題黨,請(qǐng)勿當(dāng)真。
其次無論是基于什么的NLP,都會(huì)涉及到訓(xùn)練樣本的問題,由于服務(wù)的復(fù)雜性,樣本的分類可能會(huì)有所偏差,導(dǎo)致最終結(jié)果會(huì)有偏差,請(qǐng)大家理解,請(qǐng)勿當(dāng)真,僅當(dāng)作技術(shù)思路的參考。
背景
作為一個(gè)消費(fèi)者,我們常常可以看到各種數(shù)據(jù),例如經(jīng)常公開的一些行業(yè)數(shù)據(jù),XX快遞 X月的**萬票投訴率是多少,申訴率又是多少等等這些非常專業(yè)的數(shù)據(jù),這些數(shù)據(jù)都考慮了各家的業(yè)務(wù)量的大小了,非常科學(xué)。但是我們也知道譬如犯罪率,不能光看一個(gè)犯罪率數(shù)據(jù),還得多方面看,因?yàn)橐粋€(gè)盜竊和一個(gè)命案在犯罪率的統(tǒng)計(jì)上,都是一樣的,但是我們都知道這個(gè)背后的治安問題是不一樣嚴(yán)重的。同樣用投訴率等一樣會(huì)存在這個(gè)問題,我們不得不思考,在當(dāng)前機(jī)器學(xué)習(xí)已經(jīng)在大量應(yīng)用于各個(gè)行業(yè)的情況下,有沒有其他角度來看我們的快遞/物流服務(wù)?作為一個(gè)技術(shù)的愛好者,這里做一些拋磚引玉的做法,本文一共分兩個(gè)部分,第一個(gè)是利用機(jī)器學(xué)習(xí)進(jìn)行投訴分類,另外一個(gè)是利用機(jī)器學(xué)習(xí)進(jìn)行投訴的評(píng)價(jià),本文是利用機(jī)器學(xué)習(xí)進(jìn)行投訴分類供參考。也歡迎進(jìn)行技術(shù)討論。情感分類的后續(xù)我有空再放出來(解決前面說的用率值進(jìn)行統(tǒng)計(jì)的缺陷)。
說明
1、所有樣本來源sina黑貓投訴平臺(tái)的開放數(shù)據(jù),所以本文不會(huì)提供原始的數(shù)據(jù)供大家下載,如果有人需要復(fù)現(xiàn),請(qǐng)自己想辦法解決;
2、為了用于訓(xùn)練的樣本盡可能準(zhǔn)確,本文使用的樣本均是脫敏后,提供給不同的人,讓不同的人去分類,采用少數(shù)服從多數(shù)的分類原則最終確定樣本的分類;
3、由于服務(wù)的復(fù)雜性,一個(gè)投訴樣本里面,可能會(huì)存在多種分類可能,但是這里只采用一個(gè)分類,如:
**快遞送達(dá)快*驛站,快*驛站卻找理由不派送,每次都是同樣的理由,沒時(shí)間只能自取,不送貨,態(tài)度特別不好,沒有經(jīng)過同意直接放驛站,給*通本地網(wǎng)點(diǎn)打電話,一直說他們聯(lián)系快*驛站,結(jié)果聯(lián)系了幾天還是不派送,聯(lián)系*通在線客服投訴,在線客服卻不登記,不授予投訴。本人收件地址已經(jīng)備注,不要放驛站。
這個(gè)投訴內(nèi)容分類,分類是“未經(jīng)允許放驛站”?“不送貨上門”?“服務(wù)態(tài)度”?都有可能,不同的人可能有不同的意見,這里采用一個(gè)投訴只能有一個(gè)分類,然后多人進(jìn)行這個(gè)樣本進(jìn)行標(biāo)記分類,最后服從多數(shù)人的分類來確定最后的分類。
4、由于數(shù)據(jù)是來源于互聯(lián)網(wǎng)的平臺(tái)的數(shù)據(jù),而數(shù)據(jù)是消費(fèi)者自己輸入的,可能會(huì)一面之詞的情況,不代表任何立場,僅用于學(xué)術(shù)討論。
運(yùn)行環(huán)境
1、操作系統(tǒng) Ubuntu20.4
2、Python3.9
3、paddle2.1
4、Tesla K20(之前的GPU計(jì)算卡被燒了,現(xiàn)在礦工把計(jì)算卡都炒上天了,木有錢買新卡,只能把N年前退役的K20拿出來用)
各位看官覺得有用的話,可以打賞下買個(gè)新的計(jì)算卡
整體思路
如圖示,獲取到數(shù)據(jù)后,抽取一部分出來作為樣本,進(jìn)行打標(biāo),然后對(duì)模型進(jìn)行訓(xùn)練,用訓(xùn)練后的模型對(duì)未打標(biāo)分類的投訴數(shù)據(jù)進(jìn)行預(yù)測,預(yù)測結(jié)果作為分類結(jié)果,再用結(jié)果進(jìn)行分析。
注意事項(xiàng)
1、樣本的平衡性,由于采用機(jī)器學(xué)習(xí),所以對(duì)于不同分類的樣本數(shù)據(jù),大家要進(jìn)行合理的控制,如果樣本不均衡,可能會(huì)導(dǎo)致結(jié)果的失真。這是和機(jī)器學(xué)習(xí)的特效是有關(guān)的,舉個(gè)例子來說:
如果100個(gè)樣本里面,有99個(gè)男人,1個(gè)是女人,那么最后訓(xùn)練出來的模型盡管看起來ACC非常高,但是實(shí)際可能不如人意。因?yàn)殡S便抽一個(gè)出來,預(yù)測是男人的準(zhǔn)確率都可以到99%,所以無論是訓(xùn)練的樣本還是驗(yàn)證的樣本,我們都應(yīng)該盡可能的平衡,每個(gè)分類都是差不多的數(shù)量的樣本。
2、分詞過程中無意義的詞匯的過濾。投訴的原始數(shù)據(jù)中,有很多客戶的描述得非常詳盡,但是對(duì)于我們的機(jī)器學(xué)習(xí)來說,有時(shí)反而是一種阻礙,例如:
假如樣本數(shù)據(jù)中,劉1刀~劉100刀是冠軍,王1刀~王100刀是亞軍,那么很有可能給一個(gè)叫王*刀的給他預(yù)測,預(yù)測結(jié)果就是亞軍,但是我們知道這個(gè)預(yù)測并不科學(xué),但是在機(jī)器學(xué)習(xí)中,他們洞察出來的結(jié)果就是王*刀,是亞軍的概率是99%以上??
在開始動(dòng)手?jǐn)]代碼前,先看一下結(jié)果
從這里看,模型的分類區(qū)分度還是不錯(cuò)的
今天先寫到這里,要準(zhǔn)備回家做飯了,如果大家想看,記得點(diǎn)贊+收藏,點(diǎn)贊越多,我更新動(dòng)力越足。
———————————————————————————————————————
接著更新:
下面我們正式開始看看怎樣做吧。
Setp1:分類標(biāo)準(zhǔn)
在開始學(xué)習(xí)前,我們先確定標(biāo)準(zhǔn)分類,這里我們一共分10類(這個(gè)分類或許有不合理的地方,但是大家當(dāng)作技術(shù)研究探討使用就好,因?yàn)榉诸悰]有絕對(duì)的標(biāo)準(zhǔn),例如很多信息不更新,其實(shí)是由于貨物丟(或者是虛假丟貨)了,但是這個(gè)事情我們不能確定,而客戶投訴內(nèi)容只是說貨物中途幾天不動(dòng),我們只能歸類為信息更新不及時(shí)),分別為:
破損丟失
信息不更新
虛假簽收
未經(jīng)允許放驛站
其他
亂收費(fèi)
派送不上門
不上門取件
時(shí)效
虛假物流信息
服務(wù)態(tài)度
Setp2:樣本打標(biāo)
前面提到,我們打標(biāo)是一個(gè)非常關(guān)鍵的事情,我們機(jī)器學(xué)習(xí)就譬如是教會(huì)小朋友明辨是非,而樣本則是我們的教材,如果我們的教材出問題了,教學(xué)的結(jié)果可能就是錯(cuò)誤的。這里為了追求盡可能的相對(duì)科學(xué),我們把數(shù)據(jù)脫敏后,多人進(jìn)行標(biāo)記,然后采用投票的原則,投出最后的分類。
例如,上面的例子,分別給3個(gè)人進(jìn)行打標(biāo),其中2個(gè)分類為信息不更新,1個(gè)人分類為時(shí)效問題,這里我們根據(jù)最后分類結(jié)果投票的結(jié)果,選用分類為信息不更新,盡管說分類為時(shí)效也是有一定的道理的,但是無論如何我們都需要確定下來一個(gè)唯一的分類(突然讓我想起瘋?cè)墓适??……正常的反而被認(rèn)為是瘋的,但是這就是機(jī)器學(xué)習(xí)……??)。
Setp3:數(shù)據(jù)準(zhǔn)備
獲取到的數(shù)據(jù)比較多,我們不可能對(duì)所有的投訴都進(jìn)行人工分類,這也違背了我們這期的目的,我們對(duì)一些數(shù)據(jù)進(jìn)行打標(biāo)后,每個(gè)分類抽取100個(gè)樣本,然后按照下面的格式生成一個(gè)txt文件
投訴內(nèi)容_!_分類
這里需要注意,這里我用的分割符號(hào)是_ ! _,并不是",",因?yàn)槿绻闷渌?hào),很容易和投訴內(nèi)容中的符號(hào)重疊,導(dǎo)致分割不準(zhǔn)確,所以這里用了組合符合來做分割符,當(dāng)然,你也可以按照你的習(xí)慣來,不過建議是多個(gè)符號(hào)組合的分割符號(hào)。
由于機(jī)器學(xué)習(xí)不能直接對(duì)中文進(jìn)行學(xué)習(xí),我們需要將中文進(jìn)行轉(zhuǎn)換成為編碼
#coding=utf-8
'''
生成字典,如果沒有特殊情況,可以使用默認(rèn)字典就好,如果需要優(yōu)化,可以使用默認(rèn)字典+分詞字典
'''
import os
import io
import utils.jiebainfer as jiebainfer
import utils.myfile as myfile
data_root_path = './dataset'
data_path = os.path.join(data_root_path, 'list.txt')
def create_dict(data_path, data_root_path):
print('準(zhǔn)備生成字典')
dict_set = set()
dict_words_set = set()
type_dict=set()
with io.open(data_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
print(lines)
stopwords=myfile.Readstopword(os.path.join(data_root_path, 'stop_words.txt'))
for line in lines:
title = line.split('_!_')[0].replace('\n', '')
for s in title:
dict_set.add(s)
strlist=jiebainfer.split(title)
for word in strlist:
content_str = ''
for i in word:
if myfile.is_chinese(i):
content_str = content_str+i
if word in stopwords:
print(content_str+'是停用詞')
else:
dict_words_set.add(content_str)
type = line.split('_!_')[-1].replace('\n', '')
type_dict.add(type)
dict_make(data_root_path,'character_dict.txt', dict_set)
dict_make(data_root_path,'word_dict.txt', dict_words_set)
dict_make(data_root_path,'type_dict.txt', type_dict)
print("數(shù)據(jù)字典生成完成!")
def dict_make(dict_path,name, dict_set):
dict_path = os.path.join(dict_path, name)
dict_list = []
i = 0
for s in dict_set:
dict_list.append([s, i])
i += 1
dict_txt = dict(dict_list)
end_dict = {"<unk>": i}
dict_txt.update(end_dict)
with io.open(dict_path, 'w', encoding='utf-8') as f:
f.write(str(dict_txt))
if __name__ == '__main__':
create_dict(data_path, data_root_path)
我們把樣本txt文件放到
./dataset/list.txt
運(yùn)行上面的python,則可以生成單個(gè)字的詞典和用結(jié)巴分詞的詞典,這里需要注意的是,用分詞詞典,詞典會(huì)比較大,因?yàn)橹袊臐h字就那么幾千個(gè),但是組成的詞卻是可以很多的,但是正是由于這樣,用分詞的詞典的準(zhǔn)確度會(huì)高于單字作為詞典的(樣本足夠的情況下)。(今天先寫這里,待續(xù)……)
————————————————————————————————————
完成字典工作后,我們需要把前的樣本分為訓(xùn)練樣本和驗(yàn)證訓(xùn)練效果的兩組樣本,這里我們驗(yàn)證按照20%的比例從總樣本集中抽取,并且為了得到先對(duì)比較客觀的準(zhǔn)確率數(shù)據(jù),我們的驗(yàn)證樣本不和訓(xùn)練樣本重復(fù)(其實(shí)在學(xué)習(xí)樣本不多的情況下,這兩個(gè)樣本是可以重疊,但是這樣會(huì)導(dǎo)致訓(xùn)練過程中看到的準(zhǔn)確率偏高,但是由于訓(xùn)練樣本增多了,其實(shí)效果會(huì)更加好,但是實(shí)際沒有看到的數(shù)據(jù)高)
#coding=utf-8
'''
讀取詞典和分類詞典,將文本轉(zhuǎn)化為訓(xùn)練和驗(yàn)證的數(shù)據(jù)
'''
import os
import io
import utils.jiebainfer as jiebainfer
import utils.myfile as myfile
data_root_path='./dataset'
def create_data_list(dir):
# 清空歷史數(shù)據(jù)
with io.open(dir + 'test_list.txt', 'w') as f:
pass
with io.open(dir + 'train_list.txt', 'w') as f:
pass
with io.open(data_root_path + 'error.txt', 'w') as f:
pass
with io.open(os.path.join(data_root_path, 'word_dict.txt'), 'r', encoding='utf-8') as f_data:
dict_txt = eval(f_data.readlines()[0])
print('字典長度{}'.format(len(dict_txt.keys())))
print('字典最大序列{}'.format(len(dict_txt.keys())-1))
with io.open(os.path.join(data_root_path, 'type_dict.txt'), 'r', encoding='utf-8') as f_data:
type_txt = eval(f_data.readlines()[0])
print('分類字典長度{}'.format(len(type_txt.keys())))
print('分類字典最大序列{}'.format(len(type_txt.keys())-1))
with io.open(os.path.join(dir, 'list.txt'), 'r', encoding='utf-8') as f_data:
lines = f_data.readlines()
i = 0
errorstrlist=[]
for line in lines:
title = line.split('_!_')[0].replace('\n', '')
l = line.split('_!_')[1]
print(l,title)
# 對(duì)title分詞
words_list=jiebainfer.split(title)
if i % 5 == 0:
makelistfile(dir,'test_list.txt',words_list,dict_txt,errorstrlist,type_txt,l)
else:
makelistfile(dir,'train_list.txt',words_list,dict_txt,errorstrlist,type_txt,l)
i += 1
# 無法編碼的字符記錄下來
errorrec(errorstrlist)
# 保存新的詞典
savedict(dict_txt)
print("數(shù)據(jù)列表生成完成!")
def makelistfile(dir,filename,words_list,dict_txt,errorstrlist,type_txt,l):
# 讀取停用詞
stopwords=myfile.Readstopword(os.path.join(data_root_path, 'stop_words.txt'))
labs = ""
with io.open(os.path.join(dir, filename), 'a', encoding='utf-8') as f_train:
for s in words_list:
# 只保留中文
content_str = ''
for k in s:
if myfile.is_chinese(k):
content_str = content_str+k
# 判斷是否是停用詞
if content_str in stopwords:
print(content_str+'是停用詞')
else:
try:
lab = str(dict_txt[content_str])
except:
# lab = str(dict_txt['<unk>'])
if not content_str in errorstrlist:
errorstrlist.append(content_str)
# 動(dòng)態(tài)增加到詞典
dict_txt[content_str]=len(dict_txt.keys())
lab = str(dict_txt[content_str])
labs = labs + lab + ','
labs = labs[:-1]
ln=str(type_txt[l.replace('\n','')])
labs = labs + '\t' + ln + '\n'
size=labs.split(',')
if len(size)>0 and len(l)>0:
f_train.write(labs)
else:
print('特征不夠,拋棄')
def savedict(dict):
'''
保存新的詞典
'''
with io.open(data_root_path + 'newdict.txt', 'w') as f:
f.write(str(dict))
f.close()
def errorrec(strlist):
'''
編碼過程中遇到生僻字,無法編碼,記錄一下,方便優(yōu)化字典
'''
with io.open(data_root_path + 'error.txt', 'a') as f:
for string in strlist:
f.write(string)
f.close()
if __name__ == '__main__':
create_data_list(data_root_path)
在這里,我們還把動(dòng)態(tài)擴(kuò)充詞典的功能加上了,后面如果需要增加樣本,而之前的分詞詞典沒有的,會(huì)動(dòng)態(tài)增加到新的詞典中,這樣可以使的詞典進(jìn)行動(dòng)態(tài)的變化(如果使用單字詞典,建議使用網(wǎng)上的漢字字典,基本上不用再次動(dòng)態(tài)擴(kuò)充)