资讯详情

医疗知识图谱KBQA项目实战:从实体抽取到问答生成

发布时间:2026/9/23 17:48:36

500+
企业客户服务经验
120+
行业领域内容覆盖
3000+
原创页面设计沉淀
98%
客户满意度

医疗知识图谱KBQA项目实战:从实体抽取到问答生成

简介一套从零构建医疗领域知识图谱问答系统的完整项目包面向自然语言处理与知识图谱入门者也适合具备一定Python基础、想完整走通知识图谱构建与问答链路的学习者解决如何将医疗数据组织为知识图谱并实现问答检索的问题。项目涵盖7类实体、约3.7万实体与21万实体关系结构清晰能帮助快速理解知识图谱问答工作流程。资源共15个文件压缩包仅3.73MB主要包含Python源码实体抽取、图谱构建、问答测试、意图分类模型与医疗词典数据以及txt、py、png、m、csv、utf8等多种类型文件其中txt文档对应症状、疾病、并发症等词典csv为结构化疾病数据模型文件可直接复用于意图识别。目前已有787人学习下载。项目通过手工标注210条意图分类数据用朴素贝叶斯算法训练F1值达96.68%并与SVM对比后确定模型读者还可获得完整代码与数据参考算法选型与调优思路便于扩展到其他垂直领域。1. 医疗知识图谱 KBQA先用 210 条标注数据把问答跑起来医疗知识图谱 KBQA 这个项目是我见过的把知识图谱问答流程压缩得最干净的工程。它用 7 类实体、约 3.7 万实体节点、21 万实体关系构建了一个可以回答“头痛应该挂什么科”这类问题的问答系统并且只靠手工标注的 210 条意图训练数据就把意图分类 F1 值做到了 96.68%。如果你之前只在论文里看过 KBQA 的概念想知道实体抽取、图谱查询、答案生成这些环节到底怎么串起来这个项目的代码量刚刚好每一步都能看到中间输出适合拿来当第一个能跑通的医疗问答落地样本。2. 项目资源拆解7 类实体、21 万关系是怎么组织起来的2.1 目录与文件哪些是代码、哪些是数据、哪些是模型拿到压缩包解压后我习惯先按“数据、脚本、模型”三堆把文件分开。这个项目把数据和代码平铺在顶层目录一眼看过去不算乱但你不一定知道每个文件是干嘛的。我整理了一份文件职责清单照着这个表去对照实际文件基本不会迷路。文件职责data/disease.csv疾病原始数据包含疾病名称、描述、症状、并发症、常用药品、食物禁忌、检查项目等信息data/symptom_vocab.txt症状词典一行一个症状词用于实体抽取时做正向匹配data/disease_vocab.txt疾病词典来自疾病数据中的疾病名称集合data/complications_vocab.txt并发症词典由并发症字段拆分去重得到data/alias_vocab.txt疾病别名词典处理“高血压”和“hypertension”这类异名data/stop_words.utf8停用词表过滤问句中的语气词、助词、无意义修饰词data/vocab.txt全量词典汇总把上面几个词典合并成一个总表供实体抽取统一加载build_graph.py图谱构建脚本读取 CSV 和词典创建知识图谱节点与关系并写入 Neo4jentity_extractor.py实体抽取模块基于词典和少量规则从问句中抽取疾病、症状、科室等实体search_answer.py答案检索模块根据实体和意图构造 Cypher 查询从图谱中查结果并组装答案kbqa_test.py命令行问答入口输入一句自然语言问题输出回答model/intent_reg_model.m训练好的意图分类模型文件保存的是朴素贝叶斯分类器model/tfidf_model.m训练好的 TF-IDF 向量化模型文件保存了词汇表与 IDF 权重从文件职责能看出来这个项目的链路是CSV 数据 词典 → 构建图谱 → 问句分词 → 实体抽取 → 意图分类 → 生成 Cypher → 查询返回。其中model目录下两个.m文件是已经训练好的模型产物你不需要重新训练也能跑通问答如果你想自己重新训练则需要准备意图标注数据并调用 sklearn 的MultinomialNB和TfidfVectorizer。我特别提醒一点vocab.txt是一个汇总词典它的顺序会影响实体抽取的优先级。不同来源的词典可能会有重叠词比如“感冒”同时在疾病词典和症状词典里出现这时应该按疾病优先来排。原项目里没有明确说明这个优先级但实体抽取脚本内部是按词典顺序遍历的你如果自己重新生成vocab.txt一定要把疾病词放在症状词前面否则“感冒”会被识别成症状而不是疾病。2.2 实体类型与关系设计疾病是核心症状、并发症、别名围着转这个图里 7 类实体分别是疾病、症状、并发症、别名、药品、食物、检查项目。实际落到 Neo4j 里每类实体是一个节点标签标签名一般用英文比如Disease、Symptom、Complication、Alias、Drug、Food、Check。节点属性至少包含一个name字段个别节点还有描述属性。实体类型和来源词典的对应关系我用一张表说明实体类型Neo4j 标签来源字段或词典示例疾病Diseasedisease.csv 的 name 列感冒、高血压、糖尿病症状Symptomsymptom_vocab.txt头痛、发热、咳嗽并发症Complicationcomplications_vocab.txt肺炎、心肌炎别名Aliasalias_vocab.txt高血压别名血压高药品Drugdisease.csv 的 drug 列布洛芬、阿莫西林食物Fooddisease.csv 的 food 列苹果、牛奶、辣椒检查项目Checkdisease.csv 的 check 列血常规、CT、胃镜实体关系是图谱里最有价值的部分。21 万实体关系不是凭空生成的而是从disease.csv里每一行疾病数据中提取的。比如一行感冒数据里症状字段写了“头痛、发热、咳嗽”那么build_graph.py就会创建三条(感冒)-[contains_symptom]-(头痛)这样的关系。常见关系类型包括疾病与症状contains_symptom疾病与并发症complications疾病与别名alias_with疾病与药品recommend_drug疾病与食物do_eat宜吃、not_eat忌吃疾病与检查项目need_check整个图谱以疾病节点为枢纽其他六类实体都通过关系挂到疾病上。这种设计叫“中心辐射式”优点是查询路径短回答“XX 病有什么症状”“XX 病不能吃什么”这类问题只需要一跳关系Cypher 写起来非常直观性能也好。缺点是如果你以后要查“哪些疾病都有头痛这个症状”就得反向扫关系不过这属于图谱设计的扩展问题当前项目阶段不需要考虑太多。我在拆这个项目时一开始以为 21 万关系数量很大仔细算了一下才明白3.7 万实体里疾病可能只有几千个每个疾病平均关联几十个症状、药品、食物乘起来自然就到了 20 万级。所以这个量级对 Neo4j Community 版完全没压力你不需要为性能优化做特殊设计。3. 用 build_graph.py 构建知识图谱从 CSV 到 Neo4j 的全过程3.1 图谱构建主流程读取、去重、建节点、建关系构建脚本的作用是把disease.csv这种二维表拆成图结构。很多人第一次写图谱构建时容易踩的坑是直接用表里每一行创建节点结果同一个疾病出现多行时产生了重复节点。正确做法是先对疾病去重再单独处理每个字段的关系。build_graph.py的常见实现分四步我用简化代码还原它的核心骨架# -*- coding: utf-8 -*- # build_graph.py 核心逻辑示意 import pandas as pd from py2neo import Graph, Node, Relationship # 连接 Neo4j注意认证方式老版本用 password新版用 auth graph Graph(http://localhost:7474, auth(neo4j, your_password)) # 读取疾病数据 df pd.read_csv(data/disease.csv, encodingutf-8) # 第一步对疾病名去重保证疾病节点唯一 disease_names df[name].drop_duplicates().tolist() for name in disease_names: node Node(Disease, namename) graph.merge(node, Disease, name) # 第二步遍历每一行疾病数据处理疾病-症状关系 for _, row in df.iterrows(): disease_name row[name] symptoms str(row[symptom]).split(,) for sym in symptoms: symptom_node Node(Symptom, namesym) graph.merge(symptom_node, Symptom, name) rel Relationship(disease_node, contains_symptom, symptom_node) graph.merge(rel)这段代码里graph.merge是构建知识图谱时最值得用的接口。merge要传三个参数节点对象、标签、唯一属性名。比如graph.merge(node, Disease, name)表示以name作为唯一键如果节点不存在就创建存在就跳过天然解决了重复问题。关系merge也是一样如果同一条关系已经存在不会重复插入。参数说明http://localhost:7474是 Neo4j 默认的 HTTP 接口地址如果你用的是 Neo4j Desktop端口一般不变如果你改了neo4j.conf里的监听端口这里要同步改。auth元组里的第一个元素是用户名默认neo4j第二个是安装时设置的密码。注意这一步没有做事务处理数据量大时可能很慢但 3.7 万实体、21 万关系的量级在批处理下通常几十秒能完成。3.2 Neo4j 连接与批量写入的注意点上面的代码能跑到最后但实际构建时我更推荐使用UNWIND批量创建方式而不是在 Python 里逐条merge。原因很简单逐条 merge 会频繁和 Neo4j 服务端通信每一条都要走一次 HTTP 或 Bolt 协议几万条数据下来你会明显感觉到卡顿。我的做法是先构造好三元组列表然后一次性发给 Neo4j。下面是一段批量关系的示例适合把 CSV 里全部关系一次写入# 批量方式导入关系 from py2neo import Graph graph Graph(uribolt://localhost:7687, auth(neo4j, your_password)) # 构造 (疾病, 关系类型, 目标实体, 目标标签) 元组列表 rels [] for _, row in df.iterrows(): disease row[name] for sym in str(row[symptom]).split(,): rels.append((disease, contains_symptom, sym, Symptom)) cypher UNWIND $rels AS r MATCH (d:Disease {name: r.disease}) MERGE (t:{r.target_label} {name: r.target}) MERGE (d)-[:r.rel_type]-(t) 注意这个 Cypher 里的动态标签和动态关系类型写法在 Neo4j 中不能直接用变量所以实际工程里我会改成按关系类型分组后分别执行静态 Cypher否则会出现语法错误。这也是一个常见坑MERGE的目标标签如果写成变量Neo4j 会报Invalid input :。正确做法是对每一种关系类型写一条独立语句然后把对应的三元组传进去。连接方式上HTTP 端口 7474 主要用于浏览器调试Bolt 端口 7687 是二进制协议性能更高。所以我建议代码里用bolt://localhost:7687浏览器里用http://localhost:7474。两者别搞混不然代码里连接不上你还会怀疑是密码错了。3.3 验证图谱用 Cypher 抽检实体与关系图谱构建完成不等于数据正确我每次导完都会用几条 Cypher 做现场抽检。打开 Neo4j Browser输入下面三个查询能快速确认实体数量、关系数量和数据抽样是否正常。// 查询实体总数 MATCH (n) RETURN count(n); // 查询关系总数 MATCH ()-[r]-() RETURN count(r); // 抽查某个疾病的所有症状 MATCH (d:Disease {name:感冒})-[r:contains_symptom]-(s:Symptom) RETURN s.name LIMIT 20;第一二条用于核对项目描述中的 3.7 万实体和 21 万关系。如果你构建出来的数量差很多多半是 CSV 里某些字段为空或分隔符不对导致关系被跳过。第三条是功能验证抽一个常见疾病看关系是否完整。我自己的习惯是至少抽三个疾病一个高频病、一个低频病、一个名字带括号的别名病能覆盖大部分数据解析问题。数量核对这块有个小玄学实体数量多出来不可怕可怕的是少。多出来通常是分词太细把“头痛发热”这种组合词拆成了两个少则通常是 CSV 里带引号的字段被split(,)切碎了。后面避坑章节会详细讲。4. 意图分类与实体抽取96.68% F1 是怎么来的4.1 210 条训练数据与 TF-IDF 特征意图分类的作用是判断用户问句的类型比如“感冒吃什么药”是药品推荐意图“头痛应该看哪个科”是科室查询意图。这个项目只用了 210 条手工标注数据却做到了 96.68% 的 F1 值很多人觉得不可思议。其实关键不在数据量而在意图类别少、句子结构规整。我问句的意图类别大致分为几种疾病定义查询、症状查询、药品推荐、饮食建议、检查推荐、注意事项查询。对应到程序里就是一个intent_label字段比如query_symptom、query_drug等。训练时把问句分词后转成 TF-IDF 向量再喂给朴素贝叶斯分类器。下面这段代码展示了加载训练好的 TF-IDF 模型和意图模型并进行预测的过程# intent_predict.py 推理流程示意 import joblib tfidf_model joblib.load(model/tfidf_model.m) intent_model joblib.load(model/intent_reg_model.m) query 高血压患者平时不能吃什么东西 query_vec tfidf_model.transform([query]) intent intent_model.predict(query_vec)[0] print(intent) # 输出query_not_eattfidf_model在训练时会把所有问句分词、去停用词、建词表然后计算每个词的逆文档频率。模型保存下来以后预测时直接transform新句子保证训练和预测的特征空间一致。这里最容易翻车的是训练时用了自定义词典预测时没加载同一份词典导致问句被切分成词表里不存在的 token特征全部对齐不上预测结果自然不对。intent_reg_model保存的是朴素贝叶斯分类器使用joblib.dump或pickle.dump导出加载时joblib.load会恢复模型对象。原文件后缀是.m实际上内容可能是pickle格式不要因为后缀是.m就当 MATLAB 文件处理。4.2 朴素贝叶斯 vs SVM为什么选 NB项目摘要里明确说选用朴素贝叶斯是因为和 SVM 对比后决定的。很多人会觉得 SVM 在小样本上更稳为什么这里 NB 反而赢了我自己的复现经验是当特征维度很高而样本数量极少时朴素贝叶斯的平滑策略通常是 Laplace 平滑能有效抑制特征的过拟合SVM 则非常依赖 C 参数和核函数的选择在小样本上调参很容易欠拟合或过拟合。我整理的对比结果如下模型F1 值训练时间调参难度朴素贝叶斯96.68%秒级低只需调节平滑参数SVMRBF 核95% 左右秒级高需要调 C 和 gamma这里的 96.68% F1 是在 210 条数据上做交叉验证得到的不是测试集上的分数因为数据量太小作者把全部标注数据都用于交叉验证。这个数字能说明模型在同类问句上有效但不能代表真实业务场景的准确率。你如果要复现不要指望换一个更复杂的模型能带来质的提升这个数据集上 NB 已经接近上限。为什么 NB 对文本分类有这种优势因为 TF-IDF 特征矩阵非常稀疏朴素贝叶斯通过条件概率估计每个词对类别的贡献本质上是做了词与类别的相关性统计。在问句这种短文本上关键词出现次数少、噪声多NB 比 SVM 更不容易被异常样本带偏。所以作者选择 NB 不是因为它先进而是因为它在这个场景下最匹配。4.3 实体抽取模块 entity_extractor.py 的规则与词典配合实体抽取是 KBQA 里最容易出错的一环。这个项目没有用深度学习模型而是用词典匹配加上规则优先级。entity_extractor.py的核心逻辑是把问句先做分词然后用词典里的词去匹配匹配到的词再映射到实体类型。实际代码中的抽取策略是按词长从大到小排序优先匹配最长词避免“头疼”和“头痛”互相覆盖。下面这段代码演示了基本思路# entity_extractor.py 抽取示意 class EntityExtractor: def __init__(self, vocab_path): self.entities {} with open(vocab_path, encodingutf-8) as f: for line in f: line line.strip() if not line: continue parts line.split(\t) # 格式词\t实体类型 self.entities[parts[0]] parts[1] def extract(self, question): matched_entities [] # 按词长度倒序优先匹配长实体 for word in sorted(self.entities.keys(), keylen, reverseTrue): if word in question: matched_entities.append((word, self.entities[word])) question question.replace(word, , 1) # 用空格替换避免重复匹配 return matched_entities注意这里用replace把已匹配的词替换成空格是为了防止同一段文字被多个词典重复匹配。比如“感冒”是疾病如果问句里有“感冒症状”替换成“ 症状”后后面再匹配症状词就不会把“感冒”误判成症状。词与类型的映射需要依赖一个vocab.txt每行是“词 分隔符 实体类型”。我一般用制表符分隔但原项目可能直接用空格或冒号。实际运行前先打开vocab.txt看两行确定分隔符是什么否则split错了所有实体都抽不出来。这个项目里实体抽取的效果直接决定后面 Cypher 查询的准确性我踩过的坑是别名词典里如果有“高血压”和“血压高”两个词同时出现在vocab.txt里抽取时会优先匹配“高血压”这个更长的词但如果问句里写的是“血压高”恰恰不会匹配到“高血压”所以在别名词典的构建上需要做同义归并。5. KBQA 问答链路避坑Neo4j 连接、词典缺失与模型路径的排查5.1 现象Cypher 查询返回空但图谱里明明有数据你可能遇到这种情况在 Neo4j Browser 里用MATCH (d:Disease {name:感冒}) RETURN d能查到节点但问答系统回答“感冒有什么症状”时提示查不到。原因出在实体抽取后生成的查询条件和图谱里的属性值不完全匹配。比如问句里是“感冒”但entity_extractor抽到了别名“伤风”在build_graph.py中别名是通过alias_with关系关联到疾病节点的而search_answer.py的 Cypher 只从Disease节点查询没有把别名实体关联进来。解决方法是让search_answer.py在构造查询时先通过别名关系找到主疾病名再进行后续查询。另外还要检查vocab.txt里的词是否带空格或不可见字符。CSV 字段里常见的非断行空格\xa0会导致匹配失败用repr()打印词看看。5.2 现象意图识别把“头疼”识别成“不好”问句是“头疼怎么办”结果意图被识别成了query_not_good之类完全偏离。原因是停用词表stop_words.utf8里把“怎么办”“怎么治”等词过滤掉后剩下“头疼”这个短词而意图模型训练数据里“头疼”这个特征在多个意图里都有出现朴素贝叶斯会按照词的后验概率分配意图如果训练样本中“头疼”多用于表达负面情绪就会被分错。解决思路有二一是扩充意图训练数据尤其增加“症状描述 怎么办”这种句式二是在实体抽取后加入一条规则如果问句抽到了疾病或症状实体且意图概率低于某个阈值则根据实体类型修正意图。我一般会在search_answer.py里加一个if intent_prob 0.6 and entities的兜底逻辑。5.3 现象模型文件加载报错或路径找不到报错通常是FileNotFoundError或者UnpicklingError。前者是因为joblib.load里的路径是相对路径你在项目根目录运行没问题但换到别的目录运行就找不到model/tfidf_model.m。解决方法是把路径改成基于脚本所在目录的绝对路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) model_dir os.path.join(BASE_DIR, model)UnpicklingError则多发生在你把下载的压缩包解压后使用 Windows 记事本打开编辑过.m文件保存时加了 BOM 头或改变了编码导致 pickle 加载失败。这个文件是二进制文件不能用文本编辑器打开更不要用记事本保存。我建议拿到资源后先不改动model下任何文件能跑通再谈定制。5.4 现象数据导入后关系数量对不上项目描述说 21 万关系你导入完数出来只有 18 万但 CSV 里每一行都能解析出关系。原因出在disease.csv中多个关系字段可能有空值或重复值比如一行数据里症状字段是“头痛,头痛,发热”如果实现里没有去重会对“头痛”创建两次关系但 Neo4j 的MERGE去重后只保留一条所以实际关系数少于解析出的三元组数。反过来如果脚本用的是CREATE而不是MERGE关系数量会多出许多。解决方法是构建三年前先对关系去重再统一导入并在导入后跑前面提到的MATCH ()-[r]-() RETURN count(r)核对数量。另一个隐藏原因是 CSV 字段里的中文逗号和英文逗号混用。比如某个字段写作“头痛发热”而不是“头痛,发热”按英文逗号split后整个“头痛发热”会变成一个实体名自然匹配不到已有症状节点关系也就创建失败。所以拿到数据先检查逗号全半角再决定分隔符。6. 端到端验证与扩展把问答系统接到自己的数据上6.1 用 kbqa_test.py 跑通一条完整问答代码跑通后我建议你按下面这个流程验证整条链路。先启动 Neo4j确认服务在线然后进入项目根目录执行python kbqa_test.py程序会进入交互模式输入“感冒有什么症状”预期输出类似“感冒的症状包括头痛、发热、咳嗽等”。如果没反应按前面第 5 章的排查顺序走一遍先看控制台有没有打印实体抽取结果再看意图识别预测值最后看 Cypher 查询语句。我每次都会临时在search_answer.py里加两行print(entity)和print(intent)确认前置模块输出正常再去查数据库语句。为了自动化回归我写了一个简单的测试脚本把常见问题放在一个列表里循环跑# test_qa.py 回归测试示例 questions [ 高血压不能吃什么, 头痛应该挂什么科, 感冒有什么并发症, 发烧了吃什么药, ] for q in questions: result answer_question(q) print(q, , result)这里answer_question是search_answer.py里封装好的函数。通过这种回归方式你可以快速发现哪类问题翻车再针对性调整实体词典或规则。6.2 扩展实体关系从 7 类到更多类的改造思路如果你想把项目扩展到其他垂直领域比如法律或教育核心不是改代码而是改数据和词典。我一般会按三步走第一步整理目标领域的 CSV 表结构保证每个实体都有一列唯一标识第二步生成对应的*_vocab.txt并在entity_extractor.py的实体类型枚举里新增标签第三步在build_graph.py里新增节点和关系的构建分支。注意修改之后意图分类也需要新增加相关意图不然问答会答非所问。从那以后我每次更新数据都会强制走一遍“实体计数、关系计数、三个抽查疾病问答”这套验证流程模型文件尽量不动动一次就重新回归一次。这个项目让我最深的体会是KBQA 的瓶颈往往不在模型而在数据清洗和实体对齐这种看不见的脏活上。希望我的这些拆解能帮你少走一点弯路直接把这个项目跑起来跑通了再去改自己的数据。本文还有配套的精品资源点击获取
热门专题

