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

相关文章

苹果macOS 26 Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色

《苹果macOS26Tahoe主题功能大升级:可定制图标/高亮文本/文件夹颜色》在整体系统设计方面,macOS26采用了全新的玻璃质感视觉风格,应用于Dock栏、应用图标以及桌面小部件等多个界面... 科技媒体 MACRumors 昨日(6 月 13 日)发布博文,报道称在 macOS 26 Tahoe 中

Python并行处理实战之如何使用ProcessPoolExecutor加速计算

《Python并行处理实战之如何使用ProcessPoolExecutor加速计算》Python提供了多种并行处理的方式,其中concurrent.futures模块的ProcessPoolExecu... 目录简介完整代码示例代码解释1. 导入必要的模块2. 定义处理函数3. 主函数4. 生成数字列表5.

Python实现精准提取 PDF中的文本,表格与图片

《Python实现精准提取PDF中的文本,表格与图片》在实际的系统开发中,处理PDF文件不仅限于读取整页文本,还有提取文档中的表格数据,图片或特定区域的内容,下面我们来看看如何使用Python实... 目录安装 python 库提取 PDF 文本内容:获取整页文本与指定区域内容获取页面上的所有文本内容获取

Python主动抛出异常的各种用法和场景分析

《Python主动抛出异常的各种用法和场景分析》在Python中,我们不仅可以捕获和处理异常,还可以主动抛出异常,也就是以类的方式自定义错误的类型和提示信息,这在编程中非常有用,下面我将详细解释主动抛... 目录一、为什么要主动抛出异常?二、基本语法:raise关键字基本示例三、raise的多种用法1. 抛

MySQL 设置AUTO_INCREMENT 无效的问题解决

《MySQL设置AUTO_INCREMENT无效的问题解决》本文主要介绍了MySQL设置AUTO_INCREMENT无效的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参... 目录快速设置mysql的auto_increment参数一、修改 AUTO_INCREMENT 的值。

关于跨域无效的问题及解决(java后端方案)

《关于跨域无效的问题及解决(java后端方案)》:本文主要介绍关于跨域无效的问题及解决(java后端方案),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录通用后端跨域方法1、@CrossOrigin 注解2、springboot2.0 实现WebMvcConfig

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Go语言如何判断两张图片的相似度

《Go语言如何判断两张图片的相似度》这篇文章主要为大家详细介绍了Go语言如何中实现判断两张图片的相似度的两种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 在介绍技术细节前,我们先来看看图片对比在哪些场景下可以用得到:图片去重:自动删除重复图片,为存储空间"瘦身"。想象你是一个

Java死锁问题解决方案及示例详解

《Java死锁问题解决方案及示例详解》死锁是指两个或多个线程因争夺资源而相互等待,导致所有线程都无法继续执行的一种状态,本文给大家详细介绍了Java死锁问题解决方案详解及实践样例,需要的朋友可以参考下... 目录1、简述死锁的四个必要条件:2、死锁示例代码3、如何检测死锁?3.1 使用 jstack3.2