首页 / PyTorch 入门教程 / 优化器:SGD、Adam 与参数更新

PyTorch 入门教程

优化器:SGD、Adam 与参数更新

本教程共 60 篇 · 第 14 篇 · 更新于 2026-08-17 · 约 3 分钟阅读

PyTorch优化器SGDAdamAdamW梯度下降

本节目标:理解优化器在训练中的角色,掌握 zero_grad、backward、step 三步曲,能分清 SGD、Adam、AdamW 该怎么选。

损失有了,然后呢

上一章我们拿到了损失,它告诉模型「你错得有多离谱」。可光知道错了不够,还得知道怎么改。

想象你在浓雾里下山,目标是山谷最低点。你看不清全貌,只能感受脚下的坡度:坡往哪边斜,就往哪边迈一步。这一步迈多大?迈大了可能滚下去,迈小了天黑也到不了。

优化器(optimizer)干的正是这件事:根据梯度更新参数。它内部实现的,是梯度下降(gradient descent)家族的各种算法。

三步曲:所有优化器的统一节奏

PyTorch 里无论哪个优化器,训练时的节奏都一样:

  1. optimizer.zero_grad():清空上一次的梯度
  2. loss.backward():反向传播,算出每个参数的梯度
  3. optimizer.step():按梯度更新参数
import torch
import torch.nn as nn
import torch.optim as optim

model = nn.Linear(784, 10)                        # 随便一个模型
optimizer = optim.Adam(model.parameters(), lr=0.001)

criterion = nn.CrossEntropyLoss()
inputs = torch.randn(32, 784)                     # 假装一批数据
labels = torch.randint(0, 10, (32,))

optimizer.zero_grad()       # 1. 清空梯度
outputs = model(inputs)     #    前向传播
loss = criterion(outputs, labels)
loss.backward()             # 2. 反向传播算梯度
optimizer.step()            # 3. 更新参数

创建优化器时,把模型的参数传进去:model.parameters()。这样优化器才知道要更新哪些张量。

Note

为什么必须先 zero_grad()?因为 PyTorch 的梯度是累加进 .grad 的。不清空的话,这一步的梯度会叠在上一步上面,等于用错的方向更新参数。

SGD:朴素的祖师爷

随机梯度下降(SGD,Stochastic Gradient Descent)是最古老的优化算法。每看一小批数据,就往坡度最陡的方向迈一步:

optimizer = optim.SGD(
    model.parameters(),
    lr=0.01,           # 学习率:步子大小
    momentum=0.9,      # 动量:带点惯性,下降更顺滑
    weight_decay=1e-4, # 权重衰减:抑制过拟合
)

它的特点是慢,但走得稳。加个动量(momentum)之后,就像下山时带着惯性,不容易在坑边乱晃。

图像分类这类经典任务上,SGD+动量至今仍是主流选择。收敛慢一点,但最终精度往往更高。

Adam:省心的默认款

Adam 是现在最常用的优化器。它聪明在给每个参数配了自适应学习率:变化频繁的参数步子小一点,变化少的步子大一点。

optimizer = optim.Adam(
    model.parameters(),
    lr=0.001,           # 官方推荐的默认值
    betas=(0.9, 0.999), # 一般不用动
)

对新手来说,Adam 最大的优点是省心:对学习率不敏感,lr=0.001 几乎到处通用。先跑通再说,选它基本没错。

AdamW:Adam 的升级版

Adam 有个小毛病:它的权重衰减(weight decay)实现方式不太对,和梯度更新搅在一起。AdamW 把两者拆开,正则化效果更干净。

optimizer = optim.AdamW(
    model.parameters(),
    lr=0.001,
    weight_decay=0.01,  # 通常比 Adam 设得大
)

Transformer 类模型的训练,现在几乎默认 AdamW。需要正则化时,优先 AdamW 而不是 Adam。

实用技巧

参数组:不同层不同学习率

迁移学习里常用:主干网络用小学习率慢慢调,新加的分类头用大学习率快学。

optimizer = optim.Adam([
    {'params': model.fc.parameters(), 'lr': 1e-3},          # 分类头:快学
    {'params': [p for n, p in model.named_parameters()
                if not n.startswith('fc')], 'lr': 1e-5},    # 主干:慢学
])

梯度裁剪:防爆炸

RNN 这类网络偶尔会出现梯度爆炸,一次更新把参数轰飞,损失直接变 NaN。裁剪一下就稳了:

loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

一句话:把过长的梯度向量压回 1.0 以内,方向不变,步子缩小。

梯度累积:显存不够时的变通

显存装不下大 batch?可以把好几个小 batch 的梯度攒起来,攒够了再更新一次。效果上接近大 batch 训练。

accumulation_steps = 4   # 攒 4 个小 batch 更新一次

optimizer.zero_grad()
for i, (inputs, labels) in enumerate(train_loader):
    loss = criterion(model(inputs), labels)
    loss = loss / accumulation_steps   # 平均一下,防止梯度过大
    loss.backward()

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()               # 攒够了才更新
        optimizer.zero_grad()

注意 zero_grad() 的位置变了:它跟着 step() 走,而不是每个 batch 都执行。

保存优化器状态

中断训练想接着练?光存模型不够,优化器的状态(比如 Adam 的动量)也得存:

torch.save({
    'model': model.state_dict(),
    'optimizer': optimizer.state_dict(),
}, 'checkpoint.pth')

少存 optimizer.state_dict(),恢复后 Adam 等于失忆重来,前几个 batch 会明显变差。

进阶:把 step 融进反向传播

标准流程里,梯度在 backward() 之后、step() 之前一直占着显存。显存紧张时,可以用 register_post_accumulate_grad_hook() 把优化器更新挂到每个参数上:梯度一算完立刻更新并释放,省下一整份梯度的显存。

代价是代码侵入性大,和调度器配合也麻烦。这是进阶玩法,初学了解即可。

怎么选

  • 想快速跑通、不想调参:Adam
  • 需要权重衰减、训练 Transformer:AdamW
  • 追求最终精度、有时间调参:SGD + momentum
  • 训练不稳定:降学习率 + 梯度裁剪
Tip

优化器没有银弹。新手一律从 Adam 起步,跑通了再折腾别的。别在出结果之前,就陷进「哪个优化器更好」的泥潭。