ARTICLE DETAIL

资讯详情

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

知识图谱驱动的问答系统实战:心血管疾病问答与Neo4j实现

知识图谱驱动的问答系统实战:心血管疾病问答与Neo4j实现 简介面向高校计算机、人工智能、自动化等专业学生这份基于知识图谱的心血管疾病问答系统毕业设计源码完整覆盖了疾病数据构建、知识图谱建模到问答输出的全流程。项目以心血管疾病为核心CSV文件存储心房颤动、冠心病、心肌梗死、心力衰竭等疾病与症状关系数据JSON文件组织图谱结构Python脚本实现实体抽取与问答逻辑另有JavaScript前端脚本、说明文档和演示图片辅助理解。解压后共197个文件、5.52MB包含84个CSV、84个JSON和6个Python文件等目录结构清晰便于系统学习与二次开发。代码经过调试测试确保可运行曾获答辩评审98分既能作为毕业设计参考也适合课程设计与实践进阶。目前已有195人学习项目整体借鉴价值较高基础较好者还可在此基础上修改扩展出不同功能。1. 基于知识图谱的心血管疾病问答系统不是大模型也能把问答做到 85 分先给一个反直觉的结论这类基于知识图谱的问答系统真正难的不是问答算法而是图谱里的数据够不够细、关系建得对不对。心血管疾病问答绕不开“冠心病吃什么药”“胸闷挂哪个科”“高血压能不能喝茶”这类问题表面上是自然语言处理实质上是把一句话映射成一条图查询。你不需要训练 BERT也不需要调大模型靠实体链接加规则模板就能覆盖毕业设计里绝大多数问答场景。我把这套系统拆成四块先建知识图谱再做问答管道然后用 Flask 对外提供服务最后是可视化演示。对 Python 基础和数据库有初步了解的人按这个路径两周内可以跑通。代码量不会太大但工程完整度、数据处理质量和演示效果往往比模型复杂度更影响评分这也是它能拿到 98 分的核心原因。2. 先建知识本体再谈问答心血管图谱的数据模型与 Neo4j 导入2.1 心血管实体关系怎么抽取与定义问答系统能回答什么问题取决于图谱里有什么实体和关系。以心血管疾病领域为例我一般把实体分为六类疾病、症状、药物、检查、科室、饮食禁忌。这六类不是拍脑袋定的而是从常见问句中反推出来的。用户问“胸痛是怎么回事”需要“症状”和“疾病”两个实体问“冠心病用什么药”需要“疾病”和“药物”。关系设计也要跟着问答意图走。常见关系有这些关系方向对应问题HAS_SYMPTOM疾病 - 症状冠心病有什么症状TREAT_WITH疾病 - 药物心绞痛用什么药SUGGEST_CHECK疾病 - 检查怀疑心梗需要查什么DEPARTMENT疾病 - 科室心律失常挂哪个科FORBID_DIET疾病 - 饮食高血压不能吃什么DRUG_SIDE_EFFECT药物 - 副作用阿司匹林有什么副作用数据来源不复杂常见做法是从医学公开资料、药品说明书和临床指南里手工整理。不建议一上来就写爬虫先保证前 20 个疾病、50 个症状、30 种药物的质量比堆出几千条脏数据更有说服力。每条数据要保留“来源”属性答辩时能说清楚数据怎么来的。数据格式我建议直接用 JSON 或 CSV。CSV 对非技术同学更友好每行是一个实体关系写在另一个文件里。下面是一份简化后的疾病数据name,alias,department,symptom,drug 冠心病,冠状动脉粥样硬化性心脏病,心内科,胸痛;胸闷;心悸,阿司匹林;他汀类药物 高血压,原发性高血压,心血管内科,头晕;头痛;耳鸣,硝苯地平;厄贝沙坦 心力衰竭,心衰,心内科,呼吸困难;水肿;乏力,呋塞米;美托洛尔2.2 用 Cypher 把 CSV 批量导入 Neo4j实体定义好了接下来导入图谱。第一个坑是不要把 CSV 直接手工粘贴到 Neo4j Browser。常见做法是把文件放到 Neo4j 安装目录的import文件夹下然后用LOAD CSV批量导入。这样能保证导入逻辑可重复代码也能交给老师检查。// 导入疾病节点MERGE 避免重复创建 LOAD CSV WITH HEADERS FROM file:///diseases.csv AS row MERGE (d:Disease {name: row.name}) ON CREATE SET d.alias row.alias, d.department row.department, d.symptom row.symptom, d.drug row.drug;这段代码里MERGE是关键。它会先检查图谱里是否已经存在同名疾病节点存在就不重复创建只能说ON CREATE更新属性。如果直接用CREATE同一个疾病重复导入会出现多个节点后续查询会查到重复结果问答准确率直接掉一截。导入关系时需要注意关系的两个端点必须先存在。所以顺序上先导入实体再创建关系。下面是把疾病和症状连接起来的 Cypher// 把疾病 CSV 中的 symptom 字段拆开逐条建立 HAS_SYMPTOM 关系 UNWIND row.symptom AS splitItem MATCH (d:Disease {name: row.name}) MERGE (s:Symptom {name: trim(splitItem)}) MERGE (d)-[:HAS_SYMPTOM]-(s)这里的UNWIND是为了把胸痛;胸闷;心悸拆成多行trim去掉前后空格。如果 CSV 里某个症状名后面带空格导入后症状节点会变成胸痛用户输入“胸痛”就匹配不上这是非常容易踩的坑。建议导入后立刻执行MATCH (s:Symptom) RETURN s.name检查一遍有没有脏数据。2.3 图谱构建的质量校验知道你的图谱里到底有什么图谱建完不能直接关掉先跑几条统计语句确认数据底盘。我一般会执行下面这组查询// 统计各类型节点数量 MATCH (n) RETURN labels(n)[0] AS type, count(*) AS count ORDER BY count DESC;如果你发现症状节点数量比疾病节点还少说明关系导入可能出了问题。再查一下悬空节点也就是没有任何关系的孤立点MATCH (n) WHERE NOT (n)--() RETURN labels(n)[0] AS type, n.name AS name LIMIT 50;这一步很有必要。孤立节点不会参与任何回答只会让前端可视化时多一堆孤零零的圆点。我在做数据库可视化时就遇到过 Neo4j Browser 默认只显示 25 个标签的情况打开大图看着很稀散后来才意识到是所有节点都是独立的根本不存在连边。排查完关系链路后图就密集多了。图数据库不像关系型数据库数据不联网查询你很难发现问题所以统计验证必须做。3. 问答系统核心意图识别、实体链接与 Cypher 模板生成3.1 意图设计先想清楚用户会怎么问问答管道的第一步是判断“用户在问什么”。问法五花八门但落到知识图谱上意图种类是有限的。以心血管系统为例我把意图收敛为六类意图名触发问法例子生成的语义动作symptom冠心病有什么症状查询疾病对应症状treat心绞痛吃什么药查询疾病对应药物check心力衰竭要做什么检查查询检查项目department心律失常挂哪个科查询对应科室forbid高血压不能吃什么查询饮食禁忌side_effect阿司匹林的副作用是什么查询药物副作用不要一上来就训练文本分类模型。领域数据量小标注成本高规则加词表的方式在毕业设计场景下完全够用。但规则要写对尤其要注意否定词和疑问词。比如“冠心病吃什么药”和“冠心病不该吃什么药”是两个方向前者查治疗药物后者查用药禁忌。3.2 实体链接触发先构造词典再用 jieba 的 userdict意图识别定方向实体识别定参数。对中文医学问答来说命名实体识别不需要跑 BERT一个词典分词器加实体对齐就能解决大部分情况。具体做法是先从图谱里把所有实体名称和别名查出来构造一个自定义词典然后用 jieba 的add_word把这些词全部加进去。# coding: utf-8 import jieba from py2neo import Graph class EntityRecognizer: def __init__(self, graph): self.graph graph self.disease_set set() self.symptom_set set() self.drug_set set() def load_dict_from_graph(self): 从 Neo4j 查询所有实体名称构建词典。 这里只示例疾病和症状实际项目会同步加载药物、检查、科室。 for record in self.graph.run(MATCH (d:Disease) RETURN d.name AS name, d.alias AS alias): self.disease_set.add(record[name]) if record[alias]: for alias in str(record[alias]).split(;): jieba.add_word(alias) jieba.add_word(record[name]) for record in self.graph.run(MATCH (s:Symptom) RETURN s.name AS name): self.symptom_set.add(record[name]) jieba.add_word(record[name]) def recognize(self, question): 用 jieba 切词再匹配图谱中的实体名称。 tokens jieba.lcut(question) entities {disease: [], symptom: [], drug: []} full_text question # 优先长词匹配比如“冠状动脉粥样硬化性心脏病”优先于“冠心病” for name in self.disease_set: if name in full_text: entities[disease].append(name) for name in self.symptom_set: if name in full_text: entities[symptom].append(name) for name in self.drug_set: if name in full_text: entities[drug].append(name) return entities这里的localDict我故意没用外部配置文件而是直接运行时从图库拉取好处是每次更新 Neo4j 数据重启服务词典也跟着更新不需要手动维护一份同步文件。if name in full_text这种方式比 jieba 分词后逐个比对更稳定因为图谱实体本质上是领域词表全文本子串匹配能直接绕开分词歧义。3.3 生成 Cypher 并把图查询结果拼成回答实体和意图都有了下一步就是把它们翻译成 Cypher。不要每个意图手写一堆 if-else 连接字符串那样太容易出错。我习惯把意图和查询模板做成一个映射表模板里的{disease}和{drug}是占位符运行时填实体值。# coding: utf-8 TEMPLATES { symptom: MATCH (d:Disease)-[:HAS_SYMPTOM]-(s:Symptom) WHERE d.name $name OR d.alias CONTAINS $name RETURN s.name AS result , treat: MATCH (d:Disease)-[:TREAT_WITH]-(m:Medicine) WHERE d.name $name OR d.alias CONTAINS $name RETURN m.name AS result , department: MATCH (d:Disease)-[:DEPARTMENT]-(t:Department) WHERE d.name $name OR d.alias CONTAINS $name RETURN t.name AS result } def build_answer(question, entities, intent): 根据意图和实体执行 Cypher 查询拼装成自然语言答案。 graph Graph(bolt://127.0.0.1:7687, auth(neo4j, your_password)) if not entities[disease] and not entities[drug]: return 我暂时没有理解您提到的疾病或药物换个说法试试 # 优先用疾病实体作为查询主体 name entities[disease][0] cypher TEMPLATES.get(intent) if not cypher: return 抱歉这个问题超出了我的知识范围。 results graph.run(cypher, namename).data() if not results: return f图谱中暂无{name}的相关{intent}信息。 answer_items [record[result] for record in results] return f{name}相关的{intent}信息包括{、.join(answer_items)}。模板里为什么不直接写死d.name $name因为用户很可能输入的是“风湿性心脏病”的别名“风心病”。我这里用d.alias CONTAINS $name能提升一部分匹配率。更稳妥的做法是实体链接阶段就把别名统一成标准名再传标准名给 Cypher。两者结合使用效果最好。这一段的坑在于如果用户同时问“高血压和冠心病哪个该挂心内科”实体识别会拿到两个疾病但模板一次只处理一个主体。毕业设计阶段可以不处理这种复杂问句答辩时坦白说明“系统支持单实体单意图的问答”比硬撑着自己埋 bug 要诚实得多。4. 把问答跑成 Web 服务Flask py2neo 封装与高频踩坑4.1 项目结构与依赖版本怎么选前面的代码还只是脚本毕业设计需要变成可演示的 Web 系统。我对项目目录的建议是这样cardio-qa/ ├── app.py # Flask 入口 ├── requirements.txt ├── data/ # CSV 数据文件 ├── graphModels.py # 图谱操作封装 ├── queryParser.py # 意图识别与实体链接 └── templates/ # 前端页面requirements.txt中写入关键依赖但注意版本要锁范围别直接安装最新版。我踩过 py2neo 4 到 5 的 API 变化Graph.run()返回的 Record 访问方式都有差异。稳妥的配置是flask2.2.5 py2neo2021.2.3 jieba0.42.1这组版本我在 Neo4j 4.4 上跑得很稳定。如果你用 Neo4j 5.x需要把 py2neo 换到 2021.2.4 以上但bolt://协议和密码认证方式保持一样。不要图新直接装 py2neo 6网上能找到的教程大部分不兼容。4.2 用 py2neo 封装一个可复用的 GraphService问答模块会反复执行 Cypher与其到处写Graph()不如封装一个服务类。这里要处理一个细节Flask 多线程环境下每个请求都 new 一个 Graph 连接是高成本的。我的封装方式是模块级单例同时给每次查询设置超时保护。# coding: utf-8 from flask import Flask, request, jsonify from py2neo import Graph from queryParser import EntityRecognizer, build_answer app Flask(__name__) # 模块全局只用这一个连接实例 graph Graph(bolt://127.0.0.1:7687, auth(neo4j, your_password), nameneo4j) app.route(/api/q, methods[POST]) def qa(): data request.get_json() question data.get(question, ).strip() if not question: return jsonify({error: question is empty}), 400 recognizer EntityRecognizer(graph) entities recognizer.recognize(question) intent detect_intent(question) if not entities and intent unknown: return jsonify({answer: 暂时无法回答换个问法试试。}) answer build_answer(question, entities, intent, graph) return jsonify({answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)这段代码把实体识别、意图识别和答案生成串在一起。注意detect_intent(question)函数我在这里没有展开它的本质是基于关键词字典的规则匹配器比如“什么症状”映射到symptom“挂哪个科”映射到department。调试时第一件事就是打印intent和entities确认规则有没有先指向正确。4.3 前端可视化ECharts 展示图谱不写复杂框架评分高的毕业设计通常都带一个能看到的图谱可视化页面。不用写 React 和 Vue直接一个 HTML 页面拉 ECharts 即可。后端提供一个接口返回图谱中所有节点和关系app.route(/api/graph) def graph_data(): query MATCH (n)-[r]-(m) RETURN id(n) AS source, labels(n)[0] AS sourceLabel, n.name AS sourceName, id(m) AS target, labels(m)[0] AS targetLabel, m.name AS targetName, type(r) AS relation LIMIT 500 rows [record for record in graph.run(query)] nodes [] edges [] # 用 dict 存已出现的节点 id避免重复 node_map {} for row in rows: for item in [(row[source], row[sourceLabel], row[sourceName]), (row[target], row[targetLabel], row[targetName])]: if item[0] not in node_map: node_map[item[0]] {id: item[0], name: item[2], category: item[1]} nodes.append(node_map[item[0]]) edges.append({source: row[source], target: row[target], name: row[relation]}) return jsonify({nodes: nodes, links: edges})这里LIMIT 500是故意加的。如果直接不带限制查询会把整个图谱拉起前端渲染几千个节点浏览器直接卡死。演示时就限制到 500 条关系页面加载流畅老师点击节点也能看到不少子图。如果你想在 Neo4j Browser 中看更多标签可以在浏览器设置或 Cypher 里加LIMIT 200Neo4j Browser 默认显示 25 个标签的问题就能缓解。4.4 高频故障py2neo 版本兼容和空结果排查法最常见的报错是AttributeError: Graph object has no attribute run。出现这个说明 py2neo 版本太旧v4 之前叫cypher.execute()。解决办法升级到 py2neo 2021.2.3或者把代码改成旧版 API。毕业设计答辩时一般不会现场升级库所以开发阶段就要锁定版本。第二个高频问题是“能返回结果但答案为空”。排查顺序是先打印用户输入原文确认中文编码没问题再打印识别出的实体确认图谱里有这个实体然后手动在 Neo4j Browser 里执行模板 Cypher确认关系存在。大多数情况下是实体不一致比如用户在 CSV 里写的是“心肌梗死”图谱里的标准名是“急性心肌梗死”用户问“心梗”时子串匹配不上。解决办法很简单在别名列表里补上“心梗”。# 调试时建议加了这句帮助快速定位问答链路 print([DEBUG] intent:, intent, entities:, entities, question:, question)5. 从跑通到拿高分演示脚本、问答优化与效果验证技巧5.1 为答辩准备一个 8 分钟演示脚本一个问答系统好不好老师很容易通过现场提问来判断。我建议你准备三条固定演示路径每条路径的输入是完整问句不要现场临场敲字避免手滑。第一条演示“症状查询”问“冠心病有哪些症状”展示节点高亮。第二条演示“疾病诊断推理”问“胸痛和水肿可能是哪个疾病”这需要图谱支持反向关系查询。如果你目前只做了单一疾病查询那就把第二条换成“心衰挂哪个科”。第三条演示一定要体现多跳查询能力。比如“阿司匹林有什么副作用”看起来只查一个节点但“冠心病患者吃阿司匹林需要注意什么”就需要跨疾病、药物、禁忌三个类型节点。这种多跳问题最能体现知识图谱相比关系型数据库的优势。5.2 问答效果优化同义词合并与缓存热答案知识图谱问答的体验瓶颈在于用户口语和标准术语不一致。除了在前面实体识别里加别名我还会在 Cypher 查询前做一层同义词归一化SYNONYMS { 心梗: 急性心肌梗死, 风心: 风湿性心脏病, 高血压病: 原发性高血压, } def normalize_entity(text): for key, value in SYNONYMS.items(): if key in text: text text.replace(key, value) return text词典维护成本低80% 的用户输入用几张同义词表就能覆盖。此外热点问题会被反复查询比如“高血压不能吃什么”每查询一次遍历一次图谱很浪费。常见做法是用一个字典做内存缓存命中就直接返回答案answer_cache {} def answer_with_cache(question_hash, answer): if question_hash in answer_cache: return answer_cache[question_hash] answer_cache[question_hash] answer return answer5.3 效果评估用 50 条问答验证你的图谱完整性最后做一个带指标评分的验收。从学校课程提问里搜集 50 条常见问题人工标注正确答案然后把系统答案跑一遍统计准确率# 简单示例手动建立测试集计算命中率 test_cases [ (冠心病吃什么药, [阿司匹林, 他汀类药物]), (心衰有什么症状, [呼吸困难, 水肿]), ] correct 0 for question, expected in test_cases: answer system_answer(question) if any(item in answer for item in expected): correct 1 print(f命中率: {correct}/{len(test_cases)})这步很建议写进文档。哪怕准确率只有 80%也比空口说“效果不错”更有说服力。另外建议把未命中的问题整理成一张表归因到意图识别、实体错配、图谱缺失三类这个过程能向你展示知识图谱迭代优化的闭环路径。本文还有配套的精品资源点击获取
返回列表