本次项目的目标是搭建一个基于检索式的简单的问答系统。
通过此项目,你将会有机会掌握以下几个知识点:
此项目需要的数据:
检索式的问答系统
问答系统所需要的数据已经提供,对于每一个问题都可以找得到相应的答案,所以可以理解为每一个样本数据是 <问题、答案>。 那系统的核心是当用户输入一个问题的时候,首先要找到跟这个问题最相近的已经存储在库里的问题,然后直接返回相应的答案即可。 举一个简单的例子:
假设我们的库里面已有存在以下几个<问题,答案>: <“人工智能和机器学习的关系什么?”, “其实机器学习是人工智能的一个范畴,很多人工智能的应用要基于机器学习的技术”> <“人工智能最核心的语言是什么?”, “Python”> …
假设一个用户往系统中输入了问题 “机器学习与人工智能有什么关系?”, 那这时候系统先去匹配最相近的“已经存在库里的”问题。 那在这里很显然是 “机器学习与人工智能有什么关系”和“人工智能和机器学习的关系什么”是最相近的。 所以当我们定位到这个问题之后,直接返回它的答案 “其实机器学习是人工智能的一个范畴,很多人工智能的应用要基于机器学习的技术”就可以了。所以这里的核心问题可以归结为计算两个问句(query)之间的相似度。
在本次项目中,你会频繁地使用到sklearn这个机器学习库。具体安装请见:http://scikit-learn.org/stable/install.html sklearn包含了各类机器学习算法和数据处理工具,包括本项目需要使用的词袋模型,均可以在sklearn工具包中找得到。
import json
def read_corpus(file_path):
"""
读取给定的语料库,并把问题列表和答案列表分别写入到 qlist, alist 里面。 在此过程中,不用对字符换做任何的处理(这部分需要在 Part 2.3里处理)
qlist = ["问题1", “问题2”, “问题3” ....]
alist = ["答案1", "答案2", "答案3" ....]
务必要让每一个问题和答案对应起来(下标位置一致)
"""
with open(file_path, 'r') as path:
fileJson = json.load(path)
qlist = []
alist = []
for data_dict in fileJson['data']:
for par_dict in data_dict['paragraphs']:
for qa_dict in par_dict['qas']:
qlist.append(qa_dict['question'])
try:
alist.append(qa_dict['answers'][0]['text'])
except IndexError:
qlist.pop()
assert len(qlist) == len(alist) # 确保长度一样
return qlist, alist
qlist, alist = read_corpus('./data/train-v2.0.json')
对数据的理解是任何AI工作的第一步,需要充分对手上的数据有个更直观的理解。
# TODO: 统计一下在qlist 总共出现了多少个单词? 总共出现了多少个不同的单词?
# 这里需要做简单的分词,对于英文我们根据空格来分词即可,其他过滤暂不考虑(只需分词)
from collections import Counter
word_cnt = Counter()
for text in qlist:
word_cnt.update(text.strip(' .!?').split(' '))
print(sum(word_cnt.values()))
# TODO: 统计一下qlist中每个单词出现的频率,并把这些频率排一下序,然后画成plot. 比如总共出现了总共7个不同单词,而且每个单词出现的频率为 4, 5,10,2, 1, 1,1
# 把频率排序之后就可以得到(从大到小) 10, 5, 4, 2, 1, 1, 1. 然后把这7个数plot即可(从大到小)
# 需要使用matplotlib里的plot函数。y轴是词频
import matplotlib.pyplot as plt
values = []
for item in word_cnt.most_common(100): # 打印前出现频率最高的一百个词
values.append(item[1])
print(word_cnt.most_common(100))
plt.plot(values)
plt.show()
次部分需要尝试做文本的处理。在这里我们面对的是英文文本,所以任何对英文适合的技术都可以考虑进来。
# TODO: 对于qlist, alist做文本预处理操作。 可以考虑以下几种操作:
# 1. 停用词过滤 (去网上搜一下 "english stop words list",会出现很多包含停用词库的网页,或者直接使用NLTK自带的)
# 2. 转换成lower_case: 这是一个基本的操作
# 3. 去掉一些无用的符号: 比如连续的感叹号!!!, 或者一些奇怪的单词。
# 4. 去掉出现频率很低的词:比如出现次数少于10,20....
# 5. 对于数字的处理: 分词完只有有些单词可能就是数字比如44,415,把所有这些数字都看成是一个单词,这个新的单词我们可以定义为 "#number"
# 6. stemming(利用porter stemming): 因为是英文,所以stemming也是可以做的工作
# 7. 其他(如果有的话)
# 请注意,不一定要按照上面的顺序来处理,具体处理的顺序思考一下,然后选择一个合理的顺序
# hint: 停用词用什么数据结构来存储? 不一样的数据结构会带来完全不一样的效率!
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer
from nltk.tokenize import word_tokenize
import string
import math
def preprocessing(text):
word_list = []
for word in word_tokenize(text):
word = stemmer.stem(word.lower())
word = '#number' if word.isdigit() else word
if word not in stop_words:
word_list.append(word)
return word_list
def filter_word(qlist_seg): # 根据Zipf定律保留99%的文本
'''
Zipf定律:第n常见的词的出现次数是最常见的词出现次数的1/n,
当n足够大时,数列1/n的累加约等于ln(n),即为文本总的单词数,
为使99%的文本得到覆盖则阈值点x需满足ln(x)=0.99*ln(n),x=e^(0.99*ln(n))。
'''
value_sort = sorted(word_cnt.values(), reverse=True)
ts_value = value_sort[int(math.exp(0.99 * math.log(len(word_cnt))))]
for cur in range(len(qlist_seg)):
qlist_seg[cur] = [word for word in qlist_seg[cur] if word_cnt[word] > ts_value]
return qlist_seg
stop_words = set(stopwords.words('english'))
stop_words.update(string.punctuation)
stemmer = PorterStemmer()
word_cnt = Counter()
qlist_seg = []
for text in qlist:
word_list = preprocessing(text)
qlist_seg.append(word_list)
word_cnt.update(word_list)
# 过滤低频词
qlist_seg = filter_word(qlist_seg)
注:在使用nltk的分词和停用词功能时会提示要下载它的数据集,如果直接下载失败的话可以下载我上传的nltk数据包,只需在存放python包的文件夹里将其解压即可使用nltk的各种功能。
当我们做完关键的预处理过程之后,就需要把每一个文本转换成向量。
# TODO: 把qlist中的每一个问题字符串转换成tf-idf向量, 转换之后的结果存储在X矩阵里。 X的大小是: N* D的矩阵。 这里N是问题的个数(样本个数),
# D是字典库的大小。
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform([' '.join(word_list) for word_list in qlist_seg])
# TODO: 矩阵X有什么特点? 计算一下它的稀疏度
def sparsity(X):
return 1.0 - X.nnz / float(X.shape[0] * X.shape[1])
print(X.shape)
print(sparsity(X))
from queue import PriorityQueue
def top5results(input_q):
"""
给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
1. 对于用户的输入 input_q 首先做一系列的预处理,然后再转换成tf-idf向量(利用上面的vectorizer)
2. 计算跟每个库里的问题之间的相似度
3. 找出相似度最高的top5问题的答案
"""
q_vector = vectorizer.transform([' '.join(preprocessing(input_q))])
sim = (X * q_vector.T).toarray()
pq = PriorityQueue()
for cur in range(sim.shape[0]):
pq.put((sim[cur][0], cur))
if len(pq.queue) > 5:
pq.get()
pq_rank = sorted(pq.queue, key=lambda x: x[0], reverse=True)
print([item[0] for item in pq_rank])
return [alist[item[1]] for item in pq_rank]
# TODO: 编写几个测试用例,并输出结果
print(top5results("Which airport was shut down?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results("Which airport is closed?"))
print(top5results("What government blocked aid after Cyclone Nargis?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results("Which government stopped aid after Hurricane Nargis?"))
上面的算法,一个最大的缺点是每一个用户问题都需要跟库里的所有的问题都计算相似度。假设我们库里的问题非常多,这将是效率非常低的方法。 这里面一个方案是通过倒排表的方式,先从库里面找到跟当前的输入类似的问题描述。然后针对于这些candidates问题再做余弦相似度的计算。这样会节省大量的时间。
# TODO: 基于倒排表的优化。在这里,我们可以定义一个类似于hash_map, 比如 inverted_index = {}, 然后存放包含每一个关键词的文档出现在了什么位置,
# 也就是,通过关键词的搜索首先来判断包含这些关键词的文档(比如出现至少一个),然后对于candidates问题做相似度比较。
from collections import defaultdict
inv_idx = defaultdict(set)
for cur in range(len(qlist_seg)):
for word in qlist_seg[cur]:
inv_idx[word].add(cur)
def top5results_invidx(input_q):
"""
给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
1. 利用倒排表来筛选 candidate
2. 对于用户的输入 input_q 首先做一系列的预处理,然后再转换成tf-idf向量(利用上面的vectorizer)
3. 计算跟每个库里的问题之间的相似度
4. 找出相似度最高的top5问题的答案
"""
seg = preprocessing(input_q)
candidates = set()
for word in seg:
candidates = candidates | inverted_idx[word]
candidates = list(candidates)
q_vector = vectorizer.transform([' '.join(seg)])
sim = (X[candidates] * q_vector.T).toarray()
pq = PriorityQueue()
for cur in range(sim.shape[0]):
pq.put((sim[cur][0], candidates[cur]))
if len(pq.queue) > 5:
pq.get()
pq_rank = sorted(pq.queue, key=lambda x: x[0], reverse=True)
return [alist[item[1]] for item in pq_rank]
# TODO: 编写几个测试用例,并输出结果
print(top5results_invidx("Which airport was shut down?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results_invidx("Which airport is closed?"))
print(top5results_invidx("What government blocked aid after Cyclone Nargis?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results_invidx("Which government stopped aid after Hurricane Nargis?"))
上面所用到的方法论是基于词袋模型(bag-of-words model)。这样的方法论有两个主要的问题:1. 无法计算词语之间的相似度 2. 稀疏度很高。 在2.7里面我们 讲采用词向量作为文本的表示。词向量方面需要下载: https://nlp.stanford.edu/projects/glove/ (请下载glove.6B.zip),并使用d=100的词向量(100维)。
# TODO:读取每一个单词的嵌入。这个是 D*H的矩阵,这里的D是词典库的大小, H是词向量的大小。 这里面我们给定的每个单词的词向量,那句子向量怎么表达?
# 其中,最简单的方式 句子向量 = 词向量的平均(出现在问句里的), 如果给定的词没有出现在词典库里,则忽略掉这个词。
from gensim.models import KeyedVectors
from gensim.scripts.glove2word2vec import glove2word2vec
import numpy as np
glove2word2vec('./data/glove.6B.100d.txt', './data/glove2word2vec.6B.100d.txt')
model = KeyedVectors.load_word2vec_format('./data/glove2word2vec.6B.100d.txt ')
def get_wordvec(word_list):
vector = np.zeros((1, 100))
size = len(word_list)
for word in word_list:
try:
vector += model[word]
except KeyError:
size -= 1
if size > 0:
return vector / size
else:
return vector
X = np.zeros((len(qlist_seg), 100))
for cur in range(X.shape[0]):
X[cur] = get_wordvec(qlist_seg[cur])
X = X / np.linalg.norm(X, axis=1, keepdims=True)
def top5results_emb(input_q):
"""
给定用户输入的问题 input_q, 返回最有可能的TOP 5问题。这里面需要做到以下几点:
1. 利用倒排表来筛选 candidate
2. 对于用户的输入 input_q,转换成句子向量
3. 计算跟每个库里的问题之间的相似度
4. 找出相似度最高的top5问题的答案
"""
seg = preprocessing(input_q)
candidates = set()
for word in seg:
candidates = candidates | inverted_idx[word]
candidates = list(candidates)
q_vector = get_wordvec(seg)
q_vector = q_vector / np.linalg.norm(q_vector, axis=1, keepdims=True)
sim = X[candidates] @ q_vector.T
pq = PriorityQueue()
for cur in range(sim.shape[0]):
pq.put((sim[cur][0], candidates[cur]))
if len(pq.queue) > 5:
pq.get()
pq_rank = sorted(pq.queue, key=lambda x: x[0], reverse=True)
print([item[0] for item in pq_rank])
return [alist[item[1]] for item in pq_rank]
# TODO: 编写几个测试用例,并输出结果
print(top5results_emb("Which airport was shut down?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results_emb("Which airport is closed?"))
print(top5results_emb("What government blocked aid after Cyclone Nargis?")) # 在问题库中存在,经过对比,返回的首结果正确
print(top5results_emb("Which government stopped aid after Hurricane Nargis?"))