医疗知识图谱的问答系统详解

news/2024/12/2 21:20:55/

一、项目介绍

该项目的数据来自垂直类医疗网站寻医问药,使用爬虫脚本data_spider.py,以结构化数据为主,构建了以疾病为中心的医疗知识图谱,实体规模4.4万,实体关系规模30万。schema的设计根据所采集的结构化数据生成,对网页的结构化数据进行xpath解析。

项目的数据存储采用Neo4j图数据库,问答系统采用了规则匹配方式完成,数据操作采用neo4j声明的cypher。

项目的不足之处在于疾病的引发原因、预防等以大段文字返回,这块可引入事件抽取,可将原因结构化表示出来。

项目主要文件目录如下:

python">├── QASystemOnMedicalKG├── answer_search.py               # 问题查询及返回├── build_medicalgraph.py          # 将结构化json数据导入neo4j├── chatbot_graph.py               # 问答程序脚本├── QASystemOnMedicalKG/data├── hepatopathy.json           # 肝病知识数据├── medical.json               # 全科知识数据├── QASystemOnMedicalKG/dict├── check.txt                  # 诊断检查项目实体库├── deny.txt                   # 否定词库├── department.txt             # 医疗科目实体库├── disease.txt                # 疾病实体库├── drug.txt                   # 药品实体库├── food.txt                   # 食物实体库├── producer.txt               # 在售药品库├── symptom.txt                # 疾病症状实体库├── QASystemOnMedicalKG/prepare_data├── build_data.py              # 数据库操作脚本├── data_spider.py             # 数据采集脚本├── max_cut.py                 # 基于词典的最大前向/后向匹配├── question_classifier.py         # 问句类型分类脚本├── question_parser.py             # 问句解析脚本

二、爬虫部分

爬虫部分我没有实际操作,简单看了一下源码。

数据来源为寻医问药网的疾病百科 http://jib.xywy.com/ 。点入具体的疾病页面如下:

爬取疾病介绍页的简介、病因、预防、症状、检查、治疗、并发症、饮食保健等详情页的内容。

爬虫模块使用的是urllib库,数据存在MongoDB数据库中。

其中并发症使用了自己写的max_cut匹配脚本中的双向最大向前匹配max_biward_cut。

三、知识库部分

知识库包含7类规模为4.4万的知识实体,11类规模约30万实体关系,具体如下:

 

(注意:belongs_to包括 科室属于科室 和 疾病属于科室 两种关系)

(注意:疾病的属性还包括cure_department)

知识库的构建是通过build_medicalgraph.py脚本实现。

build_medicalgraph.py

该脚本构建了一个MedicalGraph类,定义了Graph类的成员变量g和json数据路径成员变量data_path。

python">class MedicalGraph:def __init__(self):cur_dir = '\\'.join(os.path.abspath(__file__).split('\\')[:-1])   # 获取当前绝对路径的上层目录 linux中应用'/'split和joinself.data_path = os.path.join(cur_dir, 'data\hepatopathy.json')   # 获取json文件路径self.g = Graph(host="127.0.0.1",  # neo4j 搭载服务器的ip地址,ifconfig可获取到http_port=7474,  # neo4j 服务器监听的端口号user="neo4j",  # 数据库user name,如果没有更改过,应该是neo4jpassword="******")

类中的函数如下:

read_nodes函数: 读取数据文件
定义节点变量(list类型)
disease_infos包含了所有的疾病信息,为元素为disease_dict(dict类型)的list

定义节点实体关系变量(list类型)

逐行读取json数据,每行一个disease_dict(dict类型),包含疾病的各种属性(注意:除上述8种属性还有cure_department和symptom两种实体也列入疾病dict里)

对于json里的字典键,如果是疾病的属性,则加入disease_dict中:

disease_dict['desc'] = data_json['desc']

如果和疾病有关系,则加入对应的关系列表:

 for acompany in data_json['acompany']:
     rels_acompany.append([disease, acompany])

如果是某个其他实体,则加入对应的实体列表:

check = data_json['check']
checks += check

注意:

symptoms既是疾病的属性,又有疾病—症状的关系。

cure_department在json中有两种形式,除了添加cure_department属性到disease_dict实体字典里和departments实体列表里。还需要提取关系,如果只有一个科室则直接提取疾病—科室关系(rels_category),如果有两个科室,还需要提取科室—科室关系(rels_department)。

