07-优化器进阶¶
学习时间: 约6-8小时 难度级别: ⭐⭐⭐ 中级 前置知识: 神经网络基础、反向传播算法、损失函数与优化基础 学习目标: 深入理解各种优化算法的数学原理与实现,掌握学习率调度策略
目录¶
- 1. 优化问题概述
- 2. 梯度下降变体
- 3. 动量方法
- 4. 自适应学习率方法
- 5. Adam 系列优化器
- 6. 大规模训练优化器
- 7. 学习率调度策略
- 8. 超参数调优方法
- 9. 优化器选择指南
- 10. 练习与自我检查
1. 优化问题概述¶
1.1 深度学习中的优化目标¶
深度学习的训练本质上是一个非凸优化问题:
其中 \(\theta\) 是模型参数,\((x_i, y_i)\) 是训练数据,\(\ell\) 是损失函数。
1.2 优化面临的挑战¶
- 非凸性: 损失曲面存在大量局部极小值和鞍点
- 病态曲率: 不同方向的曲率差异不同(条件数大),导致梯度下降在某些方向上震荡
- 随机噪声: 使用 mini-batch 引入的梯度估计噪声
- 高维空间: 参数空间可能有数百万甚至数十亿维
- 梯度消失/爆炸: 深层网络中梯度可能变得极小或极大
1.3 鞍点问题¶
在高维空间中,鞍点比局部极小值更常见。在一个 \(n\) 维空间中,一个临界点成为局部极小值需要所有 \(n\) 个方向都是"碗底",概率为 \((\frac{1}{2})^n\)(假设各方向等概率)。因此高维中几乎所有临界点都是鞍点。
2. 梯度下降变体¶
2.1 批量梯度下降(Batch Gradient Descent)¶
使用全部训练数据计算梯度:
优点: 梯度准确,收敛稳定 缺点: 计算代价大,无法利用数据冗余,容易陷入局部极小
2.2 随机梯度下降(SGD)¶
每次仅用一个样本计算梯度:
优点: 计算快,随机性有助于逃离局部极小 缺点: 梯度噪声大,收敛不稳定
2.3 小批量梯度下降(Mini-batch SGD)¶
实践中最常用的折中方案:
其中 \(\mathcal{B}\) 是大小为 \(B\) 的 mini-batch(通常 \(B\) = 32, 64, 128, 256)。
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. 动量方法¶
3.1 经典动量(Momentum)¶
动量法借鉴了物理学中的动量概念,利用过去梯度的指数移动平均来加速优化:
其中 \(\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\)。
# ===== 手动实现带动量的 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 动量是经典动量的改进。核心思想:先按动量方向"预看一步",再在预看位置计算梯度。
直觉理解:经典动量是"先走再看",Nesterov 是"先看再走"。通过在预估位置计算梯度,可以提前预知动量方向是否正确,从而更快地修正方向。
# ===== 手动实现 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. 自适应学习率方法¶
4.1 Adagrad¶
Adagrad(Duchi et al., 2011)为每个参数自适应地调整学习率。频繁更新的参数获得较小的学习率,不频繁更新的参数获得较大的学习率。
算法:
其中 \(g_t = \nabla_\theta \mathcal{L}(\theta_t)\),\(G_t\) 是梯度平方的累积和。
优点: 适合稀疏数据和特征 缺点: 学习率单调递减,后期可能过小导致训练过早停止
# ===== 手动实现 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 学习率单调递减的问题,使用指数移动平均代替简单累加:
其中 \(\rho\) 是衰减率(通常为 0.9 或 0.99)。
直觉理解:RMSProp 只记住"最近"的梯度历史,使得学习率可以适当增大(当近期梯度较小时)。
# ===== 手动实现 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 系列优化器¶
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\)
# ===== 手动实现 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 的自适应学习率。
# ===== 手动实现 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 的已知问题¶
- 泛化性能: Adam 在某些任务上的泛化性能可能不如 SGD + Momentum
- \(\beta_2\) 选择: 默认的 \(\beta_2=0.999\) 在某些任务中可能不是最优
- 训练不稳定性: 在 Transformer 训练中可能出现 loss spike
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+解耦权重衰减 | 更好的泛化 |
6. 大规模训练优化器¶
6.1 LAMB(Layer-wise Adaptive Moments for Batch Training)¶
LAMB(You et al., 2020)是为大 batch size 训练设计的优化器。核心思想是对每一层的更新进行归一化,使其与参数范数成比例:
其中 \(\frac{\|\theta_t\|}{\|r_t\|}\) 是信赖比(trust ratio),确保更新步长与参数量级匹配。
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. 学习率调度策略¶
7.1 为什么需要学习率调度¶
- 初期: 需要较大学习率快速探索参数空间
- 中期: 适当降低学习率以更精细地优化
- 后期: 使用很小的学习率微调到好的极小值
7.2 StepLR¶
每隔固定步数将学习率乘以衰减因子:
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(余弦退火)¶
学习率按余弦函数从初始值衰减到最小值:
# 余弦退火
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
)
7.4 Warmup¶
训练初期使用非常小的学习率,线性增长到目标学习率,然后进行常规衰减:
# ===== 手动实现 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)提出的超收敛策略,学习率先增后减,整个训练过程只有一个周期:
# 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 学习率调度可视化¶
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. 超参数调优方法¶
8.1 网格搜索(Grid Search)¶
遍历参数组合的笛卡尔积。
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}")
8.2 随机搜索(Random Search)¶
从参数空间中随机采样,通常比网格搜索更高效(Bergstra & Bengio, 2012)。
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)来建模超参数与性能之间的关系。
# 使用 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)提出的方法,通过一次训练来确定最佳学习率范围:
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 完整训练示例¶
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. 练习与自我检查¶
练习题¶
-
手动实现:从零实现 SGD、Momentum、Adam 优化器,在二维 Rosenbrock 函数上可视化优化轨迹。
-
优化器对比:在 MNIST 上分别使用 SGD、Momentum SGD、Adam、AdamW 训练同一个网络,绘制训练曲线并比较收敛速度。
-
学习率调度:实现 Warmup + Cosine Annealing 调度器,在 CIFAR-10 上对比有无 Warmup 的训练效果。
-
LR Finder:实现学习率查找器,确定你模型的最佳学习率范围。
-
超参数搜索:使用 Optuna 为一个图像分类任务搜索最佳超参数组合(学习率、权重衰减、优化器类型、Dropout 率)。
-
AdamW vs Adam+L2:实验验证 AdamW 和 Adam + L2 正则化的区别,观察权重分布差异。
自我检查清单¶
- 能解释 Momentum、Nesterov 动量的直觉和数学推导
- 理解 Adagrad → RMSProp → Adam 的演进关系
- 能从零实现 Adam 优化器
- 深刻理解 AdamW 与 Adam + L2 正则化的区别
- 熟悉至少3种学习率调度策略的使用方法
- 能根据任务类型选择合适的优化器和学习率
- 了解 Warmup 的必要性和实现方式
- 掌握至少一种超参数调优方法
- 能写出完整的训练循环(含优化器+调度器+梯度裁剪)
下一章: 02-卷积神经网络/01-卷积神经网络基础 — 进入卷积神经网络的世界









