跳转至

07-优化器进阶

学习时间: 约6-8小时 难度级别: ⭐⭐⭐ 中级 前置知识: 神经网络基础、反向传播算法、损失函数与优化基础 学习目标: 深入理解各种优化算法的数学原理与实现,掌握学习率调度策略


目录

优化器收敛速度对比:SGD、Momentum、Adam、AdamW的损失下降和准确率提升


1. 优化问题概述

1.1 深度学习中的优化目标

深度学习的训练本质上是一个非凸优化问题:

\[\min_{\theta} \mathcal{L}(\theta) = \frac{1}{N} \sum_{i=1}^{N} \ell(f(x_i; \theta), y_i)\]

其中 \(\theta\) 是模型参数,\((x_i, y_i)\) 是训练数据,\(\ell\) 是损失函数。

1.2 优化面临的挑战

  1. 非凸性: 损失曲面存在大量局部极小值和鞍点
  2. 病态曲率: 不同方向的曲率差异不同(条件数大),导致梯度下降在某些方向上震荡
  3. 随机噪声: 使用 mini-batch 引入的梯度估计噪声
  4. 高维空间: 参数空间可能有数百万甚至数十亿维
  5. 梯度消失/爆炸: 深层网络中梯度可能变得极小或极大

1.3 鞍点问题

在高维空间中,鞍点比局部极小值更常见。在一个 \(n\) 维空间中,一个临界点成为局部极小值需要所有 \(n\) 个方向都是"碗底",概率为 \((\frac{1}{2})^n\)(假设各方向等概率)。因此高维中几乎所有临界点都是鞍点。

梯度下降优化器对比:批量梯度下降、随机梯度下降、小批量梯度下降


2. 梯度下降变体

2.1 批量梯度下降(Batch Gradient Descent)

使用全部训练数据计算梯度:

\[\theta_{t+1} = \theta_t - \eta \nabla_{\theta} \mathcal{L}(\theta_t) = \theta_t - \frac{\eta}{N} \sum_{i=1}^{N} \nabla_{\theta} \ell_i\]

优点: 梯度准确,收敛稳定 缺点: 计算代价大,无法利用数据冗余,容易陷入局部极小

2.2 随机梯度下降(SGD)

每次仅用一个样本计算梯度:

\[\theta_{t+1} = \theta_t - \eta \nabla_{\theta} \ell(f(x_i; \theta_t), y_i)\]

优点: 计算快,随机性有助于逃离局部极小 缺点: 梯度噪声大,收敛不稳定

2.3 小批量梯度下降(Mini-batch SGD)

实践中最常用的折中方案:

\[\theta_{t+1} = \theta_t - \eta \cdot \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \nabla_{\theta} \ell_i\]

其中 \(\mathcal{B}\) 是大小为 \(B\) 的 mini-batch(通常 \(B\) = 32, 64, 128, 256)。

Python
import torch
import torch.nn as nn

