赞
踩
ChatBOT即交流机器人,目前主要有三类:QABOT、TASKBOT、CHATBOT。本项目实战中主要实现两个简单的QABOT、CHATBOT。
QABOT的常见流程,给出一个问题,经过神经网络或者其他模型提取出关键字,然后根据关键字去搜寻答案的过程。主要分为信息检索与知识图谱,信息检索就是字面意思。知识图谱则是存储知识与知识之间的关系,将问答转换为查询语句,实现推理,也就是上面的提取关键词过程。主要实现方式:tfidf、SVM、朴素贝叶斯、RNN、CNN。
信息检索加seq2seq方法。
需要实现一个聊天机器人,该机器人可以起到智能客服的作用。由于语料的限制,准备了相关的编程问题,回答什么是python、python有什么优势等问题。
实现流程:
后面的QA机器人是问答模型内容,我们将在前面将基础功能进行完善,后续实现问答模型只用实现内部细节就好了。
实际运作流程:
接受用户的问题之后对问题进行基础的处理,然后对问题进行分类判断意图。如果用户希望咨询问题就调用问答模型,希望进行闲聊就调用闲聊模型。
使用seq2seq模型,该模型包括了对文本的embedding、编码层、attention机制的处理、解码层这几个内容。
使用召回和排序的机制进行实现,实现速度和准确率。
具体细节:
问题分析:对问题的基础性处理包括分词、词性的获取、词向量的获取。
问题召回:通过机器学习的方法进行海选,找到相似问题的前k个。
问题排序:通过深度学习模型计算准确率然后进行排序
设置阈值:返回结果
需要在虚拟环境下完成该项目,所以需要用Anaconda去建立一个新环境,对于的torch等包也要下载好。
常见conda命令:
conda create --name 环境名 python=3.x
conda create -n B --clone A 克隆已有环境
conda list 列出所有已安装的包
conda activate 环境名 激活环境
conda deactivate 环境名 退出环境
还要安装fasttext,用于文本分类,直接在对应环境pip install fasttext。
pysparnn直接去Github下载zip文件,然后复制到对应环境的scripts中,然后在当前文件路径中cmd运行pip install 对应文件名以及文件后缀,完成安装。
jieba的下载:pip install jieba即可
便完成了基本环境的配置。
由于我没有在base环境下安装包,我是直接在我之前学习Pytorch里面安装包的。所以为了方便我直接克隆了Pytorch环境,然后下载了一些其他的包。最后配置一个叫ChatBOT的环境成功,如图:
构建如下项目结构,其中corpus用于存放读取语料库、prepare_corpus用于准备语料库。config用于存放一些配置信息,比如文件路径。
jieba常用方法:lcut用于切分句子返回一个列表、load_userdict用于加载用户词典。
在test_user_dict中测试一下jieba是否能正常使用:
然后我们用自己创建的用户词典,user_dict区划分该句子:
在test_user_dict定义一个函数用于测试用户词典。
然后再main函数中运行对应函数,结果和用jieba自带词典效果一样。 由于是自己创造的词典,为了和方便分词并与jieba所默认的词性进行区分,我们在自己创造的词典的内容后加入kc即可(任意名字)。
上面就是手动创造词典的方式,为了获取更多的语料,我们可以去输入法官网下载相关词典。
搜狗输入法官网:https://pinyin.sogou.com/dict/cate/index/97?rf=dictindex
下载了一个计算机词汇大全,显然这是特殊的文件格式,我们可以用github上已有的转换器将该文件转换为txt文件,
下载地址:https://github.com/studyzy/imewlconverter,当然是在Release版本中找对应的操作系统。
下载完毕,由于该程序基于C#开发,所以还需要下载.Net 8.0,正常安装即可。
接下来我们只用将下载好的scel文件导入进去,就可以获得txt文件了。
转换成功!
词典内容,我们不太需要拼音,所以需要简单处理下该文件。
用这段代码处理一下该文件,
- import config
-
-
- if __name__ == '__main__':
- f = open('sogou_dict1.txt', 'w+')
- f1 = open(config.user_dict_path2, 'rb+') #user_dict_path2即转换文件对象的路径
- line = list(f1.readline().strip().split())
- line = [str(line[i], 'utf-8') for i in range(len(line))]
- while line:
- f.writelines(line[-1] + ' sgjsj')
- f.write('\n')
- line = list(f1.readline().strip().split())
- line = [str(line[i], 'utf-8') for i in range(len(line))]
处理结果如下,词性应该是sgjsj,我最开始打错了,就是搜狗计算机的意思。:
即分词后句子不重要的词语,常见停用词下载:https://github.com/goto456/stopwords
利用学习课程整理好的问答对,把问题分成以下类型。
概念问题、课程优势、语言优势等类型。
kc指的是课程、jgmc表示机构名称。
定义一个lib包用于存放基本的方法,比如分词、停用词获取。
分词api实现:cut_words负责实现中英文字符切分,cut负责判断条件,如果不用分为单个词,则使用jieba的lcut方法进行分词。
- """
- 分词
- """
- import logging
- import jieba
- import jieba.posseg as psg #posseg是lcut所在的库
- import config
- import re
- import string
-
- #加载词典
- jieba.load_userdict(config.user_dict_path)
-
- """
- 分词api
- """
- #关闭jiebalog输出即jieba的日志输出
- """
- logging优先级,从低到高
- DEBUG
- INFO
- WARNING
- ERROR
- CRITICAL
- """
- jieba.setLogLevel(logging.INFO) #logging用于记录程序运行的信息
-
- #单字分割,英文部分
- letters = string.ascii_lowercase #小写字母
- #单字分割 去除的标点
- filters= [",", "-", ".", " "]
- #停用词
- stopwords = set([i.strip() for i in open(config.stopwords_path,encoding='utf-8').readlines()])
- def cut_word(sentence):
- """实现中英文分词"""
- # 对中文按照字进行处理,对英文不分为字母
- sentence = re.sub("\s+", " ", sentence) #匹配空白,如空格、tab,将这些转换为单个空白符
- sentence = sentence.strip()
- result = []
- temp = ""
- for word in sentence:
- if word.lower() in letters:
- temp += word.lower()
- else:
- if temp != "": # 不是字母
- result.append(temp)
- temp = ""
- if word.strip() in filters: # 标点符号
- continue
- else: # 是单个字
- result.append(word)
- if temp != "": # 最后的temp中包含字母
- result.append(temp)
- return result
-
- def cut(sentence, by_words=False, use_stopwords=False, with_sg=False):
- """
- :param sentence: 传入参数
- :param by_words: 是否按照单个字进行分词
- :param use_stopwords:是否使用停用词
- :param with_sg:是否返回词性
- :return:
- """
- assert by_words != True or with_sg != True, "根据word切分时候无法返回词性"
- if by_words:
- return cut_word(sentence)
- else:
- ret = psg.lcut(sentence) #按照用户提供的词典进行分词
- #jieba获取词性直接.flag就好了
- if use_stopwords:
- ret = [(i.word, i.flag) for i in ret if i.word not in stopwords]
- if not with_sg:
- ret = [i.word for i in ret]
- return ret
使用jieba进行词语分类:
使用自定义的cut_word分类:
启用体用词与词性的jieba分类,可以看到,问号被丢弃了:
获取停用词可以另外创建一个py文件,如果每次分词都要计算一遍停用词速度会有所降低。可以采用在main中获取stopwords。
文本分类的目的就是为了意图识别。这是因为文本分类之后得到的是词向量,然后再对词向量进行关键词抽取,这些工作都是为意图识别打下基础。文本分类往往是一个多分类问题,可以理解为你希望机器人回答什么问题,就有多少种意图,就有多少个类别。
机器学习的分类方法:朴素贝叶斯和决策树
机器学习的文本分类流程:特征工程、模型构建、训练、评估。
如果效果不好就删除一下不太重要的词语,也可以用集成学习方法。
深度学习分类方法就比较熟悉了:文本embedding、多次线性和非线性的变换、根据变换结果计算得到损失函数、然后反向传播更新原来参数,然后输出较好的结果。
但是显然,我们此次项目的重点不在文本分类中,如果自己去设计并训练模型,效率实在是太低了,必须要在文本分类的过程中采取比较好的方案。于是选择了fasttext,速度快准确率高、便于使用。
fasttext基本使用如下:
建立模型:fastText.train_supervised(f,wordNgrams=1.epoch=),wordNgrams指的是一组数据中有几个词语。
保存模型:save_model(f)
加载:load_model(f)
predict:预测
fasttext要求格式为data+‘\t’+__label__+目标值。
准备两个内容:问题文本以及闲聊文本。
问题文本:采用课程提供的文本以及模板构造的文本(如果有需要私聊我即可)
闲聊文本:使用小黄鸡语料
下载地址: https://github.com/fateleak/dgk_lost_conv/tree/master/results
准备好即可~
创建如下目录:
在config文件下添加如下路径:
具体代码:
- import json
-
- import pandas
- import config
- from lib.cut_sentence import cut
- from tqdm import tqdm#进度条库
- def keywords_in_line(line):
- """判断是否line中有符合要求的词"""
- keywords = ["传智播客", "传智", "黑马程序员", "黑马", "python",
- "人工智能", "c语言", "c++", "java", "javaee", "前端", "移动开发", "ui",
- "ue", "大数据", "软件测试", "php", "h5", "产品经理", "linux", "运维", "go语言",
- "区块链", "影视制作", "pmp", "项目管理", "新媒体", "小程序", "前端"]
- for keyword in keywords:
- if keyword in line:
- return True
- return False
- def process_xiaohuangji(file):
- """处理小黄鸡的语料,小黄鸡的语料分为E开头和M开头。M开头有两个,M1为问答、M2为回答"""
- num = 0
- for line in tqdm(open(config.xiao_huang_ji_path, encoding='utf-8').readlines(), desc='xiaohuangji'): #读取数据
- if line.startswith('E'):
- flag = 0
- continue
- elif line.startswith('M'):
- if flag == 0: #第一个M出现
- line = line[1:].strip()
- flag = 1
- else:
- continue #不需要回答
-
- lined_cuted = ' '.join(cut(line))
- if not keywords_in_line(lined_cuted):
- lined_cuted = lined_cuted + '\t' + '__label__chat'
- num += 1
- file.write(lined_cuted + '\n')
- return num
- def process_at_hand(file):
- """
- json是JavaScript类型的格式,python提供json.load方法去加载该文件格式。
- 该文件打开后可以发现是一个字典,所有需要先拿到对应的keys
- """
- num = 0
- total_lines = json.loads(open(config.by_hand_path, encoding='utf-8').read())
- for key in total_lines.keys():
- for lines in tqdm(total_lines[key], desc='byhand'):
- for line in lines:
- if '校区' in line:
- lined_cuted = ' '.join(cut(line))
- lined_cuted = lined_cuted + '\t' + '__label__QA'
- num += 1
- file.write(lined_cuted + '\n')
- return num
- def process_crawled_data(file):
- """处理抓取的数据"""
- num = 0
- for line in tqdm(open(config.pachong_path, encoding='utf-8'), desc='crawled_data'):
- lined_cuted = ' '.join(cut(line))
- num += 1
- lined_cuted = lined_cuted + '\t' + '__label__QA'
- file.write(lined_cuted + '\n')
- return num
- def process():
- f = open(config.classify_corpus_path, 'a', encoding='utf-8')
- #处理小黄鸡
- num_chat = process_xiaohuangji(f)
- #处理手动构造数据
- num_q = process_at_hand(f)
- #处理爬虫数据
- num_q += process_crawled_data(f)
-
- f.close()
- print(num_chat, num_q)
-
然后在main函数中执行process()即可 :
构建一个新的python包,用于封装模型的创建和加载,并创建一个用于测试模型的py文件。
代码:
- import fasttext
- import config
-
- """
- 构建模型
- """
- def build_classify_model():
- #输入数据中只包含一个词语
- model = fasttext.train_supervised(config.classify_corpus_path, wordNgrams=1, epoch=20, minCount=5)
- model.save_model(config.classify_model_path)
- """
- 加载模型
- """
- def get_classify_model():
- model = fasttext.load_model(config.classify_model_path)
- return model
测试代码:
- """
- 用于测试构建的model
- """
-
- from classify.build_mode import build_classify_model,get_classify_model
-
- if __name__ == '__main__':
- build_classify_model()
完成模型的构建
实例测试一下:
- """
- 用于测试构建的model
- """
-
- from classify.build_mode import build_classify_model, get_classify_model
-
- if __name__ == '__main__':
- model = get_classify_model()
- text = ['您 吃 饭 了 吗', '今 天 天 气 非 常 好', 'python', 'python 好 学 么']
- print(model.predict(text))
-
- """
- ([['__label__chat'], ['__label__chat'], ['__label__QA'], ['__label__chat']], [array([1.00001], dtype=float32), array([1.0000087], dtype=float32), array([0.8048437], dtype=float32), array([1.0000099], dtype=float32)])
- """
模型认为第一个句子、第二个句子是是聊天的可能性比较大,第三个句子是问题的可能比较大,第四个句子可能是聊天的可能性比较大。但我们在项目中提到过,和编程语言有关的都是问题。为了更好的进行模型评估,我们可以将数据进行切分为训练集与数据集。
在config中准备两个路径,一个是测试集路径,一个是训练集路径。将build_classify_corpus的代码修改为:
用random的choice方法去从flags中随机挑选一个值,为0则为训练集,为1则为测试集。
- import json
-
- import pandas
- import config
- from lib.cut_sentence import cut
- from tqdm import tqdm#进度条库
- import random
- flags = [0, 0, 0, 0, 1] #五分之一的数据作为测试集,五分之四的数据作为训练集
- def keywords_in_line(line):
- """判断是否line中有符合要求的词"""
- keywords = ["传智播客", "传智", "黑马程序员", "黑马", "python",
- "人工智能", "c语言", "c++", "java", "javaee", "前端", "移动开发", "ui",
- "ue", "大数据", "软件测试", "php", "h5", "产品经理", "linux", "运维", "go语言",
- "区块链", "影视制作", "pmp", "项目管理", "新媒体", "小程序", "前端"]
- for keyword in keywords:
- if keyword in line:
- return True
- return False
- def process_xiaohuangji(f_train, f_test):
- """处理小黄鸡的语料,小黄鸡的语料分为E开头和M开头。M开头有两个,M1为问答、M2为回答"""
- num_train = 0
- num_test = 0
- for line in tqdm(open(config.xiao_huang_ji_path, encoding='utf-8').readlines(), desc='xiaohuangji'): #读取数据
- if line.startswith('E'):
- flag = 0
- continue
- elif line.startswith('M'):
- if flag == 0: #第一个M出现
- line = line[1:].strip()
- flag = 1
- else:
- continue #不需要回答
-
- lined_cuted = ' '.join(cut(line))
- if not keywords_in_line(lined_cuted):
- lined_cuted = lined_cuted + '\t' + '__label__chat'
- if random.choice(flags) == 0: #随机从列表中选择一个值
- f_train.write(lined_cuted + '\n')
- num_train += 1
- else:
- f_test.write(lined_cuted + '\n')
- num_test += 1
- return num_train, num_test
- def process_at_hand(f_train, f_test):
- """
- json是JavaScript类型的格式,python提供json.load方法去加载该文件格式。
- 该文件打开后可以发现是一个字典,所有需要先拿到对应的keys
- """
- num_train = 0
- num_test = 0
- total_lines = json.loads(open(config.by_hand_path, encoding='utf-8').read())
- for key in total_lines.keys():
- for lines in tqdm(total_lines[key], desc='byhand'):
- for line in lines:
- if '校区' in line:
- lined_cuted = ' '.join(cut(line))
- lined_cuted = lined_cuted + '\t' + '__label__QA'
- if random.choice(flags) == 0: # 随机从列表中选择一个值
- f_train.write(lined_cuted + '\n')
- num_train += 1
- else:
- f_test.write(lined_cuted + '\n')
- num_test += 1
- return num_train, num_test
- def process_crawled_data(f_train, f_test):
- """处理抓取的数据"""
- num_train = 0
- num_test = 0
- for line in tqdm(open(config.pachong_path, encoding='utf-8'), desc='crawled_data'):
- lined_cuted = ' '.join(cut(line))
- lined_cuted = lined_cuted + '\t' + '__label__QA'
- if random.choice(flags) == 0: # 随机从列表中选择一个值
- f_train.write(lined_cuted + '\n')
- num_train += 1
- else:
- f_test.write(lined_cuted + '\n')
- num_test += 1
- return num_train, num_test
- def process():
- f_train = open(config.classify_corpus_train_path, 'a', encoding='utf-8')
- f_test = open(config.classify_corpus_test_path, 'a', encoding='utf-8')
- #处理小黄鸡
- num_chat_train, num_chat_test = process_xiaohuangji(f_train, f_test)
- #处理手动构造数据
- num_q_train, num_q_test = process_at_hand(f_train, f_test)
- #处理爬虫数据
- _a, _b = process_crawled_data(f_train, f_test)
- num_q_train += _a
- num_q_test += _b
- f_train.close()
- f_test.close()
- print(f'聊天语料训练集数量{num_chat_train}, 聊天语料测试集数量{num_chat_test}')
- print(f'问题语料训练集数量{num_q_train}, 问题语料测试集数量{num_q_test}')
调整完后,训练集约有17000词、测试集约有3000词。
将模型封装为一个类,并判断准确率:
- """
- 构造模型进行预测
- """
- import fasttext
- import config
- from lib import cut_sentence
-
-
- class Classify:
- def __init__(self):
- self.ft_word_model = fasttext.load_model(config.fasttext_word_model_path)
- self.ft_model = fasttext.load_model(config.fasttext_model_path)
-
- def is_qa(self,sentence_info):
- python_qs_list = [" ".join(sentence_info["cuted_sentence"])]
- result = self.ft_model.predict(python_qs_list)
-
- python_qs_list = [" ".join(cut_sentence.cut(sentence_info["sentence"],by_word=True))]
- words_result = self.ft_word_model.predict(python_qs_list)
-
- acc,word_acc = self.get_qa_prob(result,words_result)
- if acc>0.95 or word_acc>0.95:
- #是QA
- return True
- else:
- return False
-
- def get_qa_prob(self,result,words_result):
- label, acc, word_label, word_acc = zip(*result, *words_result)
- label = label[0]
- acc = acc[0]
- word_label = word_label[0]
- word_acc = word_acc[0]
- if label == "__label__chat":
- acc = 1 - acc
- if word_label == "__label__chat":
- word_acc = 1 - word_acc
- return acc,word_acc
只需要在config中添加 fasttext_word_model_path、fasttext_model_path的路径,然后再将对应模型进行训练即可。若需要评测按字分和按词分方法的准确性,只需要调用该类中的方法即可。
显然,fasttext给我们带来了便利,下面了解一下该模型的原理。
fasttext的架构仅有三层,输入层、隐含层、输出层。
输入层是对文本进行embedding之后的向量具有N-garm特征,隐藏层是对于输入数据进求和平均,输出层输出文档对应标签。
N-garm即一种词袋模型,一种统计词频的手段,相比于单纯统计单词出现次数的其他方法,N-garm还考虑前面出现的词语。主要方法为,第n个词的出现与前n-1个词相关。对于fasttext的输入层而言,不仅有我们自己输入的数据,还要经过N-garm处理的词语进行输入。
理解层次的softmax,先要理解哈夫曼树。哈夫曼树我们都比较熟悉了,这里主要用到哈夫曼编码,就是任意字符的编码都不是另外一个字符编码的前缀。通过哈夫曼编码,对于一个文本序列而言,此时最小带权路径长度就是报文的最短长度,保证报文总编码长度最小。
而层次的softmax,就是将哈夫曼树的对应权重设置为参数,通过向后传播不断更新该参数,损失函数认为对数似然函数。
从除当前label之外的其他label选择几个为负样本,作为出现负样本的概率添加到损失函数。借此提高训练速度、模拟真实场景的噪声情况,让模型的稳健性更强。
因为负数样本用Relu或leaky-Relu函数会将数值映射为0或者接近为0得极大负数,使得在参数更新过程中,直接不用计算对应参数,大大减少了计算量。
4.8号更新线.........................
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。