【AI 算法精讲 03】Adam 优化器:自适应学习率的数学推导与工程选择

文章目录

    • [一、为什么需要 Adam 优化器](#一、为什么需要 Adam 优化器)
      • [1.1 SGD 的困境](#1.1 SGD 的困境)
      • [1.2 从 SGD 到自适应学习率](#1.2 从 SGD 到自适应学习率)
    • 二、算法原理
      • [2.1 前置知识:指数移动平均](#2.1 前置知识:指数移动平均)
      • [2.2 一阶动量:梯度方向的指数平均](#2.2 一阶动量:梯度方向的指数平均)
      • [2.3 二阶动量:梯度幅度的指数平均](#2.3 二阶动量:梯度幅度的指数平均)
      • [2.4 参数更新公式](#2.4 参数更新公式)
      • [2.5 完整算法流程](#2.5 完整算法流程)
      • [2.6 与其他优化器的关系](#2.6 与其他优化器的关系)
        • AdaGrad
        • RMSProp
        • [Momentum SGD](#Momentum SGD)
        • [Adam = Momentum + RMSProp](#Adam = Momentum + RMSProp)
      • [2.7 偏差校正的必要性](#2.7 偏差校正的必要性)
      • [2.8 AdamW:权重衰减解耦](#2.8 AdamW:权重衰减解耦)
    • [三、Python 实现](#三、Python 实现)
      • [3.1 从零实现:手写 Adam 优化器](#3.1 从零实现:手写 Adam 优化器)
      • [3.2 PyTorch 中的 Adam 和 AdamW](#3.2 PyTorch 中的 Adam 和 AdamW)
        • [关键 API 对比](#关键 API 对比)
    • 四、参数调优与变体对比
      • [4.1 超参数推荐](#4.1 超参数推荐)
      • [4.2 学习率预热(Warmup)](#4.2 学习率预热(Warmup))
      • [4.3 优化器对比实验](#4.3 优化器对比实验)
      • [4.4 AMSGrad:解决二阶矩过小问题](#4.4 AMSGrad:解决二阶矩过小问题)
    • 五、在客服系统中的实际应用
      • [5.1 场景:客服工单分类](#5.1 场景:客服工单分类)
      • [5.2 优化器选择决策](#5.2 优化器选择决策)
    • 六、常见陷阱
      • [陷阱 1:忘记偏差校正的后果](#陷阱 1:忘记偏差校正的后果)
      • [陷阱 2:Adam 的 weight_decay 不是 AdamW 的 weight_decay](#陷阱 2:Adam 的 weight_decay 不是 AdamW 的 weight_decay)
      • [陷阱 3: ϵ \epsilon ϵ 的位置影响数值稳定性](#陷阱 3: ϵ \epsilon ϵ 的位置影响数值稳定性)
      • [陷阱 4:稀疏梯度下 β 2 \beta_2 β2 的选择](#陷阱 4:稀疏梯度下 β 2 \beta_2 β2 的选择)
      • [陷阱 5:Adam 不保证收敛到全局最优](#陷阱 5:Adam 不保证收敛到全局最优)
      • [陷阱 6:大批量训练需要调整 ϵ \epsilon ϵ](#陷阱 6:大批量训练需要调整 ϵ \epsilon ϵ)
    • 七、总结

一、为什么需要 Adam 优化器

1.1 SGD 的困境

梯度下降是最基础的优化算法,但在深度学习中面临三个核心问题:

学习率统一问题 :标准 SGD 对所有参数使用相同的学习率 η \eta η。但在实际模型中,不同参数的梯度量级可能差异巨大------嵌入层参数梯度可能在 10 − 6 10^{-6} 10−6 量级,而全连接层梯度可能在 10 − 2 10^{-2} 10−2 量级。统一学习率要么让小梯度参数更新太慢,要么让大梯度参数震荡爆炸。

稀疏梯度问题:在 NLP 和推荐系统中,Embedding 层的梯度极其稀疏------一个 batch 中只有少量 token 被激活。SGD 对这些稀疏更新的参数和频繁更新的参数使用相同的学习率,导致稀疏特征学习严重不足。

鞍点和高原:在高原区域(梯度接近零但非最优点),SGD 的动量项可能因为历史梯度方向一致而"冲过"高原,也可能因为梯度太小而完全停滞。

1.2 从 SGD 到自适应学习率

Adam(Adaptive Moment Estimation)的设计思路是:为每个参数维护独立的自适应学习率,这个学习率基于该参数的梯度历史动态调整。梯度大的参数缩小学习率(防止震荡),梯度小的参数放大学习率(加速学习)。

关键洞察:Adam 不是一个新的优化算法,而是「动量加速」和「自适应学习率」两种思想的融合,并通过偏差校正解决了冷启动问题。


二、算法原理

2.1 前置知识:指数移动平均

指数移动平均(EMA)是 Adam 的核心组件。给定序列 { g 1 , g 2 , ... , g t } \{g_1, g_2, \ldots, g_t\} {g1,g2,...,gt},EMA 定义为:

m t = β ⋅ m t − 1 + ( 1 − β ) ⋅ g t , m 0 = 0 m_t = \beta \cdot m_{t-1} + (1 - \beta) \cdot g_t, \quad m_0 = 0 mt=β⋅mt−1+(1−β)⋅gt,m0=0

展开递推公式:

m t = ( 1 − β ) ∑ i = 1 t β t − i ⋅ g i m_t = (1 - \beta) \sum_{i=1}^{t} \beta^{t-i} \cdot g_i mt=(1−β)i=1∑tβt−i⋅gi

  • β \beta β 控制衰减速率, β \beta β 越大,历史信息权重越高
  • 有效窗口大小约为 1 1 − β \frac{1}{1-\beta} 1−β1: β = 0.9 \beta = 0.9 β=0.9 时约看最近 10 步, β = 0.999 \beta = 0.999 β=0.999 时约看最近 1000 步
偏差校正

当 m 0 = 0 m_0 = 0 m0=0 时,初期估计严重偏低: m 1 = ( 1 − β ) g 1 m_1 = (1-\beta) g_1 m1=(1−β)g1,只有真实值的 1 − β 1-\beta 1−β 倍。

m ^ t = m t 1 − β t \hat{m}_t = \frac{m_t}{1 - \beta^t} m^t=1−βtmt

当 t → ∞ t \to \infty t→∞ 时, β t → 0 \beta^t \to 0 βt→0, m ^ t → m t \hat{m}_t \to m_t m^t→mt,校正自动消失。

2.2 一阶动量:梯度方向的指数平均

Adam 维护梯度的一阶矩(均值):

m t = β 1 ⋅ m t − 1 + ( 1 − β 1 ) ⋅ g t m_t = \beta_1 \cdot m_{t-1} + (1 - \beta_1) \cdot g_t mt=β1⋅mt−1+(1−β1)⋅gt

m ^ t = m t 1 − β 1 t \hat{m}_t = \frac{m_t}{1 - \beta_1^t} m^t=1−β1tmt

  • m t m_t mt:梯度的指数移动平均,起平滑作用(类似 Momentum)
  • m ^ t \hat{m}_t m^t:偏差校正后的一阶矩估计
  • 默认 β 1 = 0.9 \beta_1 = 0.9 β1=0.9

2.3 二阶动量:梯度幅度的指数平均

Adam 同时维护梯度的二阶矩(未中心化方差):

v t = β 2 ⋅ v t − 1 + ( 1 − β 2 ) ⋅ g t 2 v_t = \beta_2 \cdot v_{t-1} + (1 - \beta_2) \cdot g_t^2 vt=β2⋅vt−1+(1−β2)⋅gt2

v ^ t = v t 1 − β 2 t \hat{v}_t = \frac{v_t}{1 - \beta_2^t} v^t=1−β2tvt

  • v t v_t vt:梯度平方的指数移动平均,反映梯度幅度
  • v ^ t \hat{v}_t v^t:偏差校正后的二阶矩估计
  • 默认 β 2 = 0.999 \beta_2 = 0.999 β2=0.999
  • g t 2 g_t^2 gt2 表示逐元素平方

2.4 参数更新公式

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

逐元素理解:

θ i , t + 1 = θ i , t − η v ^ i , t + ϵ ⋅ m ^ i , t \theta_{i,t+1} = \theta_{i,t} - \frac{\eta}{\sqrt{\hat{v}{i,t}} + \epsilon} \cdot \hat{m}{i,t} θi,t+1=θi,t−v^i,t +ϵη⋅m^i,t

  • η v ^ i , t + ϵ \frac{\eta}{\sqrt{\hat{v}_{i,t}} + \epsilon} v^i,t +ϵη:参数 i i i 的有效学习率,随梯度幅度自适应调整
  • m ^ i , t \hat{m}_{i,t} m^i,t:参数 i i i 的更新方向,是平滑后的梯度
  • ϵ \epsilon ϵ:数值稳定项,防止除零(默认 10 − 8 10^{-8} 10−8)

直觉

  • 梯度一直很大的参数 → v t v_t vt 大 → 有效学习率小 → 自动减速
  • 梯度一直很小的参数 → v t v_t vt 小 → 有效学习率大 → 自动加速
  • 梯度方向频繁变化的参数 → m t m_t mt 被平滑 → 减少震荡

2.5 完整算法流程

步骤 操作 公式
1 初始化 m 0 = 0 , v 0 = 0 , t = 0 m_0 = 0, v_0 = 0, t = 0 m0=0,v0=0,t=0
2 计算梯度 g t = ∇ L ( θ t − 1 ) g_t = \nabla L(\theta_{t-1}) gt=∇L(θt−1)
3 更新一阶矩 m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t mt=β1mt−1+(1−β1)gt
4 更新二阶矩 v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 vt=β2vt−1+(1−β2)gt2
5 偏差校正 m ^ t = m t / ( 1 − β 1 t ) \hat{m}_t = m_t / (1-\beta_1^t) m^t=mt/(1−β1t), v ^ t = v t / ( 1 − β 2 t ) \hat{v}_t = v_t / (1-\beta_2^t) v^t=vt/(1−β2t)
6 更新参数 θ t = θ t − 1 − η ⋅ m ^ t / ( v ^ t + ϵ ) \theta_t = \theta_{t-1} - \eta \cdot \hat{m}_t / (\sqrt{\hat{v}_t} + \epsilon) θt=θt−1−η⋅m^t/(v^t +ϵ)

2.6 与其他优化器的关系

AdaGrad

v t = ∑ i = 1 t g i 2 , θ t + 1 = θ t − η v t + ϵ ⊙ g t v_t = \sum_{i=1}^{t} g_i^2, \quad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{v_t} + \epsilon} \odot g_t vt=i=1∑tgi2,θt+1=θt−vt +ϵη⊙gt

AdaGrad 累积所有历史梯度平方,学习率单调递减,最终趋于零。在稀疏数据上表现好,但稠密梯度下过早收敛。

RMSProp

v t = β v t − 1 + ( 1 − β ) g t 2 , θ t + 1 = θ t − η v t + ϵ ⊙ g t v_t = \beta v_{t-1} + (1-\beta) g_t^2, \quad \theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{v_t} + \epsilon} \odot g_t vt=βvt−1+(1−β)gt2,θt+1=θt−vt +ϵη⊙gt

RMSProp 用 EMA 替代全量累积,解决 AdaGrad 学习率衰减问题。但只用二阶矩,没有动量加速。

Momentum SGD

m t = β m t − 1 + g t , θ t + 1 = θ t − η ⋅ m t m_t = \beta m_{t-1} + g_t, \quad \theta_{t+1} = \theta_t - \eta \cdot m_t mt=βmt−1+gt,θt+1=θt−η⋅mt

Momentum 只用一阶矩,没有自适应学习率。

Adam = Momentum + RMSProp

m t = β 1 m t − 1 + ( 1 − β 1 ) g t ⏟ Momentum + v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 ⏟ RMSProp + 偏差校正 ⏟ 冷启动修复 \underbrace{m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t}{\text{Momentum}} + \underbrace{v_t = \beta_2 v{t-1} + (1-\beta_2) g_t^2}{\text{RMSProp}} + \underbrace{\text{偏差校正}}{\text{冷启动修复}} Momentum mt=β1mt−1+(1−β1)gt+RMSProp vt=β2vt−1+(1−β2)gt2+冷启动修复 偏差校正

2.7 偏差校正的必要性

不做偏差校正时, m 0 = 0 , v 0 = 0 m_0 = 0, v_0 = 0 m0=0,v0=0 导致初期估计严重偏低:

步数 t t t β 1 t \beta_1^t β1t ( β 1 = 0.9 \beta_1=0.9 β1=0.9) 1 − β 1 t 1-\beta_1^t 1−β1t β 2 t \beta_2^t β2t ( β 2 = 0.999 \beta_2=0.999 β2=0.999) 1 − β 2 t 1-\beta_2^t 1−β2t
1 0.9 0.1 0.999 0.001
10 0.349 0.651 0.990 0.010
100 0.00003 ~1.0 0.905 0.095
1000 ~0 ~1.0 0.368 0.632
10000 ~0 ~1.0 ~0.001 ~1.0
  • β 2 = 0.999 \beta_2 = 0.999 β2=0.999 时,前 1000 步二阶矩估计偏低到真实值的 0.1 % ∼ 63.2 % 0.1\% \sim 63.2\% 0.1%∼63.2%
  • 不做校正,初期有效学习率会异常偏大( v t v_t vt 偏小导致 η v t \frac{\eta}{\sqrt{v_t}} vt η 偏大)
  • 偏差校正确保初始阶段就有合理的有效学习率

2.8 AdamW:权重衰减解耦

原始 Adam 中,L2 正则化通过在梯度上加 λ θ \lambda \theta λθ 实现:

g t = ∇ L ( θ t ) + λ θ t g_t = \nabla L(\theta_t) + \lambda \theta_t gt=∇L(θt)+λθt

但这会导致权重衰减被一阶矩和二阶矩「吸收」,实际衰减量与 β 1 , β 2 \beta_1, \beta_2 β1,β2 相关,不可控。

AdamW 将权重衰减从梯度中分离,直接在参数更新时衰减:

g t = ∇ L ( θ t ) g_t = \nabla L(\theta_t) gt=∇L(θt)

θ t + 1 = θ t − η ⋅ ( m ^ t v ^ t + ϵ + λ θ t ) \theta_{t+1} = \theta_t - \eta \cdot \left( \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \theta_t \right) θt+1=θt−η⋅(v^t +ϵm^t+λθt)

区别

方案 L2 正则(Adam) 解耦权重衰减(AdamW)
衰减项位置 加到梯度上 直接加到更新量
受 m t m_t mt 影响
受 v t v_t vt 影响
衰减率可控
实际效果 衰减不均匀 均匀衰减

三、Python 实现

3.1 从零实现:手写 Adam 优化器

python 复制代码
import numpy as np
from typing import List, Callable

class AdamOptimizer:
    """从零实现的 Adam 优化器,支持偏差校正和权重衰减。"""

    def __init__(self, params: List[np.ndarray], lr: float = 1e-3,
                 betas: tuple = (0.9, 0.999), eps: float = 1e-8,
                 weight_decay: float = 0.0):
        self.params = params
        self.lr = lr
        self.beta1, self.beta2 = betas
        self.eps = eps
        self.weight_decay = weight_decay
        self.t = 0
        # 为每个参数初始化一阶矩和二阶矩
        self.m = [np.zeros_like(p) for p in params]
        self.v = [np.zeros_like(p) for p in params]

    def step(self, grads: List[np.ndarray]) -> None:
        """执行一步参数更新。"""
        self.t += 1

        for i, (param, grad) in enumerate(zip(self.params, grads)):
            # AdamW:权重衰减直接加到更新量,不加到梯度
            if self.weight_decay > 0:
                param -= self.lr * self.weight_decay * param

            # 更新一阶矩(梯度的 EMA)
            self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grad
            # 更新二阶矩(梯度平方的 EMA)
            self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grad ** 2

            # 偏差校正
            m_hat = self.m[i] / (1 - self.beta1 ** self.t)
            v_hat = self.v[i] / (1 - self.beta2 ** self.t)

            # 参数更新
            param -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)

    def zero_grad(self) -> None:
        """清空梯度(在此实现中不需要,梯度从外部传入)。"""
        pass


def train_simple_model():
    """用一个简单的二次函数验证 Adam 优化器。"""
    np.random.seed(42)

    # 目标函数:f(x, y) = (x - 3)^2 + 10 * (y + 2)^2
    # 最优点:(3, -2)
    def loss_fn(x, y):
        return (x - 3) ** 2 + 10 * (y + 2) ** 2

    def grad_fn(x, y):
        dx = 2 * (x - 3)
        dy = 20 * (y + 2)
        return np.array([dx, dy])

    # 初始参数
    params = [np.array([0.0, 0.0])]
    optimizer = AdamOptimizer(params, lr=0.1, betas=(0.9, 0.999))

    losses = []
    for step in range(200):
        x, y = params[0]
        loss = loss_fn(x, y)
        losses.append(loss)

        grads = [grad_fn(x, y)]
        optimizer.step(grads)

    final_x, final_y = params[0]
    print(f"初始位置: (0.0, 0.0)")
    print(f"最终位置: ({final_x:.4f}, {final_y:.4f})")
    print(f"目标位置: (3.0, -2.0)")
    print(f"最终损失: {losses[-1]:.8f}")
    print(f"初始损失: {losses[0]:.4f}")


if __name__ == "__main__":
    train_simple_model()

输出示例:

text 复制代码
初始位置: (0.0, 0.0)
最终位置: (2.9999, -1.9999)
目标位置: (3.0, -2.0)
最终损失: 0.00000001
初始损失: 49.0000

3.2 PyTorch 中的 Adam 和 AdamW

python 复制代码
import torch
import torch.nn as nn

class MLP(nn.Module):
    """3 层全连接网络。"""

    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, hidden_dim)
        self.fc3 = nn.Linear(hidden_dim, output_dim)
        self.relu = nn.ReLU()

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        h1 = self.relu(self.fc1(x))
        h2 = self.relu(self.fc2(h1))
        return self.fc3(h2)


def train_with_adam():
    torch.manual_seed(42)
    model = MLP(784, 128, 10)

    # Adam:L2 正则化通过 weight_decay 实现(等价于原始 Adam)
    optimizer_adam = torch.optim.Adam(
        model.parameters(), lr=1e-3, betas=(0.9, 0.999),
        eps=1e-8, weight_decay=1e-4
    )

    # AdamW:权重衰减解耦,推荐使用
    optimizer_adamw = torch.optim.AdamW(
        model.parameters(), lr=1e-3, betas=(0.9, 0.999),
        eps=1e-8, weight_decay=1e-4
    )

    criterion = nn.CrossEntropyLoss()
    X = torch.randn(100, 784)
    y = torch.randint(0, 10, (100,))

    for epoch in range(10):
        total_loss = 0.0
        for i in range(0, len(X), 32):
            batch_x = X[i:i + 32]
            batch_y = y[i:i + 32]
            optimizer_adamw.zero_grad()
            logits = model(batch_x)
            loss = criterion(logits, batch_y)
            loss.backward()
            optimizer_adamw.step()
            total_loss += loss.item()
        print(f"Epoch {epoch+1}/10, Loss: {total_loss / 4:.4f}")


if __name__ == "__main__":
    train_with_adam()
关键 API 对比
API Adam AdamW 说明
lr 基础学习率
betas ( β 1 , β 2 ) (\beta_1, \beta_2) (β1,β2)
eps 数值稳定项
weight_decay L2 正则 解耦衰减 语义不同
amsgrad 使用二阶矩最大值
python 复制代码
# AMSGrad 变体:防止二阶矩估计过小导致学习率爆炸
optimizer = torch.optim.Adam(
    model.parameters(), lr=1e-3, amsgrad=True
)

四、参数调优与变体对比

4.1 超参数推荐

参数 推荐值 调参建议
η \eta η(学习率) 1 e - 3 1e\text{-}3 1e-3 最重要超参,范围 1 e - 4 ∼ 1 e - 2 1e\text{-}4 \sim 1e\text{-}2 1e-4∼1e-2
β 1 \beta_1 β1 0.9 很少调整,NLP 任务可试 0.8
β 2 \beta_2 β2 0.999 稀疏梯度场景可试 0.99
ϵ \epsilon ϵ 1 e - 8 1e\text{-}8 1e-8 大批量训练可设 1 e - 6 1e\text{-}6 1e-6
weight_decay 1 e - 2 1e\text{-}2 1e-2 AdamW 推荐,范围 1 e - 5 ∼ 1 e - 1 1e\text{-}5 \sim 1e\text{-}1 1e-5∼1e-1

4.2 学习率预热(Warmup)

Adam 在训练初期,一阶矩和二阶矩的估计不稳定。学习率预热通过在初始阶段逐步提高学习率来缓解:

python 复制代码
import torch

class WarmupScheduler:
    """线性预热 + 余弦退火调度器。"""

    def __init__(self, optimizer: torch.optim.Optimizer,
                 warmup_steps: int, total_steps: int,
                 min_lr_ratio: float = 0.01):
        self.optimizer = optimizer
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.min_lr_ratio = min_lr_ratio
        self.base_lrs = [group['lr'] for group in optimizer.param_groups]
        self.current_step = 0

    def step(self) -> None:
        self.current_step += 1
        for i, group in enumerate(self.optimizer.param_groups):
            base_lr = self.base_lrs[i]
            if self.current_step <= self.warmup_steps:
                # 线性预热
                ratio = self.current_step / self.warmup_steps
            else:
                # 余弦退火
                progress = (self.current_step - self.warmup_steps) / \
                           (self.total_steps - self.warmup_steps)
                ratio = self.min_lr_ratio + \
                        (1 - self.min_lr_ratio) * \
                        0.5 * (1 + np.cos(np.pi * progress))
            group['lr'] = base_lr * ratio


# 使用示例
model = nn.Linear(100, 10)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = WarmupScheduler(optimizer, warmup_steps=100, total_steps=1000)

for step in range(1000):
    # 训练步骤...
    optimizer.step()
    scheduler.step()
预热策略对比
策略 公式 适用场景
线性预热 η t = η max ⁡ ⋅ t T w a r m \eta_t = \eta_{\max} \cdot \frac{t}{T_{warm}} ηt=ηmax⋅Twarmt 通用
余弦预热 η t = η max ⁡ ⋅ 0.5 ( 1 − cos ⁡ ( π t T w a r m ) ) \eta_t = \eta_{\max} \cdot 0.5(1 - \cos(\pi \frac{t}{T_{warm}})) ηt=ηmax⋅0.5(1−cos(πTwarmt)) 平滑过渡
指数预热 η t = η max ⁡ ⋅ e − 5 ( 1 − t T w a r m ) \eta_t = \eta_{\max} \cdot e^{-5(1 - \frac{t}{T_{warm}})} ηt=ηmax⋅e−5(1−Twarmt) 快速升温

4.3 优化器对比实验

python 复制代码
import torch
import torch.nn as nn
import numpy as np

def compare_optimizers():
    """对比 SGD、Momentum、AdaGrad、RMSProp、Adam 在同一任务上的表现。"""
    torch.manual_seed(42)
    X = torch.randn(500, 20)
    y = torch.randint(0, 5, (500,))

    optimizers = {
        'SGD': lambda p: torch.optim.SGD(p, lr=0.01),
        'Momentum': lambda p: torch.optim.SGD(p, lr=0.01, momentum=0.9),
        'AdaGrad': lambda p: torch.optim.Adagrad(p, lr=0.01),
        'RMSProp': lambda p: torch.optim.RMSprop(p, lr=0.01, alpha=0.99),
        'Adam': lambda p: torch.optim.Adam(p, lr=0.01, betas=(0.9, 0.999)),
        'AdamW': lambda p: torch.optim.AdamW(p, lr=0.01, weight_decay=0.01),
    }

    results = {}
    epochs = 50
    criterion = nn.CrossEntropyLoss()

    for name, opt_fn in optimizers.items():
        torch.manual_seed(42)
        model = nn.Sequential(
            nn.Linear(20, 64), nn.ReLU(),
            nn.Linear(64, 64), nn.ReLU(),
            nn.Linear(64, 5)
        )
        optimizer = opt_fn(model.parameters())

        epoch_losses = []
        for epoch in range(epochs):
            total_loss = 0.0
            for i in range(0, len(X), 32):
                batch_x = X[i:i+32]
                batch_y = y[i:i+32]
                optimizer.zero_grad()
                loss = criterion(model(batch_x), batch_y)
                loss.backward()
                optimizer.step()
                total_loss += loss.item()
            epoch_losses.append(total_loss / 16)
        results[name] = epoch_losses

    # 打印最终损失
    print(f"{'优化器':>12} | {'初始Loss':>10} | {'最终Loss':>10} | {'收敛速度':>8}")
    print("-" * 50)
    for name, losses in results.items():
        speed = "快" if losses[10] < losses[0] * 0.5 else "慢"
        print(f"{name:>12} | {losses[0]:>10.4f} | {losses[-1]:>10.4f} | {speed:>8}")

if __name__ == "__main__":
    compare_optimizers()

4.4 AMSGrad:解决二阶矩过小问题

Adam 在某些情况下二阶矩 v t v_t vt 可能意外缩小,导致有效学习率突然增大,训练发散。AMSGrad 取历史最大值:

v ^ t = max ⁡ ( v ^ t − 1 , v t ) \hat{v}t = \max(\hat{v}{t-1}, v_t) v^t=max(v^t−1,vt)

python 复制代码
# PyTorch 中使用 AMSGrad
optimizer = torch.optim.Adam(
    model.parameters(), lr=1e-3, amsgrad=True
)
变体 二阶矩处理 优点 适用场景
Adam EMA 平滑跟踪 大多数场景
AMSGrad EMA + 最大值 防止发散 不稳定训练
AdaBelief 用 g t − m t g_t - m_t gt−mt 的方差 更好的收敛 追求泛化性能

五、在客服系统中的实际应用

5.1 场景:客服工单分类

客服系统每天接收大量工单,需要自动分类到不同部门(技术支持、账单、投诉、咨询等)。使用 BERT 微调做文本分类,Adam 是训练优化器的首选。

python 复制代码
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

class TicketDataset(Dataset):
    """客服工单数据集。"""

    def __init__(self, n_samples: int = 5000, n_features: int = 768, n_classes: int = 5):
        torch.manual_seed(42)
        # 模拟 BERT embedding 后的特征
        self.features = torch.randn(n_samples, n_features)
        # 模拟类别标签
        self.labels = torch.randint(0, n_classes, (n_samples,))

    def __len__(self) -> int:
        return len(self.labels)

    def __getitem__(self, idx: int):
        return self.features[idx], self.labels[idx]


class TicketClassifier(nn.Module):
    """客服工单分类模型。"""

    def __init__(self, input_dim: int = 768, hidden_dim: int = 256,
                 num_classes: int = 5, dropout: float = 0.3):
        super().__init__()
        self.classifier = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.LayerNorm(hidden_dim),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.LayerNorm(hidden_dim // 2),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim // 2, num_classes)
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.classifier(x)


def train_ticket_classifier():
    # 数据
    dataset = TicketDataset(n_samples=5000, n_features=768, n_classes=5)
    train_size = int(0.8 * len(dataset))
    val_size = len(dataset) - train_size
    train_ds, val_ds = torch.utils.data.random_split(dataset, [train_size, val_size])
    train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
    val_loader = DataLoader(val_ds, batch_size=64, shuffle=False)

    # 模型
    model = TicketClassifier(input_dim=768, hidden_dim=256, num_classes=5, dropout=0.3)

    # AdamW 优化器 + 学习率预热
    optimizer = torch.optim.AdamW(
        model.parameters(), lr=2e-4, betas=(0.9, 0.999),
        weight_decay=0.01, eps=1e-8
    )

    scheduler = WarmupScheduler(optimizer, warmup_steps=50, total_steps=500)
    criterion = nn.CrossEntropyLoss(label_smoothing=0.1)

    best_val_acc = 0.0
    for epoch in range(10):
        # 训练
        model.train()
        total_loss = 0.0
        for batch_x, batch_y in train_loader:
            optimizer.zero_grad()
            logits = model(batch_x)
            loss = criterion(logits, batch_y)
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()
            scheduler.step()
            total_loss += loss.item()

        # 验证
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for batch_x, batch_y in val_loader:
                logits = model(batch_x)
                preds = logits.argmax(dim=1)
                correct += (preds == batch_y).sum().item()
                total += len(batch_y)

        val_acc = correct / total
        avg_loss = total_loss / len(train_loader)
        print(f"Epoch {epoch+1}/10 | Loss: {avg_loss:.4f} | Val Acc: {val_acc:.4f} | LR: {optimizer.param_groups[0]['lr']:.6f}")

        if val_acc > best_val_acc:
            best_val_acc = val_acc
            torch.save(model.state_dict(), 'best_ticket_classifier.pt')

    print(f"\n最佳验证准确率: {best_val_acc:.4f}")


if __name__ == "__main__":
    train_ticket_classifier()

5.2 优化器选择决策

在客服系统的不同阶段,优化器选择策略不同:

阶段 优化器 学习率 理由
预训练 AdamW 2 e - 4 2e\text{-}4 2e-4 + warmup 需要快速收敛
微调 AdamW 5 e - 5 5e\text{-}5 5e-5 小学习率保护预训练特征
增量训练 SGD + Momentum 1 e - 3 1e\text{-}3 1e-3 新数据量小,避免过拟合
超参搜索 Adam 1 e - 3 1e\text{-}3 1e-3 快速评估不同架构

六、常见陷阱

陷阱 1:忘记偏差校正的后果

不做偏差校正时,训练初期有效学习率是正常值的约 1 1 − β 2 = 1 0.001 ≈ 31.6 \frac{1}{\sqrt{1-\beta_2}} = \frac{1}{\sqrt{0.001}} \approx 31.6 1−β2 1=0.001 1≈31.6 倍。这会导致初始参数更新过大,模型可能直接发散。

python 复制代码
# 错误:手动实现 Adam 时忘记偏差校正
class AdamNoBiasCorrection:
    def step(self, grads):
        for i, grad in enumerate(grads):
            self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * grad
            self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * grad ** 2
            # 缺少偏差校正!
            self.params[i] -= self.lr * self.m[i] / (np.sqrt(self.v[i]) + self.eps)
            # 初期 v[i] ≈ (1-beta2)*grad^2 ≈ 0.001*grad^2
            # 有效学习率 ≈ lr / sqrt(0.001) / grad ≈ 31.6 * lr / grad

陷阱 2:Adam 的 weight_decay 不是 AdamW 的 weight_decay

PyTorch 中 torch.optim.Adam(weight_decay=0.01) 把权重衰减加到梯度上,受 β 1 , β 2 \beta_1, \beta_2 β1,β2 影响,衰减不均匀。torch.optim.AdamW(weight_decay=0.01) 是解耦的,衰减均匀可控。

python 复制代码
# 两者不等价!
optimizer_a = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=0.01)   # L2 正则
optimizer_b = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)   # 解耦衰减

# 推荐使用 AdamW,特别是 Transformer 模型

陷阱 3: ϵ \epsilon ϵ 的位置影响数值稳定性

原始论文中 ϵ \epsilon ϵ 在 v t \sqrt{v_t} vt 外面: η v t + ϵ \frac{\eta}{\sqrt{v_t} + \epsilon} vt +ϵη

PyTorch 实现中 ϵ \epsilon ϵ 在 v t + ϵ \sqrt{v_t + \epsilon} vt+ϵ 里面

当 v t ≫ ϵ v_t \gg \epsilon vt≫ϵ 时两者等价,但当 v t ≈ 0 v_t \approx 0 vt≈0 时差异显著:

python 复制代码
# 原始 Adam 论文公式
update = lr * m_hat / (np.sqrt(v_hat) + eps)

# PyTorch 实现
update = lr * m_hat / np.sqrt(v_hat + eps)

# 当 v_hat ≈ 0 时:
# 论文公式:lr * m_hat / eps  → 可能非常大
# PyTorch:lr * m_hat / sqrt(eps) → 相对温和

陷阱 4:稀疏梯度下 β 2 \beta_2 β2 的选择

在 NLP 模型中,Embedding 层的某些参数可能长时间不被更新。默认 β 2 = 0.999 \beta_2 = 0.999 β2=0.999 意味着有效窗口约 1000 步。如果一个 Embedding 参数每 500 步才被更新一次,二阶矩估计会严重过时。

python 复制代码
# 对于稀疏梯度场景,减小 beta2
optimizer = torch.optim.AdamW(
    model.parameters(), lr=1e-3,
    betas=(0.9, 0.99),  # beta2=0.99 而非 0.999
    weight_decay=0.01
)

陷阱 5:Adam 不保证收敛到全局最优

Adam 在非凸优化中没有全局最优保证。特别是在学习率固定的情况下,可能陷入鞍点附近的高原区域。解决方案:

python 复制代码
# 使用余弦退火让学习率周期性降低
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=100, eta_min=1e-6
)

# 或使用 Lookahead 包装器提高稳定性
# base_optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)
# lookahead = Lookahead(base_optimizer, k=5, alpha=0.5)

陷阱 6:大批量训练需要调整 ϵ \epsilon ϵ

大批量训练时,梯度方差更小, v t v_t vt 可能非常小。默认 ϵ = 1 e - 8 \epsilon = 1e\text{-}8 ϵ=1e-8 可能导致有效学习率过大。

python 复制代码
# 大批量训练(batch_size > 4096)时增大 epsilon
optimizer = torch.optim.AdamW(
    model.parameters(), lr=1e-3,
    eps=1e-6,  # 而非默认的 1e-8
    weight_decay=0.01
)

七、总结

维度 详情
核心思想 一阶矩(动量)+ 二阶矩(自适应学习率)+ 偏差校正
关键参数 η \eta η(最重要)、 β 1 = 0.9 \beta_1=0.9 β1=0.9、 β 2 = 0.999 \beta_2=0.999 β2=0.999、 ϵ = 1 e - 8 \epsilon=1e\text{-}8 ϵ=1e-8
优势 自适应学习率、收敛快、对超参数不敏感、适合稀疏梯度
劣势 泛化性能可能不如 SGD+Momentum、内存占用翻倍(存 m m m 和 v v v)
降级策略 训练不稳定 → AMSGrad;泛化差 → SGD+Momentum 或 Lookahead
选型建议 默认 AdamW + warmup;学术研究对比 SGD+Momentum;大模型训练用 AdamW
与 SGD 关系 Adam 在稠密梯度上优于 SGD,稀疏梯度上优势更明显
与 AdamW 关系 AdamW 是 Adam 的改进版,权重衰减更合理,Transformer 标配