# ===== 手动实现 Mini-batch SGD =====
class ManualSGD:
    def __init__(self, params, lr=0.01):  # __init__构造方法,创建对象时自动调用
        self.params = list(params)
        self.lr = lr

    def step(self):
        with torch.no_grad():  # 禁用梯度计算,节省内存
            for param in self.params:
                if param.grad is not None:
                    param -= self.lr * param.grad

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# ===== PyTorch 中的 SGD =====
model = nn.Linear(10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 训练步骤
x = torch.randn(32, 10)
y = torch.randn(32, 1)
criterion = nn.MSELoss()

output = model(x)
loss = criterion(output, y)
optimizer.zero_grad()  # 清零梯度,防止梯度累积
loss.backward()  # 反向传播计算梯度
optimizer.step()  # 根据梯度更新模型参数

3. 动量方法

动量方法可视化:Momentum减少震荡,Nesterov加速收敛

3.1 经典动量(Momentum)

动量法借鉴了物理学中的动量概念,利用过去梯度的指数移动平均来加速优化:

\[v_t = \mu v_{t-1} + \eta \nabla_{\theta} \mathcal{L}(\theta_t)$$ $$\theta_{t+1} = \theta_t - v_t\]

其中 \(\mu\) 是动量系数(通常为 0.9 或 0.99),\(v_t\) 是速度向量。

直觉理解:想象一个球从山坡上滚下来。动量使球在梯度一致的方向上加速,在梯度相反的方向上减速(因为正负梯度会相互抵消),从而有效减少震荡。

注意: PyTorch 中的 Momentum 实现与上述公式略有不同,使用的是 \(v_t = \mu v_{t-1} + \nabla_\theta \mathcal{L}\)\(\theta_{t+1} = \theta_t - \eta v_t\)

Python
# ===== 手动实现带动量的 SGD =====
class ManualSGDMomentum:
    def __init__(self, params, lr=0.01, momentum=0.9):
        self.params = list(params)
        self.lr = lr
        self.momentum = momentum
        self.velocities = [torch.zeros_like(p) for p in self.params]  # 列表推导式,简洁创建列表

    def step(self):
        with torch.no_grad():
            for i, param in enumerate(self.params):  # enumerate同时获取索引和元素
                if param.grad is not None:
                    self.velocities[i] = self.momentum * self.velocities[i] + param.grad
                    param -= self.lr * self.velocities[i]

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

3.2 Nesterov 加速梯度(NAG)

Nesterov 动量是经典动量的改进。核心思想:先按动量方向"预看一步",再在预看位置计算梯度

\[v_t = \mu v_{t-1} + \eta \nabla_{\theta} \mathcal{L}(\theta_t - \mu v_{t-1})$$ $$\theta_{t+1} = \theta_t - v_t\]

直觉理解:经典动量是"先走再看",Nesterov 是"先看再走"。通过在预估位置计算梯度,可以提前预知动量方向是否正确,从而更快地修正方向。

Python
# ===== 手动实现 Nesterov 动量 =====
class ManualNesterovSGD:
    def __init__(self, params, lr=0.01, momentum=0.9):
        self.params = list(params)
        self.lr = lr
        self.momentum = momentum
        self.velocities = [torch.zeros_like(p) for p in self.params]

    def step(self):
        with torch.no_grad():
            for i, param in enumerate(self.params):
                if param.grad is not None:
                    v_prev = self.velocities[i].clone()
                    self.velocities[i] = self.momentum * self.velocities[i] + self.lr * param.grad
                    # Nesterov 更新
                    param -= -self.momentum * v_prev + (1 + self.momentum) * self.velocities[i]

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)

4. 自适应学习率方法

自适应学习率方法对比:Adagrad、RMSProp和Adam的学习率变化

4.1 Adagrad

Adagrad(Duchi et al., 2011)为每个参数自适应地调整学习率。频繁更新的参数获得较小的学习率,不频繁更新的参数获得较大的学习率。

算法

\[G_t = G_{t-1} + g_t^2 \quad \text{(逐元素平方和累加)}\]
\[\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \odot g_t\]

其中 \(g_t = \nabla_\theta \mathcal{L}(\theta_t)\)\(G_t\) 是梯度平方的累积和。

优点: 适合稀疏数据和特征 缺点: 学习率单调递减,后期可能过小导致训练过早停止

Python
# ===== 手动实现 Adagrad =====
class ManualAdagrad:
    def __init__(self, params, lr=0.01, eps=1e-8):
        self.params = list(params)
        self.lr = lr
        self.eps = eps
        self.squared_grads = [torch.zeros_like(p) for p in self.params]

    def step(self):
        with torch.no_grad():
            for i, param in enumerate(self.params):
                if param.grad is not None:
                    self.squared_grads[i] += param.grad ** 2
                    param -= self.lr * param.grad / (torch.sqrt(self.squared_grads[i]) + self.eps)

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置
optimizer = torch.optim.Adagrad(model.parameters(), lr=0.01)

4.2 RMSProp

RMSProp(Hinton, 2012)解决了 Adagrad 学习率单调递减的问题,使用指数移动平均代替简单累加:

