跳到主要内容

Adam 优化器详解

Adam (Adaptive Moment Estimation) 是目前深度学习领域最流行、最常用的优化器,简直就是优化器界的"瑞士军刀"。

如果说 SGD 是一个醉汉,Momentum 是一个有惯性的铁球,那么 Adam 就是一辆装配了"自适应悬挂系统"的智能跑车

它之所以强,是因为它结合了两大流派的优点:

  1. Momentum (动量):解决"方向"问题(惯性,不乱晃)
  2. RMSProp (自适应学习率):解决"步长"问题(根据地形自动调整刹车或油门)

1. 核心痛点:为什么需要 Adam?

在 Momentum 中,我们虽然解决了震荡问题,但还有一个大问题:所有的参数都共享同一个学习率 η\eta

  • 场景: 假设你有两个参数 w1w_1w2w_2
    • w1w_1 对应的坡度非常陡峭(梯度很大),如果步子迈大了,容易飞出去
    • w2w_2 对应的坡度非常平缓(梯度很小),如果步子迈小了,走到猴年马月
  • Momentum 的做法: 对不起,我也没办法,大家用一样的步长
  • Adam 的做法: 看人下菜碟
    • 对陡峭的参数,我自动把学习率调小一点(谨慎)
    • 对平缓的参数,我自动把学习率调大一点(加速)

2. Adam 的三个核心组件

Adam 的名字来源于 "Adaptive Moment Estimation"(自适应矩估计)。它维护了两个"变量箱子"(Moment):

组件 A:一阶矩(First Moment)

这就完全等同于我们讲的 Momentum。它记录梯度的指数移动平均值(方向)。

mt=β1mt1+(1β1)gtm_t = \beta_1 \cdot m_{t-1} + (1-\beta_1) \cdot g_t

组件 B:二阶矩(Second Moment)

这是 Adam 的精髓。它记录梯度的平方的指数移动平均值(也就是梯度的方差/能量)。

vt=β2vt1+(1β2)gt2v_t = \beta_2 \cdot v_{t-1} + (1-\beta_2) \cdot g_t^2

组件 C:偏差修正(Bias Correction)

因为 mmvv 初始值都是 0,刚开始训练时,它们会趋向于 0(偏置),导致起步太慢。所以 Adam 做了一个数学上的修正。

m^t=mt1β1t,v^t=vt1β2t\hat{m}_t = \frac{m_t}{1-\beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1-\beta_2^t}

最终更新公式

θt+1=θtηm^tv^t+ϵ\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}

3. 参数详解

3.1 gtg_t:当前梯度 (Current Gradient)

gt=θJ(θt)g_t = \nabla_\theta J(\theta_t)
  • 含义: 在第 tt 步时,损失函数 JJ 对参数 θ\theta 的导数
  • 物理含义: 此时此刻脚下的坡度(加速度)
  • 维度: 与参数 θ\theta 的维度相同(如果模型有 100 万个参数,梯度就是 100 万维的向量)
  • 作用: 这是每一步的"新信息源",告诉优化器当前应该往哪个方向走

3.2 mtm_t:一阶矩估计 (First Moment Estimate)

mt=β1mt1+(1β1)gtm_t = \beta_1 \cdot m_{t-1} + (1-\beta_1) \cdot g_t
  • 含义: 梯度的指数移动平均值(Exponential Moving Average, EMA)
  • 初始值: m0=0m_0 = 0(全零向量)
  • 物理含义: 相当于 Momentum 中的"速度",记录了历史梯度的加权平均方向
  • 直觉理解:
    • 如果过去几步都往东走,mtm_t 就会指向东边
    • 如果这一步突然要往西走,mtm_t 不会立刻掉头,而是慢慢转向(惯性)
  • 作用: 平滑梯度,减少噪声,保持更新方向的一致性

3.3 vtv_t:二阶矩估计 (Second Moment Estimate)

