1.1 优化器到底是什么?——从零开始的理解
假设你被蒙上眼睛,扔进一座连绵起伏的大山里。你的任务是:找到这座山的最低点(全局最低谷)。你手里没有地图,没有GPS,只能靠两只脚来探索。
每走一步,你就用脚踩一踩地面:哪边的坡度最陡?然后朝那个方向迈一步。这就是最朴素的 梯度下降——摸着坡度走,总能走到谷底。
神经网络训练 = 找山谷最低点。这个山谷不是什么地理山脉,而是 损失函数(Loss Function) 的地形。山的每个位置代表一组权重参数 W,山的高度代表损失 L(W)。我们要找 损失最小的那组权重。
优化器(Optimizer)就是决定:
- 往哪个方向走?——负梯度方向(下山最快的方向)
- 每一步走多大?——学习率(步长)
- 怎么越走越聪明?——用历史经验加速收敛
一个完整的训练循环:
- 前向传播:输入数据经过网络,算出一个预测结果和对应的损失 L
- 反向传播:用链式法则,算出每个参数 w 对损失 L 的偏导数 ∂L/∂w(梯度)
- 更新参数:W_new = W_old - α × 梯度(朝负梯度的方向迈一步)
- 回到步骤1,直到损失不再下降
类比总结:
| 现实世界 | 神经网络 |
|---|---|
| 山的位置 (x, y) | 参数权重 W |
| 山的高度 | 损失 L(W) |
| 脚的探路(感知坡度) | 梯度 ∇L(W)(反向传播) |
| 向下坡方向迈一步 | W = W - α·∇L |
| 步长大小 | 学习率 α |
| 你(探索者) | 优化器 Optimizer |
1.2 梯度下降的数学本质——从导数一步步推导
先从最简单的情况开始:假设我们只有一个参数 w,损失函数是 f(w) = w²。这个函数在 w=0 时取最小值 0。我们来看怎么用梯度下降找到 w=0。
什么是导数?
导数 f’(w) 表示函数在 w 点的 瞬时变化率。f’(w) > 0 表示函数在 w 处上升,f’(w) < 0 表示下降。f’(w) = 0 表示可能是最低点或最高点。
对 f(w)=w² 求导:f’(w) = 2w。
- 当 w=3,f’(3)=6 > 0 → 上升方向 → 要向左走(减小w)才能下降
- 当 w=-2,f’(-2)=-4 < 0 → 下降方向 → 要向右走(增大w)才能下降
梯度下降的核心公式:
w_{new} = w_{old} - α × f’(w_{old})为什么是减号?因为导数指向函数 上升 最快的方向,我们要下降,所以取 负方向。
参数说明表:
| 符号 | 名称 | 含义 | 类比 |
|---|---|---|---|
| w_{old} | 当前参数值 | 当前山上的位置 | 你现在的坐标 |
| w_{new} | 更新后的参数 | 迈一步后的新位置 | 你下一步的坐标 |
| α | 学习率(步长) | 每一步走多大 | 步子的大小 |
| f’(w_{old}) | 导数/梯度 | 当前位置的坡度 | 脚下地面的倾斜程度 |
| -α·f’(w_{old}) | 参数更新量 | 朝哪个方向走多远 | 一步的位移向量 |
数值例子——手算梯度下降:
设 w_0 = 3,α = 0.1,f(w)=w²,f’(w)=2w
第1步:w_0=3, f’(3)=6, 更新量=-0.1×6=-0.6, w_1=3-0.6=2.4, f(w_1)=5.76 第2步:w_1=2.4, f’(2.4)=4.8, 更新量=-0.1×4.8=-0.48, w_2=2.4-0.48=1.92, f(w_2)=3.69 第3步:w_2=1.92, f’(1.92)=3.84, 更新量=-0.384, w_3=1.536, f(w_3)=2.36 第4步:w_3=1.536, f’(1.536)=3.072, 更新量=-0.307, w_4=1.229, f(w_4)=1.51 第5步:w_4=1.229, f’(1.229)=2.458, 更新量=-0.246, w_5=0.983, f(w_5)=0.97 ... 经过约20步,w ≈ 0.06,损失 ≈ 0.004 经过约30步,w ≈ 0.006,损失 ≈ 0.00004 —— 已接近全局最低点 w=0
注意到一个重要现象:越接近谷底,梯度越小(f’(w) → 0),参数的更新量越小,收敛变得平滑而缓慢。这是梯度下降的特点:前期快,后期慢。
实际神经网络有 数百万到数十亿个参数,每个参数都有自己的梯度。优化器的任务就是协调好这庞大的更新过程。
1.3 学习率——优化器最关键的旋钮
学习率 α 是优化器最重要的超参数。它决定了每一步参数更新的幅度。设得不对,训练直接失败。
三种常见情况:
- α 过小(比如 0.0000001):每一步像蚂蚁爬,需要极多步才能到谷底。训练时间从几小时变成几周,而且可能困在很小的局部洼地里出不来。
- α 过大(比如 0.5):一步跨太大,直接从谷底这头跳到了对面山坡上,甚至震荡发散,损失越优化越大。
- α 刚好(比如 0.1):每一步稳健下降,n步后到达谷底。
用 f(w)=w² 演示不同学习率的效果:
| α | w_0=3 | w_1 | w_2 | w_3 | 结果 |
|---|---|---|---|---|---|
| 0.01 | 3.00 | 2.94 | 2.88 | 2.82 | 太慢,100步才到0.5 |
| 0.10 | 3.00 | 2.40 | 1.92 | 1.54 | 良好,30步接近0 |
| 0.50 | 3.00 | 0.00 | 0.00 | 0.00 | 刚好一步到0(巧合) |
| 0.60 | 3.00 | -0.60 | 0.12 | -0.024 | 震荡,但能收敛 |
| 1.00 | 3.00 | -3.00 | 3.00 | -3.00 | 反复震荡永不收敛 |
| 1.10 | 3.00 | -3.60 | 4.32 | -5.18 | 发散!损失越来越大 |
日常类比——做饭加盐:
- 学习率 = 每次加盐的勺子大小
- 损失 = 汤的咸度偏差(太淡或太咸都不好)
- α 太小:每次加一粒盐,尝了100次还是淡的
- α 太大:一勺下去直接咸死人,只能在“太咸”和“太淡”之间反复横跳
- α 刚好:两三勺就调到完美咸度
实际训练中的学习率策略:通常从 α=0.001(1e-3)开始调整。随着训练进行,学习率通常会 衰减——开始时大步探索,后期小步精细调整。
练习 1——梯度下降手算
Q1:f(w)=2w²,初始 w=4,α=0.2。手动计算前3步的 w 值和损失值。
Q2:f(w)=w²+3w,初始 w=5,α=0.1。先求导,再手动计算前2步。
Q3:如果 α=0.5,用 f(w)=w²,w=3 开始,计算第1步参数。损失是变大还是变小了?为什么?
Q4:假设损失函数在某个参数处的梯度是 0——这意味着什么?下一步该怎么办?
Q5:用自己的话解释:为什么梯度下降用 负梯度方向而不是正梯度方向?
查看答案
A1:f(w)=2w²,f’(w)=4w
第1步:w₀=4, f’(4)=16, 更新=-0.2×16=-3.2, w₁=0.8, f(w₁)=2×0.64=1.28 第2步:w₁=0.8, f’(0.8)=3.2, 更新=-0.2×3.2=-0.64, w₂=0.16, f(w₂)=2×0.0256=0.0512 第3步:w₂=0.16, f’(0.16)=0.64, 更新=-0.2×0.64=-0.128, w₃=0.032, f(w₃)=2×0.001024=0.002048
A2:f(w)=w²+3w,f’(w)=2w+3
第1步:w₀=5, f’(5)=13, 更新=-0.1×13=-1.3, w₁=3.7, f(w₁)=13.69+11.1=24.79 第2步:w₁=3.7, f’(3.7)=7.4+3=10.4, 更新=-1.04, w₂=2.66, f(w₂)=7.08+7.98=15.06 (实际最小值在 w=-1.5 处)
A3:w₀=3, f’(3)=6, 更新=-0.5×6=-3, w₁=0, f(0)=0。损失从 f(3)=9 降到了 0。α=0.5 对这个特殊函数恰好一步到最小值,但一般情况下是巧合,不表示 α=0.5 总是好的。
A4:梯度为0意味着参数处于 临界点——可能是局部最低点、局部最高点或鞍点。如果是真正的极小值点,那已经找到了,停止更新。如果是鞍点,需要添加动量或扰动来逃脱。
A5:因为导数 f’(w) 指向函数 上升最快 的方向。我们要最小化损失,所以需要朝反方向走——即 负梯度方向。数学上就是 w_{new}=w_{old}-α·f’(w_{old}) 中的减号。
2.1 BGD(批量梯度下降)——用全部数据算一次方向
假设你有 N=1000 条训练数据。BGD 的做法是:用全部1000条数据计算梯度,然后更新一次参数。
公式:
W_{new} = W - α × (1/N) ∑_{i=1}^{N} ∇L_i(W)其中 ∇L_i(W) 是第 i 条数据对当前参数 W 的梯度。1/N 的作用是取平均,让梯度大小不受数据量影响。
参数说明表:
| 符号 | 含义 | 类比 |
|---|---|---|
| N | 总训练样本数 | 指南上所有城市 |
| ∑_{i=1}^{N} ∇L_i(W) | 所有样本的梯度之和 | 问所有人“该往哪走”,然后把方向加起来 |
| (1/N)∑ | 平均梯度 | 取所有人的“平均意见” |
| α×平均梯度 | 参数更新量 | 按“平均意见”迈一步 |
数值例子:
假设只有 3 条数据,损失函数 L(W)=W²(简化版,实际每个样本损失不同)
数据1的梯度:∇L₁=2 × 5 = 10 (对应W=5的位置) 数据2的梯度:∇L₂=2 × 3 = 6 (W=3) 数据3的梯度:∇L₃=2 × 1 = 2 (W=1) 平均梯度 = (10+6+2)/3 = 18/3 = 6 α=0.1 → 更新量 = -0.1×6 = -0.6 新的 W = (5+3+1)/3 - 0.6 = 3 - 0.6 = 2.4 (这里假设我们用一个W代表所有参数的简化情况)
类比——全班旅游投票:
全班 N 个同学投票决定去哪里玩。BGD = 先问每一个人,统计全部意见后,取一个平均方向,然后出发。方向很精准,但问完所有人要很久很久。
BGD 的优缺点:
- ✅ 梯度方向准确,因为用了全部数据
- ✅ 每一步损失一定下降(或持平),不会震荡
- ❌ 计算极慢——每更新一步得算 N 条数据的梯度
- ❌ 如果 N=100万,一次更新就要算100万次前向和反向传播,几个小时才能更新一次
- ❌ 不能在线学习(新数据来了不能即时更新)
实际使用:BGD 几乎没有人在大模型训练中使用了,太慢了。
2.2 SGD(随机梯度下降)——随机抽一个人问方向
SGD 和 BGD 只有一个区别:每次只用一个随机样本计算梯度。
公式:
W_{new} = W - α × ∇L_i(W) (i 是随机选的一个样本)数值例子——SGD 与 BGD 对比:
还是上面3条数据,W=3,α=0.1:
BGD: 平均梯度=(10+6+2)/3=6, 更新=-0.6, 新W=2.4(每次都一样) SGD(假设随机顺序:数据2→数据3→数据1→数据2...): 第1步:抽到数据2, ∇L=6, 更新=-0.6, W=3-0.6=2.4 第2步:抽到数据3, ∇L=4, 更新=-0.4, W=2.4-0.4=2.0 第3步:抽到数据1, ∇L=10, 更新=-1.0, W=2.0-1.0=1.0 第4步:抽到数据2, ∇L=2, 更新=-0.2, W=1.0-0.2=0.8 ...
可以看到 SGD 每一步的方向有 噪声(波动),但整体仍然在往正确的方向走。
类比——拍脑袋决策 vs 全面调研:
SGD 就像领导 随机拦下一个员工问意见,不等别人就直接调头走了。有时候问到的人给出离谱的建议,你就走了弯路(噪声大)。但更多时候,这个随机问路的方式让你 动作超快——别人还在收集意见时你已经走出十步了。
SGD 的优缺点:
- ✅ 计算极快——每步只用1条数据
- ✅ 可以流式/在线学习——新数据来了立刻更新
- ✅ 噪声有好处——可能跳过局部最小值(像热力学扰动让人摆脱困境)
- ❌ 梯度噪声大——更新方向不一定准确
- ❌ 不会严格收敛——会在最小值附近来回震荡
- ❌ 不能利用 GPU 的大规模并行计算优势(一次只算一条数据太浪费 GPU)
2.3 Mini-batch SGD(小批量梯度下降)——实战之王
BGD 太慢,SGD 太颠,实战中没人用这两种极端。大家用的是 Mini-batch SGD。
做法:每次随机取一个小批量(mini-batch),比如 32 条或 128 条数据,用这一个批量的数据计算平均梯度,然后更新一次参数。
公式:
W_{new} = W - α × (1/B) ∑_{i=1}^{B} ∇L_i(W)其中 B 是小批量的大小(batch size,也叫 batch_size)。
参数说明表:
| 参数 | 典型值 | 说明 |
|---|---|---|
| batch_size (B) | 16, 32, 64, 128, 256 | 每次用多少条数据算梯度 |
| epoch | 1, 3, 10, 100 | 完整遍历一次全部数据 = 1个epoch |
| iteration | N / B | 每个epoch内的更新次数 |
数值例子——完整的一次训练过程:
假设 N=1000 条数据,batch_size=32,α=0.01:
每个iteration:随机取32条 → 计算32个梯度 → 平均 → 更新参数 每个epoch: 1000/32 = 31.25 ≈ 32次iteration(最后一个batch可能不足32) 训练10个epoch: 共 10×32 = 320 次参数更新
三种方法对比表:
| 属性 | BGD | Mini-batch SGD | SGD |
|---|---|---|---|
| 每次用数据量 | 全部 N | B(如32) | 1 |
| 更新次数/epoch | 1 | N/B | N |
| 梯度噪声 | 无(精确) | 中等 | 大(高方差) |
| 收敛速度 | 极慢 | 最快 | 快但震荡 |
| GPU利用率 | 高(一次全算) | 高(矩阵并行) | 极低(浪费GPU) |
| 实际使用 | 几乎不用 | 99%场景用这个 | 很少单独用 |
为什么 batch_size 通常取 32 或 128?
- 2 的幂次(32, 64, 128, 256)对 GPU 内存对齐友好
- 太小(<16):梯度噪声大,GPU并行能力没充分利用
- 太大(>512):梯度更精确,但 GPU 显存可能不够,而且太大的 batch 会降低泛化能力(收敛到“尖锐”的极小值)
- 经验法则:起步用 32,内存够就 128,再大收益递减
SGD/Mini-batch 的核心直觉:不要试图完美计算梯度再更新,而是 边走边修正。方向有一点偏差没关系,因为你会不断用新的信息调整方向。走得快比走得准更重要。
练习 2——BGD/SGD/Mini-batch
Q1:训练集有 50000 条数据,batch_size=128。一个 epoch 有多少次 iteration?训练 20 个 epoch 总共多少次 parameter update?
Q2:为什么说 SGD 的噪声有时候反而是好事?
Q3:BGD 每次一定要用 ∑/N 取平均梯度。如果不取平均,直接用梯度之和,会有什么问题?
Q4:你发现训练损失曲线在下降过程中有剧烈震荡。可能是什么原因?应该怎么调整?
Q5:如果 batch_size=1(即纯SGD),和 batch_size=1024 相比,训练速度哪个更快?泛化能力哪个更好?为什么?
查看答案
A1:每 epoch iteration = 50000/128 = 390.625 → 向上取整 = 391 次。20 个 epoch = 391×20 = 7820 次 parameter update。
A2:损失函数有很多 局部极小值(山里的许多小坑)。BGD 的精确梯度会稳稳掉进最近的坑(局部最优)出不来。而 SGD 的噪声就像随机踢了一脚——可能一脚把参数踢出小坑,继续寻找真正的全局最低点。这就是 随机性作为一种正则化。
A3:如果不取平均,梯度之和 = N × 平均梯度。假设 N=100000,更新量 = α×N×平均梯度 ≈ 100000 × α × 平均梯度。这会导致更新量过大,参数直接发散。取平均保证了 步长不依赖于数据量。
A4:可能原因:1) 学习率 α 太大;2) batch_size 太小导致噪声太大;3) 数据本身有噪声或异常值。解决方案:1) 减小 α(比如从0.01降到0.001);2) 增大 batch_size(从32增加到128);3) 检查数据质量。
A5:速度上:batch_size=1024 单步计算慢(需要算1024条数据),但每步更准,每 epoch 更新次数少(50000/1024 ≈ 49次),总时间可能略快。泛化上:batch_size=1 通常泛化更好(噪声带来正则化效果),batch_size=1024 泛化可能变差(梯度太准,陷入尖锐极小值)。实践中 batch_size 通常在 32~256 之间做权衡。
3.1 Momentum(动量法)——让优化器有“惯性”
SGD 有什么问题?
想象一个
Momentum 的解决方案——加入惯性:
就像从山顶推下一个球。球不会每步都重新“看坡度决定方向”,而是 保持之前的速度,加上当前坡度带来的加速度。即使某个时刻坡度水平方向为0,球也会因为惯性继续前进。
公式:
v_t = β × v_{t-1} + ∇L(W_t)
W_{t+1} = W_t - α × v_t参数说明表:
| 符号 | 名称 | 含义 | 典型值 | 类比 |
|---|---|---|---|---|
| v_t | 速度/动量 | 当前步的更新方向(带历史信息) | - | 球的当前速度 |
| v_{t-1} | 上一步的速度 | 上一步累计的更新方向 | - | 球上一步的速度 |
| β | 动量衰减系数 | 保留多少上一步的速度 | 0.9 | 地面摩擦力——摩擦力越小保留越多 |
| ∇L(W_t) | 当前梯度 | 当前位置的坡度产生的力 | - | 当前坡度给球的加速度 |
| α | 学习率 | 总体步长缩放因子 | 0.001~0.1 | 球的质量(质量越大越难加速/减速) |
数值例子——一步步手算 Momentum:
假设在某方向上的梯度序列为:g=[3, 2, 1, 0.5, 0.1, -0.2, -0.1, 0],β=0.9,α=0.1
初始化:v=0 第1步:梯度g=3 v = 0.9×0 + 3 = 3 ΔW = -0.1×3 = -0.3 新W = W - 0.3 第2步:梯度g=2 v = 0.9×3 + 2 = 2.7 + 2 = 4.7 ΔW = -0.1×4.7 = -0.47 新W = W - 0.47 第3步:梯度g=1 v = 0.9×4.7 + 1 = 4.23 + 1 = 5.23 ΔW = -0.1×5.23 = -0.523 新W = W - 0.523 第4步:梯度g=0.5 v = 0.9×5.23 + 0.5 = 4.707 + 0.5 = 5.207 ΔW = -0.521 第5步:梯度g=0.1 v = 0.9×5.207 + 0.1 = 4.686 + 0.1 = 4.786 ΔW = -0.479 第6步:梯度g=-0.2(注意梯度方向反转了!) v = 0.9×4.786 + (-0.2) = 4.307 - 0.2 = 4.107 ΔW = -0.411 (虽然反转了,但惯性很大,只减速了一点,仍然前进) 第7步:梯度g=-0.1 v = 0.9×4.107 + (-0.1) = 3.696 - 0.1 = 3.596 ΔW = -0.360 第8步:梯度g=0 v = 0.9×3.596 + 0 = 3.236 ΔW = -0.324 (即使梯度为0,惯性仍让参数继续前进!)
与不加 Momentum 的 SGD 对比:
不加 Momentum:第6步梯度为负→直接往回走;第8步梯度为0→完全不更新。加了 Momentum:即使梯度方向变了或为0,惯性让参数继续朝原方向前进,同时逐渐减速,不会突然转向。
日常类比——开车:
- SGD = 你每走一步都停下来,重新看路牌决定方向
- Momentum = 你开车时带着速度,踩刹车要一段距离才能停下来,踩油门速度是逐渐加上去的
- β = 车的惯性系数(0.9=冰面开车,惯性大;0.1=沙地开车,摩擦力大)
- 梯度 = 油门(正梯度)或刹车(负梯度)
3.2 Momentum 在“窄峡谷”地形上为什么有效?
问题场景:一个峡谷,长轴方向(谷底方向)梯度=0.1,短轴方向(震荡方向)梯度=±5。
SGD 的行为:
第1步:向左震荡5 + 前进0.1 = 方向5.01(几乎横向走) 第2步:向右震荡5 + 前进0.1 = 方向-4.99(又横跳回来) ... 每一步都在左右横跳,前进极其缓慢
Momentum 的行为(β=0.9):
第1步:v = 0.9×0 + [5, 0.1] = [5, 0.1] 第2步:v = 0.9×[5,0.1] + [-5,0.1] = [4.5,-0.09] + [-5,0.1] = [-0.5, 0.01] 第3步:v = 0.9×[-0.5,0.01] + [5,0.1] = [-0.45,0.009] + [5,0.1] = [4.55, 0.109] 第4步:v = 0.9×[4.55,0.109] + [-5,0.1] = [4.095,0.098] + [-5,0.1] = [-0.905, 0.198] ... 关键:震荡方向(短轴)的梯度正负交替,累积后正负抵消; 前进方向(长轴)的梯度同向,累积后叠加加速!
结论:Momentum 天然 抑制震荡、加速前进。震荡方向的梯度一正一负互相抵消,前进方向的梯度不断叠加。就像一个秋千,你只在它荡回来时推一下,它就越荡越高——Momentum 就是在正确的时机“顺势推一把”。
Nesterov Momentum(NAG)——比标准 Momentum 更聪明:
NAG 的想法是:既然我们知道当前速度会带惯性往前走,为什么不先“预见”一下到哪?
标准 Momentum: 1. 算当前位置的梯度 g 2. 更新速度:v = βv + g 3. 更新参数:W = W - αv Nesterov Momentum: 1. 先按惯性“跳一步”到预估位置:W_temp = W - βv 2. 在预估位置计算梯度 g_lookahead 3. 更新速度:v = βv + g_lookahead 4. 更新参数:W = W - αv
NAG 的 提前看一步 行为,让它在接近最小值时不会冲过头,收敛更稳定。就像司机快到路口时提前松油门,而不是到路口才急刹车。
练习 3——Momentum
Q1:初始 v=0,梯度序列 g=[2, 3, 4, 5],β=0.8,α=0.1。手算前3步的 v 值和更新量。
Q2:β=0.9 和 β=0.5 有什么区别?什么时候应该用更大的 β?
Q3:梯度方向突然逆转(从+10变成-10),Momentum 会立刻反向吗?还是要花几步?请用 β=0.9 手算说明。
Q4:Nesterov Momentum 为什么比标准 Momentum 收敛更快?
Q5:用自己的话解释:为什么 Momentum 能解决峡谷地形中的震荡问题?
查看答案
A1:
第1步:v = 0.8×0 + 2 = 2, 更新 = -0.1×2 = -0.2 第2步:v = 0.8×2 + 3 = 1.6+3 = 4.6, 更新 = -0.1×4.6 = -0.46 第3步:v = 0.8×4.6 + 4 = 3.68+4 = 7.68, 更新 = -0.1×7.68 = -0.768
A2:β=0.9 保留了 90% 的上一步速度,惯性很大,适合地形连续、方向一致的情况。β=0.5 只保留50%,惯性小,响应快,适合梯度频繁变化、噪声大的情况。通常在平坦或较平滑的地形上用更大的 β(如0.9),在地形崎岖时用更小的 β(如0.5)。深度学习默认通用值就是 β=0.9。
A3:设初始 v=10,梯度突然从+10变成-10
转变前:v = 0.9×v_prev + 10 = 很大正值 第1步(梯度=-10):v = 0.9×10 + (-10) = 9-10 = -1 —— 方向终于反了,但很小 第2步(梯度=-10):v = 0.9×(-1) + (-10) = -0.9-10 = -10.9 —— 反向加速 第3步(梯度=-10):v = 0.9×(-10.9) + (-10) = -9.81-10 = -19.81 —— 反向全速 Momentum 需要约2步才能逆转方向。这就是“惯性”的表现——不会瞬间掉头。
A4:Nesterov 因为 提前看了一步 位置的梯度,当快到山谷时能提前减速(预估位置的梯度可能更平缓或已经反转),避免冲过头。标准 Momentum 要到“撞上”谷壁才反应,容易来回震荡几次。形象地说:NAG 像有“刹车灯”,标准 Momentum 像“撞墙才停”。
A5:峡谷地形中,短轴方向(震荡方向)的梯度正负交替出现。Momentum 的 v_t = βv_{t-1} + g_t 对历史有平滑作用。正负交替的梯度在累积中互相抵消(+5 -5 +5 -5 ≈ 0),而长轴方向同向的梯度不断叠加(+0.1 +0.1 +0.1 +0.1 → 累积变大)。最终效果:震荡被抑制,前进被加速。
4.1 AdaGrad——不同参数用不同学习率
一个问题:前面的 BGD、SGD、Momentum 对所有参数都用同一个学习率 α。但实际中,不同参数的梯度数量级差异很大。
例子:神经网络中,靠近输入的层和靠近输出的层,梯度大小可能差1000倍。用一个统一的学习率,要么对 “大梯度参数”震荡,要么对 “小梯度参数”学不动。
AdaGrad 的想法:经常更新的参数降学习率(因为梯度大,已经学了很多),不常更新的参数保持高学习率(因为梯度小,需要多学)。
公式:
G_t = G_{t-1} + (∇L(W_t))² (每个参数的梯度平方累积)
W_{t+1} = W_t - α / (√(G_t) + ε) × ∇L(W_t)参数说明表:
| 符号 | 含义 | 类比 |
|---|---|---|
| G_t | 历史梯度平方的累积和 | 计步器——记录每个参数“走了多少步” |
| (∇L)² | 当前梯度的平方 | 这次走的步幅的平方 |
| √(G_t) | 梯度累积量的平方根 | 根据“总运动量”调整步长 |
| ε | 数值稳定常数 | 防止除以0 |
| α / (√(G_t) + ε) | 自适应学习率 | 每个参数自己“算”出来的专属步长 |
数值例子:
有两个参数 w₁ 和 w₂,初始化 α=0.1,ε=1e-8(忽略 ε 简化计算):
时间步 w₁的梯度 w₂的梯度 G(w₁) G(w₂) w₁的学习率 w₂的学习率 t=1 0.5 0.01 0.25 0.0001 0.1/0.5=0.2 0.1/0.01=10 t=2 0.3 0.02 0.34 0.0005 0.1/0.58=0.17 0.1/0.022=4.5 t=3 0.2 0.01 0.38 0.0006 0.1/0.62=0.16 0.1/0.024=4.08 t=10 ... ... 大约2.0 大约0.002 0.1/1.4=0.07 0.1/0.045=2.2 可以看到:w₁(梯度大)的学习率从0.2快速下降到0.07 w₂(梯度小)的学习率一直保持很大(4-10倍于w₁) 这让w₂这个大梯度能快速追上——解决了学习率不均衡问题!
AdaGrad 的问题——学习率会降为零:
G_t 是 单调递增 的(不断累积正数)。随着训练进行,G_t → ∞,α/√(G_t) → 0。参数最终 完全停止学习。对于需要长期训练的任务(如大模型训练数百亿步),这不可接受。
日常类比——学英语:AdaGrad 像是给每个单词一个“努力计数器”。你每背一次单词,它的“努力”就累积一点,然后后续分配给它的复习时间就减少。缺点:越往后你学任何新东西都越来越慢,最后完全不想学了。
4.2 RMSprop——修复 AdaGrad 的“学习率死亡”问题
关键改进:不用全部历史的梯度平方累积,而用 指数移动平均(EWMA)——只记住 “最近一段时间的梯度大小”,老梯度的影响逐渐衰减。
公式:
v_t = β₂ × v_{t-1} + (1 - β₂) × (∇L(W_t))²
W_{t+1} = W_t - α / (√(v_t) + ε) × ∇L(W_t)与 AdaGrad 的区别:
| 对比项 | AdaGrad | RMSprop |
|---|---|---|
| 累计方式 | 求和 G = ∑g² | 移动平均 v = β₂·v + (1-β₂)·g² |
| 历史影响 | 永不衰减,全部保留 | 指数衰减,越旧权重越小 |
| 学习率 | 单调递减 → 最终为0 | 动态调整,不会归零 |
| 适用场景 | 稀疏特征(NLP/推荐系统) | 通用(几乎所有任务) |
参数说明表:
| 符号 | 名称 | 含义 | 典型值 |
|---|---|---|---|
| v_t | 梯度平方的移动平均 | 计算最近的“梯度大小” | - |
| β₂ | 衰减率 | 历史信息的保留比例 | 0.999 |
| 1-β₂ | 新信息比重 | 当前梯度平方占的权重 | 0.001 |
| α | 全局学习率 | 基础步长 | 0.001 |
| ε | 数值稳定常数 | 防止除以0 | 1e-8 |
数值例子——RMSprop vs AdaGrad:
设每步梯度恒为 0.5,对比两种方法的 G_t 或 v_t 变化(β₂=0.9):
时间 AdaGrad(G) RMSprop(v) t=1 0.25 0.1×0 + 0.9×0.25 = 0.225 t=2 0.50 0.9×0.225 + 0.1×0.25 = 0.203+0.025 = 0.228 t=3 0.75 0.9×0.228 + 0.025 = 0.205+0.025 = 0.230 t=10 2.50 0.232 t=100 25.0 0.249 t=1000 250.0 0.250 AdaGrad: G_t 线性增长,学习率 α/√(G_t) 从0.2→0.02→0.006... 一直下降到0 RMSprop: v_t 趋于稳定值 0.25,学习率 α/√(0.25)=0.1/0.5=0.2 保持不变!
日常类比——记笔记:
- AdaGrad = 把所有笔记越堆越高,最后要找一条信息需要翻完整座山
- RMSprop = 用一个笔记本,不断翻新——最新记的放在最上面,旧的内容慢慢被覆盖
RMSprop 完美解决了 AdaGrad 的学习率衰减到零的问题,是 Adam 算法的重要组成部分。
练习 4——AdaGrad & RMSprop
Q1:为什么 AdaGrad 中 G_t 要累加梯度的 平方 而不是直接用梯度?
Q2:梯度序列 g=[3, 2, 1, 0.5, 0.2],α=0.01。用 AdaGrad 手算前3步的学习率(忽略 ε)。
Q3:梯度序列同 Q2,用 RMSprop(β₂=0.9,α=0.01)手算前3步的学习率。对比 AdaGrad 的差异。
Q4:为什么 RMSprop 的 β₂ 典型值是 0.999 这么大?如果改成 0.9 会怎样?
Q5:在自然语言处理(NLP)任务中,一些词频繁出现(如“的”“是”),一些词很少出现。AdaGrad 的每个参数自适应学习率机制为什么对这种场景特别有利?
查看答案
A1:因为梯度有正有负,直接累加会正负抵消 = 0。平方(或绝对值)确保累加值永远为正,能正确反映“该参数历史上梯度的大小”。取 √(G_t) 是为了把量级恢复到与梯度一致(因为平方放大了量级)。
A2:
t=1: g=3, G=9, lr=0.01/3=0.0033 t=2: g=2, G=9+4=13, lr=0.01/√13=0.01/3.606=0.00277 t=3: g=1, G=13+1=14, lr=0.01/√14=0.01/3.742=0.00267
A3:
β₂=0.9 t=1: g=3, g²=9, v=0.9×0+0.1×9=0.9, lr=0.01/√0.9=0.01/0.949=0.01054 t=2: g=2, g²=4, v=0.9×0.9+0.1×4=0.81+0.4=1.21, lr=0.01/√1.21=0.01/1.1=0.00909 t=3: g=1, g²=1, v=0.9×1.21+0.1×1=1.089+0.1=1.189, lr=0.01/√1.189=0.01/1.09=0.00917 差异:AdaGrad的学习率单调递减(0.0033→0.00277→0.00267), RMSprop的学习率动态调整(0.0105→0.0091→0.0092),先降后稳,不会一直降到零。
A4:β₂=0.999 意味着 v 会保留近 1000 步的梯度平方信息,对梯度大小的估计非常平滑稳定。如果改成 β₂=0.9,则只保留近 10 步的信息,v 对梯度变化更敏感,自适应学习率波动更大。大模型训练中通常用 0.999 或 0.9999 获得平滑的梯度估计。
A5:NLP 中词向量的更新极度不均衡。“的”这个字每次出现都会被更新,它的 G 累积很大,学习率很快变小→已经学好了,不再需要大幅调整。而“变压器”这种低频词出现的次数少,G 累积小,学习率保持很大→当它出现时能快速学。AdaGrad 的这种“给稀有特征高学习率”的特性在 NLP 和推荐系统中非常有效。
5.1 Adam——Momentum + RMSprop 的完美融合
Adam = Adaptive Moment Estimation(自适应矩估计)
这是目前 最常用、最推荐的首选优化器。它将 Momentum(动量)和 RMSprop(自适应学习率)两个思想融合在一起,同时解决了两个问题:
- Momentum 部分:用指数移动平均平滑梯度方向,抑制震荡、加速收敛
- RMSprop 部分:用梯度平方的指数移动平均给每个参数自适应学习率
完整公式(高中数学版本):
m_t = β₁ × m_{t-1} + (1 - β₁) × g_t (1) 动量:梯度的移动平均
v_t = β₂ × v_{t-1} + (1 - β₂) × g_t² (2) 自适应:梯度平方的移动平均
m̂_t = m_t / (1 - β₁^t) (3) 偏差校正
v̂_t = v_t / (1 - β₂^t) (4) 偏差校正
W_{t+1} = W_t - α / (√(v̂_t) + ε) × m̂_t (5) 最终更新参数说明表:
| 符号 | 名称 | 含义 | 典型值 | 类比 |
|---|---|---|---|---|
| g_t | 当前梯度 | 当前位置损失函数的坡度 | - | 脚底下地面的倾斜方向 |
| m_t | 一阶矩(动量) | 梯度的移动平均,代表方向 | - | 速度的方向(向哪边冲) |
| v_t | 二阶矩(自适应) | 梯度平方的移动平均,代表大小 | - | 路面的颠簸程度 |
| β₁ | 一阶矩衰减率 | 控制动量保留多少历史 | 0.9 | 车辆的惯性系数 |
| β₂ | 二阶矩衰减率 | 控制自适应量的平滑程度 | 0.999 | 路面感知的“记忆长度” |
| m̂_t | 偏差校正后动量 | 修正初始零偏后的方向 | - | 校准后的指南针 |
| v̂_t | 偏差校正后自适应量 | 修正初始零偏后的梯度大小 | - | 校准后的速度表 |
| α | 学习率 | 全局步长 | 0.001 | 驾驶的“油门基准” |
| ε | 稳定常数 | 防止除以0 | 1e-8 | 安全缓冲垫 |
为什么需要 “偏差校正”?
Adam 初始化时 m_0=0, v_0=0。前几步的移动平均值会严重偏向0(因为一开始全是0)。偏差校正 = 把被0拉低的平均值 “拉伸回来”。公式 m̂_t = m_t/(1-β₁^t) 中,分母随 t 增大从接近0逐渐趋近1,正好补偿了初始的“零偏”。
例子:β₁=0.9, 前几步 m_t 和 m̂_t 对比 t=1: m_1=0.0+0.1×g=0.1g, 校正 m̂=0.1g/(1-0.9)=0.1g/0.1=g (校正后等于真实梯度g!) t=2: m_2=0.9×0.1g+0.1×g=0.19g, 校正 m̂=0.19g/(1-0.81)=0.19g/0.19=g t=3: m_3=0.9×0.19g+0.1×g=0.271g, 校正 m̂=0.271g/(1-0.729)=0.271g/0.271=g 实际上校正后 m̂_t 约等于 g 的移动平均值,初始偏差被完全消除!
5.2 Adam 手算例子——一步步跑通整个流程
设 β₁=0.9, β₂=0.999, α=0.01, ε=1e-8(本算例中忽略 ε),梯度序列 g=[0.5, -1.0, 2.0, 0.3, -0.8]
初始: m_0=0, v_0=0 === t=1, g=0.5 === m_1 = 0.9×0 + 0.1×0.5 = 0.05 v_1 = 0.999×0 + 0.001×0.25 = 0.00025 m̂_1 = 0.05 / (1 - 0.9^1) = 0.05/0.1 = 0.5 v̂_1 = 0.00025 / (1 - 0.999^1) = 0.00025/0.001 = 0.25 ΔW = -0.01 × 0.5 / √0.25 = -0.01 × 0.5/0.5 = -0.01 === t=2, g=-1.0 === m_2 = 0.9×0.05 + 0.1×(-1.0) = 0.045 - 0.1 = -0.055 v_2 = 0.999×0.00025 + 0.001×1.0 = 0.00024975 + 0.001 = 0.00124975 m̂_2 = -0.055 / (1 - 0.9^2) = -0.055/(1-0.81) = -0.055/0.19 = -0.2895 v̂_2 = 0.00124975 / (1 - 0.999^2) = 0.00124975/(1-0.998001) = 0.00124975/0.001999 ≈ 0.625 ΔW = -0.01 × (-0.2895) / √0.625 = 0.002895/0.791 = 0.00366 === t=3, g=2.0 === m_3 = 0.9×(-0.055) + 0.1×2.0 = -0.0495 + 0.2 = 0.1505 v_3 = 0.999×0.00124975 + 0.001×4.0 = 0.0012485 + 0.004 = 0.0052485 m̂_3 = 0.1505 / (1 - 0.9^3) = 0.1505/(1-0.729) = 0.1505/0.271 = 0.555 v̂_3 = 0.0052485 / (1 - 0.999^3) = 0.0052485/(1-0.997003) = 0.0052485/0.002997 ≈ 1.751 ΔW = -0.01 × 0.555 / √1.751 = -0.00555/1.323 = -0.00419 === 总结 === t=1: ΔW=-0.01 (原始梯度0.5, 被缩小但方向正确) t=2: ΔW=+0.00366 (梯度-1.0反向应减W, 但动量方向仍然正向! 惯性拉扯中) t=3: ΔW=-0.00419 (梯度2.0改变方向, 动量跟上了) 对比纯SGD: t1:-0.005, t2:+0.01, t3:-0.02 (3倍于Adam的更新量) Adam的更新量更小更平滑, 因为RMSprop感知到梯度大小在变, 自动调节了学习率
Adam 的直觉理解——自动驾驶:
- SGD = 刚拿驾照的新手:每步都重新判断方向,方向盘大幅乱转
- Momentum = 有经验的老司机:有预判,转弯顺滑
- AdaGrad/RMSprop = 自适应的车:根据路况自动调节油门大小
- Adam = 顶级自动驾驶:既有老司机的预判(Momentum),又能根据路况自动调油门(RMSprop)
5.3 AdamW——Adam 的修复版(现代大模型的标准配置)
Adam 有一个隐藏问题——L2正则化(权重衰减)的实现方式不对。
问题:标准 Adam 中,L2正则化项 λ·W 被加在损失函数中参与梯度计算,然后梯度被 Adam 的移动平均值平滑和自适应学习率缩放。这导致 权重的衰减效果不稳定——被自适应学习率改变了衰减量。
AdamW 的修复:把权重衰减 从梯度计算中剥离出来,在 Adam 更新完成后,直接对权重做一次独立的衰减。
标准 Adam 的 L2 正则化(有问题): 1. 计算损失梯度时包含 L2 项: g_t = g_loss + λ·W 2. g_t 被 Adam 的 m/v 机制缩放 3. 最终更新量 = -α·m̂/√(v̂) —— 权重衰减也被缩放了! AdamW 的权重衰减(正确): 1. 计算损失梯度时不含 L2: g_t = g_loss 2. g_t 被 Adam 的 m/v 机制正常缩放 3. 在更新最后额外加一步: W -= α·λ·W(或 W *= 1-α·λ) —— 权重衰减不再被自适应学习率干扰!
为什么这对大模型训练重要?
LLM 训练中,权重衰减帮助控制模型复杂度、防止过拟合。AdamW 让权重衰减的效果 可预测且稳定,是 GPT、LLaMA 系列的标准配置。
PyTorch 代码示例:
# Adam(旧方式) import torch.optim as optim optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.01) # AdamW(推荐方式) optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01) # 区别:PyTorch 的 Adam 的 weight_decay 是错的(旧实现), # AdamW 的 weight_decay 才是正确的权重衰减。
练习 5——Adam 综合
Q1:Adam 的四行公式分别代表什么?(用一句话概括每行)
Q2:β₁=0.9 的“有效记忆长度”是多少步?(提示:指数移动平均的半衰期公式 t½ ≈ ln(0.5)/ln(β))
Q3:梯度序列 g=[1, 1, 1, 1, 1, -10, 1, 1, 1, 1],对比 Adam 和纯 SGD(α=0.01)在第 6 步(大梯度-10出现时)的更新量差异。Adam 参数:β₁=0.9, β₂=0.999, α=0.01
Q4:为什么 AdamW 比标准 Adam 更适合训练大语言模型?
Q5:如果训练中损失突然“爆炸”(spike),用 Adam 优化器时可能是什么问题?怎么修复?
查看答案
A1:
公式(1) m_t = β₁·m_{t-1} + (1-β₁)·g_t —— 梯度的移动平均(动量)
公式(2) v_t = β₂·v_{t-1} + (1-β₂)·g_t² —— 梯度平方的移动平均(自适应)
公式(3) m̂ = m/(1-β₁^t) —— 偏差校正(去初始化零偏)
公式(4) v̂ = v/(1-β₂^t) —— 偏差校正
公式(5) W -= α·m̂/(√(v̂)+ε) —— 最终更新A2:β₁=0.9, ln(0.5)/ln(0.9) = -0.693/-0.105 ≈ 6.6步。动量大约保留最近 6-7 步的梯度信息。β₂=0.999 的有效记忆:ln(0.5)/ln(0.999) = -0.693/-0.001 ≈ 693步。自适应量保留约 700 步的梯度平方信息。
A3:
纯SGD: 第6步更新 = -0.01 × (-10) = +0.10(方向突然大幅反转) Adam: 前5步累积: m_5 ≈ 1(5步平均约1) v_5 ≈ 1(5步平方平均约1) m̂_5=1, v̂_5=1(偏差校正后约1) 第5步更新 = -0.01×1/√1 = -0.01 第6步(g=-10): m_6 = 0.9×1 + 0.1×(-10) = 0.9-1.0 = -0.1 v_6 = 0.999×1 + 0.001×100 = 0.999+0.1 = 1.099 m̂_6 = -0.1/(1-0.9^6) = -0.1/(1-0.531) = -0.1/0.469 = -0.213 v̂_6 = 1.099/(1-0.999^6) = 1.099/(1-0.994) = 1.099/0.006 ≈ 183 更新 = -0.01 × (-0.213)/√183 = 0.00213/13.53 = 0.000158 对比: SGD 更新 +0.10(剧烈震荡), Adam 更新 +0.000158(几乎不变) Adam 因为有动量平滑和大梯度-10被 v 感知到(v̂=183巨大) 自适应学习率 α/√(v̂)=0.01/13.53=0.00074 乘以动量 -0.213 得更新量 -0.000158,方向几乎没变!
A4:AdamW 的权重衰减 独立于梯度自适应机制。标准 Adam 的 L2 权重项被 α/√(v̂) 缩放后效果不可控——不同参数的“真实衰减量”不同。AdamW 的权重衰减直接作用在参数上,效果稳定、可预测。对于有数十亿参数的大模型,这种稳定性和可预测性极为重要。Hugging Face Transformers 库默认使用 AdamW 作为优化器。
A5:可能的 Adam 相关问题及修复:
- 学习率太大:即使有自适应学习率,α=0.01 对大模型仍可能过大。修复:将 α 从 0.001 降到 0.0001
- ε 太小:某些参数经过偏差校正后 v̂ 很小,α/√(v̂) 可能爆炸。修复:将 ε 从 1e-8 增大到 1e-6
- 梯度裁剪不足:某个 mini-batch 产生异常大的梯度。修复:加梯度裁剪 clip_grad_norm(model.parameters(), max_norm=1.0)
- 权重衰减过大:weight_decay 太大导参数被过度衰减。修复:减小 weight_decay
6.1 六大优化器全面对比
进化路线图:
BGD → SGD → Mini-batch SGD(基础框架)
↓
Momentum(+惯性)→ Nesterov Momentum(+预见)
AdaGrad(+每个参数自适应)→ RMSprop(+滑动窗口替代无限累积)
Adam = Momentum + RMSprop + 偏差校正
↓
AdamW = Adam + 正确权重衰减
完整对比表:
| 优化器 | 核心公式 | 超参数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| BGD | W -= α×(1/N)∑∇L | α | 梯度精确,一定收敛 | 极慢,无法在线学习 | 小数据集、演示教学 |
| SGD | W -= α×∇L_i | α | 极快,在线学习,能跳出局部最优 | 噪声大,不严格收敛 | 在线学习 |
| Mini-batch | W -= α×(1/B)∑∇L | α, B | 速度与精度平衡,GPU友好 | 须选好batch_size | 几乎所有实际任务 |
| Momentum | v=βv+∇L W-=αv | α, β=0.9 | 抑制震荡,加速收敛 | 可能冲过头 | 峡谷地形、CV任务 |
| AdaGrad | G+=g² W-=αg/√(G) | α, ε=1e-8 | 自适应学习率,适合稀疏特征 | 学习率最终归零 | NLP、推荐系统 |
| RMSprop | v=βv+(1-β)g² W-=αg/√(v) | α, β=0.999, ε=1e-8 | 自适应学习率,不归零 | 无动量(可震荡) | 通用(Momentum的“另一半”) |
| Adam | m=β₁m+(1-β₁)g v=β₂v+(1-β₂)g² W-=α·m̂/(√v̂+ε) | α=0.001, β₁=0.9 β₂=0.999, ε=1e-8 | Momentum+RMSprop=双重优势 | 显存占用稍大(存m和v) | 通用首选! |
| AdamW | Adam + 独立权重衰减 | 同上 + weight_decay | 正确的权重衰减,更稳定 | 多一个超参数 | 大模型训练标准 |
6.2 实战经验:怎么选择优化器?
一句话指南:
“先用 Adam/AdamW 快速跑通,再考虑要不要换。”
详细建议:
| 你的情况 | 推荐优化器 | 理由 |
|---|---|---|
| 刚入门,第一次跑神经网络 | Adam | 默认参数(0.001)通常就能工作,不需要太多调参 |
| 训练大语言模型(LLM) | AdamW | 独立权重衰减稳定,是GPT/LLaMA的标准 |
| 图像分类/CNN/CV任务 | Adam 或 SGD + Momentum | SGD+动量在CV社区有大量经验,更易调优 |
| 数据极度稀疏(NLP/推荐) | Adam 或 AdaGrad | 自适应学习率对稀疏特征天然有利 |
| 需要极限泛化性能 | SGD + Momentum + 学习率调度 | SGD虽然需要更多调参,但在某些任务上泛化略优 |
| 资源受限(显存不够) | SGD + Momentum | Adam需要额外存m和v(显存翻倍),SGD只需存参数 |
调参建议:
- 学习率 α:最重要的超参数。Adam 从 0.001 开始,不收敛就降(0.0003、0.0001),震荡太厉害也降
- batch_size:起步 32,显存够用 128,再大收益递减
- β₁(动量衰减):不要动!0.9 几乎总是最优
- β₂(自适应衰减):不要动!0.999/0.9999 几乎总是最优
- ε:不要动!1e-8 足够。如果训练不稳定可以试试 1e-6
- weight_decay(AdamW):LLM 通常 0.01~0.1,CV 通常 0.0001~0.001
学习率调度(Learning Rate Schedule)——配合优化器使用:
光靠优化器还不够,好的学习率调度能显著提升训练效果:
- Step Decay:每 N 个 epoch 学习率减半(如每30epoch×0.1)
- Cosine Annealing:学习率按余弦曲线从大变小再变大(重启),帮助跳出局部最优
- Warmup:前几步从小学习率逐步升到目标值,防止初始不稳定
- ReduceLROnPlateau:损失不再下降时自动降低学习率
PyTorch 使用示例:
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
# 定义模型
model = MyNeuralNetwork()
# 选择优化器
optimizer = optim.AdamW(
model.parameters(),
lr=0.001, # 学习率
betas=(0.9, 0.999), # β₁, β₂
eps=1e-8, # ε
weight_decay=0.01 # 权重衰减
)
# 学习率调度器
scheduler = CosineAnnealingLR(
optimizer,
T_max=100, # 余弦完成一个周期需要多少步
eta_min=1e-6 # 最小学习率
)
# 训练循环
for epoch in range(100):
for batch in dataloader:
# 前向 + 反向 + 更新
loss = model(batch)
loss.backward()
# 梯度裁剪(可选,推荐大模型训练时使用)
torch.nn.utils.clip_grad_norm_(
model.parameters(), max_norm=1.0)
optimizer.step()
optimizer.zero_grad()
# 每 epoch 更新学习率
scheduler.step()练习 6——优化器选择与综合应用
Q1:你接到一个任务:训练一个 BERT 模型用于文本分类。你会选择什么优化器?学习率和超参数大概设多少?请给出理由。
Q2:训练过程中发现损失在大约 200 步后就不再下降了(plateau),但离理想的精度还差很远。应该怎么解决?
Q3:你的 GPU 只有 8GB 显存,想训练一个有 1 亿参数的模型。选 Adam 还是 SGD+Momentum?为什么?
Q4:用自己的话总结:Adam 相比 SGD+Momentum 的根本优势是什么?SGD+Momentum 相比 Adam 的根本优势又是什么?
Q5:一个模型用 Adam 训练时损失曲线:前 500 步正常下降,第 501 步突然飙升 100 倍,然后又恢复正常。分析可能的原因和解决方案。
查看答案
A1:推荐 AdamW(Hugging Face Transformers 的默认优化器)。超参数设置:lr=2e-5(BERT 微调的经典学习率),weight_decay=0.01,β₁=0.9, β₂=0.999。并配合 warmup(前 10% 的步数从 0 线性增长到 2e-5)。理由:BERT 微调是迁移学习场景,AdamW 的稳定性最好,2e-5 是经过海量实验确定的经典值。
A2:解决方案:
- 使用 ReduceLROnPlateau 调度器——patience=10 步的话,验证损失 10 步没降就自动降低学习率(如×0.1)
- 重启 Adam 的 m 和 v(用 optimizer.state = {} 清空)——有时候优化器的动量状态导致“踩坑”
- 检查是否陷入局部最优——可以尝试 Cosine Annealing with Warm Restarts
A3:Adam 每个参数需要存 3 份 数据:参数 W(4字节)、动量 m(4字节)、自适应量 v(4字节)= 12 字节/参数。1 亿参数 = 1.2GB。加上模型本身和其他中间变量,8GB 非常勉强。SGD+Momentum 只需存 2 份:参数 W(4字节)+ v(4字节)= 8 字节/参数。1 亿参数 = 0.8GB。显存压力小很多。所以推荐 SGD + Momentum 或 使用 Adam 的混合精度训练(梯度半精度,减少显存占用)。
A4:
- Adam 的根本优势 = 自适应学习率。你不用花大量时间调学习率,Adam 自己会根据每个参数的历史梯度大小动态调整学习率,让所有参数都处于“合适”的更新节奏。
- SGD+Momentum 的根本优势 = 泛化能力。SGD 的固定学习率(配合合适调度)在图像分类等任务上被证明泛化性能略优于 Adam。同时显存占用更小,而且在某些社区的调参经验积累远超 Adam。
A5:可能原因:
- 数据异常:当前 batch 包含异常标签或噪声数据。解决:检查数据质量,加数据清洗
- 梯度爆炸:梯度突然变得极大,v 来不及适应。解决:加梯度裁剪 clip_grad_norm(1.0)
- ε 太小:某个参数 v 极小,α/√(ε) 爆炸。解决:ε 从 1e-8 改为 1e-6
- 数值不稳定:特殊操作(如 softmax 后的 log)导致数值溢出。解决:用 log_softmax 替代 log(softmax)
单次 spike 如果不重复出现通常不影响最终收敛,但如果反复出现就需要检查原因。