Seq2Seq模型生成重复无意义问题及梯度异常排查求助
训练集与验证集损失均已收敛,但生成的问题(无论训练集还是测试集)均无意义且存在大量token重复。已尝试调整多种超参数并核对输入输出张量,发现以下异常:
异常现象1:Decoder输出向量出现极高值
第一个epoch进行到一半时,模型输出的out向量开始出现极大值,示例:
Out: tensor([[ 0.2016, 103.7198, 90.4739, ..., 0.9419, 0.4810, -0.2869]]
梯度裁剪实现
猜测是梯度消失/爆炸问题,使用了梯度裁剪,但效果存疑,代码如下:
for p in model_params: p.register_hook(lambda grad: torch.clamp( grad, -clip_value, clip_value))
训练曲线
10K样本,batch size=128,lr=0.065,lr_decay=0.99,dropout=0.25
模型结构
Encoder(GNN)
学习输入图(3-4个节点和边)的节点嵌入,池化得到图嵌入作为Decoder初始隐藏状态:
class QuestionGraphGNN(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels, dropout, aggr='mean'): super(QuestionGraphGNN, self).__init__() nn1 = torch.nn.Sequential( torch.nn.Linear(in_channels, hidden_channels), torch.nn.ReLU(), torch.nn.Linear(hidden_channels, in_channels * hidden_channels)) self.conv = NNConv(in_channels, hidden_channels, nn1, aggr=aggr) self.lin = nn.Linear(hidden_channels, out_channels) self.dropout = dropout def forward(self, x, edge_index, edge_attr): x = self.conv(x, edge_index, edge_attr) x = F.leaky_relu(x) x = F.dropout(x, p=self.dropout) x = self.lin(x) return x
Encoder维度:in_channels=301,hidden_channels=256,out_channels=301
Decoder(RNN)
out向量在forward中打印,代码如下:
class DecoderRNN(nn.Module): def __init__(self, embedding_size, output_size, dropout): super(DecoderRNN, self).__init__() self.output_size = output_size self.dropout = dropout self.embedding = nn.Embedding(output_size, embedding_size) self.gru1 = nn.GRU(embedding_size, embedding_size) self.gru2 = nn.GRU(embedding_size, embedding_size) self.gru3 = nn.GRU(embedding_size, embedding_size) self.out = nn.Linear(embedding_size, output_size) self.logsoftmax = nn.LogSoftmax(dim=1) def forward(self, inp, hidden): output = self.embedding(inp).view(1, 1, -1) output = F.leaky_relu(output) output = F.dropout(output, p=self.dropout) output, hidden = self.gru1(output, hidden) output = F.dropout(output, p=self.dropout) output, hidden = self.gru2(output, hidden) output, hidden = self.gru3(output, hidden) out = self.out(output[0]) print("Out: ", out) output = self.logsoftmax(out) return output, hidden
Decoder维度:embedding_size=301,output_size=约1.2K(词汇表大小)
训练配置
- 损失函数:PyTorch
NLLLoss() - 优化器:SGD
- 训练循环采用top-k采样,解码函数:
def translate(self, data): # Get node embeddings of the input graph h = self.encoder(data.node_embeddings, data.edge_index, data.edge_embeddings) # Pool node embeddings into single graph embedding graph_embedding = self.get_graph_embeddings(h, data.graph_dict) # Pass graph embedding through decoder self.encoder.eval() self.decoder.eval() with torch.no_grad(): # Initialize first input and hidden state decoder_input = torch.tensor( [[self.vocab.SOS['idx']]], device=self.device) decoder_hidden = graph_embedding.view(1, 1, -1) decoder_tokens = [] for di in range(self.dec_max_length): decoder_output, decoder_hidden = self.decoder( decoder_input, decoder_hidden) topv, topi = decoder_output.data.topk(1) if topi.item() == self.vocab.EOS['idx']: break else: word = self.vocab.index2word[topi.item()] word = word.upper( ) if word == self.vocab.UNK['token'].lower() else word decoder_tokens.append(word) decoder_input = topi.squeeze().detach() return decoder_tokens
异常现象2:GRU输出饱和
最后一层GRU(self.gru3)的输出向量中大量值接近1或-1,示例:
tensor([[[-0.9984, -0.9950, 1.0000, -0.9889, -1.0000, -0.9770, -0.0299, -0.9996, 0.9996, 1.0000, -0.0176, -0.5815, -0.9998, -0.0265, -0.1471, 0.9998, -1.0000, -0.2356, 0.9964, 0.9936, -0.9998, 0.0652, -0.9999, 0.9999, -1.0000, -0.9998, -0.9999, 0.9998, -1.0000, -0.9997, 0.9850, 0.9994, -0.9998, -1.0000, -1.0000, 0.9977, 0.9015, -0.9982, 1.0000, 0.9980, -1.0000, 0.9859, 0.6670, 0.9998, 0.3827, 0.9999, 0.9953, -0.9989, 0.1287, 1.0000, 1.0000, -1.0000, 0.9778, 1.0000, 1.0000, -0.9907, ...
1. 梯度裁剪失效的核心原因
当前用的register_hook方式存在陷阱:lambda函数在循环中会共享变量,导致所有参数的hook都使用最后一次循环的clip_value,实际未生效。改用PyTorch内置的裁剪方法更可靠:
loss.backward() # 按值裁剪 torch.nn.utils.clip_grad_value_(model_params, clip_value) # 或按范数裁剪(更常用) # torch.nn.utils.clip_grad_norm_(model_params, max_norm=1.0) optimizer.step()
2. GRU输出饱和与数值爆炸
GRU输出大量接近±1的值,说明模型出现激活饱和,加上Decoder最后一层线性层输出极值,直接导致LogSoftmax后概率集中在少数token,生成重复无意义内容。解决方向:
- 降低学习率:当前0.065的SGD学习率过高,Seq2Seq模型的SGD初始LR建议降到0.001~0.01区间,或改用Adam优化器(自适应学习率更稳定)。
- 减少GRU层数:3层GRU叠加容易导致梯度消失/爆炸,先尝试1-2层,验证是否还会出现饱和。
- 调整Dropout位置:GRU之间的Dropout会加剧梯度消失,可去掉GRU之间的Dropout,仅保留Embedding后的一次。
3. 损失收敛但生成无效的原因
损失收敛是因为训练时靠教师强制(Teacher Forcing)拟合标签,但Decoder自主生成能力极差,本质是训练-推断差异加上模型数值不稳定。额外优化点:
- 加入注意力机制:当前Decoder仅靠初始图嵌入,无法捕捉输入图的细节,导致生成脱离输入,需给Decoder添加注意力模块关联Encoder的节点/图嵌入。
- 调整采样策略:推断时top-k=1容易陷入循环,可尝试k=5~10,或加入温度系数(temperature>1)软化概率分布。
- 检查池化层:确认
get_graph_embeddings的池化逻辑(均值/最大值等)是否合理,池化后的嵌入需处于合理数值范围,避免Decoder初始状态异常。
4. 其他细节修正
- LogSoftmax维度校验:训练时如果是batch输入,需确认
logsoftmax的维度与NLLLoss的输入要求匹配(NLLLoss默认输入为[N, C],target为[N])。 - 权重初始化:手动初始化线性层权重为小范围值(如Xavier初始化),避免初始阶段梯度爆炸。
内容的提问来源于stack exchange,提问作者DustyAvocado

