NLP基础:检索式问答系统实战
1. 目的与思路
检索式问答系统所需要的数据已经提供,对于每一个问题都可以找得到相应的答案,所以可以理解为每一个样本数据是 <问题、答案>pair。 那系统的核心是当用户输入一个问题的时候,首先要找到跟这个问题最相近的已经存储在库里的问题,然后直接返回相应的答案即可。
- 最简单的思路是:将用户的输入与问题库中每个问题进行比较,找到与输入最相似的问题,并将该问题对应的答案返回给用户即可。这里衡量相似度通过计算输入与问题表示之间的欧式距离、余弦相似度实现。
- 上述的思路简单,但是操作复杂度高,因为要计算输入与库中的每一个问题进行相似度计算。因此要考虑优化,引入倒排表,通过层层过滤,将可选的问题范围逐步缩小。比如,可以先筛选出与用户输入有1个公共字符的问题,甚至是2个、3个…条件越严格,那么候选的问题数量就越少,计算量大大减小。
- 计算输入与问题的相似度时,需要得到它们的向量表示。在这里,首先采用TF-IDF文本表示,进行计算;其次,采用已经训练好的glove.6B 100维词向量,通过average操作,得到句子的整体向量表示进行计算。
2. 简单思路的实现
将用户的输入与问题库中每个问题进行比较,找到与输入最相似的Top5问题,并将Top5问题对应的答案返回给用户即可。
2.1 问题-答案 库的读取
采用的数据集是机器阅读理解数据集(SQuAD 2.0),一共86821个问题-答案 pair。
#读取数据
# 分数(5)
import json
import matplotlib.pyplot as plt
import numpy as np
from nltk.stem.porter import PorterStemmer
from sklearn.feature_extraction.text import TfidfVectorizer
def read_corpus():
"""
读取给定的语料库,并把问题列表和答案列表分别写入到 qlist, alist 里面。 在此过程中,不用对字符换做任何的处理(这部分需要在 Part 2.3里处理)
qlist = ["问题1", “问题2”, “问题3” ....]
alist = ["答案1", "答案2", "答案3" ....]
务必要让每一个问题和答案对应起来(下标位置一致)
"""
qlist = []
alist = []
with open("././data/train-v2.0.json") as f:
all_data = json.load(f)['data']
for data in all_data:
paragraphs = data['paragraphs']
for paragraph in paragraphs:
for qa in paragraph['qas']:
# print(qa['id'])
if qa['answers']:
qlist.append(qa['question'])
alist.append(qa['answers'][0]['text'])
assert len(qlist) == len(alist) # 确保长度一样
print("Load question and answer success. The length :{}".format(len(qlist)))
return qlist, alist
qlist, alist = read_corpus()
运行结果:
Load question and answer success. The length :86821
2.2 对数据的相关统计
2.2.1 单词统计
# TODO: 统计一下在qlist 总共出现了多少个单词? 总共出现了多少个不同的单词?
# 这里需要做简单的分词,对于英文我们根据空格来分词即可,其他过滤暂不考虑(只需分词)
word_voc = []
for question in qlist:
question = question.replace('?', ' ?')
line = question.strip().split()
word_voc += line
word_voc = set(word_voc)
word_total = len(word_voc)
print("Num of total words:{}".format(word_total))#51930
运行结果:
Num of total words:51930
2.2.2 单词频率统计
# TODO: 统计一下qlist中每个单词出现的频率,并把这些频率排一下序,然后画成plot. 比如总共出现了总共7个不同单词,而且每个单词出现的频率为 4, 5,10,2, 1, 1,1
# 把频率排序之后就可以得到(从大到小) 10, 5, 4, 2, 1, 1, 1. 然后把这7个数plot即可(从大到小)
# 需要使用matplotlib里的plot函数。y轴是词频
word_freq = {
}#统计qlist的单词频率
for question in qlist:
question = question.replace('?', ' ?')
line = question.strip().split()
for word in line:
if word in word_freq:
word_freq[word] += 1
else:
word_freq[word] = 1
sort_word_freq = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
all_word = []
word_count = []
for word, count in sort_word_freq:
all_word.append(word)
word_count.append(count)
# scale_ls = range(len(all_word))
# plt.plot(scale_ls, word_count)
# plt.xticks(scale_ls, all_word)
plt.bar(range(20), word_count[:20], color='rgb', tick_label=all_word[:20])#画出前20个词
plt.show()
运行结果:
从上面的图中可以看, 这样的一个图的走势跟反比例函数形状很类似,也就是学术界的 Zipf’s law:第二常见的频率是最常见频率的出现次数的½,第三常见的频率是最