vt=β2vt1+(1β2)gt2v_t = \beta_2 \cdot v_{t-1} + (1-\beta_2) \cdot g_t^2
  • 含义: 梯度平方的指数移动平均值
  • 初始值: v0=0v_0 = 0(全零向量)
  • 注意: gt2g_t^2逐元素平方(element-wise),不是矩阵乘法
  • 物理含义: 测量每个参数的梯度有多"活跃"或"剧烈"
  • 直觉理解:
    • 如果某个参数的梯度一直很大(陡峭地形),vtv_t 就会很大
    • 如果某个参数的梯度一直很小(平坦地形),vtv_t 就会很小
  • 作用: 用于自适应调整学习率vtv_t 大的参数,学习率会被缩小

3.4 β1\beta_1:一阶矩衰减率 (First Moment Decay Rate)

  • 含义: 控制一阶矩 mtm_t 的衰减速度
  • 典型值: 0.9
  • 取值范围: [0,1)[0, 1)
  • 物理含义: 惯性系数,决定保留多少历史信息
  • 直觉理解:
    • β1=0.9\beta_1 = 0.9 意味着:mtm_t 中有 90% 来自历史,10% 来自当前梯度
    • β1=0\beta_1 = 0:完全没有惯性,mt=gtm_t = g_t,退化成普通梯度
    • β1=0.99\beta_1 = 0.99:惯性非常大,方向变化很慢
  • 调参建议:
    • 默认值 0.9 通常就够用
    • 如果训练不稳定,可以尝试增大到 0.95

3.5 β2\beta_2:二阶矩衰减率 (Second Moment Decay Rate)

  • 含义: 控制二阶矩 vtv_t 的衰减速度
  • 典型值: 0.999
  • 取值范围: [0,1)[0, 1)
  • 物理含义: 路况记忆系数,决定记住多久的地形信息
  • 直觉理解:
    • β2=0.999\beta_2 = 0.999 意味着:vtv_t 有 99.9% 来自历史,只有 0.1% 来自当前
    • 这使得 vtv_t 变化非常缓慢,是一个长期的"能量估计"
    • β2\beta_2β1\beta_1 大,是因为我们希望二阶矩更稳定
  • 调参建议:
    • 默认值 0.999 几乎不需要改
    • 如果遇到训练后期学习率过小的问题,可以尝试 0.99

3.6 m^t\hat{m}_t:偏差修正后的一阶矩

m^t=mt1β1t\hat{m}_t = \frac{m_t}{1-\beta_1^t}
  • 含义:mtm_t 进行偏差修正后的值
  • 为什么需要修正?
    • 因为 m0=0m_0 = 0,在训练初期,mtm_t 会偏向于 0
    • 例如 t=1t=1 时:m1=0.9×0+0.1×g1=0.1g1m_1 = 0.9 \times 0 + 0.1 \times g_1 = 0.1 g_1
    • 这比真实的梯度 g1g_1 小了 10 倍!
  • 修正原理:
    • t=1,β1=0.9t=1, \beta_1=0.9 时:分母 =10.91=0.1= 1 - 0.9^1 = 0.1
    • m^1=0.1g10.1=g1\hat{m}_1 = \frac{0.1 g_1}{0.1} = g_1,修正回正常值
  • 随时间变化:
    • 随着 tt 增大,β1t0\beta_1^t \to 0,分母 1\to 1
    • 修正效果逐渐消失,这正是我们想要的(只在初期起作用)

3.7 v^t\hat{v}_t:偏差修正后的二阶矩

v^t=vt1β2t\hat{v}_t = \frac{v_t}{1-\beta_2^t}
  • 含义:vtv_t 进行偏差修正后的值
  • 修正原理:m^t\hat{m}_t 相同
  • 注意: 由于 β2=0.999\beta_2 = 0.999β1=0.9\beta_1 = 0.9 更接近 1,vtv_t 的偏差修正在更多步数内都会起作用
    • β2100=0.9991000.905\beta_2^{100} = 0.999^{100} \approx 0.905,100 步后分母才约等于 0.095

