简介:这是一套面向医疗AI开发者与知识图谱初学者的垂直领域问答系统实战资源,聚焦医疗场景下的自然语言理解与结构化知识检索问题,适用于高校科研、竞赛项目及工程原型开发。资源包含57个文件,涵盖14个核心Python模块(如BiLSTM_CRF、text_cnn、answer_search等)、16个医疗领域文本数据(disease、symptom、drug等),以及7张流程图与交互示意图(PNG/SVG/GIF),完整呈现从爬虫采集、Neo4j知识图谱构建到深度学习问句解析与答案生成的全链路实现,压缩包大小为120.5MB。已有4392人学习下载,资源结构清晰,模块解耦明确——data目录组织原始语料,model目录封装分类与NER模型,app目录提供可直接调用的问答接口,配套README与LICENSE确保开箱即用。
1. 项目缘起:为什么我们需要一个医疗知识图谱问答系统?
作为一名在医疗信息化和数据科学领域摸爬滚打了多年的从业者,我见过太多因为信息孤岛和知识碎片化而导致的效率瓶颈。医生想快速查询某种药物的相互作用,需要翻阅多本厚重的药典;患者想了解自己的症状可能对应哪些疾病,往往在搜索引擎里被各种广告和真假难辨的信息淹没。传统的数据库查询僵硬,而通用的大语言模型(LLM)在专业领域又容易“一本正经地胡说八道”,缺乏对结构化事实的精准把握。
这正是知识图谱(Knowledge Graph)结合问答系统(QA System)大显身手的地方。它能把散落在病历、文献、药品说明书里的实体(如疾病、症状、药品、检查)和关系(如“导致”、“治疗”、“禁忌”)编织成一张巨大的、可推理的网络。当你问“高血压患者可以服用布洛芬吗?”,系统不是去匹配关键词,而是沿着图谱中的路径推理:高血压 ->(常用药物)-> 降压药 ->(可能与)-> 非甾体抗炎药(如布洛芬) ->(相互作用)-> 影响降压效果或增加肾脏风险,最终给你一个基于事实的、可解释的回答。
最近,随着RAG(检索增强生成)和本地化大模型(如Qwen2-7B)的火热,很多人开始探索将向量搜索与知识图谱结合。但在我看来,对于医疗这种强逻辑、重关系、追求精确的领域,基于规则和图谱遍历的“老派”方法,在核心事实检索上依然有不可替代的优势——它结果确定,没有幻觉。本项目要实现的,正是这样一个基于Python、从零构建、数据代码完整、可直接运行的医疗领域知识图谱问答系统原型。它不依赖昂贵的商用API,也不强求顶配GPU,旨在为你揭示从数据处理、图谱构建到智能问答的全链路核心技术与实战细节。
2. 技术栈选型与核心组件拆解
一个完整的知识图谱问答系统,可以清晰地分为“建图”、“存图”、“问图”三个核心阶段。我们的技术栈也围绕此展开。
2.1 图谱构建:从非结构化文本到结构化三元组
医疗数据多为非结构化或半结构化文本(如电子病历、医学文献)。第一步就是信息抽取(Information Extraction)。
数据处理与实体识别(NER):我们使用
spaCy库。虽然其开箱即用的医学模型精度有限,但对于原型系统而言,它是一个快速起步的优秀选择。我们主要依赖其实体识别功能,并可以结合基于词典的方法(比如从医学标准术语库如UMLS、ICD-10中构建疾病、药品词典)来提升召回率。更高级的方案可以训练一个BERT+CRF的定制化NER模型,但这需要大量标注数据。import spacy # 加载英文模型(中文医疗可考虑`spacy-zh`或`THULAC`等) nlp = spacy.load(“en_core_web_sm”) text = “Patient with hypertension and diabetes was prescribed Metformin.” doc = nlp(text) for ent in doc.ents: print(ent.text, ent.label_) # 输出可能识别出”hypertension”为疾病,”Metformin”为药品。注意:
spaCy的默认模型对医学专有名词识别不佳。实践中,我们通常会准备一个医疗实体词典,对文本进行扫描匹配,作为对模型预测结果的补充和修正,这是提升初期效果的关键技巧。关系抽取(RE):这是难点。我们采用一种基于规则和依存句法分析的轻量级方法。例如,通过分析句子的依存树,寻找连接两个实体的特定路径或关键词(如“was prescribed”、“due to”)。对于“Patient with hypertension and diabetes”,我们可以解析出“with”连接了“Patient”和“hypertension & diabetes”,从而抽取出(Patient, has_symptom, hypertension)和(Patient, has_symptom, diabetes)的假设关系。虽然规则难以覆盖所有情况,但对于一个限定领域(如仅关注“疾病-症状-药品”),精心设计几十条规则也能达到不错的效果。
2.2 图谱存储与查询:为什么选择Neo4j?
存储“图”数据,图数据库是不二之选。在Neo4j、JanusGraph、Nebula Graph中,我们选择Neo4j,原因如下:
- 原生图存储与计算:它的数据模型就是节点和关系,与知识图谱的概念完美契合,查询性能极高。
- Cypher查询语言直观:像说英语一样查询图数据,学习成本低,开发效率高。例如,查找治疗某种疾病的药物:
MATCH (d:Disease {name:‘Hypertension’})<-[:TREATS]-(m:Drug) RETURN m.name。 - 丰富的可视化工具:Neo4j Browser能直观展示图谱结构,对于调试和理解数据关系至关重要。
- 活跃的社区与完善的Python驱动:
neo4jPython驱动稳定易用,方便我们将抽取的三元组批量导入。
安装与连接非常简单:
# 通过Docker快速启动一个Neo4j实例 docker run \ --name medical-kg \ -p 7474:7474 -p 7687:7687 \ -d \ --env NEO4J_AUTH=neo4j/your_password \ neo4j:latestfrom neo4j import GraphDatabase class Neo4jHandler: def __init__(self, uri, user, password): self._driver = GraphDatabase.driver(uri, auth=(user, password)) def close(self): self._driver.close() def create_entity(self, entity_name, entity_type): with self._driver.session() as session: session.run(“MERGE (e:” + entity_type + ” {name: $name})”, name=entity_name) # 更多方法:创建关系、查询等...2.3 问答引擎设计:从自然语言问题到Cypher查询
这是系统的“大脑”。它的任务是将用户自然语言问题(如“什么药治高血压?”)转换为可执行的Cypher查询。
我们设计一个管道(Pipeline):
意图识别与实体链接:首先,判断问题类型(是问症状、问治疗、问病因还是问禁忌?)。同时,识别问题中的实体(如“高血压”),并链接到知识图谱中已有的、标准化的节点(解决同义词问题,如“高血压”链接到“Hypertension”节点)。
- 意图识别:可以视为一个文本分类问题。对于简单系统,我们可以用关键词规则(如“什么药治”对应“查询治疗方案”)。更鲁棒的做法是训练一个简单的分类器(如用
scikit-learn的SVM或使用轻量级BERT模型)。 - 实体链接:这需要一张“同义词表”。我们维护一个字典,将“高血压”、“血压高”、“Hypertension”都映射到标准节点ID或名称上。
- 意图识别:可以视为一个文本分类问题。对于简单系统,我们可以用关键词规则(如“什么药治”对应“查询治疗方案”)。更鲁棒的做法是训练一个简单的分类器(如用
查询模板匹配:针对不同的意图,我们预置一系列Cypher查询模板。
- 意图:
query_treatment - 模板:
MATCH (d:Disease)<-[:TREATS]-(m:Drug) WHERE d.name = ‘{disease}’ RETURN m.name - 系统将识别出的实体“高血压”填入模板的
{disease}槽位,形成最终查询:MATCH (d:Disease)<-[:TREATS]-(m:Drug) WHERE d.name = ‘Hypertension’ RETURN m.name
- 意图:
执行查询与结果组装:执行Cypher查询,从Neo4j获取结构化的结果(如药品列表)。然后将这些结果填充到自然语言回答模板中,生成最终回复。
- 回答模板:
用于治疗{disease}的药物包括:{drug_list}。 - 最终回答:
用于治疗高血压的药物包括:卡托普利、硝苯地平、氢氯噻嗪。
- 回答模板:
这种基于模板的方法,虽然灵活性不如端到端的深度学习模型,但优点极其突出:零幻觉、结果精确、可解释性强。你总能知道答案是从图谱中的哪条路径推导出来的。
3. 从零到一:系统完整实现步骤详解
让我们抛开理论,进入实战环节。假设我们已经收集了一些简单的医疗文本数据(存放在data/medical_texts.txt中)。
3.1 第一步:环境搭建与数据准备
创建一个干净的Python环境(推荐使用conda或venv),并安装核心依赖。
# 创建环境 conda create -n medical-kg-qa python=3.9 conda activate medical-kg-qa # 安装核心库 pip install spacy neo4j pandas jieba # 中文处理加jieba python -m spacy download en_core_web_sm # 下载spacy模型 # 准备一个简单的医疗同义词词典(synonyms.json) { “高血压”: [“血压高”, “Hypertension”], “糖尿病”: [“Diabetes Mellitus”, “DM”], “阿司匹林”: [“Aspirin”], “发烧”: [“发热”, “Fever”] }3.2 第二步:构建知识图谱——代码实操
我们编写一个build_kg.py脚本,完成从文本到Neo4j数据库的完整流程。
# build_kg.py import spacy import json from neo4j_handler import Neo4jHandler # 假设我们将Neo4j操作封装在此类中 class MedicalKGBuilder: def __init__(self, spacy_model=“en_core_web_sm”, synonym_path=“synonyms.json”): self.nlp = spacy.load(spacy_model) with open(synonym_path, ‘r’, encoding=‘utf-8’) as f: self.synonym_map = json.load(f) self.neo4j_driver = Neo4jHandler(“bolt://localhost:7687”, “neo4j”, “your_password”) # 定义我们关注的实体类型和关系类型 self.entity_types = [“DISEASE”, “DRUG”, “SYMPTOM”] self.relation_patterns = { “TREATS”: [“treat”, “prescribe”, “medication for”], “CAUSES”: [“cause”, “lead to”, “result in”], “HAS_SYMPTOM”: [“symptom”, “manifest”, “show”] } def extract_triples_from_text(self, text): “”“基于规则和句法分析抽取三元组。这是一个简化示例。”“” doc = self.nlp(text) triples = [] # 简单的共现+规则:如果在同一句子中,实体A和B出现,且有关键词连接,则假设存在关系 for sent in doc.sents: sent_entities = [] # 存储(实体文本, 实体类型) # 这里应调用更完善的NER函数,为简化,我们假设已识别 # sent_entities = self._custom_ner(sent.text) # 示例:手动添加假设实体 if “hypertension” in sent.text.lower(): sent_entities.append((“Hypertension”, “DISEASE”)) if “metformin” in sent.text.lower(): sent_entities.append((“Metformin”, “DRUG”)) if “treat” in sent.text.lower(): # 简单规则:如果句子包含”treat”,且存在疾病和药物实体,则建立TREATS关系 diseases = [e for e in sent_entities if e[1] == “DISEASE”] drugs = [e for e in sent_entities if e[1] == “DRUG”] for d in diseases: for dr in drugs: triples.append((d[0], “TREATS”, dr[0])) # 注意:这里关系方向是 药物 -> TREATS -> 疾病 return triples def standardize_entity(self, entity_name): “”“实体标准化,将同义词映射到标准名。”“” for std_name, synonyms in self.synonym_map.items(): if entity_name == std_name or entity_name in synonyms: return std_name return entity_name # 若未找到,返回原名称 def build_from_file(self, file_path): “”“从文件读取文本,构建图谱。”“” with open(file_path, ‘r’, encoding=‘utf-8’) as f: texts = f.readlines() all_triples = [] for text in texts: triples = self.extract_triples_from_text(text) all_triples.extend(triples) # 去重并导入Neo4j unique_triples = set(all_triples) for sub, rel, obj in unique_triples: std_sub = self.standardize_entity(sub) std_obj = self.standardize_entity(obj) # 创建节点 self.neo4j_driver.create_entity_if_not_exists(std_sub, self._infer_type(std_sub)) self.neo4j_driver.create_entity_if_not_exists(std_obj, self._infer_type(std_obj)) # 创建关系 self.neo4j_driver.create_relationship(std_sub, rel, std_obj) print(f“知识图谱构建完成,共处理{len(unique_triples)}条三元组。”) def _infer_type(self, entity_name): “”“根据实体名简单推断类型(实际项目应用更复杂的逻辑)。”“” # 这里仅为示例,实际应根据词典或模型判断 if entity_name in self.synonym_map.get(“高血压”, []): return “DISEASE” # … 其他推断逻辑 return “ENTITY” if __name__ == “__main__”: builder = MedicalKGBuilder() builder.build_from_file(“data/medical_texts.txt”)实操心得:在真实项目中,
extract_triples_from_text函数是整个系统的瓶颈和核心。上述代码仅为示意。一个可运行的初级版本,强烈建议从“半结构化”数据开始,比如从医学百科网站爬取或使用公开的结构化数据集(如Disease-Symptom关系对),直接生成三元组导入Neo4j。这能让你快速搭建起一个可查询的图谱原型,验证问答流程,而不是在复杂且不完美的信息抽取上耗费过多初期精力。
3.3 第三步:实现问答引擎——逻辑解析与代码
接下来,我们实现qa_engine.py。
# qa_engine.py import re from neo4j_handler import Neo4jHandler class MedicalQAEngine: def __init__(self): self.neo4j_driver = Neo4jHandler(“bolt://localhost:7687”, “neo4j”, “your_password”) self.intent_patterns = { “query_treatment”: r“(什么药.*治|治疗|用药).*?(疾病|病)?([\u4e00-\u9fa5]+)”, “query_symptom”: r“([\u4e00-\u9fa5]+)有.*?症状|([\u4e00-\u9fa5]+)的症状”, “query_disease_by_symptom”: r“症状.*?是.*?什么病|([\u4e00-\u9fa5]+).*?可能.*?什么病” } self.answer_templates = { “query_treatment”: “用于治疗{disease}的常见药物包括:{answer}。”, “query_symptom”: “{disease}的常见症状有:{answer}。”, “query_disease_by_symptom”: “出现{symptom}症状,可能罹患的疾病有:{answer}。” } def parse_question(self, question): “”“解析问题,识别意图和实体。”“” question = question.strip() detected_intent = None extracted_entity = None for intent, pattern in self.intent_patterns.items(): match = re.search(pattern, question) if match: detected_intent = intent # 从匹配组中提取实体,这里逻辑需根据正则表达式设计调整 groups = match.groups() for g in groups: if g and len(g) > 1: # 简单过滤掉短或无意义的组 extracted_entity = g break if extracted_entity: break return detected_intent, extracted_entity def query_kg(self, intent, entity): “”“根据意图和实体,组装并执行Cypher查询。”“” if not intent or not entity: return “未能理解您的问题,请尝试换一种方式提问。” cypher_template = “” if intent == “query_treatment”: cypher_template = “MATCH (d:Disease)<-[:TREATS]-(m:Drug) WHERE d.name = ‘{entity}’ RETURN m.name AS result” elif intent == “query_symptom”: cypher_template = “MATCH (d:Disease)-[:HAS_SYMPTOM]->(s:Symptom) WHERE d.name = ‘{entity}’ RETURN s.name AS result” elif intent == “query_disease_by_symptom”: cypher_template = “MATCH (s:Symptom)<-[:HAS_SYMPTOM]-(d:Disease) WHERE s.name = ‘{entity}’ RETURN d.name AS result” else: return “暂不支持该类型的问题。” query = cypher_template.format(entity=entity) try: records = self.neo4j_driver.run_query(query) results = [record[“result”] for record in records] return “、”.join(results) if results else “知识图谱中暂无相关信息。” except Exception as e: return f“查询知识图谱时出错:{e}” def answer_question(self, question): “”“对外提供的问答接口。”“” intent, entity = self.parse_question(question) kg_answer = self.query_kg(intent, entity) if intent and intent in self.answer_templates: final_answer = self.answer_templates[intent].format(**{intent.split(‘_’)[-1]: entity, “answer”: kg_answer}) else: final_answer = kg_answer return final_answer if __name__ == “__main__”: qa = MedicalQAEngine() while True: q = input(“请输入您的问题(输入’quit’退出): “) if q.lower() == ‘quit’: break answer = qa.answer_question(q) print(“回答:”, answer)3.4 第四步:搭建一个简单的Web界面(可选但推荐)
为了让系统更易用,我们用Flask快速搭建一个Web API。
# app.py from flask import Flask, request, jsonify from qa_engine import MedicalQAEngine app = Flask(__name__) qa_system = MedicalQAEngine() @app.route(‘/ask’, methods=[‘POST’]) def ask(): data = request.get_json() question = data.get(‘question’, ‘’) if not question: return jsonify({‘error’: ‘No question provided’}), 400 answer = qa_system.answer_question(question) return jsonify({‘question’: question, ‘answer’: answer}) if __name__ == ‘__main__’: app.run(debug=True, port=5000)运行后,你就可以通过curl或Postman发送请求了:curl -X POST -H “Content-Type: application/json” -d ‘{“question”: “高血压吃什么药?”}’ http://localhost:5000/ask。
4. 项目运行、调试与效果优化指南
4.1 如何让这个系统真正跑起来?
- 确保Neo4j服务运行:执行
docker ps确认medical-kg容器正在运行。访问http://localhost:7474,使用默认账号neo4j和你的密码登录Neo4j Browser。 - 准备初始数据:在
data/medical_texts.txt中放入一些简单的句子,例如:Metformin is used to treat diabetes. Hypertension may cause headache. Aspirin can relieve fever and pain. Diabetes has symptoms such as polyuria and polydipsia. - 顺序执行脚本:
python build_kg.py # 构建知识图谱,数据将导入Neo4j python app.py # 启动Flask问答服务 - 验证与查询:
- 在Neo4j Browser中执行
MATCH (n) RETURN n LIMIT 25,应该能看到创建的节点和关系图。 - 打开浏览器或使用
curl测试问答API。
- 在Neo4j Browser中执行
4.2 效果优化:从“能用”到“好用”
初始系统必然简陋。以下是几个关键的优化方向:
- 提升NER精度:
- 词典扩充:集成公开的医学词库,如UMLS的Metathesaurus(需申请),或中文的《医学主题词表》(MeSH)。
- 模型微调:使用
spacy的ner组件,在少量标注的医疗文本上进行迁移学习。或者使用像BERT-BiLSTM-CRF这样的架构从头训练。
- 强化关系抽取:
- 规则引擎升级:使用更复杂的句法模式(如Semgrex)或引入依存关系规则。
- 引入深度学习模型:对于复杂句子,可以考虑使用预训练语言模型(如BERT)进行关系分类,将两个实体及其上下文输入模型,判断关系类型。
- 丰富问答能力:
- 多跳推理:当前系统是单跳查询(疾病->药物)。如何回答“治疗高血压的药物有哪些副作用?”这就需要多跳:高血压->治疗药物->每种药物的副作用。这需要设计更复杂的Cypher查询模板。
- 模糊匹配与实体消歧:当用户输入“高血脂”但图谱中只有“高脂血症”时,需要模糊匹配。当“苹果”可能指水果也可能指公司时,需要根据上下文消歧。这需要引入实体链接(Entity Linking)模块。
- 引入RAG作为补充:对于图谱中未覆盖的、或需要长篇解释的知识(如疾病详细描述、用药指南),可以额外维护一个医疗文档向量库。系统先在图谱中查询精确答案,若未果或答案不完整,则转向向量库检索相关文档片段,并利用本地LLM(如通过
llama.cpp运行的Qwen2-7B)生成补充说明。这就是图谱的精确性与LLM的生成灵活性的结合。
4.3 避坑实录:我踩过的那些坑
- Neo4j连接超时:确保防火墙开放了7687端口(Bolt协议),并且Docker容器映射正确。连接字符串是
bolt://localhost:7687,不是http。 - Cypher查询性能慢:当图谱变大时,像
MATCH (n) WHERE n.name = ‘xxx’这样的查询会全图扫描。务必为节点标签和属性创建索引:CREATE INDEX ON :Disease(name)和CREATE INDEX ON :Drug(name)。 - 实体标准化不一致:这是维护的噩梦。务必在项目初期就设计好标准术语表,所有抽取的实体都必须经过这个表的映射和清洗,确保图谱内同一个概念只有一个标准节点。
- 正则表达式意图识别过于脆弱:用户提问方式千变万化。规则列表会越来越长且难以维护。当规则超过50条时,就该考虑引入一个意图分类模型了,哪怕只是一个简单的FastText或TextCNN模型,其泛化能力也远胜于一堆
if-else。 - 数据质量决定天花板:知识图谱领域有句名言“Garbage in, garbage out”。投入在数据清洗、标准化和验证上的时间,远比后期调优算法回报更高。务必建立一套数据质量的检查和修正流程。
这个项目提供了一个完整的骨架和可运行的代码。它的价值不在于解决所有问题,而在于清晰地展示了构建一个领域知识图谱问答系统的核心路径、技术选型的权衡以及每一步的具体实现。你可以以此为基础,根据实际需求,在数据、算法、交互等任何一个环节进行深化和扩展,打造出真正适用于特定场景的智能问答工具。
本文还有配套的精品资源,点击获取