当前位置:   article > 正文

NLP简单项目实战——ChatBOT(一)

NLP简单项目实战——ChatBOT(一)

一、项目准备阶段:

(一)什么是ChatBOT:

        ChatBOT即交流机器人,目前主要有三类:QABOT、TASKBOT、CHATBOT。本项目实战中主要实现两个简单的QABOT、CHATBOT。

        1.QABOT常见实现手段:

        QABOT的常见流程,给出一个问题,经过神经网络或者其他模型提取出关键字,然后根据关键字去搜寻答案的过程。主要分为信息检索与知识图谱,信息检索就是字面意思。知识图谱则是存储知识与知识之间的关系,将问答转换为查询语句,实现推理,也就是上面的提取关键词过程。主要实现方式:tfidf、SVM、朴素贝叶斯、RNN、CNN。

        2.CHATBOT的常见实现手段:

        信息检索加seq2seq方法。

(二)需求分析和流程介绍:

        需要实现一个聊天机器人,该机器人可以起到智能客服的作用。由于语料的限制,准备了相关的编程问题,回答什么是python、python有什么优势等问题。

        实现流程:        

        后面的QA机器人是问答模型内容,我们将在前面将基础功能进行完善,后续实现问答模型只用实现内部细节就好了。

        实际运作流程:

        接受用户的问题之后对问题进行基础的处理,然后对问题进行分类判断意图。如果用户希望咨询问题就调用问答模型,希望进行闲聊就调用闲聊模型。

        1.闲聊模型:

        使用seq2seq模型,该模型包括了对文本的embedding、编码层、attention机制的处理、解码层这几个内容。

        2.问答模型:

        使用召回和排序的机制进行实现,实现速度和准确率。

        具体细节:

        问题分析:对问题的基础性处理包括分词、词性的获取、词向量的获取。

        问题召回:通过机器学习的方法进行海选,找到相似问题的前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的环境成功,如图:

(四)语料准备:

        1.字典下载

        构建如下项目结构,其中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文件了。

        转换成功! 

         

         词典内容,我们不太需要拼音,所以需要简单处理下该文件。

        用这段代码处理一下该文件,

  1. import config
  2. if __name__ == '__main__':
  3. f = open('sogou_dict1.txt', 'w+')
  4. f1 = open(config.user_dict_path2, 'rb+') #user_dict_path2即转换文件对象的路径
  5. line = list(f1.readline().strip().split())
  6. line = [str(line[i], 'utf-8') for i in range(len(line))]
  7. while line:
  8. f.writelines(line[-1] + ' sgjsj')
  9. f.write('\n')
  10. line = list(f1.readline().strip().split())
  11. line = [str(line[i], 'utf-8') for i in range(len(line))]

        处理结果如下,词性应该是sgjsj,我最开始打错了,就是搜狗计算机的意思。:

        2.停用词:

        即分词后句子不重要的词语,常见停用词下载:https://github.com/goto456/stopwords

        3.问答对:

        利用学习课程整理好的问答对,把问题分成以下类型。

        概念问题、课程优势、语言优势等类型。

        kc指的是课程、jgmc表示机构名称。 

        4.字典展示:

