# 问答系统 **Repository Path**: dot9527/question-answering-system ## Basic Information - **Project Name**: 问答系统 - **Description**: 搭建一个简单的问答系统 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2025-12-16 - **Last Updated**: 2025-12-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 搭建一个简单的问答系统 *** glove.6B.200d.txt这个文件链接:https://www.kaggle.com/incorpes/glove6b200d 下载的时候需要翻墙 *** ## 一、问答系统任务介绍 ### 1. 模块介绍   问答系统所需要的数据已经提供,对于每一个问题都可以找得到相应的答案,所以可以理解为每一个样本数据是 ``<问题、答案>``。 那系统的核心是当用户输入一个问题的时候,首先要找到跟这个问题最相近的已经存储在库里的问题,然后直接返回相应的答案即可(但实际上也可以抽取其中的实体或者关键词)。 举一个简单的例子: 假设我们的库里面已有存在以下几个<问题,答案>: - <"贪心学院主要做什么方面的业务?”, “他们主要做人工智能方面的教育”> - <“国内有哪些做人工智能教育的公司?”, “贪心学院”> - <"人工智能和机器学习的关系什么?", "其实机器学习是人工智能的一个范畴,很多人工智能的应用要基于机器学习的技术"> - <"人工智能最核心的语言是什么?", ”Python“> 假设一个用户往系统中输入了问题 “贪心学院是做什么的?”, 那这时候系统先去匹配最相近的“已经存在库里的”问题。 那在这里很显然是 “贪心学院是做什么的”和“贪心学院主要做什么方面的业务?”是最相近的。 所以当我们定位到这个问题之后,直接返回它的答案 “他们主要做人工智能方面的教育”就可以了。 所以这里的核心问题可以归结为**计算两个问句(query)之间的相似度**。 ### 2. 数据介绍   问答系统项目涉及的模块包括: - 文本的读取: 需要从相应的文件里读取```(问题,答案)``` - 文本预处理: 清洗文本很重要,需要涉及到```停用词过滤```等工作 - 文本的表示: 如果表示一个句子是非常核心的问题,这里会涉及到```tf-idf```, ```Glove```以及```BERT Embedding``` - 文本相似度匹配: 在基于检索式系统中一个核心的部分是计算文本之间的```相似度```,从而选择相似度最高的问题然后返回这些问题的答案 - 倒排表: 为了加速搜索速度,我们需要设计```倒排表```来存储每一个词与出现的文本 - 词义匹配:直接使用倒排表会忽略到一些意思上相近但不完全一样的单词,我们需要做这部分的处理。我们需要提前构建好```相似的单词```然后搜索阶段使用 - 拼写纠错:我们不能保证用户输入的准确,所以第一步需要做用户输入检查,如果发现用户拼错了,我们需要及时在后台改正,然后按照修改后的在库里面搜索 - 文档的排序: 最后返回结果的排序根据文档之间```余弦相似度```有关,同时也跟倒排表中匹配的单词有关 ### 3. 项目工具介绍   在本次项目中,你将会用到以下几个工具: - ```sklearn```。具体安装请见:http://scikit-learn.org/stable/install.html sklearn包含了各类机器学习算法和数据处理工具,包括本项目需要使用的词袋模型,均可以在sklearn工具包中找得到。 - ```jieba```,用来做分词。具体使用方法请见 https://github.com/fxsjy/jieba - ```bert embedding```: https://github.com/imgarylai/bert-embedding - ```nltk```:https://www.nltk.org/index.html ## 二、搭建问答系统 ### 1. 文本读取   需要从文本中读取数据,此处需要读取的文件是```train-v2.0.json```,并把读取的文件存入一个列表里(list) ### 2. 可视化分析   对于给定的样本数据,做一些可视化分析来更好地理解数据 ### 3. 文本预处理   对于问题本身需要做一些停用词过滤等文本方面的处理 #### 3.1 无用符号过滤   去掉一些无用的符号: 比如连续的感叹号!!!, 或者一些奇怪的单词。 #### 3.2 停用词过滤   去网上搜一下 “english stop words list”,会出现很多包含停用词库的网页,或者直接使用NLTK自带的 #### 3.3 去掉低频率的词   比如出现次数少于10,20… (合理选择阈值) #### 3.4 处理数字   对于数字的处理: 分词完只有有些单词可能就是数字比如44,415,把所有这些数字都看成是一个单词,这个新的单词我们可以定义为 “#number” ### 4.文本表示 #### 4.1 使用tf-idf表示向量   把```qlist```中的每一个问题的字符串转换成```tf-idf```向量, 转换之后的结果存储在```X```矩阵里。``X``的大小是: ``N* D``的矩阵。 这里``N``是问题的个数(样本个数),``D``是词典库的大小。 >词袋模型常用TF-IDF来计算权重,公式为: >$TF-IDF(t,d)=TF(t,d)×IDF(t)$ 其中TF(t,d)为单词t在文档d中出现的频率,IDF(t)是逆文档频率,用来衡量单词t对表达语义所起的重要性,表示为 $IDF(t)=log{[(文章总数)/(句含单词t的文章总数+1)]}$ 直观的解释是,如果一个单词在非常多的文章里面都出现,那么它可能是一个比较通用的词汇,对于区分某篇文章特殊语义的贡献较小,因此对权重做一定惩罚。 #### 4.2 使用wordvec + average pooling   wordvec本质上是通过前馈神经网络的训练参数。从表现上来看,wordvec表征能力比TF-IDF表征能力强很多。词向量方面需要下载: https://nlp.stanford.edu/projects/glove/ (请下载``glove.6B.zip``),并使用``d=200``的词向量(200维),我资源里面也传了一份。这样获得的词向量是已经训练好的词向量。 每个词向量获取完之后,即可以得到一个句子的向量。 我们通过``average pooling``来实现句子的向量。 #### 4.3 使用BERT + average pooling   BERT与wordvec非常相似,只不过BERT是基于Transformer的深层神经网络来训练得到向量的表示,学到的效果比浅层的神经网络效果好。我们不做任何的训练,而是直接使用已经训练好的BERT embedding. 为了获取BERT-embedding,可以直接下载已经训练好的模型,从而获得每一个单词的向量。可以从这里获取: https://github.com/imgarylai/bert-embedding ,请使用```bert_12_768_12``` 当然,你也可以从其他source获取也没问题,只要是合理的词向量。 ### 5.相似度匹配与搜索   在这部分里,我们需要把用户每一个输入跟知识库里的每一个问题做一个相似度计算,从而得出最相似的问题。但对于这个问题,时间复杂度其实很高,所以我们需要结合倒排表来获取相似度最高的问题,从而获得答案。下面介绍几种最常见的相似度匹配算法的设计。 #### 5.1 tf-idf + 余弦相似度   我们可以直接基于计算出来的``tf-idf``向量,计算用户最新问题与库中存储的问题之间的相似度,从而选择相似度最高的问题的答案。这个方法的复杂度为``O(N)``, ``N``是库中问题的个数。 #### 5.2 倒排索引表的创建   倒排表可以用于加快匹配的过程。**倒排表**的创建其实很简单,最简单的方法就是循环所有的单词一遍,然后记录每一个单词所出现的文档,然后把这些文档的ID保存成list即可。我们可以定义一个类似于```hash_map```, 比如 ``inverted_index = {}``, 然后存放包含每一个关键词的文档出现在了什么位置,也就是,通过关键词的搜索首先来判断包含这些关键词的文档(比如出现至少一个),然后对于candidates问题做相似度比较。 >倒排索引表基于一个强假设:两个句子包含的相同的词越多,两个句子越相似。实际上会漏掉一些句子,相同的词不多,但是句子相似。 #### 5.3 语义相似度   语义的相似度,可以这么理解: 两个单词比如car, auto这两个单词长得不一样,但从语义上还是类似的。如果只是使用倒排表,我们不能考虑到这些单词之间的相似度,这就导致如果我们搜索句子里包含了``car``, 则我们没法获取到包含auto的所有的文档。所以我们希望把这些信息也存下来。那这个问题如何解决呢? 其实也不难,可以提前构建好相似度的关系,比如对于``car``这个单词,一开始就找好跟它意思上比较类似的单词比如top 10,这些都标记为``related words``。所以最后我们就可以创建一个保存``related words``的一个``map``. 比如调用``related_words['car']``即可以调取出跟``car``意思上相近的TOP 10的单词。那这个``related_words``又如何构建呢? 在这里我们仍然使用``Glove``向量,然后计算一下俩俩的相似度(余弦相似度)。之后对于每一个词,存储跟它最相近的top 10单词,最终结果保存在``related_words``里面。 这个计算需要发生在离线,因为计算量很大,复杂度为``O(V*V)``, V是单词的总数。这个结果保存在``related_words.txt``里。 我们在使用的时候直接从文件里读取就可以了,不用再重复计算。 #### 5.4 利用倒排索引表进行搜索   在这里,我们使用倒排表先获得一批候选问题,然后再通过余弦相似度做精准匹配,这样一来可以节省大量的时间。搜索过程分成两步: 1. 使用倒排表把候选问题全部提取出来。首先,对输入的新问题做分词等必要的预处理工作,然后对于句子里的每一个单词,从``related_words``里提取出跟它意思相近的top 10单词, 然后根据这些top词从倒排表里提取相关的文档,把所有的文档返回。 这部分可以放在下面的函数当中,也可以放在外部。 2. 然后针对于这些文档做余弦相似度的计算,最后排序并选出最好的答案。 ### 6.拼写纠错   这一部分并不是文本匹配的后续部分,但在问答系统中是经常用到的,这是因为考虑到用户输入是不一定正确的。这个额外的技术手段用来保证query的正确性。其实用户在输入问题的时候,不能期待他一定会输入正确,有可能输入的单词的拼写错误的。这个时候我们需要后台及时捕获拼写错误,并进行纠正,然后再通过修正之后的结果再跟库里的问题做匹配。这里我们需要实现一个简单的拼写纠错的代码,然后自动去修复错误的单词。 这里使用的拼写纠错方法是使用`noisy channel model`。 我们回想一下它的表示: $$c^* = \text{argmax}_{c\in candidates} ~~p(c|s) = \text{argmax}_{c\in candidates} ~~p(s|c)p(c)$$ 这里的```candidates```指的是针对于错误的单词的候选集,这部分我们可以假定是通过`edit_distance`来获取的(比如生成跟当前的词距离为1/2的所有的valid 单词。 valid单词可以定义为存在词典里的单词。 ```c```代表的是正确的单词, ```s```代表的是用户错误拼写的单词。 所以我们的目的是要寻找出在``candidates``里让上述概率最大的正确写法``c``。 $p(s|c)$,这个概率我们可以通过历史数据来获得,也就是对于一个正确的单词$c$, 有百分之多少人把它写成了错误的形式1,形式2... 这部分的数据可以从``spell_errors.txt``里面找得到。但在这个文件里,我们并没有标记这个概率,所以可以使用`uniform probability`来表示。这个也叫做`channel probability`。 $p(c)$,这一项代表的是语言模型,也就是假如我们把错误的$s$,改造成了$c$, 把它加入到当前的语句之后有多通顺?在本次项目里我们使用bigram来评估这个概率。 >举个例子: 假如有两个候选 $c_1, c_2$, 然后我们希望分别计算出这个语言模型的概率。 由于我们使用的是``bigram``, 我们需要计算出两个概率,分别是当前词前面和后面词的``bigram``概率。 用一个例子来表示: 给定: ``We are go to school tomorrow``, 对于这句话我们希望把中间的``go``替换成正确的形式,假如候选集里有个,分别是``going``, ``went``, 这时候我们分别对这俩计算如下的概率:$p(going|are)p(to|going)$和 $p(went|are)p(to|went)$, 然后把这个概率当做是$p(c)$的概率。 然后再跟``channel probability``结合给出最终的概率大小。那这里的$p(are|going)$这些bigram概率又如何计算呢?答案是训练一个语言模型! 但训练一个语言模型需要一些文本数据,这个数据怎么找? 在这次项目里我们会用到``nltk``自带的``reuters``的文本类数据来训练一个语言模型。当然,如果你有资源你也可以尝试其他更大的数据。最终目的就是计算出``bigram``概率。 #### 6.1 训练一个语言模型   在这里,我们使用``nltk``自带的``reuters``数据来训练一个语言模型。 使用``add-one smoothing``。 #### 6.2 计算channel probability   这个概率我们可以通过历史数据来获得,也就是对于一个正确的单词$c$, 有百分之多少人把它写成了错误的形式1,形式2... 这部分的数据可以从``spell_errors.txt``里面找得到。但在这个文件里,我们并没有标记这个概率,所以可以使用`uniform probability`来表示。这个也叫做`channel probability`。下面基于``spell_errors.txt``文件构建``channel probability``, 其中$channel[c][s]$表示正确的单词$c$被写错成$s$的概率。 #### 6.3 根据错别字生成所有候选集合   给定一个错误的单词,首先生成跟这个单词距离为1或者2的所有的候选集合。 #### 6.4 给定一个输入,如果有错误给予纠正   给定一个输入``query``, 如果这里有些单词是拼错的,就需要把它纠正过来。这部分的实现可以简单一点: 对于``query``分词,然后把分词后的每一个单词在词库里面搜一下,假设搜不到的话可以认为是拼写错误的! 如果拼写错误了再通过``channel``和``bigram``来计算最适合的候选。 #### 6.5 基于拼写纠错算法,实现用户输入自动矫正   首先有了用户的输入``query``, 然后做必要的处理把句子转换成tokens的形状,然后对于每一个token比较是否是valid, 如果不是的话就进行下面的修正过程。 博客链接:https://blog.csdn.net/weixin_46649052/article/details/118573356