
简介这份资源面向计算机相关专业的毕业设计、课程设计及项目开发学习者提供一套基于Python的真菌性中医皮肤病知识图谱与辅助诊断系统完整实现。项目围绕中医皮肤病领域将疾病信息、病因、临床表现、药方与治疗方法等结构化数据组织为知识图谱并在此基础上实现智能问诊、辅助诊断、治疗方案推荐及疗效评估反馈等功能同时包含层次聚类与关联规则分析等数据挖掘环节适合作为知识图谱与医疗辅助诊断方向的学习参考。资源包共20个文件约12.26MB以6个Python源码文件为核心辅以json数据文件、xlsx词典与转置结果表、png可视化图表及说明文档覆盖从图谱构建到模型应用的完整链路。目前已有210人学习下载。读者可据此理解知识图谱构建、聚类与关联规则分析、诊断模型设计及结果可视化的整体思路并在此基础上延伸开发。1. 真菌性中医皮肤病知识图谱从散落医案到可查询的诊断网络皮肤科门诊里最常听到的一句话是「这到底是湿疹还是癣」。真菌性皮肤病和湿疹、银屑病在皮损形态上高度重叠而中医辨证又讲究「望闻问切」四诊合参年轻医生很难在几分钟内把「湿热下注」「血虚风燥」这些证型和具体方剂对应准确。这个项目的核心思路是把中医皮肤科教材、临床医案、方剂学资料里的实体和关系抽出来用 Python 搭一套知识图谱再在图谱之上做一层辅助诊断——输入症状和舌脉系统给出可能的证型、对应方剂和相似医案。它解决的不是「替代医生」而是把散落在几十本教材和几百份医案里的隐性关联显性化。适合三类人一是做计算机毕业设计、课程设计的学生需要一个有真实数据、有算法、有可视化、能讲清楚技术选型的题目二是中医信息化方向的从业者想验证知识图谱在专科领域的落地边界三是想学 Neo4j 加 Python 全链路、但苦于没有合适数据集的开发者。下面按「数据怎么来 → 图谱怎么建 → 诊断怎么做 → 坑在哪」的顺序拆开讲。2. 数据层真菌性皮肤病的中医语料从哪来、怎么清洗2.1 三类数据源与各自的抽取难度做中医知识图谱数据源决定了后面 70% 的工作量。我一般把数据分成三类难度递增。第一类是结构化程度较高的教材和药典比如《中医外科学》里的病名、证型、治法、方剂条目这类内容本身就有层级用正则加规则就能抽出「病名—证型—方剂」三元组。第二类是临床医案通常是「患者男35 岁主诉……舌红苔黄腻脉滑数辨证为湿热下注方用龙胆泻肝汤加减」这种半自由文本需要分词加实体识别。第三类是方剂组成一味药对应多个方剂一个方剂对应多个证型是典型的多对多关系适合用图数据库存。真菌性皮肤病在这个体系里的特殊之处在于它的西医病名体癣、股癣、手足癣、花斑癣和中医病名圆癣、紫白癜风、鹅掌风需要做映射否则图谱查出来的证型和医生说的对不上。这一步映射表必须手工校对没有捷径。2.2 用 Python 做实体抽取的最小可跑脚本下面这段代码演示从医案文本里抽「症状—证型—方剂」三元组的最小逻辑。实际项目里我会把规则拆成配置文件这里为了可读性写在一起。import re import jieba import jieba.posseg as pseg # 症状词典和证型词典实际项目从 CSV 加载 SYMPTOM_DICT [瘙痒, 红斑, 丘疹, 水疱, 脱屑, 苔黄腻, 脉滑数, 舌红] SYNDROME_DICT [湿热下注, 血虚风燥, 风湿热蕴, 脾虚湿盛] FORMULA_DICT [龙胆泻肝汤, 除湿胃苓汤, 当归饮子, 消风散] def extract_triples(text): triples [] # 1. 先定位证型证型是三元组的锚点 syndrome None for s in SYNDROME_DICT: if s in text: syndrome s break if not syndrome: return triples # 2. 抽症状词典匹配 词性过滤避免把方剂名误判成症状 symptoms [w for w in SYMPTOM_DICT if w in text] # 3. 抽方剂 formulas [f for f in FORMULA_DICT if f in text] for sym in symptoms: triples.append((sym, 表现为, syndrome)) for f in formulas: triples.append((syndrome, 治法用方, f)) return triples if __name__ __main__: sample 患者皮肤瘙痒伴红斑舌红苔黄腻脉滑数辨证为湿热下注方用龙胆泻肝汤加减。 for t in extract_triples(sample): print(t)逻辑说明先找证型作为锚点是因为医案里症状描述可能很长但证型通常只有一个以它为圆心向外连边能大幅降低误抽。参数上SYMPTOM_DICT和SYNDROME_DICT必须分开维护不要混在一个列表里否则「湿热下注」会被当成症状。实际跑的时候jieba主要用于处理词典没覆盖到的自由文本如果数据源是教材这种规范文本纯正则反而更稳。提示症状词典里一定要收录舌象和脉象词中医辨证里舌脉的权重往往高于皮损描述漏掉它们图谱的推理能力会明显下降。2.3 数据清洗里最容易被忽略的两件事一是同义词归一。「瘙痒」和「痒」「瘙痒明显」要归到同一个实体「苔黄腻」和「舌苔黄腻」也要合并否则图谱里会出现大量一度节点查询时匹配不上。我一般建一张同义词表在入库前统一替换。二是证型的粒度控制。教材里「湿热下注」和「湿热蕴结」有时混用如果全按字面建节点图谱会碎掉。常见做法是建一层「证型大类」把细粒度证型挂到大类下诊断时先匹配大类再细化。这一步没有标准答案取决于你的数据量和诊断精度要求。3. 图谱构建用 Neo4j 把三元组变成可查询的网络3.1 节点和关系的设计别把图谱建成关系型数据库很多人第一次做知识图谱会把所有字段都塞进节点属性结果查询时还是靠WHERE过滤图数据库的优势完全没发挥出来。正确的做法是能作为查询入口的实体一律建节点描述性文字才放属性。这个项目里我设计的节点类型有六类疾病含中西医病名映射、证型、症状含舌脉、方剂、中药、医案。关系类型主要有疾病-辨证分型-证型、证型-临床表现-症状、证型-治法用方-方剂、方剂-组成-中药、医案-辨证-证型、医案-使用方剂-方剂。// 建约束避免重复节点这一步在导入前必须做 CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (d:Disease) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT syndrome_name IF NOT EXISTS FOR (s:Syndrome) REQUIRE s.name IS UNIQUE; CREATE CONSTRAINT symptom_name IF NOT EXISTS FOR (s:Symptom) REQUIRE s.name IS UNIQUE; CREATE CONSTRAINT formula_name IF NOT EXISTS FOR (f:Formula) REQUIRE f.name IS UNIQUE; CREATE CONSTRAINT herb_name IF NOT EXISTS FOR (h:Herb) REQUIRE h.name IS UNIQUE; // 导入一条完整链路疾病-证型-症状-方剂-中药 MERGE (d:Disease {name: 足癣}) MERGE (s:Syndrome {name: 湿热下注}) MERGE (sym:Symptom {name: 苔黄腻}) MERGE (f:Formula {name: 龙胆泻肝汤}) MERGE (h:Herb {name: 龙胆草}) MERGE (d)-[:辨证分型]-(s) MERGE (s)-[:临床表现]-(sym) MERGE (s)-[:治法用方]-(f) MERGE (f)-[:组成]-(h);逻辑说明MERGE而不是CREATE是因为导入脚本会反复跑MERGE保证同名节点只建一次。约束必须在导入前建好否则几万条数据进去后再去重代价极大。参数上节点名统一用中文全称不要用缩写方便后续和前端展示对齐。3.2 用 Python 批量导入py2neo 的批量写法与事务控制单条MERGE在数据量上千以后会明显变慢正确做法是用UNWIND批量提交。下面是从 CSV 读三元组并批量入库的脚本。from py2neo import Graph import csv graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) def batch_import(csv_path, batch_size500): with open(csv_path, encodingutf-8) as f: reader csv.DictReader(f) batch [] for row in reader: batch.append(row) if len(batch) batch_size: _flush(batch) batch [] if batch: _flush(batch) def _flush(batch): # 用 UNWIND 把一批三元组一次性提交减少网络往返 query UNWIND $rows AS row MERGE (a:Entity {name: row.head}) MERGE (b:Entity {name: row.tail}) MERGE (a)-[r:REL {type: row.relation}]-(b) graph.run(query, rowsbatch) if __name__ __main__: batch_import(triples.csv)逻辑说明UNWIND把列表展开成多行Neo4j 在一次事务里处理比循环单条快一个数量级。batch_size设 500 到 1000 比较稳太大容易触发内存告警。这里为了通用把节点都建成Entity实际项目里应该按类型分开建查询时才能用标签索引加速。注意导入前先在小批量数据上验证关系方向(a)-[:REL]-(b)的方向反了后面所有路径查询都会查不到结果这是血泪经验。3.3 验证图谱连通性三个必查的 Cypher图谱建完不能只看节点数要验证它是不是「连通的」。我一般跑三个查询一是孤立节点检查二是核心证型的路径深度三是任意两个证型之间是否存在共享症状。// 1. 找没有任何关系的孤立节点 MATCH (n) WHERE NOT (n)--() RETURN n.name LIMIT 20; // 2. 查湿热下注到中药的最长路径验证链路完整 MATCH p (s:Syndrome {name: 湿热下注})-[:临床表现|治法用方|组成*1..3]-(h:Herb) RETURN p LIMIT 10; // 3. 找两个证型共享的症状用于后续相似度计算 MATCH (s1:Syndrome {name: 湿热下注})-[:临床表现]-(sym)-[:临床表现]-(s2:Syndrome) WHERE s1 s2 RETURN s2.name, collect(sym.name) AS shared_symptoms;逻辑说明第一个查询能快速发现清洗不彻底留下的垃圾节点。第二个查询验证「证型→方剂→中药」这条链路是否通如果查不到结果说明关系类型名写错了或者方向反了。第三个查询是辅助诊断的基础共享症状越多两个证型越容易混淆这正是系统需要给出鉴别提示的地方。4. 辅助诊断从症状输入到证型推荐的匹配算法4.1 基于症状加权的相似度计算辅助诊断的本质是用户输入一组症状系统在证型节点里找匹配度最高的。最简单的做法是 Jaccard 相似度但它把所有症状权重当成一样而中医里舌脉的鉴别价值远高于一般皮损。所以我在项目里用的是加权相似度。def weighted_similarity(input_symptoms, syndrome_symptoms, weight_map): input_symptoms: 用户输入的症状集合 syndrome_symptoms: 证型关联的症状集合 weight_map: 症状权重字典舌脉权重高 input_set set(input_symptoms) syndrome_set set(syndrome_symptoms) intersection input_set syndrome_set if not intersection: return 0.0 # 加权交集除以加权并集 inter_weight sum(weight_map.get(s, 1.0) for s in intersection) union_weight sum(weight_map.get(s, 1.0) for s in (input_set | syndrome_set)) return inter_weight / union_weight if union_weight else 0.0 # 权重配置舌脉类给 2.0普通皮损给 1.0 WEIGHT {舌红: 2.0, 苔黄腻: 2.0, 脉滑数: 2.0, 瘙痒: 1.0, 红斑: 1.0}逻辑说明加权 Jaccard 的好处是当用户只输入了「舌红苔黄腻」而没有描述皮损时系统依然能给出合理推荐因为舌脉权重高。参数上权重值不需要很精确2:1 的比例就能明显改善排序关键是舌脉要单独归类。4.2 从 Neo4j 拉取候选证型并排序实际系统里候选证型不是全量比对而是先用图谱做一轮粗筛找出和输入症状有至少一个共同节点的证型再算相似度。def recommend_syndrome(graph, input_symptoms, top_k3): query MATCH (s:Syndrome)-[:临床表现]-(sym:Symptom) WHERE sym.name IN $symptoms WITH s, collect(sym.name) AS matched RETURN s.name AS syndrome, matched candidates graph.run(query, symptomsinput_symptoms).data() results [] for c in candidates: # 拉取该证型的完整症状集 full graph.run( MATCH (s:Syndrome {name:$name})-[:临床表现]-(sym) RETURN collect(sym.name) AS all_sym, namec[syndrome] ).data()[0][all_sym] score weighted_similarity(input_symptoms, full, WEIGHT) results.append({syndrome: c[syndrome], score: round(score, 3)}) results.sort(keylambda x: x[score], reverseTrue) return results[:top_k]逻辑说明先用WHERE sym.name IN $symptoms做粗筛把候选证型从几十个降到几个再算精确相似度这是性能关键。top_k一般取 3给医生留出鉴别空间只给一个结果反而不好用。返回结果里带上匹配到的症状前端可以高亮展示让医生知道系统为什么这么推荐。4.3 方剂推荐与医案回溯证型确定后方剂推荐就是图谱里的路径查询证型-治法用方-方剂。但更实用的是同时返回相似医案让医生看到真实病例是怎么处理的。// 根据证型推荐方剂并附带该方剂的历史医案数量作为可信度参考 MATCH (s:Syndrome {name: $syndrome})-[:治法用方]-(f:Formula) OPTIONAL MATCH (c:Case)-[:使用方剂]-(f) RETURN f.name AS formula, count(c) AS case_count ORDER BY case_count DESC;逻辑说明OPTIONAL MATCH保证即使某个方剂没有关联医案也能返回不会漏掉。case_count是一个朴素的可信度指标医案越多说明这个方剂在该证型下越常用。参数上如果医案数据量少这个排序参考价值有限可以改用教材里的方剂优先级字段。5. 避坑与排查做中医知识图谱最容易翻车的五个地方5.1 症状实体粒度太细导致匹配不上现象用户输入「痒」系统匹配不到任何证型因为图谱里存的是「瘙痒」。原因没有做同义词归一实体粒度不一致。解决建同义词表在查询入口做一次归一化把「痒」「瘙痒」「瘙痒明显」统一映射到「瘙痒」。这张表要持续维护每发现一个漏匹配就补一条。5.2 关系方向反了导致路径查询为空现象MATCH (s:Syndrome)-[:临床表现]-(sym)查不到结果但节点明明存在。原因导入时写成了(sym)-[:临床表现]-(s)方向反了。解决Cypher 查询里关系方向必须和导入时一致。排查方法是先跑MATCH (a)-[r]-(b) RETURN a.name, type(r), b.name LIMIT 10看实际方向。如果方向确实反了要么改查询要么重新导入别想着用无向查询糊弄性能会差很多。5.3 批量导入时事务过大导致内存溢出现象导入几万条数据时 Neo4j 报OutOfMemoryError或者导入速度突然变慢。原因batch_size设得太大一次事务里MERGE的节点太多。解决把batch_size降到 500 以下并且每批之间加短暂 sleep。另外导入前先建好唯一约束能让MERGE走索引而不是全表扫描速度差好几倍。5.4 证型相似度算出来全是高分现象不管输入什么症状排第一的证型分数都在 0.8 以上没有区分度。原因证型之间的症状集重叠度太高或者权重配置让常见症状主导了分数。解决检查症状集把「红斑」「丘疹」这类几乎所有皮肤病都有的症状权重调低把舌脉和特征性症状权重调高。另外可以引入逆文档频率的思路出现频率越高的症状权重越低。5.5 中西医病名没做映射导致查询断裂现象医生输入「体癣」系统查不到因为图谱里存的是「圆癣」。原因没有建中西医病名映射表。解决建一张映射表在查询入口把西医病名转成中医病名再查。这张表要覆盖常见真菌性皮肤病体癣-圆癣、股癣-阴癣、手足癣-鹅掌风、花斑癣-紫白癜风。映射关系可以一对多查询时取并集。6. 进阶技巧用图嵌入给证型做向量化提升推荐上限加权相似度能解决 80% 的场景但它的天花板很明显只能匹配到图谱里显式存在的症状用户描述一个没收录的症状就完全失效。进阶做法是用图嵌入把证型转成向量再用向量相似度做召回。我一般用 Node2Vec 或 GraphSAGE在 Neo4j 的图上跑。下面是用node2vec库的最小示例输入是边列表。import networkx as nx from node2vec import Node2Vec # 从 Neo4j 导出边列表这里用 networkx 模拟 G nx.Graph() edges [ (湿热下注, 苔黄腻), (湿热下注, 龙胆泻肝汤), (血虚风燥, 脱屑), (血虚风燥, 当归饮子), (湿热下注, 红斑), (血虚风燥, 瘙痒), ] G.add_edges_from(edges) # 训练嵌入dimensions 一般取 64 或 128 node2vec Node2Vec(G, dimensions64, walk_length10, num_walks100, workers2) model node2vec.fit(window5, min_count1) # 查和「湿热下注」最接近的节点 similar model.wv.most_similar(湿热下注, topn5) print(similar)逻辑说明dimensions取 64 就够中医图谱节点规模不大维度太高反而过拟合。walk_length和num_walks决定随机游走的充分程度数据量小的时候可以调低。训练完的向量存进向量库诊断时把用户输入的症状也映射成向量症状节点的向量平均再算余弦相似度就能召回图谱里没有直接连边的证型。验证这套方法有没有用我一般做留一法从医案里抽掉一个症状看系统还能不能推荐出正确证型。加权相似度在抽掉舌脉时准确率会掉得厉害图嵌入相对稳一些因为它利用了症状之间的共现结构。最后说个我自己的习惯每次改完权重或换嵌入维度我都会固定跑一遍那 20 条手工标注的测试医案把 top3 命中率记下来。不记数字光凭感觉调参最后一定是一笔糊涂账。这个项目不难难的是数据清洗和参数验证的耐心把这两块做扎实毕业设计也好实际落地也好都够用了。希望帮到你。本文还有配套的精品资源点击获取