ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

融合语句-实体特征与BERT的中文实体关系抽取模型SEF-BERT实战

融合语句-实体特征与BERT的中文实体关系抽取模型SEF-BERT实战 简介这份资源是一篇聚焦中文实体关系抽取的学术文档面向自然语言处理方向的研究生、算法工程师及信息抽取入门者用于理解如何在BERT基础上融合语句与实体特征以提升关系分类效果。压缩包内仅含1个docx文件约383KB为完整论文式文档涵盖引言、算法模型与实验分析等章节便于系统阅读与引用。文中提出SEF-BERT模型将句子全局特征向量与两个目标实体的均值特征向量分别做矩阵运算与全连接得到同时包含句子和实体信息的融合特征并梳理了从规则模板、机器学习到MV-RNN、卷积网络、多端注意力及Transformer、ALBERT、ERNIE的技术演进脉络还给出BERT-base-Chinese的12层、768维结构说明。目前已有137人学习适合希望快速掌握中文关系抽取主流方法、为问答系统或文本理解任务打基础的读者参考。1. 为什么中文实体关系抽取不能只靠 BERT 的 [CLS] 向量做过中文信息抽取的人大多踩过同一个坑把句子丢进 BERT取[CLS]位置的输出接一个 softmax 做关系分类在 SemEval 这类英文数据集上看着还行一换到中文金融公告、医疗病历、招投标文本F1 就掉到 60 出头。原因不复杂——中文没有天然的词边界实体本身往往是多字词甚至嵌套结构而[CLS]这个向量是在整句自注意力之后被平均出来的它记住了语义却把两个实体的位置信息和类型信息稀释掉了。融合语句-实体特征与 BERT 的中文实体关系抽取模型要解决的就是这件事在 BERT 提供的上下文语义之上显式地把这两个实体分别是什么类型、在句子里处于什么位置、它们之间的相对距离是多少这些结构化特征拼回去让分类器同时看到语义和结构。它适合已经跑通 BERT 微调、但发现纯语义方案在中文长句和实体密集句上召回不足的团队也适合需要把关系抽取接进知识图谱流水线的工程同学。下面从特征设计讲到可复现的训练代码再到参数调优和排错。2. SEF-BERT 的特征工程语句特征与实体特征怎么拼2.1 语句特征不只是 [CLS]还要保留实体边界纯 BERT 方案默认[CLS]承载全句语义但在关系抽取里真正决定关系的是两个实体之间的局部上下文。常见做法是把句子按实体位置切成五段实体1之前的文本、实体1、实体1与实体2之间的文本、实体2、实体2之后的文本。这种分段方式在中文里尤其重要因为中文的关系触发词如收购任职于隶属于经常紧贴实体出现切段后模型能更聚焦。具体到向量层面除了[CLS]我一般还会取实体首尾 token 的隐状态做平均池化得到实体级语义向量。这样一句话最终产出三类语句侧特征全句[CLS]向量、实体间片段池化向量、实体首尾平均向量。2.2 实体特征类型、位置、距离三个维度实体特征是可枚举、可离散化的这正是它比纯语义更稳的地方。工程上通常构造以下字段特征名含义取值示例编码方式head_type头实体类型PER / ORG / LOC类型 embedding维度 32tail_type尾实体类型PER / ORG / LOC类型 embedding维度 32head_pos头实体起始位置0~511位置 embedding维度 32tail_pos尾实体起始位置0~511位置 embedding维度 32distance两实体 token 距离-128~128分桶 embedding维度 32seg_ids实体所在分段标记0/1/2分段 embedding维度 16这些离散特征各自过一层 embedding 后拼接再经过一个线性层压到与 BERT 隐层同维base 版是 768最后与语句特征做拼接或门控融合。2.3 融合方式拼接、门控还是交叉注意力最省事的是直接 concat把[CLS]向量和实体特征向量拼成 768160 维送分类头。但拼接会让分类器偏向维度更高的那一侧实践中更稳的是门控融合import torch import torch.nn as nn class GatedFusion(nn.Module): def __init__(self, hidden_size768, feat_size160): super().__init__() self.proj nn.Linear(feat_size, hidden_size) # 把实体特征投影到同维 self.gate nn.Linear(hidden_size * 2, hidden_size) # 门控权重 def forward(self, sent_vec, ent_feat): # sent_vec: [B, 768] 语句侧向量 # ent_feat: [B, 160] 实体侧离散特征 ent_proj self.proj(ent_feat) # [B, 768] g torch.sigmoid(self.gate(torch.cat([sent_vec, ent_proj], dim-1))) return g * sent_vec (1 - g) * ent_proj # 加权融合逻辑说明proj负责把低维实体特征升到 768 维避免维度不匹配gate学习一个 0~1 的权重让模型自己决定当前样本更依赖语义还是结构。参数上feat_size要和你实际拼接的实体特征总维度对齐类型 32×2 位置 32×2 距离 32 分段 16 160改特征就得同步改这个数否则nn.Linear会直接报维度错误。3. 用 HuggingFace 搭一个可训练的 SEF-BERT 模型3.1 数据格式与实体标记方案中文关系抽取的数据通常以三元组形式给出(头实体, 关系, 尾实体)加原始句子。训练前要把实体位置映射到 token 下标。中文 BERT 用字级别 tokenizer一个汉字基本对应一个 token但标点和英文会被拆开所以必须用offset_mapping做字符到 token 的对齐不能直接用字符下标。from transformers import BertTokenizerFast tokenizer BertTokenizerFast.from_pretrained(bert-base-chinese) def encode(sentence, head, tail, max_len256): enc tokenizer(sentence, max_lengthmax_len, truncationTrue, return_offsets_mappingTrue, return_tensorspt) offsets enc[offset_mapping][0].tolist() head_start sentence.find(head) tail_start sentence.find(tail) # 字符位置 - token 位置 def char2tok(pos): for i, (s, e) in enumerate(offsets): if s pos e: return i return 0 enc[head_pos] char2tok(head_start) enc[tail_pos] char2tok(tail_start) enc.pop(offset_mapping) return enc逻辑说明return_offsets_mappingTrue是这段代码的关键它返回每个 token 在原句中的字符区间。char2tok遍历区间找到覆盖目标字符位置的 token 下标。注意find只能定位首次出现如果同一实体在句中多次出现需要改用更精确的标注位置否则头尾实体会错位训练时表现为 loss 不降。3.2 模型主体BERT 编码器加特征融合头from transformers import BertModel import torch.nn as nn class SEFBert(nn.Module): def __init__(self, num_relations, num_types10, max_pos256): super().__init__() self.bert BertModel.from_pretrained(bert-base-chinese) self.type_emb nn.Embedding(num_types, 32) self.pos_emb nn.Embedding(max_pos, 32) self.dist_emb nn.Embedding(257, 32) # 距离分桶 -128~128 self.seg_emb nn.Embedding(3, 16) self.fusion GatedFusion(768, 160) self.classifier nn.Linear(768, num_relations) def forward(self, input_ids, attention_mask, token_type_ids, head_type, tail_type, head_pos, tail_pos, distance, seg_id): out self.bert(input_ids, attention_mask, token_type_ids) cls_vec out.last_hidden_state[:, 0] # [B, 768] feat torch.cat([ self.type_emb(head_type), self.type_emb(tail_type), self.pos_emb(head_pos), self.pos_emb(tail_pos), self.dist_emb(distance 128), self.seg_emb(seg_id) ], dim-1) # [B, 160] fused self.fusion(cls_vec, feat) return self.classifier(fused)逻辑说明dist_emb的索引要加 128 偏移因为 embedding 不接受负索引num_types要覆盖你数据里所有实体类型漏一个就会在type_emb处越界。classifier输出维度等于关系类别数包含一个无关系类。这套结构在 24GB 显存的卡上batch size 16、序列长度 256 可以稳定跑起来。3.3 训练循环与损失函数选择关系抽取普遍存在类别不平衡无关系样本往往占七成以上。直接用交叉熵会让模型倾向于全预测无关系。常见做法是加类别权重或者对无关系类做下采样。from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps200, num_training_stepstotal_steps) criterion nn.CrossEntropyLoss(weighttorch.tensor(class_weights)) for batch in dataloader: logits model(**batch) loss criterion(logits, batch[labels]) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad()逻辑说明lr2e-5是 BERT 微调的常规起点再大容易在头几个 step 就把预训练权重冲坏clip_grad_norm_设 1.0 防止梯度爆炸中文长句里这一步很必要。class_weights一般取类别频率的倒数归一化无关系类权重压到 0.3 左右效果比较稳。4. 参数调优与中文场景的排错清单4.1 三个必调参数学习率、最大长度、距离分桶学习率决定能不能收敛最大长度决定长句信息是否被截断距离分桶决定远距离实体对是否被区分开。这三者互相牵制调的时候建议固定两个动一个。参数推荐范围影响调整信号learning_rate1e-5 ~ 3e-5收敛速度与稳定性loss 震荡就降到 1e-5max_len128 / 256 / 512长句召回截断率高就加到 512dist_bucket128 / 256远距离实体区分度长文本 F1 低就加大距离分桶这里有个细节如果直接用原始距离做 embedding超过训练集最大距离的样本会越界。分桶的做法是把距离按区间映射比如 0~4 各占一桶5~8 合并一桶超过 64 全部归到最后一桶这样既省参数又提升泛化。4.2 中文特有的坑分词、嵌套实体与标点第一个坑是分词器。bert-base-chinese是字级但如果你换成某些词级中文 BERT实体边界可能落在词中间char2tok那套逻辑就失效了必须改成按词对齐。第二个坑是嵌套实体比如北京大学里北京是 LOC、北京大学是 ORG同一位置对应两个实体标注时要保留多组头尾位置模型侧要么做多标签要么拆成多条样本。第三个坑是全角标点和空格中文文本里全角逗号、书名号经常被 tokenizer 拆成独立 token导致实体间距离虚高预处理阶段统一转半角能减少这类噪声。4.3 验证指标与错误分析不要只看准确率。关系抽取要看每一类的 P/R/F1尤其是样本少的关系类型。验证时按关系类别分组统计找出 F1 最低的三类再抽样看错误样本。常见错误模式有两类一是实体识别本身错了关系再准也没用这类要回到 NER 环节二是关系触发词被截断比如收购被 max_len 切掉这类调长度或改分段策略。# 按类别输出分类报告定位弱项 python eval.py --ckpt best.pt --test data/test.json \ --report per_class --topk_errors 20逻辑说明--report per_class让评估脚本输出每类指标--topk_errors导出置信度最低的若干样本供人工检查。这一步比盲目调参有用得多多数时候 F1 上不去是数据标注问题而不是模型问题。5. 把 SEF-BERT 接进知识图谱流水线的两个技巧第一个技巧是关系抽取和实体识别解耦但共享编码器。实际流水线里 NER 和关系抽取往往分开训练但两者都依赖 BERT 编码可以共享底层参数、只在上层分叉这样推理时一次前向就能同时出实体和关系吞吐能提升接近一倍。共享后要注意两个任务的 loss 量级通常给 NER 加个 0.5 的权重系数平衡。第二个技巧是推理阶段的实体对剪枝。一句话里如果有 N 个实体两两组合是 N² 个候选对长句里这个数会爆炸。常见做法是先按距离过滤超过阈值比如 30 个 token的实体对直接判无关系再用模型打分能砍掉大半计算量。剪枝阈值要在验证集上扫一遍太激进会丢召回。def prune_pairs(entities, max_dist30): pairs [] for i in range(len(entities)): for j in range(i 1, len(entities)): if abs(entities[j][start] - entities[i][end]) max_dist: pairs.append((entities[i], entities[j])) return pairs逻辑说明max_dist控制剪枝强度中文里关系触发词通常离实体不远30 是个保守起点。剪枝后剩余实体对送模型批量打分配合torch.no_grad()和半精度推理单卡 QPS 能到几百。上线前记得用真实分布的长句压测一遍确认剪枝没有把关键关系误杀。本文还有配套的精品资源点击获取
返回列表