\[E[g^2]_t = \rho \cdot E[g^2]_{t-1} + (1-\rho) \cdot g_t^2\]
\[\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{E[g^2]_t + \epsilon}} \cdot g_t\]

其中 \(\rho\) 是衰减率(通常为 0.9 或 0.99)。

直觉理解:RMSProp 只记住"最近"的梯度历史,使得学习率可以适当增大(当近期梯度较小时)。

Python
# ===== 手动实现 RMSProp =====
class ManualRMSProp:
    def __init__(self, params, lr=0.001, rho=0.9, eps=1e-8):
        self.params = list(params)
        self.lr = lr
        self.rho = rho
        self.eps = eps
        self.sq_avg = [torch.zeros_like(p) for p in self.params]

    def step(self):
        with torch.no_grad():
            for i, param in enumerate(self.params):
                if param.grad is not None:
                    self.sq_avg[i] = self.rho * self.sq_avg[i] + (1 - self.rho) * param.grad ** 2
                    param -= self.lr * param.grad / (torch.sqrt(self.sq_avg[i]) + self.eps)

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置
optimizer = torch.optim.RMSprop(model.parameters(), lr=0.001, alpha=0.9)

5. Adam 系列优化器

Adam算法流程图:初始化→计算梯度→更新一阶矩→更新二阶矩→偏差修正→参数更新

5.1 Adam(Adaptive Moment Estimation)

Adam(Kingma & Ba, 2015)结合了 Momentum 和 RMSProp 的优点,同时维护梯度的一阶矩(均值)和二阶矩(未中心化方差)的指数移动平均:

一阶矩估计(动量项): $\(m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t\)$

二阶矩估计(自适应学习率项): $\(v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2\)$

偏差修正(初始阶段 \(m_t\)\(v_t\) 偏向零,需要修正): $\(\hat{m}_t = \frac{m_t}{1-\beta_1^t}\)$ $\(\hat{v}_t = \frac{v_t}{1-\beta_2^t}\)$

参数更新: $\(\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t\)$

默认超参数\(\beta_1=0.9\)\(\beta_2=0.999\)\(\epsilon=10^{-8}\)\(\eta=0.001\)

Python
# ===== 手动实现 Adam =====
class ManualAdam:
    def __init__(self, params, lr=0.001, betas=(0.9, 0.999), eps=1e-8):
        self.params = list(params)
        self.lr = lr
        self.beta1, self.beta2 = betas
        self.eps = eps
        self.m = [torch.zeros_like(p) for p in self.params]  # 一阶矩
        self.v = [torch.zeros_like(p) for p in self.params]  # 二阶矩
        self.t = 0  # 时间步

    def step(self):
        self.t += 1
        with torch.no_grad():
            for i, param in enumerate(self.params):
                if param.grad is not None:
                    g = param.grad

                    # 更新一阶矩和二阶矩
                    self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * g
                    self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * g ** 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 / (torch.sqrt(v_hat) + self.eps)

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999))

5.2 AdamW(Adam with Decoupled Weight Decay)

Loshchilov & Hutter(2019)指出 Adam 中的 L2 正则化与权重衰减不等价,并提出了 AdamW — 将权重衰减从梯度更新中解耦。

Adam + L2 正则化(错误方式): $\(g_t = \nabla_\theta \mathcal{L}(\theta_t) + \lambda \theta_t\)$ 然后按 Adam 流程更新。问题:L2 梯度也被自适应学习率缩放了。

AdamW(正确方式): $\(\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t - \eta \lambda \theta_t\)$

权重衰减项独立于 Adam 的自适应学习率。

Python
# ===== 手动实现 AdamW =====
class ManualAdamW:
    def __init__(self, params, lr=0.001, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01):
        self.params = list(params)
        self.lr = lr
        self.beta1, self.beta2 = betas
        self.eps = eps
        self.weight_decay = weight_decay
        self.m = [torch.zeros_like(p) for p in self.params]
        self.v = [torch.zeros_like(p) for p in self.params]
        self.t = 0

    def step(self):
        self.t += 1
        with torch.no_grad():
            for i, param in enumerate(self.params):
                if param.grad is not None:
                    g = param.grad

                    # Adam 更新
                    self.m[i] = self.beta1 * self.m[i] + (1 - self.beta1) * g
                    self.v[i] = self.beta2 * self.v[i] + (1 - self.beta2) * g ** 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 / (torch.sqrt(v_hat) + self.eps) + self.weight_decay * param)

    def zero_grad(self):
        for param in self.params:
            if param.grad is not None:
                param.grad.zero_()

