前言
首先聲明一下,這個是一個技術文章,中間可能會涉及到一些公司,名稱均已經做脫敏處理,請勿對號入座。標題純屬是做一下標題黨,請勿當真。
其次無論是基于什么的NLP,都會涉及到訓練樣本的問題,由于服務的復雜性,樣本的分類可能會有所偏差,導致最終結果會有偏差,請大家理解,請勿當真,僅當作技術思路的參考。
背景
作為一個消費者,我們常??梢钥吹礁鞣N數據,例如經常公開的一些行業(yè)數據,XX快遞 X月的**萬票投訴率是多少,申訴率又是多少等等這些非常專業(yè)的數據,這些數據都考慮了各家的業(yè)務量的大小了,非??茖W。但是我們也知道譬如犯罪率,不能光看一個犯罪率數據,還得多方面看,因為一個盜竊和一個命案在犯罪率的統(tǒng)計上,都是一樣的,但是我們都知道這個背后的治安問題是不一樣嚴重的。同樣用投訴率等一樣會存在這個問題,我們不得不思考,在當前機器學習已經在大量應用于各個行業(yè)的情況下,有沒有其他角度來看我們的快遞/物流服務?作為一個技術的愛好者,這里做一些拋磚引玉的做法,本文一共分兩個部分,第一個是利用機器學習進行投訴分類,另外一個是利用機器學習進行投訴的評價,本文是利用機器學習進行投訴分類供參考。也歡迎進行技術討論。情感分類的后續(xù)我有空再放出來(解決前面說的用率值進行統(tǒng)計的缺陷)。
說明
1、所有樣本來源sina黑貓投訴平臺的開放數據,所以本文不會提供原始的數據供大家下載,如果有人需要復現,請自己想辦法解決;
2、為了用于訓練的樣本盡可能準確,本文使用的樣本均是脫敏后,提供給不同的人,讓不同的人去分類,采用少數服從多數的分類原則最終確定樣本的分類;
3、由于服務的復雜性,一個投訴樣本里面,可能會存在多種分類可能,但是這里只采用一個分類,如:
**快遞送達快*驛站,快*驛站卻找理由不派送,每次都是同樣的理由,沒時間只能自取,不送貨,態(tài)度特別不好,沒有經過同意直接放驛站,給*通本地網點打電話,一直說他們聯(lián)系快*驛站,結果聯(lián)系了幾天還是不派送,聯(lián)系*通在線客服投訴,在線客服卻不登記,不授予投訴。本人收件地址已經備注,不要放驛站。
這個投訴內容分類,分類是“未經允許放驛站”?“不送貨上門”?“服務態(tài)度”?都有可能,不同的人可能有不同的意見,這里采用一個投訴只能有一個分類,然后多人進行這個樣本進行標記分類,最后服從多數人的分類來確定最后的分類。
4、由于數據是來源于互聯(lián)網的平臺的數據,而數據是消費者自己輸入的,可能會一面之詞的情況,不代表任何立場,僅用于學術討論。
運行環(huán)境
1、操作系統(tǒng) Ubuntu20.4
2、Python3.9
3、paddle2.1
4、Tesla K20(之前的GPU計算卡被燒了,現在礦工把計算卡都炒上天了,木有錢買新卡,只能把N年前退役的K20拿出來用)
各位看官覺得有用的話,可以打賞下買個新的計算卡
整體思路

如圖示,獲取到數據后,抽取一部分出來作為樣本,進行打標,然后對模型進行訓練,用訓練后的模型對未打標分類的投訴數據進行預測,預測結果作為分類結果,再用結果進行分析。
注意事項
1、樣本的平衡性,由于采用機器學習,所以對于不同分類的樣本數據,大家要進行合理的控制,如果樣本不均衡,可能會導致結果的失真。這是和機器學習的特效是有關的,舉個例子來說:
如果100個樣本里面,有99個男人,1個是女人,那么最后訓練出來的模型盡管看起來ACC非常高,但是實際可能不如人意。因為隨便抽一個出來,預測是男人的準確率都可以到99%,所以無論是訓練的樣本還是驗證的樣本,我們都應該盡可能的平衡,每個分類都是差不多的數量的樣本。
2、分詞過程中無意義的詞匯的過濾。投訴的原始數據中,有很多客戶的描述得非常詳盡,但是對于我們的機器學習來說,有時反而是一種阻礙,例如:
假如樣本數據中,劉1刀~劉100刀是冠軍,王1刀~王100刀是亞軍,那么很有可能給一個叫王*刀的給他預測,預測結果就是亞軍,但是我們知道這個預測并不科學,但是在機器學習中,他們洞察出來的結果就是王*刀,是亞軍的概率是99%以上??
在開始動手擼代碼前,先看一下結果

