简介本资源为基于Python实现的医疗知识图谱知识问答系统完整项目包面向计算机相关专业的毕业设计、期末大作业与课程设计需求者也适合希望入门知识图谱与问答系统开发的初学者。项目以医疗领域为背景涵盖知识图谱构建、实体关系存储与自然语言问答等核心模块代码附有详细注释新手也能读懂。压缩包共188个文件约115.23MB包含41个py源码文件、44个txt说明与数据文件、21个html页面、22张png界面截图以及js、css、json、db等前端与数据库资源另含Neo4j图数据库存储文件结构完整、便于部署。目前已有266人学习下载。项目功能完善、界面美观、操作简单下载后简单部署即可运行可作为高分毕设或课程设计参考帮助读者快速掌握医疗知识图谱问答系统的实现思路与工程组织方式。1. 医疗知识图谱问答系统一份能直接跑起来的毕设代码包毕业设计选题卡在“医疗知识图谱的知识问答系统”这个方向的人不少但真正能跑通、能讲清楚、能扛住答辩追问的代码包不多。这份基于 Python 实现的医疗知识图谱问答系统核心思路是用 Neo4j 存图、用 Python 做意图识别和查询模板匹配前端配一套 Bootstrap 页面下载下来按教程部署就能用。它解决的不是“从零教你搭图谱”的问题而是给你一套已经调通的问答链路用户输入“感冒吃什么药”系统能解析出疾病实体、匹配查询模板、从图数据库里捞出答案并返回。适合正在做毕设、期末大作业、课程设计的人尤其是时间紧、需要快速出成果、但又不想拿一个纯 CRUD 系统糊弄的计算机相关专业学生。代码带注释新手能看懂老手能直接改。2. 环境搭建与 Neo4j 图库初始化从零到能查2.1 为什么选 Neo4j 而不是 MySQL 存医疗数据医疗知识图谱的本质是实体和关系。疾病、症状、药品、检查项目、科室、食物这些实体之间不是简单的表关联而是多对多、带属性、需要路径查询的图结构。比如“糖尿病患者不能吃哪些含糖量高的食物”用 SQL 写要 join 好几张表用 Cypher 一句MATCH (d:疾病)-[:禁忌食物]-(f:食物) WHERE d.name糖尿病 RETURN f.name就出来了。常见做法是 MySQL 存用户和日志Neo4j 存图谱本体这份代码包也是这么分的。选 Neo4j 的另一个理由是它自带可视化界面答辩演示时直接打开浏览器就能展示图谱结构比黑乎乎的命令行有说服力。2.2 安装 Python 依赖与 Neo4j 社区版先确认 Python 版本建议 3.8 到 3.10太新的版本某些图数据库驱动可能不兼容。用 conda 或 venv 建虚拟环境都行我一般用 venv干净。python -m venv medkg_env source medkg_env/bin/activate # Windows 用 medkg_env\Scripts\activate pip install py2neo2021.2.3 neo4j4.4.0 flask2.0.3 jieba0.42.1 pandas1.3.5这里锁了版本号因为 py2neo 和 neo4j 驱动的 API 在不同大版本之间变化不小用最新版容易遇到Graph.run()返回类型变了之类的玄学问题。Neo4j 社区版去官网下 4.4 系列的桌面版或服务版安装时记住设置的密码后面连接要用。启动后默认 Bolt 端口 7687HTTP 端口 7474。2.3 导入医疗图谱数据CSV 批量建节点和关系代码包里通常带一个data目录里面是整理好的 CSV 或 JSON。常见结构是disease.csv、symptom.csv、drug.csv以及关系文件disease_symptom.csv这种。导入时不要一条条CREATE用LOAD CSV批量走。from py2neo import Graph, Node, Relationship import pandas as pd # 连接图库改成你自己的密码 graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) # 读疾病数据批量建节点 df_dis pd.read_csv(data/disease.csv) for _, row in df_dis.iterrows(): node Node(疾病, namerow[name], descrow[desc]) graph.create(node) # 单条创建数据量大时改用 merge 或批量事务 # 建疾病-症状关系 df_rel pd.read_csv(data/disease_symptom.csv) for _, row in df_rel.iterrows(): d graph.nodes.match(疾病, namerow[disease]).first() s graph.nodes.match(症状, namerow[symptom]).first() if d and s: graph.create(Relationship(d, 症状, s))逻辑说明Node第一个参数是标签后面是属性键值对。Relationship第一个参数是起点节点第二个是关系类型第三个是终点节点。参数说明auth里用户名默认 neo4j密码是你安装时设的。如果数据量上万逐条create会慢常见优化是攒 500 条走一次graph.create(subgraph)或者直接用 Cypher 的LOAD CSV语句速度能快一个数量级。导入完在 Neo4j 浏览器里执行MATCH (n) RETURN count(n)确认节点数对得上。3. 问答链路拆解从用户输入到图谱查询3.1 意图识别与实体抽取jieba 分词加词典匹配用户不会按图谱的字段名说话。问“高血压不能吃什么”和“得了高血压忌口哪些食物”是一个意思但字符串不一样。这份代码包的做法是先用 jieba 分词再拿医疗词典去匹配实体最后用关键词判断意图。词典就是图谱里所有疾病名、症状名、药品名的集合从 Neo4j 里拉出来存成列表。import jieba from py2neo import Graph graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) # 从图库拉取所有实体名构建自定义词典 diseases [r[name] for r in graph.run(MATCH (d:疾病) RETURN d.name AS name)] symptoms [r[name] for r in graph.run(MATCH (s:症状) RETURN s.name AS name)] drugs [r[name] for r in graph.run(MATCH (d:药品) RETURN d.name AS name)] for word in diseases symptoms drugs: jieba.add_word(word) # 把医疗实体加进分词词典避免被切碎 def extract_entity(question): words jieba.lcut(question) for w in words: if w in diseases: return 疾病, w if w in symptoms: return 症状, w if w in drugs: return 药品, w return None, None逻辑说明jieba.add_word是关键一步不加的话“高血压”可能被切成“高/血压”实体匹配就断了。参数说明lcut返回列表遍历顺序按词在句中出现的位置。如果一句话里出现多个实体这份代码只取第一个匹配到的够用但不完美想升级可以改成返回列表再按意图筛选。3.2 查询模板匹配把自然语言映射成 Cypher拿到实体和意图后下一步是选查询模板。代码包里一般会定义一个template字典键是意图标签值是对应的 Cypher 语句模板。意图标签靠问题里的关键词判断比如出现“症状”就查疾病对应症状出现“吃什么药”就查治疗药品。templates { 疾病症状: MATCH (d:疾病)-[:症状]-(s:症状) WHERE d.name$name RETURN s.name AS answer, 疾病药品: MATCH (d:疾病)-[:治疗药品]-(m:药品) WHERE d.name$name RETURN m.name AS answer, 疾病忌口: MATCH (d:疾病)-[:禁忌食物]-(f:食物) WHERE d.name$name RETURN f.name AS answer, 症状疾病: MATCH (s:症状)-[:症状]-(d:疾病) WHERE s.name$name RETURN d.name AS answer, } def get_intent(question): if 症状 in question: return 疾病症状 if 药 in question or 治疗 in question: return 疾病药品 if 吃 in question or 忌口 in question or 食物 in question: return 疾病忌口 if 可能 in question or 什么病 in question: return 症状疾病 return None def query_graph(intent, entity): cypher templates.get(intent) if not cypher: return 暂时无法理解这个问题 results graph.run(cypher, nameentity).data() if not results: return 图谱中没有找到相关信息 return 、.join([r[answer] for r in results])逻辑说明graph.run的第二个参数nameentity是参数化查询防止 Cypher 注入。参数说明templates里的$name是占位符实际执行时被替换成实体名。data()把结果转成字典列表。如果返回空说明图谱里没有这条关系不是代码错了是数据没覆盖到。常见做法是在导入数据时补全关系而不是在查询层硬编。3.3 Flask 接口与前端联调把问答封装成 HTTP 服务问答逻辑跑通后用 Flask 包一层接口前端用 AJAX 调。代码包里前端是 Bootstrap 写的一个输入框加一个结果展示区够用。from flask import Flask, request, jsonify, render_template app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/ask, methods[POST]) def ask(): question request.json.get(question, ) etype, entity extract_entity(question) if not entity: return jsonify({answer: 没有识别到医疗实体请换个说法}) intent get_intent(question) answer query_graph(intent, entity) return jsonify({answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明/ask接收 JSON返回 JSON。参数说明host0.0.0.0让局域网内其他设备也能访问答辩时用手机演示方便。debugTrue开发阶段用部署时关掉。前端index.html里用fetch(/ask, {method:POST, ...})发请求把返回的answer塞进页面。联调时如果跨域Flask 加CORS(app)就行但这份代码包前后端同源部署一般遇不到。4. 避坑与排查那些答辩前夜才发现的坑4.1 Neo4j 连接报错ServiceUnavailable现象Python 脚本一跑就抛py2neo.errors.ServiceUnavailable: Cannot connect to Bolt。原因通常是 Neo4j 服务没启动或者端口被占或者密码错了。解决先去 Neo4j Desktop 或服务管理器确认状态是 running浏览器打开http://localhost:7474能进就说明服务正常。密码错的话在 Neo4j 浏览器里执行ALTER USER neo4j SET PASSWORD 新密码改掉再同步改 Python 里的auth。4.2 中文实体匹配不上现象问“感冒了怎么办”系统返回“没有识别到医疗实体”。原因多半是 jieba 词典没加载全或者图谱里存的疾病名是“普通感冒”而用户说的是“感冒”。解决导入数据时统一实体命名别混用简称和全称在extract_entity里加一层模糊匹配比如if w in diseases or any(w in d for d in diseases)但要注意别把“肺炎”匹配到“肺炎球菌”上得控制匹配长度。4.3 Cypher 查询返回空但图谱里明明有数据现象手动在 Neo4j 浏览器里跑MATCH (d:疾病)-[:症状]-(s) WHERE d.name高血压 RETURN s.name有结果但 Python 里跑返回空。原因通常是关系类型写错了比如建图时用的是HAS_SYMPTOM查询模板里写的是症状。解决在浏览器里执行CALL db.relationshipTypes()看实际关系类型名把模板里的字符串改对。另一个可能是参数名对不上$name传进去的是实体名但节点属性键是disease_name而不是name。4.4 前端页面样式丢失现象打开页面只有文字没有样式Bootstrap 的栅格和按钮全失效。原因一般是静态文件路径不对Flask 默认找static目录代码包里如果放在assets或css下就找不到。解决确认render_template返回的 HTML 里引用路径是{{ url_for(static, filenamecss/bootstrap.min.css) }}这种或者直接把 CSS 文件放到static/css/下。项目正文里列出的bootstrap.min.css、info.css、style.css就是这三个文件别漏拷。4.5 导入数据时节点重复创建现象跑了两遍导入脚本图谱里同一个疾病出现两次查询结果重复。原因是用Node加graph.create是纯创建不去重。解决改用graph.merge(node, 疾病, name)第二个参数是标签第三个是唯一属性键这样同名节点只会保留一个。已经重复的可以执行MATCH (d:疾病) WITH d.name AS name, collect(d) AS nodes WHERE size(nodes) 1 FOREACH (n IN tail(nodes) | DETACH DELETE n)清理。5. 进阶技巧把问答准确率从及格拉到优秀5.1 用同义词表扩展实体匹配图谱里的实体名是固定的但用户问法千变万化。“心梗”和“心肌梗死”是一个病“阿司匹林”和“阿斯匹林”是同一个药。常见做法是维护一张同义词表在extract_entity之前先把问题里的同义词替换成标准名。synonyms { 心梗: 心肌梗死, 阿斯匹林: 阿司匹林, 高血压病: 高血压, 糖尿病: 糖尿病, } def normalize_question(question): for k, v in synonyms.items(): if k in question: question question.replace(k, v) return question逻辑说明替换要在分词之前做否则“心梗”被切成“心/梗”就替换不上了。参数说明synonyms字典的键是用户可能说的词值是图谱里的标准实体名。这张表可以手动维护也可以从图谱的别名属性里自动生成。我一般会从 Neo4j 里拉MATCH (n) WHERE n.alias IS NOT NULL RETURN n.name, n.alias来批量构建。5.2 多实体问题的处理用户可能问“高血压和糖尿病哪个更危险”一句话里两个疾病。这份代码包默认只取第一个实体遇到这种问题会漏。改进思路是extract_entity返回实体列表get_intent根据问题里的比较词“哪个”“对比”“区别”走对比模板。def extract_entities(question): words jieba.lcut(question) found [] for w in words: if w in diseases: found.append((疾病, w)) elif w in drugs: found.append((药品, w)) return found def query_compare(entities): if len(entities) 2: return 需要两个实体才能对比 e1, e2 entities[0][1], entities[1][1] cypher MATCH (d1:疾病 {name:$e1})-[:症状]-(s:症状)-[:症状]-(d2:疾病 {name:$e2}) RETURN s.name AS common results graph.run(cypher, e1e1, e2e2).data() if results: return f{e1}和{e2}的共同症状有 、.join([r[common] for r in results]) return f没有找到{e1}和{e2}的共同症状逻辑说明对比查询用 Cypher 的路径匹配找共同邻居这是图数据库的强项。参数说明{name:$e1}是内联属性匹配等价于WHERE d1.name$e1。如果两个实体之间没有共同症状返回提示而不是空字符串用户体验更好。5.3 答辩演示前的自检清单答辩前别只跑一遍“感冒吃什么药”就完事。我习惯按下面这张表过一遍每个类型至少测三个问题记录返回结果和响应时间。测试类型示例问题预期结果检查点疾病查症状高血压有哪些症状返回症状列表实体识别是否正确疾病查药品糖尿病吃什么药返回药品列表关系类型是否匹配症状查疾病头痛可能是什么病返回疾病列表反向查询是否通忌口查询痛风不能吃什么返回食物列表禁忌关系是否存在无实体问题今天天气怎么样提示未识别实体兜底逻辑是否生效多实体问题高血压和糖尿病共同症状返回共同症状多实体抽取是否支持这张表也是查漏补缺的依据。哪一类返回空就去 Neo4j 里确认对应的关系数据有没有导入。响应时间超过 2 秒的检查是不是每次查询都重新拉了全量实体词典是的话把词典加载提到应用启动时做一次别放在请求里。5.4 图谱数据补全的优先级代码包自带的医疗数据覆盖范围有限答辩时老师可能问一个偏门病。与其临时加数据不如提前按优先级补先补常见慢性病高血压、糖尿病、冠心病再补高发传染病流感、肺结核最后补药品和食物的禁忌关系。补数据时注意关系方向要和已有模板一致别一个用(疾病)-[:症状]-(症状)另一个用(症状)-[:属于]-(疾病)查询模板会乱。从那以后我每次拿到新的图谱数据包都先跑一遍CALL db.schema.visualization()看关系类型和方向确认和代码里的模板对得上再导入。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?