☰
中文医学文本实体关系抽取Python源码包拆解与实战
2026/10/3 2:46:14 网站建设 项目流程

简介:本资源为基于Python实现的中文医学文本实体关系抽取完整源码包,面向人工智能、自然语言处理方向的高校学生与开发者,尤其适合作为期末大作业、课程设计或入门级NLP项目实践参考。包内共13个文件,以12个py源码文件与1个txt使用说明为主,压缩包约28KB,涵盖实体识别、关系抽取、模型定义、评估脚本及Flask接口服务等模块,结构清晰便于二次开发。资源围绕中文医学文本这一垂直领域展开,涉及数据预处理、模型构建、关系API封装与运行评估等关键环节,可帮助读者理解从数据到服务部署的完整流程。目前已有514人学习下载,适合需要快速获取可运行代码、对照调试与拓展实验的读者参考使用。

1. 中文医学文本实体关系抽取:一份能跑通的 Python 源码包拆解

医学文本里「阿司匹林」和「胃出血」之间到底是「导致」还是「治疗」,机器读错一个字,下游的用药提醒、病历质控、知识图谱构建全跟着翻车。这份基于 Python 实现的中文医学文本实体关系抽取源码包,解决的就是从非结构化病历、文献里把「实体对 + 关系类型」结构化抽出来的问题。它包含实体识别与关系分类两条主流程,配套 Flask 接口、评估脚本和共享数据结构模块,适合做课程设计、期末大作业,也适合想快速搭一套医学信息抽取原型的从业者。拿到手先别急着改模型,把数据格式和调用链摸清楚,后面省一半时间。

2. 源码包结构与运行链路:从 run_entity.py 到 Flask 接口

2.1 目录里每个文件到底管什么

先把包解开,根目录下能看到这些核心文件:run_entity.py、run_relation.py、run_eval.py、relation_api.py、run_relation_api.py、flask_server.py、models.py、utils.py、some_function.py,以及shared/目录下的__init__.py、data_structures.py、const.py。这不是一个「一个脚本打天下」的玩具工程,而是把实体抽取、关系抽取、评估、服务化拆开了。

文件职责什么时候会动它
run_entity.py实体识别入口,加载模型跑推理换实体模型、调 batch size
run_relation.py关系分类入口,输入实体对输出关系换关系模型、改标签映射
run_eval.py评估脚本,算 P/R/F1验证自己训的模型
models.py模型结构定义换编码器、改分类头
utils.py数据加载、分词、指标工具改数据路径、改预处理
shared/const.py全局常量、标签集合加关系类型、改实体类型
shared/data_structures.py样本、实体、关系的数据类改字段、加元信息
relation_api.py关系抽取的 API 封装对外提供服务
flask_server.pyHTTP 服务入口部署、调端口

常见做法是:run_entity.py先跑出实体,run_relation.py再基于实体对做关系分类,run_eval.py拿标注数据算指标,最后flask_server.py把整条链路包成接口。这个分层很清晰,改哪一层都不会把别的层带崩。

2.2 环境准备与依赖安装

源码包没有锁死 Python 版本,但这类中文 NLP 工程一般跑在 Python 3.7~3.9 上最稳。先建虚拟环境,别直接往系统 Python 里装,不然依赖冲突能折腾一下午。

# 建虚拟环境,python3.8 兼容性最好 python3.8 -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 升级 pip,老版本装包容易卡 pip install --upgrade pip # 装核心依赖,版本按实际报错微调 pip install torch==1.13.1 pip install transformers==4.30.2 pip install flask pip install numpy pandas scikit-learn pip install jieba

逻辑说明:torch和transformers是模型推理的底座,flask给服务化用,jieba处理中文分词。参数上,torch版本要和 CUDA 对齐——有 GPU 就装对应 cu 版本,没 GPU 装 CPU 版也能跑,只是慢。transformers别装太新,4.30 附近对老模型结构兼容性好,装到 4.4x 有时加载权重会报 key 不匹配。

