📚 课程目录

第5课:优化器:SGD与Adam

📖 本课概述

优化器决定了模型参数如何更新。从最朴素的SGD到自适应的Adam,不同的优化器在收敛速度、稳定性和泛化能力上各有特点。本课深入理解优化器的数学原理和实践选择。

🏃 一、随机梯度下降 (SGD)

1.1 基本SGD

θₜ₊₁ = θₜ - η · ∇L(θₜ)

η:学习率,∇L:损失函数关于参数的梯度

1.2 动量 (Momentum)

vₜ = γvₜ₋₁ + η·∇L(θₜ)

θₜ₊₁ = θₜ - vₜ

γ通常取0.9,v是速度(历史梯度累积)

动量帮助SGD:①加速沿正确方向前进②抑制震荡③可能冲出浅的局部最优。

1.3 Nesterov动量

vₜ = γvₜ₋₁ + η·∇L(θₜ - γvₜ₋₁)

先"往前看一步"再计算梯度,比标准动量更有前瞻性。

⚙️ 二、自适应学习率优化器

2.1 Adam

mₜ = β₁mₜ₋₁ + (1-β₁)gₜ (一阶矩估计)

vₜ = β₂vₜ₋₁ + (1-β₂)gₜ² (二阶矩估计)

m̂ₜ = mₜ/(1-β₁ᵗ), v̂ₜ = vₜ/(1-β₂ᵗ) (偏差修正)

θₜ₊₁ = θₜ - η·m̂ₜ/(√v̂ₜ + ε)

默认:β₁=0.9, β₂=0.999, ε=10⁻⁸

2.2 AdamW

AdamW将权重衰减与梯度更新解耦,避免了Adam+L2正则化的缺陷:

θₜ₊₁ = θₜ - η·(m̂ₜ/(√v̂ₜ + ε) + λθₜ)

λ是权重衰减系数(通常0.01-0.1)

🔬 三、优化器对比实验


import torch
import torch.nn as nn
import numpy as np

# 对比不同优化器
torch.manual_seed(42)

# 创建一个有挑战性的损失景观
X = torch.randn(500, 10)
y = (X[:, 0]**2 + X[:, 1] * 2 - X[:, 2] > 0).float().unsqueeze(1)

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(10, 64), nn.ReLU(),
            nn.Linear(64, 32), nn.ReLU(),
            nn.Linear(32, 1), nn.Sigmoid()
        )
    def forward(self, x):
        return self.net(x)

optimizers_config = {
    "SGD(lr=0.1)": lambda p: torch.optim.SGD(p, lr=0.1),
    "SGD+Momentum(0.9)": lambda p: torch.optim.SGD(p, lr=0.1, momentum=0.9),
    "SGD+Nesterov": lambda p: torch.optim.SGD(p, lr=0.1, momentum=0.9, nesterov=True),
    "Adam(lr=0.001)": lambda p: torch.optim.Adam(p, lr=0.001),
    "AdamW(lr=0.001)": lambda p: torch.optim.AdamW(p, lr=0.001, weight_decay=0.01),
    "RMSprop(lr=0.01)": lambda p: torch.optim.RMSprop(p, lr=0.01),
}

loss_fn = nn.BCELoss()
results = {}

for name, opt_fn in optimizers_config.items():
    torch.manual_seed(42)
    model = Net()
    optimizer = opt_fn(model.parameters())
    
    losses = []
    for epoch in range(300):
        optimizer.zero_grad()
        output = model(X)
        loss = loss_fn(output, y)
        loss.backward()
        optimizer.step()
        if epoch % 50 == 0 or epoch == 299:
            losses.append((epoch, loss.item()))
    
    results[name] = losses
    print(f"{name:>25}: " + " → ".join([f"e{e}={l:.4f}" for e,l in losses]))
🟢 运行结果 — 优化器对比 ✅验证通过 SGD(lr=0.1): e0=0.6871 → e50=0.5093 → e100=0.2891 → e150=0.2127 → e200=0.1691 → e250=0.1346 → e299=0.1071 SGD+Momentum(0.9): e0=0.6871 → e50=0.0847 → e100=0.0137 → e150=0.0056 → e200=0.0033 → e250=0.0022 → e299=0.0016 SGD+Nesterov: e0=0.6871 → e50=0.0750 → e100=0.0130 → e150=0.0054 → e200=0.0031 → e250=0.0021 → e299=0.0016 Adam(lr=0.001): e0=0.6871 → e50=0.3960 → e100=0.1751 → e150=0.0982 → e200=0.0536 → e250=0.0290 → e299=0.0169 AdamW(lr=0.001): e0=0.6871 → e50=0.3962 → e100=0.1753 → e150=0.0984 → e200=0.0538 → e250=0.0292 → e299=0.0170 RMSprop(lr=0.01): e0=0.6871 → e50=0.0086 → e100=0.0019 → e150=0.0007 → e200=0.0004 → e250=0.0002 → e299=0.0001

📊 四、Adam内部状态与学习率调度


import torch
import torch.nn as nn
import math

# Adam优化器内部状态可视化
torch.manual_seed(42)
model = nn.Linear(5, 1)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

X = torch.randn(32, 5)
y = torch.randn(32, 1)
loss_fn = nn.MSELoss()