python">        if 'cure_department' in data_json:cure_department = data_json['cure_department']if len(cure_department) == 1:rels_category.append([disease, cure_department[0]])if len(cure_department) == 2:big = cure_department[0]small = cure_department[1]rels_department.append([small, big])      # 提取科室——科室关系rels_category.append([disease, small])disease_dict['cure_department'] = cure_departmentdepartments += cure_department

drug_details的形式为"drug_detail" : [ "惠普森穿心莲内酯片(穿心莲内酯片)", "北京同仁堂百咳静糖浆(百咳静糖浆)"],即包括药品名和生产厂商,因为字符串和括号的原因,提取药品—在售药品的关系的方式略有不同:

python">        if 'drug_detail' in data_json:drug_detail = data_json['drug_detail']producer = [i.split('(')[0] for i in drug_detail]rels_drug_producer += [[i.split('(')[0], i.split('(')[-1].replace(')', '')] for i in drug_detail]producers += producer

函数返回set去重后的所有实体、疾病属性信息和实体间关系。

create_graphnodes函数:创建知识图谱实体节点类型schema

首先调用read_nodes函数得到存储实体和实体间关系的变量。

知识图谱中主要包含两类节点,一类为中心疾病节点,包含各种疾病属性;一类为普通实体节点,即药品、食物等,不包含属性,分别调用以下两个函数创建:

create_diseases_nodes函数:创建知识图谱中心疾病的节点

对每一条disease_infos,调用py2neo库中Graph类的create函数,在neo4j中创建label为"Disease"的Node,disease_dict中的属性即为节点中的属性。

python">node = Node("Disease", name=disease_dict['name'], desc=disease_dict['desc'],prevent=disease_dict['prevent'] ,cause=disease_dict['cause'],easy_get=disease_dict['easy_get'],cure_lasttime=disease_dict['cure_lasttime'],cure_department=disease_dict['cure_department'],cure_way=disease_dict['cure_way'] , cured_prob=disease_dict['cured_prob'])
self.g.create(node)

create_node函数:创建普通实体节点模块

对每一类实体,在neo4j中创建label为实体类别,name为具体实体名称的节点。

python">        for node_name in nodes:node = Node(label, name=node_name)self.g.create(node)

create_graphrels函数:创建实体关系边

同样调用read_nodes函数得到存储实体和实体间关系的变量。

再对模块函数create_relationship传入不同的变量参数,创建11类实体关系边。

create_relationship函数:创建实体关联边模块

首先对存储实体关系的list变量进行去重,因为实体关系为形如[[“a”,“b”],[“c”,“d”]]的嵌套list,无法直接用set去重,所以先将嵌套内层的list转为字符串,再用set。

去重后调用py2neo库中Graph类的run函数,使用Cypher语言直接执行Neo4j CQL语句,对每一对实体关系在neo4j里创建边:

python">query = "match(p:%s),(q:%s) where p.name='%s'and q.name='%s' create (p)-[rel:%s{name:'%s'}]->(q)" % (start_node, end_node, p, q, rel_type, rel_name)
try:self.g.run(query)count += 1print(rel_type, count, all)
except Exception as e:print(e)

export_data函数:导出数据到txt

调用read_nodes函数得到存储实体和实体间关系的变量。
逐行写入各变量对应的txt。

四、问答部分

问答系统支持的问答类型

 本项目的问答系统完全基于规则匹配实现,通过关键词匹配,对问句进行分类,医疗问题本身属于封闭域类场景,对领域问题进行穷举并分类,然后使用cypher的match去匹配查找neo4j,根据返回数据组装问句回答,最后返回结果。

问答框架的构建是通过chatbot_graph.py、answer_search.py、question_classifier.py、question_parser.py等脚本实现。

chatbot_graph.py

首先从需要运行的chatbot_graph.py文件开始分析。

该脚本构造了一个问答类ChatBotGraph,定义了QuestionClassifier类型的成员变量classifier、QuestionPase类型的成员变量parser和AnswerSearcher类型的成员变量searcher。

python">class ChatBotGraph:def __init__(self):self.classifier = QuestionClassifier()self.parser = QuestionPaser()self.searcher = AnswerSearcher()

该问答类的成员函数仅有一个chat_main函数

chat_main函数

首先传入用户输入问题,调用self.classifier.classify进行问句分类,如果没有对应的分类结果,则输出模板句式。如果有分类结果,则调用self.parser.parser_main对问句进行解析,再调用self.searcher.search_main查找对应的答案,如果有则返回答案,如果没有则输出模板句式。

