简介基于Python、BERT与词典技术实现的医药知识图谱自动问答系统完整毕设项目包含源码、文档说明、超详细安装教程、数据与训练好的模型适合计算机相关专业学生用于毕业设计、课程设计或项目实战进阶。项目共363个文件以Python脚本、JavaScript/CSS/HTML前端组件、文本数据、Markdown文档及pkl模型文件为主压缩包72.36MB目录结构清晰便于按模块查看与启动。相比常见问答实现项目在症状到疾病推理上做了多处优化重爬症状文本并用AC算法提取症状词典生成99492个疾病-症状三元组口语化症状先用BERTCRF做命名实体识别再经SBERT计算相似度完成实体链接前端以ECharts力引导图展示知识图谱支持多症状交集推理。已有269人学习下载从Neo4j环境启动到运行主程序均有完整说明评审得分96.5分适合作为高分毕设参考并在此基础上扩展功能。1. 医药知识图谱问答系统Pyhon BERT 词典组合拳医药领域的自动问答想做到「能直接问、能答得准」难点从来不在算法选得多新而在数据怎么对齐、实体怎么消歧、多症状怎么交集。这套基于 Python BERT 词典的医药知识图谱自动问答系统是作者大四毕业设计、评审 96.5 分核心优化点在于把原项目「无法从症状回推疾病」的短板补上了——用爬虫重建症状描述、AC 算法对齐症状名得到 4377 个标准症状、重构出 99492 条「疾病-症状」三元组并引入 BERTCRF 做口语化症状的命名实体识别再用 SBERT 向量相似度配合重叠字长度完成实体链接。适合正在做毕设的计算机相关专业学生、想实操知识图谱问答落地的学习者也适合作为课设或期末大作业的项目原型。这套资源能解决的不只是「跑起来」而是「从数据构建到问答闭环」的完整链路。2. 知识图谱数据层从症状库到三元组的重构路线2.1 AC 自动机做症状对齐为什么不用人工匹配原项目最大的痛点是用户描述症状时系统经常答不上来「疾病」——本质上就是知识图谱里「疾病-症状」关系的质量太差。作者的处理思路很直接先用爬虫把每个疾病的症状描述原文抓下来再把原网站症状库里的所有症状名做成一个字典然后用 AC 自动机Aho-Corasick算法在描述文本里做多模式匹配一次遍历找出所有出现在文本中的症状名。这步的关键在于 AC 算法用「fail 指针」把模式串匹配的复杂度降到 O(n)症状字典再大也不会因为逐个比对而卡死。最终从描述文本中对齐出 4377 个标准症状名重建出 99492 条三元组。这个数字意味着每个疾病平均有二十多条症状关联数据密度比原项目高了一个量级。from ahocorasick import Automaton # 构建AC自动机symptom_dict是症状名字典 automaton Automaton() for idx, symptom in enumerate(symptom_dict): automaton.add_word(symptom, (idx, symptom)) automaton.make_automaton() # 对疾病描述文本做多模式匹配 def extract_symptoms(desc_text): matched [] for end_index, (idx, symptom) in automaton.iter(desc_text): matched.append(symptom) return matched逻辑说明Automaton.add_word 把所有症状名加入字典树make_automaton 构建 fail 指针。遍历描述文本时 iter 方法返回所有匹配到的症状名及其位置end_index 表示匹配终点可以用它计算症状在原文中的起始位置方便后续做实体标注。参数说明如果症状词典规模很大几万级构建自动机会有几十秒的初始化耗时建议把构建好的 AC 自动机序列化存档启动时直接加载。另外症状名里有「头痛」和「偏头痛」这种包含关系AC 匹配会同时命中此时需要按最长匹配后处理否则数据会有重叠噪音。2.2 三元组重构与 Neo4j 导入原始数据只是「疾病 - 症状描述文本」重构后的标准三元组是「疾病 - 症状名」并且支持多个症状推理疾病时的交集查询。把三元组导入 Neo4j 4.1.4 社区版存储结构用节点 关系两个标签LOAD CSV WITH HEADERS FROM file:///symptom_triples.csv AS row MERGE (d:Disease {name: row.disease}) MERGE (s:Symptom {name: row.symptom}) MERGE (d)-[:HAS_SYMPTOM]-(s)这段 Cypher 用 MERGE 做了节点幂等去重跑脚本前 CSV 里如果已经存在「感冒」节点MERGE 不会重复创建而是直接匹配。HAS_SYMPTOM 关系是「疾病 - 症状」的有向边在后端查询时顺着这个方向做多症状疾病交集。如果你的数据量比这套大比如超过百万级三元组建议用 neo4j-admin import 批量导入而不是 LOAD CSV后者在十万条级别就明显开始慢了。导入完成后可以快速验证数据质量MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) RETURN d.name, count(s) AS symptom_cnt ORDER BY symptom_cnt DESC LIMIT 5;查询结果应该能看到每个疾病关联的症状数量在几十条量级如果某些疾病只有一两条说明爬虫抓取的描述文本太短需要回查源数据。2.3 图谱属性缺失患病率这个 todo 要自己补项目原 README 里明确写了「重新构建疾病的患病率属性」目前没做这也是答辩时常见的追问点。作者标注了从高到低演示的 TODO。这个注意是善意的提醒如果照搬源码做毕设最好把患病率属性补上。做法是在疾病节点上加一个 prevalence 属性数据类型用 float 表示占比。MATCH (d:Disease {name: 感冒}) SET d.prevalence 0.65;有了患病率属性多症状命中多个候选疾病时就能按 prevalence 排序而不是按图谱里第一个出现的顺序展示。这一条建议放进论文里能明显提升「工作量和完整性」的评价。作者原文在 README 里注释了两个 todo患病率和深度学习意图识别建议复现时二选一给补掉否则答辩容易被问到。3. BERTCRF 口语化命名实体识别把大白话翻译成标准症状3.1 为什么必须上 NER 模型而不是继续用词典AC 自动机再全也只能匹配「库里有」的症状名现实里用户说的往往是「脑袋昏昏沉沉的」「嗓子跟冒烟似的」——这类口语化表达无法命中症状字典。作者的做法是训练一个 BERTCRF 序列标注模型把问句里的实体先抽出来再做实体链接回标准症状。标注方案用的是 BIOCC 表示实体类型B-Symptom、I-Symptom、B-Drug、I-Drug、B-Disease、I-Disease以及 O 表示非实体。训练数据不是人工标注的而是基于问题模板自动生成先定义「我最近有[症状]」这类模板再从领域词典里随机抽取词填充词槽位每个词在数据集里只出现一次避免模型记住特定词的偏置。这是低成本构建 NER 训练集的典型做法确实适用于毕设场景。# 自动构建训练数据示例 import random question_templates [ 我最近有{}, {}该怎么治, 医生说我得了{}, ] entity_types { 感冒: Disease, 头痛: Symptom, 布洛芬: Drug, } def build_ner_corpus(templates, entity_dict, n10000): samples [] for _ in range(n): template random.choice(templates) entity random.choice(list(entity_dict.keys())) entity_type entity_dict[entity] question template.format(entity) # BIOC标注 labels tag_question(question, entity, entity_type) samples.append((question, labels)) return samples逻辑说明tag_question 函数需要自己实现——找到实体在 question 中的起始和结束位置把实体首字标为 B-类型、其余字标为 I-类型其他字全部标 O。注意中文 NER 的标注是「字」级别而不是「词」级别比如「布洛芬」三个字依次是 B-Drug、I-Drug、I-Drug。参数说明模板数量越多生成的问句多样性越好。原项目描述里每个词只出现一次这个设计是为了防止模型走捷径——如果「头痛」反复出现在训练集里模型可能直接记住这个词而不是学上下文特征。建议模板覆盖到三十个以上包含提问句式、「我XX了」「XX怎么办」等口语化变体。3.2 模型训练与部署transformers CRF 层训练好的模型参数放在百度网盘链接: https://pan.baidu.com/s/1ZGCzYIs-Vo1bsL40XRPtfw 密码: 96ln解压后放到目录 med_kg/ner_model/outputs 下。如果你要自己重训基础做法是在 HuggingFace 的 BERT 输出上叠一个 CRF 层import torch from transformers import BertModel, BertTokenizer from torchcrf import CRF class BertCRFForNER(torch.nn.Module): def __init__(self, bert_model_name, num_labels): super().__init__() self.bert BertModel.from_pretrained(bert_model_name) self.dropout torch.nn.Dropout(0.1) self.fc torch.nn.Linear(self.bert.config.hidden_size, num_labels) self.crf CRF(num_labels, batch_firstTrue) def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_ids, attention_maskattention_mask) emissions self.fc(self.dropout(outputs.last_hidden_state)) if labels is not None: return -self.crf(emissions, labels, maskattention_mask.bool()) return self.crf.decode(emissions, maskattention_mask.bool())逻辑说明BERT 输出每个 token 的 768 维向量经过一层线性映射到标签维度比如 symptom/disease/drug 各有 B、I、O总共 7 类。CRF 层的作用是约束标签序列的合法性——比如 I-Symptom 前面必须是 B-Symptom 而不能是 B-Drug这类序列层面的约束是纯 BERT Softmax 做不到的。参数说明训练时 batch_size 建议取 16 或 32学习率 2e-5 起步BERT 部分可以冻结前几层加速微调。CRF 的 loss 返回的是负对数似然所以前向里取负号。decode 时用维特比算法输出全局最优路径比逐位置 argmax 稳定得多。3.3 模型启动失败的排查资源里带了训练好的模型但 BERT 类模型最容易出问题的是路径加载和版本兼容。若运行python med_kg/manage.py runserver后 NER 接口报错优先确认 outputs 目录下有 pytorch_model.bin 或者 tf_model.h5且 transformers 库版本与训练时一致。常见报错是「Some weights of the model checkpoint were not used」——这是正常的预训练权重裁剪提示不是错误。真正的坑是中文 BERT 的 tokenizer 用的是 bert-base-chinese 的 vocab.txt如果本地没网就会卡在加载阶段建议先把模型和词典都离线放置好。4. 问答后端与实体链接从提到词到标准实体的等价转换4.1 SBERT 向量相似度加重叠字重排NER 抽出了「脑袋昏沉」这类短语但它不是标准症状名。实体链接要把它映射到知识图谱里的标准症状节点。作者的做法是先生成领域词典里所有标准词的 SBERT embedding 并缓存线上推理时计算提及词 embedding 与所有候选的余弦相似度取 top20再用「重叠字长度」做重排。这个组合很实用纯向量相似度对同义表达友好但对「头痛」和「偏头痛」这类字面高度重叠的候选区分度不够纯字面匹配又处理不了「脑袋疼」这类同义替换。两者结合的逻辑是——先靠向量召回候选再用字面重叠做精排正好互补。from sentence_transformers import SentenceTransformer import numpy as np class EntityLinker: def __init__(self, dict_path, model_nameparaphrase-multilingual-MiniLM-L12-v2): self.model SentenceTransformer(model_name) self.symptom_dict load_dict(dict_path) self.embeddings {s: self.model.encode(s) for s in self.symptom_dict} def link(self, mention): mention_emb self.model.encode(mention) scores [(s, cosine(mention_emb, emb)) for s, emb in self.embeddings.items()] top20 sorted(scores, keylambda x: -x[1])[:20] # 重叠字长度重排 top20.sort(keylambda x: overlap_len(mention, x[0]), reverseTrue) return top20[0][0] def overlap_len(a, b): return sum(1 for ch in a if ch in b)逻辑说明embedding 缓存是关键工程决策——如果每次推理都对几千个症状实时编码延迟会到秒级。启动时构建一次内存字典推理阶段只是查表加余弦计算。参数说明paraphrase-multilingual-MiniLM-L12-v2 是个轻量多语言 SBERT 模型中文效果够用且速度快。如果追求更高精度可以换text2vec-base-chinese但内存占用会涨上去。重叠字长度这里用的是「字符级交集计数」没考虑顺序和双字词边界实现简单但你可以在自己的版本里换成最长公共子串效果会更好一点。4.2 意图识别规则模板怎么撑起问答原项目的意图识别没有上深度学习而是基于提问词 提及词类型做模板匹配。核心意图有三类症状查疾病「我头痛发烧」、疾病查症状「感冒有什么症状」、药物查疾病「布洛芬治什么病」。实现上就是维护一个提问词字典比如「有什么症状」「怎么治」「是什么病」各自映射到意图类型再结合 NER 抽取出的实体类型做交叉判断。INTENT_RULES { symptom_to_disease: [我.*(有|出现).*, .*(头晕|头痛|发烧).*怎么办], disease_to_symptom: [(感冒|肺炎).*什么症状, .*的症状有哪些], } def detect_intent(question): intent None for name, patterns in INTENT_RULES.items(): if any(re.match(p, question) for p in patterns): intent name break if intent symptom_to_disease and has_multiple_symptoms(question): return multi_symptom_to_disease return intent逻辑说明先匹配意图模板再判断问句中是否包含多个症状实体如果多个就走特殊的「多症状疾病交集」逻辑。参数说明规则模板的覆盖度直接决定问答系统的鲁棒性。作者也把「深度学习意图识别」写进了 todo但说实话在医药垂直域、问题模式固定时模板匹配的准确率反而更高——至少不会瞎猜意图。建议你在论文里把「基于规则的意图识别与基于模板的 NER 数据生成」打包描述为一个完整的「词典增强」技术路线这样解释得通。4.3 多症状疾病交集一个关键的工程返回值原项目是对每个症状单独找疾病然后 union 合并——导致用户说「发烧、咳嗽」时会返回一大堆只匹配其中一个症状的疾病。作者的优化是把 union 改成 intersection按「发烧」候选疾病集合和「咳嗽」候选疾病集合求交集只有同时匹配所有症状的疾病才会返回。这个改动对问答体验的提升是决定性的也是答辩时可以重点讲的一个细节。MATCH (s1:Symptom {name: 发烧})-[:HAS_SYMPTOM]-(d:Disease) WITH collect(d) AS fever_diseases MATCH (s2:Symptom {name: 咳嗽})-[:HAS_SYMPTOM]-(d:Disease) WHERE d IN fever_diseases RETURN d.name;逻辑说明第一步先收集发烧相关的疾病集合第二步查咳嗽相关疾病并用 WHERE d IN fever_diseases 做集合过滤。这两个症状并列而不是嵌套子查询。注意这个 WHERE 是在内存里做集合判断不是图数据库的索引查询。参数说明如果症状数量超过三个建议在 Python 端先分别查出每个症状的疾病 id 集合再用 set 的交集操作比 Cypher 里多条件要直观得多尤其当候选疾病集合是几千条量级时。5. 避坑与常见问题排查从下载到跑通的血泪记录5.1 症状字典与爬虫数据的编码坑现象AC 自动机匹配结果全是乱码或者有些疾病描述根本匹配不到任何症状。原因原网站的编码不是 UTF-8爬虫存下来的文本被错误解码成了 Latin-1 或 GBK导致中文症状名和字典里的标准名对不上。解决爬虫保存时统一用resp.encoding utf-8强制指定或者先resp.apparent_encoding探测再转码。匹配前对文本做一遍清理——去掉换行、制表符、多余的 HTML 标签。import re def clean_text(raw): text raw.replace( , ).replace(\u3000, ) text re.sub(r[^], , text) return text.strip()5.2 Neo4j 启动不了版本和 JDK 对齐现象bin/neo4j start报错提示 Java 版本不兼容或者提示 JAVA_HOME 找不到。原因Neo4j 4.1.4 要求 Java 11但你本机装的是 Java 8 或者 Java 17。版本不匹配是最常见的其次是 JDK 路径没写进系统环境变量。解决项目自带与 Neo4j 4.x 匹配的 Java 版本启动前检查java -version确认 11.0.x 或 1.8 版本4.x 社区版对 8 也兼容但 4.1 官方建议 11。把 JAVA_HOME 设为 JDK 安装目录并在 PATH 里加上%JAVA_HOME%\bin重新打开终端再执行 bin/neo4j start。5.3 BERT 模型加载失败路径和版本一致性现象Django 启动后首次调用 NER 接口报「No such file or directory: med_kg/ner_model/outputs」。原因训练好的模型参数没有放到正确目录或者路径大小写、斜杠方向在 Windows 下出错。另一个常见情况是 transformers 版本升级后旧模型权重文件格式pytorch_model.bin与新版本代码不完全兼容。解决下载网盘里的模型文件确认目录结构是med_kg/ner_model/outputs/pytorch_model.bin、config.json、vocab.txt。如果 transformers 版本不一致尝试降级到与你训练时相同版本例如 4.18.0。Windows 下建议用绝对路径写死不要依赖相对路径。5.4 Windows 下 run.bat 跑完页面能开但问答无响应现象前端页面加载出来了力引导图也渲染了但输入问题后一直转圈。原因前端请求的端口和后端 Django 启动端口不一致。项目里 run.bat 可能写死了 8000 端口但 Neo4j 默认 Bolt 端口是 7687如果后端连接 Neo4j 的配置写错端口后端接口会一直等数据库响应前端当然拿不到结果。解决先看浏览器 Network 面板里 POST 请求的 URL确认请求打到了 Django 的http://127.0.0.1:8000/qa之类的地址。再看 med_kg/settings.py 里 NEO4J_BOLT_URL 是否写的bolt://localhost:7687账号密码是否为 neo4j/neo4j首次启动后默认密码要在 Neo4j 里改掉。把这两处对齐后重启 Django问题基本能解决。5.5 多症状交集返回空结果标准症状名的脱节现象用户说「头痛、发热、咳嗽」系统返回「未找到相关疾病」。原因NER 抽出的「发热」不是症状库里的标准名标准库里是「发烧」或者三个症状里有一个链接失败导致交集为空——后端如果直接用空集合去交集结果必然为空。解决在实体链接层做「兜底映射」维护一个口语-标准名的别名表比如「发热-发烧」「脑袋疼-头痛」。如果 top20 候选里最高相似度低于阈值比如 0.3就把该症状标记为「未链接」在多症状交集时跳过它而不是直接返回空。这样即使有一个症状链接失败其他症状还能正确推理出候选疾病。6. 把问答系统做深验证方法、参数调优路径与扩展方向整套系统跑通后验证「优化是否真的有效」需要一个可量化的评测方法。作者原项目在答辩时拿到了 96.5 分但如果你要在自己论文里写实验章节建议这样设计评测集从症状字典里随机抽取 200 个症状用模板自动生成「我[症状]怎么办」这类问题再构造 50 条纯口语化表达比如「头快要炸了」「浑身没劲」分别走旧版纯 AC 匹配和新版BERTCRF SBERT 链接跑一遍答案人工标注是否正确命中疾病集合。通常新版在口语化样本上的准确率会有大幅提升——旧版这种样本大概率直接 miss新版可以靠 NER 抽到「头」「炸」这类实体并链接到「头痛」这是最有说服力的对比数据。参数调优方面有四个点第一NER 模型的阈值——BIOC 预测时如果某个实体 token 的置信度低于 0.8宁可让它当 O也不要把错误词槽喂给实体链接否则会出现「把时间状语当症状」的噪声第二SBERT 相似度的 top-K 取值——K 太小比如 5容易漏匹配K 太大比如 50会让重排阶段的计算量增加但准确率提升有限20 是个合理折中第三重叠字长度的权重——纯字符交集对单字症状「吐」会出现大量误判建议把 min_overlap 限制在 2 以上即至少重叠两个字才算有效候选第四Neo4j 查询超时——单个症状的候选疾病查询可以控制在 200ms 以内三个症状的交集如果超过 1s就要考虑在 Python 端预聚合疾病-症状的倒排索引用内存 set 运算替代图查询。扩展方向最值得做的是「患病率属性」。项目 README 明确留了这个 todo。你可以爬取疾病词条里「患病率」或「发病率」字段解析成 float 存入 Disease 节点。然后在后端返回多个候选疾病时按 prevalence 降序排列。这个扩展不仅能解决「多个候选疾病按什么顺序展示」的问题还能让系统回答「我更可能是哪种病」这种真实需求。做法在 2.3 节提过但真正要落地建议你给每个疾病加一个置信度的解释字段:「根据您的症状最可能患有感冒匹配 5 个症状人群患病率约 65%」这样答辩时能展示的不是一个 toy demo而是有临床可解释性的系统。最后一个建议来自我拆过的十几个知识图谱项目的共同教训如果你要在自己的机器上复现一定先用python med_kg/manage.py runserver跑通最小链路再碰前端。检查顺序是——Neo4j 能查浏览器打开 http://localhost:7474 验证、NER 接口能返回实体标签、实体链接能映射到标准症状、最后才看 Echarts 图。每次改完模型参数我会强制走一遍「输入口语化症状 - 查看 NER 输出 - 查看实体链接结果 - 查看 Cypher 查询返回」四个环节在日志里打点计时哪一环慢了就优化哪一环。把那 4377 个症状名和 99492 条三元组当作你的基线数据每改一个模块就对比一次基线的准确率和延迟这样你论文里的实验数据都是自己实测出来的而不是拍脑袋写的。这套资源对你来说应该是一份很好的起点希望帮到你。本文还有配套的精品资源点击获取