task04笔记

本文介绍了问答系统的基本概念,包括封闭领域与开放领域的问答系统,以及基于流水线和端到端实现的架构。重点讲述了Query理解中的意图识别和槽值填充,并提供了EntityExtractor类的代码实现,该类用于从用户输入转化为知识库查询语句,涉及命名实体识别和意图识别。通过ACTree和相似度计算进行实体匹配,使用朴素贝叶斯进行意图分类。
摘要由CSDN通过智能技术生成

Datawhale 知识图谱组队学习 之 Task 4 用户输入->知识库的查询语句

特别鸣谢:QASystemOnMedicalGraph

目录

一、引言

本部分任务主要是将用户输入问答系统的自然语言转化成知识库的查询语句,因此本文将分成两部分进行介绍。

  • 第一部分介绍任务所涉及的背景知识;
  • 第二部分则是相应的代码和其注释

二、什么是问答系统?

2.1 问答系统简介

问答系统(Question Answering System,QA System)是用来回答人提出的自然语言问题的系统。根据划分标准不同,问答系统可以被分为各种不同的类型。

  • 问答系统从知识领域划分:

    • 封闭领域:封闭领域系统专注于回答特定领域的问题,由于问题领域受限,系统有比较大的发挥空间,可以导入领域知识或将答案来源全部转换成结构性资料来有效提升系统的表现;
    • 开放领域:开放领域系统则希望不设限问题的内容范围,因此其难度也相对较大。
  • 问答系统从实现方式划分:

    • 基于流水线(pipeline)实现:如下图 1 所示,基于流水线实现的问答系统有四大核心模块,分别由自然语言理解(NLU)、对话状态跟踪器(DST)、对话策略(DPL)和自然语言生成(NLG)依次串联构成的一条流水线,各模块可独立设计,模块间协作完成任务。
    • 基于端到端(end-to-end)实现:基于端到端实现的问答系统,主要是结合深度学习技术,通过海量数据训练,挖掘出从用户自然语言输入到系统自然语言输出的整体映射关系,而忽略中间过程的一种方法。但就目前工业界整体应用而言,工业界的问答系统目前大多采用的还是基于流水线实现的方式。

图 1 基于流水线(pipeline)实现

  • 问答系统从答案来源划分:
    • 「知识库问答」。是目前的研究热点。知识库问答(knowledge base question answering, KB-QA)即给定自然语言问题,通过对问题进行语义理解和解析,进而利用知识库进行查询、推理得出答案。如下图 2 所示:
    • 「常问问题问答」;
    • 「新闻问答」;
    • 「网际网路问答」;

图 2 知识库问答

2.2 Query理解

2.2.1 Query理解介绍

Query理解 (QU,Query Understanding),简单来说就是从词法、句法、语义三个层面对 Query 进行结构化解析。

  • 搜索 Query 理解包含的模块主要有:
    • Query预处理
    • Query纠错
    • Query扩展
    • Query归一
    • 意图识别
    • 槽值填充
    • Term重要性分析;

由于本任务后面代码主要涉及意图识别和槽位解析,因此这里仅对这两部分内容做介绍:

2.2.2 意图识别
  • 介绍:意图识别是用来检测用户当前输入的意图,通常其被建模为将一段自然语言文本分类为预先设定的一个或多个意图的文本分类任务。
  • 所用方法:和文本分类模型的方法大同小异,主要有:
    • 基于词典模板的规则分类
    • 传统的机器学习模型(文本特征工程+分类器)
    • 深度学习模型(Fasttext、TextCNN、BiLSTM + Self-Attention、BERT等)

图 3 意图识别

2.2.3 槽值填充
  • 介绍:槽值填充就是根据我们既定的一些结构化字段,将用户输入的信息中与其对应的部分提取出来。因此,槽值填充经常被建模为序列标注的任务。
  • 举例介绍:例如下图所示的 Query “北京飞成都的机票”,通过意图分类模型可以识别出 Query 的整体意图是订机票,在此基础上进一步语义解析出对应的出发地 Depart=“北京”,到达地 Arrive=“成都”,所以生成的形式化表达可以是:Ticket=Order(Depart,Arrive),Depart={北京},Arrive={成都}。

图 4 槽值填充

  • 序列标注的任务常用的模型有:【注:这部分内容,第二期知识图谱组队学习将进行介绍】
    • 词典匹配;
    • BiLSTM + CRF;
    • IDCNN
    • BERT等。