提示:如果pip install torch卡在下载,先确认网络能访问包源,或者换国内镜像源,别硬等。

2.3 跑通实体识别:run_entity.py 的调用方式

实体识别是第一步,输入一段中文医学文本,输出实体列表和类型。典型调用长这样:

# 基本用法:指定输入文件和模型目录 python run_entity.py \ --input_file data/sample.txt \ --model_dir checkpoints/entity_model \ --batch_size 16 \ --max_seq_len 128 \ --device cuda:0

逻辑说明:--input_file是待抽取的文本,一行一条;--model_dir指向实体模型权重目录;--batch_size控制一次喂多少条,显存小就调到 8 或 4;--max_seq_len是截断长度,医学文本句子普遍不长,128 够用,长病历可以提到 256;--device有 GPU 写cuda:0,没有写cpu。

跑完会在输出目录生成实体结果文件,格式一般是「文本 + 实体 + 类型 + 起止位置」。如果报FileNotFoundError,先看--model_dir路径对不对;如果报CUDA out of memory,把batch_size砍半再试。这一步跑通,说明环境和模型加载都没问题。

2.4 关系抽取与评估:run_relation.py 和 run_eval.py

实体有了,接下来判断实体对之间的关系。run_relation.py的输入是实体识别结果,输出是「实体1 - 关系 - 实体2」三元组。

# 关系抽取:基于实体结果跑关系分类 python run_relation.py \ --entity_file outputs/entities.json \ --model_dir checkpoints/relation_model \ --relation_dict shared/const.py \ --batch_size 32 \ --device cuda:0 # 评估:拿标注数据算指标 python run_eval.py \ --pred_file outputs/relations.json \ --gold_file data/gold_relations.json \ --output_file outputs/eval_report.txt

逻辑说明:--entity_file是上一步的实体输出,--relation_dict指定关系标签集合(在shared/const.py里定义),--batch_size关系分类通常比实体识别能吃更大 batch。run_eval.py的--pred_file是预测结果,--gold_file是人工标注,输出 P/R/F1。如果 F1 低得离谱,先别怀疑模型,检查预测和标注的标签名是否一致——标签对不上,指标必然崩。

3. 数据格式与标签体系:shared 目录里的黑匣子

3.1 data_structures.py 定义了哪些字段

shared/data_structures.py是整个工程的数据契约,实体、关系、样本都在这定义。常见结构是Entity(文本、类型、起止)、Relation(头实体、尾实体、关系类型)、Sample(原文、实体列表、关系列表)。改数据格式前先读这个文件,不然预处理和模型对不上。

# shared/data_structures.py 典型结构(示意) from dataclasses import dataclass, field from typing import List @dataclass class Entity: text: str # 实体文本,如"阿司匹林" type: str # 实体类型,如"药物" start: int # 起始位置 end: int # 结束位置 @dataclass class Relation: head: Entity # 头实体 tail: Entity # 尾实体 rel_type: str # 关系类型,如"导致" @dataclass class Sample: text: str # 原始文本 entities: List[Entity] = field(default_factory=list) relations: List[Relation] = field(default_factory=list)

逻辑说明:用dataclass定义,字段清晰,序列化方便。start/end是字符级偏移,中文按字符算,别按字节算,否则位置全错。rel_type直接存字符串,和const.py里的标签集合对应。

3.2 const.py 里的标签集合怎么改

shared/const.py管全局常量,最重要的是实体类型和关系类型的枚举。想加一个新关系类型,比如「禁忌」,就在这里加,然后确认模型分类头维度跟着变。

# shared/const.py 典型内容(示意) ENTITY_TYPES = ["药物", "疾病", "症状", "检查", "部位"] RELATION_TYPES = [ "导致", # 药物导致症状 "治疗", # 药物治疗疾病 "检查", # 检查诊断疾病 "位于", # 疾病位于部位 "禁忌", # 新增:药物禁忌人群 ] REL2ID = {r: i for i, r in enumerate(RELATION_TYPES)} ID2REL = {i: r for r, i in REL2ID.items()}

