机器学习基础(十二):优化器大全
一、引言
训练神经网络,本质上是在做一件事:找到一组参数 \(\theta\),让损失函数 \(L(\theta)\) 尽可能小。
这个"找"的过程,就是优化器的工作。
你可能遇到过这样的困惑:
- 用 SGD 训练,loss 抖动剧烈,收敛很慢;
- 换成 Adam,一开始很快,但最终精度比 SGD 差;
- 论文里说用 AdamW,到底和 Adam 有什么区别?
- 学习率调多少合适?
这篇文章从梯度下降的本质出发,逐步推导每种优化器的原理,最后给出实战选择建议。
二、梯度下降基础
2.1 参数更新的核心公式
所有优化器的基础都是:
其中:
- \(\theta\):模型参数
- \(\eta\):学习率(learning rate)
- \(g = \nabla_\theta L\):损失对参数的梯度
2.2 三种梯度下降变体
| 变体 | 每次用多少数据 | 特点 |
|---|---|---|
| 批量梯度下降(BGD) | 全部训练集 | 稳定但极慢,内存爆炸 |
| 随机梯度下降(SGD) | 单个样本 | 快但噪声极大 |
| 小批量梯度下降(Mini-batch SGD) | 一个 batch(如 32/64/128) | 实践中最常用的"SGD" |
我们平时说的"SGD"几乎都是指 Mini-batch SGD。
2.3 SGD 的问题
朴素 SGD 有几个明显缺陷:
- 学习率难调:太大震荡,太小收敛慢;
- 各维度梯度尺度差异大:有的参数梯度很大,有的很小,统一学习率顾此失彼;
- 容易陷入鞍点/局部最小:梯度为零但不是全局最优。
后续所有优化器,都是在针对这些问题做改进。
三、SGD + 动量(Momentum)
3.1 直觉理解
想象一个球从山坡滚下来:
- 朴素 SGD:球每次只看当前坡度,遇到小坑就被卡住了;
- Momentum SGD:球有惯性,积累了速度,能冲过小坑,在平坦方向加速。
3.2 公式推导
引入速度向量 \(v\)(动量项):
其中 \(\beta\) 通常取 0.9,表示保留 90% 的历史速度。
展开来看:
动量是历史梯度的指数加权移动平均,方向一致时加速,方向震荡时抵消。
3.3 Nesterov 动量(NAG)
动量 SGD 的升级版:先"预判"参数位置,再计算梯度。
Nesterov 相当于"往前看一步再踩刹车",收敛更快、更稳定。
# PyTorch 中使用 SGD + Nesterov Momentum
optimizer = torch.optim.SGD(
model.parameters(),
lr=0.01,
momentum=0.9,
nesterov=True,
weight_decay=1e-4 # L2 正则化
)
四、AdaGrad:自适应学习率
4.1 核心思想
不同参数应该有不同的学习率:
- 更新频繁的参数(梯度大)→ 学习率应该小;
- 更新稀少的参数(梯度小)→ 学习率应该大。
4.2 公式
\(G_t\) 是历史梯度平方的累积和,梯度越大的方向,分母越大,学习率越小。
4.3 缺陷
\(G_t\) 单调递增,学习率会永远递减,训练后期可能"停止学习"。
五、RMSProp:修复 AdaGrad 的衰减问题
5.1 改进点
用指数加权移动平均代替累积和,让"遗忘"旧梯度:
\(\rho\) 通常取 0.9,\(G_t\) 不再单调递增,学习率可以自动回升。
optimizer = torch.optim.RMSprop(
model.parameters(),
lr=0.001,
alpha=0.9, # 对应 ρ
eps=1e-8
)
六、Adam:最流行的优化器
6.1 设计理念
Adam = Momentum(一阶矩)+ RMSProp(二阶矩)
把动量和自适应学习率合二为一:
6.2 完整公式
一阶矩(动量,梯度的指数加权平均):
二阶矩(梯度平方的指数加权平均):
偏差修正(初始时 \(m_0=0, v_0=0\),修正初始偏差):
参数更新:
默认超参数:
| 参数 | 默认值 | 含义 |
|---|---|---|
| \(\beta_1\) | 0.9 | 动量衰减系数 |
| \(\beta_2\) | 0.999 | 梯度平方衰减系数 |
| \(\epsilon\) | 1e-8 | 数值稳定项 |
| \(\eta\) | 1e-3 | 初始学习率 |
optimizer = torch.optim.Adam(
model.parameters(),
lr=1e-3,
betas=(0.9, 0.999),
eps=1e-8
)
6.3 Adam 的优点
- 收敛快:前期训练速度远超 SGD;
- 对学习率不敏感:1e-3 在绝大多数任务上都能用;
- 适合稀疏梯度:NLP 任务首选。
6.4 Adam 的问题:泛化性差
实验发现,虽然 Adam 收敛快,但最终精度往往不如精调过的 SGD+Momentum。
原因分析:Adam 的自适应学习率会让某些方向的梯度"被抑制过度",导致跳过更好的极小值。
七、AdamW:修复 L2 正则化的 Adam
7.1 问题背景
在 Adam 中加 L2 正则化,通常写法是:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
看起来没问题,但实际上存在缺陷:weight_decay 在 Adam 中并不等价于 L2 正则化!
7.2 原因详解
标准 L2 正则化在梯度下降中等价于:
但 Adam 的更新公式会对梯度(包含正则化项 \(\lambda\theta\))做自适应缩放,导致正则化强度随梯度大小变化,不同参数受到的正则化程度不一致。
7.3 AdamW 的解法
将权重衰减从梯度中分离出来,直接作用在参数上:
权重衰减项不经过自适应缩放,正则化效果更干净。
# AdamW:Transformer、BERT、ViT 的标配优化器
optimizer = torch.optim.AdamW(
model.parameters(),
lr=1e-4,
betas=(0.9, 0.999),
weight_decay=0.01 # 这里的 weight_decay 才是真正的 L2 正则化
)
结论:只要你用 Adam + weight_decay,就应该用 AdamW 而不是 Adam。
八、其他值得了解的优化器
8.1 AMSGrad
Adam 的变种,保证二阶矩单调不减,理论上更稳定:
实践中提升有限,了解即可。
8.2 RAdam(Rectified Adam)
自动处理 Adam 早期训练不稳定问题,相当于"自适应 warmup":
# 需要安装 torch-optimizer 或使用 PyTorch 内置
from torch.optim import RAdam
optimizer = RAdam(model.parameters(), lr=1e-3)
8.3 Lion(Evolved by Google Brain)
2023 年 Google Brain 通过程序搜索发现的优化器,只用梯度的符号更新参数:
- 内存占用比 Adam 少(只需一个动量项);
- 在大模型训练中表现优异;
- 学习率通常设为 Adam 的 1/3 到 1/10。
九、学习率调度策略
优化器 × 学习率调度 = 完整的训练策略。固定学习率往往不是最优的。
9.1 常用调度器
StepLR(每 N 轮衰减一次):
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
CosineAnnealingLR(余弦退火,最常用):
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
OneCycleLR(先升后降,配合 SGD 效果极好):
scheduler = torch.optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=0.1, steps_per_epoch=len(train_loader), epochs=50
)
Warmup + Cosine Decay(Transformer 标配):
from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR
warmup = LinearLR(optimizer, start_factor=0.1, total_iters=10)
cosine = CosineAnnealingLR(optimizer, T_max=90)
scheduler = SequentialLR(optimizer, schedulers=[warmup, cosine], milestones=[10])
9.2 学习率调度的使用方式
for epoch in range(num_epochs):
train(model, optimizer)
scheduler.step() # 每个 epoch 结束后更新学习率
十、综合实战:用不同优化器训练同一个网络
下面用一段代码,在 CIFAR-10 上对比 SGD、Adam、AdamW 的训练曲线:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
# ---- 数据 ----
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=128, shuffle=True, num_workers=2)
# ---- 模型(简单 CNN)----
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 32, 3, padding=1)
self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
self.pool = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(64 * 8 * 8, 256)
self.drop = nn.Dropout(0.5)
self.fc2 = nn.Linear(256, 10)
def forward(self, x):
x = self.pool(F.relu(self.bn1(self.conv1(x))))
x = self.pool(F.relu(self.bn2(self.conv2(x))))
x = x.view(x.size(0), -1)
x = F.relu(self.fc1(x))
x = self.drop(x)
return self.fc2(x)
# ---- 训练函数 ----
def train_model(optimizer_name, num_epochs=20):
model = SimpleCNN().cuda()
criterion = nn.CrossEntropyLoss()
if optimizer_name == 'SGD':
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)
elif optimizer_name == 'Adam':
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = None
elif optimizer_name == 'AdamW':
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = None
loss_history = []
for epoch in range(num_epochs):
model.train()
total_loss = 0
for images, labels in train_loader:
images, labels = images.cuda(), labels.cuda()
optimizer.zero_grad()
loss = criterion(model(images), labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
if scheduler:
scheduler.step()
avg_loss = total_loss / len(train_loader)
loss_history.append(avg_loss)
print(f"[{optimizer_name}] Epoch {epoch+1}/{num_epochs} Loss: {avg_loss:.4f}")
return loss_history
# ---- 对比实验 ----
results = {}
for opt in ['SGD', 'Adam', 'AdamW']:
results[opt] = train_model(opt)
# ---- 绘图 ----
plt.figure(figsize=(10, 5))
for opt, losses in results.items():
plt.plot(losses, label=opt)
plt.xlabel('Epoch')
plt.ylabel('Training Loss')
plt.title('Optimizer Comparison on CIFAR-10')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150)
plt.show()
十一、优化器选择指南
根据任务类型和场景,一张表帮你决策:
| 场景 | 推荐优化器 | 学习率建议 |
|---|---|---|
| CV 分类(追求极致精度) | SGD + Momentum + Cosine | 0.1,配合 warmup |
| CV 分类(快速原型) | AdamW | 1e-3 |
| NLP / Transformer | AdamW + warmup | 1e-4 ~ 5e-4 |
| GAN 训练 | Adam(生成器 + 判别器) | 2e-4 |
| 强化学习 | Adam / RMSProp | 1e-4 ~ 3e-4 |
| 大模型 fine-tuning | AdamW | 1e-5 ~ 5e-5 |
| 资源受限(大模型训练) | Lion | Adam lr 的 1/5 |
经验法则
- 首选 AdamW,设
lr=1e-3,weight_decay=0.01,加上 Cosine 调度,覆盖 80% 场景; - 如果要冲 SOTA 精度,试试 SGD + Momentum + OneCycleLR;
- 用了 Adam 一定要用 AdamW,weight_decay 才是真正的正则化;
- 学习率永远要配合调度器,裸学习率很少是最优的;
- warmup 是 Transformer 的标配,前 5~10% 步骤线性升温,避免早期梯度爆炸。
十二、各优化器原理总结
梯度下降演进路线:
SGD ── 简单,需仔细调参
└── + Momentum ── 加速收敛,减少震荡(β=0.9)
└── Nesterov ── 预判+动量,更快更稳
AdaGrad ── 自适应学习率,但会衰减到零
└── RMSProp ── 用指数平均替代累积,解决衰减问题
Adam = Momentum + RMSProp ── 快速收敛,工程首选
└── AMSGrad ── 理论更稳,实践差别不大
└── RAdam ── 自适应warmup,更鲁棒
└── AdamW ── 修复L2正则化,现代标准
└── Lion ── 只用符号,更省内存
十三、总结
优化器的选择不是玄学,有规律可循:
- 理解动量:它让梯度"有惯性",减少震荡;
- 理解自适应学习率:让每个参数有自己的步长;
- Adam = 两者结合,收敛快但泛化稍差;
- AdamW 修复了 Adam 的正则化问题,是现代深度学习的标准配置;
- 学习率调度和优化器同样重要,余弦退火是万金油。
炼丹口诀:AdamW 起步,Cosine 收敛,SGD 冲精度,Warmup 保稳定。

浙公网安备 33010602011771号