# PyTorch 内置 AdamW(目前最推荐的优化器之一)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)

5.3 Adam 的已知问题

  1. 泛化性能: Adam 在某些任务上的泛化性能可能不如 SGD + Momentum
  2. \(\beta_2\) 选择: 默认的 \(\beta_2=0.999\) 在某些任务中可能不是最优
  3. 训练不稳定性: 在 Transformer 训练中可能出现 loss spike

AdamW vs Adam对比:权重衰减机制和泛化性能

5.4 各优化器数学总结

优化器 更新规则 关键特性
SGD \(\theta \leftarrow \theta - \eta g\) 最基础
Momentum \(v \leftarrow \mu v + g;\; \theta \leftarrow \theta - \eta v\) 加速收敛
Nesterov 预看位置计算梯度 更好的收敛率
Adagrad 累积梯度平方自适应 适合稀疏梯度
RMSProp 指数移动平均代替累积 解决学习率递减
Adam 一阶矩+二阶矩+偏差修正 自适应+动量
AdamW Adam+解耦权重衰减 更好的泛化

损失曲面上的优化路径对比:SGD、Momentum、Adam在Rosenbrock函数上的优化轨迹


6. 大规模训练优化器

6.1 LAMB(Layer-wise Adaptive Moments for Batch Training)

LAMB(You et al., 2020)是为大 batch size 训练设计的优化器。核心思想是对每一层的更新进行归一化,使其与参数范数成比例:

\[r_t = \frac{m_t / (1-\beta_1^t)}{\sqrt{v_t / (1-\beta_2^t)} + \epsilon} + \lambda \theta_t\]
\[\theta_{t+1} = \theta_t - \eta \cdot \frac{\|\theta_t\|}{\|r_t\|} \cdot r_t\]

其中 \(\frac{\|\theta_t\|}{\|r_t\|}\) 是信赖比(trust ratio),确保更新步长与参数量级匹配。

Python
class LAMB(torch.optim.Optimizer):
    """LAMB 优化器的简化实现"""
    def __init__(self, params, lr=0.001, betas=(0.9, 0.999), eps=1e-6, weight_decay=0.01):
        defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
        super().__init__(params, defaults)  # super()调用父类方法

    def step(self):
        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None:
                    continue

                grad = p.grad.data
                state = self.state[p]

                if len(state) == 0:
                    state['step'] = 0
                    state['m'] = torch.zeros_like(p.data)
                    state['v'] = torch.zeros_like(p.data)

                state['step'] += 1
                m, v = state['m'], state['v']
                beta1, beta2 = group['betas']

                # 更新一阶矩和二阶矩
                m.mul_(beta1).add_(grad, alpha=1 - beta1)
                v.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)

                # 偏差修正
                m_hat = m / (1 - beta1 ** state['step'])
                v_hat = v / (1 - beta2 ** state['step'])

                # Adam 更新
                adam_update = m_hat / (torch.sqrt(v_hat) + group['eps'])

                # 加权重衰减
                update = adam_update + group['weight_decay'] * p.data

                # LAMB 信赖比
                weight_norm = p.data.norm(2)
                update_norm = update.norm(2)

                if weight_norm > 0 and update_norm > 0:
                    trust_ratio = weight_norm / update_norm
                else:
                    trust_ratio = 1.0

                p.data.add_(update, alpha=-group['lr'] * trust_ratio)

7. 学习率调度策略

学习率调度策略对比:StepLR、MultiStepLR、ExponentialLR、CosineAnnealing、WarmRestarts、OneCycleLR

7.1 为什么需要学习率调度

  • 初期: 需要较大学习率快速探索参数空间
  • 中期: 适当降低学习率以更精细地优化
  • 后期: 使用很小的学习率微调到好的极小值

