为什么我实现的Transformer在序列标注任务上效果不如BiLSTM?
问题背景
我目前在处理序列标注任务,采用单个Transformer Encoder来获取序列每个元素的logits。在对Transformer和BiLSTM进行实验后,发现在我的场景下BiLSTM表现更优,因此想确认是否是我的Transformer实现存在问题,以下是相关实现代码:
import torch import torch.nn as nn import torch.nn.functional as F import math def create_mask(src, lengths): """Create a mask hiding future tokens Parameters: src (tensor): the source tensor having shape [batch_size, number_of_steps, features_dimensions] length (list): a list of integers representing the length (i.e. number_of_steps) of each sample in the batch.""" mask = [] max_len = src.shape[1] for index, i in enumerate(src): # The mask consists in tensors having false at the step number that doesn't need to be hidden and true otherwise mask.append([False if (i+1)>lengths[index] else True for i in range(max_len)]) return torch.tensor(mask) class PositionalEncoding(nn.Module): def __init__(self, d_model, dropout=0.1, max_len=5000, device = 'cpu'): super().__init__() self.dropout = nn.Dropout(p=dropout) self.device = device position = torch.arange(max_len).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model) pe = torch.zeros(1, max_len, d_model) pe[0, :, 0::2] = torch.sin(position * div_term) pe[0, :, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe) def forward(self, x): x = x + self.pe[:, :x.size(1), :].to(self.device) return self.dropout(x) class Transformer(nn.Module): """Class implementing transformer ecnoder, partially based on pytorch official transformer tutorial""" def __init__(self, in_dim, h_dim, n_heads, n_layers, dropout=0.2, drop_out = 0.0, batch_first = True, device = 'cpu', positional_encoding = True): super(Transformer, self).__init__() self.model_type = 'Transformer' self.pos_encoder = PositionalEncoding(in_dim, dropout, device = device) encoder_layers = nn.TransformerEncoderLayer(in_dim, n_heads, h_dim, dropout) self.transformer_encoder = nn.TransformerEncoder(encoder_layers, n_layers, norm=nn.LayerNorm(in_dim)) self.in_dim = in_dim self.drop_out = drop_out self.positional_encoding = positional_encoding def forward(self, src, mask = None, line_len=None): src = src * math.sqrt(self.in_dim) if self.positional_encoding: src = self.pos_encoder(src) if line_len is not None and mask is None: mask = create_mask(src, line_len) else: mask = None output = self.transformer_encoder(src, src_key_padding_mask = mask) if self.drop_out: output = F.dropout(output, p = self.drop_out) return src, output
上述代码运行后,输出隐层状态会传入额外的线性层,采用二分类CrossEntropy损失和Adam优化器进行训练。已经尝试了多组超参数组合,但BiLSTM表现仍然更优。
实现问题排查
你的Transformer实现存在多个明显错误,是导致效果不如BiLSTM的核心原因:
- Padding mask逻辑完全写反:PyTorch的
src_key_padding_mask要求被mask的padding位置为True,有效位置为False。你现有create_mask的逻辑是将有效位置设为True、padding位置设为False,等于直接把所有有效输入都屏蔽了,注意力计算完全失效。同时函数注释写的是“隐藏未来token”,这是Decoder端用的序列掩码,Encoder端的padding mask不需要隐藏未来token,注释也存在误导。 - 未传入batch_first参数:你自定义的Transformer类设了
batch_first=True,但初始化nn.TransformerEncoderLayer时没有传入batch_first=True,该层默认输入形状是[序列长度, 批次大小, 特征维度],和你传入的[批次大小, 序列长度, 特征维度]不匹配,注意力计算逻辑完全错误。 - mask赋值逻辑错误:forward方法中的逻辑是只要外部传入了mask,或者line_len为空,就会直接把mask设为None,等于你手动传入的mask永远不会生效,padding位置没有被正确屏蔽。
- 位置编码设备处理冗余:你已经用
register_buffer注册了位置编码pe,它会自动跟随模型移动到对应设备,不需要手动调用to(self.device),如果模型移到GPU但初始化时device参数还是默认的cpu,反而会出现设备不匹配的报错。
修复实现后效果仍不如BiLSTM的可能原因
如果修复上述错误后效果还是没有超过BiLSTM,属于正常情况,和任务、数据特性相关:
- 数据集规模较小:Transformer的拟合能力强但需要更多数据支撑,小数据集下BiLSTM自带的序列局部依赖归纳偏置更有优势,更容易收敛到更好的效果。
- 序列长度偏短:短序列场景下Transformer的全局注意力优势无法体现,BiLSTM对局部相邻特征的建模效率更高,同等参数量下效果更好。
- 超参数适配不足:Transformer对学习率更敏感,通常需要比LSTM更小的学习率,搭配学习率warmup策略才能稳定收敛,直接使用Adam默认学习率很容易出现训崩的情况。
- 任务特性更适配BiLSTM:如果你的序列标注任务强依赖相邻token的局部特征,比如分词、简单实体识别等,BiLSTM的序列建模特性本身就比Transformer更贴合这类任务的需求。
内容的提问来源于stack exchange,提问作者Iacopo Ghinassi
相关产品推荐
相关产品推荐

