ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中文信息抽取实战:实体、关系与事件抽取全解析

中文信息抽取实战:实体、关系与事件抽取全解析 简介面向中文信息抽取学习与课程设计的Python项目合集覆盖实体抽取、关系抽取、事件抽取三大任务适合需要完成NLP类大作业、毕业设计或入门抽取技术的开发者。项目内置人民日报中文NER语料、DUIE关系数据及DUEE事件数据集提供基于bert4keras的BERT-CRF、GlobalPointer、Casrel等多种实现每个模块均包含训练、验证与预测流程只需调整do_train和do_predict参数即可运行。其中实体抽取在测试集上F1可达0.95左右关系抽取与事件抽取也提供了完整可复现代码便于横向对比不同模型效果。压缩包共16个文件以9个Python源码为主辅以JSON预测结果、占位文件和项目说明文档整体仅5.16MB轻量易部署。已有1152人学习使用项目说明中给出了数据集与预训练模型的下载地址可快速复现实验并在此基础上二次开发非常适合课程汇报、期末大作业与简历项目素材。1. 实体抽取、关系抽取、事件抽取这份zip到底替你省了什么如果你手里正好有一份基于Python的中文信息抽取工程里面同时包含源码、数据集、训练好的模型和项目说明那你大概率是踩过“BERT模型能跑但没数据”“有数据但模型训了三天也没收敛”这些坑之后才来找它的。实体抽取、关系抽取、事件抽取这三个任务合起来就是把一段中文新闻变成一张知识图谱先找出“阿里巴巴”“杭州”这些实体再判断它们之间的关系最后还原“发布新品”这件事的触发词和参与角色。这份工程适合毕业设计、知识图谱预研和文本结构化落地但对完全没有Python基础的人并不友好——你至少得会建虚拟环境、装依赖、跑脚本。2. 三个抽取任务各自怎么建模先看数据和标签再碰代码2.1 实体抽取BIO标注与CRF解码的常识中文实体抽取要解决的是“把句子里的实体片段找出来并分类”。比如“华为在东莞发布了新产品”期望输出是“华为(组织机构)、东莞(地点)、新产品(产品)”。几乎所有开源中文NER工程都把它建模成序列标注每个字一个标签标签体系最常见的是BIO另外还有BIOES和BMES区别只在是否把实体尾部和单字实体单列出来。# 一份标准的BIO标注样例字和标签用空格分隔 华 B-ORG 为 I-ORG 在 O 东 B-LOC 莞 I-LOC 发 O 布 O 新 B-PRD 产 I-PRD 品 I-PRD为什么要用字级而不是词级中文分词本身就可能错分词错一步实体边界就跟着错所以工程上宁可用字级输入。BERT类模型天然按字切分不存在分词环节这也是为什么实体抽取深度模型只要遇到中文场景几乎清一色是BERT编码加CRF解码。CRF层的作用不是提高特征提取能力而是给标签序列加约束比如B-ORG后面不能直接跟I-PER这些转移规则是靠训练从数据里学出来的。只用Softmax不建模这些约束就会频繁出现“实体尾巴对不上头”的翻车输出。数据侧的约定同样重要。标注原始文件可以存成上面的BIO文本也可以在JSON里用整数start与end标出实体边界而且工程上统一遵循左闭右开规则[start, end)表示“包含起始位置、不包含结束位置”。这个约定决定了解码时怎么从字位置列表切片end减start才是实体字数写代码时搞反了后面所有脚本都会对不上。拿到这份zip先别急着训练打开数据文件看三个信息实体类型集合、标签体系是BIO还是BIOES、实体边界是左闭右开还是左闭右闭。三样只要有一个和配置不一致训练出来的模型就是废的。2.2 关系抽取管道法省事但传错联合抽取一步到位关系抽取的任务是从给定文本和头尾实体里判断它们属于预定义关系集合中的哪一个。“马云创办了阿里巴巴”里实体对(马云, 阿里巴巴)的关系是“创始人”。这一步工程上有两种主流路线。管道法是最容易跑通的方案先跑NER拿到所有实体对再对每个实体对做关系分类。关系分类实质是一段文本分类任务把“头实体尾实体上下文片段”拼起来送进编码器输出关系类型。优点是实现简单、各模块可单独替换缺点是上游实体只要错一个下游关系分类就跟着错错误会逐层累加。更头疼的是负样本处理当句子里有n个实体候选组合是n的平方量级大部分组合之间根本没有关系硬训会压低召回。联合抽取是近年来更被推荐的做法。常见实现是CasRel这一系BERT给整句编码先识别出所有头实体再为每个预定义关系分别解码尾实体。它共享同一份BERT编码头实体识别的结果直接喂给尾实体解码错误传播被减弱。代价是训练时要为每个关系单独生成一组尾实体标签显存占用比管道法高代码也更绕。选型只需要记住一句话数据量小、只做演示验证用管道法数据量破万、想认真提效果直接上联合抽取。关系集合的标签设计决定了模型上限。同一个领域里关系类型是收窄成10个还是扩到50个训练难度完全不同。工程上这组关系通常存成一个relation2id.json训练和推理必须共用同一份文件推理时千万不要手动增删关系否则会出现后面避坑章要展开的标签ID错位问题。维度管道法先NER再关系分类联合抽取共享编码实现难度低中到高错误传播明显较弱负样本处理容易爆炸可控显存占用较低较高2.3 事件抽取触发词、事件类型与论元角色相比前两个任务事件抽取最接近“理解句子”。它要回答发生了什么事件、事件类型是什么、谁是参与者、时间地点在哪。工程上普遍拆成两个子任务先做触发词识别与事件类型分类再做论元角色抽取。例如“公司A在昨日完成了对B公司的收购”触发词是“收购”事件类型为“商业并购”三个论元分别是“收购方公司A、被收购方B公司、时间昨日”。开源工程对这一步的处理方式差异很大。有的把触发词识别建模成序列标注和NER几乎一样只是标签变成B-TRIG、I-TRIG加事件类型有的把它当成阅读理解问题构造“找出文本中的收购事件”这样的问句去抽答案。论元抽取则常见做法是给定触发词和候选实体判断每个候选实体扮演了什么角色本质是一个多分类问题。数据格式上最容易出问题的是论元重合。一个触发词可以带多个论元同一个实体也可能同时承担“收购方”和“被告”两种角色还可能一句话里嵌套两个事件。标注规则一旦含糊训练数据质量会直线下降模型学到的是标注员之间的分歧。项目说明里通常会给出事件类型集合和论元角色集合两份JSON这两份文件是整个事件抽取模块的起点必须最先核对。我拿到这类工程会先数一下事件类型有多少类、论元角色有多少种再把这个数字和模型配置里的类别数对一遍对不上就直接改配置而不是去改代码。3. 把源码跑通环境安装、目录结构与最小推理命令3.1 环境依赖先校准Python和torch版本拿到zip第一步不是读代码是把环境装对。项目说明里如果给了requirements.txt就按它来没给的话我一般按下面这套组合装# 用Python 3.8或3.9实测Python 3.11在部分旧版torch下会编译报错 python3.9 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txt # 如果requirements里没有torch按自己机器选一个装 # CPU版 pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cpu # 有N卡想用CUDA把url里的cpu换成cu121这段命令的要点是先把虚拟环境建出来避免把依赖装进系统Python。python官网下载安装包时安装界面里“Add Python to PATH”最容易漏漏掉以后命令行敲python会提示找不到命令这不是这份工程的问题但会卡住你半小时。torch版本方面CPU版足够跑推理和小规模训练训练大模型才需要GPU版。验证安装成功就在虚拟环境里敲python -c import torch, transformers; print(torch.__version__)能打出版本号再进行下一步。3.2 目录结构先认出哪些是模型、哪些是数据解压zip后先不要双击瞎点在终端里看一遍目录树. ├── README.md # 项目说明最该先读 ├── requirements.txt ├── config/ │ ├── ner.yaml │ ├── re.yaml │ └── ee.yaml ├── data/ │ ├── raw/ # 原始标注数据 │ └── processed/ # 预处理后的训练集/开发集 ├── models/ │ ├── ner_best.pth │ ├── re_best.pth │ └── ee_best.pth ├── src/ │ ├── ner/ # 实体抽取代码 │ ├── re/ # 关系抽取代码 │ └── ee/ # 事件抽取代码 └── scripts/ ├── train_ner.py ├── predict.py └── eval.py这份Python源码工程的布局比较典型。README是最权威的项目说明里面会写训练命令、推理命令、依赖版本先用十分钟把README通读一遍比盲猜省时间。models目录里带后缀.bth、.pt、.ckpt的文件是训练好的模型config里通常是对应模型的结构参数和标签映射。scripts下是可执行入口。我一般不会直接改src里的源码而是在外面包一层自己的调用脚本这样以后想更新依赖版本不用在源码里翻来翻去。提示如果发现models目录里只有模型文件没有配置JSON先别慌。模型权重里往往一起存了label2id尝试用模型自带的id2label输出看一遍标签集合比手动猜要可靠。3.3 加载训练好的模型做最小推理常见做法是模型权重文件加配置文件分开保存推理脚本按“加载预训练编码器→加载权重→构建解码器”的顺序走。下面是一段最小可用的实体抽取推理代码# scripts/predict.py import json from transformers import BertTokenizer from src.ner.model import load_ner_model model_path models/ner_best.pth config_path config/ner.json # 1. 初始化中文BERT分词器 tokenizer BertTokenizer.from_pretrained(bert-base-chinese) # 2. 加载训练好的模型device可以传cpu或cuda:0 model load_ner_model(model_path, config_path, devicecpu) text 阿里巴巴集团在杭州发布了新零售平台 # 3. 做预测max_seq_len要和训练时保持一致 result model.predict( text, tokenizertokenizer, max_seq_len128, label_threshold0.5, ) for ent in result[entities]: print(json.dumps(ent, ensure_asciiFalse))逻辑说明第1步拿到BERT分词器中文文本送入之前要先转成token id第2步的load_ner_model内部会读配置里的标签数量、网络结构再把这个工程的模型权重灌进网络第3步model.predict返回的是一个字典entities里是实体列表每条实体包含type、text、span。这里max_seq_len是硬约束中文按字切分128表示最多吃进128个字超长部分会在末尾截断所以长句的尾部实体会被砍掉这是截断策略的正常代价不是bug。如果你连GPU都没有推理用CPU也可以跑只是慢一些。跑之前先敲python scripts/predict.py对照README里的输出示例核对一条样例文本这一步通了说明这份zip至少在推理链路上是完整的。4. 自己训练与微调数据集准备、标签映射与超参设置4.1 把JSON标注转成模型要吃的输入训练之前要先搞懂这份工程吃什么格式的数据。早期项目喜欢直接吃BIO文本现代工程更多用JSON因为关系抽取和事件抽取很难用BIO表达。下面是一份常见的中文信息抽取JSON标注格式{ text: 刘强东创办了京东, entities: [ {type: PER, start: 0, end: 3, name: 刘强东}, {type: ORG, start: 5, end: 7, name: 京东} ], relations: [ {subject: 刘强东, object: 京东, relation: founder} ] }这里start和end是左右闭区间还是左闭右开必须在数据预处理里先确认。如果按左闭右开理解“刘强东”的start0、end3正好三个字如果按闭区间理解end应该写2。所以拿到别人的数据集第一件事是写个脚本验一遍实体字数def json_to_bio_text(text, entities, label2id): 把JSON标注转成BIO标签序列验证左闭右开规则 tags [O] * len(text) for ent in entities: start, end ent[start], ent[end] ent_type ent[type] if end len(text): raise ValueError(fend越界: {start}-{end}, 文本长度{len(text)}) if start end: raise ValueError(fstart必须小于end: {start}-{end}) tags[start] fB-{ent_type} for pos in range(start 1, end): tags[pos] fI-{ent_type} return list(text), tags text 刘强东创办了京东 entities [ {type: PER, start: 0, end: 3, name: 刘强东}, {type: ORG, start: 5, end: 7, name: 京东}, ] chars, tags json_to_bio_text(text, entities, label2idNone) print(list(zip(chars, tags))) # [(刘,B-PER), (强,I-PER), (东,I-PER), (创,O), # (办,O), (了,O), (京,B-ORG), (东,I-ORG)]参数说明这个函数检查end是否超过文本长度、start是否小于end然后从start到end把标签依次置为B和I。如果原始数据用的是闭区间跑一遍这个函数就会在“刘强东”位置报出start0,end3的越界或错位错误这就是数据格式不一致的信号。label2id在这里还没用上真正训练时要由实体类型集合生成映射比如PER→0、ORG→1再逐个替换。数据准备好之后通常要按8:1:1切出训练集、开发集、测试集。切分时要按文本ID切不要按行随机切否则同一篇文档的句子会同时出现在训练和测试里评估指标虚高。4.2 训练命令与关键超参从一份可复现的命令开始环境没问题、数据格式确认过就可以跑训练了。常见的训练入口是scripts/train_ner.py支持命令行参数覆盖配置文件python scripts/train_ner.py --config config/ner.yaml \ --train data/processed/train.json \ --dev data/processed/dev.json \ --model_dir checkpoints/ner/ \ --batch_size 16 \ --max_seq_len 128 \ --lr 3e-5 \ --epochs 5 \ --gpu 0几个参数值得展开说。lr设定为3e-5这是BERT类模型微调的常用量级如果下游任务数据很少可以降到2e-5数据量很大可以试5e-5但超过5e-5容易出现训练损失震荡。batch_size设为16是兼顾显存和收敛速度的常见做法显存不够就减半到8同时把梯度累积设成2等效batch不变。max_seq_len设128如果文本特别长可以调到256但训练时间会明显变长。epochs设5轮配合开发集上的早停patience通常设3避免过拟合。训练启动后一定要盯两个东西训练损失是否下降、开发集F1是否上升。如果损失下降但F1不动多半是数据标签映射错了如果损失直接不下降先把学习率调低一个数量级再试。4.3 评估按实体span算F1而不是按字算对几个评估脚本这段相对独立任何训练好的模型都能套用# scripts/eval.py import json from seqeval.metrics import classification_report with open(results/pred_entities.json, r, encodingutf-8) as f: preds json.load(f) # seqeval的输入是二维list每个样本是一条标签list y_true preds[true_labels] y_pred preds[pred_labels] print(classification_report(y_true, y_pred, digits4))逻辑说明seqeval这个库按“实体片段”计算精确率、召回率和F1一个实体只有类型和边界完全一致才算预测正确片段内部错一个字都算错。这正是信息抽取任务要的评估方式因为实体抽取的产出要直接喂给下游关系抽取边界错一个字符后面就全错位。打印出来的报告里除了总F1每一类实体会有单独的P/R/F1。如果某个低频实体类型F1明显偏低那不是调参能解决的是训练样本太少需要回去补数据。5. 避坑五个最常翻车的问题及排查方法5.1 现象加载训练好的模型后预测标签全乱所有实体都被识别成同一个类型或者B、I、O顺序错乱打印出来的实体文本边界歪到离谱。原因模型权重里保存的label2id顺序和当前代码里的顺序不一致。比如训练时标签列表是[O,B-PER,I-PER]推理时配置文件里写成了[O,I-PER,B-PER]同一个ID表示的类别就串了。解决打开模型的配置文件检查label2id是否和训练输出日志里的标签顺序完全一致。更保险的做法是不依赖配置文件直接加载权重时从权重里读出id2label。如果权重里没存就翻训练日志找当初打印的标签列表照着补一份。5.2 现象训练刚跑完第一个epoch就报CUDA out of memory显存直接爆掉进程被系统杀掉。原因常见两个一个是batch_size设得太大BERT base模型16G显存只够跑16左右另一个是max_seq_len设成512序列越长注意力矩阵占用的显存是平方级增长。解决先把batch_size降到8再把max_seq_len降到128两个都改完还爆就用混合精度训练。PyTorch里加一行fp16True或torch.cuda.amp.autocast()显存大约省一半。我一般遵循一个口诀显存不够先降长度再降批量最后才考虑换小模型。5.3 现象事件抽取里一个事件总是丢论元触发词识别对了但收购方、被收购方老丢其中一个或者两个论元被标成同一个角色。原因事件标注里普遍存在论元重合。一个实体可能既是A事件的收购方又是B事件里的交易对象而训练代码里可能用了简单去重把重复位置的标注覆盖了。解决在解码阶段不要把论元存成字典按角色覆盖要存成列表一个论元可以同时挂在多个角色下。检查项目说明里的标注样例确认同一个实体是否被允许出现在两个arguments里如果不允许那就是数据标注本身有分歧需要统一规则。5.4 现象关系抽取的预测结果几乎全是“无关系”精确率看着还行召回率低得没法用输出里大部分负样本。原因关系分类的负样本比例天然极高。一句普通文本里抽出的实体对有关系的可能是几十对里才一对模型学到的最优策略就是什么都预测为无关系。解决先看训练数据里的正负样本比例负样本占比超过95%是常态这时需要做下采样让正负比控制在1:1到1:3之间。训练时给正样本加权重或者在评估时放宽预测阈值从默认的0.5往下扫到0.3、0.2扫出一份P/R曲线再决定阈值。5.5 现象加载模型时报警missing keys或unexpected keys代码能跑起来但模型文件加载报出一大堆键名不匹配。原因模型结构文件和训练好的权重不是同一个版本。最常见的是源码里改过网络层名比如把lstm改成bilstm或者bert版本从base换成了large权重文件还是老版本。解决别急着改源码先看missing keys具体是哪些层。如果是最后几层分类器那多半是类别数对不上得对齐标签数量如果是编码器中间层基本可以判断是模型结构版本不一致这个zip里的源码和模型就不配套需要去找到和权重配套的那个源码版本或者放弃这个模型文件重新训练。6. 从跑通到能用验证方法、调参顺序与增量训练跑通这份工程只算万里长城第一步真正要落到业务里我建议按下面这个顺序做一遍验证和优化。先建立回归基线。拿官方训练好的模型读20条你没见过的测试文本把预测结果导出成JSON文件存好。这个文件就是你的基线后面改任何参数、换任何数据都要和这份基线做对比而不是只看总F1涨没涨。很多时候F1涨了0.5个点但某些高频实体类型的边界反而变乱了只看一个指标根本发现不了。再按顺序调参。第一步固定学习率在2e-5、3e-5、5e-5三档里各跑一遍开发集选F1最高的。第二步动max_seq_len如果长尾实体占比高就试着拉长到256否则保持128不变。第三步加正则weight_decay从0.01调到0.1配合早停。每一个调整都只动一个变量一次改两个参数等于没做对照实验。最后做领域增量训练。如果这份工程的数据集是通用新闻语料而你要处理的是法律合同或医疗病历模型在这里会明显水土不服。增量训练的做法是用官方训练好的模型权重作为初始化用你自己领域的几千条标注数据接着训练学习率压低到1e-5训练轮数减到2到3轮。这样做的好处是保留通用中文语义同时把领域特征学进来。我自己带团队时立过一条规矩每一轮实验都要求把预测结果落盘存档文件名里带上参数和日期。因为NLP实验里最不缺的就是“上次那个效果不错的参数”缺的是能把它找回来的后悔药AI训练有时候就是玄学不如把过程管成工程。希望这份笔记能帮你在中文信息抽取这条路上少翻几次车尽快把模型从实验台搬到能用的地方去。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进