python">def chat_main(self, sent):answer = '您好,我是肝病问答小助手,希望可以帮到您。祝您身体棒棒!'res_classify = self.classifier.classify(sent)if not res_classify:return answerres_sql = self.parser.parser_main(res_classify)final_answers = self.searcher.search_main(res_sql)if not final_answers:return answerelse:return '\n'.join(final_answers)

由此可以看出,问答框架包含问句分类、问句解析、查询结果三个步骤,具体一步步分析。

首先是问句分类,是通过question_classifier.py脚本实现的。

question_classifier.py

该脚本构造了一个问题分类的类QuestionClassifier,定义了特征词路径、特征词、领域actree、词典、问句疑问词等成员变量。

特征词除了7类实体还包括由全部7类实体词构成的领域词region_words、否定词库deny_words。

构建领域actree通过调用self.build_actree实现。

构建词典通过调用self.build_wdtype_dict()实现。

问句疑问词包含了疾病的属性和边相关的问题词,参考上文中问答系统支持的问答类型

build_actree函数

该函数构建领域actree,加速过滤。通过python的ahocorasick库实现。

ahocorasick是一种字符串匹配算法,由两种数据结构实现:trie和Aho-Corasick自动机。

Trie是一个字符串索引的词典,检索相关项时时间和字符串长度成正比。

AC自动机能够在一次运行中找到给定集合所有字符串。AC自动机其实就是在Trie树上实现KMP,可以完成多模式串的匹配。

具体ahocorasick用法非本文重点,可参考https://blog.csdn.net/pirage/article/details/51657178等博文。

python">    def build_actree(self, wordlist):actree = ahocorasick.Automaton()         # 初始化trie树for index, word in enumerate(wordlist):actree.add_word(word, (index, word))     # 向trie树中添加单词actree.make_automaton()    # 将trie树转化为Aho-Corasick自动机return actree

build_wdtype_dict函数

该函数根据7类实体构造 {特征词:特征词对应类型} 词典。

python">wd_dict = dict()
for wd in self.region_words:wd_dict[wd] = []if wd in self.disease_wds:wd_dict[wd].append('disease')...

check_medical函数

通过ahocorasick库的iter()函数匹配领域词,将有重复字符串的领域词去除短的,取最长的领域词返回。功能为过滤问句中含有的领域词,返回{问句中的领域词:词所对应的实体类型}。

python">def check_medical(self, question):region_wds = []for i in self.region_tree.iter(question):   # ahocorasick库 匹配问题  iter返回一个元组,i的形式如(3, (23192, '乙肝'))wd = i[1][1]      # 匹配到的词region_wds.append(wd)stop_wds = []for wd1 in region_wds:for wd2 in region_wds:if wd1 in wd2 and wd1 != wd2:stop_wds.append(wd1)       # stop_wds取重复的短的词,如region_wds=['乙肝', '肝硬化', '硬化'],则stop_wds=['硬化']final_wds = [i for i in region_wds if i not in stop_wds]     # final_wds取长词final_dict = {i:self.wdtype_dict.get(i) for i in final_wds}return final_dict

check_word函数

该函数检查问句中是否含有某实体类型内的特征词。

python">def check_words(self, wds, sent):for wd in wds:if wd in sent:return Truereturn False

classify函数

该函数为分类主函数。

首先调用check_medical函数,获取问句中包含的领域词及其所在领域,并收集问句当中所涉及到的实体类型;

接着基于特征词进行分类,即调用check_word函数,看问句中是否包含某领域特征词,以及该领域是否在问句中包含的region_words的实体类型(types)里,以此来判断问句属于哪种类型。(好绕)

示例如下:

python"># 症状
if self.check_words(self.symptom_qwds, question) and ('disease' in types):question_type = 'disease_symptom'question_types.append(question_type)if self.check_words(self.symptom_qwds, question) and ('symptom' in types):question_type = 'symptom_disease'question_types.append(question_type)
python">#已知食物找疾病
if self.check_words(self.food_qwds+self.cure_qwds, question) and 'food' in types:deny_status = self.check_words(self.deny_words, question)if deny_status:question_type = 'food_not_disease'else:question_type = 'food_do_disease'question_types.append(question_type)

