训练问答深度学习模型效果不佳的问题排查
问题描述
训练一个简单深度学习问答模型时,验证损失较低但生成序列质量极差。已知条件:
- 训练数据量充足,模型规模足够捕捉数据模式,已尝试调整多种超参数,损失曲线无过拟合迹象。
- 输入为
(batch_size, max_length)的批量分词语句,仅使用<pad>填充token和<unk>未知词token,未引入<sos>起始符与<eos>结束符,认为模型会生成固定max_length长度序列,无需上述两种token。
模型结构
模型由以下模块组成:
- 嵌入层:为输入
src生成token embedding和positional embedding - Transformer编码器
- 线性层解码器:接收编码器输出生成logits
嵌入层与编码器输出形状为(batch_size, max_length, vocab_size),生成token形状为(batch_size, max_length),采用交叉熵损失训练。
模型代码
import torch import torch.nn as nn import torch.nn.functional as F from torch.nn import TransformerEncoderLayer, TransformerEncoder, LayerNorm # 假设Embedding是自定义或扩展的实现 class Embedding(nn.Module): def __init__(self, vocab_size, hidden_size, max_position_embeddings, pad_token_id): super().__init__() self.token_emb = nn.Embedding(vocab_size, hidden_size, pad_token_id) self.pos_emb = nn.Embedding(max_position_embeddings, hidden_size) def forward(self, x): seq_len = x.size(1) positions = torch.arange(seq_len, device=x.device) return self.token_emb(x) + self.pos_emb(positions) class Model(nn.Module): def __init__(self, vocab_size, hidden_size, max_length, pad_id, num_encoder_layers: int = 2, layer_norm_eps: float = 1e-5): super(Model, self).__init__() self.pad_id = pad_id self.embeddings = Embedding( vocab_size=vocab_size, hidden_size=hidden_size, max_position_embeddings=max_length, pad_token_id=pad_id ) # 修正原代码参数错误:第二个参数是注意力头数nhead,而非编码器层数 encoder_layer = TransformerEncoderLayer(hidden_size, nhead=hidden_size//64, batch_first=True) encoder_norm = LayerNorm(hidden_size, eps=layer_norm_eps) self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm) self.fc = nn.Linear(hidden_size, vocab_size) def forward(self, src, trg=None): src_embeddings = self.embeddings(src) output = self.encoder(src_embeddings) logits = self.fc(output) if trg is not None: loss = F.cross_entropy( input=logits.view(-1, logits.size(-1)), target=trg.view(-1), ) else: loss = None predicted_ids = torch.argmax(logits, dim=-1) return predicted_ids, loss
核心问题分析
1. 模型结构不符合问答生成任务逻辑
当前模型是编码器+线性层的序列标注范式,本质是对输入序列的每个位置做token分类,而非问答生成需要的“从输入问题生成独立回答序列”的Seq2Seq逻辑。训练时强制模型将输入问题的每个位置与回答序列的对应位置绑定,完全违背问答任务中“输入问题到输出回答”的语义映射关系。
2. 缺失/的致命影响
即使要生成固定长度序列,没有起始符和结束符会导致模型无法区分序列的语义边界:
- 训练时模型无法学习“从起始位置开始生成回答”的逻辑
- 推理时模型没有停止信号,只能盲目填充到
max_length,大概率生成无意义重复或<pad>token
3. 损失计算未忽略填充token
当前损失计算未屏蔽<pad>位置的损失,模型会为了降低损失,倾向于生成大量<pad>或高频无意义token,导致验证损失低但生成内容完全不符合预期。
4. TransformerEncoderLayer参数传递错误
原代码中误将编码器层数num_encoder_layers作为注意力头数nhead传入,会导致注意力机制配置不合理,严重影响模型对语义的捕捉能力。
解决方案建议
1. 重构为Transformer编码器-解码器架构
问答生成属于Seq2Seq任务,必须使用编码器-解码器结构:
- 编码器负责编码输入问题的语义信息
- 解码器基于编码器输出+已生成的token,自回归生成回答序列
2. 引入/ token
- 分词时给每个回答序列添加
<sos>(开头)和<eos>(结尾) - 训练时解码器输入为带
<sos>的序列,目标为带<eos>的序列 - 推理时从
<sos>开始生成,直到出现<eos>或达到max_length
3. 修正损失计算,忽略填充token
在交叉熵损失中添加ignore_index=self.pad_id,避免模型因预测填充位置被惩罚:
loss = F.cross_entropy( input=logits.view(-1, logits.size(-1)), target=trg.view(-1), ignore_index=self.pad_id )
4. 修正Transformer层参数
确保注意力头数nhead能被hidden_size整除(如hidden_size=512时nhead=8),并开启batch_first=True适配输入形状:
encoder_layer = TransformerEncoderLayer( d_model=hidden_size, nhead=hidden_size//64, batch_first=True )
5. 改用自回归生成方式
推理阶段不要直接对编码器输出做argmax,而是逐token生成:
- 初始输入为
<sos>token - 每次将已生成的序列输入解码器,结合编码器输出预测下一个token
- 重复直到生成
<eos>或达到max_length
内容的提问来源于stack exchange,提问作者Luke Wardford

