拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python医疗知识图谱问答系统毕设代码包:Neo4j与Flask实战

Python医疗知识图谱问答系统毕设代码包:Neo4j与Flask实战

简介:本资源为基于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 py2neo==2021.2.3 neo4j==4.4.0 flask==2.0.3 jieba==0.42.1 pandas==1.3.5

这里锁了版本号,因为 py2neo 和 neo4j 驱动的 API 在不同大版本之间变化不小,用最新版容易遇到Graph.run()返回类型变了之类的玄学问题。Neo4j 社区版去官网下 4.4 系列的桌面版或服务版,安装时记住设置的密码,后面连接要用。启动后默认 Bolt 端口 7687,HTTP 端口 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("疾病", name=row["name"], desc=row["desc"]) graph.create(node) # 单条创建,数据量大时改用 merge 或批量事务 # 建疾病-症状关系 df_rel = pd.read_csv("data/disease_symptom.csv") for _, row in df_rel.iterrows(): d = graph.nodes.match("疾病", name=row["disease"]).first() s = graph.nodes.match("症状", name=row["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, name=entity).data() if not results: return "图谱中没有找到相关信息" return "、".join([r["answer"] for r in results])

逻辑说明:graph.run的第二个参数name=entity是参数化查询,防止 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(host="0.0.0.0", port=5000, debug=True)

逻辑说明:/ask接收 JSON,返回 JSON。参数说明:host="0.0.0.0"让局域网内其他设备也能访问,答辩时用手机演示方便。debug=True开发阶段用,部署时关掉。前端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', filename='css/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, e1=e1, e2=e2).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()看关系类型和方向,确认和代码里的模板对得上再导入。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表