如果没有查到若没有查到相关的外部查询信息,且类型为疾病,那么则将该疾病的描述信息返回(question_types = ['disease_desc']);若类型为症状,那么则将该症状的对应的疾病信息返回(question_types = ['symptom_disease'])。

然后将分类结果进行合并处理,组装成一个字典返回。

注意:

  • 食物相关的问题需要检查否定词self.deny_words来判断是do_eat还是not_eat。
  • 已知食物找疾病和已知检查项目查相应疾病的时候,check_words需要加上self.cure_qwds。

question_parser.py

问句分类后需要对问句进行解析。

该脚本创建一个QuestionPaser类,该类包含三个成员函数。

build_entitydict函数

例如:从分类结果的{'args': {'头痛': ['disease', 'symptom']}, 'question_types': ['disease_cureprob']}中获取args,返回{'disease': ['头痛'], 'symptom': ['头痛']}的形式。

sql_transfer函数

该函数真的不同的问题类型,转换为Cypher查询语言并返回。
例如:

# 查询疾病的原因
if question_type == 'disease_cause':
    sql = ["MATCH (m:Disease) where m.name = '{0}' return m.name, m.cause".format(i) for i in entities]

# 查询疾病的忌口

elif question_type == 'disease_not_food':
    sql = ["MATCH (m:Disease)-[r:no_eat]->(n:Food) where m.name = '{0}' return m.name, r.name, n.name".format(i) for i in entities]

注意:

查询可能为查询中心疾病节点的属性,也可能为查询关联边。
疾病的并发症需要双向查询。
建议吃的东西包括do_eat和recommand_eat两种关联边。
查询药品相关记得扩充药品别名,包括common_drug和recommand_durg两种关联边。
parser_main函数
该函数为问句解析主函数。
首先传入问句分类结果,获取问句中领域词及其实体类型。
接着调用build_entitydict函数,返回形如{'实体类型':['领域词'],...}的entity_dict字典。
然后对问句分类返回值中[‘question_types’]的每一个question_type,调用sql_transfer函数转换为neo4j的Cypher语言。
最后组合每种question_type转换后的sql查询语句。

answer_search.py
问句解析之后需要对解析后的结果进行查询。
该脚本创建了一个AnswerSearcher类。与build_medicalgraph.py类似,该类定义了Graph类的成员变量g和返回答案列举的最大个数num_list。
该类的成员函数有两个,一个查询主函数一个回复模块。

search_main函数
传入问题解析的结果sqls,将保存在queries里的[‘question_type’]和[‘sql’]分别取出。
首先调用self.g.run(query).data()函数执行[‘sql’]中的查询语句得到查询结果,
再根据[‘question_type’]的不同调用answer_prettify函数将查询结果和答案话术结合起来。
最后返回最终的答案。

answer_prettify函数
该函数根据对应的qustion_type,调用相应的回复模板。
示例如下:

python">elif question_type == 'disease_cause':desc = [i['m.cause'] for i in answers]subject = answers[0]['m.name']final_answer = '{0}可能的成因有:{1}'.format(subject, ';'.join(list(set(desc))[:self.num_limit]))

五、改进

1. 缺失实体填充

在用户连续提问的时候,缺失使用上轮对话的疾病,如:

python">用户:乙肝怎么治
小助手: 乙肝可以尝试如下治疗:药物治疗;支持性治疗;对症治疗
用户:那有什么忌口吗
小助手: 乙肝忌食的食物包括有:咸鱼;咸鸭蛋;鸭血(白鸭);啤酒

这里用户的第二个问题没有疾病实体,默认采用上一轮的疾病实体。

方法是在question_classifier.py的check_medical函数里增加全局变量:

python">global diseases_dict
if final_dict:diseases_dict = final_dict

并在classify函数里判断:

python">if not medical_dict:if 'diseases_dict' in globals():    # 判断是否是首次提问,若首次提问,则diseases_dict无值medical_dict = diseases_dictelse:return {}

2. 增加疾病属性can_eat

增加了一个疾病属性:can_eat,对应增加了一个问题分类:

python"># 推荐食品
if self.check_words(self.food_qwds, question) and 'disease' in types:deny_status = self.check_words(self.deny_words, question)if deny_status:question_type = 'disease_not_food'else:question_type = 'disease_do_food'if self.check_words(['能吃','能喝','可以吃','可以喝'], question):question_types.append('disease_can_eat')print(question_type)question_types.append(question_type)

