You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Seq2Seq模型生成重复无意义问题及梯度异常排查求助

问题:基于图的Seq2Seq问题生成模型生成无意义重复Token,伴随数值异常

训练集与验证集损失均已收敛,但生成的问题(无论训练集还是测试集)均无意义且存在大量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
Loss Curves

模型结构

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 08:20:26