三、任务实践

图 5 基于知识图谱的问答系统框架

四、 主体类 EntityExtractor 框架介绍

import os
import ahocorasick  # AC自动机
from sklearn.externals import joblib
import jieba    # 分词
import numpy as np

class EntityExtractor:
    def __init__(self):
        pass

    # 构造actree,加速过滤
    def build_actree(self, wordlist)
       
    # 模式匹配, 得到匹配的词和类型。如疾病,疾病别名,并发症,症状
    def entity_reg(self, question)       

    # 当全匹配失败时,就采用相似度计算来找相似的词
    def find_sim_words(self, question)
       
    # 采用DP方法计算编辑距离
    def editDistanceDP(self, s1, s2)      

    # 计算词语和字典中的词的相似度,相同字符的个数/min(|A|,|B|)   +  余弦相似度
    def simCal(self, word, entities, flag)
       
    # 基于特征词分类
    def check_words(self, wds, sent)
        
    # 提取问题的TF-IDF特征
    def tfidf_features(self, text, vectorizer)        

    # 提取问题的关键词特征
    def other_features(self, text)
        
    # 预测意图
    def model_predict(self, x, model)
        
    # 实体抽取主函数
    def extractor(self, question)       

五、命名实体识别任务实践

5.1 命名实体识别整体思路介绍

  • step 1:对于用户的输入,先使用预先构建的疾病、疾病别名、并发症和症状的AC Tree进行匹配;
  • step 2:若全都无法匹配到相应实体,则使用结巴切词库对用户输入的文本进行切分;
  • step 3:然后将每一个词都去与疾病词库、疾病别名词库、并发症词库和症状词库中的词计算相似度得分(overlap score、余弦相似度分数和编辑距离分数),如果相似度得分超过0.7,则认为该词是这一类实体;
  • step 4:最后排序选取最相关的词作为实体(项目所有的实体类型如下图所示,但实体识别时仅使用了疾病、别名、并发症和症状四种实体)

图 6 实体介绍

本部分所有的代码都来自 entity_extractor.py 中的 EntityExtractor 类,为了方便讲解,对类内的内容进行重新组织注释

5.2 结合代码介绍

5.2.1 构建 AC Tree

先通过 entity_extractor.py 中 类 EntityExtractor 的 build_actree 函数构建AC Tree

  • 函数模块
    def build_actree(self, wordlist):
        """
        构造actree,加速过滤
        :param wordlist:四张词表
        :return:actree
        """
        actree = ahocorasick.Automaton()
        # 向树中添加单词
        for index, word in enumerate(wordlist):
            actree.add_word(word, (index, word))
        actree.make_automaton()
        return actree
  • 函数调用模块
    def __init__(self):
        # 先读取disease_vocab, symptom_vocab, alias_vocab, complications_vocab四张词表;
        # 再取词表中内容存放在对应类型的实体列表disease_entities, alias_entities, symptom_entities, complication_entities中;
        # 构造领域actree
        self.disease_tree = self.build_actree(list(set(self.disease_entities)))
        self.alias_tree = self.build_actree(list(set(self.alias_entities)))
        self.symptom_tree = self.build_actree(list(set(self.symptom_entities)))
        self.complication_tree = self.build_actree(list(set(self.complication_entities)))
5.2.2 使用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

理解:传入用户问句question,分别使用已经建立好的四个AC Tree对question进行初步检索,
如果用户问句已经是包含在AC Tree中的词,取出,判断是否已经在result中,如果不在就添加进result,最后返回result结果
被extractor(question)函数调用

5.2.3 使用 相似度进行实体匹配

gensim包用途:用于从原始的非结构化的文本中,无监督地学习到文本隐层的主题向量表达

5.2.3.1 基本概念(应用于Genism)
  • 语料Corpus: 一组原始文本的集合,用于无监督地训练文本主题的隐层结构。无需人工标注的附加信息。在Genism中,Corpus常是一个可迭代的对象,如列表,每一次迭代返回一个可用于表达文本对象的稀疏向量;
  • 向量Vector:由一组文本特征构成的列表
  • 稀疏向量SpaceVector:略去向量中多余的0元素,此时向量的每一个元素时一个(key,value)的元组;
  • 模型Model:定义了两个向量空间的变换