从构建知识图谱到问句分类、问句解析、查询结果也需作出相应修改。

3.补充个别问句疑问词

使覆盖的问句更全,详见修改版github。

六、总结

基于规则的问答系统没有复杂的算法,一般采用模板匹配的方式寻找匹配度最高的答案,回答结果依赖于问句类型、模板语料库的覆盖全面性,面对已知的问题,可以给出合适的答案,对于模板匹配不到的问题或问句类型,经常遇到的有三种回答方式:

  1. 给出一个无厘头的答案;
  2. 婉转的回答不知道,提示用户换种方式去问;
  3. 转移话题,回避问题;

基于知识图谱的问答系统的主要特征是知识图谱,系统依赖一个或多个领域的实体,并基于图谱进行推理或演绎,深度回答用户的问题,基于知识图谱的问答系统更擅长回答知识性问题,与基于模板的聊天机器人有所不同的是它更直接、直观的给用户答案。对于不能回答、或不知道的问题,一般直接返回失败,而不是转移话题避免尴尬。

整个问答系统的优劣依赖于知识图谱中知识的数量与质量。也算是利弊共存吧!知识图谱图谱具有良好的可扩展性,扩展了知识图谱也就是扩展了问答系统的知识库。如果问句在射程范围内,可轻松回答,但如果不幸脱靶,则体验大打折扣。

从知识图谱的角度分析,大多数知识图谱规模不足,主要原因还是数据来源以及技术上知识的抽取与推理困难。


http://www.ppmy.cn/news/1551848.html

相关文章

使用CLIP大模型实现视频定位:从理论到实践

使用CLIP大模型实现视频定位:从理论到实践 引言 随着多媒体内容的爆炸式增长,如何高效地从海量视频中定位和检索特定内容成为了一个重要的研究课题。传统的视频检索方法通常依赖于人工标注的元数据或基于视觉特征的匹配,这些方法在处理大规模数据时存在效率低下、准确率不…

农业强国助农平台:科技赋能,助力乡村振兴

在数字化转型的大潮中,农业作为国民经济的基础产业,也在积极探索着属于自己的数字化转型之路。2025年,随着“农业强国助农平台”的正式上线运营,一场以科技为驱动的助农行动正在全国范围内如火如荼地展开。这一平台由财政部“农业…

qml项目创建的区别

在Qt框架中,你可以使用不同的模板来创建应用程序。你提到的这几个项目类型主要针对的是Qt的不同模块和用户界面技术。下面我将分别解释这些项目类型的区别: 根据你提供的信息,以下是每个项目模板的详细描述和适用场景: Qt Widgets…

解锁软件构建的艺术:六种架构模式的解析

一、概述 软件架构是构建软件系统的核心,它规定了系统的组织结构、组件行为以及组件间的交互方式。正确选择架构对系统的性能、可维护性和可扩展性至关重要。 二、架构类型详解与技术选型 1.分层架构(Layered Architecture) 场景示例&#…

UE5_建立自己的资产库

资产库需要用到一个插件: UAsset Browser - 直接在当前项目预览其他UE项目资产(.uasset 文件) - 直接迁移其他UE项目资产到当前项目 - 不用另外打开资产项目查看资产,迁移资产(麻烦) 插件官网插件文档插…

uniapp中父组件调用子组件方法

实现过程&#xff08;setup语法糖形式下&#xff09; 在子组件完成方法逻辑&#xff0c;并封装。在子组件中使用defineExpose暴露子组件的该方法。在父组件完成子组件ref的绑定。通过ref调用子组件暴露的方法。 子组件示例 <template> </template><script se…

网易博客旧文-----安卓界面代码例子研究(三)

安卓界面代码例子研究&#xff08;三&#xff09; 2014-03-19 14:01:17| 分类&#xff1a; 安卓开发 | 标签&#xff1a; |举报 |字号大中小 订阅 续三 本文对安卓SDK带的例子做了研究&#xff0c;主要讲运行后的界面抓取出来&#xff0c;供以后使用的时候参考。 一、文字切换…

机器学习之RLHF(人类反馈强化学习)

RLHF(Reinforcement Learning with Human Feedback,基于人类反馈的强化学习) 是一种结合人类反馈和强化学习(RL)技术的算法,旨在通过人类的评价和偏好优化智能体的行为,使其更符合人类期望。这种方法近年来在大规模语言模型(如 OpenAI 的 GPT 系列)训练中取得了显著成…