引言
本部分任务主要是将用户输入问答系统的自然语言转化成知识库的查询语句,因此本文将分成两部分进行介绍。
- 第一部分介绍任务所涉及的背景知识;
- 第二部分则是相应的代码和其注释
问答系统
问答系统简介
问答系统(Question Answering System,QA System),是未来自然语言处理的明日之星。问答系统外部的行为上来看,其与目前主流资讯检索技术有两点不同:首先是查询方式为完整而口语化的问句,再来则是其回传的为高精准度网页结果或明确的答案字串。以Ask Jeeves为例,使用者不需要思考该使用什么样的问法才能够得到理想的答案,只需要用口语化的方式直接提问如“请问谁是美国总统?”即可。而系统在了解使用者问句后,会非常清楚地回答特朗普是美国总统。面对这种系统,使用者不需要费心去一一检视搜索引擎回传的网页,对于资讯检索的效率与资讯的普及都有很大帮助。从系统内部来看,问答系统使用了大量有别于传统资讯检索系统自然语言处理技术,如自然语言剖析(Natural Language Parsing)、问题分类(Question Classification)、专名辨识(Named Entity Recognition)等等。少数系统甚至会使用复杂的逻辑推理机制,来区隔出需要推理机制才能够区隔出来的答案。在系统所使用的资料上,除了传统资讯检索会使用到的资料外(如字典),问答系统还会使用本体论等语义资料,或者利用网页来增加资料的丰富性。
截至目前为止,最著名的问答系统应属IBM的沃森系统。该系统在2011年于Jeopardy节目中,与人类同场较劲,并获得最后的胜利。
问答系统分类
从知识领域划分,可分为封闭领域以及开放领域两类系统。封闭领域系统专注于回答特定领域的问题,如医药或特定公司等。由于问题领域受限,系统有比较大的发挥空间,可以导入如专属本体论等知识,或将答案来源全部转换成结构性资料,来有效提升系统的表现。开放领域系统则希望不设限问题的内容范围,天文地理无所不问。系统中所有知识与元件都必须尽量做到与领域不相关,当然难度也相对地提高。
-
从答案来源划分,可分为“数据库问答”、“常问问题问答”、“新闻问答”、“互联网问答”等系统。数据库是最常见的结构化资料储存媒介。虽然透过操控SQL语言便能够有效率地存取资料,但有些系统试图提供更直觉的自然语言查询界面,希望能进一步降低学习门槛。1970年代的LUNAR系统算是早期成功的案例,其正确答题率可以达到百分之七十,可回答月球陨石相关资料。微软的English Query则是近期的一个商业产品。English Query在剖析完英文问句后,会根据底层数据库结构,自动产生出相对应的SQL查询。虽然有这些成功系统案例,但数据库问答系统似乎很难被大众所接受,其中一个因素可能是因为对于结构化资料来说,结构化的查询界面在查询上更为方便。常问问题(Frequently Asked Questions, FAQs)是公司或者长期经营领域中常见的重要资源。一份FAQ资料包含了一个问句以及相对应的答案描述。FAQ问答系统的主要责任在比对使用者问句与现有FAQ问句的相似度,此与其他问答系统着重在答案语料中撷取答案的作法不同。另一种重要的系统为新闻问答系统。今日新闻媒体都已经数字化了,每日累积所产生的新闻资讯量是相当可观的,加上新闻的内容广泛丰富,作为开放领域问答系统的答案来源是最适合不过的。这样的特性使得此类系统的评估较为容易,因此稍后会提到的国际评估会议都是采用此类系统作为评估对象。最后一类的是互联网问答系统,这些系统利用搜索引擎回传的结果网页,从中撷取答案。主要挑战在于如何处理网络多异质性的资料,以及高噪声网页过滤等问题。
-
从实现方式划分,可分为基于流水线和端到端两类。基于流水线(pipeline)实现:如下图 1 所示,基于流水线实现的问答系统有四大核心模块,分别由自然语言理解(NLU)、对话状态跟踪器(DST)、对话策略(DPL)和自然语言生成(NLG)依次串联构成的一条流水线,各模块可独立设计,模块间协作完成任务。基于端到端(end-to-end)实现:基于端到端实现的问答系统,主要是结合深度学习技术,通过海量数据训练,挖掘出从用户自然语言输入到系统自然语言输出的整体映射关系,而忽略中间过程的一种方法。但就目前工业界整体应用而言,工业界的问答系统目前大多采用的还是基于流水线实现的方式。
Query理解
什么是Query理解
query理解是整个搜索系统中最上游的一环,负责的是从query中提取信息,从而了解用户希望通过这个query搜索出什么。
query理解,决定了下游的搜索召回策略。底层数据从技术上,有各种类型的数据库需要检索;从算法策略上,也有多种召回的方案,例如高准确的、高召回的等等,要用什么策略,这要取决于query理解的结论。
例子
要把一个事情说清楚,举例是一个很好的方法。来一个query:唐人街探案
直观的看,这里有一个核心词——唐人街探案,其核心意图就是想看看唐人街探案的相关内容吧,来看看系统内干了些啥:
纠错:初步看来,没有错误,过。 意图识别和实体识别:有唐人街探案这个实体,常见的首先是一部电影,最近还上了网剧,从热度上看,由于网剧比较新,所以用户在近期更可能看的是网剧,当然信息不足,不代表用户真的就只想看网剧,所以电影的东西也要给一些,最大限度保证满足需求。
好了,以百度为例看看结果:
前4条,分别给的是百科、爱奇艺网剧、豆瓣电影评论、爱奇艺电影。基本上就覆盖了我上面的分析内容,用户只输入了一个简单的实体,就会给出精准的对应信息,百科了解概况,爱奇艺网剧满足近况,豆瓣电影有影评,最终补充了电影,满足更为全面的请求。
我们来复杂一些,升级为唐人街探案网剧怎么样。
唐人街探案还是一个实体,网剧和电影双意图,但是由于用户输入了网剧,有关电影的内容基本上就可以不出了,最后来了个怎么样,说明用户是更在乎影评,而非要看电视剧了,当然给电视剧了用户不会反感,属于弱意图了。好了,来看百度结果:
前5条都围绕着剧评进行,可以说是分析的非常精准了,且前面几条也是比较出名的媒体给出的答案,知乎、新闻、豆瓣、松子电影,第六条很机智的给了爱奇艺的链接了,而且不是展开的,而是一个摘要形式的,大家可以对比一下上一条搜索的结果区别,从这里,大家就能理解,query理解具体做了些什么事情。
query理解的内容
那么,要做query,要做什么工作呢。仔细想想,其实主要就是下面几个:
- 纠错改写。针对用户输错的,没输入完全的,内容,进行修正。底层数据库只支持精准搜索,因此需要将query改写到正确的内容下。
- 意图识别。通过分析语义等方式,在一定的类目结构下,识别出具体意图。这个意图识别的目标,大家可以理解为告诉下游,需要在哪个库数据进行搜索。
- 实体识别。其实和意图识别一样,只不过,粒度更细,但是是词级别的分析,从query中抽取关键的实体,如果说意图识别是为了告诉下游该检索那个数据库,那实体识别就是为了告诉下游,在该数据库下,该检索哪些字段。
- 词权重问题。query里面有两个词,两个文档分别匹配到了其中一个词,那谁能靠前?这就要看匹配到什么内容更为重要。如家宾馆,匹配到一个如家酒店和五洲宾馆,如家酒店应该在前,这里就是为了解决这个问题。
query理解的具体操作
query理解下的所有内容,除了意图识别本身外,其实我都或多或少介绍过。
纠错改写
- 基于统计挖掘,分析最高频的正确答案,在用户错误的时候,分析他的真实意图,改写过去。
- 基于机器学习和深度学习,识别错误,改正错误。
意图识别
意图识别简单的理解,其实是一次文本分类,那么文本分类,我们把思路拓展开,其实也是两条路——传统方法和NLP。
- 传统方法想必很多人其实了解的并不多,但其实是搜索领域内非常常见,通过规则、词典、正则等方式进行识别,准确率高、速度快。
- NLP,通过语义分析的手段,文本分类,达到语义分析的目的。
实体识别
其实问题抽象出来,就是个难度高于文本分类的序列标注问题,搜索中的命名实体识别,我聊过的,在这里:
具体思路仍然分为两派,传统方法和NLP。
词权重问题
- 统计的方法,其中tfidf最为常见,而由于query的长度都不长,所以其实就是idf的计算了。
- NLP方法,其实就是序列标注问题的升级版了。
任务实践
命名实体识别任务实践
命名实体识别整体思路介绍
- step 1:对于用户的输入,先使用预先构建的疾病、疾病别名、并发症和症状的AC Tree进行匹配;
- step 2:若全都无法匹配到相应实体,则使用结巴切词库对用户输入的文本进行切分;
- step 3:然后将每一个词都去与疾病词库、疾病别名词库、并发症词库和症状词库中的词计算相似度得分(overlap score、余弦相似度分数和编辑距离分数),如果相似度得分超过0.7,则认为该词是这一类实体;
-
step 4:最后排序选取最相关的词作为实体(项目所有的实体类型如下图所示,但实体识别时仅使用了疾病、别名、并发症和症状四种实体)
构建 AC Tree
def build_actree(self, wordlist):
"""
构造actree,加速过滤
:param wordlist:
:return:
"""
actree = ahocorasick.Automaton()
# 向树中添加单词
for index, word in enumerate(wordlist):
actree.add_word(word, (index, word))
actree.make_automaton()
return actree
这一块主要使用了AC自动机字符串匹配算法Aho-Corasick,通俗说就是有个大的列表,客户输入一句话,如何根据客户输入的一句话,从大列表中匹配出字符串交集。
比如我们有一个wordlist列表,长度很长,包含43430个元素:
['长春海外制药接骨续筋片', '香菇炖甲鱼', '三鹤药业黄柏胶囊', '上海衡山熊去氧胆酸片', '升和药业依托泊苷注射液', '怡诺思', '人格障碍', '转铁蛋白饱和度', '脾囊肿', '素烧白萝卜', '利君现代冠脉宁片',
'上海复华药业注射用还原型谷', '阴囊上有白色小疙瘩', '腹痛伴休克', '成都通德胰激肽原酶肠溶片', '蒸猪肝', '河北百善血尿胶囊', '精神障碍', '输卵管畸形', '元和抑眩宁胶囊', '莲藕豆腐', '辰欣哈西奈德溶液',
'信谊烟酸片', '慢性胆囊炎', '参芪降糖颗粒', '康普药业盐酸普萘洛尔片', '西安迪赛胸腺肽肠溶片', '双鹭药业注射用复合辅酶', '慢性筛窦炎', '新高制药维胺酯维E乳膏', '冰黄肤乐软膏', '神经类疾病', '液晶热图',
'枣(干)', '股外侧皮神经病', '浙江惠松硅炭银片', '牙根外露', '湖北潜江氯霉素滴眼液', '盐类皮质激素分泌过多', '五子衍宗丸', '小儿阵发性睡眠性血红蛋白尿症', '功能失调性子宫出血病', '茵栀黄口服液',
'眼底出血和渗出', '斯达制药注射用头孢噻肟钠', '复方白芷酊', '胫腓骨骨折', '西南药业氯霉素片', '宫颈炎', '茶碱缓释胶囊', '原发性硬化性胆管炎', '郑州韩都利肺胶囊', '咽反射消失', '脊髓灰质炎',
'甲状腺片', '回盲瓣功能不全', '乙肝e抗体(抗...', '马齿苋粥', '动脉硬化', '宝宝乐', '肠闭锁', '肺放线菌病', '江苏晨牌产妇安颗粒', '犬吠样咳嗽', '胃康灵胶囊', '小儿烟酸缺乏病', '青龙防风通圣丸',
'广东南国维生素C片', '碘化油咀嚼片', '西乐葆', '伟哥甲磺酸酚妥拉明分散片', '成都迪康药业樟脑醑', '斑疹', '五花炖墨鱼', '肉炖芸豆粉条', '陕西东泰制药益脉康胶囊', '桔梗八味颗粒', '华南牌溴丙胺太林片',
'吉林敖东洮南小牛脾提取物注', '仁青芒觉', '血吸虫病与肝胆疾病',...,'持续性枕横位难产', '弯曲菌感染', '丝瓜蘑菇肉片汤', '长春银诺克清咽片', '肝叶萎缩', '迪皿盐酸左西替利嗪口服溶液']
index, (index, word)如下:
0 (0, '长春海外制药接骨续筋片')
1 (1, '香菇炖甲鱼')
2 (2, '三鹤药业黄柏胶囊')
3 (3, '上海衡山熊去氧胆酸片')
4 (4, '升和药业依托泊苷注射液')
5 (5, '怡诺思')
6 (6, '人格障碍')
7 (7, '转铁蛋白饱和度')
8 (8, '脾囊肿')
9 (9, '素烧白萝卜')
10 (10, '利君现代冠脉宁片')
......
43422 (43422, '弯曲菌感染')
43423 (43423, '丝瓜蘑菇肉片汤')
43424 (43424, '长春银诺克清咽片')
43425 (43425, '肝叶萎缩')
43426 (43426, '迪皿盐酸左西替利嗪口服溶液')
43427 (43427, '华润天和麝香壮骨膏')
43428 (43428, '湖北恒安曲咪新乳膏')
43429 (43429, '子宫小')
#############################
import ahocorasick
actree = ahocorasick.Automaton()
for index, word in enumerate(wordlist):
actree.add_word(word, (index, word))
actree.make_automaton()
#其中wordlist就是上面的那个长度为43430的列表
for i in actree.iter('昨天发烧,服用了阿司匹林,并且还吃了牛黄清胃丸,饭是吃了瓜烧白菜,大便有点色浅'):
print(i)
这样客户输入一个字符串,我们能够快速的从之前的列表中匹配出相应的实体元素:
因此我们可以使用AC Tree进行问句过滤,得到匹配的词和类型。如疾病,疾病别名,并发症,症状
def entity_reg(self, question):
"""
模式匹配, 得到匹配的词和类型。如疾病,疾病别名,并发症,症状
:param question:str
:return:
"""
self.result = {}
for i in self.disease_tree.iter(question):
word = i[1][1]
if "Disease" not in self.result:
self.result["Disease"] = [word]
else:
self.result["Disease"].append(word)
for i in self.alias_tree.iter(question):
word = i[1][1]
if "Alias" not in self.result:
self.result["Alias"] = [word]
else:
self.result["Alias"].append(word)
for i in self.symptom_tree.iter(question):
wd = i[1][1]
if "Symptom" not in self.result:
self.result["Symptom"] = [wd]
else:
self.result["Symptom"].append(wd)
for i in self.complication_tree.iter(question):
wd = i[1][1]
if "Complication" not in self.result:
self.result["Complication"] = [wd]
else:
self.result["Complication"] .append(wd)
return self.result
使用相似度进行实体匹配
当AC Tree的匹配都没有匹配到实体时,使用查找相似词的方式进行实体匹配
def find_sim_words(self, question):
"""
当全匹配失败时,就采用相似度计算来找相似的词
:param question:
:return:
"""
import re
import string
from gensim.models import KeyedVectors
# 使用结巴加载自定义词典
jieba.load_userdict(self.vocab_path)
# 加载词向量
self.model = KeyedVectors.load_word2vec_format(self.word2vec_path, binary=False)
# 数据预处理,正则去除特殊符号
sentence = re.sub("[{}]", re.escape(string.punctuation), question)
sentence = re.sub("[,。‘’;:?、!【】]", " ", sentence)
sentence = sentence.strip()
# 使用结巴进行分词
words = [w.strip() for w in jieba.cut(sentence) if w.strip() not in self.stopwords and len(w.strip()) >= 2]
alist = []
# 对每个词,都让其与每类实体词典进行相似对比,
# 最终选取分数最高的实体和其属于的实体类型
for word in words:
temp = [self.disease_entities, self.alias_entities, self.symptom_entities, self.complication_entities]
for i in range(len(temp)):
flag = ''
if i == 0:
flag = "Disease"
elif i == 1:
flag = "Alias"
elif i == 2:
flag = "Symptom"
else:
flag = "Complication"
scores = self.simCal(word, temp[i], flag)
alist.extend(scores)
temp1 = sorted(alist, key=lambda k: k[1], reverse=True)
if temp1:
self.result[temp1[0][2]] = [temp1[0][0]]
# 计算词语和字典中的词的相似度
def simCal(self, word, entities, flag):
"""
计算词语和字典中的词的相似度
相同字符的个数/min(|A|,|B|) + 余弦相似度
:param word: str
:param entities:List
:return:
"""
a = len(word)
scores = []
for entity in entities:
sim_num = 0
b = len(entity)
c = len(set(entity+word))
temp = []
for w in word:
if w in entity:
sim_num += 1
if sim_num != 0:
score1 = sim_num / c # overlap score
temp.append(score1)
try:
score2 = self.model.similarity(word, entity) # 余弦相似度分数
temp.append(score2)
except:
pass
score3 = 1 - self.editDistanceDP(word, entity) / (a + b) # 编辑距离分数
if score3:
temp.append(score3)
score = sum(temp) / len(temp)
if score >= 0.7:
scores.append((entity, score, flag))
scores.sort(key=lambda k: k[1], reverse=True)
return scores
意图识别任务实践
意图识别整体思路介绍
- step 1:利用TF-IDF表征文本特征,同时构建一些人工特征(每一类意图常见词在句子中出现的个数);
- step 2:训练朴素贝叶斯模型进行意图识别任务;
-
step 3:使用实体信息进行意图的纠正和补充。
该项目通过手工标记210条意图分类训练数据,并采用朴素贝叶斯算法训练得到意图分类模型。其最佳测试效果的F1值达到了96.68%。
意图识别整体步骤介绍
特征构建
- TF-IDF特征
# 提取问题的TF-IDF特征
def tfidf_features(self, text, vectorizer):
"""
提取问题的TF-IDF特征
:param text:
:param vectorizer:
:return:
"""
jieba.load_userdict(self.vocab_path)
words = [w.strip() for w in jieba.cut(text) if w.strip() and w.strip() not in self.stopwords]
sents = [' '.join(words)]
tfidf = vectorizer.transform(sents).toarray()
return tfidf
- 人工特征
self.symptom_qwds = ['什么症状', '哪些症状', '症状有哪些', '症状是什么', '什么表征', '哪些表征', '表征是什么',
'什么现象', '哪些现象', '现象有哪些', '症候', '什么表现', '哪些表现', '表现有哪些',
'什么行为', '哪些行为', '行为有哪些', '什么状况', '哪些状况', '状况有哪些', '现象是什么',
'表现是什么', '行为是什么'] # 询问症状
self.cureway_qwds = ['药', '药品', '用药', '胶囊', '口服液', '炎片', '吃什么药', '用什么药', '怎么办',
'买什么药', '怎么治疗', '如何医治', '怎么医治', '怎么治', '怎么医', '如何治',
'医治方式', '疗法', '咋治', '咋办', '咋治', '治疗方法'] # 询问治疗方法
self.lasttime_qwds = ['周期', '多久', '多长时间', '多少时间', '几天', '几年', '多少天', '多少小时',
'几个小时', '多少年', '多久能好', '痊愈', '康复'] # 询问治疗周期
self.cureprob_qwds = ['多大概率能治好', '多大几率能治好', '治好希望大么', '几率', '几成', '比例',
'可能性', '能治', '可治', '可以治', '可以医', '能治好吗', '可以治好吗', '会好吗',
'能好吗', '治愈吗'] # 询问治愈率
self.check_qwds = ['检查什么', '检查项目', '哪些检查', '什么检查', '检查哪些', '项目', '检测什么',
'哪些检测', '检测哪些', '化验什么', '哪些化验', '化验哪些', '哪些体检', '怎么查找',
'如何查找', '怎么检查', '如何检查', '怎么检测', '如何检测'] # 询问检查项目
self.belong_qwds = ['属于什么科', '什么科', '科室', '挂什么', '挂哪个', '哪个科', '哪些科'] # 询问科室
self.disase_qwds = ['什么病', '啥病', '得了什么', '得了哪种', '怎么回事', '咋回事', '回事',
'什么情况', '什么问题', '什么毛病', '啥毛病', '哪种病'] # 询问疾病
def other_features(self, text):
"""
提取问题的关键词特征
:param text:
:return:
"""
features = [0] * 7
for d in self.disase_qwds:
if d in text:
features[0] += 1
for s in self.symptom_qwds:
if s in text:
features[1] += 1
for c in self.cureway_qwds:
if c in text:
features[2] += 1
for c in self.check_qwds:
if c in text:
features[3] += 1
for p in self.lasttime_qwds:
if p in text:
features[4] += 1
for r in self.cureprob_qwds:
if r in text:
features[5] += 1
for d in self.belong_qwds:
if d in text:
features[6] += 1
m = max(features)
n = min(features)
normed_features = []
if m == n:
normed_features = features
else:
for i in features:
j = (i - n) / (m - n)
normed_features.append(j)
return np.array(normed_features)
使用朴素贝叶斯进行文本分类
- 项目没有给出训练过程,可参考下面sklearn的例子
# 项目没有给出训练过程,可参考下面sklearn的例子
from sklearn.naive_bayes import MultinomialNB
mnb = MultinomialNB()
mnb.fit(X_train,y_train)
y_predict = mnb.predict(X_test)
# 意图分类模型文件
self.tfidf_path = os.path.join(cur_dir, 'model/tfidf_model.m')
self.nb_path = os.path.join(cur_dir, 'model/intent_reg_model.m') #朴素贝叶斯模型
self.tfidf_model = joblib.load(self.tfidf_path)
self.nb_model = joblib.load(self.nb_path)
# 意图预测
tfidf_feature = self.tfidf_features(question, self.tfidf_model)
other_feature = self.other_features(question)
m = other_feature.shape
other_feature = np.reshape(other_feature, (1, m[0]))
feature = np.concatenate((tfidf_feature, other_feature), axis=1)
predicted = self.model_predict(feature, self.nb_model)
intentions.append(predicted[0])
- 根据所识别的实体进行补充和纠正意图
# 已知疾病,查询症状
if self.check_words(self.symptom_qwds, question) and ('Disease' in types or 'Alia' in types):
intention = "query_symptom"
if intention not in intentions:
intentions.append(intention)
# 已知疾病或症状,查询治疗方法
if self.check_words(self.cureway_qwds, question) and \
('Disease' in types or 'Symptom' in types or 'Alias' in types or 'Complication' in types):
intention = "query_cureway"
if intention not in intentions:
intentions.append(intention)
# 已知疾病或症状,查询治疗周期
if self.check_words(self.lasttime_qwds, question) and ('Disease' in types or 'Alia' in types):
intention = "query_period"
if intention not in intentions:
intentions.append(intention)
# 已知疾病,查询治愈率
if self.check_words(self.cureprob_qwds, question) and ('Disease' in types or 'Alias' in types):
intention = "query_rate"
if intention not in intentions:
intentions.append(intention)
# 已知疾病,查询检查项目
if self.check_words(self.check_qwds, question) and ('Disease' in types or 'Alias' in types):
intention = "query_checklist"
if intention not in intentions:
intentions.append(intention)
# 查询科室
if self.check_words(self.belong_qwds, question) and \
('Disease' in types or 'Symptom' in types or 'Alias' in types or 'Complication' in types):
intention = "query_department"
if intention not in intentions:
intentions.append(intention)
# 已知症状,查询疾病
if self.check_words(self.disase_qwds, question) and ("Symptom" in types or "Complication" in types):
intention = "query_disease"
if intention not in intentions:
intentions.append(intention)
# 若没有检测到意图,且已知疾病,则返回疾病的描述
if not intentions and ('Disease' in types or 'Alias' in types):
intention = "disease_describe"
if intention not in intentions:
intentions.append(intention)
# 若是疾病和症状同时出现,且出现了查询疾病的特征词,则意图为查询疾病
if self.check_words(self.disase_qwds, question) and ('Disease' in types or 'Alias' in types) \
and ("Symptom" in types or "Complication" in types):
intention = "query_disease"
if intention not in intentions:
intentions.append(intention)
# 若没有识别出实体或意图则调用其它方法
if not intentions or not types:
intention = "QA_matching"
if intention not in intentions:
intentions.append(intention)
self.result["intentions"] = intentions
后续就是通过上述得到的意图信息和实体信息选择对应的模版,并将实体信息填充入组成查询语句进行数据库查询。
参考资料:
https://github.com/datawhalechina/team-learning-nlp/blob/master/KnowledgeGraph_Basic/task04.md#%E7%9B%AE%E5%BD%95
https://zhuanlan.zhihu.com/p/136313695
https://zh.wikipedia.org/wiki/%E5%95%8F%E7%AD%94%E7%B3%BB%E7%B5%B1