torch.optim 是 PyTorch 的优化器包:不负责求导,只消费 torch.autograd 算好的梯度、按既定算法更新 torch.nn 模块的参数,并配套 param groups、lr scheduler、梯度裁剪与断点续训机制。

torch.optim

职责边界

优化器与 autograd 的分工是理解本篇的前提:

  • autograd 负责求导:loss.backward() 沿动态图反传,把 写进每个参数的 .grad 属性
  • optimizer 负责更新:step() 读取 .grad,按算法(动量、自适应学习率等)计算增量并写回参数

优化器本身不感知 loss、不感知计算图,它只是一组按算法规则消费梯度的状态机。这一解耦使任何自定义损失都能接入同一套优化器。

训练闭环

for inputs, targets in loader:
    optimizer.zero_grad(set_to_none=True)  # 梯度置 None 而非清零,省一次写内存,也更省显存
    loss = criterion(model(inputs), targets)
    loss.backward()                        # 梯度累积进 param.grad
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)  # 可选:裁剪
    optimizer.step()                       # 按算法更新参数

三个动作各司其职:zero_grad() 清掉上一轮梯度(PyTorch 梯度默认累加,不清空会混入历史);backward() 属于 autograd;step() 才是优化器的工作。set_to_none=True 自 PyTorch 2.0 起已是默认值,值得显式写出以表明意图。

torch.cuda 与混合精度下的 AMP 训练需将 step() 包在 GradScaler 中,且梯度裁剪要在 scaler.unscale_(optimizer) 之后进行,否则裁的是缩放后的梯度。

优化器族系

优化器更新规则要点状态份数适用场景
SGD0学习率调度配合好的精调阶段;泛化常优于自适应方法
SGD + momentum累积速度 ,按 更新1(速度)CV 大 batch 训练(ResNet 系标配)
Adam一阶/二阶矩指数滑动平均 + 偏差修正2(、)快速实验、NLP 小模型、稀疏梯度的默认起点
AdamWAdam + 解耦权重衰减2(、)Transformer/LLM 事实标准
RMSprop只用二阶矩做梯度缩放1()RNN 时代遗产,现多被 Adam 取代

状态与参数同形状、驻留显存——Adam 族因此固定多占 2 倍参数量的显存,是 分布式训练总览 中显存墙估算与 ZeRO-1 分片的首要对象。

Adam 单步更新:

其中 、 分别是梯度及其平方的指数滑动平均(默认 ,,)。偏差修正使训练初期估计无偏,这是 Adam 冷启动稳定的关键。

AdamW 与 Adam 的权重衰减差异

Adam 把 weight decay 实现为 L2 正则:将 加进梯度后再经过 的自适应缩放。后果是衰减强度被梯度历史绑架——梯度大的参数衰减被压缩、梯度小的参数衰减被放大,违背了正则化的初衷。

AdamW 将两者解耦:梯度照常走自适应通路,权重衰减直接独立施加:

每个参数获得与梯度量级无关的均匀衰减。这对权重本应保持小量级的 Transformer 尤其重要:AdamW + weight decay 0.01–0.1 几乎是预训练的固定搭配,混用 Adam + L2 会带来可测的泛化损失。

param groups 与分组学习率

构造优化器时传入的其实是一组 dict,每个 dict 是一个 param group,拥有独立的超参数:

optim.AdamW([
    {"params": model.backbone.parameters(), "lr": 1e-5},   # 预训练骨干:小步
    {"params": model.head.parameters(), "lr": 1e-3},       # 新增头部:大步
    {"params": model.norm.parameters(), "weight_decay": 0.0},  # norm/bias 不衰减
])

微调、禁用 norm 层权重衰减、冻结/解冻分层训练都靠它实现。运行期改学习率的正确方式是 optimizer.param_groups[i]["lr"] = new_lr——optimizer 本身没有 set_lr 方法;更规范的做法是交给 scheduler。

lr scheduler

scheduler 持有 optimizer 引用,按 step 计数改写 param group 里的 lr:

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)
for epoch in range(epochs):
    for batch in loader:
        ...  # optimizer.step()
    scheduler.step()  # 每 epoch 调用;OneCycleLR 则每 batch 调用
Scheduler曲线典型用途
StepLR每 N 步乘 简单分段衰减
CosineAnnealingLR余弦退火至 eta_min长训练的主流选择
OneCycleLRwarmup 升至 max_lr 再退火快速收敛,自带 warmup,每 batch step
LinearLR + SequentialLR线性 warmup 拼接其他曲线Transformer 训练冷启动稳定

warmup 的动机:训练初期 Adam 二阶矩估计噪声大、大 lr 易震荡,先线性升 lr 再进入主曲线可稳定前期。规则:scheduler.step() 必须在 optimizer.step() 之后调用(PyTorch 1.1.0 起强制,顺序颠倒会告警且计数错位)。

梯度裁剪

clip_grad_norm_ 在 backward() 之后、step() 之前调用,按全局 L2 范数缩放梯度:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 若全局范数超过 1.0,所有梯度等比例缩放到 1.0;否则不动

它不改变梯度方向,只压制模长,是对梯度爆炸的廉价保险,RNN/Transformer 训练的常规操作。另有按元素截断的 clip_grad_val_,较少使用。

断点续训

优化器内部状态(Adam 的 、,SGD 的动量)不在模型权重里,resume 时若只加载 model.state_dict(),动量清零会导致 loss 突跳。标准做法是把三者一起存:

torch.save({
    "model": model.state_dict(),
    "optimizer": optimizer.state_dict(),      # 动量/二阶矩 + param_groups 超参数
    "scheduler": scheduler.state_dict(),      # 已进行的 step 计数
    "epoch": epoch,
}, "ckpt.pt")

optimizer.load_state_dict() 按 param 顺序索引匹配参数(不按名字),因此加载前不能改变模型参数的注册顺序。这些状态的体积是参数量的 2–3 倍,是 大模型检查点分片策略(ZeRO/FSDP)主要优化对象;保存格式细节见模型权重保存。

torch.optim 在 PyTorch 架构总览中位于 autograd 之上的一层:向下消费 .grad,向上被训练循环与 scheduler 驱动,是连接求导与收敛的最后一环。