逻辑说明:REL2ID和ID2REL是双向映射,模型输出的是 id,转回标签靠ID2REL。加关系类型后,models.py里分类头的输出维度要同步改成len(RELATION_TYPES),否则加载权重会报维度不匹配。这是最容易踩的坑之一。

3.3 数据预处理与分词注意点

中文医学文本分词是个玄学。通用分词器会把「非小细胞肺癌」切成「非小细胞 / 肺癌」,实体边界就错了。常见做法是:实体识别阶段用字符级输入,避免分词误差;关系分类阶段再把实体文本拼进模板。

# utils.py 里常见的文本处理逻辑(示意) import jieba def tokenize(text, mode="char"): if mode == "char": return list(text) # 字符级,实体边界准 else: return list(jieba.cut(text)) # 词级,语义更整 def build_relation_input(text, head, tail): # 用特殊标记标出头尾实体,喂给关系分类模型 marked = text.replace(head.text, f"[H]{head.text}[/H]") marked = marked.replace(tail.text, f"[T]{tail.text}[/T]") return marked

逻辑说明:tokenize支持字符级和词级两种模式,实体识别建议字符级。build_relation_input用[H]、[T]标记头尾实体,这是关系分类的常见套路,模型靠标记定位实体。注意replace可能误替换——如果实体文本在别处也出现,会标错位置,严谨做法是按start/end偏移插入标记。

4. 服务化与接口调用:flask_server.py 怎么用

4.1 启动 Flask 服务

flask_server.py把整条链路包成 HTTP 接口,方便别的系统调用。启动方式:

# 启动服务,默认 5000 端口 python flask_server.py --host 0.0.0.0 --port 5000 --entity_model checkpoints/entity_model --relation_model checkpoints/relation_model

逻辑说明:--host 0.0.0.0允许外部访问,只本机用写127.0.0.1;--port改端口,被占用就换;两个--model参数分别指向实体和关系模型。启动后日志会打印监听地址,看到Running on http://...就说明起来了。

4.2 接口请求与返回格式

服务起来后,用 curl 或 Python 请求测试:

# 发一条文本,拿实体和关系 curl -X POST http://127.0.0.1:5000/extract \ -H "Content-Type: application/json" \ -d '{"text": "阿司匹林可用于治疗冠心病,但可能引起胃出血。"}'

返回结构一般是:

{ "entities": [ {"text": "阿司匹林", "type": "药物", "start": 0, "end": 4}, {"text": "冠心病", "type": "疾病", "start": 9, "end": 12}, {"text": "胃出血", "type": "症状", "start": 18, "end": 21} ], "relations": [ {"head": "阿司匹林", "tail": "冠心病", "type": "治疗"}, {"head": "阿司匹林", "tail": "胃出血", "type": "导致"} ] }

逻辑说明:entities是实体列表,relations是三元组。如果返回空,先确认模型加载成功,再看输入文本是否为空。接口超时一般是模型推理慢,调小batch_size或换 GPU。

4.3 relation_api.py 与 run_relation_api.py 的分工

relation_api.py是关系抽取的 API 封装层,run_relation_api.py是它的启动入口。这种拆法方便单独部署关系服务,不和实体服务绑死。常见做法是实体和关系各起一个服务,通过内部调用串联,这样任一模型更新不影响另一个。

# relation_api.py 典型封装(示意) from flask import Flask, request, jsonify from models import RelationModel from utils import load_model, build_relation_input app = Flask(__name__) model = None def init_model(model_dir): global model model = load_model(model_dir) @app.route("/relation", methods=["POST"]) def predict_relation(): data = request.get_json() text = data["text"] head = data["head"] tail = data["tail"] inp = build_relation_input(text, head, tail) rel = model.predict(inp) return jsonify({"relation": rel})