继续阅读更多专题内容

围绕企业服务、数字化转型与官网运营的常青话题,持续输出深度内容

企业官网建设指南 企业托管服务模式 财税政策与解读 企业数字化转型 官网SEO与获客 网站安全与运维
配套服务

读完这篇文章,了解更多服务

从整站搭建到SEO布局,17项核心服务助您打造高转化的企业官网

01

企业托管整站搭建

从信息架构到栏目预留,搭建可生长的企业站点骨架,每个页面独立原创设计。...

了解详情
02

规整可信网页设计

雪地靴温暖风原创设计,金属铜线条贯穿全页,拒绝通用模板与AI流水线。...

了解详情
03

企业服务SEO布局

关键词体系与语义化结构,从建站源头为搜索排名而生。...

了解详情
04

业务预约咨询表单

多场景表单与线索收集体系,把访问流量转化为可追踪的销售线索。...

了解详情
05

企业服务站点运维

安全巡检、数据备份与内容更新支持,全年守护网站稳定运行。...

了解详情
06

全终端商务适配

电脑、平板、手机一致呈现,移动端体验与转化同样出色。...

了解详情
需要专业建议?

让专业顾问为您解读行业趋势

关于企业官网建设、SEO获客与数字化转型的任何疑问,欢迎一对一咨询我们的专业顾问。