共计 2524 个字符,预计需要花费 7 分钟才能阅读完成。
在机器学习模型训练中,梯度下降陷入局部最优是导致收敛效果差的常见问题。本文将系统介绍 5 种解决方案的数学原理和实现差异,通过 PyTorch 代码对比各方法在 MNIST 数据集上的收敛曲线,并提供学习率衰减策略选择、批量大小调优等生产环境最佳实践。

1. 问题描述:局部最优的直观理解
想象一个复杂的损失函数曲面,在非凸情况下可能存在多个低谷(局部最优)和一个全局最低点(全局最优)。梯度下降算法就像一个小球沿着曲面滚动,很容易被困在某个局部低谷中无法逃脱。
数学上,标准梯度下降的更新公式为:
$$\theta_{t+1} = \theta_t – \eta \nabla J(\theta_t)$$
其中 $\eta$ 是学习率,$\nabla J(\theta_t)$ 是梯度。这个简单规则在凸函数上能保证收敛,但在非凸情况下就可能陷入困境。
2. 解决方案
2.1 基础方法:动量法
动量法通过引入 ” 惯性 ” 帮助参数更新跳出局部最优。SGD with Momentum 的更新公式:
$$
\begin{aligned}
v_t &= \gamma v_{t-1} + \eta \nabla J(\theta_t) \
\theta_{t+1} &= \theta_t – v_t
\end{aligned}
$$
Nesterov 加速梯度 (NAG) 是其改进版,先根据当前动量方向前进一小步再计算梯度:
$$
\begin{aligned}
v_t &= \gamma v_{t-1} + \eta \nabla J(\theta_t – \gamma v_{t-1}) \
\theta_{t+1} &= \theta_t – v_t
\end{aligned}
$$
2.2 自适应方法
Adam 结合了动量思想和自适应学习率,其核心公式:
$$
\begin{aligned}
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 \
\hat{m}t &= \frac{m_t}{1-\beta_1^t} \
\hat{v}_t &= \frac{v_t}{1-\beta_2^t} \
\theta_t
\end{aligned}
$$} &= \theta_t – \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m
2.3 随机方法
Mini-batch 梯度下降通过引入噪声帮助跳出局部最优。其关键是在合理范围内减小 batch size(如从 256 降到 64),增加更新的随机性。
3. PyTorch 实现对比
import torch
import torch.optim as optim
from visdom import Visdom
# 初始化可视化
viz = Visdom()
win = viz.line(Y=torch.zeros((1, 5)),
opts=dict(legend=['SGD', 'Momentum', 'NAG', 'Adam', 'Adagrad'],
title='Loss Curve Comparison'))
# 定义模型和数据加载器
model = ...
train_loader = ...
# 定义不同优化器
optimizers = [optim.SGD(model.parameters(), lr=0.01),
optim.SGD(model.parameters(), lr=0.01, momentum=0.9),
optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True),
optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999)),
optim.Adagrad(model.parameters(), lr=0.01)
]
# 训练循环
losses = [[] for _ in range(5)]
for epoch in range(10):
for i, (data, target) in enumerate(train_loader):
for opt_idx, optimizer in enumerate(optimizers):
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
losses[opt_idx].append(loss.item())
# 更新可视化
viz.line(Y=torch.tensor([[sum(l)/len(l) for l in losses]]),
X=torch.tensor([epoch]),
win=win,
update='append')
4. 避坑指南
4.1 学习率与批量大小
- 大 batch size 需要更大的学习率,但不宜超过线性比例
- 实践中可参考线性缩放规则:$\eta_{new} = \eta_{base} \times \frac{batch_{new}}{batch_{base}}$
4.2 早停策略
- 验证集损失连续 N 个 epoch 不下降时停止(N 通常取 5 -10)
- 保留最佳模型副本,避免过度训练
4.3 梯度累积
当 GPU 内存不足时,可以通过多次前向 - 反向传播累积梯度后再更新参数:
accum_steps = 4
for i, (data, target) in enumerate(train_loader):
output = model(data)
loss = criterion(output, target)
loss = loss / accum_steps # 平均损失
loss.backward()
if (i+1) % accum_steps == 0:
optimizer.step()
optimizer.zero_grad()
5. 思考题
- 如何设计动态调整优化器参数的策略?比如根据训练进度自动调整 Adam 的 $\beta_1$、$\beta_2$
- 在 Transformer 训练中这些方法有何特殊注意事项?特别是与学习率 warmup、层归一化的配合
通过本文的实验和分析,我们可以看到不同优化方法在不同场景下各有优势。在实际项目中,建议先用 Adam 快速验证模型可行性,再根据具体需求切换到更精细的优化策略。
