第6章:优化算法(Optimization Algorithms)
一、导读
1.1 本章学习目标
第6章深入讲解深度学习中使用的优化算法。优化是深度学习的核心——训练神经网络就是在高维非凸损失曲面上寻找好的参数值。优化算法的效率和稳定性直接决定了模型能否成功训练。
本章的核心学习目标包括:
- 理解梯度下降的基本原理:批量梯度下降、随机梯度下降、小批量梯度下降
- 掌握动量法的原理:如何加速收敛和穿越鞍点
- 理解自适应学习率算法:AdaGrad、RMSProp、Adam的设计思想
- 掌握学习率调度策略:衰减、预热、余弦退火
- 理解优化中的挑战:鞍点、病态曲率、梯度爆炸/消失
- 掌握实际训练中的调优技巧
1.2 为什么本章如此重要
优化是深度学习训练的核心引擎。即使模型架构和损失函数设计得再好,如果优化算法选择不当,模型也无法收敛到好的解。
关键问题:
- 为什么Adam是默认选择?它的优势在哪里?
- 学习率为什么是最重要的超参数?
- 为什么训练初期用大学习率,后期用小学习率?
- 梯度消失/爆炸如何影响优化?
理解优化算法的原理,才能正确选择和调优超参数,解决训练中遇到的收敛问题。
1.3 核心问题引导
随机梯度下降为什么比批量梯度下降更常用?噪声有什么好处?
动量法的物理直觉是什么?它如何帮助穿越鞍点?
Adam为什么成为默认优化器?它解决了哪些问题?
学习率预热(warmup)为什么重要?
鞍点在高维空间中为什么比局部最小值更常见?
如何判断优化过程中遇到了什么问题?
二、核心概念详解
2.1 梯度下降基础
2.1.1 批量梯度下降
import numpy as np
def batch_gradient_descent(gradient_fn, x0, lr=0.01, n_iters=100):
"""批量梯度下降"""
x = x0.copy()
history = [x.copy()]
for _ in range(n_iters):
grad = gradient_fn(x)
x -= lr * grad
history.append(x.copy())
return np.array(history)
# 示例:Rosenbrock函数
def rosenbrock(x):
return (1 - x[0])**2 + 100*(x[1] - x[0]**2)**2
def rosenbrock_grad(x):
dx0 = -2*(1 - x[0]) + 200*(x[0]**2 - x[1])*2*x[0]
dx1 = 200*(x[1] - x[0]**2)
return np.array([dx0, dx1])
# 优化
x0 = np.array([-1.0, 1.0])
history = batch_gradient_descent(rosenbrock_grad, x0, lr=0.001, n_iters=1000)
# 可视化
x = np.linspace(-2, 2, 200)
y = np.linspace(-1, 3, 200)
X, Y = np.meshgrid(x, y)
Z = (1 - X)**2 + 100*(Y - X**2)**2
plt.figure(figsize=(10, 8))
plt.contour(X, Y, Z, levels=50, cmap='viridis', alpha=0.5)
plt.plot(history[:, 0], history[:, 1], 'r.-', markersize=3, label='BGD')
plt.plot([1], [1], 'g*', markersize=15, label='最优解 (1, 1)')
plt.xlabel('x0')
plt.ylabel('x1')
plt.title('批量梯度下降在Rosenbrock函数上')
plt.legend()
plt.savefig('batch_gd.png', dpi=150)
plt.show()2.1.2 随机梯度下降(SGD)
def sgd(gradient_fn, data, targets, loss_fn, x0, lr=0.01, n_epochs=100):
"""随机梯度下降"""
x = x0.copy()
n_samples = data.shape[0]
loss_history = []
for epoch in range(n_epochs):
# 随机打乱
indices = np.random.permutation(n_samples)
for i in indices:
grad = gradient_fn(x, data[i:i+1], targets[i:i+1])
x -= lr * grad
# 计算当前损失
loss = loss_fn(x, data, targets)
loss_history.append(loss)
return x, loss_history
# SGD的噪声有助于逃离局部最小值
# 但也导致收敛时震荡2.1.3 小批量梯度下降
def minibatch_sgd(model, data, targets, loss_fn, lr=0.01, batch_size=32, n_epochs=100):
"""小批量梯度下降"""
n_samples = data.shape[0]
for epoch in range(n_epochs):
indices = np.random.permutation(n_samples)
for start in range(0, n_samples, batch_size):
end = min(start + batch_size, n_samples)
batch_idx = indices[start:end]
batch_data = data[batch_idx]
batch_targets = targets[batch_idx]
# 计算梯度并更新
# ...
return model2.2 动量法
2.2.1 原理
动量法引入"速度"变量,累积历史梯度信息:
v_t = μ·v_{t-1} + η·∇L(θ_t)
θ_{t+1} = θ_t - v_tdef sgd_with_momentum(grad_fn, x0, lr=0.01, momentum=0.9, n_iters=100):
"""带动量的SGD"""
x = x0.copy()
v = np.zeros_like(x)
history = [x.copy()]
for _ in range(n_iters):
grad = grad_fn(x)
v = momentum * v + lr * grad
x -= v
history.append(x.copy())
return np.array(history)
# 对比:有动量 vs 无动量
history_no_mom = batch_gradient_descent(rosenbrock_grad, x0, lr=0.001, n_iters=1000)
history_mom = sgd_with_momentum(rosenbrock_grad, x0, lr=0.001, momentum=0.9, n_iters=1000)
plt.figure(figsize=(10, 8))
plt.contour(X, Y, Z, levels=50, cmap='viridis', alpha=0.5)
plt.plot(history_no_mom[:, 0], history_no_mom[:, 1], 'r.-', markersize=2, label='无动量', alpha=0.7)
plt.plot(history_mom[:, 0], history_mom[:, 1], 'b.-', markersize=2, label='动量=0.9', alpha=0.7)
plt.plot([1], [1], 'g*', markersize=15, label='最优解')
plt.legend()
plt.title('动量法加速收敛')
plt.savefig('momentum.png', dpi=150)
plt.show()2.2.2 动量的物理直觉
# 动量法的物理类比:球在损失曲面上滚动
# 速度v累积梯度(力),位置θ是球的坐标
# 动量μ是摩擦系数的倒数
# Nesterov动量( lookahead)
def nesterov_momentum(grad_fn, x0, lr=0.01, momentum=0.9, n_iters=100):
"""Nesterov加速梯度"""
x = x0.copy()
v = np.zeros_like(x)
history = [x.copy()]
for _ in range(n_iters):
# 先 lookahead
x_lookahead = x - momentum * v
grad = grad_fn(x_lookahead)
v = momentum * v + lr * grad
x -= v
history.append(x.copy())
return np.array(history)2.3 自适应学习率算法
2.3.1 AdaGrad
def adagrad(grad_fn, x0, lr=0.01, epsilon=1e-8, n_iters=100):
"""AdaGrad:每个参数独立的学习率"""
x = x0.copy()
G = np.zeros_like(x) # 累积梯度平方和
history = [x.copy()]
for _ in range(n_iters):
grad = grad_fn(x)
G += grad ** 2
x -= lr * grad / (np.sqrt(G) + epsilon)
history.append(x.copy())
return np.array(history)
# AdaGrad的问题:累积梯度导致学习率单调递减
# 后期学习率过小,无法继续学习2.3.2 RMSProp
def rmsprop(grad_fn, x0, lr=0.001, decay=0.9, epsilon=1e-8, n_iters=100):
"""RMSProp:指数移动平均替代累积"""
x = x0.copy()
v = np.zeros_like(x) # 梯度平方的移动平均
history = [x.copy()]
for _ in range(n_iters):
grad = grad_fn(x)
v = decay * v + (1 - decay) * grad ** 2
x -= lr * grad / (np.sqrt(v) + epsilon)
history.append(x.copy())
return np.array(history)2.3.3 Adam
def adam(grad_fn, x0, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8, n_iters=100):
"""Adam:动量 + RMSProp"""
x = x0.copy()
m = np.zeros_like(x) # 一阶矩(均值)
v = np.zeros_like(x) # 二阶矩(未中心化的方差)
history = [x.copy()]
for t in range(1, n_iters + 1):
grad = grad_fn(x)
# 更新矩估计
m = beta1 * m + (1 - beta1) * grad
v = beta2 * v + (1 - beta2) * grad ** 2
# 偏差修正
m_hat = m / (1 - beta1 ** t)
v_hat = v / (1 - beta2 ** t)
# 更新参数
x -= lr * m_hat / (np.sqrt(v_hat) + epsilon)
history.append(x.copy())
return np.array(history)
# 对比所有优化器
x0 = np.array([-1.0, 1.0])
optimizers = {
'SGD': lambda: batch_gradient_descent(rosenbrock_grad, x0, lr=0.001, n_iters=2000),
'SGD+Momentum': lambda: sgd_with_momentum(rosenbrock_grad, x0, lr=0.001, momentum=0.9, n_iters=2000),
'AdaGrad': lambda: adagrad(rosenbrock_grad, x0, lr=0.1, n_iters=2000),
'RMSProp': lambda: rmsprop(rosenbrock_grad, x0, lr=0.01, n_iters=2000),
'Adam': lambda: adam(rosenbrock_grad, x0, lr=0.01, n_iters=2000),
}
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
for idx, (name, opt_fn) in enumerate(optimizers.items()):
ax = axes[idx // 3, idx % 3]
history = opt_fn()
ax.contour(X, Y, Z, levels=50, cmap='viridis', alpha=0.5)
ax.plot(history[:, 0], history[:, 1], 'r.-', markersize=2)
ax.plot([1], [1], 'g*', markersize=15)
ax.set_title(name)
ax.set_xlim(-2, 2)
ax.set_ylim(-1, 3)
plt.tight_layout()
plt.savefig('optimizer_comparison.png', dpi=150)
plt.show()2.4 学习率调度
2.4.1 常见调度策略
import torch.optim as optim
# 1. StepLR:每step_size个epoch,学习率乘以gamma
scheduler1 = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
# 2. MultiStepLR:在指定epoch乘以gamma
scheduler2 = optim.lr_scheduler.MultiStepLR(optimizer, milestones=[30, 60, 90], gamma=0.1)
# 3. ExponentialLR:每个epoch乘以gamma
scheduler3 = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)
# 4. CosineAnnealingLR:余弦退火
scheduler4 = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
# 5. CosineAnnealingWarmRestarts:带热重启的余弦退火
scheduler5 = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)
# 可视化学习率调度
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
model = torch.nn.Linear(10, 1)
optimizers_list = [
('StepLR', optim.lr_scheduler.StepLR(optim.SGD(model.parameters(), lr=1.0), 30, 0.1)),
('MultiStepLR', optim.lr_scheduler.MultiStepLR(optim.SGD(model.parameters(), lr=1.0), [30, 60, 90], 0.1)),
('ExponentialLR', optim.lr_scheduler.ExponentialLR(optim.SGD(model.parameters(), lr=1.0), 0.99)),
('CosineAnnealing', optim.lr_scheduler.CosineAnnealingLR(optim.SGD(model.parameters(), lr=1.0), 100)),
('CosineWarmRestarts', optim.lr_scheduler.CosineAnnealingWarmRestarts(optim.SGD(model.parameters(), lr=1.0), 10, 2)),
]
for idx, (name, scheduler) in enumerate(optimizers_list):
ax = axes[idx // 3, idx % 3]
lrs = []
for epoch in range(100):
lrs.append(scheduler.get_last_lr()[0])
scheduler.step()
ax.plot(lrs)
ax.set_title(name)
ax.set_xlabel('Epoch')
ax.set_ylabel('Learning Rate')
plt.tight_layout()
plt.savefig('lr_schedulers.png', dpi=150)
plt.show()2.4.2 学习率预热
class WarmupScheduler:
def __init__(self, optimizer, warmup_epochs, base_lr):
self.optimizer = optimizer
self.warmup_epochs = warmup_epochs
self.base_lr = base_lr
self.epoch = 0
def step(self):
self.epoch += 1
if self.epoch <= self.warmup_epochs:
lr = self.base_lr * self.epoch / self.warmup_epochs
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
# Transformer的Noam调度
class NoamScheduler:
def __init__(self, optimizer, model_dim, warmup_steps=4000):
self.optimizer = optimizer
self.model_dim = model_dim
self.warmup_steps = warmup_steps
self.step_num = 0
def step(self):
self.step_num += 1
lr = self.model_dim ** (-0.5) * min(
self.step_num ** (-0.5),
self.step_num * self.warmup_steps ** (-1.5)
)
for param_group in self.optimizer.param_groups:
param_group['lr'] = lr
# 可视化Noam调度
scheduler = NoamScheduler(None, model_dim=512, warmup_steps=4000)
lrs = []
for _ in range(20000):
scheduler.step()
lrs.append(scheduler.optimizer.param_groups[0]['lr'] if scheduler.optimizer else
512**(-0.5) * min(scheduler.step_num**(-0.5),
scheduler.step_num * 4000**(-1.5)))
plt.figure(figsize=(10, 4))
plt.plot(lrs)
plt.xlabel('Step')
plt.ylabel('Learning Rate')
plt.title('Noam Scheduler (Transformer)')
plt.savefig('noam_scheduler.png', dpi=150)
plt.show()2.5 优化中的挑战
2.5.1 鞍点问题
# 鞍点:梯度为0但不是最优点
# 在高维空间中,鞍点比局部最小值更常见
# 二维鞍点示例:f(x, y) = x² - y²
def saddle_function(x):
return x[0]**2 - x[1]**2
def saddle_grad(x):
return np.array([2*x[0], -2*x[1]])
# SGD容易卡在鞍点附近
# 动量法可以帮助逃离鞍点
x0 = np.array([0.5, 0.5])
history_sgd = batch_gradient_descent(saddle_grad, x0, lr=0.1, n_iters=50)
history_mom = sgd_with_momentum(saddle_grad, x0, lr=0.1, momentum=0.9, n_iters=50)
x = np.linspace(-2, 2, 200)
y = np.linspace(-2, 2, 200)
X, Y = np.meshgrid(x, y)
Z = X**2 - Y**2
plt.figure(figsize=(10, 8))
plt.contour(X, Y, Z, levels=50, cmap='RdBu_r')
plt.plot(history_sgd[:, 0], history_sgd[:, 1], 'r.-', label='SGD')
plt.plot(history_mom[:, 0], history_mom[:, 1], 'b.-', label='Momentum')
plt.plot([0], [0], 'k*', markersize=15, label='鞍点')
plt.legend()
plt.title('鞍点问题')
plt.savefig('saddle_point.png', dpi=150)
plt.show()2.5.2 梯度爆炸与消失
import torch
import torch.nn as nn
# 梯度消失:深层网络中梯度指数级衰减
def test_gradient_flow(depth, activation='sigmoid'):
"""测试梯度流"""
layers = []
for _ in range(depth):
layers.append(nn.Linear(100, 100))
if activation == 'sigmoid':
layers.append(nn.Sigmoid())
elif activation == 'relu':
layers.append(nn.ReLU())
elif activation == 'tanh':
layers.append(nn.Tanh())
model = nn.Sequential(*layers)
# 初始化
for m in model.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_normal_(m.weight)
x = torch.randn(32, 100)
out = model(x)
loss = out.sum()
loss.backward()
# 检查每层的梯度范数
grad_norms = []
for name, param in model.named_parameters():
if param.grad is not None:
grad_norms.append(param.grad.norm().item())
return grad_norms
# 比较不同激活函数的梯度流
activations = ['sigmoid', 'tanh', 'relu']
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for idx, act in enumerate(activations):
grad_norms = test_gradient_flow(20, act)
axes[idx].plot(grad_norms[::2], 'o-') # 只看权重梯度
axes[idx].set_title(f'{act.upper()}')
axes[idx].set_xlabel('Layer')
axes[idx].set_ylabel('Gradient Norm')
axes[idx].set_yscale('log')
plt.suptitle('梯度流对比(20层网络)')
plt.tight_layout()
plt.savefig('gradient_flow.png', dpi=150)
plt.show()三、实战案例
3.1 案例一:优化器在MNIST上的对比
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 数据
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
train_loader = DataLoader(datasets.MNIST('./data', train=True, download=True, transform=transform), batch_size=128, shuffle=True)
test_loader = DataLoader(datasets.MNIST('./data', train=False, transform=transform), batch_size=1000)
# 模型
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
def forward(self, x):
return self.fc(x.view(-1, 784))
# 优化器对比
optimizer_configs = {
'SGD': lambda p: torch.optim.SGD(p, lr=0.01),
'SGD+Momentum': lambda p: torch.optim.SGD(p, lr=0.01, momentum=0.9),
'AdaGrad': lambda p: torch.optim.Adagrad(p, lr=0.01),
'RMSProp': lambda p: torch.optim.RMSprop(p, lr=0.001),
'Adam': lambda p: torch.optim.Adam(p, lr=0.001),
}
results = {}
for name, opt_fn in optimizer_configs.items():
model = SimpleNet()
optimizer = opt_fn(model.parameters())
criterion = nn.CrossEntropyLoss()
test_accs = []
for epoch in range(5):
model.train()
for data, target in train_loader:
optimizer.zero_grad()
loss = criterion(model(data), target)
loss.backward()
optimizer.step()
model.eval()
correct, total = 0, 0
with torch.no_grad():
for data, target in test_loader:
pred = model(data).argmax(1)
correct += pred.eq(target).sum().item()
total += target.size(0)
test_accs.append(correct / total)
results[name] = test_accs
print(f"{name:15s}: 最终准确率 = {test_accs[-1]:.4f}")
# 可视化
plt.figure(figsize=(10, 5))
for name, accs in results.items():
plt.plot(accs, 'o-', label=name)
plt.xlabel('Epoch')
plt.ylabel('Test Accuracy')
plt.title('不同优化器的收敛对比')
plt.legend()
plt.savefig('optimizer_mnist.png', dpi=150)
plt.show()四、本章小结
本章系统介绍了深度学习的优化算法:
梯度下降变体:批量GD精确但慢,SGD快但震荡,小批量GD是实践中的标准选择。
动量法:累积历史梯度信息,加速收敛并帮助逃离鞍点。Nesterov动量通过lookahead进一步提高性能。
自适应学习率:AdaGrad为每个参数维护独立学习率但学习率单调递减;RMSProp用指数移动平均解决此问题;Adam结合动量和RMSProp,成为默认选择。
学习率调度:StepLR、CosineAnnealing、Warmup等策略帮助模型在训练初期快速学习,后期精细调优。
优化挑战:鞍点在高维空间中比局部最小值更常见;梯度消失/爆炸是深层网络训练的主要障碍。
选择合适的优化器和学习率调度是训练成功的关键。Adam是安全默认选择,但SGD+Momentum在某些任务上泛化更好。
五、思考与练习
思考题
为什么Adam在某些任务上泛化性能不如SGD?AMSGrad如何解决这个问题?
学习率预热为什么对Transformer训练很重要?
为什么小批量大小会影响泛化?大批量和小批量各有什么优劣?
梯度裁剪(gradient clipping)如何解决梯度爆炸问题?
为什么有些优化器在训练集上收敛快但泛化差?这与损失曲面的几何有什么关系?
编程练习
优化器对比:在CIFAR-10上系统比较SGD、Adam、AdaGrad、RMSProp的收敛速度和泛化性能。
学习率调度:实现余弦退火和线性预热,比较不同调度策略的效果。
动量可视化:在二维损失曲面上可视化动量法的轨迹,观察动量如何帮助穿越鞍点。
梯度流分析:训练不同深度的网络,分析每层的梯度分布。
自适应学习率:手动实现Adam算法,与PyTorch的实现对比。