多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python医疗知识图谱问答系统源码实战:从Neo4j建模到NER问句解析

Python医疗知识图谱问答系统源码实战:从Neo4j建模到NER问句解析 简介这是一套面向Python开发者与医疗信息化学习者的医疗知识图谱自动问答系统源码适合希望掌握知识图谱构建、自然语言处理与图数据库查询的中级实践者。项目围绕医疗数据预处理、图谱建模、语义分析、问题匹配与结果展示展开可帮助理解从医学词汇、病症、疾病、药物到治疗方法的实体关系建模流程并借助图数据库查询语言完成问答检索。资源包共26个文件以8个py脚本、8个txt词典、5个xml配置、2个json数据文件为主另含jpg示意图与iml工程配置压缩包约15.54MB目录涵盖数据爬取、图谱构建、问题分类、答案搜索与聊天机器人等模块。已有184人学习下载读者可据此搭建可运行的医疗问答原型掌握数据清洗、实体识别、关系抽取与Web界面展示的完整链路并参考词典与JSON样本快速替换或扩展自有医疗语料。1. 从一份医疗知识图谱问答源码说起它到底解决了什么问题医院信息科最头疼的场景之一是患者或基层医生用自然语言问「二甲双胍能不能和格列齐特一起吃」而院内系统只会返回一堆需要人工翻页的文档。基于 Python 的医疗知识图谱自动问答系统本质就是把「实体识别 关系查询 答案生成」串成一条流水线先把疾病、症状、药品、检查项等实体和它们之间的关系存进图数据库再把用户问句解析成图查询语句最后把查询结果拼成一句人话返回。这套源码类项目之所以在 python 入门 和 python 教程 搜索里长期有热度是因为它同时踩中了三个刚需Python 语法友好、知识图谱有可视化效果、问答系统能直接演示。适合谁有 Python 基础、想做一个能写进简历或课程设计的完整项目的人也适合医疗信息化方向、想验证图谱问答可行性的工程师。下面我按「先跑通、再拆解、后避坑」的顺序把这份源码类项目该怎么做讲清楚。2. 医疗知识图谱问答系统的四层架构与选型理由2.1 为什么是「图谱 问答」而不是纯检索纯关键词检索在医疗场景下有个硬伤同义词和上下位关系处理不了。用户问「心梗」文档里写的是「心肌梗死」用户问「降糖药」文档里列的是具体药名。知识图谱用节点和边显式表达「心肌梗死—别名—心梗」「二甲双胍—属于—双胍类降糖药」查询时先做实体归一再沿边扩展召回率和准确率都比倒排索引高一个档次。常见做法是用 Neo4j 存图因为它的 Cypher 查询语言对「多跳关系」表达最自然Python 通过 py2neo 或官方 neo4j 驱动就能操作。选型上如果你只是做课程设计Neo4j 社区版足够如果要上生产再考虑 NebulaGraph 或 JanusGraph但源码类项目九成用 Neo4j 起步。2.2 四层架构拆解从问句到答案一份能跑的医疗问答源码通常分四层。第一层是数据层疾病、症状、药品、检查、科室五类实体以及「疾病-症状」「疾病-药品」「药品-疾病」「疾病-检查」四类关系。第二层是存储层Neo4j 图数据库节点属性包括名称、别名、描述。第三层是处理层用 jieba 做中文分词用词典或 BERT 做命名实体识别NER把问句里的「二甲双胍」「糖尿病」抽出来。第四层是交互层Flask 或 FastAPI 起一个 Web 接口前端用简单 HTML 或 Vue 展示。这四层里处理层是精度瓶颈存储层是性能瓶颈交互层反而最简单。2.3 环境准备Python 版本与依赖清单我一般用 Python 3.8 到 3.10太新的版本某些图数据库驱动会报兼容错误。依赖装这几个就够跑通最小闭环# 创建虚拟环境避免污染全局 python -m venv medqa_env source medqa_env/bin/activate # Windows 用 medqa_env\Scripts\activate # 核心依赖 pip install neo4j5.14.0 # 图数据库驱动 pip install jieba0.42.1 # 中文分词 pip install flask3.0.0 # Web 框架 pip install pandas2.1.0 # 数据处理参数说明neo4j 驱动版本要和 Neo4j 服务端大版本对齐5.x 驱动连 5.x 服务端最稳jieba 用默认词典即可医疗专有名词后续通过自定义词典补。装完后用python -c import neo4j, jieba, flask验证没报错说明环境通了。这一步翻车的点通常是 pip 源太慢换国内镜像即可但别用来源不明的第三方包。3. 从零构建医疗知识图谱数据建模与 Neo4j 导入3.1 实体与关系设计五类节点四类边医疗知识图谱的 schema 不需要太复杂课程设计级别五类节点足够Disease疾病、Symptom症状、Drug药品、Check检查、Department科室。关系设计要克制常见四类Disease-[:HAS_SYMPTOM]-Symptom、Disease-[:USE_DRUG]-Drug、Disease-[:NEED_CHECK]-Check、Disease-[:BELONG_TO]-Department。为什么关系要克制因为每多一类关系NER 和意图识别就要多处理一种问法源码类项目最容易死在「关系太多但问句覆盖不全」。我一般先定 20 种常见病、50 种症状、30 种药把闭环跑通再扩。3.2 用 Cypher 建节点和关系Neo4j 的 Cypher 语法接近 ASCII 画图建节点和关系很直观。下面这段可以直接在 Neo4j Browser 里跑// 建疾病节点name 加唯一约束防止重复 CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (d:Disease) REQUIRE d.name IS UNIQUE; // 建症状节点 CREATE CONSTRAINT symptom_name IF NOT EXISTS FOR (s:Symptom) REQUIRE s.name IS UNIQUE; // 插入一个疾病和它的症状关系 MERGE (d:Disease {name: 2型糖尿病}) SET d.alias 糖尿病, d.desc 以高血糖为特征的代谢性疾病 MERGE (s:Symptom {name: 多饮}) MERGE (d)-[:HAS_SYMPTOM]-(s); // 查询糖尿病有哪些症状 MATCH (d:Disease {name: 2型糖尿病})-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name;逻辑说明MERGE 是「有则匹配、无则创建」比 CREATE 安全避免重复插入。约束CONSTRAINT一定要先建否则批量导入时会出现同名节点。参数上节点属性建议只存 name、alias、desc 三个别把大段文本塞进图里查询会变慢。批量导入时用LOAD CSV比逐条 MERGE 快一个数量级CSV 列名和 Cypher 里的属性名要对齐。3.3 用 Python 批量导入并做数据校验手工敲 Cypher 只适合演示真实项目要用 Python 脚本批量灌数据。下面是一个可复用的导入模板from neo4j import GraphDatabase import pandas as pd # 连接参数bolt 协议默认 7687 端口 driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, 你的密码)) def import_disease(tx, name, alias, desc): tx.run(MERGE (d:Disease {name: $name}) SET d.alias $alias, d.desc $desc, namename, aliasalias, descdesc) # 读 CSV列名name,alias,desc df pd.read_csv(disease.csv) with driver.session() as session: for _, row in df.iterrows(): session.execute_write(import_disease, row[name], row[alias], row[desc]) # 校验统计节点数 with driver.session() as session: count session.run(MATCH (d:Disease) RETURN count(d) AS c).single()[c] print(f疾病节点总数{count}) driver.close()逻辑说明execute_write会自动管理事务比手动 begin/commit 省心。参数上CSV 编码用 utf-8-sig否则中文列名可能带 BOM 导致读不到。校验步骤不能省导入后一定要 count 一下数量对不上说明有行被跳过。常见坑是 CSV 里有空值MERGE 时属性设成 None查询会漏掉这些节点。4. 问句解析与答案生成NER、意图识别与 Cypher 模板4.1 用 jieba 加自定义词典做实体抽取医疗问句的实体抽取课程设计级别用 jieba 加自定义词典就能到可用水平。先把疾病、症状、药品名写进一个 txt 词典每行「词 词频 词性」然后加载import jieba import jieba.posseg as pseg # 加载自定义词典格式词语 词频 词性 jieba.load_userdict(medical_dict.txt) def extract_entities(question): words pseg.cut(question) entities [] for word, flag in words: # n 是名词nz 是其他专名自定义词性用 DIS/DRU/SYM if flag in (n, nz, DIS, DRU, SYM): entities.append((word, flag)) return entities # 测试 print(extract_entities(糖尿病有哪些症状)) # 输出[(糖尿病, DIS), (症状, n)]逻辑说明pseg.cut同时返回词和词性自定义词典里把疾病标成 DIS、药品标成 DRU抽取时按词性过滤。参数上词频给一个较大值比如 1000能提高该词被切出来的优先级。这一步的边界是jieba 对嵌套实体如「2型糖尿病」里的「糖尿病」会切碎需要后处理合并。熟手可以直接上 BERTCRF 做 NER但源码类项目用 jieba 性价比最高。4.2 意图识别把问句映射到查询模板意图识别的本质是分类用户问的是「症状」「用药」「检查」还是「科室」。最简单可靠的做法是关键词匹配加规则别一上来就上模型。维护一个意图-关键词表意图触发关键词对应 Cypher 模板查症状症状、表现、征兆MATCH (d:Disease)-[:HAS_SYMPTOM]-(s)查用药吃什么药、用药、治疗MATCH (d:Disease)-[:USE_DRUG]-(dr)查检查检查、化验、项目MATCH (d:Disease)-[:NEED_CHECK]-(c)查科室挂什么科、科室MATCH (d:Disease)-[:BELONG_TO]-(dep)规则匹配的准确率在封闭领域能到 85% 以上比小样本训模型稳。参数上关键词表要覆盖同义说法「吃什么药」和「用什么药」都要列进去。如果问句里同时出现多个意图词按优先级取第一个匹配的或者返回多答案。4.3 答案生成从 Cypher 结果到自然语言查到结果后不能直接返回列表要拼成一句人话。下面是一个答案模板函数def generate_answer(intent, entity, results): if not results: return f抱歉知识库中暂未收录「{entity}」的相关信息。 items 、.join(results) templates { 查症状: f{entity}的常见症状包括{items}。, 查用药: f用于治疗{entity}的常见药物有{items}。, 查检查: f{entity}通常需要做以下检查{items}。, 查科室: f{entity}建议就诊科室{items}。, } return templates.get(intent, f查询结果{items}) # 配合查询使用 # results 来自 session.run(cypher).value() print(generate_answer(查症状, 2型糖尿病, [多饮, 多尿, 体重下降]))逻辑说明模板函数把意图、实体、结果三要素拼成句子空结果要有兜底话术否则前端会显示空白。参数上results用.value()取出来是列表直接 join 即可。这一步的坑是结果里可能有重复项查询时加DISTINCT去重。5. 避坑与排查医疗问答源码落地时最容易翻车的五件事5.1 现象问「糖尿病」查不到问「2型糖尿病」才有结果原因实体归一没做图谱里存的是全称用户输入的是简称或别名。解决在节点上加 alias 属性查询时先做别名匹配或者建一个「别名-标准名」映射表NER 抽到别名后先转标准名再查图。我一般会在 Cypher 里用WHERE d.name $name OR d.alias $name兜底。5.2 现象Neo4j 连接报「Unable to connect」但服务明明启动了原因驱动版本和服务端版本不匹配或者 bolt 端口被防火墙拦了。解决先确认服务端版本Neo4j Browser 里CALL dbms.components()驱动大版本对齐再确认 7687 端口通不通本地用telnet localhost 7687测。血泪经验是别用最新版驱动连老服务端兼容性玄学问题很多。5.3 现象jieba 把「二甲双胍」切成「二甲」「双胍」原因自定义词典没加载或者词频给太低。解决确认load_userdict在cut之前调用词典里词频给 1000 以上。如果还不行用jieba.add_word(二甲双胍, freq1000, tagDRU)动态加词。注意词典文件编码必须是 utf-8否则加载静默失败。5.4 现象批量导入几万条数据后查询越来越慢原因没建索引Neo4j 全图扫描。解决对每个实体的 name 属性建索引CREATE INDEX disease_name_index FOR (d:Disease) ON (d.name)。参数上索引建在查询最频繁的属性上别每个属性都建写入会变慢。导入前建约束约束自带索引是最省事的做法。5.5 现象前端返回的答案里混着乱码或问号原因Flask 返回的 JSON 没设ensure_asciiFalse或者数据库连接字符集不对。解决Flask 里用jsonify时加app.config[JSON_AS_ASCII] FalsePython 脚本读写文件统一用encodingutf-8。这个坑不致命但很烦排查时先看是数据库里就乱码还是只在返回时乱码。6. 进阶技巧把问答准确率从 70% 拉到 90% 的两个实操手段第一个手段是「问句改写 同义词扩展」。用户问「血糖高是什么病」直接 NER 抽不到疾病实体因为「血糖高」是症状描述。我一般维护一张症状-疾病反向表抽到症状后反查可能疾病再让用户确认。代码上就是在意图识别前加一层「症状转疾病」# 症状到疾病的候选映射 symptom_to_disease { 血糖高: [2型糖尿病, 1型糖尿病], 胸闷: [冠心病, 心绞痛], } def rewrite_question(entities): for word, flag in entities: if word in symptom_to_disease: # 返回候选疾病列表交给前端做二次选择 return symptom_to_disease[word] return None逻辑说明这层改写把「症状问句」转成「疾病问句」召回率提升明显。参数上映射表要人工审核别用模型自动生成医疗场景错一个药名就是事故。第二个手段是「查询结果缓存」。同一问题反复查图很浪费用functools.lru_cache或 Redis 缓存「问句-答案」对命中率在演示场景能到 40% 以上。缓存 key 用归一化后的问句别用原始字符串否则「糖尿病症状」和「糖尿病的症状」会算两次。最后一个习惯每次改完 NER 词典或 Cypher 模板一定跑一遍回归测试集。我一般准备 50 条标准问句和期望答案改完跑一遍看准确率有没有掉。这个后悔药比事后 debug 便宜得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表