首页 / 资讯中心 / 文章详情

中文医学文本实体关系抽取Python源码包:从数据预处理到模型推理的完整实战

中文医学文本实体关系抽取Python源码包:从数据预处理到模型推理的完整实战 ★ FEATURED ARTICLE
简介本资源面向计算机与医学信息处理方向的学生及开发者提供一套基于Python的中文医学文本实体关系抽取完整实现适合课程设计、毕业设计或NLP入门实战。项目以CHIP-2020-2中文医学文本实体关系抽取数据集为核心涵盖儿科与百种常见疾病语料包含近7.5万三元组、2.8万疾病语句及53种预定义schema并针对临床实践文本指代关系明显的特点设计了主题实体补全与句子拼接规则。压缩包共55个文件约4.03MB以19个Python源码文件为主体辅以json数据配置、csv训练日志、xml工程配置及png训练曲线图覆盖模型定义、数据加载、schema解析、训练评估与预测全流程。资源中已包含GPLinker模型实现、chinese_roformer预训练配置及多轮训练结果记录读者可据此复现基线、分析F1变化并理解医学关系抽取的工程细节。目前已有1567人学习下载适合希望快速掌握中文医学NLP项目落地方法的读者参考。1. 中文医学文本实体关系抽取一份能跑通的 Python 源码包到底长什么样如果你正在做医学 NLP 方向的课程设计大概率会遇到一个尴尬局面网上能找到的实体关系抽取代码要么是英文数据集上的 demo要么是只有模型没有数据、只有数据没有预处理、只有预处理又没有评估脚本的三缺一工程。真正拿到手能从头跑通、能改、能写进报告里的完整项目其实不多。这份基于 Python 实现的中文医学文本实体关系抽取源码包解决的正是这个断层——它把数据、代码、说明文档打包在一起目标很明确让你在本地把一条中文医学句子里的实体识别出来再把实体之间的关系判出来。它适合三类人一是做课程设计、需要一份结构完整可复现工程的学生二是刚转医学 NLP、想找一个中文场景练手项目的工程师三是需要快速搭一个关系抽取 baseline、再往上叠模型的研究者。核心任务拆开看就两件事——命名实体识别NER负责找出高血压阿司匹林这类医学实体关系抽取RE负责判断它们之间是治疗导致还是禁忌关系。这份资源把这两步串成了一条流水线下面我按实际拆包和跑通的顺序把关键环节、参数和坑一个个讲清楚。2. 拆开压缩包先看什么目录结构、数据格式与标签体系拿到一个源码包最忌讳的就是上来就python train.py。中文医学文本的坑八成埋在数据格式和标签定义里先把这两块摸清楚后面能省掉大量调试时间。2.1 目录结构与各文件职责一个合格的医学实体关系抽取工程目录通常长这样不同版本命名略有差异但职责划分基本一致medical_re/ ├── data/ │ ├── raw/ # 原始标注文件通常是 json 或 brat 格式 │ ├── processed/ # 预处理后、可直接喂给模型的数据 │ └── label_schema.json # 实体类型 关系类型的标签定义 ├── src/ │ ├── preprocess.py # 数据清洗、切分、格式转换 │ ├── dataset.py # Dataset / DataLoader 封装 │ ├── model.py # NER RE 模型定义 │ ├── train.py # 训练入口 │ └── predict.py # 推理入口 ├── configs/ │ └── default.yaml # 超参数、路径、模型配置 ├── requirements.txt └── README.md # 项目说明先读label_schema.json它决定了整个任务的天花板。医学实体常见类型包括疾病Disease、症状Symptom、药物Drug、检查Exam、身体部位BodyPart关系类型常见的有治疗Treat、导致Cause、诊断Diagnose、禁忌Contraindication。如果标签体系和你报告里要写的对不上先改这里别改模型。2.2 数据格式从原始标注到模型输入中文医学标注数据最常见的两种来源一种是 json 行格式每行一句话带实体和关系列表另一种是 brat 的.ann.txt组合。预处理脚本要做的就是把它们统一成模型能吃的格式。下面是一段典型的预处理逻辑import json def convert_to_spans(sample): 把原始标注转成 (text, entities, relations) 三元组 text sample[text] entities [] for ent in sample.get(entities, []): # start/end 是字符级偏移中文按字符算不要按字节 entities.append({ id: ent[id], type: ent[type], start: ent[start], end: ent[end], mention: text[ent[start]:ent[end]] }) relations [] for rel in sample.get(relations, []): relations.append({ head: rel[head], # 头实体 id tail: rel[tail], # 尾实体 id type: rel[type] # 关系类型 }) return text, entities, relations with open(data/raw/train.json, r, encodingutf-8) as f: for line in f: sample json.loads(line) text, ents, rels convert_to_spans(sample) # 这里可以继续做长度过滤、实体对齐校验逻辑说明start/end是字符级偏移中文一个字算一个字符千万别用len(text.encode(utf-8))去算否则偏移全错。参数上head/tail存的是实体 id 而不是文本这样能避免同一句话里出现两个高血压时关系指代不清。转换完一定要做一次校验每个关系的 head/tail 是否都能在实体列表里找到找不到的直接丢弃并打日志这是最常见的脏数据来源。2.3 标签体系对齐别让 BIO 标注毁在空格上如果 NER 用 BIO 标注B-Disease、I-Disease、O预处理时最容易翻车的地方是实体边界和标点。中文医学文本里经常出现2型糖尿病非小细胞肺癌这种带数字和英文的实体分词器如果按空格切会把一个实体切成两半。常见做法是NER 阶段按字符级序列标注不依赖分词RE 阶段再把实体 mention 拼回去。这样能绕开大部分分词器对医学术语支持不好的问题。3. 模型怎么搭NER 与 RE 的联合流水线实现数据理顺之后才轮到模型。这份工程的价值在于它给了一条能跑的 baseline 流水线而不是一个刷榜模型。理解它的结构你才知道该往哪儿改。3.1 NER 分支BiLSTM-CRF 还是 BERT 微调源码里 NER 部分常见两种实现轻量的 BiLSTM-CRF和基于预训练模型的 BERT 线性分类头。课程设计场景下如果机器没有 GPUBiLSTM-CRF 更友好如果有 GPU 且想冲指标用中文预训练模型微调效果明显更好。核心训练循环大致如下import torch import torch.nn as nn class NERModel(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_tags): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.classifier nn.Linear(hidden_dim * 2, num_tags) def forward(self, input_ids, attention_maskNone): emb self.embedding(input_ids) out, _ self.lstm(emb) logits self.classifier(out) # [B, L, num_tags] return logits逻辑说明embedding把字符 id 映射成向量padding_idx0保证 padding 不参与梯度双向 LSTM 捕捉上下文最后线性层输出每个位置的标签分数。参数上hidden_dim一般取 128 或 256num_tags等于标签体系里 BIO 标签总数实体类型数 × 2 1。如果加了 CRF 层要在 logits 后接 CRF 做序列解码能显著减少非法标签转移比如 I-Disease 直接跟在 O 后面。3.2 RE 分支把实体对喂进分类器关系抽取的输入是句子 两个实体输出是关系类别。常见做法是把实体位置用特殊标记包起来再送进编码器def build_re_input(text, head_span, tail_span, tokenizer, max_len256): 在实体两侧插入标记构造关系分类输入 h_start, h_end head_span t_start, t_end tail_span # 用特殊符号标注实体边界避免模型靠位置硬记 marked (text[:h_start] [E1] text[h_start:h_end] [/E1] text[h_end:t_start] [E2] text[t_start:t_end] [/E2] text[t_end:]) enc tokenizer(marked, max_lengthmax_len, truncationTrue, paddingmax_length, return_tensorspt) return enc逻辑说明[E1]/[E2]是实体标记让模型知道哪段是头实体、哪段是尾实体而不是靠固定位置去猜。参数上max_len取 256 通常够用医学句子很少超过这个长度truncationTrue防止超长句子报错。注意头尾实体顺序要固定比如统一 head 在前否则模型学到的模式会乱。3.3 训练配置与关键超参数配置集中在configs/default.yaml几个必须关注的参数参数常见取值说明batch_size16 / 32显存不够就往下调别硬撑learning_rate1e-3LSTM/ 2e-5BERT预训练模型学习率要小一个量级max_seq_len128 / 256按数据实际长度分布定别盲目拉满num_epochs20 / 5LSTM 收敛慢BERT 微调通常 3-5 轮dropout0.3 / 0.1小数据集上 dropout 调大能压过拟合训练入口一般就是python src/train.py --config configs/default.yaml。跑之前先确认data/processed/下已经有预处理产物否则脚本会直接报文件不存在。4. 避坑与排查中文医学数据上最容易翻车的五件事这一章是我自己踩过、也在别人工程里反复见到的坑按现象 → 原因 → 解决列出来照着排查能省掉大半天。4.1 实体偏移错位mention 取出来是乱码现象预处理后打印实体 mention发现取出来的是半个词或者带上了标点。原因原始标注的偏移是按字节算的而 Python 字符串切片按字符算中文一个字符占 3 个字节偏移直接对不上。解决统一在读取时把偏移当字符处理如果原始数据是字节偏移先用text.encode(utf-8)切片再 decode 回来或者干脆重新按字符标注一遍。4.2 关系样本极度不平衡模型全预测成无关系现象训练 loss 降得很快但验证集上关系 F1 接近 0混淆矩阵里几乎全是无关系。原因医学文本里大部分实体对之间没有关系负样本占比可能超过 90%。解决对负样本做下采样或者用 focal loss 给难样本加权评估时别只看 accuracy要看关系类别的 macro-F1。4.3 分词器和医学术语打架现象BERT 分词后非小细胞肺癌被切成非小细胞肺癌多个 token实体边界信息丢失。原因通用中文分词器没见过多少医学术语。解决NER 走字符级标注绕开分词RE 阶段在实体两侧加特殊标记让模型显式知道边界而不是依赖分词结果。4.4 显存溢出batch 调小还是 OOM现象把 batch_size 从 32 调到 8 还是爆显存。原因序列长度没截断个别超长句子把显存吃满。解决在 Dataset 里做长度过滤或截断max_seq_len设成数据 95 分位长度同时确认没有在计算图上累积历史loss.backward()后记得optimizer.zero_grad()。4.5 评估指标对不上复现不出 README 里的数字现象按 README 跑完F1 比说明里低十几个点。原因随机种子没固定、数据划分不一致或者评估脚本用的是实体级还是关系级口径不同。解决在配置里固定seed确认 train/dev/test 划分和说明一致评估时明确是严格匹配实体边界和类型都对还是宽松匹配报告里写清楚口径。提示每次改完预处理先在小样本比如 100 条上跑一遍全流程确认实体和关系都能正确解析再上全量数据训练。5. 从跑通到用起来推理、验证与一个提效技巧把模型训出来只是第一步真正让它能用的是推理和验证环节。这一章讲怎么把训练好的模型接到新句子上以及一个我常用的快速验证技巧。5.1 推理脚本怎么写推理入口predict.py的核心是把一句话走完 NER RE 两步def predict_sentence(text, ner_model, re_model, tokenizer, id2rel): # 第一步NER 抽实体 entities ner_model.predict(text) # 返回 [(type, start, end), ...] # 第二步枚举实体对逐对判关系 results [] for i in range(len(entities)): for j in range(len(entities)): if i j: continue head, tail entities[i], entities[j] enc build_re_input(text, (head[1], head[2]), (tail[1], tail[2]), tokenizer) with torch.no_grad(): logits re_model(**enc).logits rel_id logits.argmax(-1).item() if rel_id ! 0: # 0 表示无关系 results.append((head, tail, id2rel[rel_id])) return results逻辑说明先 NER 拿到所有实体再对实体对两两组合判关系rel_id ! 0过滤掉无关系对。参数上id2rel是关系 id 到名称的映射从label_schema.json读进来。注意实体对枚举是 O(n²)实体多的长句会慢实际用的时候可以加个距离阈值超过一定字符距离的实体对直接跳过。5.2 验证方法别只看总体 F1评估关系抽取建议分类型看指标。下面这张表是我习惯的验证维度验证维度看什么异常信号实体级 F1NER 抽得准不准某类实体 F1 明显低查该类样本量关系级 macro-F1各类关系平均表现少数类被多数类淹没混淆矩阵哪些关系互相混淆治疗和缓解经常混错误样本抽查人工看 20 条错例发现系统性偏移或标注错误抽查错误样本这一步最容易被跳过但往往最有价值。我见过好几次模型指标还行、但错例全是同一类实体边界问题改完预处理指标直接涨一截。5.3 一个提效技巧用规则先兜底再上模型医学文本里有一部分关系是高度模式化的比如X 用于治疗 YX 可导致 Y。我的习惯是先用几条正则规则做高置信度抽取把明显的关系直接输出剩下的再交给模型。这样既能提召回又能减少模型在简单样本上的压力。规则不用多覆盖三五种高频句式就够剩下的交给模型去学。从那以后我每次拿到一个新的医学关系抽取工程都会先跑一遍小样本全流程、再抽查错误样本确认数据管线没问题才敢开训。这份源码包的价值不在于模型多强而在于它把数据、代码、说明凑齐了让你能把精力花在真正要改的地方。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站