别再死记硬背了!用PyTorch代码实战搞懂SGD、Adam优化器的核心区别

深度学习模型的训练过程中,优化器的选择往往决定了模型能否快速收敛到理想状态。对于准备面试的开发者来说,死记硬背SGD和Adam的理论区别远远不够——真正的理解来自于动手实践。本文将带你用PyTorch构建一个简单的神经网络,通过可视化训练过程,直观感受不同优化器的工作原理。

1. 环境准备与基础模型搭建

在开始比较优化器之前,我们需要准备一个标准的实验环境。这个环境要足够简单以突出优化器的差异,又要足够复杂以展示不同优化器的特性。我们选择在Jupyter Notebook中实现整个过程,方便实时观察训练动态。

首先安装必要的库:

pip install torch torchvision matplotlib numpy

接下来构建一个基础的全连接神经网络,用于后续的优化器实验。这个网络将处理一个简单的二分类任务:

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons

# 生成模拟数据
X, y = make_moons(n_samples=1000, noise=0.1, random_state=42)
X = torch.FloatTensor(X)
y = torch.FloatTensor(y).view(-1, 1)

# 定义简单神经网络
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(2, 16)
        self.fc2 = nn.Linear(16, 1)
        self.sigmoid = nn.Sigmoid()
        
    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.sigmoid(self.fc2(x))
        return x

提示:我们使用make_moons生成非线性可分的数据集,这种数据能够更好地展示优化器在不同地形下的表现差异。

2. SGD优化器实战分析

随机梯度下降(SGD)是最基础的优化器,理解它的工作原理是掌握其他优化器的前提。我们先实现一个标准的SGD训练过程,然后逐步添加动量等改进特性。

2.1 基础SGD实现

# 初始化模型和优化器
model = SimpleNet()
optimizer = optim.SGD(model.parameters(), lr=0.1)
criterion = nn.BCELoss()

# 训练过程
losses = []
for epoch in range(100):
    optimizer.zero_grad()
    outputs = model(X)
    loss = criterion(outputs, y)
    loss.backward()
    optimizer.step()
    losses.append(loss.item())
    
# 绘制损失曲线
plt.plot(losses)
plt.title("SGD Training Loss")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.show()

运行这段代码,你会观察到损失函数下降的曲线。基础SGD的主要特点是:

  • 更新方向完全依赖当前batch的梯度
  • 学习率在整个训练过程中保持不变
  • 容易陷入局部极小值或鞍点

2.2 加入动量的SGD

动量是SGD的一个重要改进,它通过累积过去的梯度信息来平滑更新方向:

# 带动量的SGD
optimizer_momentum = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)

losses_momentum = []
for epoch in range(100):
    optimizer_momentum.zero_grad()
    outputs = model(X)
    loss = criterion(outputs, y)
    loss.backward()
    optimizer_momentum.step()
    losses_momentum.append(loss.item())

# 对比曲线
plt.plot(losses, label='Vanilla SGD')
plt.plot(losses_momentum, label='SGD with Momentum')
plt.legend()
plt.show()

动量机制解决了SGD的两个主要问题:

  1. 梯度噪声:通过累积历史梯度,平滑了随机采样带来的噪声
  2. 病态曲率:在峡谷地形中,动量可以帮助参数更新跨越平缓区域

注意:动量系数通常设置为0.9左右,这个值决定了历史梯度对当前更新的影响程度。

3. Adam优化器深度解析

Adam(Adaptive Moment Estimation)是当前最流行的优化器之一,它结合了动量思想和自适应学习率。让我们通过代码来理解它的核心机制。

3.1 基础Adam实现

# Adam优化器
model = SimpleNet()
optimizer_adam = optim.Adam(model.parameters(), lr=0.01)

losses_adam = []
for epoch in range(100):
    optimizer_adam.zero_grad()
    outputs = model(X)
    loss = criterion(outputs, y)
    loss.backward()
    optimizer_adam.step()
    losses_adam.append(loss.item())

