首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >训练时忽略优化器参数更新?梯度未更新的潜在原因与修复方案

训练时忽略优化器参数更新?梯度未更新的潜在原因与修复方案

原创
作者头像
九年义务漏网鲨鱼
发布2025-12-29 08:58:13
发布2025-12-29 08:58:13
2830
举报

训练时忽略优化器参数更新?梯度未更新的潜在原因与修复方案

场景:在训练神经网络时,常常遇到优化器不更新模型参数,尽管梯度计算正常且没有出现 NaN。最开始我们以为是数据问题或模型设计问题,但深入排查发现,问题其实出在梯度更新环节:比如没有清空梯度、模型参数没有正确传递到优化器、或优化器没有正确调用 step() 等。本文将通过复盘这一问题的根本原因,给出解决方案并提供可复现实验。


❓ Bug 现象

  • 训练过程中,损失看似逐步减少,但模型准确率、精度等其他指标却没有显著提高。
  • 优化器和模型参数都没有错误地初始化或被修改,但训练仍停滞不前。
  • 使用 torch.no_grad() 或其他方式确保梯度计算正常,但在 optimizer.step() 之后模型的参数并未更新。
  • 训练过程中打印梯度时,发现某些参数的梯度为 None0,无法成功反向传播。

📽️ 场景复现

保存为 optimizer_update_bug.py,CPU 可直接运行。

