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

相关文章

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

PHP应用中处理限流和API节流的最佳实践

《PHP应用中处理限流和API节流的最佳实践》限流和API节流对于确保Web应用程序的可靠性、安全性和可扩展性至关重要,本文将详细介绍PHP应用中处理限流和API节流的最佳实践,下面就来和小编一起学习... 目录限流的重要性在 php 中实施限流的最佳实践使用集中式存储进行状态管理(如 Redis)采用滑动

Vue3绑定props默认值问题

《Vue3绑定props默认值问题》使用Vue3的defineProps配合TypeScript的interface定义props类型,并通过withDefaults设置默认值,使组件能安全访问传入的... 目录前言步骤步骤1:使用 defineProps 定义 Props步骤2:设置默认值总结前言使用T

Python实现精确小数计算的完全指南

《Python实现精确小数计算的完全指南》在金融计算、科学实验和工程领域,浮点数精度问题一直是开发者面临的重大挑战,本文将深入解析Python精确小数计算技术体系,感兴趣的小伙伴可以了解一下... 目录引言:小数精度问题的核心挑战一、浮点数精度问题分析1.1 浮点数精度陷阱1.2 浮点数误差来源二、基础解决

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

Web服务器-Nginx-高并发问题

《Web服务器-Nginx-高并发问题》Nginx通过事件驱动、I/O多路复用和异步非阻塞技术高效处理高并发,结合动静分离和限流策略,提升性能与稳定性... 目录前言一、架构1. 原生多进程架构2. 事件驱动模型3. IO多路复用4. 异步非阻塞 I/O5. Nginx高并发配置实战二、动静分离1. 职责2