3.8 η\eta:学习率 (Learning Rate)

  • 含义: 控制每一步更新的基础步长
  • 典型值: 0.001(比 SGD 的 0.01 要小)
  • 取值范围: (0,+)(0, +\infty),实际一般在 [105,102][10^{-5}, 10^{-2}]
  • 为什么比 SGD 小?
    • Adam 自带"加速"机制,学习率设太大容易发散
    • 0.001 是论文作者推荐的默认值
  • 调参建议:
    • 初学者直接用 0.001
    • 微调预训练模型时,可以用更小的值如 2×1052 \times 10^{-5}
    • 配合学习率调度器(如 Warmup)效果更好

3.9 ϵ\epsilon:数值稳定项 (Epsilon)

  • 含义: 防止除以零的小常数
  • 典型值: 10810^{-8} (即 1e-8)
  • 取值范围: (0,1)(0, 1),通常是非常小的正数
  • 为什么需要?
    • 分母是 v^t\sqrt{\hat{v}_t},如果 v^t=0\hat{v}_t = 0,就会除以零
    • 加上 ϵ\epsilon 确保分母永远不为零
  • 调参建议:
    • 几乎不需要调整
    • 在某些精度敏感的场景(如半精度训练),可能需要增大到 10610^{-6}10410^{-4}

3.10 θt\theta_t:模型参数 (Model Parameters)

  • 含义: 模型在第 tt 步时的所有可训练参数
  • 示例: 神经网络中的权重矩阵 WW 和偏置 bb
  • 维度: 可以是数百万甚至数十亿维的向量
  • 作用: 这是我们要优化的目标,通过不断更新 θ\theta 来最小化损失函数

3.11 tt:时间步 (Time Step)

  • 含义: 当前是第几次参数更新(从 1 开始计数)
  • 作用:
    • 用于偏差修正公式中的指数计算
    • tt 越大,偏差修正的影响越小
  • 注意: 每调用一次 optimizer.step()tt 就加 1

4. 公式的直觉理解

让我们用人话翻译最终更新公式:

θt+1=θtηm^tv^t+ϵ\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}

分子 m^t\hat{m}_t:往这个方向走!

  • 基于惯性和当前坡度计算出的前进方向
  • 融合了历史信息和当前梯度

分母 v^t\sqrt{\hat{v}_t}:根据路况调整步幅!

  • 如果某个参数的梯度一直很大(陡峭),vtv_t 很大 → 分母很大 → 步长变小(小心翼翼)
  • 如果某个参数的梯度一直很小(平坦),vtv_t 很小 → 分母很小 → 步长变大(大步流星)

为什么要开根号?

  • vtv_t 是梯度平方的平均,量纲是"梯度²"
  • 开根号后变回"梯度"的量纲,与分子 mtm_t 匹配
  • 这样分数的结果是一个无量纲的"调整系数"

5. 形象比喻:全地形越野车

优化器比喻特点
SGD你的脚深一脚浅一脚,完全看当前地形
Momentum大铁球有惯性,跑得快,但急转弯困难
Adam智能越野车自适应悬挂 + 强劲引擎

Adam 的两个核心系统:

  • 引擎 (Momentum mtm_t):提供持续向前的动力,保持方向稳定
  • 自适应悬挂 (RMSProp vtv_t)
    • 遇到颠簸路段(梯度大),悬挂变软,减震,慢行
    • 遇到平直路段(梯度小),悬挂变硬,加速冲刺

6. PyTorch 代码实现

基础用法

import torch.optim as optim

# 参数对应公式里的符号:
# lr: 学习率 (η)
# betas: 元组 (β1, β2)
# eps: (epsilon) 防止除以零

optimizer = optim.Adam(
model.parameters(),
lr=0.001, # η: 学习率
betas=(0.9, 0.999), # (β1, β2): 一阶矩和二阶矩的衰减率
eps=1e-8 # ε: 数值稳定项
)

完整训练示例

import torch
import torch.nn as nn
import torch.optim as optim

# 创建简单数据
X = torch.randn(100, 10)
y = torch.randn(100, 1)

