ARTICLE DETAIL

资讯详情

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

医学知识图谱问答系统源码解析:基于Neo4j与Python的完整实践

医学知识图谱问答系统源码解析:基于Neo4j与Python的完整实践 简介这套源码是面向医学信息处理场景的Python知识图谱问答系统适合有Python基础、关注人工智能医学应用的开发者解决医学知识检索效率与专业查询门槛问题。资源包共31个文件以Python源码为核心覆盖知识图谱构建、问题分类、意图解析、答案检索等模块并搭配医学数据、示例图片、说明文档与演示文稿约49.19MB已有176人学习下载。项目以build_medicalgraph.py构建知识图谱question_parser.py解析查询意图answer_search.py完成答案检索整合为完整问答链路数据目录内置医学术语与实体关系便于复现与二次开发。下载即可获得全部源码、配套医学数据、readme文档、运行效果图与PPT汇报材料适合中高级开发者系统学习。1. 医学知识图谱问答系统先从一份能跑的 Python 源码说起我最初接触这份源码是因为要给一个医疗信息检索的 demo 找骨架。翻完chatbot_graph.py和build_medicalgraph.py后发现它并不是那种只给界面、逻辑全靠猜的玩具项目——从数据准备、实体词典到 Neo4j 图谱构建、问题意图分类再到 Cypher 查询和答案组织整条链路是完整闭环的。也就是说你拿到的是「上传原始医疗数据 → 构建医学知识图谱 → 跑起一个能对话的问答机器人」的全套工程代码而不是某个孤立算法片段。这套东西对两类人最有用一类是想在课程设计或简历项目里落地知识图谱问答的在校生另一类是刚接触医学信息处理、想快速验证 Neo4j Python 技术栈的开发者。下面我按自己拆项目的习惯从数据怎么来、图谱怎么建、问答链路怎么走到部署和踩坑一条条给你讲透。2. 先看数据怎么来prepare_data、data 与词典文件的角色2.1 原始数据形态从 JSON 到词典文件打开data目录能看到的是一批 txt 词典包括disease.txt、drug.txt、food.txt、producer.txt、symptom.txt、check.txt、department.txt。这些文件本质上就是实体词典每条记录对应一个医学实体名比如疾病名称、药品名称、检查项目、所属科室等。在问答系统的 pipeline 里它们承担两个任务一是用于构建知识图谱时的实体节点去重二是用于question_classifier.py做问题实体抽取时的匹配源。再看prepare_data目录里面是原始数据的准备逻辑。常见做法是先抓取或整理出结构化的医疗数据再写成中间 JSON 文件。代码包里medical.json就是这个中间产物它把实体属性、实体间关系组织成了统一的键值结构。build_data.py和data_spider.py的分工很清晰后者负责从公开数据源采集前者负责清洗和结构化。我一般建议直接在build_data.py里改数据源路径因为它把解析逻辑收口在一个地方方便后续维护。2.2 数据清洗时的候选生成与最大匹配max_cut.py是整个数据准备链路里容易被忽略但很关键的文件。它的作用是对文本做基于词典的最大正向匹配分词。为什么要自己做分词而不是直接上 jieba因为在医学场景下通用分词器经常把「过敏性鼻炎」切成「过敏性」和「鼻炎」而医学实体要求全词匹配否则图谱里的节点就对不上。max_cut.py实现的是正向最大匹配算法逻辑提炼出来就是def max_forward_cut(sentence, word_dict): # 正向最大匹配从句首开始按最大词长优先匹配 result [] i 0 max_len 5 # 根据词典中最长实体长度调整一般为 5~8 while i len(sentence): matched False # 从当前指针位置由长到短尝试匹配 for j in range(max_len, 0, -1): word sentence[i:i j] if word in word_dict: result.append(word) i j matched True break if not matched: # 词典中找不到按单字切分并跳过 result.append(sentence[i]) i 1 return result这里max_len是在性能和精度之间的折中设得太大短句匹配会做很多无效尝试设得太小长实体永远匹配不上。医学实体里「冠状动脉粥样硬化性心脏病」这类名称很长我一般会把max_len调到 8 附近。词典的加载方式也值得注意——要转成 set 而不是 list因为 set 的成员判断是 O(1)在data_spider.py抓取大量文本时会明显降低耗时。实际跑数据准备时建议先小批量跑一遍把分词结果打印出来看确认没有把实体拦腰切断后再全量执行。2.3 实体与关系的结构化输出数据准备阶段的最终产物是实体表和关系表。实体表涵盖疾病、症状、药物、食物、检查、科室、药品生产商这些类型关系表则定义了疾病与症状的「表现为」关系、疾病与药物的「治疗用药」关系、疾病与科室的「所属科室」关系、疾病与食物的「宜吃/忌吃」关系等。这些在medical.json中都能找到对应字段。构建图谱时build_medicalgraph.py会读取这个 JSON在 Neo4j 里为每个实体创建节点为每个关系创建边。一句话总结数据准备不是杂活它决定了图谱的边界和问答的精度。后面所有意图识别、实体匹配、查询模板全部依赖这一步产出的词典和 JSON。3. 构建医学知识图谱build_medicalgraph.py 的节点与关系设计3.1 为什么选 Neo4j 而不是关系型数据库知识图谱问答的查询模式是「给定实体找多跳关系」比如用户问「高血压应该注意什么饮食」翻译成图谱操作就是找到“高血压”这个疾病节点沿着「忌吃」边找到食物节点再返回这些食物名称。这类多跳查询如果用 MySQL 实现要么写一堆 JOIN要么在应用层递归查性能和代码复杂度都很糟糕。Neo4j 的 Cypher 查询语言天然支持多跳模式匹配一个MATCH (d:Disease)-[:忌吃]-(f:Food) WHERE d.name高血压 RETURN f.name就搞定了。这也是这份源码选 Neo4j 的原因。3.2 建图脚本核心逻辑与 Cypher 写库build_medicalgraph.py的主干逻辑可以分成三步连接 Neo4j、清空旧数据、循环写入节点和关系。核心片段如下from py2neo import Graph, Node, Relationship # 连接 Neo4j默认 bolt 端口号 7687账号密码按本地环境改 graph Graph(http://localhost:7474, auth(neo4j, 123456)) # 清空图数据库避免重复导入产生脏数据 graph.run(MATCH (n) DETACH DELETE n) # 以疾病节点为例将 medical.json 中的实体写入图库 for disease_item in medical_data[disease_list]: disease_node Node(Disease, namedisease_item[name], descdisease_item.get(desc, ), categorydisease_item.get(category, )) graph.create(disease_node) # 处理“常用药品”关系目标节点为 Drug 类型 for drug_name in disease_item.get(drug_list, []): drug_node graph.nodes.match(Drug, namedrug_name).first() if drug_node is None: drug_node Node(Drug, namedrug_name) graph.create(drug_node) rel Relationship(disease_node, 治疗用药, drug_node) graph.create(rel)这段代码里有几个工程细节graph.nodes.match(...).first()是典型的「先查后建」写法避免重复创建同名节点get方法带默认值保证缺失字段不会让脚本崩溃。写入关系前先确认两个节点都存在否则 Neo4j 会报「关系端点缺失」的错误。对于食品、检查、科室这些节点逻辑完全一致区别仅在节点标签和关系类型上。节点标签建议统一用英文首字母大写形式比如Disease、Drug、Food、Check关系用中文描述比如治疗用药、宜吃、忌吃、所属科室。这种混搭的好处是查询语句里能一眼看出边的业务含义同时节点类型保持稳定。建完图后在 Neo4j Browser 里执行MATCH (n:Disease) RETURN n LIMIT 25如果能看到节点和关系正确渲染图谱构建就成功了。3.3 图谱结构对问答的支撑意义图谱结构的质量直接决定问答系统能回答什么。比如用户问「肺炎有哪些症状」系统需要先从Disease节点匹配到“肺炎”再沿表现为边找到Symptom节点。如果图谱里这个关系缺失或方向反了答案自然为空。此外图谱中同一实体的别名问题也值得留意比如「乙肝」和「乙型肝炎」如果不同时写入图谱用户用「乙肝」提问时就匹配不到。代码包里没有单独做实体对齐实际使用时可以在build_data.py阶段人工维护一份别名映射表把同义实体归一化后再写入图谱。4. 问答链路拆解question_classifier、question_parser 与 answer_search4.1 问题分类器怎么判断用户想问什么question_classifier.py做的事是把用户的自然语言问题归类到预设意图上。这份源码的意图分类采用基于特征词表的规则方法而不是深度学习模型。原因很现实医学问答的意图类别相对固定规则方法可控、可解释、不依赖训练数据。项目里定义的意图大致可以分成这么几类意图类别用户问题示例特征词疾病症状查询肺炎有什么症状症状、表现、临床治疗用药查询高血压吃什么药药、治疗、用药宜吃食物查询糖尿病适合吃什么宜吃、饮食、吃什么忌吃食物查询感冒不能吃什么忌吃、不能吃、避免检查项目查询乙肝需要做什么检查检查、检测、查所属科室查询骨折挂什么科挂科、科室、门诊疾病定义查询什么是冠心病什么是、定义、介绍特征词表是硬编码在question_classifier.py里的每个意图对应一组关键词。分类时遍历所有意图计算问题文本命中的特征词数量取命中最多的作为分类结果。如果最高分出现并列就按优先级取前面的意图这个优先级其实就是特征词的置信度排序。这种方案虽然简单但在限定领域内准确率相当高跑通 demo 完全够用。4.2 问题解析器用正则从问题里抠实体question_parser.py负责两件事第一从问题文本中抽取出医学实体第二根据意图和实体生成对应的 Cypher 查询语句。实体抽取的核心是正则匹配代码结构大概是这样的import re def extract_entity(self, question, word_dict): # 按词典中最长实体优先的原则逐个尝试匹配 for word in sorted(word_dict, keylambda x: len(x), reverseTrue): if re.search(word, question): return word return None这里有个关键点实体匹配用的是「最长优先」策略而不是「最先出现」策略。比如问题「病毒性肺炎吃什么药」词典里同时有「肺炎」和「病毒性肺炎」必须优先匹配后者否则后面 Cypher 查询会定位到错误节点。word_dict的来源就是前面data目录下的那些 txt 文件和medical.json里的实体集合。实际调试时最常遇到的现象是「实体没抽出来」这时候优先怀疑词典里没有收录该词其次才是正则写法问题。生成 Cypher 的部分是根据意图和实体拼接查询语句。以「疾病症状查询」和「治疗用药查询」为例# 根据意图和实体拼装不同的 Cypher 模板 if question_type disease_symptom: cypher MATCH (d:Disease)-[:表现为]-(s:Symptom) \ WHERE d.name{entity} RETURN s.name.format(entityentity) elif question_type disease_drug: cypher MATCH (d:Disease)-[:治疗用药]-(dr:Drug) \ WHERE d.name{entity} RETURN dr.name.format(entityentity)你可能注意到了Cypher 是用字符串拼接出来的存在注入风险。但由于这个系统的输入是终端命令行且实体来自可控词典实际风险不大。如果你要接 Web 接口建议改成参数化查询比如py2neo的graph.run(cypher, entityentity)写法让实体作为参数传入而不是拼进字符串。4.3 答案搜索器把查询结果转成自然语言答复answer_search.py是查询链路的最后一环。它从question_parser拿到 Cypher执行查询得到一组节点再把这些节点包装成用户能读懂的话。比如查询结果是[布洛芬, 对乙酰氨基酚]answer_search.py会根据意图类型在结果前补上合适的引导语「根据您的查询常用的治疗药物包括布洛芬、对乙酰氨基酚」。多结果合并时的去重值得留意。Neo4j 的MATCH返回结果可能包含重复项特别是通过多路径匹配到同一节点时必须用set去重后再拼装答案否则用户会看到重复的药名。此外这种基于模板的答复方式本质上没有做推理和排序所有结果一视同仁地展示。在 demo 场景没问题如果要接近临床可用还需要按证据强度或频次给结果排序那属于后续优化方向了。4.4 对话机器人把三件套串成完整流程chatbot_graph.py是主入口它把分类、解析、搜索三件套串起来形成一个循环对话流程。逻辑核心是接收用户输入 → 调用question_classifier得到意图 → 调用question_parser得到实体和 Cypher → 调用answer_search得到答案 → 打印给用户。判断循环是否结束时一般用输入是否为quit或exit来退出。while True: question input(用户) if question.strip() in (quit, exit): break # 分类问题意图例如 disease_symptom / disease_drug question_type classifier.classify(question) # 解析实体并生成 Cypher 查询 cypher parser.get_cypher(question, question_type) # 在图谱中执行查询生成自然语言答案 answer searcher.search(cypher) print(助手, answer)三个角色之间的数据流很干净分类器只输出意图字符串解析器只输出 Cypher搜索器只处理 Cypher 并返回答案文本。每一层都可以独立替换比如把规则分类器换成 BERT 分类模型不会影响下游代码。这种解耦设计是这份源码里最值得学习的地方。5. 避坑与排查四个最容易翻车的真实案例5.1 Neo4j 连不上服务没起或者端口写错现象运行build_medicalgraph.py时报py2neo.errors.ConnectionUnavailable或Unauthorized。原因90% 的情况是 Neo4j 服务没启动或者默认密码没改。另外Neo4j 4.x 之后默认启用 bolt 端口 7687但py2neo连接串如果只用http://localhost:7474某些版本会尝试用 bolt 协议重定向导致握手失败。解决先确认 Neo4j 已启动浏览器访问http://localhost:7474能看到管理界面再用py2neo.Graph(bolt://localhost:7687, auth(neo4j, 你改的密码))连接。第一次登录 Neo4j 会强制修改默认密码连接前务必到管理界面把密码改掉。5.2 查询结果总为空不是代码问题是实体没进图谱现象问「肺炎有什么症状」返回空列表但图谱里明明能看到肺炎节点。原因question_parser里的实体匹配用了re.search而re.search是子串匹配不是全词匹配。当用户问「大叶性肺炎有什么症状」时匹配到的实体是「肺炎」而不是「大叶性肺炎」图谱里没有单独的「肺炎」节点只有「大叶性肺炎」于是查询为空。解决区分两种处理策略。一种是实体匹配时优先精确匹配如果整个问题在词典中有完整实体用完整实体另一种是图谱里同时建一个「肺炎」的父类节点把「大叶性肺炎」作为子类。两个方案各有适用场景demo 阶段我一般用前者改动最小。5.3 重复节点多写库时没做存在性检查现象Neo4j Browser 里查MATCH (n:Drug) RETURN count(n)发现同名药物出现了好几十条。原因build_medicalgraph.py写入关系前对新实体节点直接graph.create()没先查一下这个名称的节点是否已存在。当同一药物出现在多个疾病的「治疗用药」列表里时就被重复创建。解决在创建任何实体前先graph.nodes.match(Drug, namedrug_name).first()查一遍存在则复用不存在才创建。你也可以在 Neo4j 里给节点加唯一约束比如CREATE CONSTRAINT ON (d:Drug) ASSERT d.name IS UNIQUE这样重复写入会直接报错方便提前发现问题。5.4 中文路径乱码Windows 下读数据文件失败现象运行时日志显示UnicodeDecodeError或者在 Windows 终端下打开 txt 文件时中文全部乱码。原因data目录下的词典文件如果用的是 UTF-8 编码而 Windows 下open()默认用 GBK 解码就会出问题。解决所有open()调用统一指定encodingutf-8比如open(data/disease.txt, r, encodingutf-8)。如果你发现文件本身是 GBK 编码就改用encodinggbk。这是一行代码的问题但不写清楚能让初学者查一下午。6. 部署与运行验证从源码到能对话的完整流程6.1 环境准备与依赖安装这份源码的运行环境要求不高Windows、macOS、Linux 都能跑。Python 版本建议 3.6 到 3.8 之间因为py2neo和pyahocorasick如果有用到在更高版本下可能存在兼容问题。依赖的核心包只有两个py2neo和Flask如果需要 Web API。安装命令pip install py2neo2021.2.3 pip install flaskpy2neo的版本是个大坑4.x 之后的 API 与 3.x 差别很大比如Graph.run()的返回处理方式不同。源码是按 2021.2.3 版写的如果你用了最新版 5.xgraph.nodes.match()的写法会有兼容性问题。装完依赖后建议在 Python 里先跑一句from py2neo import Graph验证安装成功避免后面排查半天才发现是导入失败。6.2 按顺序执行三件事准备数据 → 建图谱 → 跑问答环境就绪后按顺序执行数据准备、图谱构建和问答启动三个步骤完整命令如下# 1. 进入项目根目录先看一下数据文件是否存在 ls data/ cat medical.json | head -n 20 # 2. 执行建图脚本前提是 Neo4j 已启动 python build_medicalgraph.py # 3. 启动问答系统进入交互模式 python chatbot_graph.py第二步执行时如果终端输出了一堆节点创建日志没有报错说明建图成功。第三步启动后终端会进入对话循环。现在你可以用最基础的四类问题测试系统的响应症状类肺炎有什么症状用药类高血压吃什么药饮食类糖尿病宜吃什么 / 感冒忌吃什么科室类骨折挂什么科如果这几个问题都能返回非空结果说明整条链路是通的。此时再问一些变体问题比如「冠心病的临床表现」或「乙肝不能吃什么」感受一下系统在实体匹配和意图分类上的边界在哪里。6.3 给新手的一个验证手法把中间结果打印出来如果某个问题没返回期望答案不要急着改代码。我习惯先在chatbot_graph.py的循环里临时加上打印语句把每一步的中间产物暴露出来print([DEBUG] 意图, question_type) print([DEBUG] Cypher, cypher) print([DEBUG] 答案原始结果, answer)通过这三行你能快速定位问题出在哪个环节意图标错了是分类器的问题Cypher 里实体为空是解析器没抽到Cypher 正确但结果为空是图谱数据缺失。这套方法我在拆任何知识图谱问答项目时都会用排查效率远高于直接改查询模板。6.4 一个值得尝试的进阶改动接入简单 Web 接口命令行交互适合验证功能如果想展示给评审或朋友看可以包一层极简的 Flask 接口。核心思路是复用chatbot_graph.py里的三个类只把input()和print()换成 HTTP 请求和 JSON 响应from flask import Flask, request, jsonify from chatbot_graph import ChatBotGraph app Flask(__name__) handler ChatBotGraph() app.route(/qa, methods[POST]) def qa(): data request.get_json() question data.get(question, ) answer handler.answer(question) # 实际项目中ChatBotGraph 需要封装 answer 方法 return jsonify({answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port5000)注意源码里chatbot_graph.py的交互逻辑是写在while循环里的直接拿来做 Web 接口需要把单轮会话逻辑抽成一个answer(question)方法这个改动大概十五分钟能完成但对「让项目可演示」这个目标帮助极大。从那以后我每次拿到类似的知识图谱问答项目都会先把交互流程跑通一遍再决定是加 Web 层还是做意图扩展。这套「先跑通主线再动手改」的顺序算是拆了这么多源码项目后最值钱的一条习惯希望帮到你。本文还有配套的精品资源点击获取
返回列表