7.2 StepLR

每隔固定步数将学习率乘以衰减因子:

\[\eta_t = \eta_0 \cdot \gamma^{\lfloor t / T \rfloor}\]
Python
import torch.optim.lr_scheduler as lr_scheduler

optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# 每30个epoch学习率乘以0.1
scheduler = lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)

# 多步衰减
scheduler = lr_scheduler.MultiStepLR(optimizer, milestones=[30, 60, 90], gamma=0.1)

for epoch in range(100):
    train(...)
    scheduler.step()  # 在 epoch 结束时调用

7.3 Cosine Annealing(余弦退火)

学习率按余弦函数从初始值衰减到最小值:

\[\eta_t = \eta_{\min} + \frac{1}{2}(\eta_{\max} - \eta_{\min})\left(1 + \cos\left(\frac{t}{T}\pi\right)\right)\]
Python
# 余弦退火
scheduler = lr_scheduler.CosineAnnealingLR(
    optimizer,
    T_max=100,         # 周期长度(epoch 数)
    eta_min=1e-6       # 最小学习率
)

# 带热重启的余弦退火(SGDR)
scheduler = lr_scheduler.CosineAnnealingWarmRestarts(
    optimizer,
    T_0=10,            # 第一个周期长度
    T_mult=2,          # 每次重启后周期长度翻倍
    eta_min=1e-6
)

Warmup可视化:防止训练初期的loss spike,提高训练稳定性

7.4 Warmup

训练初期使用非常小的学习率,线性增长到目标学习率,然后进行常规衰减:

\[\eta_t = \begin{cases} \eta_{\max} \cdot \frac{t}{T_{\text{warmup}}} & \text{if } t < T_{\text{warmup}} \\ \text{decay}(\eta_{\max}, t - T_{\text{warmup}}) & \text{if } t \geq T_{\text{warmup}} \end{cases}\]
Python
# ===== 手动实现 Warmup + Cosine Decay =====
class WarmupCosineScheduler:
    def __init__(self, optimizer, warmup_steps, total_steps, min_lr=1e-6):
        self.optimizer = optimizer
        self.warmup_steps = warmup_steps
        self.total_steps = total_steps
        self.min_lr = min_lr
        self.base_lrs = [group['lr'] for group in optimizer.param_groups]
        self.current_step = 0

    def step(self):
        self.current_step += 1
        if self.current_step <= self.warmup_steps:
            # 线性 Warmup
            scale = self.current_step / self.warmup_steps
        else:
            # Cosine Decay
            progress = (self.current_step - self.warmup_steps) / (self.total_steps - self.warmup_steps)
            scale = 0.5 * (1 + math.cos(math.pi * progress))

        for i, group in enumerate(self.optimizer.param_groups):
            group['lr'] = self.min_lr + (self.base_lrs[i] - self.min_lr) * scale

    def get_lr(self):
        return [group['lr'] for group in self.optimizer.param_groups]

# ===== 使用 PyTorch 的 LambdaLR =====
import math

def warmup_cosine_lambda(step, warmup_steps=1000, total_steps=10000):
    if step < warmup_steps:
        return step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return 0.5 * (1 + math.cos(math.pi * progress))

scheduler = lr_scheduler.LambdaLR(
    optimizer,
    lr_lambda=lambda step: warmup_cosine_lambda(step, 1000, 10000)  # lambda匿名函数,简洁定义单行函数
)

7.5 OneCycleLR

Smith & Topin(2019)提出的超收敛策略,学习率先增后减,整个训练过程只有一个周期:

Python
# OneCycleLR
scheduler = lr_scheduler.OneCycleLR(
    optimizer,
    max_lr=0.01,           # 峰值学习率
    total_steps=10000,     # 总训练步数
    pct_start=0.3,         # warmup 阶段占总步数的比例
    anneal_strategy='cos', # 退火策略
    div_factor=25,         # 初始学习率 = max_lr / div_factor
    final_div_factor=1e4   # 最终学习率 = max_lr / final_div_factor
)

