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 篇会讲)。
核心要点小结
- 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=βvt−1+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=βvt−1+gt 一步步算)
-
用一句话解释:为什么 Momentum 能减少峡谷里的震荡?
-
排错实战:下面这段训练 ResNet 的配置,loss 降不下去,可能是什么原因?
pythonoptimizer = 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 到进阶,持续更新)
觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我更新的最大动力!