资讯动态

Python+Neo4j医疗知识图谱问答系统实战:从数据清洗到Cypher查询

发布时间:2026/10/13 0:34:31 来源:尧图企业网站定制
简介基于Python的知识图谱医疗领域问答系统毕业设计资料包面向计算机相关专业学生、毕业设计与课程设计人群适合快速搭建从知识图谱构建到问答交互的完整项目也可作为中小型智能问答应用的技术参考。资源共1403个文件、38.72MB包含77个Python源码、820个Java与216个XML等程序文件以及32个JSON、20个CSV、13个H5等数据和配置并附论文PDF、PNG截图与README文档其中Python文件负责问答核心逻辑与匹配Java/XML构成配套工程模块数据文件支撑图谱构建和模型训练。已有103人学习浏览项目经导师指导并获评审98分全部代码均通过本地编译与调试可稳定运行。除完整代码外还附带数据集与论文资料覆盖医疗实体识别、关系抽取、意图匹配等关键环节适合作为毕业设计基础工程也方便快速完成系统演示与设计报告。1. 医疗知识图谱问答系统的真实定位先搞懂它帮你解决什么问题做医疗方向毕业设计时很多同学第一反应是上BERT、上深度学习模型结果数据量不够、训练卡死、答辩被问得说不出话。这个基于Python的知识图谱医疗问答系统走的是另一条更稳的路子用Neo4j建医疗知识图谱把疾病、症状、药品、检查之间的关系存成图再通过规则词表加Cypher查询的方式做问答。它不依赖大模型算力不要求你有GPU一台普通笔记本就能跑通。它解决的是毕设里最核心的“系统能跑、逻辑能讲、代码能展示”三个诉求同时把知识图谱构建、实体关系抽取、意图识别、查询生成这一整条链路完整地走了一遍适合正在做毕业设计、需要项目实战练习或者想在简历上写一个完整AI项目的计算机相关专业学生。下面我按自己的拆解习惯把这套资源的代码结构、数据流和关键坑逐一讲清楚。2. 知识图谱数据层实体抽取清洗与Neo4j图存储2.1 医疗数据从哪来把自己当数据工程师洗一遍这套资源里的数据是半结构化的医疗文本常见来源是公开医疗网站的疾病百科、药品说明书和检查检验介绍。要做成知识图谱第一步不是急着写代码而是把这些数据整理成“实体表”和“关系表”。实体表里是疾病、症状、药品、检查、科室这类节点关系表里是“疾病_症状”“疾病_药品”“疾病_检查”这类边。我一般会先写一个清洗脚本把原始数据里的空值、重复项、别名问题处理掉。比如“高血压”和“高血压病”在源数据里是两个写法但图谱里应该是同一个节点。下面这个脚本的思路就是做字段对齐和别名归一化。import pandas as pd # 原始数据疾病表、症状表、药品表假设是csv格式 disease_df pd.read_csv(data/disease.csv) symptom_df pd.read_csv(data/symptom.csv) drug_df pd.read_csv(data/drug.csv) # 字段统一把不同来源的“疾病名”列改成统一命名 disease_df disease_df.rename(columns{disease_name: name}) symptom_df symptom_df.rename(columns{symptom_name: name}) drug_df drug_df.rename(columns{drug_name: name}) # 去重与空值处理 disease_df[name] disease_df[name].str.strip() disease_df disease_df.dropna(subset[name]) disease_df disease_df.drop_duplicates(subset[name]) # 别名归一化维护一份别名映射表把“高血压病”映射到“高血压” alias_map {高血压病: 高血压, 糖尿病II型: 2型糖尿病} disease_df[name] disease_df[name].replace(alias_map)这里最值得说明的是别名归一化。问答系统能不能答对问题很大程度取决于图谱里的实体是否统一。用户问“高血压病要注意什么”如果图谱里只存了“高血压”那就查不到。所以我在这个环节会反复检查别名映射表宁可多花时间也不能省这一步。参数方面strip()去掉首尾空格dropna按subset指定列去空drop_duplicates按subset去重这三个操作是清洗阶段最常见的组合。2.2 实体与关系建模节点和边怎么设计才利于问答知识图谱建得好不好不是看节点多不多而是看问答需求能不能被图结构直接支撑。这套资源里实体类型大致分为五类疾病、症状、药品、检查、科室。关系类型则围绕疾病展开包括疾病相关症状、疾病推荐药品、疾病需做检查、疾病所属科室等。我整理了一张映射表方便你把数据对齐到图模型上实体类型节点标签主要属性示意关系疾病Diseasename, desc, cause关联症状/药品/检查症状Symptomname被疾病关联药品Drugname, usage, dosage治疗疾病检查Checkname, intro确诊疾病科室Departmentname归属疾病选择Neo4j而不是MySQL关键原因是医疗问答里大量查询是“多跳”的比如用户问“高血压患者头痛吃什么药”系统要从疾病节点出发沿着“疾病-症状”找到头痛再沿“疾病-药品”找到对应药品这种多跳查询在图数据库里用一条Cypher就能写出来而在MySQL里要多次JOIN。另一方面Neo4j自带可视化界面答辩时直接打开浏览器展示图谱结构比贴一张ER图直观得多这也是毕设选题时一个很实在的加分点。2.3 批量写入Neo4jpy2neo写入脚本与参数说明数据清洗完、模型定好后下一步就是把数据写进Neo4j。这套资源用的是py2neo库我在这类项目里也一直用py2neo因为它的Node、Relationship对象和Cypher的映射关系比较直接写起来不容易出错。下面是一段核心写入逻辑。from py2neo import Graph, Node, Relationship, Subgraph # 连接Neo4j数据库uri是连接地址auth是用户名密码 graph Graph(http://localhost:7474, auth(neo4j, 123456)) # 清空旧数据保证重复执行不会产生脏数据 graph.delete_all() # 先创建所有疾病节点 disease_nodes [] for _, row in disease_df.iterrows(): node Node(Disease, namerow[name], descrow.get(desc, )) disease_nodes.append(node) graph.create(Subgraph(disease_nodes)) # 创建关系疾病-症状 rels [] for _, row in relation_df.iterrows(): disease_node graph.nodes.match(Disease, namerow[disease]).first() symptom_node graph.nodes.match(Symptom, namerow[symptom]).first() if disease_node is not None and symptom_node is not None: rel Relationship(disease_node, HAS_SYMPTOM, symptom_node) rels.append(rel) graph.create(Subgraph(rels))这段代码里delete_all()是开发阶段最实用的一个方法脚本反复跑不会积累重复数据。graph.nodes.match()按标签和属性查节点返回空时要记得判空否则创建关系时传了None会直接报错。还有个细节是批量创建用Subgraph打包而不是一条条create数据量到几千条时性能差别明显。这套资源的数据量不算大几万条以内这样写都没问题。2.4 数据验证用Cypher查询检查图是否建对写完数据不等于建完图我习惯性跑几条Cypher确认节点数量和关系数量对得上。在Neo4j浏览器里执行下面这几条基本能把问题暴露出来。// 统计各类节点数量 MATCH (n:Disease) RETURN count(n); MATCH (n:Symptom) RETURN count(n); // 抽查一个疾病的所有关系 MATCH (d:Disease {name: 高血压})-[r]-(n) RETURN type(r), n.name LIMIT 20; // 检查是否存在孤立的疾病节点没有关联任何症状 MATCH (d:Disease) WHERE NOT (d)-[:HAS_SYMPTOM]-(:Symptom) RETURN d.name LIMIT 10;第一条确认节点数量级和源数据一致第二条看单拎一个疾病的周边关系是否齐全第三条专门查“孤儿节点”如果疾病没有关联任何症状说明关系表里这条疾病的记录漏了。这是我在这个资源里最看重的一步——知识图谱问答的准确率上限由图谱本身决定代码写得再好图里的关系是空的一样答不对。3. 问答系统核心链路意图识别、槽位填充与Cypher生成3.1 意图识别方案选型为什么优先用规则词表而不是深度学习问答系统的第一步是理解用户问什么。医疗场景的问题类型其实比较有限常见就这几类查症状、查治疗药物、查检查项目、查科室、查疾病定义。这套资源里采用的是规则词表为主、分类模型兜底的做法而不是一上来就训练深度学习模型。我的选型理由很直接毕设场景下训练数据不好搞标注1000条医疗问句已经够呛BERT微调动不动就要一两个小时而且医疗领域分词、专业术语对模型效果影响很大模型一旦答错又说不清原因。规则词表则完全不同——每类意图对应一组触发词比如“吃什么药”“怎么治疗”触发治疗意图“有哪些症状”“会有什么表现”触发症状意图逻辑透明答辩时一句话就能讲明白。常见做法是先定义意图字典再用关键词匹配算出每个意图的得分得分最高的就是当前问句的意图。我一般还会加一个“兜底意图”用户问的问题不在已知类型里时系统返回提示话术而不是硬答这个设计在答辩演示时非常加分。3.2 槽位填充实现最长匹配与别名库兜底意图识别只能知道用户问的是“症状类”还是“治疗类”还必须知道用户具体问的是哪种病。这一步叫槽位填充。这套资源的做法是拿图谱里的实体名和别名去匹配问句命中哪个实体就把哪个实体名作为查询参数。import re # 加载实体名与别名映射这里从disease_df生成一份dict entity_alias {高血压: [高血压, 高血压病, 血压高], 糖尿病: [糖尿病, 血糖高, 消渴症]} def extract_entity(question, entity_typeDisease): 从问句中抽取实体返回匹配到的标准名和原词 # 优先按别名长度降序匹配避免高血压被高血压病的先匹配 candidates [] for std_name, aliases in entity_alias.items(): for alias in aliases: if alias in question: candidates.append((std_name, alias)) if not candidates: return None, None # 选取最长匹配匹配到的原文越长越精确 candidates.sort(keylambda x: len(x[1]), reverseTrue) return candidates[0][0], candidates[0][1] question 高血压病平时吃什么药 std_name, raw extract_entity(question) print(std_name, raw) # 输出高血压 高血压病这段代码里最容易被忽略的是“按别名长度降序排序”。如果不排序dict遍历的顺序可能会让“高血压”先命中返回的实体没有错但用户问的是“高血压病”在后续回答案时如果有专门针对“高血压病”的说明信息就会丢失。另外entity_alias这个映射表在真实项目里我会单独抽成一个JSON文件方便做毕设的人往里面补充术语不要硬编码在代码里。匹配到实体后最好同时保留标准名和原始词文本回显时用原始词查询数据库时用标准名两不误。3.3 查询生成与答案回填从意图到Cypher再到自然语言槽位填充完成就进入最后一步生成Cypher查询把数据库返回的结果拼成自然语言答案。这部分的坑往往不在写Cypher本身而在拼接时的引号和参数转义。我在这套资源里看到的做法是先用参数名占位再通过py2neo传参避免直接拼接字符串。def answer(question): intent detect_intent(question) # 返回意图名称 entity, raw extract_entity(question) if entity is None: return 暂时无法从问题中识别到相关疾病请换一种说法试试。 # 按意图分发查询模板 if intent symptom: cql MATCH (d:Disease {name: $name})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name LIMIT 10 elif intent drug: cql MATCH (d:Disease {name: $name})-[:RECOMMEND_DRUG]-(m:Drug) RETURN m.name LIMIT 10 elif intent check: cql MATCH (d:Disease {name: $name})-[:NEED_CHECK]-(c:Check) RETURN c.name LIMIT 10 else: return 该问题暂不支持回答你可以试试问症状、用药或检查。 result graph.run(cql, nameentity).data() if not result: return 图谱中暂时没有找到【{}】的相关信息。.format(entity) names [r[list(r.keys())[0]] for r in result] return {}的{}包括{}.format(entity, intent, 、.join(names))这里是整套系统最核心的一段逻辑几个关键点要特别注意。第一Cypher里用$name作参数占位符然后graph.run(cql, nameentity)传参防止用户输入里的引号破坏查询语句这是安全基本盘。第二意图分发用if-elif写清楚每类意图对应一个查询模板不要试图写一个万能的Cypher因为不同关系的查询路径不一样硬统一后反而难维护。第三返回结果要处理空列表图谱里没有这个实体的关系时给用户一个明确反馈而不是直接抛异常。实际运行中我建议把result先打印出来看结构再写回填逻辑py2neo返回的list里每个元素是dict直接用下标取值容易踩坑。4. 运行部署与最小验证把项目完整跑起来4.1 环境准备与依赖安装版本搭配是第一道坎我拆过不少毕设资源很多项目代码本身没问题最后卡在环境装不上。这个医疗问答系统依赖项不多但版本搭配有讲究。主要依赖是Python 3.8、Neo4j Community 4.x、py2neo 2021.2.3、pandas、Flask。其中py2neo和Neo4j的版本匹配最容易出问题py2neo 4.x配Neo4j 3.xpy2neo 2021.x配Neo4j 4.x这个对应关系先确认好再安装。# 创建虚拟环境避免污染全局Python python3.8 -m venv venv source venv/bin/activate # 安装核心依赖 pip install py2neo2021.2.3 pandas flask # 确认neo4j已启动Linux/Mac下用neo4j console前台启动方便看日志 neo4j console安装完用一行命令验证库能不能正常导入。py2neo这个库在import时报错八成是版本不对去PyPI看下对应关系改版本重装比在代码层面排查更快。4.2 启动主程序命令行交互与HTTP接口两种方式环境配好后主程序一般有两种启动方式。第一种是纯命令行交互适合自己调试第二种是包一个Flask接口适合答辩时演示也方便前端对接。这套资源里两种方式都有我只讲Flask接口部分因为答辩演示时在浏览器里发请求比在终端里敲命令更直观。from flask import Flask, request, jsonify app Flask(__name__) app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ) if not question: return jsonify({code: 400, msg: question不能为空}), 400 answer_text answer(question) return jsonify({code: 200, question: question, answer: answer_text}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这个接口设计很简洁POST一个JSON格式的question字段返回JSON格式的答案。host0.0.0.0是为了让局域网内的设备也能访问如果只是本机调试改成127.0.0.1更安全。启动后我习惯直接拿curl试一下确认整条链路通不通。curl -X POST http://localhost:5000/qa \ -H Content-Type: application/json \ -d {question: 高血压有哪些症状}返回的JSON里如果能看到症状列表说明图谱、意图识别、槽位填充、Cypher查询整条链路已经通了。这个时候再去做前端页面或者扩展功能就有了一个可靠的底座。4.3 最小验证带一份测试集跑回归统计命中率系统能跑通之后不能只在交互界面里试几个问题就完事。答辩前要跑一份回归测试集把问句、期望答案类型、实际回答结果都记录下来。我一般会准备40到50条测试问句覆盖症状、用药、检查、科室、不在图谱里的实体、无关问题六类场景然后统计答对的比例。import csv test_cases [ {question: 感冒有什么症状, expect: 感冒, type: symptom}, {question: 高血压患者应该吃什么药, expect: 高血压, type: drug}, {question: 糖尿病的检查项目有哪些, expect: 糖尿病, type: check}, # 更多测试用例... ] correct 0 failed [] for case in test_cases: try: resp answer(case[question]) # 规则答案里包含期望疾病名且非兜底话术算答对 if case[expect] in resp and 暂不支持 not in resp: correct 1 else: failed.append(case) except Exception as e: failed.append({**case, error: str(e)}) print(准确率{}/{} {:.2f}.format(correct, len(test_cases), correct / len(test_cases))) for item in failed: print(失败用例, item)这段测试逻辑很简单但价值很高。它能把“感觉系统还行”变成“系统答对了85%”而且是可复现的数字。我给这个资源写验证脚本时特别注意一点判断标准里把“暂不支持”这类兜底话术判定为失败因为用户来问是希望得到答案的不是来看系统怎么优雅地拒绝。实际测试中命中率低于80%时我会回头查图谱数据是不是有缺漏而不是急着调代码这是排查这个问题最省力的方向。5. 避坑指南医疗知识图谱问答里最常见的五个坑5.1 Neo4j连接不上认证与驱动版本双坑现象代码里Graph(http://localhost:7474)执行后报Unauthorized或ConnectionError。原因最常见的是Neo4j数据库的初始密码没改默认账号是neo4j第一次启动时需要设置新密码另一个原因是py2neo的Graph()参数写错连接Neo4j用的是HTTP端口7474不是Bolt端口7687用错就直接ConnectionError。解决浏览器打开http://localhost:7474确认Neo4j能正常访问并记住密码写入代码时把认证参数补全两端密码保持一致如果还连不上看Neo4j服务端日志不要光盯Python端的报错。5.2 中文乱码与分词边界现象写入Neo4j后浏览器里显示的中文是乱码或者问答匹配时“高血压”匹配不到“血压高”。原因CSV文件编码不一致用Excel另存的CSV通常是GBK而Python默认按UTF-8读取读进来就是乱码分词方面jieba等工具默认词典不包含医疗术语导致实体在问句中被切开比如“慢性粒细胞白血病”被切成了“慢性”“粒”“细胞”等碎片。解决读取CSV时显式指定编码常见的做法是先试utf-8再回退到gbk实体匹配不要完全依赖分词直接用字符串包含关系或预先构建的别名表去做绕过分词这层不确定性。医疗名称里“病”“症”“炎”这些尾缀很容易被切开我的习惯是永远在实体匹配里用完整词表做包含判断。5.3 动态拼Cypher时报错引号和传参方式现象用户问“高血压保健”时系统报Neo.ClientError.Statement.SyntaxError仔细看是单引号把Cypher语句断了。原因代码里用MATCH (d:Disease {name: {0}}).format(entity)这种字符串拼接方式用户输入里的引号或特殊字符破坏了Cypher结构。解决统一改成参数化查询graph.run(cql, nameentity)Cypher里用$name占位符。这不是性能优化是稳定性优化问答系统面向的用户输入不受控字符串拼接的方式早晚翻车。5.4 命中率上不去别名库不足与实体重叠现象测试集覆盖了意图类型但准确率一直卡在70%左右单看每个失败用例又觉得数据没问题。原因两个原因叠加。一是别名库太薄用户口语里说“血压高”而图谱里只有“高血压”匹配不到二是实体存在包含关系比如“感冒”和“病毒性感冒”同时存在“感冒”问句被匹配到了上级实体而期望答案里包含的是下级实体的信息。解决先统计失败用例中的未命中实体把这些词批量补进别名映射表针对实体包含关系匹配时优先取最长命中词避免匹配到过泛的上级实体。这步做完通常能把准确率拉高10个点。5.5 资源包里的残留文件别乱跑先看清核心目录结构现象解压这套资源后看到里面有.aidl、gradlew.bat、native-lib.cpp、一堆CSS文件容易误以为项目被传错了或者不完整。原因毕设资源打包时经常是直接从本机工作目录压缩的有些是之前做过Android或前端项目的残留文件被一并卷了进来。这是毕设类资源里非常普遍的情况不代表Python主程序缺失。解决先找核心入口一般找main.py、app.py、run.py这类文件再看有没有data、utils、kg等目录。把无关文件忽略掉只从核心代码入手跑通流程即可。真要清理的话等项目完全跑通后再删避免误删依赖文件。6. 往前一步给问答系统加“无答案检测”答辩时直接展示回归结果这套系统基础链路已经完整但有一个细节值得加强当图谱中没有用户问的相关实体时当前逻辑会返回“暂不支持”这种回答在演示时略显生硬。我习惯加一层无答案检测把“识别到实体但没有关系数据”和“连实体都没识别到”两种情况区分开分别给不同的兜底话术。先说实现思路。在answer()函数里extract_entity返回空时说明连实体都没识别到这时应该提示用户补充疾病名称而实体识别到了但Cypher查询结果为空说明图谱缺数据这时应该明说“图谱中暂时没有该疾病的相关症状/药品信息”引导用户换个疾病问。实际代码改动很小就是在原有if not result分支里再做一个判断同时把兜底话术分两级if entity is None: return 没有从问题中识别到疾病名称请注明具体疾病例如高血压有哪些症状 if result is None or len(result) 0: return 图谱中暂时没有【{}】的{}数据你可以换个疾病问问。.format(raw, intent)这两条话术的区别很有价值——前者说明意图识别和槽位模块的正常工作预期后者说明图谱覆盖度的局限。答辩时被问到“系统答不上来怎么办”你可以直接演示这两条回复并解释系统能区分“没听懂”和“没有数据”这是很多自娱自乐的毕设系统不具备的设计。另一个值得做的改进是输出一份可打印的问答记录。在Flask接口里每次问答都追加一条日志包含时间、问句、意图、命中的实体、答案答辩时直接打开日志给人看。测个二三十条不同问题日志一拉出来系统的行为轨迹一目了然比口头说“效果还不错”有说服力得多。最后说一个我自己踩过的教训。当时给一个问答系统调优连续改了好几版实体匹配和话术每次改完都在交互界面里手动试几个问题觉得没什么问题就提交了。结果答辩前跑回归测试才发现之前“感觉没问题”的那几版里有一版把“糖尿病”的别名映射错位了导致所有糖尿病相关问题都答非所问。从那以后我每次改完代码都强制走一遍回归测试脚本把准确率变化打印出来对比数据不变好就不算改完。这套医疗知识图谱问答资源本身底子比较规整只要你也坚持用回归测试来约束每一步改动答辩时心里会踏实很多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