自然语言处理-应用场景-聊天机器人(三):MaLSTM【基于FAQ 的问答系统】【文本向量化-->问题召回(利用PySparNN句子相似度计算海选相似问题)-->问题排序(深度学习:句子相似度计算)】

本文主要是介绍自然语言处理-应用场景-聊天机器人(三):MaLSTM【基于FAQ 的问答系统】【文本向量化-->问题召回(利用PySparNN句子相似度计算海选相似问题)-->问题排序(深度学习:句子相似度计算)】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、问答机器人介绍

1. 问答机器人

在前面的课程中,我们已经对问答机器人介绍过,这里的问答机器人是我们在分类之后,对特定问题进行回答的一种机器人。至于回答的问题的类型,取决于我们的语料。

当前我们需要实现的问答机器人是一个回答编程语言(比如python是什么python难么等)相关问题的机器人

2. 问答机器人的实现逻辑

在这里插入图片描述

主要实现逻辑:从现有的问答对中,选择出和问题最相似的问题,并且获取其相似度(一个数值),如果相似度大于阈值,则返回这个最相似的问题对应的答案

问答机器人的实现可以大致分为三步步骤:

  1. 对问题的处理
  2. 对相似问题进行的机器学习召回
  3. 对召回的结果进行深度学习排序

2.1 对问题的处理

对问题的处理过程中,我们可以考虑以下问题:

  1. 基础处理【清理】:对问题进行基础的清洗,去除特殊符号等
  2. 主体识别【用主语来过滤结果】:问题主语的识别,判断问题中是否包含特定的主语,比如python等,提取出来之后,方便后续对问题进行过滤。
    • 可以看出,不仅需要对用户输入的问题进行处理,获取主语,还需要对现有问答对进行处理
  3. 获取问题句子的词向量表示【计算相似度】:可以考虑使用词频,tf-idf等值,方便召回的时候使用

2.2 问题的召回【使用机器学习算法】

召回:可以理解为是一个海选的操作,就是从现有的问答对中选择可能相似的前K个问题。

为什么要进行召回?

主要目的是为了后续进行排序的时候,减少需要计算的数据量,比如有10万个问答对,直接通过深度学习肯定是可以获取所有的相似度,但是速度慢。
所以考虑使用机器学习的方法进行一次海选。

那么,如何实现召回呢?

前面我们介绍,召回就是选择前K个最相似的问题,所以召回的实现就是想办法通过机器学习的手段计算器相似度。

可以思考的方法:

  1. 使用词袋模型,获取词频矩阵,计算相似度
  2. 使用tfidf,获取tfidf的矩阵,计算相似度

上述的方法理论上都可行,但是当候选计算的词语数量太多的时候,需要挨个计算相似度,非常耗时。

所以可以考虑以下两点:

  1. 通过前面获取的主语,对问题进行过滤
  2. 使用聚类的方法,对数据先聚类,再计算某几个类别中的相似度,而不用去计算全部。

但是还有一个问题,供大家慢慢思考:

不管是词频,还是tfidf,获取的结果肯定是没有考虑文字顺序的,效果不一定是最好的,那么此时,应该如何让最后召回的效果更好呢?

2.3 问题的排序【使用深度学习模型来实现】

排序过程,使用了召回的结果作为输入,同时输出的是最相似的那一个。

整个过程使用深度学习实现。深度学习虽然训练的速度慢,但是整体效果肯定比机器学习好(机器学习受限于特征工程,数据量等因素,没有办法深入的学会不同问题之间的内在相似度),所以通过自建的深度学习神经网络模型,获取最后的相似度。

使用深度学习的神经网络模型这样一个黑匣子,在训练数据足够多的时候,能够学习到用户的各种不同输入的问题,当我们把目标值(相似的问题)给定的情况下,让模型自己去找到这些训练数据目标值和特征值之间相似的表示方法。

那么此时,有以下两个问题:

  1. 训练的数据的来源:使用什么数据,来训练模型,最后返回模型的相似度

    • 可以考虑根据现有的问答对去手动构造,但是构造的数据不一定能够覆盖后续用户提问的全部问题。
    • 所以可以考虑通过程序去采集网站上相似的问题,比如百度知道的搜索结果。
  2. 模型该如何构建

    模型可以有两个输入,输出为一个数值,两个输入的处理方法肯定是一样的。这种网络结构我们经常把它称作孪生神经网络

    很明显,我们队输入的数据需要进行编码的操作,比如 word embedding + LSTM/BiLSTM/GRU/BiGRU等

    两个编码之后的结果,我们可以进行组合,然后通过一个多层的神经网络,输出一个数字,把这个数值定义为我们的相似度。

    当然我们的深层的神经网络在最开始的时候也并不是计算的相似度,但是我们的训练数据的目标值是相似度,在N多次的训练之后,确定了输入和输出的表示方法之后,那么最后的模型输出就是相似度了。