逻辑说明:init_model在启动时加载模型,避免每次请求都加载。/relation接口接收文本和头尾实体,返回关系类型。参数上,head、tail要传实体对象或至少传文本和位置,只传文本容易定位错。

5. 避坑与常见问题排查

5.1 模型加载报 key 不匹配

现象:启动时报Missing key(s) in state_dict或Unexpected key(s)。原因:模型结构改了(比如加了关系类型),但权重还是旧的,或者transformers版本差异导致参数名变了。解决:确认const.py里标签数量和models.py分类头维度一致;换回训练时的transformers版本;实在不行重新训一版权重。

5.2 实体位置偏移对不上

现象:关系抽取时头尾实体定位错,抽出的关系张冠李戴。原因:实体识别输出的是字符偏移,但预处理时做了分词或截断,偏移没同步更新。解决:统一用字符级偏移,截断时同步裁剪实体位置;build_relation_input按start/end插入标记,别用replace。

5.3 评估指标异常低

现象:run_eval.py跑出来 F1 只有零点几。原因:预测和标注的标签名不一致(比如一个用「导致」一个用「引起」),或者数据没对齐。解决:先打印几条预测和标注对比,确认标签体系一致;检查gold_file格式和pred_file是否同构。

5.4 Flask 服务启动后请求超时

现象:接口调不通,或等很久才返回。原因:模型在 CPU 上跑,或者batch_size太大导致单次推理慢。解决:换 GPU;调小batch_size;给 Flask 加超时配置;确认没有在请求里重复加载模型。

5.5 中文编码问题

现象:读文件报UnicodeDecodeError,或输出乱码。原因:文件编码不是 UTF-8,Windows 下常见 GBK。解决:读写文件统一指定encoding="utf-8";open时加errors="ignore"兜底;确认终端编码也是 UTF-8。

6. 进阶技巧:把抽取结果接进知识图谱

跑通基础流程后,真正有价值的是把三元组接进下游。我一般会加一层后处理,把关系结果转成图数据库能吃的格式。以 Neo4j 为例,先导出 CSV:

# 把关系结果转成 Neo4j 导入格式 import json import csv def export_to_neo4j(relation_file, out_nodes, out_edges): with open(relation_file, encoding="utf-8") as f: data = json.load(f) nodes = {} # 去重实体 edges = [] # 关系边 for item in data: for ent in item["entities"]: key = (ent["text"], ent["type"]) nodes[key] = ent["type"] for rel in item["relations"]: edges.append((rel["head"], rel["type"], rel["tail"])) with open(out_nodes, "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["name", "type"]) for (name, typ) in nodes: w.writerow([name, typ]) with open(out_edges, "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["head", "relation", "tail"]) for e in edges: w.writerow(e) export_to_neo4j("outputs/relations.json", "nodes.csv", "edges.csv")

逻辑说明:nodes用(文本, 类型)去重,避免同一实体重复建节点;edges存头实体、关系、尾实体。导出的 CSV 用 Neo4j 的LOAD CSV导入,节点和边分开建。参数上,relation_file是上一步输出,out_nodes、out_edges是导出路径。

验证方法:导入后跑一句MATCH (a)-[r]->(b) RETURN a, r, b LIMIT 25,看图谱是否连通。如果边大量悬空,说明实体名没对齐——实体识别和关系抽取的实体文本必须完全一致,差一个空格都连不上。

还有个技巧是加置信度过滤。模型输出的关系通常带概率,低于阈值的直接丢,宁可少抽也别抽错。我一般把阈值设在 0.7 附近,医学场景对准确率要求高,召回可以牺牲一点。阈值不是拍脑袋定的,拿评估集跑几组,看 P/R 曲线拐点在哪。

从那以后我每次接新模型,都强制先跑一遍run_eval.py看指标,再拿几条真实文本肉眼过一遍,最后才接下游。这套流程帮我挡掉过好几次「指标好看但实际乱抽」的翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询