资讯动态

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

发布时间:2026/10/9 1:53:54 来源:尧图企业网站定制
简介基于Python的中文信息抽取完整项目包涵盖实体抽取、关系抽取与事件抽取三大任务适合高校学生在课程设计、期末大作业或毕业设计中快速上手。压缩包共16个文件包含9个Python源码、3个JSON数据文件及项目说明文档等大小仅5.16MB其中ner目录提供bert-crf与globalpointer两种实体抽取实现re目录基于duie数据集完成关系抽取ee目录对应事件抽取模块各代码均支持训练、验证与预测切换。随包附带百度网盘链接可获取数据集、预训练模型及训练好的模型权重方便直接复现实验效果。项目说明文档对依赖环境、参数配置和运行方式均有交代降低了二次开发门槛。该资源已有1152人学习浏览特别适合具备一定Python与深度学习基础、希望参考完整项目流程完成NLP课程任务的学生或开发者。1. 拿到这个 zip先搞清楚它到底在解决什么问题收到一个标题特别长的基于Python的中文信息实体抽取、关系抽取、事件抽取源码数据集训练好的模型项目说明.zip第一反应别是马上解压跑训练而是要把它拆成三件事。实体抽取负责把文本里的“人、组织、地点、时间、金额”这些命名实体圈出来关系抽取把实体对之间的事实关系转成结构化三元组比如“王小华任职于某公司”事件抽取更进一步要识别触发词和论元角色回答“谁在什么时间对谁做了什么”。这套东西适合做舆情监控、合同比对、医学文献挖掘的人。你手头如果有明确需求但不想从 tokenizer 和标注入门这个包能让你直接站在 BERT 和标注数据的肩膀上。不过源码能用和用好是两回事下面按三条任务线拆开讲最后给你避坑和上线的实操路径。2. 实体抽取把命名实体先捞干净后面的关系才有得抽2.1 实体抽取的标注格式BIO 和 IO 的边界不能糊实体抽取最常见的训练目标是对每个 token 打一个 BIO 标签。以“王小华任职于云服务器公司”为例标注就是王/B-PER、小/I-PER、华/I-PER、任/O、职/O、于/O、云/B-ORG、服/I-ORG、务/I-ORG、器/I-ORG、公/I-ORG、司/I-ORG。这里的 PER 是人名ORG 是机构名O 表示不是实体的一部分。源码包里的数据集一般会提供train.txt / dev.txt / test.txt每行一个“字符 标签”对或者用 JSON Lines 存text和entities列表。你拿到后第一件事不是看模型而是看label2id.json或项目说明里的标签表。如果出现B-PER后面跟着I-ORG说明标注规范写得松训练时 loss 会一直震荡。我一般建议保持 BIO 而不是升级成 BIOES。BIOES 多了单个实体标记 S边界表达更精确但状态多、代码容易写错。对常规四到六类实体BIO 完全够用。真正要小心的是 O 标签比例太高通常占 85% 以上训练时如果直接用 CrossEntropy模型会偷懒倾向预测 O。这种不平衡会在后面关系抽取时放大所以源码里如果看到loss_weight参数不要嫌麻烦先填上。2.2 实体抽取的模型选型从现成工具到 BERT-CRF实体抽取模型可以分三档选择。第一档是直接用 LTP、HanLP 这类预训练工具包零成本、开箱即用但它们的实体类别是固定的对“合同编号”“案由”这种垂直领域新实体基本无能为力。第二档是拿bert-base-chinese做微调后面接一个线性分类器这是中文实体抽取的性价比之王。第三档是在第二档基础上加 CRF 层或用分词词典做外部特征专门处理资金、日期、产品型号这种边界不稳定的实体。如果你的 GPU 显存只有 8G 左右别盲目上 12 层大模型bert-base-chinese加一个 CRF 头通常比直接上roberta-large更稳。对千级样本的标注数据训练 3 到 5 个 epoch 一般就够再多就会把训练集背下来验证集 F1 反而往下掉。很多所谓“项目说明”里吹的 98% 准确率多半是在同一个领域测试集上测的换一批文本分分钟打回原形。2.3 可以抄的实体抽取预测代码加载训练好的模型输出 BIO 标签假设 zip 里的source_model/bert_ner是一个已经保存好的 BERT 实体模型下面这段代码直接可以跑预测# 实体抽取预测加载已训练好的模型 from transformers import AutoTokenizer, AutoModelForTokenClassification model_path source_model/bert_ner label_list [O, B-PER, I-PER, B-ORG, I-ORG, B-LOC, I-LOC] tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForTokenClassification.from_pretrained(model_path) text 王小华任职于云服务器公司 inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128) logits model(**inputs).logits pred_ids logits.argmax(-1)[0].tolist() input_ids inputs[input_ids][0] tokens tokenizer.convert_ids_to_tokens(input_ids) for token, label_id in zip(tokens, pred_ids): token token.replace(#, ) if token in [[CLS], [SEP], [PAD]] or label_list[label_id] O: continue print(token, label_list[label_id])这段代码的逻辑是先用 tokenizer 把句子切成 token然后通过模型 logits 的最后一维求 argmax得到每个 token 的标签 id。注意label_list的顺序必须和模型训练时保持一致顺序乱了所有预测都会错位。中文 BERT 默认按字切分所以“王小华”三个字会各拿一个标签。参数说明max_length128对短文本没问题但合同、新闻原文经常超过 512 字这时候不能简单截断否则关系抽取里“头实体在句首、尾实体在句尾”的结构会被切断。另外如果你的模型是从 BiLSTM-CRF 代码训练出来的最后不能直接argmax要找源码里的viterbi_decode函数让 CRF 约束显式生效。3. 关系抽取实体对有了关系分类是另一码事3.1 关系抽取的目标不是猜关系而是给定实体对做分类实体抽取做完文本里能圈出一堆“人名、公司、地点”但这还不够。关系抽取要回答的是“这两个实体之间到底存不存在某种关系、存在哪种关系”。常见做法是把关系抽取建模成分类任务输入原句、头实体、尾实体输出关系类别比如“任职于”“位于”“收购”“治疗”。这里最容易犯的错是忘了关系抽取和实体抽取的错误是串联的。实体边界错一个字符后面关系分类的输入就带了脏数据。我在实际项目里见过最多的翻车场景是 BERT 实体抽取 F1 已经到 92%关系抽取却只有 64%查了半天最后发现是 NER 把“云服务公司”少圈了“云”字导致“公司”和“云”被分成两个实体。所以选型的时候要先问自己你手里是已经有一批实体标注了还是只有原始文本如果只有原始文本老老实实走管道法先 NER 再关系分类等发现误差传导太严重再考虑联合模型搬上来。3.2 常用关系数据集形式和三元组结构中文开源关系数据集常见的是以三元组为单位组织比如 DuIE 这类百科抽取任务每行 JSON 长这样{ text: 王小华任职于云服务器公司, spo_list: [ { subject: 王小华, predicate: 任职于, object: 云服务器公司 } ] }这个格式和实际落地很接近模型学习的目标就是“给定原句和实体 span预测 predicate”。在你自己的项目里关系类别要先人工定一个白名单不是越多越好。关系类别从 5 类扩到 20 类数据量至少得翻一倍否则很多长尾关系根本学不动。数据预处理时我习惯把负样本单独构造出来取文本里随机两个实体如果它们之间没有定义的关系就打上“无关”标签。负样本比例控制在 1:2 到 1:3太少模型分不清太多模型会把所有关系都判成无关。3.3 一个能用的关系分类代码骨架BERT 加实体对拼接下面给一个最简单、不依赖额外位置编码的关系分类推理代码# 关系抽取把原句和实体对拼接成 BERT 输入 from transformers import AutoTokenizer, AutoModelForSequenceClassification model_path source_model/bert_relation rel_label [无关, 任职于, 位于, 收购, 成立于] rel2id {label: i for i, label in enumerate(rel_label)} tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained(model_path) def predict_relation(text, subj, obj, threshold0.6): inputs tokenizer( text, text_pairf{subj} {obj}, truncationTrue, max_length128, return_tensorspt ) logits model(**inputs).logits scores logits.softmax(-1)[0] pred_id scores.argmax().item() conf scores[pred_id].item() if rel_label[pred_id] 无关 or conf threshold: return None return {subject: subj, predicate: rel_label[pred_id], object: obj, confidence: conf}这里的text_pair会把实体对拼到句子后让模型同时看到原句和候选实体文本不依赖位置标记实现成本最低。逻辑上先取预测类别再用阈值过滤低置信度结果。阈值建议在验证集上做一次网格搜索不要拍脑袋定 0.5。我曾在一个金融公告项目里把阈值从 0.5 抬到 0.8垃圾三元组明显变少召回掉了一截最后还是按 P/R 曲线的平衡点定的。如果 zip 里的源码提供的是 CasRel 这类联合模型代码结构会变成“先抽头实体再为每个头实体抽尾实体和关系”输入格式不再是一个句子对而是整句加上实体类型嵌入。这类模型适合关系多且实体高度重叠的场景但训练调参复杂度高不建议第一天就上手。4. 事件抽取触发词和论元角色别拿实体抽取的思路硬套4.1 事件抽取任务的拆分触发词识别和论元角色标注事件抽取比关系抽取又高一阶。以“中国计算机大会将于 8 月在北京举办”为例这里的事件触发词是“举办”事件类型是“会议/举办”论元角色包括时间“8月”、地点“北京”、主办方“中国计算机大会”。模型要同时学会两个子任务一是找到触发词并判断事件类型二是给触发词周围的实体分配论元角色。这和你前面跑实体抽取完全是两套标签体系。实体抽取只关心“什么是命名实体”事件抽取要关心“这个实体在这个事件里扮演什么角色”。同一个实体“北京”在“北京位于华北”里只是一个地点实体在“公司在北京举办活动”里就成了“地点”论元单独做实体抽取根本不够。4.2 事件标注格式触发词和论元分开标源码包里如果有事件数据通常不会只给一段 BIO 序列。更常见的是按事件类型分组每条事件记录包含以下字段{ text: 中国计算机大会将于8月在北京举办, events: [ { trigger: 举办, trigger_offset: [12, 13], event_type: 会议/举办, arguments: [ {role: 时间, entity: 8月, offset: [8, 8]}, {role: 地点, entity: 北京, offset: [12, 13]}, {role: 主办方, entity: 中国计算机大会, offset: [0, 5]} ] } ] }这里的 offset 是字符级索引必须和原文字符对齐。用 Python 处理中文时别直接用len()判断偏移因为全角空格、中文标点、emoji 都会让字节偏移出问题。我一般会在预处理阶段先转成字符数组再记录每个字的位置。标注时注意一个原则事件类型决定论元角色的候选集合。“开庭”事件有原告、被告、法院、时间“收购”事件有收购方、标的、金额。如果你把所有事件类型塞进一个大角色集合模型会出现严重混淆训练和推理都慢。4.3 一个最小的事件抽取模型结构共享 BERT 编码器两个输出头事件抽取不太建议用“实体抽取 关系抽取”两条管道硬拼因为触发词和论元在句子里往往离得不远直接共享编码器能少算很多特征。下面是一个常见的最小模型结构# 事件抽取模型共享 BERT触发词和论元各接一个线性层 from torch import nn from transformers import BertModel class EventExtractionModel(nn.Module): def __init__(self, bert_path, num_trigger_types, num_argument_roles): super().__init__() self.bert BertModel.from_pretrained(bert_path) self.trigger_head nn.Linear(768, num_trigger_types) self.argument_head nn.Linear(768, num_argument_roles) def forward(self, input_ids, attention_mask): sequence_output self.bert(input_ids, attention_maskattention_mask).last_hidden_state trigger_logits self.trigger_head(sequence_output) # 每个token是不是触发词 argument_logits self.argument_head(sequence_output) # 每个token属于哪个论元角色 return trigger_logits, argument_logits这里的trigger_head在全序列的每个 token 上做多分类例如判断“举办”“宣布”是不是触发词argument_head在每个 token 上预测论元角色例如“北京”是不是 TIME 或 LOC。两个输出共享 BERT 编码器训练时可以两个 loss 相加。但这里有个隐藏坑一个句子可能同时存在多个事件。比如“张三宣布起诉李四”“宣布”和“起诉”都可能触发不同事件如果单一argument_head被设计成每个 token 只能有一个标签第二个事件的信息就丢了。源码包里如果只是单头分类项目说明里多半会写“只支持单事件句子”你拿多事件文本去测效果会神秘地差一大截。5. 避坑与调优中文信息抽取最容易翻车的 5 个操作细节5.1 现象实体标签和 token 对不上loss 变成 nan你用自己的 BERT 代码训练实体抽取时偶尔会遇到 loss 发警告甚至变成 nan。原因是中文文本里混了英文或特殊字符bert-base-chinese会把“ABC”切分成AB和##C两个 token而你的字符级标签只有“ABC”一个长度。解决不要假设字符数和 token 数相等。用tokenizer(text, return_offsets_mappingTrue)拿到每个 token 对应的原始字符偏移然后按偏移把标签映射到 token。如果 zip 里的数据集太老直接用“字-标签”逐行读也要在训练代码里判断 token 是否以##开头。5.2 现象关系抽取测试集 F1 比实体抽取低一大截这是最典型的上游错误传导。你 NER 做得好不代表关系分类能学好因为关系分类的输入是实体 span实体边界错一个字符整个句子上下文就被带偏。更隐蔽的是实体类别错误比如“苹果”被标成 ORG但原句里它是水果。解决对关系模型做误差归因。把测试集中的实体标签替换成人工正确标签再跑一遍关系抽取如果 F1 大幅回升说明问题在 NER关系模型本身没问题。下一步就是修实体边界而不是继续堆关系模型的参数量。5.3 现象一个句子里多个事件同时存在抽取结果漏一半“张三宣布起诉李四”这类文本按事件抽取标准流程至少有两个事件宣布、起诉。但很多项目源码里的模型只有一个trigger_head每个 token 只能得到一个预测标签于是“宣布”和“起诉”二者只能留一个。解决改用多标签分类或者按事件类型拆成独立的二分类预测头即每个 token 对所有事件类型分别算一次概率。代价是显存占用变大但事件抽取本身对上下文重叠容忍度低这一点投入是值得的。5.4 现象长文本截断后关系跨句全丢新闻和合同动辄几千字如果统一把max_length128的截断直接套到预测接口上跨句关系基本全废。比如主体在句子 A客体在句子 B截断后模型永远看不到完整上下文。解决先做滑窗切分窗口之间保留重叠区域并记录字符偏移。关系分类时只取同时包含头实体和尾实体的窗口事件抽取时先用候选触发词定位窗口再在窗口内做论元角色标注。不要相信“拉长到 512 就万事大吉”512 对长文档还是不够。5.5 现象同一份代码开两个随机种子F1 差 3 个点中文 NLP 项目的 F1 在一两个点上浮动很常见但差 3 到 5 个点通常不是模型问题是训练流程没固定随机种子。PyTorch 和 HuggingFace 各有seed参数还有数据加载器的shuffle顺序任何一个不一致验证集上挑出的最优 checkpoint 也会不一样。解决训练脚本里同时设置torch.manual_seed(42)、tokenizer不随机、TrainingArguments(seed42)并且指定load_best_model_at_endTrue。这听起来像玄学其实原因是模型选 checkpoint 的时机被随机量干扰了。固定 seed 之后你复现出来的波动能控制在 0.5 个点以内。6. 部署前最后一公里用一个脚本验证三元组再塞进 FastAPI拿到训练好的模型后别急着对外提供接口先写一个批量验证脚本。这个脚本要能读入一批测试文本输出所有三元组和置信度接着人工抽查 50 到 100 条。我会把实体置信度和关系置信度分开看避免一条低分三元组混进报表。# 三元组验证脚本带上置信度过滤避免垃圾输出 def extract_triples(text, ner_model, rel_model): entities predict_ner(ner_model, text) triples [] for i, subj in enumerate(entities): for obj in entities[i 1:]: rel predict_relation(rel_model, text, subj[text], obj[text]) if rel and rel[confidence] 0.75: triples.append(rel) return triples这里每个三元组都被打了置信度阈值。把阈值调低输出变多但人工审核成本直线上升调高输出干净但有些难例被过滤掉。没有绝对正确答案项目说明里如果没写推荐阈值就在验证集上自己画一条 P/R 曲线。如果要暴露成 HTTP 接口常见的做法是用 FastAPI 包一层# FastAPI 接口接收文本返回结构化三元组 from fastapi import FastAPI app FastAPI() app.post(/extract) async def extract(payload: dict): text payload.get(text, ) triples extract_triples(text, ner_model, rel_model) return {triples: triples}这个接口只能算 demo真正的生产环境还差几件事用 ONNX 或 TensorRT 把 BERT 拉起来提速、对输入长度做滑窗上限、给每个请求加 trace_id 方便排查。我吃过一次亏上线某金融公告信息抽取服务当时手一抖把关系阈值从 0.7 改到 0.5结果数据库里多出几万条“某某公司位于某某公司”的垃圾三元组最后花两天清洗。之后我养成了一个习惯每次部署前都要在未标注文本里抽 100 条结果打印出来人工过一眼别看曲线自己骗自己。希望这些步骤能帮你把这套源码真正用起来也少走几个我走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