從這里看,模型的分類區(qū)分度還是不錯的
今天先寫到這里,要準備回家做飯了,如果大家想看,記得點贊+收藏,點贊越多,我更新動力越足。
———————————————————————————————————————
接著更新:
下面我們正式開始看看怎樣做吧。
Setp1:分類標準
在開始學習前,我們先確定標準分類,這里我們一共分10類(這個分類或許有不合理的地方,但是大家當作技術研究探討使用就好,因為分類沒有絕對的標準,例如很多信息不更新,其實是由于貨物丟(或者是虛假丟貨)了,但是這個事情我們不能確定,而客戶投訴內容只是說貨物中途幾天不動,我們只能歸類為信息更新不及時),分別為:
破損丟失
信息不更新
虛假簽收
未經允許放驛站
其他
亂收費
派送不上門
不上門取件
時效
虛假物流信息
服務態(tài)度
Setp2:樣本打標
前面提到,我們打標是一個非常關鍵的事情,我們機器學習就譬如是教會小朋友明辨是非,而樣本則是我們的教材,如果我們的教材出問題了,教學的結果可能就是錯誤的。這里為了追求盡可能的相對科學,我們把數據脫敏后,多人進行標記,然后采用投票的原則,投出最后的分類。

例如,上面的例子,分別給3個人進行打標,其中2個分類為信息不更新,1個人分類為時效問題,這里我們根據最后分類結果投票的結果,選用分類為信息不更新,盡管說分類為時效也是有一定的道理的,但是無論如何我們都需要確定下來一個唯一的分類(突然讓我想起瘋泉的故事??……正常的反而被認為是瘋的,但是這就是機器學習……??)。
Setp3:數據準備
獲取到的數據比較多,我們不可能對所有的投訴都進行人工分類,這也違背了我們這期的目的,我們對一些數據進行打標后,每個分類抽取100個樣本,然后按照下面的格式生成一個txt文件
投訴內容_!_分類
這里需要注意,這里我用的分割符號是_ ! _,并不是",",因為如果用其他符號,很容易和投訴內容中的符號重疊,導致分割不準確,所以這里用了組合符合來做分割符,當然,你也可以按照你的習慣來,不過建議是多個符號組合的分割符號。
由于機器學習不能直接對中文進行學習,我們需要將中文進行轉換成為編碼
#coding=utf-8
'''
生成字典,如果沒有特殊情況,可以使用默認字典就好,如果需要優(yōu)化,可以使用默認字典+分詞字典
'''
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('準備生成字典')
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("數據字典生成完成!")
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
運行上面的python,則可以生成單個字的詞典和用結巴分詞的詞典,這里需要注意的是,用分詞詞典,詞典會比較大,因為中國的漢字就那么幾千個,但是組成的詞卻是可以很多的,但是正是由于這樣,用分詞的詞典的準確度會高于單字作為詞典的(樣本足夠的情況下)。(今天先寫這里,待續(xù)……)
————————————————————————————————————
完成字典工作后,我們需要把前的樣本分為訓練樣本和驗證訓練效果的兩組樣本,這里我們驗證按照20%的比例從總樣本集中抽取,并且為了得到先對比較客觀的準確率數據,我們的驗證樣本不和訓練樣本重復(其實在學習樣本不多的情況下,這兩個樣本是可以重疊,但是這樣會導致訓練過程中看到的準確率偏高,但是由于訓練樣本增多了,其實效果會更加好,但是實際沒有看到的數據高)
#coding=utf-8
'''
讀取詞典和分類詞典,將文本轉化為訓練和驗證的數據
'''
import os
import io
import utils.jiebainfer as jiebainfer
import utils.myfile as myfile
data_root_path='./dataset'
def create_data_list(dir):
# 清空歷史數據
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)
# 對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("數據列表生成完成!")
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)
# 動態(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)
在這里,我們還把動態(tài)擴充詞典的功能加上了,后面如果需要增加樣本,而之前的分詞詞典沒有的,會動態(tài)增加到新的詞典中,這樣可以使的詞典進行動態(tài)的變化(如果使用單字詞典,建議使用網上的漢字字典,基本上不用再次動態(tài)擴充)