SGD+Momentum介绍:
SGD+Momentum(带动量的随机梯度下降)是 SGD 的增强版,它通过模拟物理中的 "惯性" 来加速收敛,减少训练过程中的震荡。下面用通俗的方式讲解其原理和用法,并提供详细代码示例。
为什么需要 Momentum?
普通 SGD 就像盲人下山,每一步只根据脚下的坡度决定方向,容易在陡坡处来回震荡,在平缓区域走得很慢。
Momentum(动量)就像给盲人装上了滑轮鞋:
- 下坡时会积累 "惯性",加速前进
- 遇到小颠簸(噪声)时,不会立刻改变方向,保持原有趋势
- 帮助跳出局部最小值,更快找到全局最优解
核心公式可以理解为:
当前更新量 = 动量×上一次更新量 + 学习率×当前梯度
新参数 = 旧参数 - 当前更新量
完整代码示例(对比有无动量的效果)
下面通过一个曲线拟合任务,直观展示 Momentum 的作用:
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np
# 设置随机种子,保证结果可复现
torch.manual_seed(42)
np.random.seed(42)
# 1. 生成带噪声的非线性数据(模拟真实场景)
x = torch.linspace(-5, 5, 100).view(-1, 1) # 100个从-5到5的点
y = 2 * x**3 + 3 * x**2 - 12 * x + 5 + torch.randn_like(x) * 10 # 三次函数+噪声
# 2. 定义模型(包含多层和非线性激活)
class ComplexModel(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(1, 32), # 输入层
nn.ReLU(), # 非线性激活
nn.Linear(32, 32), # 隐藏层
nn.ReLU(), # 非线性激活
nn.Linear(32, 1) # 输出层
)
def forward(self, x):
return self.layers(x)
# 创建两个相同结构的模型,分别用于对比
model_with_momentum = ComplexModel()
model_no_momentum = ComplexModel() # 结构相同,参数独立
# 3. 定义损失函数(均方误差)
loss_fn = nn.MSELoss()
# 4. 初始化优化器
# 带动量的SGD(推荐用法)
optimizer_with_momentum = torch.optim.SGD(
model_with_momentum.parameters(),
lr=0.0001, # 学习率:带动量时可适当减小
momentum=0.9, # 动量值:0.9是经过验证的最佳实践值
weight_decay=1e-5 # 权重衰减:防止过拟合
)
# 无动量的普通SGD(作为对比)
optimizer_no_momentum = torch.optim.SGD(
model_no_momentum.parameters(),
lr=0.0001, # 相同学习率
momentum=0.0 # 关闭动量
)
# 5. 学习率调度器:动态调整学习率
scheduler_with_momentum = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer_with_momentum,
mode='min', # 当损失不再减小时触发
factor=0.5, # 学习率乘以0.5
patience=10, # 容忍10轮无改善
verbose=True # 打印学习率变化信息
)
scheduler_no_momentum = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer_no_momentum,
mode='min',
factor=0.5,
patience=10,
verbose=True
)
# 6. 训练模型并记录过程
epochs = 500
losses_with_momentum = []
losses_no_momentum = []
for epoch in range(epochs):
# 训练带动量的模型
model_with_momentum.train() # 切换到训练模式
y_pred_m = model_with_momentum(x)
loss_m = loss_fn(y_pred_m, y)
losses_with_momentum.append(loss_m.item())
# 标准训练三步:清空梯度→反向传播→更新参数
optimizer_with_momentum.zero_grad()
loss_m.backward()
optimizer_with_momentum.step()
scheduler_with_momentum.step(loss_m) # 根据损失调整学习率
# 训练无动量的模型(相同步骤)
model_no_momentum.train()
y_pred_nm = model_no_momentum(x)
loss_nm = loss_fn(y_pred_nm, y)
losses_no_momentum.append(loss_nm.item())
optimizer_no_momentum.zero_grad()
loss_nm.backward()
optimizer_no_momentum.step()
scheduler_no_momentum.step(loss_nm)
# 每50轮打印一次进度
if (epoch + 1) % 50 == 0:
print(f"轮次 {epoch+1}/{epochs}")
print(f"带动量损失: {loss_m.item():.4f} | 无动量损失: {loss_nm.item():.4f}\n")
# 7. 可视化结果
plt.figure(figsize=(14, 6))
# 左图:拟合效果对比
plt.subplot(1, 2, 1)
plt.scatter(x.numpy(), y.numpy(), alpha=0.5, label='原始数据')
plt.plot(x.numpy(), model_with_momentum(x).detach().numpy(), 'r-', linewidth=2, label='SGD+Momentum')
plt.plot(x.numpy(), model_no_momentum(x).detach().numpy(), 'b--', linewidth=2, label='普通SGD')
plt.title('拟合效果对比')
plt.legend()
# 右图:损失下降曲线对比
plt.subplot(1, 2, 2)
plt.plot(range(epochs), losses_with_momentum, 'r-', label='带动量 (momentum=0.9)')
plt.plot(range(epochs), losses_no_momentum, 'b--', label='无动量 (momentum=0.0)')
plt.title('损失下降趋势')
plt.xlabel('训练轮次')
plt.ylabel('损失值')
plt.yscale('log') # 对数刻度,更清晰展示差异
plt.legend()
plt.tight_layout()
plt.show()
关键知识点解析
1. SGD+Momentum 的核心参数
momentum
:- 取值范围:0~1,0.9 是最常用的最佳值
- 物理意义:可以理解为 "惯性保留比例",0.9 表示保留 90% 的上一次更新方向
- 效果:值越大,惯性越强,适合在平坦区域加速;值太小则接近普通 SGD
lr
(学习率):- 带动量时建议比普通 SGD 小一些(比如普通 SGD 用 0.01,带动量可用 0.001)
- 因为动量会累积更新幅度,太大容易冲过最优值
weight_decay
:- 可选参数,相当于 L2 正则化,防止模型过拟合
- 推荐值:1e-5 ~ 1e-3,模型越复杂,可适当增大
2. 为什么 Momentum 效果更好?
从代码运行结果可以看到:
- 带动量的模型损失下降更快(红线始终在蓝线下方)
- 收敛更稳定,不会出现剧烈震荡
- 最终拟合效果更接近真实曲线
这是因为:
- 当梯度方向不变时(比如持续下坡),动量会不断累积,加速前进
- 当梯度方向频繁变化时(比如崎岖地形),动量会平滑震荡,保持整体方向
3. 实际使用技巧
- 初始化建议:先用
momentum=0.9
作为基准,这在 90% 的场景都有效 - 学习率配合:如果损失波动大,先减小学习率,而不是调低动量
- 与调度器结合:一定要用学习率调度器(如代码中的
ReduceLROnPlateau
),当损失停滞时自动减小学习率 - 适用场景:尤其适合复杂模型(如深层 CNN、多层感知机)和噪声多的数据
4. 常见误区
- 不要盲目增大动量:超过 0.95 可能导致 "冲过" 最优值,反而不稳定
- 不是所有场景都需要动量:简单线性模型可能用普通 SGD 效果差不多
- 动量不是银弹:它加速收敛,但不会改变最终收敛的最小值(只是更快到达)
通过这个示例,你可以清晰看到 Momentum 对训练过程的改善。实际项目中,建议优先使用SGD(lr=0.001, momentum=0.9)
作为起点,再根据损失曲线微调参数,这是经过大量实践验证的高效策略。
参数选择:
调整 SGD+Momentum 的参数是提升模型性能的关键,需要结合模型表现、损失曲线和实际场景来灵活调整。下面用通俗易懂的方式讲解核心参数的调整方法和实战技巧。
核心参数及调整逻辑
SGD+Momentum 有 3 个最关键的参数:学习率(lr)、动量(momentum)、权重衰减(weight_decay)。调整的核心逻辑是:让损失稳定下降,既不震荡发散,也不收敛过慢。
1. 学习率(lr):控制每一步的 “步长”
学习率是最重要的参数,直接决定训练是否有效。
调整方法:
初始值选择:
复杂模型(如深层 CNN)建议从1e-4 ~ 1e-3
开始;简单模型(如线性回归)可从1e-3 ~ 1e-2
开始。
带动量时,学习率通常比纯 SGD 小(因为动量会累积步长)。判断是否合适:
- 若损失 剧烈震荡 或突然上升(甚至 NaN):学习率太大,需减小(比如除以 10)。
- 若损失 下降极慢 或几乎不变:学习率太小,需增大(比如乘以 2~5)。
- 理想状态:损失持续下降,前期快、后期缓,无大幅波动。
实战技巧:
用 “学习率搜索” 找到合适范围:先设一个很小的 lr,逐渐增大,记录损失下降最快的 lr 区间。
例如:从1e-6
开始,每次乘以 10,观察损失变化,找到损失开始快速下降的 lr。
2. 动量(momentum):控制 “惯性大小”
动量决定了优化器对历史梯度的 “记忆” 程度,影响收敛速度和稳定性。
调整方法:
默认值:推荐先固定为
0.9
(这是经过大量实验验证的最佳起点)。何时需要调整:
- 若损失 震荡依然严重(即使 lr 合适):可适当增大动量(如 0.95),增强平滑效果。
- 若模型 收敛过慢 或陷入局部最优:可适当减小动量(如 0.8),让优化器更 “灵活” 地改变方向。
- 极端情况:数据噪声极大时,动量可设为 0.99(更强的平滑);简单凸优化问题可设为 0.5(减少惯性干扰)。
注意:动量不是越大越好,过大(如 > 0.99)可能导致优化器 “冲过” 最优值,反而不稳定。
3. 权重衰减(weight_decay):控制 “过拟合”
权重衰减本质是 L2 正则化,通过限制参数大小防止模型过度拟合训练数据。
调整方法:
- 初始值:默认设为
0
,观察是否过拟合后再添加。 - 判断是否需要:
- 若 训练损失小但测试损失大(明显过拟合):增大 weight_decay(如从 1e-5→1e-4→1e-3)。
- 若 训练和测试损失都大(欠拟合):减小或关闭 weight_decay(避免过度限制模型)。
- 常见范围:
1e-5 ~ 1e-3
,模型越复杂(参数越多),可能需要越大的权重衰减。
结合学习率调度器:动态调整
固定学习率很难适应整个训练过程(前期需要大步,后期需要微调),建议搭配学习率调度器,让参数调整更智能。
常用调度器及用法:
StepLR:按固定轮次衰减
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
每 50 轮将学习率乘以 0.5,适合损失稳定下降的场景。
ReduceLROnPlateau:损失停滞时衰减(推荐)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10)
当损失连续 10 轮不下降时,学习率减半,适合复杂任务。
CosineAnnealingLR:余弦式衰减
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
学习率先慢后快衰减,适合需要精细调优的场景(如竞赛)。
完整调优流程(实战步骤)
初筛参数:
- 固定
momentum=0.9
,weight_decay=0
,用不同 lr(如 1e-4、5e-4、1e-3)训练 10~20 轮,找到能让损失明显下降的 lr。
- 固定
优化收敛:
- 用第一步找到的 lr,对比
momentum=0.8/0.9/0.95
,选择损失下降最快且稳定的动量值。
- 用第一步找到的 lr,对比
防止过拟合:
- 若出现过拟合,逐步增大
weight_decay
(每次乘以 10),直到训练 / 测试损失差距缩小。
- 若出现过拟合,逐步增大
动态调整:
- 加入学习率调度器,让后期学习率自动减小,进一步优化收敛。
观察验证:
- 绘制损失曲线(训练 + 验证),确保损失持续下降且无明显震荡。
- 若效果不佳,回到步骤 1 重新调整 lr(最关键参数)。
示例:参数调整前后对比
假设初始参数训练时出现以下问题,如何调整?
问题场景 | 调整方案 |
---|---|
损失剧烈震荡,甚至发散 | 学习率太大 → 减小 lr(如从 1e-3→1e-4) |
损失下降极慢,50 轮后变化很小 | 学习率太小 → 增大 lr(如从 1e-5→1e-4) |
损失下降快但后期波动大 | 动量不足 → 增大 momentum(如 0.9→0.95) |
模型在训练集表现好,测试集差 | 过拟合 → 增大 weight_decay(如 1e-5→1e-4) |
损失下降到一定程度后停滞 | 学习率固定不变 → 加入调度器(如 ReduceLROnPlateau) |
总结
调整 SGD+Momentum 的核心是 “先调学习率,再调动量,最后加权重衰减”,并结合学习率调度器动态优化。记住:没有 “万能参数”,需要根据具体模型和数据反复实验,观察损失曲线的变化是最直接的判断依据。