代码语言:javascript
复制
# optimizer_update_bug.py
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
​
torch.manual_seed(0)
​
class SimpleMLP(nn.Module):
    def __init__(self):
        super(SimpleMLP, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 128)
        self.fc2 = nn.Linear(128, 10)
        
    def forward(self, x):
        x = torch.flatten(x, 1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x
​
def make_loader(n=1024, batch_size=64):
    X = torch.randn(n, 28, 28)
    y = torch.randint(0, 10, (n,))
    dataset = torch.utils.data.TensorDataset(X, y)
    return torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
​
def train(model, dataloader, optimizer, epochs=10):
    for epoch in range(epochs):
        model.train()
        total_loss = 0
        correct = 0
        total = 0
        for data, target in dataloader:
            optimizer.zero_grad()  # 清空梯度
            output = model(data)  # 前向传播
            loss = F.cross_entropy(output, target)  # 计算损失
            loss.backward()  # 反向传播
            
            optimizer.step()  # 更新参数
            total_loss += loss.item()
​
            _, predicted = output.max(1)
            correct += (predicted == target).sum().item()
            total += target.size(0)
​
        avg_loss = total_loss / len(dataloader)
        accuracy = 100. * correct / total
        print(f"Epoch {epoch+1}, Loss: {avg_loss:.4f}, Accuracy: {accuracy:.2f}%")
​
def experiment():
    model = SimpleMLP()
    optimizer = optim.SGD(model.parameters(), lr=0.1)
    dataloader = make_loader()
​
    # 错误的优化器参数更新
    optimizer.zero_grad()  # 此处应该被注释,避免清空梯度后未进行更新
    for epoch in range(5):
        train(model, dataloader, optimizer)  # 训练 5 轮
    print("Training completed.")
​
if __name__ == "__main__":
    experiment()

你会发现:

  • 如果你不调用 optimizer.zero_grad() 或把其放在错误的位置,模型的梯度没有被清空,因此训练过程中模型的梯度一直在累加,导致参数更新不正常。
  • 如果没有正确调用 optimizer.step(),则会导致模型参数没有更新,训练看似进行了,但实际没有任何进展。

Debug 过程

  1. 检查梯度是否被清空 在每次 optimizer.step() 前确保调用 optimizer.zero_grad(),否则,梯度将累积并可能导致不正确的参数更新。
代码语言:javascript
复制
optimizer.zero_grad()  # 确保每次开始计算新的梯度时,梯度清零
  1. 检查优化器是否正确更新 确认在每次 backward() 之后调用 optimizer.step(),并确保 model.parameters() 被传递给优化器。
代码语言:javascript
复制
optimizer.step()  # 更新参数
  1. 确认模型参数参与训练 在训练过程中,可以通过打印模型参数的 .grad 属性来确保每个参数都有梯度。
代码语言:javascript
复制
for param in model.parameters():
    if param.grad is None:
        print(f"Parameter {param} has no gradient.")
  1. 检查学习率是否设置合理 如果梯度不更新,可能是学习率设置得过高或过低。尝试调整学习率并观察训练曲线的变化。
代码语言:javascript
复制
optimizer = optim.SGD(model.parameters(), lr=0.1)  # 调整学习率
  1. 确保没有手动重置梯度 除非明确需要,否则不要手动重置梯度。optimizer.zero_grad() 是训练循环中唯一需要的梯度清空操作。

代码修改

  1. 确保在每次更新前清空梯度
代码语言:javascript
复制
for epoch in range(epochs):
    model.train()
    for data, target in dataloader:
        optimizer.zero_grad()  # 确保每次梯度计算前清空旧梯度
        output = model(data)
        loss = F.cross_entropy(output, target)
        loss.backward()
        optimizer.step()  # 更新参数
  1. 修复模型参数未更新的 bug

确保每次反向传播后,调用 optimizer.step() 来更新模型参数,并在每轮训练前调用 optimizer.zero_grad() 清空梯度。

代码语言:javascript
复制
# 修复训练过程中的梯度更新问题
optimizer.zero_grad()  # 清空梯度
loss.backward()        # 反向传播
optimizer.step()       # 更新模型参数

监控与护栏

  1. 监控梯度是否被清零 每次训练前,确保使用 optimizer.zero_grad() 清空梯度。
代码语言:javascript
复制
for param in model.parameters():
    if param.grad is None:
        print(f"Parameter {param} has no gradient.")
  1. 在训练时打印优化器的学习率 打印优化器当前的学习率,以确保学习率没有变化太快,导致收敛不稳定。
代码语言:javascript
复制
for param_group in optimizer.param_groups:
    print("Current learning rate:", param_group['lr'])
  1. 监控显存使用情况 在训练过程中,监控显存的使用情况,确保每次更新后显存使用稳定,避免因为参数未更新或其他问题导致显存泄漏。
代码语言:javascript
复制
import torch
print(f"Memory allocated: {torch.cuda.memory_allocated()} bytes")

Q & A

  • optimizer.zero_grad() 为什么必须在每次训练之前调用? optimizer.zero_grad() 用于清空上一次的梯度,否则在每次反向传播时,梯度将累积,导致参数更新不准确。
  • optimizer.step() 什么时候应该调用? optimizer.step() 应该在每次 loss.backward() 之后调用,用来更新模型的参数。每次调用 step() 后,都需要清空梯度。
  • 为什么梯度会积累? 梯度是通过反向传播累积的,每次调用 loss.backward() 时,计算的梯度会被加到已有的梯度中。为了防止梯度的累积,需要调用 optimizer.zero_grad() 清空之前的梯度。
  • 如果没有调用 optimizer.step() 会发生什么? 如果没有调用 optimizer.step(),即使梯度计算正常,模型的参数也不会更新,导致训练没有实际进展。
  • 如何检查梯度是否为 None 可以通过访问每个模型参数的 .grad 属性来检查是否计算了梯度。如果为 None,则说明该参数没有计算梯度。

结语

在神经网络训练过程中,正确的梯度更新步骤是训练是否有效的关键。通过确保每次反向传播后调用 optimizer.step() 来更新模型参数,并在每次开始新一轮时调用 optimizer.zero_grad() 来清空旧梯度,我们可以确保训练过程的正确性与稳定性。通过本文提供的可复现实验和修复模板,你可以轻松排查和解决训练过程中由于梯度更新不当导致的潜在问题,确保模型能够稳定收敛并实现最佳性能

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 训练时忽略优化器参数更新?梯度未更新的潜在原因与修复方案
    • ❓ Bug 现象
    • 📽️ 场景复现
    • Debug 过程
    • 代码修改
    • 监控与护栏
    • Q & A
    • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档