想象一下你在滑雪场第一次尝试从山坡滑下。如果只是简单地顺着当前坡度方向移动(传统梯度下降),可能会因为雪面凹凸不平而卡在小坑里。但当你学会利用身体惯性(动量)时,即使遇到小坑也能保持前进势头——这正是Momentum优化器的核心思想。本文将用这类生活化类比,带你直观理解PyTorch中torch.optim.SGD(momentum=0.9)背后的物理智慧。
动量(Momentum)本质上是一个物体运动状态的持续性表现。当冰壶运动员推出石壶后,即便停止施力,石壶仍会滑行数十米;自行车下坡时,即使不踩踏板速度也会越来越快。这些现象背后有两个关键要素:
用数学公式表示这种关系:
code复制当前速度 = 当前推力 + 前一时刻速度 × 衰减系数
在PyTorch的SGD with Momentum中,这个衰减系数就是momentum参数(通常设为0.9)。下面这个表格展示了不同动量值对应的物理类比:
| 动量参数值 | 物理类比 | 优化行为特征 |
|---|---|---|
| 0 | 完全无惯性 | 传统梯度下降 |
| 0.5 | 沙地行走 | 适度平滑更新方向 |
| 0.9 | 冰面滑行 | 显著加速收敛 |
| 0.99 | 太空无重力环境 | 可能过度震荡 |
提示:动量参数本质上是在当前梯度与历史更新方向之间做加权平均,0.9意味着历史方向占90%权重
传统梯度下降就像一位谨慎的滑雪者:
python复制# 传统SGD更新规则
w = w - learning_rate * gradient
这种方式的缺陷在复杂地形中尤为明显:
引入动量后的更新方式更像专业滑雪选手:
python复制# SGD with Momentum更新规则
velocity = momentum * velocity - learning_rate * gradient
w = w + velocity
实际代码示例对比:
python复制import torch
# 创建测试函数 y = x^4 + 3x^3 - 20x^2
def func(x):
return x**4 + 3*x**3 - 20*x**2
# 传统SGD
x_sgd = torch.tensor([5.], requires_grad=True)
opt_sgd = torch.optim.SGD([x_sgd], lr=0.01)
# SGD with Momentum
x_momentum = torch.tensor([5.], requires_grad=True)
opt_momentum = torch.optim.SGD([x_momentum], lr=0.01, momentum=0.9)
# 训练过程记录
loss_history = {'sgd': [], 'momentum': []}
for epoch in range(100):
# 传统SGD
y_sgd = func(x_sgd)
y_sgd.backward()
opt_sgd.step()
opt_sgd.zero_grad()
loss_history['sgd'].append(y_sgd.item())
# Momentum SGD
y_momentum = func(x_momentum)
y_momentum.backward()
opt_momentum.step()
opt_momentum.zero_grad()
loss_history['momentum'].append(y_momentum.item())
运行后会明显观察到:
动量机制本质上是一种特殊的加权平均方法——指数加权移动平均(EWMA)。这种计算方式使得:
数学表达式为:
code复制v_t = β·v_{t-1} + (1-β)·θ_t
其中β=0.9时,各时刻梯度的权重分布如下:
| 时间步 | 权重计算 | 实际权重 |
|---|---|---|
| t | (1-β) | 10% |
| t-1 | (1-β)·β | 9% |
| t-2 | (1-β)·β² | 8.1% |
| ... | ... | ... |
| t-10 | (1-β)·β^10 | ≈3.5% |
这种特性带来三个实用优势:
可视化不同β值的效果:
python复制import numpy as np
import matplotlib.pyplot as plt
def exp_weights(beta, steps):
return [(1-beta)*beta**i for i in range(steps)]
betas = [0.5, 0.9, 0.95, 0.99]
plt.figure(figsize=(10,6))
for beta in betas:
weights = exp_weights(beta, 100)
plt.plot(weights, label=f'β={beta}')
plt.legend()
plt.title('Exponential Weight Distribution')
plt.xlabel('Time steps ago')
plt.ylabel('Weight')
plt.show()
在实际项目中使用Momentum时,有几个关键经验值得注意:
学习率与动量的配合
典型问题与解决方案
损失震荡剧烈
收敛速度慢
Nesterov动量改进
python复制optimizer = torch.optim.SGD(
params,
lr=0.01,
momentum=0.9,
nesterov=True # 启用Nesterov加速
)
Nesterov变体会先按动量方向"展望"一步,再计算梯度,通常能获得更好效果
不同任务的经验设置
| 任务类型 | 推荐动量值 | 学习率范围 |
|---|---|---|
| 图像分类 | 0.9-0.95 | 0.1-0.001 |
| 目标检测 | 0.9 | 0.01-0.0001 |
| 自然语言处理 | 0.95-0.99 | 0.001-0.00001 |
| 生成对抗网络 | 0.5-0.9 | 0.0001-0.00001 |
在ResNet训练中,动量设置为0.9配合初始学习率0.1,之后按计划衰减,是经过验证的可靠方案。当改用Adam等自适应优化器时,虽然不需要手动设置动量参数,但理解其原理对调试β1参数同样有帮助。