自然语言处理-应用场景-聊天机器人(三):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

相关文章

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

maven异常Invalid bound statement(not found)的问题解决

《maven异常Invalidboundstatement(notfound)的问题解决》本文详细介绍了Maven项目中常见的Invalidboundstatement异常及其解决方案,文中通过... 目录Maven异常:Invalid bound statement (not found) 详解问题描述可

idea粘贴空格时显示NBSP的问题及解决方案

《idea粘贴空格时显示NBSP的问题及解决方案》在IDEA中粘贴代码时出现大量空格占位符NBSP,可以通过取消勾选AdvancedSettings中的相应选项来解决... 目录1、背景介绍2、解决办法3、处理完成总结1、背景介绍python在idehttp://www.chinasem.cna粘贴代码,出

SpringBoot整合Kafka启动失败的常见错误问题总结(推荐)

《SpringBoot整合Kafka启动失败的常见错误问题总结(推荐)》本文总结了SpringBoot项目整合Kafka启动失败的常见错误,包括Kafka服务器连接问题、序列化配置错误、依赖配置问题、... 目录一、Kafka服务器连接问题1. Kafka服务器无法连接2. 开发环境与生产环境网络不通二、序

SpringSecurity中的跨域问题处理方案

《SpringSecurity中的跨域问题处理方案》本文介绍了跨域资源共享(CORS)技术在JavaEE开发中的应用,详细讲解了CORS的工作原理,包括简单请求和非简单请求的处理方式,本文结合实例代码... 目录1.什么是CORS2.简单请求3.非简单请求4.Spring跨域解决方案4.1.@CrossOr

SQL 注入攻击(SQL Injection)原理、利用方式与防御策略深度解析

《SQL注入攻击(SQLInjection)原理、利用方式与防御策略深度解析》本文将从SQL注入的基本原理、攻击方式、常见利用手法,到企业级防御方案进行全面讲解,以帮助开发者和安全人员更系统地理解... 目录一、前言二、SQL 注入攻击的基本概念三、SQL 注入常见类型分析1. 基于错误回显的注入(Erro

nacos服务无法注册到nacos服务中心问题及解决

《nacos服务无法注册到nacos服务中心问题及解决》本文详细描述了在Linux服务器上使用Tomcat启动Java程序时,服务无法注册到Nacos的排查过程,通过一系列排查步骤,发现问题出在Tom... 目录简介依赖异常情况排查断点调试原因解决NacosRegisterOnWar结果总结简介1、程序在

Nginx内置变量应用场景分析

《Nginx内置变量应用场景分析》Nginx内置变量速查表,涵盖请求URI、客户端信息、服务器信息、文件路径、响应与性能等类别,这篇文章给大家介绍Nginx内置变量应用场景分析,感兴趣的朋友跟随小编一... 目录1. Nginx 内置变量速查表2. 核心变量详解与应用场景3. 实际应用举例4. 注意事项Ng

解决java.util.RandomAccessSubList cannot be cast to java.util.ArrayList错误的问题

《解决java.util.RandomAccessSubListcannotbecasttojava.util.ArrayList错误的问题》当你尝试将RandomAccessSubList... 目录Java.util.RandomAccessSubList cannot be cast to java.

Apache服务器IP自动跳转域名的问题及解决方案

《Apache服务器IP自动跳转域名的问题及解决方案》本教程将详细介绍如何通过Apache虚拟主机配置实现这一功能,并解决常见问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录​​问题背景​​解决方案​​方法 1:修改 httpd-vhosts.conf(推荐)​​步骤