(五)文本分词:

        定义一个lib包用于存放基本的方法,比如分词、停用词获取。

        分词api实现:cut_words负责实现中英文字符切分,cut负责判断条件,如果不用分为单个词,则使用jieba的lcut方法进行分词。

  1. """
  2. 分词
  3. """
  4. import logging
  5. import jieba
  6. import jieba.posseg as psg #posseg是lcut所在的库
  7. import config
  8. import re
  9. import string
  10. #加载词典
  11. jieba.load_userdict(config.user_dict_path)
  12. """
  13. 分词api
  14. """
  15. #关闭jiebalog输出即jieba的日志输出
  16. """
  17. logging优先级,从低到高
  18. DEBUG
  19. INFO
  20. WARNING
  21. ERROR
  22. CRITICAL
  23. """
  24. jieba.setLogLevel(logging.INFO) #logging用于记录程序运行的信息
  25. #单字分割,英文部分
  26. letters = string.ascii_lowercase #小写字母
  27. #单字分割 去除的标点
  28. filters= [",", "-", ".", " "]
  29. #停用词
  30. stopwords = set([i.strip() for i in open(config.stopwords_path,encoding='utf-8').readlines()])
  31. def cut_word(sentence):
  32. """实现中英文分词"""
  33. # 对中文按照字进行处理,对英文不分为字母
  34. sentence = re.sub("\s+", " ", sentence) #匹配空白,如空格、tab,将这些转换为单个空白符
  35. sentence = sentence.strip()
  36. result = []
  37. temp = ""
  38. for word in sentence:
  39. if word.lower() in letters:
  40. temp += word.lower()
  41. else:
  42. if temp != "": # 不是字母
  43. result.append(temp)
  44. temp = ""
  45. if word.strip() in filters: # 标点符号
  46. continue
  47. else: # 是单个字
  48. result.append(word)
  49. if temp != "": # 最后的temp中包含字母
  50. result.append(temp)
  51. return result
  52. def cut(sentence, by_words=False, use_stopwords=False, with_sg=False):
  53. """
  54. :param sentence: 传入参数
  55. :param by_words: 是否按照单个字进行分词
  56. :param use_stopwords:是否使用停用词
  57. :param with_sg:是否返回词性
  58. :return:
  59. """
  60. assert by_words != True or with_sg != True, "根据word切分时候无法返回词性"
  61. if by_words:
  62. return cut_word(sentence)
  63. else:
  64. ret = psg.lcut(sentence) #按照用户提供的词典进行分词
  65. #jieba获取词性直接.flag就好了
  66. if use_stopwords:
  67. ret = [(i.word, i.flag) for i in ret if i.word not in stopwords]
  68. if not with_sg:
  69. ret = [i.word for i in ret]
  70. 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__+目标值。

        1.语料准备:

        准备两个内容:问题文本以及闲聊文本。

        问题文本:采用课程提供的文本以及模板构造的文本(如果有需要私聊我即可)

        闲聊文本:使用小黄鸡语料

        下载地址:  https://github.com/fateleak/dgk_lost_conv/tree/master/results 

        准备好即可~

        创建如下目录:

         在config文件下添加如下路径:

        具体代码:

  1. import json
  2. import pandas
  3. import config
  4. from lib.cut_sentence import cut
  5. from tqdm import tqdm#进度条库
  6. def keywords_in_line(line):
  7. """判断是否line中有符合要求的词"""
  8. keywords = ["传智播客", "传智", "黑马程序员", "黑马", "python",
  9. "人工智能", "c语言", "c++", "java", "javaee", "前端", "移动开发", "ui",
  10. "ue", "大数据", "软件测试", "php", "h5", "产品经理", "linux", "运维", "go语言",
  11. "区块链", "影视制作", "pmp", "项目管理", "新媒体", "小程序", "前端"]
  12. for keyword in keywords:
  13. if keyword in line:
  14. return True
  15. return False
  16. def process_xiaohuangji(file):
  17. """处理小黄鸡的语料,小黄鸡的语料分为E开头和M开头。M开头有两个,M1为问答、M2为回答"""
  18. num = 0
  19. for line in tqdm(open(config.xiao_huang_ji_path, encoding='utf-8').readlines(), desc='xiaohuangji'): #读取数据
  20. if line.startswith('E'):
  21. flag = 0
  22. continue
  23. elif line.startswith('M'):
  24. if flag == 0: #第一个M出现
  25. line = line[1:].strip()
  26. flag = 1
  27. else:
  28. continue #不需要回答
  29. lined_cuted = ' '.join(cut(line))
  30. if not keywords_in_line(lined_cuted):
  31. lined_cuted = lined_cuted + '\t' + '__label__chat'
  32. num += 1
  33. file.write(lined_cuted + '\n')
  34. return num
  35. def process_at_hand(file):
  36. """
  37. json是JavaScript类型的格式,python提供json.load方法去加载该文件格式。
  38. 该文件打开后可以发现是一个字典,所有需要先拿到对应的keys
  39. """
  40. num = 0
  41. total_lines = json.loads(open(config.by_hand_path, encoding='utf-8').read())
  42. for key in total_lines.keys():
  43. for lines in tqdm(total_lines[key], desc='byhand'):
  44. for line in lines:
  45. if '校区' in line:
  46. lined_cuted = ' '.join(cut(line))
  47. lined_cuted = lined_cuted + '\t' + '__label__QA'
  48. num += 1
  49. file.write(lined_cuted + '\n')
  50. return num
  51. def process_crawled_data(file):
  52. """处理抓取的数据"""
  53. num = 0
  54. for line in tqdm(open(config.pachong_path, encoding='utf-8'), desc='crawled_data'):
  55. lined_cuted = ' '.join(cut(line))
  56. num += 1
  57. lined_cuted = lined_cuted + '\t' + '__label__QA'
  58. file.write(lined_cuted + '\n')
  59. return num
  60. def process():
  61. f = open(config.classify_corpus_path, 'a', encoding='utf-8')
  62. #处理小黄鸡
  63. num_chat = process_xiaohuangji(f)
  64. #处理手动构造数据
  65. num_q = process_at_hand(f)
  66. #处理爬虫数据
  67. num_q += process_crawled_data(f)
  68. f.close()
  69. print(num_chat, num_q)

        然后在main函数中执行process()即可 :

        2.分类模型构建(意图识别): 

        构建一个新的python包,用于封装模型的创建和加载,并创建一个用于测试模型的py文件。

        代码:

  1. import fasttext
  2. import config
  3. """
  4. 构建模型
  5. """
  6. def build_classify_model():
  7. #输入数据中只包含一个词语
  8. model = fasttext.train_supervised(config.classify_corpus_path, wordNgrams=1, epoch=20, minCount=5)
  9. model.save_model(config.classify_model_path)
  10. """
  11. 加载模型
  12. """
  13. def get_classify_model():
  14. model = fasttext.load_model(config.classify_model_path)
  15. return model

         测试代码:

  1. """
  2. 用于测试构建的model
  3. """
  4. from classify.build_mode import build_classify_model,get_classify_model
  5. if __name__ == '__main__':
  6. build_classify_model()

        完成模型的构建

         实例测试一下:

  1. """
  2. 用于测试构建的model
  3. """
  4. from classify.build_mode import build_classify_model, get_classify_model
  5. if __name__ == '__main__':
  6. model = get_classify_model()
  7. text = ['您 吃 饭 了 吗', '今 天 天 气 非 常 好', 'python', 'python 好 学 么']
  8. print(model.predict(text))
  9. """
  10. ([['__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)])
  11. """

        模型认为第一个句子、第二个句子是是聊天的可能性比较大,第三个句子是问题的可能比较大,第四个句子可能是聊天的可能性比较大。但我们在项目中提到过,和编程语言有关的都是问题。为了更好的进行模型评估,我们可以将数据进行切分为训练集与数据集。

        在config中准备两个路径,一个是测试集路径,一个是训练集路径。将build_classify_corpus的代码修改为:

        用random的choice方法去从flags中随机挑选一个值,为0则为训练集,为1则为测试集。

  1. import json
  2. import pandas
  3. import config
  4. from lib.cut_sentence import cut
  5. from tqdm import tqdm#进度条库
  6. import random
  7. flags = [0, 0, 0, 0, 1] #五分之一的数据作为测试集,五分之四的数据作为训练集
  8. def keywords_in_line(line):
  9. """判断是否line中有符合要求的词"""
  10. keywords = ["传智播客", "传智", "黑马程序员", "黑马", "python",
  11. "人工智能", "c语言", "c++", "java", "javaee", "前端", "移动开发", "ui",
  12. "ue", "大数据", "软件测试", "php", "h5", "产品经理", "linux", "运维", "go语言",
  13. "区块链", "影视制作", "pmp", "项目管理", "新媒体", "小程序", "前端"]
  14. for keyword in keywords:
  15. if keyword in line:
  16. return True
  17. return False
  18. def process_xiaohuangji(f_train, f_test):
  19. """处理小黄鸡的语料,小黄鸡的语料分为E开头和M开头。M开头有两个,M1为问答、M2为回答"""
  20. num_train = 0
  21. num_test = 0
  22. for line in tqdm(open(config.xiao_huang_ji_path, encoding='utf-8').readlines(), desc='xiaohuangji'): #读取数据
  23. if line.startswith('E'):
  24. flag = 0
  25. continue
  26. elif line.startswith('M'):
  27. if flag == 0: #第一个M出现
  28. line = line[1:].strip()
  29. flag = 1
  30. else:
  31. continue #不需要回答
  32. lined_cuted = ' '.join(cut(line))
  33. if not keywords_in_line(lined_cuted):
  34. lined_cuted = lined_cuted + '\t' + '__label__chat'
  35. if random.choice(flags) == 0: #随机从列表中选择一个值
  36. f_train.write(lined_cuted + '\n')
  37. num_train += 1
  38. else:
  39. f_test.write(lined_cuted + '\n')
  40. num_test += 1
  41. return num_train, num_test
  42. def process_at_hand(f_train, f_test):
  43. """
  44. json是JavaScript类型的格式,python提供json.load方法去加载该文件格式。
  45. 该文件打开后可以发现是一个字典,所有需要先拿到对应的keys
  46. """
  47. num_train = 0
  48. num_test = 0
  49. total_lines = json.loads(open(config.by_hand_path, encoding='utf-8').read())
  50. for key in total_lines.keys():
  51. for lines in tqdm(total_lines[key], desc='byhand'):
  52. for line in lines:
  53. if '校区' in line:
  54. lined_cuted = ' '.join(cut(line))
  55. lined_cuted = lined_cuted + '\t' + '__label__QA'
  56. if random.choice(flags) == 0: # 随机从列表中选择一个值
  57. f_train.write(lined_cuted + '\n')
  58. num_train += 1
  59. else:
  60. f_test.write(lined_cuted + '\n')
  61. num_test += 1
  62. return num_train, num_test
  63. def process_crawled_data(f_train, f_test):
  64. """处理抓取的数据"""
  65. num_train = 0
  66. num_test = 0
  67. for line in tqdm(open(config.pachong_path, encoding='utf-8'), desc='crawled_data'):
  68. lined_cuted = ' '.join(cut(line))
  69. lined_cuted = lined_cuted + '\t' + '__label__QA'
  70. if random.choice(flags) == 0: # 随机从列表中选择一个值
  71. f_train.write(lined_cuted + '\n')
  72. num_train += 1
  73. else:
  74. f_test.write(lined_cuted + '\n')
  75. num_test += 1
  76. return num_train, num_test
  77. def process():
  78. f_train = open(config.classify_corpus_train_path, 'a', encoding='utf-8')
  79. f_test = open(config.classify_corpus_test_path, 'a', encoding='utf-8')
  80. #处理小黄鸡
  81. num_chat_train, num_chat_test = process_xiaohuangji(f_train, f_test)
  82. #处理手动构造数据
  83. num_q_train, num_q_test = process_at_hand(f_train, f_test)
  84. #处理爬虫数据
  85. _a, _b = process_crawled_data(f_train, f_test)
  86. num_q_train += _a
  87. num_q_test += _b
  88. f_train.close()
  89. f_test.close()
  90. print(f'聊天语料训练集数量{num_chat_train}, 聊天语料测试集数量{num_chat_test}')
  91. print(f'问题语料训练集数量{num_q_train}, 问题语料测试集数量{num_q_test}')

        调整完后,训练集约有17000词、测试集约有3000词。

        将模型封装为一个类,并判断准确率:

  1. """
  2. 构造模型进行预测
  3. """
  4. import fasttext
  5. import config
  6. from lib import cut_sentence
  7. class Classify:
  8. def __init__(self):
  9. self.ft_word_model = fasttext.load_model(config.fasttext_word_model_path)
  10. self.ft_model = fasttext.load_model(config.fasttext_model_path)
  11. def is_qa(self,sentence_info):
  12. python_qs_list = [" ".join(sentence_info["cuted_sentence"])]
  13. result = self.ft_model.predict(python_qs_list)
  14. python_qs_list = [" ".join(cut_sentence.cut(sentence_info["sentence"],by_word=True))]
  15. words_result = self.ft_word_model.predict(python_qs_list)
  16. acc,word_acc = self.get_qa_prob(result,words_result)
  17. if acc>0.95 or word_acc>0.95:
  18. #是QA
  19. return True
  20. else:
  21. return False
  22. def get_qa_prob(self,result,words_result):
  23. label, acc, word_label, word_acc = zip(*result, *words_result)
  24. label = label[0]
  25. acc = acc[0]
  26. word_label = word_label[0]
  27. word_acc = word_acc[0]
  28. if label == "__label__chat":
  29. acc = 1 - acc
  30. if word_label == "__label__chat":
  31. word_acc = 1 - word_acc
  32. return acc,word_acc

        只需要在config中添加 fasttext_word_model_path、fasttext_model_path的路径,然后再将对应模型进行训练即可。若需要评测按字分和按词分方法的准确性,只需要调用该类中的方法即可。

        3.fasttext原理:

        3.1神经网络模型

        显然,fasttext给我们带来了便利,下面了解一下该模型的原理。

        fasttext的架构仅有三层,输入层、隐含层、输出层。

        3.2N-garm特点

        输入层是对文本进行embedding之后的向量具有N-garm特征,隐藏层是对于输入数据进求和平均,输出层输出文档对应标签。

        N-garm即一种词袋模型,一种统计词频的手段,相比于单纯统计单词出现次数的其他方法,N-garm还考虑前面出现的词语。主要方法为,第n个词的出现与前n-1个词相关。对于fasttext的输入层而言,不仅有我们自己输入的数据,还要经过N-garm处理的词语进行输入。

        3.3对传统softmax的优化方法-层次softmax

        理解层次的softmax,先要理解哈夫曼树。哈夫曼树我们都比较熟悉了,这里主要用到哈夫曼编码,就是任意字符的编码都不是另外一个字符编码的前缀。通过哈夫曼编码,对于一个文本序列而言,此时最小带权路径长度就是报文的最短长度,保证报文总编码长度最小。

        而层次的softmax,就是将哈夫曼树的对应权重设置为参数,通过向后传播不断更新该参数,损失函数认为对数似然函数。

        3.4negative sampling

        从除当前label之外的其他label选择几个为负样本,作为出现负样本的概率添加到损失函数。借此提高训练速度、模拟真实场景的噪声情况,让模型的稳健性更强。

        因为负数样本用Relu或leaky-Relu函数会将数值映射为0或者接近为0得极大负数,使得在参数更新过程中,直接不用计算对应参数,大大减少了计算量。

        


4.8号更新线.........................        

  

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/498109
推荐阅读
相关标签
  

闽ICP备14008679号