首页 / 资讯中心 / 文章详情

基于知识图谱的心血管疾病问答系统实现:从本体设计到Neo4j落地

基于知识图谱的心血管疾病问答系统实现:从本体设计到Neo4j落地 ★ FEATURED ARTICLE
简介一套基于知识图谱的心血管疾病问答系统源码与配套数据集面向计算机相关专业的学生、教师及企业开发者适合作为毕业设计、课程设计、初期立项演示也适合新手进阶学习。压缩包共197个文件、约5.52MB以84个csv疾病数据和84个json图谱结构为核心另有6个Python脚本、15个txt说明、README文档及演示图片按数据层、图谱层与应用层组织便于分段对照学习。数据涵盖心房颤动、冠心病、心力衰竭、心肌梗死等常见心血管病种可完整实践命名实体识别、关系抽取、图谱存储与问答检索流程。项目代码已通过运行测试目前已有58人学习下载下载后参照说明即可启动遇到环境或运行问题还可远程指导同时保留了前端展示与图谱可视化的二次开发空间。1. 基于知识图谱的心血管疾病问答系统到底解决什么问题不是搜索是关联患者问“高血压挂什么科”“胸痛该做什么检查”这类问题传统关键词搜索只能返回一堆网页片段而基于知识图谱的心血管疾病问答系统会把疾病、症状、药物、检查、科室建模成一张关系网让程序按关系路径直接给出结构化答案。它能回答“冠心病吃什么药”这种单跳问题也能顺着图谱走向回答需要关联推理的多跳问题。适合正在学 Python 和知识图谱构建的开发者也适合做毕设或简历项目的学生。项目包里的文档说明一般会先交代数据字典和启动步骤读懂了再跑代码能少走很多弯路。2. 先定本体再写代码心血管图谱有哪些实体、关系和属性2.1 本体建模是问答系统的天花板能回答什么由本体决定知识图谱落到应用第一步不是写爬虫或者堆数据而是本体设计。本体决定了系统“知道什么”和“能回答什么”。如果本体里没有“疾病—科室”这条关系用户问“挂什么科”就永远答不出来加再多模板都没用。反过来本体设计得合理后续的清洗、灌库、问答都会顺畅很多。面向心血管问答场景我一般把实体收敛成五类疾病、症状、药物、检查、科室。这五类实体已经能覆盖门诊和患者最常问的高频问题。至于食物禁忌、并发症、风险因素这类实体第一版建议先不做整理成本高且问答收益有限等核心链路跑通后再扩展。实体类型和属性建议这样设计实体类型建议属性示例Disease 疾病name, desc, alias高血压、冠心病、心律失常Symptom 症状name, alias胸痛、心慌、气短Drug 药物name, alias硝酸甘油、硝苯地平Check 检查name心电图、冠脉造影、心脏彩超Department 科室name, alias心血管内科、心内科2.2 原始数据集长什么样先盘点字段再动手项目包里的数据集通常是 CSV 或 Excel 格式最常见的组织方式是“一张主表 多个关联字段”。比如 disease 表里同时有 symptom_list、drug_list、check_list、department 这几个列值用逗号或顿号分隔。这种表结构看起直观但直接拿来建图是不行的因为每个关系都要被拆成独立的一行。拿到数据后我先做三件事。第一统一列名把“疾病名称”“name”这种混用列名统一成小写英文。第二统一编码所有文件转成 UTF-8避免后续灌库乱码。第三清洗值里的杂质最常见的是“胸闷活动后加重”这种括号注释会把括号里的内容也当成实体名还有空值和“暂无”这类占位值都需要处理成统一规则。2.3 清洗与转换脚本把结构化数据拍平成三元组这里的核心任务是把一对多的关系字段拆成“头实体—关系—尾实体”三元组。以下脚本以 disease_raw.csv 为输入输出疾病节点列表和关系列表import pandas as pd df pd.read_csv(data/disease_raw.csv, encodingutf-8) df.columns [c.strip().lower() for c in df.columns] df df.fillna(未知) # 去掉括号注释统一分隔符 df[symptom_list] df[symptom_list].str.replace(r.*?, , regexTrue) df[symptom_list] df[symptom_list].str.split(r[,、;]) diseases [] relations [] for _, r in df.iterrows(): diseases.append({id: fD{r[id]}, name: r[name], desc: r.get(desc, 未知)}) for s in r[symptom_list]: s s.strip() if s and s ! 未知: relations.append([D str(r[id]), HAS_SYMPTOM, s])逻辑说明先把列名全部转小写避免后面写代码时对大小写敏感再统一分隔符中文逗号、顿号、英文逗号、分号一次清洗干净。最后把一对多的字段拍平成三元组关系行这是后续建图最需要的“半成品”格式。参数方面encoding 统一指定 utf-8replace 里开了 regexTrue 才能处理正则表达式。需要注意实体 id 用 D1、D2 这种纯数字前缀而不是直接用中文名好处是后续在 Neo4j 里做节点匹配时不容易出现编码或空格问题。中文名只作为 name 属性存进节点。2.4 别名表与实体对齐系统能不能听懂人话就看这一步这一步是几乎所有问答系统翻车的重灾区。图谱里存的实体叫“胸痛”患者可能说“胸口疼”图谱里科室叫“心血管内科”医生习惯写“心内科”。如果不在查询前做实体对齐用户问题里的词和图谱里的节点永远对不上。我用的方案是维护一张别名映射表查询前先做字符串替换alias_map { 心内科: 心血管内科, 心脏内科: 心血管内科, 胸口疼: 胸痛, 心口痛: 胸痛, 耐绞宁: 硝酸甘油, } def normalize_alias(text): for k, v in alias_map.items(): text text.replace(k, v) return text逻辑说明归一化要在所有后续处理之前执行保证进到实体匹配环节的文本已经用的是图谱里的标准名。参数说明这里用最简单的字符串替换就够几十到几百条别名时完全可维护。不要一上来就上模糊匹配工具别名表的好处是每一条替换都可解释、可排查出问题一眼能看出来。提示用药和症状相关的知识来自公开医学资料的结构化整理仅用于项目学习演示不能替代医生处方。这句话建议写进你的项目 README 里。3. 把数据灌进 Neo4jpy2neo 批量写入、唯一约束与图谱验证3.1 为什么选 Neo4j别用邻接矩阵硬扛问答系统学 Python 时很多人会接触到用邻接矩阵表示图但那是给几十个节点的演示用的。问答系统一旦到了几百上千实体、需要做多跳查询时用邻接矩阵手写遍历路径就很吃力了。MySQL 也能做关联查询但多跳关系要反复 JOIN写起来繁琐灵活度也不够。Neo4j 的优势是 Cypher 查询语言专门为关系遍历设计图形化 Browser 还能直接看到图谱长什么样调试时比对着表格直观得多。下面是常见选型对比方案适合规模查询方式部署成本MySQL 多表 JOIN关系深度浅SQL低Python 邻接矩阵几十节点的演示Python 遍历最低Neo4j千级以上、多跳问答Cypher 图查询中RDF 三元组库也是备选但对问答场景来说Neo4j 的成熟度和可视化调试体验更合适。先把环境装好Python 版本 3.8 以上就完全够用Neo4j 用社区版即可。3.2 py2neo 灌库脚本把三元组装成图谱Neo4j 部署完成后用 py2neo 连接数据库并写入节点和关系。这是一个最小可跑的灌库脚本from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, 你的密码)) # 清空旧数据避免重复灌库 graph.run(MATCH (n) DETACH DELETE n) # 创建疾病节点 for row in diseases: node Node(Disease, namerow[name], descrow[desc]) graph.create(node) # 先按 name 索引节点再创建关系 graph.run(CREATE INDEX IF NOT EXISTS FOR (d:Disease) ON (d.name)) for rel in relations: head graph.nodes.match(Disease, nameD rel[0]).first() tail graph.nodes.match(Symptom, namerel[2]).first() if head and tail: graph.create(Relationship(head, rel[1], tail))逻辑说明第一步 DETACH DELETE 清空所有节点和关系保证反复调试时图谱是干净的第二步创建疾病节点第三步给 Disease.name 建索引这样后面按名字查节点时不会全表扫描。参数说明auth 元组第一个元素是用户名第二个是密码不能用默认密码 neo4j/neo4j 直接连Neo4j 首次启动会要求你设置新密码。这里有个关键建议py2neo 的版本坑非常多很多人的连接问题都出在版本上。py2neo v5 的接口变化较大我一般固定用 py2neo2021.2.3 来配合 Neo4j 4.x/5.x并且统一用 graph.run() 传 Cypher 字符串的方式执行而不是依赖 NodeMatch 等高级 API。这样即使版本有差异核心逻辑也不容易坏。3.3 唯一约束二次灌库不翻车的后悔药第一次灌库跑通后第二次跑脚本最容易出现的问题是节点重复。如果灌库脚本用了 CREATE 而不是 MERGE每跑一次就会生成一批一模一样的节点高血压在图谱里变成三个问答查询结果重复。解决办法是给节点加唯一约束。Neo4j 4.x 以上用CREATE CONSTRAINT FOR (d:Disease) REQUIRE d.name IS UNIQUE如果用的是旧版 Neo4j则写成CREATE CONSTRAINT ON (d:Disease) ASSERT d.name IS UNIQUE加约束之后再配合 MERGE 写入数据库会先按 name 查找已有节点存在则复用不存在才创建。这样无论脚本跑多少遍图谱里的每个实体都只有一份。这个操作建议在灌库脚本开头就做而不是等重复数据出现后再清理。3.4 灌库后验证五条必跑的 Cypher 查询灌库成功不等于数据没问题。我用一组 Cypher 查询来验收图谱质量每条都有具体目的// 查看各类实体数量分布 MATCH (n) RETURN labels(n), count(*) // 查看高血压一跳以内的所有邻居 MATCH (d:Disease {name: 高血压})--(s) RETURN s第一条能快速判断是否某个实体类型完全没建上比如科室数量为 0 说明清洗时 dept 字段没有被拆进去。第二条抽查单节点的关系情况如果某个疾病的邻居只有症状没有药物说明原始数据里该字段为空。还需要跑一条核心业务查询// 冠心病有哪些推荐药物 MATCH (d:Disease {name: 冠心病})-[:RECOMMEND_DRUG]-(dr:Drug) RETURN dr.name最终验证标准有三个实体类型数量不为 0、每个疾病节点至少有一条症状关系、孤立节点数量接近 0。孤立节点多说明关系拆分行在清洗阶段丢了需要回到第 2 章的拆分逻辑排查。4. 避坑指南灌库、编码、版本和实体匹配的五个血泪记录4.1 二次灌库图谱翻倍CREATE 用太狠现象灌库脚本跑了两次Browser 里“高血压”出现了两个节点问答结果变成重复列表。原因是脚本里用的 CREATE 无条件创建新节点完全不会检查库里是否已存在同名实体。解决方法是把节点和关系的创建语句全部改成 MERGE并在 name 属性上加唯一约束。MERGE 会先按 name 做查找找到就复用找不到才创建。注意只给 CREATE 换 MERGE 还不够必须配合唯一约束否则数据量大时 MERGE 的性能会很差。4.2 py2neo 版本玄学本地连不上 Neo4j现象pip install py2neo 装完最新版执行 Graph(bolt://localhost:7687, auth(neo4j, 密码)) 时报认证失败或者握手错误而且错误信息看着像网络问题。原因多半是 py2neo v5 与当前 Neo4j 版本的驱动协议不兼容或者密码还是初始状态没改。解决方法是把 py2neo 固定到 2021.2.3 版本同时确认 Neo4j 的用户名是 neo4j、密码是你在安装时自行设置的那个。执行 Cypher 时建议统一走 graph.run() 字符串方式不要用不同版本里差异很大的 NodeMatch 写法。4.3 中文全变问号编码问题从 CSV 一直带到图谱现象用 pandas 清洗后写入 Neo4jBrowser 里节点显示全是问号或者执行 graph.run() 时抛 UnicodeEncodeError。原因绝大多数是文件读取时没指定编码Windows 下 Excel 另存的 CSV 默认是 GBK而 Neo4j 和 py2neo 都按 UTF-8 处理。解决方法是所有读取和写入都显式加 encodingutf-8pd.read_csv 的 encoding 参数、open() 时的 encoding 参数都要写。如果数据是从 Excel 导出的另存为 CSV 时选择“CSV UTF-8”格式而不是默认的“CSV”。4.4 用户问题里的实体匹配不上分词把实体切碎了现象用户问“高血压吃什么药比较好”程序返回“没有找到实体”但图谱里明明有高血压节点。原因是 jieba 默认词典没有收录“高血压”把句子切成了“高血压吃”“什么药”实体被拦腰切断。解决方法是给 jieba 加载自定义词典把疾病、症状、药物、检查、科室的名称全部放进 user_dict.txtimport jieba jieba.load_userdict(data/user_dict.txt)同时做一层兜底不依赖分词结果直接遍历图谱所有实体名判断哪个实体名出现在问句字符串里。这一层“包含匹配”虽然笨但在短问题上准确率非常高是问答系统里最实用的兜底策略。4.5 心内科还是心血管内科别名表不全导致答案空白现象用户问“心口疼挂什么科”系统回答空白但图谱里确实有“胸痛”症状和“心血管内科”科室。原因就是别名缺失问句里“心口疼”和图谱里“胸痛”对不上实体识别环节直接失败。解决方法是完善 alias_map把用户常见说法逐一映射到标准名。这里有个技巧做完一轮问答测试后把所有回答失败的句子收集起来看到底是哪个别名没覆盖补进别名表再跑一遍。实体对齐的质量应该优先于图谱规模别急着加新实体先把已有实体的别名做透。5. 跑通问答并验收五类高频问句的模板与验证方法5.1 问答主流程一句话归一化、识别实体、套模板、查图谱整个问答系统收敛成一条流水线别名归一让“心口疼”变成“胸痛”实体识别找出问句里涉及的疾病或症状意图判定判断用户问的是症状、药物、科室还是检查最后组装 Cypher 查询图谱并生成答案。意图模板决定了系统的“可回答边界”模板越多能接住的问法越多。问法示例意图组装出的 Cypher高血压有什么症状查症状MATCH (d:Disease {name})−[:HAS_SYMPTOM]-(s) RETURN s胸痛可能是什么病查疾病MATCH (s:Symptom {name})-[:HAS_SYMPTOM]-(d) RETURN d高血压吃什么药查药物MATCH (d:Disease {name})−[:RECOMMEND_DRUG]-(dr) RETURN dr高血压挂什么科查科室MATCH (d:Disease {name})−[:DEPT_OF]-(dept) RETURN dept高血压做什么检查查检查MATCH (d:Disease {name})−[:NEED_CHECK]-(c) RETURN c5.2 核心问答函数与五类例句验收def answer(question): q normalize_alias(question) # 1. 别名归一 entity next((e for e in entity_names if e in q), None) if not entity: return 这个问题我还没覆盖建议先去医院确认 for intent, cql in TEMPLATES.items(): if any(k in q for k in intent_keywords[intent]): rows graph.run(cql, nameentity).data() names [r[name] for r in rows] return 、.join(names) if names else 知识库里没有查到 return 换个问法再试试参数说明entity_names 来自所有图谱实体名列表程序启动时构建一次并缓存intent_keywords 是每类意图的问法词表例如 symptom 对应“症状、表现、反应”。验收时跑五条代表句高血压有什么症状、胸痛可能是什么病、高血压吃什么药、高血压挂什么科、高血压需要做什么检查。每条都要返回非空答案重复执行两次结果一致。我第一次做这个项目时把重心全放在扩大图谱规模上直到跑验收才发现“心口疼挂什么科”答不出来问题根本不在图谱而在别名表。先把别名补全再做图谱扩展这个顺序能省掉一大半调试时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站