欢迎光临
我们一直在努力

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 亲手走一步

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 震荡轨迹,沿着山谷前进极慢。

一张等高线俯视图

交互组件 1

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

这就是 Momentum 要解决的难题。


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

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

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

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

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

更新公式

v

t

=

β

v

t

1

+

g

t

v_t = \\beta v_{t-1} + g_t

vt=βvt1+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+βgt1+β2gt2+

动量

v

t

v_t

vt 其实是历史梯度的指数加权平均(指数移动平均)。

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

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

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

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=βvt1+L(wtαβvt1)

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αβvt1

代码:加一个 nesterov=True

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

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

左右两小图对比普通 Momentum 与 Nesterov


五、SGD API 参数详解

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

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

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

gtgt+λwt)。这在数学上不完全等价于"解耦权重衰减"(decoupled weight decay)。这个细微差别,正是后来 AdamW 出现的动机之一(第 54 篇会讲)。


核心要点小结

  • SGD 是最朴素的梯度下降:更新式

    w

    t

    +

    1

    =

    w

    t

    α

    g

    t

    w_{t+1} = w_t – \\alpha g_t

    wt+1=wtαgt,方向完全由当前梯度决定,不带任何记忆。

  • Momentum 给更新加了惯性:动量

    v

    t

    =

    β

    v

    t

    1

    +

    g

    t

    v_t = \\beta v_{t-1} + g_t

    vt=βvt1+gt 是历史梯度的指数加权平均;同向累加、反向抵消,于是减震荡、加速收敛。

  • 动量系数通常取 0.9:加了 momentum 后有效步长变大,可能需要相应调小学习率。
  • Nesterov 先按惯性冲一步再看梯度,修正更及时,实际差异不大。
  • SGD + Momentum 在图像分类上仍是首选,且对学习率比 Adam 敏感,入门从 lr=0.01 起。
  • 别把名字绕晕:PyTorch 里的 SGD 实际跑的是 Mini-batch SGD,名字里的 SGD 指的是"用(小批量)梯度更新"这族方法。

  • 动手思考题

  • 手算体会惯性: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=βvt1+gt 一步步算)

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

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

    optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0)

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

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

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


    📚 关于本系列

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

    🔗 在博客上阅读本文原版(含可交互组件、公式动画) 👉 SGD 与 Momentum 从零理解:给最朴素的优化器加上"惯性"

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

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

    赞(0)
    未经允许不得转载:171主机测评 » PyTorch:SGD 与 Momentum 从零理解:给最朴素的优化器加上“惯性“
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址