恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
关系抽取模型怎么选?从CNN到BERT的实战对比与避坑指南
首页
资讯中心
/
关系抽取模型怎么选?从CNN到BERT的实战对比与避坑指南
关系抽取模型怎么选?从CNN到BERT的实战对比与避坑指南
发布时间:2026/10/14 1:32:48
简介面向自然语言处理与深度学习研究者、关系抽取方向开发者资源包完整实现了基于CMeIE-V2与CMeIE两个中文医学数据集的多模型关系抽取实验方案。项目在传统文本分类思路上进一步引入实体位置编码与实体类别信息结合上下文与实体特征提升分类效果并覆盖卷积神经网络、循环神经网络、胶囊网络、基于剪枝依赖树的图卷积网络、Transformer及预训练模型BERT六类主流架构可作为算法对比与论文复现的参考基线。资源包共484个文件压缩后大小约21.7MB核心包括249个Python脚本、182个编译生成的pyc文件、25个YAML配置文件以及CSV数据划分、模型权重、文档与演示动图等。Python代码覆盖数据处理、模型构建、训练评测全流程YAML便于调整超参数CSV文件提供训练集、验证集、测试集及关系标签对照配套说明文档可帮助快速理解项目结构。目前已有200人学习下载适合希望快速上手关系抽取基准实验、开展多模型效果对比或深入理解实体级特征工程的中高级NLP学习者。1. 关系抽取不只是分类CNN、RNN、Capsule、GCN、Transformer 和 BERT 各管一段关系抽取是信息抽取里最绕不开的一环给一句“A公司收购了B公司”要识别出 A 和 B 是什么关系。表面看是个分类问题但六个模型摆在面前时问题就变成了“该用哪个”。早期做关系抽取我用 CNN 和 RNN后来发现 CNN 擅长抓局部特征、RNN 擅长处理序列但都吃不到句法结构Capsule 能解决特征聚合的歧义GCN 能直接把依存句法树编码进网络而 Transformer 和 BERT 则把预训练语义带进了任务。它们不是互相替代而是各管一段。这篇文章从数据准备开始讲到每个模型在关系抽取里真正干的事、参数怎么设、以及从 CNN 迁移到 BERT 时最容易踩的五个坑。2. 关系标注与数据格式用什么标签体系决定了后面所有模型的输入长什么样关系抽取在动手前要先把标签体系和数据格式定死。很多做关系的项目翻车不是模型选错而是标签体系混乱——实体类别、关系类别、否定关系混在一起模型学到的全是噪声。常见做法是定义实体对和关系分类两个维度实体标记 BIO 或直接位置索引关系用固定类别表比如“收购”“投资”“位于”“毕业于”。我一般会建一个关系标签映射表同时把句子、实体位置、关系标签拆成三个独立字段方便后续构造不同模型的输入。2.1 关系标签体系与样本划分原则一个干净的关系抽取数据集每个样本至少包含句子文本、两个实体的起止位置、关系类别。类别表要尽量扁平化避免层级关系否则模型要同时学“人事变动”和“CEO离职”这种语义重叠的标签收敛困难。样本划分上关系抽取有个特殊风险同一个实体对在训练集和测试集同时出现会导致模型记住实体名字而不是学习上下文。所以严格做法是按照实体对分组划分而不是按句子随机切。关系类别数量建议控制在 10 到 30 类之间。类别太少模型学不到区分度类别太多样本不均衡问题会非常严重。工业界常用 Longtail 分布头部关系占掉 70% 样本尾部关系经常只有几百条。这种情况下Capsule 和 BERT 的差别就出来了——Capsule 在小样本上有一定韧性但真正稳的还是预训练模型加数据增强。2.2 数据格式转换从标注文本到模型输入的代码骨架下面这段代码把标注好的文本转成 BERT 和 GCN 都能用的统一格式包含 token 偏移量对齐逻辑这是最容易出错的地方。def convert_annotated_text(sentence, e1_start, e1_end, e2_start, e2_end, relation): tokens sentence.split( ) # 记录每个实体的 token 级起止位置用于后续构造邻接矩阵 e1_tokens (e1_start, e1_end - 1) e2_tokens (e2_start, e2_end - 1) labels { relation: relation, head: [CLS], # 实际项目中替换为实体类型标记 tail: [SEP] } return { sentence: tokens, entity_pair: (e1_tokens, e2_tokens), label: labels, text: sentence }这段代码的核心是把字符级实体位置转成 token 级索引。参数说明e1_start 和 e1_end 是第一个实体在原始句子里的字符起止位置e2 同理。之所以强调 token 级对齐是因为 BERT 分词器可能把一个词切成多片导致位置偏移后续算句法距离或构造图节点时全错。我在实际项目里把一个中文关系标注集转成 BERT 输入时出现过实体位置偏了一位的问题原因是空格分词和字分词的边界不一致。解决办法是统一用一个分词器先做预对齐所有实体位置都基于同一个分词结果计算不要混用两套分词。3. CNN、RNN、Capsule 在关系抽取里的真实分工与调参边界很多人把 CNN 和 RNN 直接当“传统模型”扔掉但在关系抽取里它们依然是高效的基线。CNN 做的是 n-gram 局部特征提取擅长捕捉“收购”“起诉”“毕业于”这类触发词附近的固定搭配RNN 及其变体擅长建模实体之间的时序依赖尤其是句子较长时词与词之间的前后关系。两者的核心问题在于拿不到句子的句法结构。关系抽取里“A 公司的 CEO 张三”和“张三A 公司的 CEO”语义相同但语序不同RNN 处理这种句子容易摇摆。3.1 CNN 做关系抽取窗口大小和 Max-Pooling 的搭配逻辑CNN 的关系抽取做法是把词向量拼接成矩阵然后用多个卷积核扫描局部窗口。核心参数是窗口大小和池化策略。窗口大小通常取 3 或 5太小抓不到跨实体信息太大又引入噪声。我在某开源关系数据集上调参时窗口从 3 改到 5F1 提升约 1.5 个点但继续调到 7 就掉回去了。Max-Pooling 负责从每个卷积核的输出里挑最强特征好处是保留最显著的模式坏处是丢失位置信息。import torch.nn as nn class CNNRelationClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, num_filters, window_size, num_classes): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.conv nn.Conv1d(embed_dim, num_filters, kernel_sizewindow_size) self.fc nn.Linear(num_filters, num_classes) self.dropout nn.Dropout(0.3) def forward(self, x): emb self.embed(x).permute(0, 2, 1) # B, D, L conv_out torch.relu(self.conv(emb)) pooled conv_out.max(dim2).values # 全局最大池化 return self.fc(self.dropout(pooled))参数说明embed_dim 是词向量维度中文任务 100 到 300 都常见num_filters 是卷积核数量一般 128 或 256window_size 建议先测 3、5、7 三档用验证集决定。注意这里用了 max(dim2)是对序列长度维度做池化把每个卷积核的输出压成一个值。这个层把句子长度信息全部丢掉了所以 CNN 不适合处理实体位置敏感的关系类型比如“A 在 B 之后”这种需要精确位置逻辑的关系。3.2 RNN 的变体选择BiLSTM 在关系抽取里的参数陷阱RNN 在关系抽取里一般直接用 BiLSTM双向结构能同时看到实体的左右上下文。但有个经典陷阱直接把两个实体放到句尾拼接BiLSTM 的远期依赖会衰减实体本身的信息被长句淹没。常见做法是实体感知编码——把第一个实体位置的隐藏状态和第二个实体位置的隐藏状态单独提取出来再拼上句子整体表示。class BiLSTMRelationClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(hidden_dim * 4, num_classes) def forward(self, x, e1_pos, e2_pos): emb self.embed(x) lstm_out, _ self.lstm(emb) # B, L, 2H e1_h lstm_out[torch.arange(x.size(0)), e1_pos] e2_h lstm_out[torch.arange(x.size(0)), e2_pos] sent_h lstm_out.mean(dim1) vec torch.cat([e1_h, e2_h, sent_h], dim-1) return self.fc(vec)hidden_dim 建议 128 到 256太小记不住长距离依赖太大容易过拟合小数据集。e1_pos 和 e2_pos 是实体位置的 token 索引。拼接维度是 hidden_dim * 4来自两个实体的双向 hidden state 加句子均值。这段逻辑的关键是 e1_pos 必须在预处理时算准很多玄学调参效果不稳定最后查出来是实体位置传错了。3.3 Capsule 的价值不是精度而是特征路由的稳定性Capsule 在关系抽取里被提出来核心动机是解决 CNN 池化丢位置信息的问题。CNN 的 pooling 是“赢家通吃”而 Capsule 用动态路由机制让低层特征按协议加权传给高层。实际落地时Capsule 很少单独当骨干它一般接在 CNN 或 BiLSTM 的编码层后面。我在某个关系抽取实验里把 max-pooling 换成 capsule 层后总体 F1 提升不到 1%但对长句子的鲁棒性有明显改善——长句里的关键触发词被池化冲掉的概率降低了。Capsule 的调参重点在于路由迭代次数和 capsule 维度。迭代次数 3 次是默认值超过 5 次容易过拟合capsule 维度一般取 16 或 32维度太高小数据集上矩阵运算量涨得快但收益不明显。如果你想让模型对实体位置更敏感可以在输入特征里拼接位置编码这是提升效果最快的路子比调路由参数更划算。4. GCN 融入句法依存关系抽取里被低估的结构信息GCN 解决的是 CNN 和 RNN 都拿不到的问题——句子的句法结构。关系抽取里“苹果公司 CEO 库克宣布新品”这句话实体“苹果公司”和“库克”之间隔着“CEO”这个身份词句法上是修饰关系。GCN 的做法是把依存句法树转成邻接矩阵让消息沿着树的边传递这样实体对之间的路径长度被压缩了模型更容易学到它们之间的语义关系。4.1 从依存句法树到邻接矩阵的全过程常见做法是先对每个句子做依存解析得到类似“nsubj”“dobj”这样的依存边然后构建一个 N×N 的邻接矩阵。矩阵里 A[i][j] 1 表示 token i 和 token j 之间有依存边。之后把 token 的初始特征向量放进 GCN 层每一层做一次邻居特征聚合。下面是构造邻接矩阵的代码骨架。import numpy as np def build_adjacency_matrix(tokens, dependencies): n len(tokens) adj np.zeros((n, n), dtypenp.float32) for head, dep, rel in dependencies: # head 和 dep 是 token 下标rel 是依存关系标签 adj[head][dep] 1.0 adj[dep][head] 1.0 # 无向图 # 如果想用依赖类型信息可以按关系类型拆成多个邻接矩阵 # adj_rel[rel][head][dep] 1.0 # 加上自环GCN 消息传递时保留自身特征 adj adj np.eye(n, dtypenp.float32) # 归一化D^-1/2 * A * D^-1/2 deg adj.sum(axis1) deg_inv_sqrt np.power(deg, -0.5) deg_inv_sqrt[deg_inv_sqrt np.inf] 0.0 adj_norm deg_inv_sqrt[:, None] * adj * deg_inv_sqrt[None, :] return adj_norm这段代码的关键有三处。第一把有向依存边变成无向边经验上加双向消息传递比纯有向稳定。第二自环是必须的否则每层 GCN 都会丢自身特征。第三归一化方式用的是对称归一化防止度数高的节点特征爆炸。如果你想把依存关系类型也喂进图模型可以拆成多个邻接矩阵——每个关系类型一个矩阵GCN 对每类矩阵分别聚合再拼接但特征维度会成倍增加数据量不够时容易过拟合。4.2 GCN 在关系抽取里的两种放置位置GCN 既能放在 BERT 之前做句法特征编码也能放在 BERT 之后做结构信息增强。我自己试下来放在 BERT 之后效果更稳——先用 BERT 编码 token 语义再把这些语义向量作为 GCN 节点的初始特征沿依存边做消息传递。这样句法结构信息和预训练语义是叠加而不是竞争。另一种做法是用 GCN 的输出替代位置编码作为实体感知特征对句中实体对之间的距离信息做显式建模适合实体对跨度很大的句子。GCN 的层数建议 2 到 3 层。原因和图像领域的经验一致网络太深消息传递会把远处节点的信息反复聚合节点表征趋于同质化反而丢掉了局部区分度。我在一个公开中文关系数据集上对比过2 层比 1 层提升约 2%3 层和 2 层接近4 层开始下降。另一个参数是 hidden_dim一般和 BERT 输出维度对齐比如 BERT 是 768GCN 的 hidden_dim 就取 256 到 768 之间取小了等于加了个信息瓶颈。5. Transformer 与 BERT 入局从迁移学习到微调陷阱Transformer 的自注意力机制解决了长距离依赖问题BERT 则把大量无监督语料上的语言知识迁移到关系抽取任务。但“BERT 效果一定更好”不是绝对的。在小样本、领域专有名词多、实体重叠严重的场景下BERT 微调效果可能不如 CNN 加 GCN。原因是预训练模型对通用语义掌握好但对特定领域的关系触发词不一定敏感比如医疗或法律文书里的表述套路需要足够的领域数据才能激活。5.1 Transformer 在关系抽取里的编码方式位置编码与注意力头Transformer 的输入要叠加位置编码否则自注意力机制对词序不敏感。关系抽取里常见做法是给两个实体分别加入特殊标记比如在实体前后插入 [E1] 和 [E2] 标记让模型知道当前这一对实体是谁。这个 trick 在关系抽取里几乎是标配因为它把“识别实体”和“判定关系”解耦了。注意力头的选择上12 头是常见默认值但我试过把某些任务减到 8 头小数据集上能抑制过拟合。微调 Transformer 时要注意学习率的设置。BERT 的微调学习率一般取 2e-5 到 5e-5过大容易把预训练权重冲坏Transformer from scratch 则需要 1e-4 到 3e-4 的热身阶段。关系抽取里如果句子平均长度超过 128建议用 256 作为 max length超过 512 的句子进行截断或滑窗切分。5.2 BERT 微调的输入构造实体标记与注意力掩码下面这段代码演示了如何把句子和实体位置转成 BERT 的输入格式。实体特殊标记是关键掩码要保留两段文本之间的注意力确保两个实体都能看到对方上下文。from transformers import BertTokenizer, BertForSequenceClassification import torch tokenizer BertTokenizer.from_pretrained(pretrained_bert_dir) model BertForSequenceClassification.from_pretrained( pretrained_bert_dir, num_labelsrelation_num ) def encode_with_entity_markers(text, e1_span, e2_span): # e1_span 和 e2_span 是字符级起止位置 tokens list(text) tokens.insert(e1_span[1], [E1]) # 闭区间插入结束标记 tokens.insert(e1_span[0], [E1]) tokens.insert(e2_span[1] 2, [E2]) # 偏移量取决于前面已插入的标记数 tokens.insert(e2_span[0] 2, [E2]) tokenized tokenizer(tokens, is_split_into_wordsTrue, max_length256, truncationTrue, paddingmax_length, return_tensorspt) return tokenized inputs encode_with_entity_markers(text, (8, 12), (20, 25)) outputs model(**inputs) logits outputs.logits这段代码的逻辑在偏移量处理上非常容易出错。给 e1 插入两个标记后e2 的字符位置整体后移了两位所以要加 2。如果不用 is_split_into_wordsTrue而直接传字符串tokenizer 会自己分词导致标记错位。参数说明max_length256 是经验值句子太长会截掉后半部分导致尾实体信息丢失pretrained_bert_dir 指本地模型目录实际项目里替换成自己的路径。5.3 从 CNN 迁移到 BERT样本量、学习率与过拟合的血泪经验如果你已经在 CNN 和 GCN 上跑通了关系抽取迁移到 BERT 不是简单换模型。我踩过的坑有三个。第一样本量少于 5000 条时BERT 微调经常过拟合验证集 F1 忽高忽低这时要用早停法 较小的学习率或者直接用冻结 BERT encoder、只训练分类头的方案。第二关系类别样本不均衡时BERT 比 CNN 更严重地偏向多数类因为预训练模型的语言先验会放大频率信息。第三实体标记的格式很重要很多公开代码里用的是 [E1] / [E1] 对称标记但必须放在实体两端而不是实体内部。我一般会把预热步数设在整个训练步数的 10%然后用线性衰减。优化器用 AdamWweight decay 取 0.01。如果训练曲线震荡优先检查数据里有没有实体位置标错的样本——这类噪声样本在 CNN 里影响小在 BERT 里会被放大。6. 关系抽取避坑指南五个高频翻车点和排查路径迁移到 BERT/GCN 之后关系抽取的问题不再只是“模型不收敛”而是“表面上收敛但指标不对”。这里写五条高频踩坑记录每条按现象、原因、解决展开。6.1 验证集 F1 比训练集高很多现象训练集 F1 不到 0.7验证集却到 0.85明显不合常理。原因实体对在训练集和测试集有重叠模型记住实体名字就直接出答案泛化能力是假的。解决按实体对分组划分数据。特别注意同一个实体出现在不同句子中只允许它在一个集合里出现。用循环遍历所有样本建实体对到句子列表的映射然后按实体对分组切分。6.2 BERT 微调时 loss 下降后在 0.1 附近震荡现象loss 降到 0.1 之后不再下降验证 F1 也停在原地。原因学习率过大参数在最优区间附近来回跳动。解决把学习率调低一个量级比如从 3e-5 降到 1e-5同时增大 warmup 步数。如果还不行检查是否有空梯度——dropout 率太高会让梯度噪声变大dropout 从 0.3 调到 0.1 有时效果立竿见影。6.3 GCN 加入后效果反而不如纯 BERT现象按论文套路加了句法依存树F1 掉了 2% 到 3%。原因依存解析本身有误差解析器在长难句上输出的边本身就是错的等于给图里灌了噪声。解决先统计解析器的正确率如果准确率低于 85%就不值得用 GCN。另外可以做边剪枝——只保留距离实体对最短路径上的依存边把无关分支切掉这招在长句上尤其有效。6.4 实体重叠时模型把两个实体混为一谈现象句子里出现多个实体对模型只输出一组关系或者把 A-B 的关系套到 A-C 上。原因单标签分类头只能输出一个关系遇到多实体对就无能为力。解决改用序列标注的方式做关系分类或者用分层分类——先抽实体再对每一对实体做二分类关系判断。生产中常见做法是关系抽取模型改成多标签输出每个标签对应一个关系类别用 sigmoid 而非 softmax。6.5 中文分词和实体边界把位置标记弄偏现象实体边界总是偏一个字模型指标始终上不去。原因中文用字级标注时 BERT 分词正常但如果你把 char-level 位置和 token-level 位置混用就会系统性偏移。解决统一只用一个分词器做对齐实体位置的索引必须基于同一个 tokenizer 的编码结果。最稳的办法是写个单元测试把实体标记函数覆盖的 case 跑一遍尤其是标点符号、全角字符、英文数字混排的情况。7. 验证与进阶用 BERT GCN 跑通一个最小可信的混合方案前面六章把每个模型的分工和坑都过了一遍最后一章落在一个可以直接拿去做对比实验的最小方案上BERT 编码语义GCN 叠加结构信息。这个组合在中文关系抽取里的表现通常是 BERT 基线的最好成绩再涨 1 到 3 个点代价是训练时间多一点。我不建议一上来就上 Capsule 或复杂多路融合——先把这条链路跑通拿到可信的数字再谈针对性优化。具体做法分四步。第一步用 BERT 输出 token 向量作为 GCN 节点的初始特征这一步一般要关掉 BERT 的池化层。第二步用依存解析拿到邻接矩阵按第 4 章的归一化方式处理。第三步把 GCN 的输出和 BERT 的 [CLS] 向量拼接起来。第四步接一个两层 MLP 做关系分类。下面是一个简化的训练循环骨架。for batch in dataloader: inputs, adj, labels batch outputs bert(inputs).last_hidden_state # B, L, H gcn_out gcn_layers(outputs, adj) # B, L, H cls_vec outputs[:, 0, :] gcn_vec gcn_out.max(dim1).values # 全局池化 fused torch.cat([cls_vec, gcn_vec], dim-1) logits mlp(fused) loss ce_loss(logits, labels) loss.backward() optimizer.step()关键点在于 gcn_layers 是把邻接矩阵和 token 向量一起送进图的卷积层。gcn_vec 用的是 max 池化而不是均值池化因为实体对关键证据通常集中在少数几个 token 上。门控逻辑上可以给 GCN 的输出加一个可学习的门控权重控制结构信息的注入强度权重初始设为 0.5。实际我调参时发现这个门控对长句提升明显短句上几乎没影响。跑通这个方案后强烈建议做一次消融实验关掉 GCN 看 BERT 单模型的 F1关掉 BERT 直接用词向量加 GCN 看 F1再把两者合起来看 F1。三组数字一比你就能判断结构信息在你的数据集上到底有没有用。我做过的一个中文合同关系抽取项目里BERT 单模型 F1 是 0.81加 GCN 后到 0.84提升来自跨段落长句的实体对识别——这是路径压缩带来的直接收益。最后一个经验关系抽取不是模型越复杂越好。先用 BERT 加简单分类头跑通把数据洗干净再逐步加结构信息、Capsule、多任务学习。每一步都用验证集确认收益不要同时改多个变量。我见过太多把六个模型都塞进一个 pipeline 的工程最后定位问题都无从下手。模块化、可消融比一味堆模型更重要。希望这些方法论能帮到你至少让你在下一个关系抽取任务里少走几段弯路。本文还有配套的精品资源点击获取