PyTorch中SGD 与 Momentum 从零理解:给最朴素的优化器加上“惯性“

SGD 与 Momentum 从零理解:给最朴素的优化器加上"惯性"

副标题:手把手带你搞懂参数到底怎么更新------为什么朴素 SGD 在峡谷地形里反复横跳,而一点点惯性就能让它平滑起飞,以及 Nesterov 是如何"往前看一步"的

你有没有过这种体验:模型写完了,代码能跑,可 loss 曲线像心电图一样上下乱蹦,半天不收敛?你可能会怀疑数据、怀疑网络结构,但真正"踩刹车又踩油门"的,往往是一个你以为很简单的东西------优化器怎么更新参数

第 12 篇你学过梯度下降的概念:算梯度、沿梯度反方向走一步。但"走一步"到底怎么走?步子多大?要不要带点"惯性"?这些细节,正是 SGD 和 Momentum 要回答的。

它们是一切优化器的基石。后面你要学的 Adam、AdamW,本质上都是站在这两个"老祖宗"肩膀上的------搞不懂它们,后面再高级的优化器都是空中楼阁。这一课,我们就把"走一步"这件小事彻底讲透,并用一个滚下山坡的小球贯穿全程。


一、先对齐:我们已经站在哪一步

把训练流程拆开看,其实是一条流水线:

  • 第 51 篇 (参数初始化):给参数一个起点 w 0 w_0 w0;
  • 第 45 篇backward):算出当前损失对每个参数的梯度 ∇ L \nabla L ∇L;
  • 本篇(优化器):拿到梯度,决定"下一步参数往哪挪、挪多少"。

第 12 篇给的梯度下降更新式 θ = θ − α ∇ L \theta = \theta - \alpha \nabla L θ=θ−α∇L 就是 SGD 的核心。换句话说------SGD 只是把这句公式原封不动翻译成代码

💡 一句话先记住:SGD 是最朴素的梯度下降------拿到梯度、走一步、仅此而已。Momentum 在此基础上加了"惯性":把之前的更新方向也累加进来,于是震荡被抵消、收敛被加速。


二、SGD:最朴素的更新,也是最诚实的更新

更新公式

w t + 1 = w t − α ⋅ g t w_{t+1} = w_t - \alpha \cdot g_t wt+1=wt−α⋅gt

  • w t w_t wt:当前参数(第 t t t 步);
  • α \alpha α:学习率(learning rate,PyTorch 里叫 lr);
  • g t = ∇ L ( w t ) g_t = \nabla L(w_t) gt=∇L(wt):当前这一步算出来的梯度。

每次更新就是"参数减去 学习率乘以梯度"。方向完全由"此刻"的梯度决定,和历史毫无关系------这是个关键特征,后面讲 Momentum 你会更理解它的代价。

代码:用 PyTorch 亲手走一步

python 复制代码
import torch

# 一个需要梯度的标量参数 w,初始为 1.0
w = torch.tensor([1.0], requires_grad=True)

# 优化器:最朴素的 SGD,学习率 0.1
optimizer = torch.optim.SGD([w], lr=0.1)

# 模拟一步训练:构造一个目标------让 w 接近 2
# 当 w=2 时,(w*5 - 10)^2 = (10-10)^2 = 0,损失最小
loss = (w * 5 - 10).pow(2)
loss.backward()                 # 反向传播,算出 w.grad
print(f"梯度: {w.grad}")       # 梯度: tensor([10.])  ← dL/dw = 2*5*(w*5-10) = 10
optimizer.step()               # 执行一步更新
print(f"更新后: {w}")          # 更新后: tensor([0.])
# 验证:w = 1.0 - 0.1 * 10.0 = 0.0  ✓

注意这三步的"仪式感":backward() 算梯度 → step() 更新参数。这是 PyTorch 的黄金组合,后面所有优化器都一样。

痛点来了:SGD 在"峡谷"里反复横跳

如果损失函数像一个狭长的山谷------一个方向很陡、一个方向很平(数学上叫病态条件 / ill-conditioned),SGD 就会出问题:在陡的方向上梯度巨大,它猛地一冲撞到对面的壁,再猛地弹回来;而在平的方向上梯度很小,它几乎挪不动。

结果就是:红色虚线一样的 zig-zag 震荡轨迹,沿着山谷前进极慢。

🎮 👉 点击在线体验此交互组件

这就是 Momentum 要解决的难题。


三、Momentum:给梯度加"惯性"

物理直觉:滚下山坡的小球

想象一个小球从山坡上滚下来:

  • SGD 像什么? 像一个每步都只看"脚下坡度"、迈完步就忘掉方向的人。遇到两侧山壁,他左一脚右一脚地弹,毫无记忆。
  • Momentum 像什么? 像真正有质量的小球------它一旦朝某个方向滚起来,就有了"惯性"。之前的速度会累加进下一步,所以左右摆动的力互相抵消,而向下(前进)的力被不断放大。

一句话:Momentum 用历史梯度给更新方向"攒劲",抵消震荡、加速前进。

更新公式

