多模态知识图谱驱动的中医智能诊疗系统构建与工程实现
简介基于Python的多模态知识图谱中医智能辅助诊疗平台源码属于毕业设计项目面向计算机专业毕业生与需要项目实战的学习者。项目以中医诊疗辅助为业务场景融合知识图谱与Web应用开发覆盖登录、诊断、结果展示、用户管理等典型功能模块可支撑从数据预处理、图谱构建到前端交互展示的完整流程。压缩包共69个文件约5.44MB主要包含4个Python核心脚本、5个HTML页面、CSS与JS前端逻辑、CSV症状词典及数十张舌象图片等多模态数据目录按模板、静态资源、图片数据等模块划分便于对照理解工程结构。已有74人学习。通过源码可重点参考Flask后端与知识图谱的集成方式、前端页面设计及症状数据组织方法项目经调试可运行难度适中适合用于毕设参考、课程设计或能力提升。1. 多模态知识图谱 中医智能诊疗毕业设计里最难、也最值钱的一段路很多人拿到这个题目时第一反应是先找 python 安装教程、翻一套现成 python 源码开始改。真正动手后才发现难点不在写接口而在“病历文本、舌象图片、证型方剂知识”这三类完全不同的数据怎么统一放进同一张图里还要让推理结果能说清楚“为什么”。这个项目的价值点也在这把多模态知识图谱真正建起来再叠一层可解释的诊疗推荐整体就是一个能演示、能答辩、能写进简历的完整系统。目标读者是准备拿这个方向做毕业设计或课程设计的本硕学生也适合想在中医信息化方向小步试水的后端开发同学。它不要求你懂中医但要求你懂数据建模和工程落地——这两点恰好是大多数验收老师最看重的。2. 先拆平台设计多模态数据链路与知识图谱选型2.1 三类多模态数据分别走什么链路建平台前必须先想清楚数据从哪里来、以什么格式进系统。我不建议一上来就写代码而是先画一条数据链路否则后面融合阶段必然翻车。第一类是结构化知识来自中医诊断学教材、方剂数据库、中草药数据库。它天然是“实体 关系”的形态比如“肝气郁结——表现为——胁肋胀痛”。这类数据可以直接清洗成三元组是知识图谱的底表。第二类是非结构化文本包括门诊病历、症状描述、用户自助输入的一段话。它需要做实体识别和关系抽取才能转成图谱节点。第三类是多模态图像典型的是舌象图片。舌象图片不能像文本那样分词得先做特征提取再把特征映射成“舌红、苔黄腻”这类图谱节点可用的属性标签。这三条链路不是三条平行线。文本里说“情绪抑郁”舌象图片给出“舌红”结构化知识里又写着“肝气郁结常见于情绪抑郁、舌红脉弦”——你会需要一套对齐机制让三种模态指向同一个证型节点。这条对齐机制就是整个平台的核心工作量。2.2 为什么用图存储而不是关系型数据库这是很多毕设组纠结最久的问题。老老实实建三张 MySQL 表不行吗症状表、证型表、方剂表外键关联一下也能查出“这个证型对应哪些方剂”。但中医诊疗查询天然是多跳的用户输入“胁肋胀痛 情绪抑郁”系统要查“这两个症状共同指向哪个证型”再查“该证型适合哪几个方剂、有哪些禁忌”。关系型数据库做两跳 JOIN 还勉强做到三跳、四跳SQL 就变成一张巨大的嵌套查询性能和维护成本同时失控。知识图谱把“节点 关系”作为一等公民多跳查询就是顺着边游走。平台后期如果需要加新关系比如“某味药与某证型存在配伍禁忌”图数据库可以直接声明新边类型不用改表结构。我在实际项目中对比过同样一个四跳查询Neo4j 比 MySQL 改写后的五次 JOIN 大约快一个数量级而且查询语句可读性高得多答辩时也更好解释。所以选型结论很直接图谱部分用 Neo4j前端与推荐服务用 Python 接一层驱动。2.3 最小可运行工程目录与核心数据模型动手写代码前先把工程切好。下面这个目录结构是我做过几个同类项目后沉淀下来的最小骨架按这个结构至少不会在答辩前夜发现后端和数据处理代码堆在一起乱成一团。tcm_platform/ ├── data/ │ ├── raw/ # 原始病历、舌象图、方剂表 │ ├── processed/ # 清洗后的三元组、特征标签 │ └── dictionary/ # 自定义词典、同义词表、停用词表 ├── kg/ │ ├── build_graph.py # 三元组写入 Neo4j │ ├── ner.py # 文本实体识别与关系抽取 │ ├── tongue_feature.py # 舌象图像特征提取 │ └── align.py # 多模态实体对齐 ├── service/ │ ├── recommend.py # 辨证推荐与评分逻辑 │ ├── app.py # Flask/FastAPI 接口 │ └── query.py # Cypher 查询封装 ├── tests/ # 实体识别与推荐结果测试 └── requirements.txt数据模型核心就四类节点症状Symptom、证型Syndrome、方剂Formula、中药Herb外加舌象特征TongueFeature作为辅助属性节点。关系主要有“表现为”“治疗用”“包含”“禁忌”。这个模型不追求覆盖完整中医知识体系——那是科研团队做的事毕设需要的是清晰、可扩展、能讲出设计理由。3. 构建中医多模态知识图谱文本、舌象与图写入的四个阶段3.1 结构化知识清洗与三元组生成第一步先把教材和数据库里的知识整理成机器可读的三元组。我一般用 pandas 做清洗因为这类数据通常带大量空值、别名和冗余描述。常见做法是先把原始表拆成“实体表”和“关系表”再做去重和归一化。下面这段代码演示如何从一份方剂 CSV 里提取“方剂—包含—中药”三元组并清洗掉重复项。import pandas as pd df pd.read_csv(data/raw/formulas.csv) # 只保留关键列并删除全空行 df df[[formula_name, herb_list, syndrome]].dropna(subset[herb_list]) # 方剂的药物组成用逗号分隔拆成多行三元组 triples [] for _, row in df.iterrows(): herbs [h.strip() for h in row[herb_list].split(,) if h.strip()] for herb in herbs: triples.append({ head: row[formula_name], relation: contains, tail: herb }) triples_df pd.DataFrame(triples) triples_df triples_df.drop_duplicates() # 输出三元组文件供后续查询去重使用 triples_df.to_csv(data/processed/formula_herb_triples.csv, indexFalse, encodingutf-8-sig) print(f生成三元组 {len(triples_df)} 条)逻辑说明核心思路是把一行多值字段打散成多行标准化三元组。encodingutf-8-sig是为了防止 Windows 下 Excel 打开 CSV 出现中文乱码这是处理中医数据的血泪经验。参数层面dropna(subset[herb_list])很关键方剂如果缺少组成药物这条知识就没有图谱价值直接丢掉比后面报错强。3.2 非结构化文本实体识别与关系抽取词典加规则的轻量方案中医病历文本的实体识别是很多毕设同学第一个黑匣子。多数项目一上来就想用 BERT结果标注数据只有几百条微调出来的模型在测试集上表现尚可一到真实病历就全面崩盘。我的建议是第一阶段先做“词典 正则 规则窗口”的轻量抽取它可解释、可快速迭代还能产出标注语料等数据积累到几千条再考虑训练序列标注模型。下面是一个可运行的抽取流程示例。import jieba import re # 自定义词典保证分词时中医术语不被拆散 DICT_PATH data/dictionary/tcm_dict.txt # tcm_dict.txt 每行格式词 词频 词性 # 例如肝气郁结 50 nz jieba.load_userdict(DICT_PATH) TEXT 患者近一周胁肋胀痛情绪抑郁舌红苔薄黄脉弦。 # 第一步症状实体识别用词典匹配加正则兜底 symptom_patterns [ 胁肋胀痛, 情绪抑郁, 舌红, 苔薄黄, 脉弦 ] found_symptoms [s for s in symptom_patterns if s in TEXT] # 第二步关系抽取使用触发词窗口 relation_rules [ (表现为, [情绪抑郁, 胁肋胀痛]), ] extracted [] for symptom in found_symptoms: # 模拟规则病历中同时出现两个症状默认共同指向同一证型 extracted.append({ symptom: symptom, syndrome_candidate: 肝气郁结 }) print(jieba.lcut(TEXT)) print(识别症状:, found_symptoms) print(抽取结果:, extracted)逻辑说明jieba.load_userdict是这一步的灵魂中医术语如果不加词典会被切成“肝气/郁结”甚至更碎后面所有匹配都会失效。代码中relation_rules是触发词规则的简化版真实项目里我会把规则写成“左窗口 右窗口 目标关系”的配置结构比如检测到“伴有、兼见、加之”这类连接词就认为左右两侧症状属于同一证型。参数层面tcm_dict.txt的词频会影响 jieba 的切分优先级中医术语建议统一标记为nz词性并把词频调高到 50 以上否则在长句里优先级不够分词结果依旧不稳定。3.3 舌象图像特征提取与多模态节点对齐舌象图是典型的多模态输入。完整方案是先做舌体分割再提取颜色纹理特征但舌体分割本身就是一个大坑毕业设计阶段直接用 OpenCV 加人工裁剪是性价比较高的做法真正上手也快。下面的代码演示如何从舌象图片提取 HSV 颜色特征并映射到“舌红、苔黄”这类知识图谱属性标签。import cv2 import numpy as np def extract_tongue_features(image_path): img cv2.imread(image_path) img_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 这里假设输入已经是裁剪好的舌体区域 # 统计整图的Hue和Saturation分布 h_channel img_hsv[:, :, 0] s_channel img_hsv[:, :, 1] # 简化规则平均色相偏高 - 舌红饱和度偏高 - 苔厚 avg_hue np.mean(h_channel) avg_sat np.mean(s_channel) features [] if avg_hue 8 or avg_hue 170: features.append(舌红) elif avg_hue 20: features.append(舌淡红) if avg_sat 60: features.append(苔黄) elif avg_sat 30: features.append(苔白) return features if __name__ __main__: print(extract_tongue_features(data/raw/tongue_sample.jpg))逻辑说明HSV 比 RGB 更适合做舌象颜色判断因为 RGB 对光照变化过于敏感同样的舌色在不同光源下数值差异巨大而 Hue 分量能保持稳定。代码里的阈值是我根据几十张公开舌象图统计出来的经验值不同数据集需要重新标定。多模态对齐的坑在于文本里写“舌红”图片特征给出“舌偏红”如果不对齐知识图谱里会出现两个相似却不同的节点。解决办法是维护一个同义词映射表把“舌偏红、舌尖红、舌质红”全部归一到“舌红”图像特征和文本特征最终都走这张表。3.4 用官方 Driver 批量写入 Neo4jUNWIND 复用事务图谱构建的最后一步是把三元组写进 Neo4j。很多初学者会用 py2neo 逐条循环插入数据量一到几千条就慢到怀疑人生。正确做法是用官方neo4jPython Driver配合UNWIND批量创建把网络请求次数从几千次降到一个事务。from neo4j import GraphDatabase URI bolt://localhost:7687 AUTH (neo4j, your_password) # 换成实际密码 driver GraphDatabase.driver(URI, authAUTH) def load_triples(triples): with driver.session() as session: # 使用 UNWIND 批量创建节点和关系 session.run( UNWIND $rows AS row MERGE (f:Formula {name: row.head}) MERGE (h:Herb {name: row.tail}) MERGE (f)-[r:CONTAINS]-(h) , rowstriples ) driver.close() if __name__ __main__: triples [ {head: 逍遥散, tail: 柴胡, relation: contains}, {head: 逍遥散, tail: 白芍, relation: contains}, ] load_triples(triples)逻辑说明这里有两个关键参数一是rows直接传入整个三元组列表由UNWIND内部展开避免 Python 循环二是MERGE而不是CREATE因为同类中药可能出现在不同方剂中MERGE会自动去重节点。实际写入时单事务行数建议控制在 5000 行以内超过后 Neo4j 会占用大量内存如果总数据量超过十万行可以分批调用每批一个事务这样即使中途失败重跑一次也不会产生重复节点。4. 智能辅助诊疗推理从 Cypher 查询到可解释推荐4.1 辨证推理的评分算法与规则模板图谱建好以后诊疗推荐的核心是把“症状输入”变成“证型候选”再对候选证型按匹配度打分。推荐算法不需要复杂一个可解释的加权评分模型通常比黑盒模型更适合这张场景老师也问得住。我的常见做法是这样预先定义每个证型的症状权重表用户每命中一个症状就累加对应的权重权重总和超过预设阈值的证型进入候选列表。权重不是玄学可以先用中医教材里的“必备症 常见症 次要症”三级权重再逐步调整。syndrome_rules { 肝气郁结: { mandatory: [胁肋胀痛, 情绪抑郁], common: [善太息, 脉弦], minor: [舌红], weights: {mandatory: 3, common: 2, minor: 1} } } user_symptoms [胁肋胀痛, 情绪抑郁, 舌红] def score_syndrome(user_symptoms, rule): total_score 0 hit_detail [] for s in rule[mandatory]: if s in user_symptoms: total_score rule[weights][mandatory] hit_detail.append(s) for s in rule[common]: if s in user_symptoms: total_score rule[weights][common] hit_detail.append(s) for s in rule[minor]: if s in user_symptoms: total_score rule[weights][minor] hit_detail.append(s) return total_score, hit_detail score, hits score_syndrome(user_symptoms, syndrome_rules[肝气郁结]) print(f得分: {score}, 命中项: {hits})逻辑说明权重设计的原则是“必备症决定生死、常见症提高置信度、次要症做微调”。现实中必然出现用户只报了次要症状的情况所以还要加一个最低准入线比如必备症至少命中一项否则直接不推荐。这套逻辑可以平移到“痰湿内阻、脾胃虚寒”等其他证型维护方式就是往syndrome_rules字典里加条目。4.2 多模态特征如何参与查询把图像标签变成图查询条件文本症状参与评分很自然但舌象图片的特征怎么进推理常见误区是把图像特征单独训练一个分类模型然后把分类结果和文本结果拼在一起这种方案两个模态各自黑盒平台整体难以解释。我建议换一个思路把图像提取出的特征舌红、苔黄当作“约束条件”参与到图谱查询里而不是训练模型。也就是说舌象特征先映射成图谱节点再作为 Cypher 查询的过滤条件图谱边本身承担了模态间的对齐作用。4.3 一条完整的诊疗推荐链路接收输入、图谱查询、返回方剂与禁忌把评分逻辑和图谱查询串起来就是完整的推荐服务。下面是一个 Flask 接口的简化版本它接收用户症状列表和舌象特征标签返回候选证型、方剂和用药禁忌。from flask import Flask, request, jsonify from neo4j import GraphDatabase app Flask(__name__) driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, password)) recommend_template MATCH (s:Symptom)-[:BELONGS_TO]-(z:Syndrome) WHERE s.name IN $symptoms WITH z, count(DISTINCT s) AS hit_count ORDER BY hit_count DESC LIMIT 3 MATCH (z)-[:TREATED_BY]-(f:Formula) MATCH (f)-[:CONTAINS]-(h:Herb) OPTIONAL MATCH (h)-[:HAS_CONTRAINDICATION]-(c:Contraindication) RETURN z.name AS syndrome, f.name AS formula, collect(DISTINCT h.name) AS herbs, collect(DISTINCT c.name) AS warnings app.route(/recommend, methods[POST]) def recommend(): data request.get_json() symptoms data.get(symptoms, []) tongue_features data.get(tongue_features, []) # 多模态特征统一并入症状条件 all_features list(set(symptoms tongue_features)) with driver.session() as session: result session.run(recommend_template, symptomsall_features) records [dict(r) for r in result] return jsonify({result: records}) app.route(/, methods[GET]) def health_check(): return jsonify({status: ok}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明这段代码把文本症状和舌象特征合并在一个all_features列表里然后统一进入 Cypher 查询。count(DISTINCT s)是排序核心它统计每个证型命中了多少个特征命中越多越靠前。OPTIONAL MATCH非常关键它保证某味药没有禁忌记录时查询不会因为空值而丢掉整条方剂结果。参数层面LIMIT 3是推荐条数毕设答辩时建议保持默认如果真实用户反馈推荐太宽泛可以收紧到 2同时提高评分阈值。5. 这类平台最容易翻车的 5 个坑与排查顺序5.1 实体识别效果像黑匣子症状词典怎么补都不准现象用 jieba 分词之后症状识别忽好忽坏“胃脘痛”能识别“胃部隐痛”却漏掉换了一段病历结果更飘忽零乱。原因规则方案没有覆盖症状的等价表达也没有做文本归一化而词典词频设置错误时长词优先级不足分词结果直接崩坏。解决先归一化再匹配把“胃部隐痛、胃中隐痛、胃痛隐隐”统一替换成“胃隐痛”之后再做词典匹配同时检查自定义词典词频所有中医术语统一用高于 50 的词频避免被 jieba 内部默认词频压制。5.2 Neo4j 写入慢到怀疑人生小数据量跑了半小时现象一千条三元组写入 Neo4j 花了半个多小时期间 CPU 占用不高但接口一直阻塞。原因在 Python 里用 for 循环逐条执行CREATE每一条都独立开启一次事务网络往返次数爆炸。解决改成官方 Driver 加UNWIND批量写入单事务控制在 5000 行以内。改完以后一万条三元组大概十几秒就能落库这是最明显的一次效率提升。5.3 文本症状和舌象特征对不齐图谱里出现两套“舌红”现象文本抽取出来“舌红”图像特征映射出“舌偏红”图谱里出现两个相似节点推荐时舌象特征完全派不上用场。原因两套特征各走各的映射表没有做术语对齐。解决统一维护一张同义词映射表把“舌偏红、舌尖红、舌质红”全部收敛到“舌红”标准节点。映射表也要反向使用用户输入“舌红”时系统可以把同义词列表一并作为查询条件提升召回率。5.4 前端调用推荐接口超时Flask 单线程阻塞明显现象图片上传以后浏览器转圈一分钟才出结果再点一次直接超时。原因Flask 开发服务器默认单线程舌象特征提取加图谱查询都在请求线程里同步执行图片处理慢时整个服务被阻塞。解决把舌象特征提取从推荐接口里拆出去异步处理后再回调查询或者先用 FastAPI 替代 Flask性能和并发能力会好很多。毕设演示前至少做一次压力测试连续请求十次看看有没有超时。5.5 Python 多版本和依赖互相污染答辩前环境又起不来了现象今天装 opencv 把 numpy 升了明天装 py2neo 又降了 pandas最后 flask 起不来图谱写入脚本也报错。原因没有用虚拟环境依赖版本被反复覆盖。解决所有依赖锁定在requirements.txt里固定大版本号用python -m venv venv创建独立环境不要贪方便直接用全局 Python。另外Neo4j 驱动版本差异极大4.x 和 5.x 的 API 不兼容写 AUTH 参数时务必确认和本地数据库版本一致否则 Authentication 错误会耗掉一下午。6. 答辩与验收导向指标怎么算、演示怎么走、进阶怎么做毕设验收最怕“系统能跑但说不清好坏”。建议提前准备三组指标实体识别准确率直接从测试病历里抽二十段人工标注对比系统识别结果算精确率和召回率推荐命中率准备十五个标准病例看系统推荐的前三个方剂里是否包含中医教材给出的标准方解释一致性检查推荐结果的依据是否是用户输入的原始症状而不是图谱里碰巧命中的共现项。这三组指标做完答辩老师再追问你都能拿数字说话。演示路径也要刻意设计。不要现场用随机输入建议准备一条固定演示链输入一段包含“胁肋胀痛、情绪抑郁、舌红苔黄”的病历系统推荐逍遥散加减前端展示证型、方剂、药材组成与禁忌。这条链路要提前跑通图片和文本数据都准备好避免现场等图像处理和推理超过十秒。最后说一个值得做但被多数人忽略的进阶点把知识图谱的节点和图谱拓扑结合起来用 Node2Vec 训练节点嵌入向量然后让“症状—证型—方剂”这条链上的节点向量参与语义检索。比如用户输入非标准症状“最近容易生闷气”图查询查不到直接匹配但向量检索可以找到相近的“情绪抑郁”节点再走图谱规则推荐。这样做的好处是保留了原有规则的可靠性又具备了近似检索的容错能力毕设的创新点也立住了。我现在验收类似项目必看三件事数据生成链路是否闭环、推荐结果是否可解释、换一批病历系统是否还能稳定输出。顺序对了后面就顺了希望帮到你。本文还有配套的精品资源点击获取