# 注意:OneCycleLR 是 step-level 的,每个 batch 调用一次
for epoch in range(num_epochs):
    for batch in dataloader:
        train_step(...)
        scheduler.step()  # 每个 batch 后调用

7.6 学习率调度可视化

Python
import matplotlib.pyplot as plt
import torch
import torch.optim.lr_scheduler as lr_scheduler

def visualize_schedulers():
    """可视化各种学习率调度策略"""
    fig, axes = plt.subplots(2, 3, figsize=(18, 10))
    total_epochs = 100

    schedulers_config = {
        'StepLR': lambda opt: lr_scheduler.StepLR(opt, step_size=30, gamma=0.1),
        'MultiStepLR': lambda opt: lr_scheduler.MultiStepLR(opt, milestones=[30, 60, 90], gamma=0.1),
        'CosineAnnealing': lambda opt: lr_scheduler.CosineAnnealingLR(opt, T_max=100, eta_min=1e-5),
        'ExponentialLR': lambda opt: lr_scheduler.ExponentialLR(opt, gamma=0.95),
        'OneCycleLR': lambda opt: lr_scheduler.OneCycleLR(opt, max_lr=0.1, total_steps=100),
        'CosineWarmRestart': lambda opt: lr_scheduler.CosineAnnealingWarmRestarts(opt, T_0=20, T_mult=2),
    }

    for ax, (name, sched_fn) in zip(axes.flat, schedulers_config.items()):  # zip按位置配对多个可迭代对象
        model = torch.nn.Linear(10, 1)
        opt = torch.optim.SGD(model.parameters(), lr=0.1)
        sched = sched_fn(opt)

        lrs = []
        for epoch in range(total_epochs):
            lrs.append(opt.param_groups[0]['lr'])
            sched.step()

        ax.plot(lrs, linewidth=2)
        ax.set_title(name, fontsize=14)
        ax.set_xlabel('Epoch')
        ax.set_ylabel('Learning Rate')
        ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('lr_schedulers.png', dpi=150)
    plt.show()

# visualize_schedulers()

8. 超参数调优方法

遍历参数组合的笛卡尔积。

Python
import itertools

# 定义搜索空间
param_grid = {
    'lr': [0.1, 0.01, 0.001],
    'weight_decay': [1e-3, 1e-4, 1e-5],
    'dropout': [0.3, 0.5, 0.7],
    'hidden_dim': [128, 256, 512]
}

# 网格搜索
best_val_acc = 0
best_params = None

for lr, wd, dp, hd in itertools.product(*param_grid.values()):
    model = build_model(hidden_dim=hd, dropout=dp)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)
    val_acc = train_and_evaluate(model, optimizer)

    if val_acc > best_val_acc:
        best_val_acc = val_acc
        best_params = {'lr': lr, 'weight_decay': wd, 'dropout': dp, 'hidden_dim': hd}

print(f"最佳参数: {best_params}, 验证准确率: {best_val_acc:.4f}")

从参数空间中随机采样,通常比网格搜索更高效(Bergstra & Bengio, 2012)。

Python
import random

def random_search(n_trials=50):
    best_val_acc = 0
    best_params = None

    for trial in range(n_trials):
        # 从分布中随机采样
        params = {
            'lr': 10 ** random.uniform(-4, -1),        # 对数均匀分布
            'weight_decay': 10 ** random.uniform(-5, -2),
            'dropout': random.uniform(0.1, 0.7),
            'hidden_dim': random.choice([64, 128, 256, 512]),
            'batch_size': random.choice([32, 64, 128, 256]),
        }

        model = build_model(hidden_dim=params['hidden_dim'], dropout=params['dropout'])
        optimizer = torch.optim.Adam(model.parameters(), lr=params['lr'], weight_decay=params['weight_decay'])
        val_acc = train_and_evaluate(model, optimizer, batch_size=params['batch_size'])

        if val_acc > best_val_acc:
            best_val_acc = val_acc
            best_params = params

        print(f"Trial {trial+1}/{n_trials}: val_acc={val_acc:.4f}, params={params}")

    return best_params, best_val_acc