v t = β v t − 1 + g t v_t = \beta v_{t-1} + g_t vt=βvt−1+gt

w t + 1 = w t − α ⋅ v t w_{t+1} = w_t - \alpha \cdot v_t wt+1=wt−α⋅vt

  • v t v_t vt:动量项,可以理解为"累积起来的速度";
  • β \beta β:动量系数(momentum),常用 0.9,表示历史速度保留 90%;
  • g t g_t gt:当前梯度。

把第一式展开看就明白了:

v t = g t + β g t − 1 + β 2 g t − 2 + ⋯ v_t = g_t + \beta g_{t-1} + \beta^2 g_{t-2} + \cdots vt=gt+βgt−1+β2gt−2+⋯

动量 v t v_t vt 其实是历史梯度的指数加权平均(指数移动平均)

  • 如果连续几步梯度方向一致(比如一路下坡),它们同号相加,动量越攒越大 → 走得越来越快;
  • 如果梯度来回摆(比如峡谷震荡),正负交替,加权平均后互相抵消 → 震荡被压下去。

这就是 Momentum "减震荡 + 加速"的全部秘密。

代码:只多一个参数 momentum=0.9

python 复制代码
import torch
import torch.nn as nn

model = nn.Sequential(nn.Linear(10, 1))

# 没有 momentum:纯朴素 SGD
optimizer_plain = torch.optim.SGD(model.parameters(), lr=0.01)

# 有 momentum:只多了 momentum=0.9 这一个参数
optimizer_momentum = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9    # ← 惯性系数,通常取 0.9
)

就加了 momentum=0.9,行为却天差地别。

效果对比

特性 无 Momentum 有 Momentum
峡谷里是否震荡 明显 zig-zag 大幅减少
收敛速度
需要调的参数 只有 lr 多一个 beta(momentum)

⚠️ 易混淆:SGD 与 Mini-batch SGD

这里必须点破一个常见误会:你平时在 PyTorch 里写的 torch.optim.SGD,实际跑的是 Mini-batch SGD(小批量随机梯度下降) ------每一步的 g t g_t gt 是用一个 batch 的数据算出来的,不是全量数据的精确梯度。

  • 纯 SGD / 全量梯度下降:每步用整个训练集算梯度,方向最准但太慢;
  • Mini-batch SGD(工业界默认):每步用一个 batch,有噪声但快,这也是震荡的另一来源;
  • SGD 优化器:PyTorch 里这个"SGD"名字,指的是"用(小批量)梯度做更新"这族方法,Momentum 只是给它加了惯性。

记住:论文里说"我们用 SGD 训练",几乎都指 Mini-batch SGD。别被名字绕晕。


四、Nesterov Momentum:聪明一点,"先往前看一步"

普通 Momentum 是"先看脚下坡度 g t g_t gt,再叠加历史惯性"。Nesterov 反过来想:既然你迟早要按惯性冲出去,不如先按惯性冲一步,在冲到的位置再看坡度------这样梯度是在"未来点"上算的,相当于"预判"了惯性,修正更及时。

更新公式

v t = β v t − 1 + ∇ L ( w t − α β v t − 1 ) v_t = \beta v_{t-1} + \nabla L\big(w_t - \alpha \beta v_{t-1}\big) vt=βvt−1+∇L(wt−αβvt−1)

w t + 1 = w t − α ⋅ v t w_{t+1} = w_t - \alpha \cdot v_t wt+1=wt−α⋅vt

注意梯度的求值点变了:不是在当前 w t w_t wt,而是在"按惯性先走一步后的位置" w t − α β v t − 1 w_t - \alpha \beta v_{t-1} wt−αβvt−1。

代码:加一个 nesterov=True

python 复制代码
optimizer = torch.optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    nesterov=True    # ← 启用 Nesterov 动量
)

理论上 Nesterov 比普通 Momentum 收敛更稳更快,但实际差异通常不大。知道有这个选项、会用就行,不必纠结。


五、SGD API 参数详解

torch.optim.SGD 的参数一次看清,以后调参心里有底:

python 复制代码
torch.optim.SGD(
    params,              # 模型参数(通常是 model.parameters())
    lr=0.01,             # 学习率:步子大小
    momentum=0,          # 动量系数,0.9 最常用;0 表示纯朴素 SGD
    weight_decay=0,      # 权重衰减(L2 正则化),防过拟合常用 1e-4
    dampening=0,         # 动量阻尼,一般不动
    nesterov=False       # 是否启用 Nesterov 动量
)
参数 常用值 作用
lr 0.01 / 0.1 步子大小,最敏感的参数
momentum 0.9 惯性大小
weight_decay 1e-4 限制参数过大,防过拟合
nesterov True / False 是否"往前看一步"

六、适用场景:别迷信 Adam

很多人默认"Adam 一定比 SGD 好",这是误区。在图像分类 这种任务上,SGD + Momentum + 精心设计的学习率策略(如 step decay、cosine annealing),往往能调出比 Adam 更好的最终精度,泛化性也常更强------代价是需要更多调参经验。