# 定义模型
model = nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 1)
)

# Adam 优化器
optimizer = optim.Adam(
model.parameters(),
lr=0.001,
betas=(0.9, 0.999),
eps=1e-8
)
criterion = nn.MSELoss()

# 训练循环
for epoch in range(100):
# 前向传播
pred = model(X)
loss = criterion(pred, y)

# 反向传播
optimizer.zero_grad() # 清零梯度
loss.backward() # 计算梯度 (g_t)
optimizer.step() # 更新参数 (内部更新 m_t, v_t, θ)

if (epoch + 1) % 20 == 0:
print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')

手动实现 Adam(理解原理)

import torch

def adam_step(params, grads, m, v, t, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8):
"""
手动实现 Adam 一步更新

Args:
params: 模型参数列表
grads: 梯度列表 (g_t)
m: 一阶矩列表 (m_{t-1})
v: 二阶矩列表 (v_{t-1})
t: 时间步
lr: 学习率 (η)
beta1: 一阶矩衰减率 (β1)
beta2: 二阶矩衰减率 (β2)
eps: 数值稳定项 (ε)
"""
for i, (param, grad) in enumerate(zip(params, grads)):
# 更新一阶矩: m_t = β1 * m_{t-1} + (1-β1) * g_t
m[i] = beta1 * m[i] + (1 - beta1) * grad

# 更新二阶矩: v_t = β2 * v_{t-1} + (1-β2) * g_t^2
v[i] = beta2 * v[i] + (1 - beta2) * grad ** 2

# 偏差修正
m_hat = m[i] / (1 - beta1 ** t) # m̂_t
v_hat = v[i] / (1 - beta2 ** t) # v̂_t

# 更新参数: θ_{t+1} = θ_t - η * m̂_t / (√v̂_t + ε)
param.data -= lr * m_hat / (torch.sqrt(v_hat) + eps)

7. 超参数总结表

符号PyTorch 参数默认值含义调参建议
η\etalr0.001学习率最重要的超参数,可尝试 1e-4 到 1e-2
β1\beta_1betas[0]0.9一阶矩衰减率很少需要调整
β2\beta_2betas[1]0.999二阶矩衰减率很少需要调整
ϵ\epsiloneps1e-8数值稳定项半精度训练可增大到 1e-6

8. Adam vs SGD:什么时候用哪个?

虽然 Adam 听起来完美,但在顶级的学术论文中,你依然会看到很多人用 SGD + Momentum。

Adam 的优点

  • 无脑好用: 鲁棒性好,大多数时候不需要怎么调参,收敛快
  • 适合稀疏数据: 比如 NLP(自然语言处理)任务
  • 适合初学者: 默认参数就能工作得很好

Adam 的缺点

  • 泛化能力稍弱: 有时候收敛到"尖锐"的局部最优解,测试集表现不如 SGD
  • 可能过拟合: 收敛太快,可能没有充分探索参数空间

使用建议

场景推荐优化器
做实验、搞 demo、刚上手Adam
NLP 任务(Transformer 等)AdamAdamW
刷 SOTA、训练顶级模型SGD + MomentumAdamW
微调预训练模型AdamW(修复了权重衰减的 bug)

9. 常见问题

Q1: 为什么 Adam 的学习率通常设为 0.001 而不是 0.01?

Adam 自带"加速"机制,一阶矩会累积梯度,如果学习率设太大,很容易发散。0.001 是论文作者经过大量实验推荐的默认值。

Q2: β1 和 β2 为什么默认值不一样?

  • β1=0.9\beta_1 = 0.9:一阶矩需要相对快速地响应梯度变化
  • β2=0.999\beta_2 = 0.999:二阶矩需要更稳定,是一个长期的"能量估计"

Q3: 偏差修正什么时候可以忽略?

训练约 1000 步后,β110000\beta_1^{1000} \approx 0β210000.37\beta_2^{1000} \approx 0.37,修正效果已经很小了。但前几十步的修正非常重要,不能省略。


10. 相关链接