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

PyTorch RNN训练:手动参数更新与optim.step()差异及正确用法

RNN分类任务optimizer.step()不收敛问题排查

问题背景

在复现PyTorch字符级RNN姓名分类教程时,原教程采用手写参数更新逻辑可正常收敛,改用optim.Adam配合optimizer.step()实现参数更新时,模型完全没有学习效果,损失无下降趋势,异常训练结果参考:损失不下降截图。
教程给出的基础RNN模型定义如下:

import torch.nn as nn

class RNN(nn.Module):
  def __init__(self, input_size, hidden_size, output_size):
    super(RNN, self).__init__()
    self.hidden_size = hidden_size
    
    self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
    self.i2o = nn.Linear(input_size + hidden_size, output_size)
    self.softmax = nn.LogSoftmax(dim=1)
  
  def forward(self, input, hidden):
    combined = torch.cat((input, hidden), 1)
    hidden = self.i2h(combined)
    output = self.i2o(combined)
    output = self.softmax(output)
    return output, hidden

  def initHidden(self):
    return torch.zeros(1, self.hidden_size)

n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
learning_rate = 0.005
criterion = nn.NLLLoss()

原教程可正常收敛的手写更新训练逻辑:

def train(category_tensor, name_tensor):
  hidden = rnn.initHidden()
  rnn.zero_grad()

  for i in range(name_tensor.size()[0]):
    output, hidden = rnn(name_tensor[i], hidden)
  
  loss = criterion(output, category_tensor)
  loss.backward()

  # 手写Vanilla SGD更新
  for p in rnn.parameters():
    p.data.add_(p.grad.data, alpha=-learning_rate)

  return output, loss.item()

改写后不收敛的optimizer实现:

optimizer = optim.Adam(rnn.parameters(), lr = learning_rate)  

def train(category_tensor, name_tensor):
  hidden = rnn.initHidden()
  rnn.zero_grad()

  for i in range(name_tensor.size()[0]):
    output, hidden = rnn(name_tensor[i], hidden)
  
  loss = criterion(output, category_tensor)
  
  optimizer.zero_grad()
  loss.backward()
  optimizer.step()

  return output, loss.item()

一、两种更新方式的核心差异

两种实现的差异可分为两点,其中代码逻辑错误是不收敛的核心原因,和优化器本身无关:

  • 优化算法本质不同
    原教程手写实现的是最朴素的Vanilla SGD:无动量、无自适应学习率,直接拿当前步计算出的梯度乘以负学习率,直接覆盖更新参数值,没有任何内部状态,更新逻辑完全确定。
    改写版本使用的Adam是自适应优化器,内部会为每个参数维护梯度一阶动量、二阶动量两个滑动平均状态,更新时会对梯度做偏差修正,不同参数的实际更新步长会自适应调整,本身收敛特性和Vanilla SGD有区别,但这不是不收敛的诱因。
  • 梯度清零逻辑混乱冗余
    改写代码先后两次执行梯度清零:第一次在前向传播前调用rnn.zero_grad(),第二次在loss计算完成、反向传播前调用optimizer.zero_grad()。虽然语法上第二次清零后才做反向传播,梯度值本身计算没有错误,但冗余的清零操作容易和优化器状态更新冲突,加上直接套用了Vanilla SGD的学习率,直接导致Adam更新失效。

二、optimizer.step()正确实现的调整点

只需要做3处简单修改即可正常收敛,收敛速度通常会快于原手写SGD:

  • 统一梯度清零逻辑,删除冗余调用
    删掉前向传播前的rnn.zero_grad(),将optimizer.zero_grad()统一放在train函数最开头、所有前向操作之前,避免逻辑冲突。梯度清零的核心作用是防止上一轮迭代的梯度累加到当前步,只要在当前轮反向传播前执行即可,放在最前是最稳妥的工业界写法。
  • 调整Adam适配的学习率
    原代码的learning_rate=0.005是针对Vanilla SGD调试的,Adam对学习率敏感度远高于普通SGD,将学习率调整到1e-3 ~ 3e-3区间即可,不要直接套用SGD的学习率参数。
  • 隐状态逻辑不需要修改:当前代码每个训练样本重新初始化零隐状态,符合字符级分类任务的要求,不需要对hidden做额外detach操作,每轮迭代的计算图会在迭代结束后自动释放。

修正后的可运行训练代码如下:

import torch.optim as optim

# 调整Adam适配的学习率
learning_rate = 0.001
optimizer = optim.Adam(rnn.parameters(), lr=learning_rate)
criterion = nn.NLLLoss()

def train(category_tensor, name_tensor):
  # 统一在最开头清零梯度
  optimizer.zero_grad()

  hidden = rnn.initHidden()
  for i in range(name_tensor.size()[0]):
    output, hidden = rnn(name_tensor[i], hidden)
  
  loss = criterion(output, category_tensor)
  loss.backward()
  optimizer.step()

  return output, loss.item()

内容的提问来源于stack exchange,提问作者UiJin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 14:06:26