8.3 贝叶斯优化

使用高斯过程(GP)或树结构 Parzen 估计器(TPE)来建模超参数与性能之间的关系。

Python
# 使用 Optuna 进行超参数搜索
# pip install optuna
import optuna

def objective(trial):
    # 定义超参数搜索空间
    lr = trial.suggest_float('lr', 1e-5, 1e-1, log=True)
    weight_decay = trial.suggest_float('weight_decay', 1e-6, 1e-2, log=True)
    dropout = trial.suggest_float('dropout', 0.1, 0.7)
    hidden_dim = trial.suggest_categorical('hidden_dim', [128, 256, 512])
    optimizer_name = trial.suggest_categorical('optimizer', ['Adam', 'AdamW', 'SGD'])

    model = build_model(hidden_dim=hidden_dim, dropout=dropout)

    if optimizer_name == 'Adam':
        optimizer = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
    elif optimizer_name == 'AdamW':
        optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
    else:
        optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=weight_decay)

    val_acc = train_and_evaluate(model, optimizer)
    return val_acc

# 创建 study 并开始搜索
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)

print(f"最佳参数: {study.best_params}")
print(f"最佳验证准确率: {study.best_value:.4f}")

8.4 学习率查找器(LR Finder)

Smith(2017)提出的方法,通过一次训练来确定最佳学习率范围:

Python
def lr_finder(model, train_loader, criterion, optimizer,
              start_lr=1e-7, end_lr=10, num_steps=200):
    """学习率查找器"""
    lrs, losses = [], []
    lr_mult = (end_lr / start_lr) ** (1 / num_steps)
    lr = start_lr

    # 保存初始状态
    init_state = model.state_dict().copy()

    model.train()  # train()开启训练模式
    smoothed_loss = 0
    best_loss = float('inf')

    for step, (inputs, targets) in enumerate(train_loader):
        if step >= num_steps:
            break

        # 设置学习率
        for pg in optimizer.param_groups:
            pg['lr'] = lr

        outputs = model(inputs)
        loss = criterion(outputs, targets)

        # 指数平滑
        smoothed_loss = 0.98 * smoothed_loss + 0.02 * loss.item()
        corrected_loss = smoothed_loss / (1 - 0.98 ** (step + 1))

        if corrected_loss < best_loss:
            best_loss = corrected_loss

        # 如果 loss 发散则停止
        if step > 0 and corrected_loss > 4 * best_loss:
            break

        lrs.append(lr)
        losses.append(corrected_loss)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        lr *= lr_mult

    # 恢复初始状态
    model.load_state_dict(init_state)

    # 绘制 LR vs Loss
    plt.figure(figsize=(10, 6))
    plt.plot(lrs, losses)
    plt.xscale('log')
    plt.xlabel('Learning Rate')
    plt.ylabel('Loss')
    plt.title('Learning Rate Finder')
    plt.grid(True)
    plt.show()

    # 建议:选择 loss 最快下降对应位置的学习率
    return lrs, losses

优化器选择决策树:根据任务类型选择合适的优化器

9. 优化器选择指南

9.1 常用推荐

场景 推荐优化器 学习率 调度策略
CV(CNN) SGD + Momentum 0.1 CosineAnnealing / StepLR
CV(ViT) AdamW 1e-4 ~ 3e-4 Warmup + Cosine
NLP(Transformer) AdamW 1e-4 ~ 5e-4 Warmup + Linear Decay
NLP(Fine-tuning) AdamW 2e-5 ~ 5e-5 Linear Warmup + Decay
GAN Adam (\(\beta_1\)=0.0) 1e-4 ~ 2e-4 通常不用调度
小模型/快速原型 Adam 1e-3 ReduceLROnPlateau
大 batch 分布式 LAMB / LARS 较大 Warmup + Linear

9.2 完整训练示例

Python
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import OneCycleLR
from torch.utils.data import DataLoader
import torchvision
import torchvision.transforms as transforms

