别再死记硬背了!用PyTorch代码实战搞懂SGD、Adam优化器的核心区别
别再死记硬背了!用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的两个主要问题:
- 梯度噪声:通过累积历史梯度,平滑了随机采样带来的噪声
- 病态曲率:在峡谷地形中,动量可以帮助参数更新跨越平缓区域
注意:动量系数通常设置为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()
常见的学习率调度策略包括:
- StepLR:固定步长衰减
- ReduceLROnPlateau:基于验证损失动态调整
- CosineAnnealing:余弦周期调整
- OneCycleLR:单周期学习率策略
在实际项目中,我发现结合Adam优化器和ReduceLROnPlateau调度器通常能取得不错的效果,特别是在计算机视觉任务中。当验证损失停滞时自动降低学习率,既能保证快速收敛,又能获得更好的最终性能。
更多推荐


所有评论(0)