简介:本资源是一套基于Python的实体关系抽取系统源码,面向从事自然语言处理、知识图谱构建的开发者与研究人员,尤其适用于高端装备制造领域的知识图谱自动化构建场景。系统以BERT等预训练模型为核心,实现从文本中提取结构化实体与关系信息,并引入FGM对抗性训练与EMA参数平滑更新策略,提升模型鲁棒性与稳定性。压缩包共22个文件,约1.97MB,包含8个Python脚本、12个JSON数据与配置、1个txt词表及1个md说明文档,覆盖数据加载、数据集生成、模型定义、训练入口与预测等完整流程,目录结构清晰,便于按模块阅读与二次开发。目前已有208人学习下载,适合具备一定深度学习基础、希望快速上手实体关系抽取实战的读者参考借鉴。
1. 从一份 Python 实体关系抽取系统源码说起:它到底能抽出什么
很多人第一次拿到「基于 Python 的实体关系抽取系统」这类源码包,第一反应是解压、找requirements.txt、pip install、python main.py,然后发现跑不起来,或者跑起来只输出一堆看不懂的 JSON。问题不在代码,而在于没搞清楚这套系统到底在做什么。实体关系抽取(Relation Extraction,RE)解决的是:给定一段文本,先找出里面的实体(人名、地名、机构、时间、数值等),再判断两个实体之间是什么关系,比如「张伟任职于某某科技」里,「张伟」和「某某科技」之间是「任职于」关系。它和命名实体识别(NER)是上下游关系,NER 只标实体,RE 在实体基础上加关系标签。
这套源码通常包含数据预处理、模型定义、训练、推理、评估几个模块,适合三类人:想入门 NLP 信息抽取的学生、需要从合同/新闻/工单里批量抽结构化字段的工程师、以及想拿现成代码改造成自己业务抽取器的人。它不解决「通用对话」,也不解决「情感分析」,只干一件事——把非结构化文本变成(头实体, 关系, 尾实体)三元组。下面按「先立住原理、再动手复现、最后讲坑」的顺序拆开讲。
2. 实体关系抽取系统的技术选型:为什么是 Python + 这套架构
2.1 关系抽取的两条主流路线:Pipeline 与 Joint
实体关系抽取在工程上分两大流派。Pipeline(流水线)做法是先跑 NER 模型抽实体,再把实体两两配对送进关系分类模型,判断每对实体属于哪种关系。优点是模块解耦,NER 和 RE 可以分别换模型、分别调优,调试时能清楚知道是实体抽错了还是关系判错了。缺点是误差累积,NER 漏掉的实体,RE 永远补不回来,而且实体对数量是实体数的平方级,长文本里配对爆炸。
Joint(联合)做法是一个模型同时输出实体和关系,共享编码器,常见结构是「标注序列 + 关系矩阵」或者「Span 分类」。优点是避免误差传播,推理一次出结果。缺点是训练数据标注要求高,调参更玄学,改一个损失权重可能整个模型崩掉。
这套源码大概率走的是 Pipeline 路线,因为它的模块划分清晰、依赖少、容易在单机上跑通。判断方法很简单:看代码里有没有独立的ner和relation两个模型文件,或者pipeline.py里是不是先调 NER 再调 RE。如果是,那就是 Pipeline。
2.2 编码器选型:BERT 还是 BiLSTM+CRF
关系抽取的编码器决定上限。常见组合有三种:
| 方案 | 编码器 | 适用场景 | 显存需求 | 标注量要求 |
|---|---|---|---|---|
| 经典方案 | BiLSTM + CRF | 小数据、快速验证 | 低(2G 可跑) | 几千条 |
| 主流方案 | BERT + 分类头 | 通用领域、效果优先 | 中(6G 起) | 上万条 |
| 轻量方案 | TextCNN / FastText | 短文本、高吞吐 | 极低 | 几千条 |
源码里如果出现transformers、BertModel、AutoTokenizer,说明走的是 BERT 路线;如果只有torch.nn.LSTM和CRF,那是经典方案。BERT 路线的关键参数是max_length(一般 128 或 256)、batch_size(8 或 16)、learning_rate(2e-5 到 5e-5)。BiLSTM 路线的关键是hidden_dim(128 或 256)、dropout(0.3 到 0.5)、lr(1e-3)。
选型理由很直接:如果你的业务文本是新闻、合同、报告这类规范书面语,且标注数据能到上万条,直接上 BERT;如果是工单、聊天记录这类短文本且标注少,BiLSTM+CRF 反而更稳,不容易过拟合。我一般会先用 BiLSTM 跑通全流程,确认数据格式和评估指标没问题,再换 BERT 冲效果。
2.3 数据格式:从原始标注到模型输入
关系抽取的数据格式是第一个卡点。常见标注格式有两种:一种是 JSONL,每行一个样本,包含text、entities(带起止位置和类型)、relations(头尾实体索引和关系类型);另一种是 BIO 序列标注 + 关系表。源码里通常有个data/目录,里面是train.json、dev.json、test.json。
一个典型的 JSONL 样本长这样:
{"text": "张伟于2019年加入北京某某科技有限公司", "entities": [{"id": 0, "start": 0, "end": 2, "type": "PER", "text": "张伟"}, {"id": 1, "start": 3, "end": 7, "type": "TIME", "text": "2019年"}, {"id": 2, "start": 9, "end": 17, "type": "ORG", "text": "北京某某科技有限公司"}], "relations": [{"head": 0, "tail": 2, "type": "任职于"}, {"head": 0, "tail": 1, "type": "加入时间"}]}注意start和end是字符级索引,end通常是不包含的(左闭右开)。很多源码在这里翻车,因为标注工具导出的索引有的是闭区间,有的是 token 级,直接喂给模型会错位。处理办法是写一个校验脚本,遍历所有样本,检查text[start:end]是否等于entity["text"],不等就报错。
import json def validate_span(file_path): with open(file_path, 'r', encoding='utf-8') as f: for line_no, line in enumerate(f, 1): sample = json.loads(line) text = sample['text'] for ent in sample['entities']: span = text[ent['start']:ent['end']] if span != ent['text']: print(f"第{line_no}行实体错位: 期望'{ent['text']}', 实际'{span}'") return False return True # 参数说明:file_path 指向 train.json / dev.json # 逻辑:逐行解析,用字符切片还原实体文本,和标注文本比对 # 失败时优先检查标注工具是否用了 token 索引或闭区间这个校验脚本我每次拿到新数据都会先跑一遍,能省掉后面几小时的「模型不收敛」排查。
3. 把源码跑起来:环境、训练、推理的最小闭环
3.1 环境搭建与依赖安装的四个关键点
拿到源码后不要急着pip install -r requirements.txt,先看三样东西:Python 版本、PyTorch 版本、有没有transformers。Python 建议 3.8 或 3.9,太新的 3.12 有些老包编译不过。PyTorch 版本要和 CUDA 匹配,如果源码里写的是torch==1.8.0,而你机器是 40 系显卡,可能得升到 2.x,但升完要注意torch.nn.CRF这类第三方包的兼容性。
安装顺序建议:
# 1. 建虚拟环境,避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 2. 先装 PyTorch,按官网命令选对应 CUDA 版本 pip install torch==2.0.1 torchvision --index-url https://download.pytorch.org/whl/cu118 # 3. 再装其余依赖 pip install -r requirements.txt # 4. 单独确认关键包版本 python -c "import torch, transformers; print(torch.__version__, transformers.__version__)"参数说明:cu118对应 CUDA 11.8,如果你只有 CPU,把 index-url 换成 CPU 版本。requirements.txt里如果有pytorch-crf、seqeval、fastapi,说明源码带评估和服务接口。装完先跑python -c "import torch; print(torch.cuda.is_available())",返回 False 就是没吃到 GPU,后面训练会慢十倍。
3.2 训练脚本的参数怎么改:batch_size、lr、max_length
训练入口一般是train.py或run_train.py。打开先找argparse部分,重点看这几个参数:
parser.add_argument('--batch_size', type=int, default=16) parser.add_argument('--learning_rate', type=float, default=3e-5) parser.add_argument('--max_length', type=int, default=128) parser.add_argument('--epochs', type=int, default=10) parser.add_argument('--data_dir', type=str, default='./data') parser.add_argument('--save_dir', type=str, default='./checkpoints')改法有讲究。batch_size先看显存,8G 显存跑 BERT-base 一般能到 16,跑不动就降到 8,同时把learning_rate从 3e-5 降到 2e-5,因为小 batch 梯度噪声大,学习率太高会震荡。max_length要覆盖你数据里 95% 的文本长度,短了截断丢关系,长了浪费显存。统计方法:
import json lengths = [] with open('data/train.json', 'r', encoding='utf-8') as f: for line in f: lengths.append(len(json.loads(line)['text'])) lengths.sort() print(f"95分位长度: {lengths[int(len(lengths)*0.95)]}") # 参数说明:输出值向上取整到 8 的倍数,作为 max_length # 逻辑:避免拍脑袋设 512 导致显存爆掉epochs不要设太大,BERT 微调一般 3 到 5 轮就收敛,10 轮大概率过拟合。判断过拟合看验证集 F1,如果训练 loss 一直降但验证 F1 掉头向下,就是过拟合,提前停。
3.3 推理与结果解析:从模型输出到三元组
推理脚本一般是predict.py或inference.py,输入一段文本,输出三元组。核心逻辑是:先 NER 抽实体,再对实体对做关系分类,最后过滤掉「无关系」的配对。
def extract_triples(text, ner_model, re_model, tokenizer, id2rel, threshold=0.5): # 1. NER 抽实体 entities = ner_model.predict(text) triples = [] # 2. 实体两两配对 for i in range(len(entities)): for j in range(len(entities)): if i == j: continue head, tail = entities[i], entities[j] # 3. 构造关系分类输入 inputs = tokenizer(text, head['text'], tail['text'], return_tensors='pt', truncation=True, max_length=256) with torch.no_grad(): logits = re_model(**inputs).logits prob = torch.softmax(logits, dim=-1) rel_id = torch.argmax(prob, dim=-1).item() # 4. 过滤无关系和低置信 if id2rel[rel_id] != '无关系' and prob[0][rel_id] > threshold: triples.append((head['text'], id2rel[rel_id], tail['text'])) return triples # 参数说明:threshold 控制召回与精度平衡,0.5 是起点 # 逻辑:先抽实体再配对,避免漏掉嵌套实体 # 失败时看 NER 是否漏实体,或关系分类是否把「无关系」判成具体关系threshold这个参数很关键。调低到 0.3,召回高但误报多;调到 0.7,精度高但漏抽。业务上如果下游有人工复核,可以调低;如果直接入库,调高。我一般会在验证集上画一条 P-R 曲线,选 F1 最高的点作为阈值。
4. 避坑与排查:实体关系抽取系统最常见的 5 个翻车点
4.1 实体索引错位导致模型学不到关系
现象:训练 loss 正常下降,但验证集 F1 一直在 0.1 左右,模型好像只学会了预测「无关系」。原因:标注数据里实体start/end是 token 级索引,而代码按字符级切片,导致实体文本全是乱码或空串。解决:跑 2.3 节的校验脚本,把所有错位样本找出来,用text.find(entity_text)重新计算字符索引,或者统一改成 token 级处理。这个坑我踩过两次,血泪经验是:拿到数据先校验,别信标注工具。
4.2 max_length 截断把关键关系截没了
现象:短文本抽得准,长文本(超过 200 字)关系全丢。原因:max_length设成 128,长文本后半段的实体和关系被截断,模型根本看不到。解决:统计训练集长度分布,把max_length设到 95 分位以上;如果显存不够,用滑动窗口切分文本,每段保留重叠区域,推理时合并结果。注意重叠区要够大,至少覆盖一个完整句子,否则关系会被切断。
4.3 关系类别不平衡导致「无关系」一家独大
现象:模型把所有实体对都预测成「无关系」,准确率看着很高(因为负样本占 90%),但召回率接近 0。原因:实体对里绝大多数是无关的,正负样本比例可能到 1:20。解决:训练时对正样本加权,或者用 focal loss;也可以在构造负样本时做下采样,让正负比控制在 1:3 左右。评估时不要只看 accuracy,要看正类别的 F1。
4.4 中文分词和 BERT tokenizer 不一致
现象:NER 抽出的实体边界和预期差一两个字,比如「北京某某科技」抽成「京某某科技」。原因:BERT 中文 tokenizer 是按字切分,但后处理时用了 jieba 分词的结果去对齐,两套边界对不上。解决:统一用 tokenizer 的offset_mapping还原字符位置,不要混用分词工具。如果必须用 jieba,就在 jieba 分词结果上重新训练 NER,保持训练和推理一致。
4.5 保存的模型加载时报 key 不匹配
现象:训练完保存state_dict,推理时load_state_dict报Missing key(s)或Unexpected key(s)。原因:训练时用了DataParallel或多卡,保存的 key 带module.前缀,单卡加载对不上。解决:保存时用model.module.state_dict(),或者加载时用{k.replace('module.', ''): v for k, v in state_dict.items()}去掉前缀。这个坑在多卡训练转单卡推理时必现,记住就行。
5. 进阶技巧:用规则兜底和主动学习把 F1 再拉 5 个点
模型跑通只是起点,真正上线还要解决「长尾关系抽不准」和「标注数据不够」两个问题。我一般会加两层:规则兜底和主动学习。
规则兜底针对高频、模式固定的关系。比如「任职于」「成立于」「位于」这类,用正则或关键词模板直接抽,置信度给满,模型只负责抽规则覆盖不到的。做法是写一个rule_extractor.py,和模型输出做融合:
import re RULES = [ (re.compile(r'(.{2,4})于(\d{4})年加入(.{2,20})'), ('任职于', '加入时间')), (re.compile(r'(.{2,20})成立于(\d{4})年'), ('成立时间',)), ] def rule_extract(text): triples = [] for pattern, rel_types in RULES: for match in pattern.finditer(text): groups = match.groups() if len(groups) >= 2: triples.append((groups[0], rel_types[0], groups[-1])) return triples # 参数说明:RULES 里每条规则对应一种关系模式 # 逻辑:正则命中直接产出三元组,不经过模型 # 注意:规则要定期 review,避免过拟合到某批数据融合策略是:规则命中的三元组直接采纳,模型输出的三元组如果和规则冲突,以规则为准;规则没覆盖的,用模型结果。这样在固定模式的关系上,F1 能直接拉到 0.95 以上。
主动学习解决标注不够。流程是:先用现有模型跑一遍未标注数据,挑出模型置信度在 0.4 到 0.6 之间的样本(模型最不确定的),人工标注这批,再混进训练集重训。每轮标注量不用大,500 到 1000 条就够,跑三轮通常能把 F1 从 0.7 拉到 0.8 以上。关键是选样策略,不要随机选,要选「模型分歧大」的,信息量最高。
验证方法上,除了看整体 F1,一定要分关系类型看。我习惯用seqeval或sklearn的classification_report输出每个关系类别的 P/R/F1,找出最差的两三类,针对性补数据或加规则。整体 F1 涨了但某个类别掉了,说明模型在牺牲长尾换头部,这种「假提升」上线后会翻车。
最后说个习惯:每次改完参数或数据,先跑一个小规模实验(比如 1000 条训练样本,2 个 epoch),确认流程通、指标动,再上全量。全量跑一次几小时,小实验几分钟,能省下大量等待时间。这套源码的价值不在于它本身多完美,而在于它给了你一个可改、可调、可验证的起点,剩下的就是顺着数据把每个环节抠细。希望帮到你。
本文还有配套的精品资源,点击获取