def complete_training_example():
    """完整的训练流程示例"""

    # 1. 数据准备
    transform_train = transforms.Compose([
        transforms.RandomCrop(32, padding=4),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
    ])
    transform_test = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
    ])

    trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
    testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
    train_loader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)  # DataLoader批量加载数据,支持shuffle和多进程
    test_loader = DataLoader(testset, batch_size=128, shuffle=False, num_workers=2)

    # 2. 模型
    model = torchvision.models.resnet18(num_classes=10)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)  # .to(device)将数据移至GPU/CPU

    # 3. 优化器配置
    num_epochs = 100

    # 方案A:SGD + Momentum + Cosine
    optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)

    # 方案B:AdamW + OneCycleLR
    # optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
    # scheduler = OneCycleLR(optimizer, max_lr=1e-3, epochs=num_epochs, steps_per_epoch=len(train_loader))

    criterion = nn.CrossEntropyLoss()

    # 4. 训练循环
    best_acc = 0
    for epoch in range(num_epochs):
        model.train()
        running_loss = 0
        correct = 0
        total = 0

        for inputs, targets in train_loader:
            inputs, targets = inputs.to(device), targets.to(device)

            optimizer.zero_grad()
            outputs = model(inputs)
            loss = criterion(outputs, targets)
            loss.backward()

            # 梯度裁剪
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

            optimizer.step()
            # scheduler.step()  # 如果用 OneCycleLR,在这里调用

            running_loss += loss.item()
            _, predicted = outputs.max(1)
            total += targets.size(0)
            correct += predicted.eq(targets).sum().item()

        scheduler.step()  # 如果用 CosineAnnealing,在 epoch 结束时调用

        train_acc = 100. * correct / total

        # 5. 验证
        model.eval()
        correct = 0
        total = 0
        with torch.no_grad():
            for inputs, targets in test_loader:
                inputs, targets = inputs.to(device), targets.to(device)
                outputs = model(inputs)
                _, predicted = outputs.max(1)
                total += targets.size(0)
                correct += predicted.eq(targets).sum().item()

        test_acc = 100. * correct / total
        lr = optimizer.param_groups[0]['lr']
        print(f"Epoch {epoch+1}/{num_epochs} | LR: {lr:.6f} | "
              f"Train Acc: {train_acc:.2f}% | Test Acc: {test_acc:.2f}%")

        if test_acc > best_acc:
            best_acc = test_acc
            torch.save(model.state_dict(), 'best_model.pth')

    print(f"\n最佳测试准确率: {best_acc:.2f}%")

# complete_training_example()

10. 练习与自我检查

练习题

  1. 手动实现:从零实现 SGD、Momentum、Adam 优化器,在二维 Rosenbrock 函数上可视化优化轨迹。

  2. 优化器对比:在 MNIST 上分别使用 SGD、Momentum SGD、Adam、AdamW 训练同一个网络,绘制训练曲线并比较收敛速度。

  3. 学习率调度:实现 Warmup + Cosine Annealing 调度器,在 CIFAR-10 上对比有无 Warmup 的训练效果。

  4. LR Finder:实现学习率查找器,确定你模型的最佳学习率范围。

  5. 超参数搜索:使用 Optuna 为一个图像分类任务搜索最佳超参数组合(学习率、权重衰减、优化器类型、Dropout 率)。

  6. AdamW vs Adam+L2:实验验证 AdamW 和 Adam + L2 正则化的区别,观察权重分布差异。

自我检查清单

  • 能解释 Momentum、Nesterov 动量的直觉和数学推导
  • 理解 Adagrad → RMSProp → Adam 的演进关系
  • 能从零实现 Adam 优化器
  • 深刻理解 AdamW 与 Adam + L2 正则化的区别
  • 熟悉至少3种学习率调度策略的使用方法
  • 能根据任务类型选择合适的优化器和学习率
  • 了解 Warmup 的必要性和实现方式
  • 掌握至少一种超参数调优方法
  • 能写出完整的训练循环(含优化器+调度器+梯度裁剪)

下一章: 02-卷积神经网络/01-卷积神经网络基础 — 进入卷积神经网络的世界