场景 推荐
CNN 图像分类(ResNet 等) SGD + Momentum,调好学习率可超过 Adam
入门理解优化器原理 纯 SGD,最直观
快速跑通实验 / Transformer 微调 Adam / AdamW 更快(后续篇章讲)

七、三个高频错误(收藏备用)

错误 1:学习率设太大,loss 直接变 NaN

python 复制代码
optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
# 步子太大,一下冲出合理范围,loss 爆炸成 NaN

SGD 对学习率比 Adam 敏感得多 。入门从 lr=0.01 开始试,图像任务常用 0.1 起步再配合衰减。

错误 2:加了 momentum 却忘了降学习率

加了 momentum 后,有效步长会变 (动量在累加速度)。如果之前 lr=0.01 无动量能收敛,加了 momentum=0.9 后,惯性可能把步子推过头,需要把 lr 降到比如 0.005

错误 3:混淆 weight_decay 和"解耦权重衰减"

PyTorch 的 SGD 里,weight_decay先加到梯度上、再算动量 的(即 g t ← g t + λ w t g_t \leftarrow g_t + \lambda w_t gt←gt+λwt)。这在数学上不完全等价于 "解耦权重衰减"(decoupled weight decay)。这个细微差别,正是后来 AdamW 出现的动机之一(第 54 篇会讲)。


核心要点小结

  1. SGD 是最朴素的梯度下降 :更新式 w t + 1 = w t − α g t w_{t+1} = w_t - \alpha g_t wt+1=wt−αgt,方向完全由当前梯度决定,不带任何记忆。
  2. Momentum 给更新加了惯性 :动量 v t = β v t − 1 + g t v_t = \beta v_{t-1} + g_t vt=βvt−1+gt 是历史梯度的指数加权平均;同向累加、反向抵消,于是减震荡、加速收敛。
  3. 动量系数通常取 0.9:加了 momentum 后有效步长变大,可能需要相应调小学习率。
  4. Nesterov 先按惯性冲一步再看梯度,修正更及时,实际差异不大。
  5. SGD + Momentum 在图像分类上仍是首选 ,且对学习率比 Adam 敏感,入门从 lr=0.01 起。
  6. 别把名字绕晕:PyTorch 里的 SGD 实际跑的是 Mini-batch SGD,名字里的 SGD 指的是"用(小批量)梯度更新"这族方法。

动手思考题

  1. 手算体会惯性 :SGD 没加 Momentum 时,连续 3 步梯度分别是 +1、-1、+1,最终更新方向是什么?加了 Momentum( β = 0.9 \beta=0.9 β=0.9)后,第三步的动量 v 3 v_3 v3 是多少?(提示:展开 v t = β v t − 1 + g t v_t = \beta v_{t-1} + g_t vt=βvt−1+gt 一步步算)

  2. 用一句话解释:为什么 Momentum 能减少峡谷里的震荡?

  3. 排错实战:下面这段训练 ResNet 的配置,loss 降不下去,可能是什么原因?

    python 复制代码
    optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0)

    (提示:图像任务常用多大 lr?momentum=0 意味着什么?)

  4. 改代码实验 :把上面第二节能跑的 SGD 例子,加上 momentum=0.9,改一下目标函数(比如换成 (w - 3).pow(2) 让目标变成 3),观察加了惯性后参数收敛路径有什么不同。把你跑出来的 w 变化序列贴到评论区,我们一起看 💬

下一篇我们换一个角度------不调方向,改调"步子大小":自适应学习率的起点 AdaGrad 和 RMSProp 怎么让每个参数拥有"自己的学习率"。


📚 关于本系列

本文是 「AI 学习路线 · 阶段四:PyTorch 深度学习基础」 系列中的一篇。所有文章在我的个人博客上都有 可交互动画 + 完整学习路线 版本,建议配合食用 👇

🔗 在博客上阅读本文原版(含可交互组件、公式动画)

👉 SGD 与 Momentum 从零理解:给最朴素的优化器加上"惯性"

🗺️ 查看完整 AI 学习路线 (从 0 到进阶,持续更新)

👉 bestsdz.xyz

觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我更新的最大动力!

相关推荐
WangChenGe1 小时前
Windows 10/11 上 Pyenv-win 完整安装教程
python
额恩661 小时前
自然语言处理 NLP 入门与语言学基础
人工智能·自然语言处理
paopaokaka_luck1 小时前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts
用户298698530141 小时前
PDF 转图片?三种方案,覆盖全平台与自动化场景
人工智能·后端
Coffeeee1 小时前
天天 AI Coding 的你,如果出去面试,你的竞争力是什么?
人工智能·程序员·ai编程
小飞猪。。1 小时前
笔记十八:大模型 RLHF 系统工程实战笔记
人工智能·笔记
钱六两1 小时前
Spring AI 使用 MCP 客户端(调用高德 MCP)
java·人工智能·spring
盈飞无限1 小时前
AI智能SPC重构制程管控逻辑,打造质量硬核底座
大数据·人工智能·重构
拿铁铁1 小时前
平板坡口机换碳刷时间参考手册
python·电脑