# 对比三种优化器
plt.plot(losses, label='Vanilla SGD')
plt.plot(losses_momentum, label='SGD with Momentum')
plt.plot(losses_adam, label='Adam')
plt.legend()
plt.show()

Adam的核心创新在于:

  • 一阶矩估计:类似动量的梯度平滑
  • 二阶矩估计:类似RMSProp的自适应学习率
  • 偏置校正:解决初始化时的偏差问题

3.2 Adam参数更新可视化

为了更直观地理解Adam的工作原理,我们可以可视化参数更新的路径:

def train_with_trace(optimizer):
    model = SimpleNet()
    criterion = nn.BCELoss()
    optimizer = optimizer(model.parameters(), lr=0.01)
    
    # 记录参数轨迹
    trace = []
    for epoch in range(100):
        optimizer.zero_grad()
        outputs = model(X)
        loss = criterion(outputs, y)
        loss.backward()
        optimizer.step()
        trace.append(model.fc1.weight.data.numpy().copy())
    return np.array(trace)

# 获取不同优化器的参数轨迹
trace_sgd = train_with_trace(optim.SGD)
trace_adam = train_with_trace(optim.Adam)

# 可视化前两个参数的更新路径
plt.plot(trace_sgd[:,0,0], trace_sgd[:,0,1], label='SGD')
plt.plot(trace_adam[:,0,0], trace_adam[:,0,1], label='Adam')
plt.legend()
plt.title("Parameter Update Path")
plt.show()

从参数更新路径可以看出:

特性 SGD Adam
更新方向 当前梯度方向 历史梯度与当前梯度的平衡
步长 固定 自适应调整
路径平滑度 波动较大 相对平滑
收敛速度 较慢 通常较快

4. 优化器选择与调参实践

理解了优化器的核心区别后,我们需要掌握在实际项目中如何选择和调优优化器。不同的任务和模型架构可能需要不同的优化策略。

4.1 何时选择SGD,何时选择Adam

根据实践经验,两种优化器各有适用场景:

  • SGD(带动量)更适合

    • 需要极高精度的任务(如超大型模型训练)
    • 数据分布相对简单、稳定的场景
    • 当计算资源有限时(Adam需要存储更多状态)
  • Adam更适合

    • 大多数深度学习任务
    • 非平稳目标函数
    • 稀疏梯度问题
    • 超参数调优资源有限时

4.2 优化器调参技巧

无论选择哪种优化器,合理的超参数设置都至关重要:

SGD关键参数

optim.SGD(params, lr=0.1, momentum=0.9, weight_decay=0.0001)
  • lr:通常从0.1开始尝试,根据训练动态调整
  • momentum:0.9是一个良好的默认值
  • weight_decay:L2正则化系数,防止过拟合

Adam关键参数

optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08)
  • lr:默认0.001通常表现良好
  • betas:控制一阶和二阶矩估计的衰减率
  • eps:数值稳定项,通常不需要修改

提示:在实际项目中,可以先用Adam快速获得一个基准模型,然后再尝试用调优后的SGD追求更高精度。

4.3 学习率调度策略

除了优化器本身,学习率的动态调整也至关重要。PyTorch提供了多种学习率调度器:

# 余弦退火调度器示例
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)

for epoch in range(100):
    train(...)
    scheduler.step()

常见的学习率调度策略包括:

  1. StepLR:固定步长衰减
  2. ReduceLROnPlateau:基于验证损失动态调整
  3. CosineAnnealing:余弦周期调整
  4. OneCycleLR:单周期学习率策略

在实际项目中,我发现结合Adam优化器和ReduceLROnPlateau调度器通常能取得不错的效果,特别是在计算机视觉任务中。当验证损失停滞时自动降低学习率,既能保证快速收敛,又能获得更好的最终性能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