本文主要是介绍自然语言处理-应用场景-聊天机器人(三):MaLSTM【基于FAQ 的问答系统】【文本向量化-->问题召回(利用PySparNN句子相似度计算海选相似问题)-->问题排序(深度学习:句子相似度计算)】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、问答机器人介绍
1. 问答机器人
在前面的课程中,我们已经对问答机器人介绍过,这里的问答机器人是我们在分类之后,对特定问题进行回答的一种机器人。至于回答的问题的类型,取决于我们的语料。
当前我们需要实现的问答机器人是一个回答编程语言(比如python是什么
,python难么
等)相关问题的机器人
2. 问答机器人的实现逻辑
主要实现逻辑:从现有的问答对中,选择出和问题最相似的问题,并且获取其相似度(一个数值),如果相似度大于阈值,则返回这个最相似的问题对应的答案
问答机器人的实现可以大致分为三步步骤:
- 对问题的处理
- 对相似问题进行的机器学习召回
- 对召回的结果进行深度学习排序
2.1 对问题的处理
对问题的处理过程中,我们可以考虑以下问题:
- 基础处理【清理】:对问题进行基础的清洗,去除特殊符号等
- 主体识别【用主语来过滤结果】:问题主语的识别,判断问题中是否包含特定的主语,比如
python
等,提取出来之后,方便后续对问题进行过滤。- 可以看出,不仅需要对用户输入的问题进行处理,获取主语,还需要对现有问答对进行处理
- 获取问题句子的词向量表示【计算相似度】:可以考虑使用词频,tf-idf等值,方便召回的时候使用
2.2 问题的召回【使用机器学习算法】
召回:可以理解为是一个海选的操作,就是从现有的问答对中选择可能相似的前K个问题。
为什么要进行召回?
主要目的是为了后续进行排序的时候,减少需要计算的数据量,比如有10万个问答对,直接通过深度学习肯定是可以获取所有的相似度,但是速度慢。
所以考虑使用机器学习的方法进行一次海选。
那么,如何实现召回呢?
前面我们介绍,召回就是选择前K个最相似的问题,所以召回的实现就是想办法通过机器学习的手段计算器相似度。
可以思考的方法:
- 使用词袋模型,获取词频矩阵,计算相似度
- 使用tfidf,获取tfidf的矩阵,计算相似度
上述的方法理论上都可行,但是当候选计算的词语数量太多的时候,需要挨个计算相似度,非常耗时。
所以可以考虑以下两点:
- 通过前面获取的主语,对问题进行过滤
- 使用聚类的方法,对数据先聚类,再计算某几个类别中的相似度,而不用去计算全部。
但是还有一个问题,供大家慢慢思考:
不管是词频,还是tfidf,获取的结果肯定是没有考虑文字顺序的,效果不一定是最好的,那么此时,应该如何让最后召回的效果更好呢?
2.3 问题的排序【使用深度学习模型来实现】
排序过程,使用了召回的结果作为输入,同时输出的是最相似的那一个。
整个过程使用深度学习实现。深度学习虽然训练的速度慢,但是整体效果肯定比机器学习好(机器学习受限于特征工程,数据量等因素,没有办法深入的学会不同问题之间的内在相似度),所以通过自建的深度学习神经网络模型,获取最后的相似度。
使用深度学习的神经网络模型这样一个黑匣子,在训练数据足够多的时候,能够学习到用户的各种不同输入的问题,当我们把目标值(相似的问题)给定的情况下,让模型自己去找到这些训练数据目标值和特征值之间相似的表示方法。
那么此时,有以下两个问题:
-
训练的数据的来源:使用什么数据,来训练模型,最后返回模型的相似度
- 可以考虑根据现有的问答对去手动构造,但是构造的数据不一定能够覆盖后续用户提问的全部问题。
- 所以可以考虑通过程序去采集网站上相似的问题,比如百度知道的搜索结果。
-
模型该如何构建
模型可以有两个输入,输出为一个数值,两个输入的处理方法肯定是一样的。这种网络结构我们经常把它称作孪生神经网络。
很明显,我们队输入的数据需要进行编码的操作,比如 word embedding + LSTM/BiLSTM/GRU/BiGRU等
两个编码之后的结果,我们可以进行组合,然后通过一个多层的神经网络,输出一个数字,把这个数值定义为我们的相似度。
当然我们的深层的神经网络在最开始的时候也并不是计算的相似度,但是我们的训练数据的目标值是相似度,在N多次的训练之后,确定了输入和输出的表示方法之后,那么最后的模型输出就是相似度了。
前面我们介绍了问答机器人的实现的大致思路,那么接下来,我们就来一步步的实现它
二、问答机器人的召回(海选与用户输入的问题相似的已有问题)
1. 召回的流程
流程如下:
- 准备数据,问答对的数据等
- 问题转化为向量
- 计算相似度
2. 对现有问答对的准备
这里说的问答对,是带有标准答案的问题,后续命中问答对中的问题后,会返回该问题对应的答案
为了后续使用方便,我们可以把现有问答对的处理成如下的格式,可以考虑存入数据库或者本地文件。如果数据量非常大,可以考虑放入Redis缓存数据库中:
{"问题1":{"主体":["主体1","主体3","主体3"..],"问题1分词后的句子":["word1","word2","word3"...],"答案":"答案"},"问题2":{...}
}
代码如下:
"""
处理召回的语料
"""
import json
from utils import cut
from tqdm import tqdmdef get_q_info(q):cut_by_word = cut(q,by_word=True) #单个字分词cut_temp = cut(q,use_seg=True) #entity = [i[0] for i in cut_temp if i[-1]=="kc"] #主体_cut = [i[0] for i in cut_temp] #分词return cut_by_word,_cut,entitydef get_qa_dict():fq = open("./corpus/recall/Q.txt").readlines()fa = open("./corpus/recall/A.txt").readlines()qa_dict = {}for q,a in tqdm(zip(fq,fa),total=len(fq)):q,a = q.strip(),a.strip()qa_dict[q] = {}cut_by_word, cut, entity = get_q_info(q)qa_dict[q]["cut"] = cutqa_dict[q]['cut_by_word'] = cut_by_wordqa_dict[q]["entity"] = entityqa_dict[q]["ans"] = a# print(qa_dict)with open("./corpus/recall/qa_dict.json","w") as f:f.write(json.dumps(qa_dict,ensure_ascii=False,indent=2))# json.dump(qa_dict,f)
3. 把问题转化为向量
把问答对中的问题,和用户输出的问题,转化为向量,为后续计算相似度做准备。
这里,我们使用tfidf对问答对中的问题进行处理,转化为向量矩阵。
TODO,使用单字,使用n-garm,使用BM25,使用word2vec等,让其结果更加准确
from sklearn.feature_extraction.text import TfidfVectorizer
from lib import QA_dictdef build_q_vectors():"""对问题建立索引"""lines_cuted= [q["q_cuted"] for q in QA_dict]tfidf_vectorizer = TfidfVectorizer()features_vec = tfidf_vectorizer.fit_transform(lines_cuted)#返回tfidf_vectorizer,后续还需要对用户输入的问题进行同样的处理return tfidf_vectorizer,features_vec,lines_cuted
4. 计算相似度
思路很简单。对用户输入的问题使用tfidf_vectorizer
进行处理,然后和features_vec
中的每一个结果进行计算,获取相似度。
但是由于耗时可能会很久,所以考虑使用其他方法来实现
4.1 pysparnn
的介绍
官方地址:https://github.com/facebookresearch/pysparnn
pysparnn
是一个对sparse数据进行相似邻近搜索的python库,这个库是用来实现 高维空间中寻找最相似的数据的。
注意:当数据不稀疏的时候,faiss和annoy比较合适。但是,当数据维度较高,且为稀疏数据的时候,应该考虑使用PySparNN。
4.2 PySparNN
的使用方法
pysparnn的使用非常简单,仅仅需要以下步骤,就能够完成从高维空间中寻找相似数据的结果
- 准备源数据和待搜索数据
- 对源数据进行向量化,把向量结果和源数据构造搜索的索引
- 对待搜索的数据向量化,传入索引,获取结果
import pysparnn.cluster_index as cifrom sklearn.feature_extraction.text import TfidfVectorizer# 1. 原始数据
data = ['hello world','oh hello there','Play it','Play it again Sam',
]# 2. 原始数据向量化
tv = TfidfVectorizer()
tv.fit(data)features_vec = tv.transform(data)print('type(features_vec) = {0}; \nfeatures_vec = \n{1}'.format(type(features_vec), features_vec))# 原始数据构造搜索索引
cp = ci.MultiClusterIndex(features_vec, data)# 待搜索的数据
search_data = ['oh there','Play it again Frank'
]search_features_vec = tv.transform(search_data) # 将待搜索的问题向量化print('type(search_features_vec) = {0}; \nsearch_features_vec = \n{1}'.format(type(search_features_vec), search_features_vec))# 3. 索引中传入带搜索数据,返回结果【计算相似度】
result = cp.search(search_features_vec, k=1, k_clusters=10, return_distance=False)print('result = ', result)
打印结果:
type(features_vec) = <class 'scipy.sparse.csr.csr_matrix'>;
features_vec = (0, 7) 0.7852882757103967(0, 1) 0.6191302964899972(1, 6) 0.6176143709756019(1, 3) 0.6176143709756019(1, 1) 0.48693426407352264(2, 4) 0.7071067811865475(2, 2) 0.7071067811865475(3, 5) 0.5552826649411127(3, 4) 0.43779123108611473(3, 2) 0.43779123108611473(3, 0) 0.5552826649411127
type(search_features_vec) = <class 'scipy.sparse.csr.csr_matrix'>;
search_features_vec = (0, 6) 0.7071067811865476(0, 3) 0.7071067811865476(1, 4) 0.5264054336099155(1, 2) 0.5264054336099155(1, 0) 0.6676785446095399
result = [['oh hello there'], ['Play it again Sam']]Process finished with exit code 0
使用注意点:
- 构造索引是需要传入向量和原数据,最终的结果会返回源数据
- 传入待搜索的数据时,需要传入一下几个参数:
search_features_vec
:搜索的句子的向量k
:最大的几个结果,k=1,返回最大的一个k_clusters
:对数据分为多少类进行搜索return_distance
:是否返回距离
4.3 使用PySparNN完成召回的过程
#构造索引
cp = ci.MultiClusterIndex(features_vec, lines_cuted)#对用户输入的句子进行向量化
search_vec = tfidf_vec.transform(ret)
#搜索获取结果,返回最大的8个数据,之后根据`main_entiry`进行过滤结果
cp_search_list = cp.search(search_vec, k=8, k_clusters=10, return_distance=True)exist_same_entiry = False
search_lsit = []
for _temp_call_line in cp_search_list[0]:cur_entity = QA_dict[_temp_call_line[1]]["main_entity"]if len(set(main_entity) & set(cur_entity))>0: #命名体的集合存在交集的时候返回exist_same_entiry = Truesearch_lsit.append(_temp_call_line[1])if exist_same_entiry: #存在相同的主体的时候return search_lsit
else:# print(cp_search_list)return [i[1] for i in cp_search_list[0]]
在这个过程中,需要注意,提前把cp,tfidf_vec
等内容提前准备好,而不应该在每次接收到用户的问题之后重新生成一遍,否则效率会很低
4.4 PySparNN
的原理介绍
参考地址:https://nlp.stanford.edu/IR-book/html/htmledition/cluster-pruning-1.html
pysparnn
使用的是一种cluster pruning(簇修剪)
的技术,即,开始的时候对数据进行聚类,后续再有限个类别中进行数据的搜索,根据计算的余弦相似度返回结果。
数据预处理过程如下:
- 随机选择 N \sqrt{N} N个样本作为leader, N N N 为样本总数量;
- 选择非leader的数据(follower),使用余弦相似度计算找到最近的leader
当获取到一个问题Query的时候,查询过程:
- 计算每个leader和Query的相似度,找到最相似的leader
- 然后计算问题Query和leader所在簇的相似度,找到最相似的k个,作为最终的返回结果
在上述的过程中,可以设置两个大于0的数字b1和b2
- b1表示在
数据预处理
阶段,每个follower选择b1个最相似的leader,而不是选择单独一个lader,这样不同的簇是有数据交叉的; - b2表示在查询阶段,找到最相似的b2个leader,然后再计算不同的leader中下的topk的结果
前面的描述就是b1=b2=1的情况,通过增加b1和b2
的值,我们能够有更大的机会找到更好的结果,但是这样会需要更加大量的计算。
在PySparNN中实例化索引的过程中
即:ci.MultiClusterIndex(features, records_data, num_indexes)
中,num_indexes 能够设置b1的值,默认为2。
在搜索的过程中,cp.search(search_vec, k=8, k_clusters=10, return_distance=True,num_indexes)
,num_Indexes 可以设置b2的值,默认等于b1的值。
5、使用ElasticSearch进行召回(代替pysparnn方案)
根据用户输入分词后的各个词汇,直接查询ES系统中包含各个分词索引的所有记录作为初步召回的结果
三、召回过程优化
1. 优化思路
前面的学习,我们能够返回相似的召回结果,但是,如何让这些结果更加准确呢?
我们可以从下面的角度出发:
- tfidf使用的是词频和整个文档的词语,如果用户问题的某个词语没有出现过,那么此时,计算出来的相似度可能就不准确。该问题的解决思路:
- 对用户输入的问题进行文本的对齐,比如,使用训练好的word2vector,往句子中填充非主语的其他词语的相似词语。例如
python 好学 么 -->填充后是 :python 好学 么 简单 难 嘛
,这里假设word2vector同学会了好学,简单,难
他们之间是相似的 - 使用word2vector对齐的好处除了应对未出现的词语,还能够提高主语的重要程度,让主语位置的tfidf的值更大,从而让相似度更加准确
- 对用户输入的问题进行文本的对齐,比如,使用训练好的word2vector,往句子中填充非主语的其他词语的相似词语。例如
- tfidf是一个词袋模型,没有考虑词和词之间的顺序
- 使用n-garm和词一起作为特征,转化为特征向量
- 不去使用tfidf处理句子得到向量。
- 使用BM25算法
- 或者 使用fasttext、word2vector,把句子转化为向量
2. 优化方式一:通过BM25算法代替TFIDF
2.1 BM25算法原理
BM25(BM=best matching)是TDIDF的优化版本,首先我们来看看TFIDF是怎么计算的
t f i d f i = t f ∗ i d f = 词 i 的 数 量 词 语 总 数 ∗ l o g 总 文 档 数 包 含 词 i 的 文 档 数 tfidf_i = tf*idf = \cfrac{词i的数量}{词语总数}*log\cfrac{总文档数}{包含词i的文档数} tfidfi=tf∗idf=词语总数词i的数量∗log包含词i的文档数总文档数
其中tf称为词频,idf为逆文档频率
那么BM25是如何计算的呢?
B M 25 ( i ) = 词 i 的 数 量 总 词 数 ∗ ( k + 1 ) C C + k ( 1 − b + b ∣ d ∣ a v d l ) ∗ l o g ( 总 文 档 数 包 含 i 的 文 档 数 ) C = t f = 词 i 的 数 量 总 词 数 , k > 0 , b ∈ [ 0 , 1 ] , d 为 文 档 i 的 长 度 , a v d l 是 文 档 平 均 长 度 BM25(i) = \cfrac{词i的数量}{总词数}*\cfrac{(k+1)C}{C+k(1-b+b\cfrac{|d|}{avdl})}*log(\cfrac{总文档数}{包含i的文档数}) \\ C = tf=\cfrac{词i的数量}{总词数},k>0,b\in [0,1],d为文档i的长度,avdl是文档平均长度 BM25(i)=总词数词i的数量∗C+k(1−b+b
这篇关于自然语言处理-应用场景-聊天机器人(三):MaLSTM【基于FAQ 的问答系统】【文本向量化-->问题召回(利用PySparNN句子相似度计算海选相似问题)-->问题排序(深度学习:句子相似度计算)】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!