💻完整代码 + 对比实验:GitHub 仓库
📖配套教程:CSDN 专栏
如果觉得有用,欢迎 ⭐ Star 支持!
🎯 为什么优化器这么重要?
大白话:优化器决定模型"怎么学习"
损失函数:告诉模型"你错得多离谱" 优化器:告诉模型"该怎么改参数" 类比: 损失函数 = GPS 导航(告诉你离目的地多远) 优化器 = 司机(决定怎么走、走多快)选错优化器的后果:
SGD(学习率太大): 在最低点附近震荡,永远找不到最优解 Adam(默认参数): 收敛快,但可能泛化差(训练集好,测试集差) AdamW(推荐): 收敛快 + 泛化好(最佳平衡)今天带你从SGD 到 AdamW,看清楚每个优化器的优缺点!
📊 优化器总览
| 优化器 | 年份 | 收敛速度 | 泛化能力 | 超参数 | 适用场景 |
|---|---|---|---|---|---|
| SGD | 1951 | ⭐⭐ | ⭐⭐⭐⭐⭐ | lr, momentum | 理论分析 |
| SGD + Momentum | 1964 | ⭐⭐⭐ | ⭐⭐⭐⭐ | lr, momentum | 经典选择 |
| RMSProp | 2012 | ⭐⭐⭐⭐ | ⭐⭐⭐ | lr, alpha | RNN 训练 |
| Adam | 2014 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | lr, betas | 默认选择 |
| AdamW | 2017 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | lr, betas, weight_decay | 当前最佳 |
| AdamW + Cosine | 2020 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | lr, T_max | SOTA 模型 |
关键进化:
- 收敛速度:SGD 慢 → Adam 快(10 倍)
- 泛化能力:Adam 差 → AdamW 好(+5% 准确率)
- 易用性:SGD 调参难 → AdamW 几乎不用调
1️⃣ SGD(随机梯度下降):鼻祖
核心思想
一句话总结:沿着梯度的反方向走
梯度:函数上升最快的方向 负梯度:函数下降最快的方向 优化器: 参数 = 参数 - 学习率 × 梯度大白话解释
场景:下山
你在山上,想最快到达谷底 方法 1(最速下降): 每步都往最陡的方向走 问题: - 可能走到局部最低点(不是全局最低) - 可能来回震荡 类比 SGD: 参数更新 = 当前位置 - 学习率 × 梯度代码实现
import torch import torch.nn as nn # PyTorch 内置 optimizer = torch.optim.SGD( model.parameters(), lr=0.01, # 学习率 momentum=0.0 # 动量(先设为 0) ) # 训练循环 for epoch in range(num_epochs): for inputs, targets in dataloader: optimizer.zero_grad() # 清空梯度 outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() # 计算梯度 optimizer.step() # 更新参数手写实现(理解原理)
class SGDOptimizer: """手写 SGD 优化器""" def __init__(self, params, lr=0.01): self.params = params self.lr = lr def step(self): for param in self.params: if param.grad is not None: # 参数更新公式 param.data = param.data - self.lr * param.grad def zero_grad(self): for param in self.params: if param.grad is not None: param.grad.zero_() # 使用 optimizer = SGDOptimizer(model.parameters(), lr=0.01) for epoch in range(num_epochs): for inputs, targets in dataloader: optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step()SGD 的问题
问题 1:学习率难调
学习率太大: 在最低点附近震荡,不收敛 学习率太小: 收敛太慢(走 100 步还没到谷底) 类比: 学习率 = 步长 太大:一步迈过最低点 太小:走 100 步还在半山腰问题 2:容易卡在局部最优
损失函数形状: ╱‾‾╲ ╲ ╱ ╲______ ╱ 局部最优 ╱ 全局最优 ╱ ───────────────── SGD: 从左边开始 → 下到局部最优 → 卡住 原因: 梯度为 0 就不走了 但局部最优的梯度也是 02️⃣ SGD + Momentum:加入惯性
核心思想
一句话总结:下山时保留"冲力"
SGD: 每步只看当前梯度(像盲人摸象) SGD + Momentum: 考虑之前的"冲力"(像滚雪球) 公式: v_t = momentum × v_{t-1} + lr × gradient param = param - v_t大白话解释
场景:下山
普通 SGD: Step 1: 往左走(梯度向左) Step 2: 往右走(梯度向右)← 来回震荡 SGD + Momentum: Step 1: 往左走,获得向左的速度 Step 2: 虽然梯度向右,但冲力向左,继续向左走 效果: 1. 减少震荡 2. 加速收敛 3. 更容易跳出局部最优代码实现
optimizer = torch.optim.SGD( model.parameters(), lr=0.01, momentum=0.9, # 动量系数(默认 0.9) weight_decay=1e-4 # 权重衰减(L2 正则化) )可视化对比
优化路径: SGD(无动量): ━━╲╱╲╱╲╱╲╱━━━━ ← 来回震荡 SGD + Momentum: ━━━━━━━━━━━━━━━━ ← 平滑前进 效果: - 训练快 2-3 倍 - 更稳定Momentum 的问题
学习率还是难调:
Momentum 只是"加速",没解决根本问题: - 不同参数需要不同学习率 - 不同阶段需要不同学习率 - 梯度大小变化大时不稳定3️⃣ RMSProp:自适应学习率
核心思想
一句话总结:每个参数有自己的学习率
SGD:所有参数用同一个学习率 RMSProp: 梯度大的参数 → 学习率自动减小 梯度小的参数 → 学习率自动增大 公式: E[g²]_t = β × E[g²]_{t-1} + (1-β) × g²_t param = param - lr × g_t / √(E[g²]_t + ε)大白话解释
场景:不同参数更新幅度不同
参数 1(梯度大): 普通 SGD:大步走 → 可能过冲 RMSProp:自动缩小步长 → 走得稳 参数 2(梯度小): 普通 SGD:小步走 → 太慢 RMSProp:自动放大步长 → 走得快代码实现
optimizer = torch.optim.RMSprop( model.parameters(), lr=0.001, alpha=0.99, # 衰减率(默认 0.99) eps=1e-8 # 数值稳定(防止除 0) )RMSProp 的问题
累计梯度可能无限大:
RMSProp 只考虑"近期"梯度(指数衰减) → 没有考虑"历史"总梯度 结果: 学习率可能变得很大(不稳定)4️⃣ Adam:集大成者
核心思想
一句话总结:Momentum + RMSProp 的完美结合
Adam = Momentum(一阶动量)+ RMSProp(二阶动量) 一阶动量(类似 Momentum): 记录梯度的"方向" 二阶动量(类似 RMSProp): 记录梯度的"幅度" 自适应学习率: 学习率 = 一阶动量 / √(二阶动量)大白话解释
Adam 的 3 个优势:
1. 自适应学习率(每个参数不同) → 不用手动调学习率 2. 动量加速(考虑历史梯度) → 收敛快 3. 偏差修正(初期更准确) → 训练稳定代码实现
optimizer = torch.optim.Adam( model.parameters(), lr=0.001, # 学习率 betas=(0.9, 0.999), # 一阶和二阶动量衰减率 eps=1e-8, # 数值稳定 weight_decay=0 # 权重衰减(Adam 默认不加) )Adam 的问题
泛化能力差:
实验对比(CIFAR-100): SGD + Momentum: 训练集准确率:95% 测试集准确率:85% ← 泛化好 Adam: 训练集准确率:99% 测试集准确率:80% ← 过拟合! 原因: Adam 收敛太快,跳到尖锐的最小值 SGD 收敛慢,找到平坦的最小值(泛化好)5️⃣ AdamW:当前最佳
核心思想
一句话总结:Adam + 正确的权重衰减
Adam 的问题: weight_decay 和 L2 正则化不等价 AdamW 的改进: 分离权重衰减和梯度更新 公式: Adam: 参数 = 参数 - lr × (梯度 + weight_decay × 参数) AdamW: 参数 = 参数 × (1 - lr × weight_decay) - lr × 梯度大白话解释
权重衰减的作用:
目的:防止参数变得太大(过拟合) Adam 的错误实现: weight_decay 被当成梯度的一部分 → 自适应学习率会把 weight_decay 缩小 → 正则化效果弱 AdamW 的正确实现: weight_decay 独立于梯度 → 不受自适应学习率影响 → 正则化效果好代码实现
optimizer = torch.optim.AdamW( model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01 # 注意:AdamW 必须加 weight_decay! )对比实验
# Adam vs AdamW # Adam optimizer_adam = torch.optim.Adam(model.parameters(), lr=0.001) train(model, optimizer_adam) print(f"Adam 测试集准确率: {evaluate(model):.2%}") # 80.5% # AdamW optimizer_adamw = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01) train(model, optimizer_adamw) print(f"AdamW 测试集准确率: {evaluate(model):.2%}") # 85.2% # 提升:+4.7%📊 完整对比
收敛速度对比
训练 Loss 曲线: SGD: ╱━━━━━━━━━━━━━━━━━ ↗ ↗ ↗ SGD+Momentum: ━━━━━━━━━━━╱━━━━ ↗ ↗ Adam: ╱━━━━━━━━━━━━━━━━━━━━ ↗ ↗ AdamW: ╱━━━━━━━━━━━━━━━━━━━━ ↗ ↗ 结论: SGD 最慢,Adam/AdamW 最快泛化能力对比
测试集准确率: SGD: ████████████████████ 85% SGD+Momentum: ██████████████████████ 87% Adam: ███████████████████ 80% AdamW: ██████████████████████ 85% 结论: SGD 泛化好,Adam 泛化差 AdamW 结合了速度和泛化超参数敏感性
学习率扫描(0.0001 ~ 0.1): SGD: ★☆☆☆☆ ← 非常敏感 Adam: ★★★★☆ ← 不敏感 AdamW: ★★★★★ ← 最不敏感 结论: AdamW 最"傻瓜式",几乎不用调参💡 实战建议
1. 如何选择?
快速原型(实验阶段): → AdamW(收敛快,不用调参) 追求极致性能(论文/比赛): → SGD + Momentum + Cosine LR → 需要仔细调参,但上限更高 RNN/LSTM 训练: → Adam 或 RMSProp → 梯度变化大,自适应学习率更好 推荐配置: → AdamW(99% 场景够用)2. AdamW 最佳实践
# Transformer 模型(BERT、GPT) optimizer = torch.optim.AdamW( model.parameters(), lr=3e-5, # 小学习率 betas=(0.9, 0.999), eps=1e-8, weight_decay=0.01 # 必须有! ) # 学习率预热(前 10% 训练) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=1000, num_training_steps=10000 )3. SGD 最佳实践
# CNN 训练(追求极致性能) optimizer = torch.optim.SGD( model.parameters(), lr=0.1, # 大学习率 momentum=0.9, weight_decay=1e-4, nesterov=True # Nesterov 动量(更稳) ) # Cosine 学习率衰减 scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=num_epochs )4. 优化器调试
# 监控梯度范数(判断是否爆炸) for name, param in model.named_parameters(): if param.grad is not None: grad_norm = param.grad.norm() print(f"{name}: grad_norm={grad_norm:.4f}") # 梯度裁剪(防止爆炸) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)🎯 总结
优化器进化路线:
SGD (1951) ↓ 加入动量 SGD + Momentum (1964) ↓ 自适应学习率 RMSProp (2012) ↓ 一阶 + 二阶动量 Adam (2014) ↓ 正确的权重衰减 AdamW (2017) ↓ 学习率调度 AdamW + Cosine (2020)选择原则:
- 默认用 AdamW- 99% 场景够用
- 追求极致用 SGD- 需要调参,但上限高
- 必须加权重衰减- 防止过拟合
- 配合学习率调度器- 提升性能
常用组合:
Transformer: AdamW + Cosine LR + Warmup CNN: SGD + Momentum + Cosine LR RNN: Adam + 梯度裁剪 GAN: Adam (lr=0.0002, betas=(0.5, 0.999))📚 延伸阅读
完整代码和实验:https://github.com/Lee985-cmd/AI-30-Day-Challenge
30 天 AI 挑战教程:https://blog.csdn.net/m0_67081842
评论区留言:你平时用哪个优化器?
- Adam?
- AdamW?
- SGD?
- 其他?
欢迎分享你的经验!
⭐如果这篇文章帮到你了,请 Star GitHub 项目支持一下!🌟 Star 链接
已有 12 人 Star,你的支持是我持续更新的动力!