深度学习里的优化器种类繁多,公式看着一个比一个复杂。但如果把它们放在同一个例子里逐步计算,你会发现它们其实是一条清晰的演进线:每一个新优化器,都是为了修复前一个的某个具体缺陷。
本文用同一个最简单的例子,把 SGD、Momentum、AdaGrad、RMSProp、Adam 五个优化器从头到尾走一遍,让你看清它们到底在解决什么问题、彼此有什么区别。
目录
- 统一的例子设定
- 梯度是怎么算出来的
- 1. SGD:最朴素的梯度下降
- 2. Momentum:给梯度下降加惯性
- 3. AdaGrad:给每个参数配专属学习率
- 4. RMSProp:修好 AdaGrad 的学习率衰竭
- 5. Adam:Momentum + RMSProp 合体
- 五大优化器全景对比
- 附:多层网络的梯度是怎么算的
统一的例子设定
为了让五个优化器可以直接对照,全文使用同一个极简任务。
任务:学习一条过原点的直线 y = w·x,只有一个参数 w。
训练集(真实规律是 y = 2x,我们希望模型学出 w ≈ 2):
| 样本 | x | 真实 y |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 4 |
| 3 | 3 | 6 |
损失函数:均方误差 MSE
初始值:w = 0。学习率等超参数在每个优化器里单独给出。
梯度是怎么算出来的
在进入优化器之前,先把梯度公式推清楚,后面所有计算都要用到它。
对单个样本,损失是 。用链式法则对 w 求导,设中间变量 :
直觉上这个式子拆成三部分:
- 误差
(wxᵢ − yᵢ):预测减真实,误差越大梯度越大,参数要调得越猛;误差为 0 时梯度为 0。 - 乘输入
xᵢ:输入越大的样本,w的微小变化对预测影响越大,对梯度的贡献也越大。
对全部样本求平均,得到本例的批量梯度公式(代入三个样本并化简):
这个 g(w) = 9.33·(w − 2) 会贯穿全文:w 离目标 2 越远,梯度越大。
顺带厘清 BGD / SGD / Mini-batch:三者的唯一区别是每次更新用多少数据算梯度。BGD 用全部样本、一个 epoch 更新 1 次,最稳但最慢;SGD 每个样本更新一次、抖动大但能跳出局部最优;Mini-batch 是两者折中,也是实践主流。本文为聚焦优化器本身,统一用全部样本算梯度(BGD 方式)。
1. SGD:最朴素的梯度下降
逻辑
每一步只听当前梯度的,走完就忘:
存在的问题
在"狭长山谷"型的损失曲面上(一个方向很陡、一个方向很平缓),SGD 会在陡的方向来回震荡,在平缓但正确的方向上磨蹭,收敛慢。这正是后续优化器要解决的核心痛点。
逐步计算(lr = 0.01)
| 步 | w(更新前) | 梯度 g | w(更新后) |
|---|---|---|---|
| 1 | 0 | −18.67 | 0.187 |
| 2 | 0.187 | — | 0.359 |
| 3 | 0.359 | — | 0.517 |
| 4 | 0.517 | — | 0.662 |
四步走到 0.662,平稳但慢。
PyTorch
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
注意:PyTorch 的
torch.optim.SGD实际是 mini-batch,batch 大小由 DataLoader 决定,叫 SGD 只是习惯。
2. Momentum:给梯度下降加惯性
逻辑
更新时不只看当前梯度,还累积之前几步的方向——像给下山的小球加了惯性。引入"速度"变量 v:
其中 β 通常取 0.9:这次的速度 = 保留 90% 的上次速度 + 这次的新梯度。
关键直觉:一致则加速,震荡则抵消
- 方向一致的维度:每步梯度同号,累加起来越滚越快,加速前进。
- 来回震荡的维度:这步正、下步负,累加时正负抵消,抖动被抹平。
正好治了 SGD 的两个毛病。
逐步计算(lr = 0.01,β = 0.9,v₀ = 0)
| 步 | w | 梯度 g | 速度 v | w(更新后) |
|---|---|---|---|---|
| 1 | 0 | −18.67 | −18.67 | 0.187 |
| 2 | 0.187 | −16.92 | −33.72 | 0.524 |
| 3 | 0.524 | −13.77 | −44.12 | 0.965 |
| 4 | 0.965 | −9.66 | −49.37 | 1.459 |
注意第 2 步:速度 v₂ = −33.72 比当前梯度 −16.92 大了近一倍——因为方向一致,历史速度被累加进来了。
对比 SGD(同样 4 步)
| 步 | SGD 的 w | Momentum 的 w |
|---|---|---|
| 1 | 0.187 | 0.187 |
| 2 | 0.359 | 0.524 |
| 3 | 0.517 | 0.965 |
| 4 | 0.662 | 1.459 |
方向一致时,Momentum 明显冲得更快。
冲过头会自动纠正
Momentum 加速快,代价是可能冲过目标。一旦 w 越过 2、梯度变正号,速度 v 里存的负惯性会被新正梯度先抵消再拉回,表现为在目标附近摆动几下、摆幅递减、最后停在 2,像滚进碗底的球。
PyTorch
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
3. AdaGrad:给每个参数配专属学习率
逻辑
Momentum 管的是方向;AdaGrad 管的是步长——给每个参数配一个专属学习率,某参数历史梯度越大,学习率就自动调得越小。
累积历史梯度的平方:
G 越大 → 分母越大 → 有效学习率越小。等于"更新得越多,越往后越谨慎"。
解决的问题
模型里有的参数频繁更新(梯度大)、有的很少更新(梯度小),用同一个学习率没法同时照顾。AdaGrad 让"动得多的减速、动得少的保持大步",尤其利于稀疏特征。
逐步计算(lr = 0.5,G₀ = 0)
| 步 | w | 梯度 g | G(累积) | 有效学习率 | w(更新后) |
|---|---|---|---|---|---|
| 1 | 0 | −18.67 | 348.5 | 0.0268 | 0.500 |
| 2 | 0.500 | −14.00 | 544.5 | 0.0214 | 0.800 |
| 3 | 0.800 | −11.20 | 669.9 | 0.0193 | 1.016 |
| 4 | 1.016 | −9.18 | 754.2 | 0.0182 | 1.183 |
有效学习率单调递减(0.0268 → 0.0182)——这是 AdaGrad 最鲜明的特征。
致命缺点
G 只增不减、永不衰减,学习率会一路降到接近 0。在真实的长时间训练中,模型还没学好就"走不动了"。这就是下一个优化器要修的问题。
PyTorch
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.5)
4. RMSProp:修好 AdaGrad 的学习率衰竭
逻辑
AdaGrad 的病根是"无限累加"。RMSProp 的解法:别累加所有历史,只关注最近一段——旧的逐渐遗忘。 把累加换成指数移动平均(EMA):
γ 通常取 0.9:保留 90% 旧累积(很久以前的按 0.9、0.81、0.729… 指数衰减被遗忘),掺入 10% 当前梯度平方。于是 G 反映"最近梯度大概多大",会随梯度变小而变小,不会无限膨胀。
逐步计算(lr = 0.5,γ = 0.9,G₀ = 0)
| 步 | w | 梯度 g | G(移动平均) | 有效学习率 | w(更新后) |
|---|---|---|---|---|---|
| 1 | 0 | −18.67 | 34.85 | 0.0847 | 1.581 |
| 2 | 1.581 | −3.91 | 32.90 | 0.0872 | 1.922 |
| 3 | 1.922 | −0.73 | 29.66 | 0.0918 | 1.989 |
| 4 | 1.989 | −0.103 | 26.69 | 0.0968 | 1.999 |
精髓在第 2 步:梯度从 −18.67 掉到 −3.91(接近目标了),G 也跟着往下走(34.85 → 32.90),不像 AdaGrad 只增不减。
对比 AdaGrad
| 步 | AdaGrad 有效学习率 | RMSProp 有效学习率 | AdaGrad 的 w | RMSProp 的 w |
|---|---|---|---|---|
| 1 | 0.0268 | 0.0847 | 0.500 | 1.581 |
| 2 | 0.0214 ↓ | 0.0872 | 0.800 | 1.922 |
| 3 | 0.0193 ↓ | 0.0918 | 1.016 | 1.989 |
| 4 | 0.0182 ↓ | 0.0968 | 1.183 | 1.999 |
- 学习率走势:AdaGrad 单调下降(被 G 累加拖累);RMSProp 平稳甚至回升(G 能随梯度变小而回落)。
- 收敛速度:AdaGrad 4 步才到 1.183;RMSProp 4 步已到 1.999,几乎收敛。
PyTorch
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.01, alpha=0.9) # alpha 即 γ
5. Adam:Momentum + RMSProp 合体
逻辑
Adam = Momentum(管方向)+ RMSProp(管步长),两套机制同时用。它同时维护两个累积量:
默认 β₁=0.9,β₂=0.999。
偏差修正(Adam 特有的关键细节):
更新:
分子是 Momentum 的方向,分母是 RMSProp 的自适应缩放,合在一行就是 Adam。
为什么需要偏差修正
m₀、v₀ 都从 0 开始,第一步 m₁ = 0.1·g₁ 严重低估真实梯度(偏向 0)。偏差修正把它除以 (1−β₁ᵗ) 放大回来:第一步 1−0.9¹ = 0.1,于是 m̂₁ = m₁/0.1 = g₁,修正回真实梯度。随 t 增大修正因子趋于 1,只在训练早期起作用,专治冷启动低估。
逐步计算(lr = 0.5,β₁=0.9,β₂=0.999,m₀=v₀=0)
| 步 | w | m̂(方向) | √v̂(缩放) | 更新量 | w(更新后) |
|---|---|---|---|---|---|
| 1 | 0 | −18.67 | 18.67 | +0.500 | 0.500 |
| 2 | 0.500 | −16.21 | 16.50 | +0.491 | 0.991 |
| 3 | 0.991 | −13.71 | 14.52 | +0.472 | 1.463 |
| 4 | 1.463 | −11.17 | 12.82 | +0.436 | 1.899 |
每步更新量都很接近(0.5、0.491、0.472、0.436),非常平稳。因为分子 m̂ 和分母 √v̂ 随梯度同比例缩小,相除后步长被"归一化",不会梯度大就猛冲、小就磨蹭。这让 Adam 对学习率不敏感、训练稳定,是它成为默认选择的重要原因。
PyTorch
optimizer = torch.optim.Adam(model.parameters(), lr=0.001,
betas=(0.9, 0.999), eps=1e-8)
lr=0.001 是经典默认值,很多任务开箱即用。训练大模型时的实际默认往往是 AdamW(Adam 的权重衰减改进版)。
五大优化器全景对比
用同一个例子,把整条演进线摆在一起(第 4 步的 w):
| 优化器 | 核心机制 | 解决了什么问题 | 第4步 w |
|---|---|---|---|
| SGD | 直接用当前梯度 | —(基线) | 0.662 |
| Momentum | 累积梯度方向 | SGD 的震荡与磨蹭 | 1.459 |
| AdaGrad | 累加梯度平方,自适应步长 | 各参数需要不同学习率 | 1.183 |
| RMSProp | 移动平均梯度平方 | AdaGrad 学习率衰竭到 0 | 1.999 |
| Adam | Momentum + RMSProp + 偏差修正 | 方向、步长同时自适应 | 1.899 |
关于数值:具体数字和 lr 选择有关,这里重点看机制差异而非绝对快慢。RMSProp 在这个特定例子里恰好收敛最快,但 Adam 的优势在于对各种问题的普适稳定性——真实任务里它通常是最省心的默认选择。
一句话串起整条线:
SGD 打基础 → Momentum 加方向惯性 → AdaGrad 引入自适应步长 → RMSProp 修好步长衰竭 → Adam 合二为一再加偏差修正。
理解了每一步"解决了上一个的什么问题",这五个优化器就串成了一个完整逻辑链,而不是五个孤立的公式。