前面我们介绍了问答机器人的实现的大致思路,那么接下来,我们就来一步步的实现它

二、问答机器人的召回(海选与用户输入的问题相似的已有问题)

1. 召回的流程

流程如下:

  1. 准备数据,问答对的数据等
  2. 问题转化为向量
  3. 计算相似度

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的使用非常简单,仅仅需要以下步骤,就能够完成从高维空间中寻找相似数据的结果

  1. 准备源数据和待搜索数据
  2. 对源数据进行向量化,把向量结果和源数据构造搜索的索引
  3. 对待搜索的数据向量化,传入索引,获取结果
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

使用注意点:

  1. 构造索引是需要传入向量和原数据,最终的结果会返回源数据
  2. 传入待搜索的数据时,需要传入一下几个参数:
    1. search_features_vec:搜索的句子的向量
    2. k:最大的几个结果,k=1,返回最大的一个
    3. k_clusters:对数据分为多少类进行搜索
    4. 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(簇修剪)的技术,即,开始的时候对数据进行聚类,后续再有限个类别中进行数据的搜索,根据计算的余弦相似度返回结果。

数据预处理过程如下:

  1. 随机选择 N \sqrt{N} N 个样本作为leader, N N N 为样本总数量;
  2. 选择非leader的数据(follower),使用余弦相似度计算找到最近的leader

当获取到一个问题Query的时候,查询过程:

  1. 计算每个leader和Query的相似度,找到最相似的leader
  2. 然后计算问题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. 优化思路

前面的学习,我们能够返回相似的召回结果,但是,如何让这些结果更加准确呢?

我们可以从下面的角度出发:

  1. tfidf使用的是词频和整个文档的词语,如果用户问题的某个词语没有出现过,那么此时,计算出来的相似度可能就不准确。该问题的解决思路:
    • 对用户输入的问题进行文本的对齐,比如,使用训练好的word2vector,往句子中填充非主语的其他词语的相似词语。例如python 好学 么 -->填充后是 :python 好学 么 简单 难 嘛,这里假设word2vector同学会了好学,简单,难他们之间是相似的
    • 使用word2vector对齐的好处除了应对未出现的词语,还能够提高主语的重要程度,让主语位置的tfidf的值更大,从而让相似度更加准确
  2. tfidf是一个词袋模型,没有考虑词和词之间的顺序
    • 使用n-garm和词一起作为特征,转化为特征向量
  3. 不去使用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=tfidf=ilogi
其中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)=iC+k(1b+b

这篇关于自然语言处理-应用场景-聊天机器人(三):MaLSTM【基于FAQ 的问答系统】【文本向量化-->问题召回(利用PySparNN句子相似度计算海选相似问题)-->问题排序(深度学习:句子相似度计算)】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128867

相关文章

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

Python 中的异步与同步深度解析(实践记录)

《Python中的异步与同步深度解析(实践记录)》在Python编程世界里,异步和同步的概念是理解程序执行流程和性能优化的关键,这篇文章将带你深入了解它们的差异,以及阻塞和非阻塞的特性,同时通过实际... 目录python中的异步与同步:深度解析与实践异步与同步的定义异步同步阻塞与非阻塞的概念阻塞非阻塞同步

Python Dash框架在数据可视化仪表板中的应用与实践记录

《PythonDash框架在数据可视化仪表板中的应用与实践记录》Python的PlotlyDash库提供了一种简便且强大的方式来构建和展示互动式数据仪表板,本篇文章将深入探讨如何使用Dash设计一... 目录python Dash框架在数据可视化仪表板中的应用与实践1. 什么是Plotly Dash?1.1

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

使用Python实现文本转语音(TTS)并播放音频

《使用Python实现文本转语音(TTS)并播放音频》在开发涉及语音交互或需要语音提示的应用时,文本转语音(TTS)技术是一个非常实用的工具,下面我们来看看如何使用gTTS和playsound库将文本... 目录什么是 gTTS 和 playsound安装依赖库实现步骤 1. 导入库2. 定义文本和语言 3

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

Java中&和&&以及|和||的区别、应用场景和代码示例

《Java中&和&&以及|和||的区别、应用场景和代码示例》:本文主要介绍Java中的逻辑运算符&、&&、|和||的区别,包括它们在布尔和整数类型上的应用,文中通过代码介绍的非常详细,需要的朋友可... 目录前言1. & 和 &&代码示例2. | 和 ||代码示例3. 为什么要使用 & 和 | 而不是总是使