共计 1612 个字符,预计需要花费 5 分钟才能阅读完成。
背景痛点:为什么需要动量因子?
在训练 BP 神经网络时,最基础的优化方法是随机梯度下降(SGD)。但实际使用中经常会遇到两个典型问题:
- 梯度震荡:在峡谷状误差曲面中(即一个方向梯度大、另一个方向梯度小),参数更新会像皮球一样在两侧来回弹跳,导致收敛缓慢
- 局部最优陷阱:遇到平坦区域或局部极小值时,梯度值变得非常小,参数几乎停止更新
动量因子(Momentum)的引入,就像给梯度下降过程增加了 ” 惯性 ”。它通过累积历史梯度的指数加权平均,使参数更新方向更加平滑稳定。
数学原理:普通 SGD vs 带动量的 SGD
普通 SGD 更新规则:
$$\theta_{t+1} = \theta_t – \eta \nabla J(\theta_t)$$
带动量的 SGD 更新规则:
$$
\begin{aligned}
&v_t = \beta v_{t-1} + (1-\beta)\nabla J(\theta_t) \
&\theta_{t+1} = \theta_t – \eta v_t
\end{aligned}
$$
其中:
– $\beta$ 是动量系数(通常取 0.9 左右)
– $v_t$ 是当前时刻的动量向量
– $\eta$ 是学习率
动量因子的物理意义相当于给参数更新增加了 ” 记忆 ”,当前更新方向会保留部分历史梯度的信息。这就像下坡时推一个重球:即使遇到小坑洼(局部极小值),球的惯性也能帮助它冲过去。
Python 实现详解
以下是带有动量因子的 SGD 优化器实现关键代码(基于 PyTorch 框架):
import torch
import torch.nn as nn
class SGDWithMomentum:
def __init__(self, params, lr=0.01, momentum=0.9):
self.params = list(params)
self.lr = lr
self.momentum = momentum
self.velocity = [torch.zeros_like(p) for p in self.params] # 动量向量初始化
def step(self):
for i, (p, v) in enumerate(zip(self.params, self.velocity)):
if p.grad is None:
continue
# 动量更新公式实现
v.mul_(self.momentum).add_(p.grad, alpha=1-self.momentum)
# 参数更新
p.data.add_(v, alpha=-self.lr)
关键实现细节说明:
velocity初始化:为每个参数维护一个速度向量,初始值为 0mul_和add_操作:实现 $v_t = \beta v_{t-1} + (1-\beta)\nabla J$ 的原地计算- 参数更新:使用累积的速度向量而非原始梯度
实验分析:不同 β 值的影响
在 MNIST 数据集上测试不同动量系数(固定学习率 0.01):
| β 值 | 训练表现 |
|---|---|
| 0.5 | 收敛速度中等,震荡较明显 |
| 0.9 | 收敛快且稳定(推荐默认值) |
| 0.99 | 初期缓慢,后期可能发生过冲 |

(图示:不同 β 值对应的训练损失曲线变化)
调参避坑指南
- 学习率搭配
- 使用动量时,通常需要比纯 SGD 更小的学习率
-
建议初始尝试:纯 SGD 学习率的 1 /10
-
过冲现象识别
- 当损失函数值出现周期性大幅震荡时,可能是 β 值过大
-
典型症状:验证集准确率波动大于 5%
-
与 Adam 的对比
- Adam 自带动量机制,通常无需手动设置 β
- 建议:
- 传统网络可以先用 SGD+ 动量
- 复杂结构(如 Transformer)优先尝试 Adam
关键结论
- 动量因子本质是梯度的指数加权平均,能平滑更新方向
- β=0.9 在大多数情况下是安全默认值
- 学习率需要相应调小,否则容易引发震荡
- 与 Nesterov 动量不同:标准动量先计算梯度再更新,Nesterov 先更新临时参数再计算梯度
通过合理使用动量因子,可以将神经网络的训练速度提升 2 - 5 倍,特别是在处理非凸优化问题时效果显著。建议读者在理解原理的基础上,通过可视化工具(如 TensorBoard)实时观察不同参数的效果。