5.2.3.2 models.keyedVectors
  • 该模块实现了词向量及其相似性查找
  • 其结构实质上是实体和向量之间的映射,每个实体由其字符串id标识,因此是字符串和一维数组之间的映射关系
  • 和完整模型的区别在于无法进一步的训练,有更小的RAM占用及更简单的接口
  • 这里keyedVectors应用于从磁盘加载词向量文件

当AC Tree的匹配都没有匹配到实体时,使用查找相似词的方式进行实体匹配

  • find_sim_words:导入jieba和映射词表,对用户输入的question进行预处理(删除无用的符号),对处理后的question进行jieba分词,以及判定不在停用词中。for循环对每个分词与已经保存的Disease, Alias, Symptom, Complication调用simCal函数进行相似度计算得分,排序每一个分词的分数取分数最高的类别,返回。
  • simCal:输入是一个分词,一个实体(及其标识)。使用3种分数: overlap score(相同字符的个数/min(|A|,|B|),实体词存在于该实体类别的词表中,则加1,最后求平均), 余弦相似度分数(调用keyedVectors中的方法), 编辑距离分数editDistanceDP。3种分数求平均,如果超过0.7,最后求4种实体中的排序分数最高的进行返回
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

  • EditDistanceDP,定义:从字符串A到字符串B中间需要的最少操作权重,操作权重一般是:
  • 1). 删除A末尾一个字符(deletion)
  • 2). 用B末尾插入A末尾一个字符(insertion)
  • 3). 用A末尾字符替换成B末尾的一个字符(substitution)
        def editDistanceDP(self, s1, s2):
        """
        采用DP方法计算编辑距离
        :param s1:
        :param s2:
        :return:
        """
        m = len(s1)
        n = len(s2)
        solution = [[0 for j in range(n + 1)] for i in range(m + 1)]
        for i in range(len(s2) + 1):
            solution[0][i] = i
        for i in range(len(s1) + 1):
            solution[i][0] = i

        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if s1[i - 1] == s2[j - 1]:
                    solution[i][j] = solution[i - 1][j - 1]
                else:
                    solution[i][j] = 1 + min(solution[i][j - 1], min(solution[i - 1][j],
                                                                     solution[i - 1][j - 1]))
        return solution[m][n]

六、意图识别任务实践

6.1 意图识别整体思路介绍

  • step 1:利用TF-IDF表征文本特征,同时构建一些人工特征(每一类意图常见词在句子中出现的个数);
  • step 2:训练朴素贝叶斯模型进行意图识别任务;
  • step 3:使用实体信息进行意图的纠正和补充。

图 7 意图识别整体举例介绍

该项目通过手工标记210条意图分类训练数据,并采用朴素贝叶斯算法训练得到意图分类模型。其最佳测试效果的F1值达到了96.68%。

6.2 意图识别整体思路介绍

6.2.1 特征构建
  1. TF-IDF特征
# 提取问题的TF-IDF特征
def tfidf_features(self, text, vectorizer):
    """
    提取问题的TF-IDF特征
    :param text:
    :param vectorizer:
    :return:
    """
    jieba.load_userdict(self.vocab_path)
    # 把用户question先用jieba分词,然后判断分词是否存在且不在停用词中
    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
  1. 人工特征

定义可枚举的列表,包含询问的症状、治疗方法、治疗周期等
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)
提取问题的关键词特征,分别判断上述7种类型的词是否出现在用户问句中,并做统计(出现在该类的词的数量),
然后做归一化操作,即(features的数值 - min(features)/(max(features) - min(features)))

6.2.2 使用朴素贝叶斯进行文本分类
  • 项目没有给出训练过程,可参考下面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])
  • 实体抽取主函数
    def extractor(self, question):
        self.entity_reg(question)   # 模式匹配初步判断是否有结果
        if not self.result:     # 如果模式匹配没有结果,则使用find_sim_words()函数找相似词
            self.find_sim_words(question)

        types = []  # 实体类型
        for v in self.result.keys():
            types.append(v)

        intentions = []  # 查询意图

        # 意图预测
        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
        return self.result

后续就是通过上述得到的意图信息和实体信息选择对应的模版,并将实体信息填充入组成查询语句进行数据库查询。

参考资料

  1. QASystemOnMedicalGraph
  2. NLP工具——Gensim的model.keyedvectors模块
  3. Edit Distance(编辑距离)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值