拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Bert-crf+知识图谱:医疗问答系统实体识别与图谱查询实战

简介一套完整且可直接运行的基于Python与Bert-CRF的医药知识图谱自动问答系统源码包主要面向自然语言处理、知识图谱及医疗信息化方向的开发者和研究者也适合作为相关课程设计与毕业设计的参考。项目覆盖从数据采集到系统上线的完整链路利用爬虫获取医疗数据清洗后构建Neo4j图数据库再通过Bert-CRF完成命名实体识别配合词嵌入做实体链接最终使用Django框架搭建交互界面实现疾病、药物、症状等知识问答功能。压缩包内共有364个文件其中包括117个Python脚本、90个JavaScript前端交互文件、27个CSS样式表与25个HTML页面同时还有模型文件与轻量级SQLite数据库整体压缩包约72.41MB目录按照爬虫、建模、Web等模块划分清晰便于检索学习。目前已有235人学习浏览此资源读者不仅可获得能够直接运行的完整源码还能参考训练语料、模型损失函数实现及前端页面模板从而快速复现整套系统或基于自身需求进行二次开发与功能扩展。1. 为什么医疗问答要同时用Bert-crf和知识图谱而不是只靠大模型以我过去接触过的医疗问答项目为例最常被卡住的不是建库而是让计算机分得清“阿莫西林胶囊”和“阿莫西林”“高血压”和“血压高”。这类问题靠规则不好穷举靠纯模型又容易编造答案。把 Bert-crf 序列标注和医药知识图谱组合起来是一个经过验证的经典方案前者负责从问句里切出实体后者负责把实体转成语义查询并返回可追溯的结果。这篇博文从源码包的目录结构出发依次拆解实体识别、图谱建模、问答查询三段链路给出可以直接跑起来的 python 代码和关键参数适合正在搭医药问答 demo 或学习知识图谱落地的工程师。2. 拆解源码包从Bert-crf实体识别到医药知识图谱的实体对齐在解压源码包、打开使用说明之前先要看清整个系统的数据流。常见做法是原始问句 → bert-crf 模型做字级序列标注 → 得到实体 span如疾病、药物、症状→ 通过名称词典或相似度对齐到知识图谱节点 → 在图数据库中执行查询 → 返回答案。这条链路里识别是入口对齐是瓶颈。很多初学者只盯最后一层的准确率忽略了实体对齐阶段结果识别对了却查不到答案。2.1 源码目录里你至少要认识的文件使用说明会写运行入口但实际项目的文件命名并不统一。基于常见的 python 工程习惯源码包一般长这样medical_qa/ ├── main.py # 主入口启动问答服务 ├── config.ini # 模型路径、数据库密码、端口配置 ├── bert_crf/ │ ├── model.py # Bert-crf 模型定义 │ ├── predict.py # 预测封装输入文本输出实体 │ └── label.py # BIO 标签定义和转换 ├── kg/ │ ├── neo4j_client.py # 图数据库连接 │ └── entity_link.py # 实体链接到图谱节点 ├── data/ │ ├── train_data.txt # 实体识别训练语料 │ └── medical_triples.txt # 三元组导入文件 └── requirements.txt拿到源码后先别急着装依赖打开config.ini确认三样东西模型路径是相对路径还是绝对路径、Neo4j 密码是否被写死、端口是否冲突。这三项是大多数“源码跑不起来”的根源。requirements.txt里的版本号如果和本机 python 环境冲突优先看 transformers 和 torch 的版本因为 Bert-crf 对这组的兼容性最敏感。2.2 Bert-crf 模型定义中最容易改错的一行Bert 最后一层输出是[batch_size, seq_len, hidden_size]crf 层需要把它映射到标签数量。常见的标签集合是 BIO 标记加实体类型例如B-Disease、I-Disease、B-Drug、I-Drug、O。实体类别数量直接决定num_labels的取值这一处如果不一致模型加载时会报维度不匹配。以下是典型的 Bert-crf 结构在 python 里的写法import torch from transformers import BertModel from torchcrf import CRF class BertCRF(torch.nn.Module): def __init__(self, model_name, num_labels): super().__init__() self.bert BertModel.from_pretrained(model_name) self.dropout torch.nn.Dropout(0.1) self.classifier torch.nn.Linear(self.bert.config.hidden_size, num_labels) self.crf CRF(num_labels, batch_firstTrue) def forward(self, input_ids, attention_mask): outputs self.bert(input_ids, attention_maskattention_mask) logits self.classifier(self.dropout(outputs.last_hidden_state)) return logits def decode(self, input_ids, attention_mask): logits self.forward(input_ids, attention_mask) return self.crf.decode(logits, maskattention_mask.bool())这里num_labels的计算方法是2 * 实体类别数 1。假设定义了疾病、药物、症状三类那么num_labels 2*3 1 7。预测阶段调用decode会输出每个句子的标签序列而不是概率分布这是 crf 层和单独 softmax 分类器的本质差别。单独 softmax 可能输出相邻标签混乱而 crf 能强制要求标签转移符合 BIO 规则比如不能出现O后面直接跟I-Disease。2.3 从 BIO 标签到图谱实体名的对齐逻辑模型输出的是标签序列不代表图谱里的节点名称。医疗文本常有别名问题“感冒”对应图谱节点“普通感冒”“血压高”对应“高血压”。实体对齐两个层次是常见做法精确匹配优先未命中时用编辑距离或字符向量相似度兜底。def align_entity(entity_text, node_names, max_distance2): if entity_text in node_names: return entity_text best_name None best_score max_distance for name in node_names: d levenshtein(entity_text, name) if d best_score: best_score d best_name name return best_namemax_distance控制容错程度取 1 时只接受单字符差异取 2 时能容忍“阿莫西林”和“阿莫西林胶囊”这类差距。返回结果可能是None调用方需要处理查不到实体的情况而不是直接把None拼进查询语句里。这一层逻辑决定了问答系统的上限不做对齐图谱通道必然返回空结果。实体识别还牵扯一个方向问题训练数据里的实体边界和实际问句表达方式往往会偏离。比如训练语料里写“每天三次”模型学到的可能是“每天三次”整体作为服药频次实体但用户在真实问句里只说“一天几次”。针对这种情况我在做源码级别的改造时一般会在predict.py里加一组同义表达替换在喂给模型前先把口语表达映射到训练语料风格。3. 在python环境里构建医药知识图谱Neo4j建模和三元组导入这个系统里知识图谱不是模型而是数据服务。选择 Neo4j 的主要理由在于它的 Cypher 查询语言适合表达多跳关系比如“疾病A的常用药物是什么”本质上是节点到节点的路径查询。相比 MySQL 那样的关系型数据库图数据库不需要为每种关系单独建表对不确定深度的查询也友好得多。3.1 医药大类的节点类型和关系类型定义源码的使用说明里一般会给出节点类型说明。以药品为中心节点类型可以这样设计节点类型示例节点属性Drug阿莫西林胶囊规格、用法、不良反应Disease上呼吸道感染就医科室、疗程Symptom发热体温阈值、持续时间Food香蕉属性、禁忌Check血常规参考范围关系类型围绕临床决策来设计treats、cures、has_symptom、not_eat、belongs_to。命名时注意方向一致避免treats和treated_by混用。方向不一致的后果是问答阶段写 Cypher 时match (d:Disease)-[:treats]-(dr:Drug)和match (d:Disease)-[:treats]-(dr:Drug)得到完全不同的结果排错时很难一眼看出来。3.2 Cypher 批量导入命令和属性和标签的作用源码包中常见的语料格式是 tab 分隔的三元组一行是“药物、关系、目标”。用 py2neo 导入比手动敲命令更容易管理事务边界from py2neo import Graph, Node, Relationship graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) with open(medical_triples.txt, encodingutf-8) as f: for line in f: parts line.strip().split(\t) source_node Node(Drug, nameparts[0]) target_node Node(Disease, nameparts[2]) rel Relationship(source_node, parts[1], target_node) graph.create(rel)这段代码每一行创建两个节点和一个关系效率不算高但数据量在几十万条以内时完全可接受。Node的第一个参数是标签后续是属性键值对这里用name作为匹配键。生产环境导入更大数据量时更推荐用LOAD CSV配合neo4j-admin import能利用 Neo4j 的批量导入通道速度差一个数量级。不过源码 demo 的目的在于跑通链路py2neo 的逐条写入反而更容易定位哪一行数据格式出错。提示导入前先建立唯一性约束比如CREATE CONSTRAINT FOR (d:Drug) REQUIRE d.name IS UNIQUE否则重复导入会产生大量重复节点查询结果出现同一药品返回多条不同路径答案也就失去了可信度。3.3 为什么实体识别结果的类别要映射到图谱节点类型Bert-crf 里可能定义了疾病、药物、症状三类实体图谱里却有五类节点中间就存在映射问题。比如“阿莫西林”既可能是 Drug也可能在某种语境下是过敏原这时需要候选类型过滤而不是简单的一对一。candidate_types { Drug: [Drug, Food], Disease: [Disease], Symptom: [Symptom, Check] }映射的目的有两个一是减少实体对齐时的搜索范围二是让 Cypher 生成阶段知道该往哪个方向写match。如果某个实体类别对应多个候选节点类型查询时应该按优先级逐个尝试找到第一个能返回结果的路径就停止而不是一次把所有可能全部拼接进查询。另一个值得注意的点是图谱查询结果和模型识别结果之间的交互。假设识别出“发热”这个症状实体图谱里存在“发热→has_symptom→感冒”这条边答案生成阶段要反过来想用户问的是“发热吃什么药”真正要找的是 Drug 接在 Disease 后面的节点而不是直接返回“发热”本身。这样设计模板时才能写出正确的查询链路。4. 自动问答系统的查询链路和关键参数从问句到Cypher生成问答系统分几段处理问句分类、槽位提取、查询生成、答案后处理。源码包里绝大多数实现是用模板匹配而不是让模型直接生成 Cypher因为生成的 Cypher 不可控容易产生语法错误或查出无关数据。模板方式的缺点是要维护规则优点是排错直观——问句没答对时能明确知道是意图认错了还是参数提取错了。4.1 问句分类模板和少量正则的具体写法先将问句映射到意图类型这决定后续查询的骨架。意图类型和常见问法之间的对应关系在源码里一般以规则字典形式存在意图问法特征示例drug_for_disease治疗、吃什么药、用什么药“感冒吃什么药”disease_of_symptom症状、表现、怎么了“发热是什么病的症状”drug_dosage剂量、吃多少、怎么吃“阿莫西林一天吃几次”food_not_eat不能吃、禁忌、忌口“高血压不能吃什么”check_for_disease检查、挂什么科“肺炎需要做什么检查”实现意图分类最稳妥的路径是规则优先、模型兜底。先跑一组关键词和正则import re INTENT_PATTERNS { drug_for_disease: [r治疗, r吃什么药, r用什么药], disease_of_symptom: [r什么病, r怎么了], drug_dosage: [r几次, r多少, r剂量], food_not_eat: [r不吃, r忌口, r不能吃], } def classify_intent(question): for intent, patterns in INTENT_PATTERNS.items(): for pattern in patterns: if re.search(pattern, question): return intent return Nonereturn None的分支很重要说明用户问句落在了模板覆盖范围之外此时宁可返回“暂不支持该问题”也不要进图谱查一个无意义的结果。规则列表可以根据真实日志持续补充这和训练模型不同不需要重新训练就能上线新意图。4.2 把实体和意图拼接成Cypher查询意图确定后下一步是把实体对齐结果填进 Cypher 模板。这一步需要严格区分参数和模板不能直接用字符串拼接用户输入否则空格和引号会让查询直接报错。def build_cypher(intent, entity_name, entity_type): if intent drug_for_disease: return ( fmatch (d:Disease {{name: {entity_name}}}) fmatch (d)-[:treats]-(dr:Drug) return dr.name limit 5 ) elif intent food_not_eat: return ( fmatch (d:Disease {{name: {entity_name}}}) fmatch (d)-[:not_eat]-(f:Food) return f.name limit 10 )强烈建议使用参数化查询替代 f-stringpy2neo 支持graph.run(cypher, nameentity_name)这种形式既避免注入风险也免去引号转义问题。上面的写法在源码 demo 里很常见因为不放入参的情况下自己写脚本调试最方便但放到对外服务时要换成参数化版本。提示查询结果为空时不要直接告诉用户“没找到”。先检查实体对齐是不是返回了None再检查图数据库里是否存在对应节点。一个快速验证方法是在 Neo4j 浏览器里手动执行同样的 Cypher 语句这样可以区分是查询写错还是数据缺失。4.3 答案格式化、相似度合并和最大置信度图查询返回的不一定是最终答案。比如“吃阿莫西林需要注意什么”可能同时命中不良反应、禁忌、食物关系三类结果需要合并去重。常见算法是收集所有候选答案节点按节点类型分组同一组内做文本相似度合并最后按置信度排序。def format_answer(result_rows): answers [] for row in result_rows: answer_text row[name] if not any(similar(answer_text, old) for old in answers): answers.append(answer_text) return answerssimilar可以用简单的公共前缀长度判断也可以用 jieba 分词后的重合率。这里要注意的是不要过度合并比如“阿莫西林胶囊”和“阿莫西林片”不是同一个答案具体阈值要看实际数据分布我一般从 0.6 起步调。问答系统的另外两个关键参数是最大返回数量和置信度阈值。返回数量限制在 5 到 10 之间比较合适太少用户可能看不到可用信息太多则像查出来的原始数据堆砌。置信度阈值和实体对齐的max_distance联动距离越大说明识别结果越不可靠对应答案应该排在更靠后的位置。5. 按使用说明把项目跑起来以及 3 个最容易踩的坑把源码包解压后我的建议是不直接执行python main.py而是先按使用说明里的顺序验证三个独立模块这样出了问题能快速定位范围。先建虚拟环境再装依赖避免污染全局 python 环境这一步对同时做多个项目的工程师尤其重要python -m venv venv source venv/bin/activate pip install -r requirements.txt装完依赖后先跑一次实体识别预测不用启动完整服务python -m bert_crf.predict --text 感冒吃什么药这句如果能输出包含“感冒”和实体类别Disease的结果说明模型加载正常。接着单独验证图谱连接python -m kg.neo4j_client --check最后再启动主服务用同样的问句做端到端测试。通过这种分段验证方式几乎所有源码运行问题都能被限定在模型、数据、服务三层中的某一层里。以下 3 个坑是最常见的代码层面几乎无法避免只能在部署时留意。第一个是 transformers 版本和 torch 版本不匹配。Bert-crf 源码包里 requirements 通常写的是几年的旧版本比如transformers4.x但本机已经装了 torch 2.x 的高版本模型加载时可能报weight_map相关的异常。解决办法不是全部重装而是单独装一个兼容组合通常pip install transformers4.30.0配合 torch 1.13 以上都能稳定运行。第二个是中文路径和编码问题。源码包如果是在 Windows 上打包的配置文件里的模型路径可能带有中文目录名在 Linux 上跑的时候路径直接失效。解决方式是把模型放到纯英文路径下同时确认config.ini保存为 UTF-8 编码。三元组导入文件如果不小心被存成 GBKopen()不加encodingutf-8就会遇到乱码问题。第三个是 Neo4j 版本认证变化。旧版源码用的认证接口是neo4j://新版本统一走bolt://同时密码策略变强初始密码太短会直接拒绝连接。看到Authentication rate limit一类报错时先去 Neo4j 浏览器里确认能不能登录再把neo4j_client.py里的连接串改成bolt://localhost:7687并更新密码。最后说一个验证技巧准备一组覆盖不同意图的测试问句比如“感冒吃什么药”“高血压不能吃什么”“阿莫西林一天几次”每轮改动后都跑一遍。把实体识别结果、对齐结果和最终答案分别打印到日志里这样能清楚看到是模型没识别对还是图谱里根本没有这条数据。整套链路里最值得花时间优化的就是实体对齐那一步识别错可以重训模型对齐错则往往只需要补几行词典规则。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门