ARTICLE DETAIL

资讯详情

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

医药知识图谱问答系统:BERT与领域词典混合架构实战解析

医药知识图谱问答系统:BERT与领域词典混合架构实战解析 简介面向自然语言处理与知识图谱方向学习者这套资料提供一套基于PythonBERT词典的医药知识图谱自动问答系统完整实现覆盖知识图谱构建、基于流水线的问答后端以及前端交互界面适合作为毕业设计、课程设计或项目初期演示。技术层面融合词典与BERT-CRF完成实体识别利用Sentence-BERT实现实体链接并设计了意图识别模块整体流程清晰且可运行。资源共364个文件包含117个Python源码、90个JavaScript、25个HTML以及CSS、Markdown说明、JSON配置、pkl模型和sqlite3数据库等压缩包约72MB还附带超详细安装教程与文档便于快速复现和二次开发。目前已有307人学习下载对希望从零搭建医药问答系统并掌握实体识别、实体链接、意图识别等关键环节的读者很有参考价值。 先说结论这套医药知识图谱自动问答系统最终做成了“Python BERT 领域词典”的混合架构配套源代码、文档说明、超详细安装教程和一批整理好的演示数据。无论是想入门知识图谱还是想在医药品类里做文本问答这套方案都值得拿过去改一版自己的场景。我在这篇文章里不写空话从“为什么这么选型”讲起到图谱怎么建、问答链路怎么实现、代码怎么跑通最后把我在实操中踩过的坑也一并交代清楚。医药问答和普通问答最大的不同是它既要理解“人话”又必须保证答案有据可依。纯关键词匹配答不对同义表达纯模型生成又容易编造内容所以我才把“词典”这个东西重新捡了回来。1. 项目定位与整体技术路线1.1 一个医药问答需求为什么会同时用到BERT和词典先还原一下实际场景。用户问“阿莫西林能治喉咙痛吗”数据库里可能存的是“阿莫西林 - 适应症 - 细菌性感染”如果只靠关键词匹配“喉咙痛”和“细菌性感染”根本对不上系统大概率答非所问。换成BERT之后它能通过语义向量把“喉咙痛”“喉咙发炎”“扁桃体炎”这类表述拉近召回能力立刻提升。但问题也来了。预训练BERT的中文词表里药品名、疾病名覆盖并不充分。“奥司他韦”“美托洛尔”这类词很容易被切碎成无意义的子词导致实体识别断在中间。这时候领域词典就派上用场了把医药专有名词、别名、复方剂型全部收进自定义词典分词的边界一锁定实体就能被完整捞出来。李沐老师解读BERT的视频里反复强调过预训练模型提供的是语言表达能力不是领域知识本身。这句话在医药领域体现得特别明显——最终方案必须让三者各司其职。1.2 方案对比从纯规则到混合架构在搭建前我快速对比了四种主流问答方案的工程表现这也是整个项目选型的关键依据方案语义理解能力答案可控性开发成本医药场景适用性纯规则模板差高低只能演示无法扩展关键词/BM25检索一般中低同义表达会漏召回端到端BERT生成式问答强差可能编造高不敢用于医药BERT 词典 知识图谱混合强高答案可溯源中最贴合落地需求端到端生成式模型效果看着很酷但医药场景有一个底线不能编造。模型可能把“阿莫西林不良反应”生成得头头是道里面混合了真实信息和幻觉内容这在工具类系统里是致命伤。而混合架构中BERT只负责“理解问句、召回候选”最后答案永远来自知识图谱中的真实三元组天然带证据链。1.3 完整数据流梳理整个系统的数据流分两条线。第一条是构建线原始医药数据进入清洗环节做别名归一、实体类型标注生成形如“阿莫西林 - 适应症 - 细菌性感染”的三元组再写入图存储。第二条是问答线用户问题进来后先经过BERT语义编码和词典实体识别两路信息合并后得到候选实体和候选关系再到图谱里做路径检索最后把检索结果映射成自然语言答案。两条线交汇的位置就是候选三元组集合。理解这一层后面看代码就轻松了。2. 医药知识图谱的建设过程2.1 实体、关系与数据表结构设计建图谱不需要一上来就套严格的本体论标准尤其做项目演示轻量的“实体 - 关系 - 实体”模型最务实。我的实体类型就五类药物、疾病、症状、成分、科室关系类型控制在八类以内保证问答时候选不会爆炸。实体类型和关系类型要先用表格定清楚头实体关系尾实体药物适应症疾病药物不良反应症状药物成分药物疾病常用药物药物疾病常见症状症状药物禁忌疾病/人群在这个阶段最容易犯的错误是关系类型越加越多后来每个关系下只有几条数据问答召回质量反而下降。我后来强制自己只围绕“用户可能会问什么问题”反推关系而不是把所有医学关系都塞进去。2.2 医药数据清洗与三元组生成数据来源以公开的药品说明书、疾病科普知识为主。药品说明书是信息密度最高的语料但也最脏需要做几件事把“阿莫西林胶囊”“阿莫西林片”统一成“阿莫西林”把“扑热息痛”“对乙酰氨基酚”这类别名关系维护进别名表把“禁用”“不适用”这类否定标签单独拆出来不能混入正向关系里。清洗后的结构化数据用CSV维护就行字段长这样head,head_type,relation,tail,tail_type 阿莫西林,药物,适应症,细菌性感染,疾病 布洛芬,药物,不良反应,胃肠道不适,症状 高血压,疾病,常见症状,头晕,症状 高血压,疾病,常用药物,厄贝沙坦,药物实际项目里我会把来源字段也加上比如来自哪份说明书、哪个知识条目这样后续排查数据错误时能回溯到原文。三元组生成只是把CSV读进来做过滤和去重真正花时间的是实体对齐。2.3 图存储实现与体积控制为了降低安装门槛演示版我选择用NetworkX做内存图存储零依赖、开箱即跑。生产环境再迁移到Neo4j不迟。构建图的代码很直接import networkx as nx G nx.DiGraph() for _, row in data.iterrows(): G.add_edge(row[head], row[tail], relationrow[relation]) nx.write_graphml(G, data/graph.gml)演示数据量控制在几百个节点、一两千条关系问答响应能做到毫秒级。医药图谱有一个特点节点越少答案越容易维护。前期别贪多先把“药物 - 症状 - 疾病”这个核心三角做扎实再扩充相互作用子图。3. BERT与词典协同的问答核心实现3.1 词典在医药实体识别中的兜底作用严格来说BERT本身也能通过微调做命名实体识别但项目演示环境里标注数据往往不够微调出来的NER模型边界不稳定。领域词典的优势是确定性强词典里有的词分词时一命即中词典里没有的词宁可识别不到也不要识别错。我在词典文件里存两类内容一类是标准实体名另一类是常用别名。jieba加载后跑一遍分词就能得到候选实体集合。遇到“高血压患者能不能吃厄贝沙坦”这类问句词典会把“高血压”“厄贝沙坦”都切出来实体识别这步基本不会翻车。3.2 问答主流程设计问答主流程分成四段意图识别、实体识别、图谱查询、答案生成。意图识别我用规则加语义相似度混合判断先按问题里的关键词归类再用BERT向量做二次确认。真实的源码里这块做成了三类意图意图类型典型问题表达图谱检索方向适应症查询xx主要用于什么病药物 - 适应症 - 疾病不良反应查询xx有什么副作用药物 - 不良反应 - 症状常用药物查询高血压吃什么药疾病 - 常用药物 - 药物实体识别这块词典给出高置信度候选BERT句向量负责把候选模板和用户问题做相关性排序。两者不是串行关系而是并行召回再融合这是这套系统最核心的设计。3.3 核心代码问答引擎的最小实现为了便于阅读我贴一个精简版问答引擎逻辑和完整源码一致但去掉了日志、配置和异常处理import numpy as np import torch import jieba import networkx as nx from transformers import BertTokenizer, BertModel class QAEngine: def __init__(self, model_path, graph_path, dict_path): jieba.load_userdict(dict_path) with open(dict_path, encodingutf-8) as fp: self.entity_set set(fp.read().split()) self.G nx.read_graphml(graph_path) self.tokenizer BertTokenizer.from_pretrained(model_path) self.model BertModel.from_pretrained(model_path) self.model.eval() def extract_entities(self, question): words jieba.lcut(question) return [word for word in words if word in self.entity_set] def search_paths(self, entity, max_depth2): results [] for _, target, rel in self.G.edges(entity, dataTrue): results.append((f{entity} - {rel[relation]} - {target}, 1)) if max_depth 1: for _, target2, rel2 in self.G.edges(target, dataTrue): results.append( (f{entity} - {rel[relation]} - {target} - {rel2[relation]} - {target2}, 2) ) return results def _vectorize(self, text_list): inputs self.tokenizer( text_list, paddingTrue, truncationTrue, max_length128, return_tensorspt ) with torch.no_grad(): hidden self.model(**inputs).last_hidden_state return hidden.mean(dim1) def ask(self, question): entities self.extract_entities(question) if not entities: return 未识别到图谱实体请换个问法或确认实体是否在词典中。 candidates [] for ent in set(entities): candidates.extend(self.search_paths(ent, max_depth2)) if not candidates: return 实体存在但图谱中没有找到关联关系。 q_vec self._vectorize([question])[0] scored [] for path, _depth in candidates: path_vec self._vectorize([path])[0] score np.dot(q_vec, path_vec) / (np.linalg.norm(q_vec) * np.linalg.norm(path_vec) 1e-9) scored.append((score, path)) scored.sort(reverseTrue) return \n.join(path for _, path in scored[:3])这个迷你版有两个关键点。第一候选路径本身就是“实体 - 关系 - 目标”的语义模板BERT是在为“检索路径”打分而不是为“生成句子”打分所以答案永远来自图谱。第二词典先筛掉无关实体只用少量候选路径做向量计算CPU机器也能流畅跑这是成本上的一个关键优化。4. 安装部署与运行实录4.1 环境要求与依赖安装环境要求不复杂Python 3.8及以上内存8G以上CPU就能跑有显卡更好。核心依赖文件直接放在requirements.txt里torch1.11.0 transformers4.20.0,4.40 pandas numpy jieba networkx安装命令就一条pip install -r requirements.txt强调一点transformers版本别追最新。4.20到4.39这个区间API最稳定太新的版本可能调整默认行为导致旧代码某个参数失效。遇到报错先看版本再看代码。4.2 模型与数据的准备方式模型使用中文BERT预训练权重推荐直接加载bert-base-chinese。由于Hugging Face平台的下载速度不稳定我的建议是把模型权重手动下载到本地然后加载本地目录engine QAEngine( model_path./models/bert-base-chinese, graph_path./data/graph.gml, dict_path./data/medicine_dict.txt, )数据文件准备就三步把医药数据清洗成三元组CSV把专有名词写入medicine_dict.txt运行图谱构建脚本生成graph.gml。源码包里提供了示例数据保证直接能跑通后续往里面加点自己的数据也很方便。4.3 启动系统并跑通问答启动代码极其简单三行就能跑起来engine QAEngine( model_path./models/bert-base-chinese, graph_path./data/graph.gml, dict_path./data/medicine_dict.txt, ) print(engine.ask(阿莫西林主要用于什么疾病)) print(engine.ask(布洛芬有哪些不良反应)) print(engine.ask(高血压有什么常见症状))第一次运行会加载BERT模型CPU机器大约需要十几秒之后每句问答都在毫秒级返回。输出结果会把路径直接展示出来例如“阿莫西林 - 适应症 - 细菌性感染”每一条答案都能追到图谱里的原始边。这比直接把答案甩给用户更能让人信服。5. 常见问题与排障技巧5.1 模型加载与依赖版本问题模型加载是最容易卡住的一步。常遇到的情况是transformers版本过高导致tokenizer参数失效或者本地目录没有对应的config.json文件。排查思路是先确认目录结构再确认加载的是目录而非单个文件。如果你下载的权重文件不完整模型会静默加载失败这种问题最难查建议直接对比文件大小和官方md5。依赖冲突方面一个比较实用的操作是建虚拟环境把torch、transformers和图形库隔离存放避免系统Python环境被弄乱。演示项目面向不同机器时版本越固定复现成功率越高。5.2 实体识别和答案准确率问题如果你发现答案看起来“沾边但不准确”问题大概率出在两个地方。第一个是词典里的词太泛比如“蛋白”这种词很容易把无关问句带偏需要从词典里剔除。第二个是关系类型和问句意图不匹配用户问不良反应系统却去查了适应症导致答案错位。解决方法是给答案加上置信度阈值相似度太低时直接返回“未检索到可靠答案请查阅医药说明书或咨询专业人士”。医药场景的底线是宁可答不出也不能答错。这个判断在工程上比多召回几个候选重要得多。5.3 性能优化与资源占用控制CPU模式下BERT编码是主要性能瓶颈。可以用的优化手段有三个把max_length从128降到64医药问句一般不长只让候选实体通过BERT打分不让全量图谱路径通过用向量余弦相似度做top-k粗筛再对top-20做精确排序。真实项目里我把这三招组合起来联想笔记本上单次问答耗时控制在1秒以内。如果资源特别紧张还可以换更小的中文预训练模型虽然语义效果略降但演示场景完全够用。记住一个原则先保证系统能在大众机器上跑起来再谈效果优化。6. 后续扩展与我的几点实践体会6.1 还能往哪些方向扩展这套架构的扩展空间比想象中大。最实用的方向是增加“药物相互作用”子图把同成分药物、重复用药风险做成提醒类答案这直接提升系统价值。想做前端展示的话用vue3配合关系图组件把NetworkX导出的节点JSON渲染成交互式知识图谱效果非常直观能让非技术人员一眼看懂系统在做什么。更接近生产的方向是把NetworkX换成Neo4j复杂关系查询用Cypher支持更大规模数据。意图分类也可以用标注数据微调一个轻量BERT分类器替代现在的规则加相似度混合方案问句边界判断会更细腻。6.2 我在这套系统里踩过的坑最后分享几个实打实的教训。第一数据清洗的工作量远超模型调优实体对齐和别名归一做不好后面所有环节都会跟着出错第二不要迷信端到端模型知识图谱最大的价值是答案可解释放弃这个优势等于把医药问答做回了幻觉生成器第三关系类型一定要克制少而精的关系设计让问答召回更稳定贪多只会让候选集爆炸第四演示项目必须附带一组固定的测试问题集每次改动后跑一遍回归避免改一处坏一片。这套系统做完之后我的体会是BERT负责把“人话”翻译成“机器能理解的语义”词典负责把“领域实体”锁死知识图谱负责“给答案一个可验证的出处”。三者缺一块医药问答都会变味。如果你正准备做类似场景我建议先把这个混合架构跑通再根据自己的数据去迭代关系设计和意图分类这条路绝对走得通。本文还有配套的精品资源点击获取
返回列表