print("=== Adam优化器内部状态 ===")
print("参数: weight")
for step in range(10):
    optimizer.zero_grad()
    loss = loss_fn(model(X), y)
    loss.backward()
    optimizer.step()
    
    # 获取Adam内部状态
    state = optimizer.state_dict()['state']
    param = list(model.parameters())[0]
    
    if state:
        step_val = state[0].get('step', 0)
        exp_avg = state[0].get('exp_avg', torch.zeros_like(param))
        exp_avg_sq = state[0].get('max_exp_avg_sq' if 'max_exp_avg_sq' in state[0] else 'exp_avg_sq', torch.zeros_like(param))
        
        print(f"Step {step}: loss={loss.item():.6f}, grad_norm={param.grad.norm().item():.6f}, "
              f"m_norm={exp_avg.norm().item():.6f}, v_norm={exp_avg_sq.norm().item():.6f}")
    else:
        print(f"Step {step}: loss={loss.item():.6f}")

# 学习率调度器对比
print("\n=== 学习率调度 ===")
model2 = nn.Linear(5, 1)
base_lr = 0.1

schedulers = {
    "StepLR(step=5,γ=0.5)": lambda opt: torch.optim.lr_scheduler.StepLR(opt, step_size=5, gamma=0.5),
    "CosineAnnealing": lambda opt: torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=20),
    "ExponentialLR(γ=0.9)": lambda opt: torch.optim.lr_scheduler.ExponentialLR(opt, gamma=0.9),
}

for name, sched_fn in schedulers.items():
    opt = torch.optim.SGD(model2.parameters(), lr=base_lr)
    sched = sched_fn(opt)
    lrs = [base_lr]
    for _ in range(19):
        sched.step()
        lrs.append(opt.param_groups[0]['lr'])
    print(f"{name}: " + " → ".join([f"{lr:.5f}" for lr in lrs[::4]]) + f" → {lrs[-1]:.5f}")
🟢 运行结果 — Adam状态与LR调度 ✅验证通过 === Adam优化器内部状态 === 参数: weight Step 0: loss=1.226830, grad_norm=1.290656, m_norm=0.129066, v_norm=0.000970 Step 1: loss=1.201225, grad_norm=1.245624, m_norm=0.240712, v_norm=0.001880 Step 2: loss=1.176825, grad_norm=1.201976, m_norm=0.336814, v_norm=0.002734 Step 3: loss=1.153494, grad_norm=1.160791, m_norm=0.419175, v_norm=0.003537 Step 4: loss=1.131194, grad_norm=1.121447, m_norm=0.489348, v_norm=0.004294 Step 5: loss=1.109970, grad_norm=1.083295, m_norm=0.548660, v_norm=0.005007 Step 6: loss=1.089855, grad_norm=1.045954, m_norm=0.598265, v_norm=0.005679 Step 7: loss=1.070850, grad_norm=1.009084, m_norm=0.639172, v_norm=0.006312 Step 8: loss=1.052927, grad_norm=0.972373, m_norm=0.672258, v_norm=0.006906 Step 9: loss=1.036049, grad_norm=0.935697, m_norm=0.698306, v_norm=0.007464 === 学习率调度 === StepLR(step=5,γ=0.5): 0.10000 → 0.10000 → 0.05000 → 0.02500 → 0.01250 → 0.01250 CosineAnnealing: 0.10000 → 0.09045 → 0.06545 → 0.03455 → 0.00955 → 0.00062 ExponentialLR(γ=0.9): 0.10000 → 0.06561 → 0.04305 → 0.02824 → 0.01853 → 0.01351

📋 五、优化器选择建议

场景推荐优化器原因
快速实验Adam / AdamW对初始LR不敏感,收敛快
追求最佳泛化SGD+Momentum泛化能力通常更好
Transformer/NLPAdamW标准选择
CV / 大规模训练SGD+Momentum+预热节省内存,泛化好
💡 实践建议:先用Adam快速验证思路,确定模型能收敛后,再切SGD+Momentum追求更好泛化。学习率预热(Warmup)对深层网络很重要!

🔬 六、优化器进阶

6.1 LAMB优化器

Layer-wise Adaptive Moments optimizer for Batch training,专为超大batch训练设计。

核心:逐层自适应缩放更新幅度

r = ‖m̂/(√v̂+ε) + λw‖

更新: w = w - η × min(r_max, r)/r × (m̂/(√v̂+ε) + λw)

可以在64K batch size下稳定训练BERT

6.2 Lion优化器

2023年Google Brain发现的更简单优化器:

更新方向: sign(β₁·m + (1-β₁)·g)

比Adam更少内存(不需要二阶矩)

在许多任务上与Adam持平或更优

6.3 优化器选择决策树

💡 决策流程:

📖 七、延伸阅读

💡 推荐资源:

📝 练习

练习1:手动实现Adam

不使用torch.optim,手动实现Adam优化器,与PyTorch内置Adam对比。

练习2:学习率预热

实现线性预热策略:前N步从0线性增加到目标学习率,观察对训练稳定性的影响。

练习3:优化器切换

先用Adam训练100轮,再切到SGD+Momentum继续训练,这种策略有什么优势?

🏆

成就解锁:优化大师

你已经掌握了深度学习最核心的优化算法。
选对优化器,训练事半功倍!