前言
首先声明一下,这个是一个技术文章,中间可能会涉及到一些公司,名称均已经做脱敏处理,请勿对号入座。标题纯属是做一下标题党,请勿当真。
其次无论是基于什么的NLP,都会涉及到训练样本的问题,由于服务的复杂性,样本的分类可能会有所偏差,导致最终结果会有偏差,请大家理解,请勿当真,仅当作技术思路的参考。
背景
作为一个消费者,我们常常可以看到各种数据,例如经常公开的一些行业数据,XX快递 X月的**万票投诉率是多少,申诉率又是多少等等这些非常专业的数据,这些数据都考虑了各家的业务量的大小了,非常科学。但是我们也知道譬如犯罪率,不能光看一个犯罪率数据,还得多方面看,因为一个盗窃和一个命案在犯罪率的统计上,都是一样的,但是我们都知道这个背后的治安问题是不一样严重的。同样用投诉率等一样会存在这个问题,我们不得不思考,在当前机器学习已经在大量应用于各个行业的情况下,有没有其他角度来看我们的快递/物流服务?作为一个技术的爱好者,这里做一些抛砖引玉的做法,本文一共分两个部分,第一个是利用机器学习进行投诉分类,另外一个是利用机器学习进行投诉的评价,本文是利用机器学习进行投诉分类供参考。也欢迎进行技术讨论。情感分类的后续我有空再放出来(解决前面说的用率值进行统计的缺陷)。
说明
1、所有样本来源sina黑猫投诉平台的开放数据,所以本文不会提供原始的数据供大家下载,如果有人需要复现,请自己想办法解决;
2、为了用于训练的样本尽可能准确,本文使用的样本均是脱敏后,提供给不同的人,让不同的人去分类,采用少数服从多数的分类原则最终确定样本的分类;
3、由于服务的复杂性,一个投诉样本里面,可能会存在多种分类可能,但是这里只采用一个分类,如:
**快递送达快*驿站,快*驿站却找理由不派送,每次都是同样的理由,没时间只能自取,不送货,态度特别不好,没有经过同意直接放驿站,给*通本地网点打电话,一直说他们联系快*驿站,结果联系了几天还是不派送,联系*通在线客服投诉,在线客服却不登记,不授予投诉。本人收件地址已经备注,不要放驿站。
这个投诉内容分类,分类是“未经允许放驿站”?“不送货上门”?“服务态度”?都有可能,不同的人可能有不同的意见,这里采用一个投诉只能有一个分类,然后多人进行这个样本进行标记分类,最后服从多数人的分类来确定最后的分类。
4、由于数据是来源于互联网的平台的数据,而数据是消费者自己输入的,可能会一面之词的情况,不代表任何立场,仅用于学术讨论。
运行环境
1、操作系统 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%以上
在开始动手撸代码前,先看一下结果
从这里看,模型的分类区分度还是不错的
今天先写到这里,要准备回家做饭了,如果大家想看,记得点赞+收藏,点赞越多,我更新动力越足。
———————————————————————————————————————
接着更新:
下面我们正式开始看看怎样做吧。
Setp1:分类标准
在开始学习前,我们先确定标准分类,这里我们一共分10类(这个分类或许有不合理的地方,但是大家当作技术研究探讨使用就好,因为分类没有绝对的标准,例如很多信息不更新,其实是由于货物丢(或者是虚假丢货)了,但是这个事情我们不能确定,而客户投诉内容只是说货物中途几天不动,我们只能归类为信息更新不及时),分别为:
破损丢失
信息不更新
虚假签收
未经允许放驿站
其他
乱收费
派送不上门
不上门取件
时效
虚假物流信息
服务态度
Setp2:样本打标
前面提到,我们打标是一个非常关键的事情,我们机器学习就譬如是教会小朋友明辨是非,而样本则是我们的教材,如果我们的教材出问题了,教学的结果可能就是错误的。这里为了追求尽可能的相对科学,我们把数据脱敏后,多人进行标记,然后采用投票的原则,投出最后的分类。
例如,上面的例子,分别给3个人进行打标,其中2个分类为信息不更新,1个人分类为时效问题,这里我们根据最后分类结果投票的结果,选用分类为信息不更新,尽管说分类为时效也是有一定的道理的,但是无论如何我们都需要确定下来一个唯一的分类(突然让我想起疯泉的故事……正常的反而被认为是疯的,但是这就是机器学习……)。
Setp3:数据准备
获取到的数据比较多,我们不可能对所有的投诉都进行人工分类,这也违背了我们这期的目的,我们对一些数据进行打标后,每个分类抽取100个样本,然后按照下面的格式生成一个txt文件
投诉内容_!_分类
这里需要注意,这里我用的分割符号是_ ! _,并不是",",因为如果用其他符号,很容易和投诉内容中的符号重叠,导致分割不准确,所以这里用了组合符合来做分割符,当然,你也可以按照你的习惯来,不过建议是多个符号组合的分割符号。
由于机器学习不能直接对中文进行学习,我们需要将中文进行转换成为编码
#coding=utf-8
'''
生成字典,如果没有特殊情况,可以使用默认字典就好,如果需要优化,可以使用默认字典+分词字典
'''
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 = {"": 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,则可以生成单个字的词典和用结巴分词的词典,这里需要注意的是,用分词词典,词典会比较大,因为中国的汉字就那么几千个,但是组成的词却是可以很多的,但是正是由于这样,用分词的词典的准确度会高于单字作为词典的(样本足够的情况下)。(今天先写这里,待续……)
————————————————————————————————————
完成字典工作后,我们需要把前的样本分为训练样本和验证训练效果的两组样本,这里我们验证按照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[''])
if not content_str in errorstrlist:
errorstrlist.append(content_str)
# 动态增加到词典
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):
'''
编码过程中遇到生僻字,无法编码,记录一下,方便优化字典
'''
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)
在这里,我们还把动态扩充词典的功能加上了,后面如果需要增加样本,而之前的分词词典没有的,会动态增加到新的词典中,这样可以使的词典进行动态的变化(如果使用单字词典,建议使用网上的汉字字典,基本上不用再次动态扩充)