深度学习篇---SGD+Momentum优化器

发布于:2025-09-04 ⋅ 阅读:(19) ⋅ 点赞:(0)

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,模型越复杂(参数越多),可能需要越大的权重衰减。

结合学习率调度器:动态调整

固定学习率很难适应整个训练过程(前期需要大步,后期需要微调),建议搭配学习率调度器,让参数调整更智能。

常用调度器及用法

  1. StepLR:按固定轮次衰减

    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
    
     

    每 50 轮将学习率乘以 0.5,适合损失稳定下降的场景。

  2. ReduceLROnPlateau:损失停滞时衰减(推荐)

    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=10)
    
     

    当损失连续 10 轮不下降时,学习率减半,适合复杂任务。

  3. CosineAnnealingLR:余弦式衰减

    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
    
     

    学习率先慢后快衰减,适合需要精细调优的场景(如竞赛)。

完整调优流程(实战步骤)

  1. 初筛参数

    • 固定 momentum=0.9weight_decay=0,用不同 lr(如 1e-4、5e-4、1e-3)训练 10~20 轮,找到能让损失明显下降的 lr。
  2. 优化收敛

    • 用第一步找到的 lr,对比 momentum=0.8/0.9/0.95,选择损失下降最快且稳定的动量值。
  3. 防止过拟合

    • 若出现过拟合,逐步增大 weight_decay(每次乘以 10),直到训练 / 测试损失差距缩小。
  4. 动态调整

    • 加入学习率调度器,让后期学习率自动减小,进一步优化收敛。
  5. 观察验证

    • 绘制损失曲线(训练 + 验证),确保损失持续下降且无明显震荡。
    • 若效果不佳,回到步骤 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 的核心是 “先调学习率,再调动量,最后加权重衰减”,并结合学习率调度器动态优化。记住:没有 “万能参数”,需要根据具体模型和数据反复实验,观察损失曲线的变化是最直接的判断依据。


网站公告

今日签到

点亮在社区的每一天
去签到