6深度学习

优化算法

训练技巧

SGDAdam学习率调度

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章:优化算法(Optimization Algorithms)

一、导读

1.1 本章学习目标

第6章深入讲解深度学习中使用的优化算法。优化是深度学习的核心——训练神经网络就是在高维非凸损失曲面上寻找好的参数值。优化算法的效率和稳定性直接决定了模型能否成功训练。

本章的核心学习目标包括:

  • 理解梯度下降的基本原理:批量梯度下降、随机梯度下降、小批量梯度下降
  • 掌握动量法的原理:如何加速收敛和穿越鞍点
  • 理解自适应学习率算法:AdaGrad、RMSProp、Adam的设计思想
  • 掌握学习率调度策略:衰减、预热、余弦退火
  • 理解优化中的挑战:鞍点、病态曲率、梯度爆炸/消失
  • 掌握实际训练中的调优技巧

1.2 为什么本章如此重要

优化是深度学习训练的核心引擎。即使模型架构和损失函数设计得再好,如果优化算法选择不当,模型也无法收敛到好的解。

关键问题:

  • 为什么Adam是默认选择?它的优势在哪里?
  • 学习率为什么是最重要的超参数?
  • 为什么训练初期用大学习率,后期用小学习率?
  • 梯度消失/爆炸如何影响优化?

理解优化算法的原理,才能正确选择和调优超参数,解决训练中遇到的收敛问题。

1.3 核心问题引导

随机梯度下降为什么比批量梯度下降更常用?噪声有什么好处?

动量法的物理直觉是什么?它如何帮助穿越鞍点?

Adam为什么成为默认优化器?它解决了哪些问题?

学习率预热(warmup)为什么重要?

鞍点在高维空间中为什么比局部最小值更常见?

如何判断优化过程中遇到了什么问题?


二、核心概念详解

2.1 梯度下降基础

2.1.1 批量梯度下降

python
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)

python
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 小批量梯度下降

python
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 model

2.2 动量法

2.2.1 原理

动量法引入"速度"变量,累积历史梯度信息:

v_t = μ·v_{t-1} + η·∇L(θ_t)
θ_{t+1} = θ_t - v_t
python
def 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 动量的物理直觉

python
# 动量法的物理类比:球在损失曲面上滚动
# 速度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

python
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

python
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

python
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 常见调度策略

python
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 学习率预热

python
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 鞍点问题

python
# 鞍点:梯度为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 梯度爆炸与消失

python
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上的对比

python
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的实现对比。

6/10优化算法
0%