一、引言:学习率在深度学习中的作用

在之前的博客中,我们完成了数据增强、模型训练、保存和推理的完整流程。但如果你仔细观察训练过程,会发现一个有趣的现象:损失值在训练初期快速下降,但到了后期却卡住了,无论如何训练都难以继续改善。

这背后的原因",很可能就是学习率(Learning Rate) 设置不当。

学习率控制着模型参数更新的步长,是深度学习中最重要、也最难调的超参数之一:

  • 学习率太大:参数更新步子太大,损失值剧烈震荡,甚至发散,永远到不了最优点。

  • 学习率太小:参数更新步子太小,训练速度极慢,可能陷入局部最优无法跳出。

  • 固定学习率:训练初期合适的学习率,到了后期可能太大;后期合适的学习率,在初期又太小。

那么,有没有一种方法能让学习率在训练过程中自动调整呢?答案是肯定的——学习率调度器(Learning Rate Scheduler)

本篇博客将基于完整代码,讲解两种常用的学习率调度策略:StepLRReduceLROnPlateau,并介绍权重衰减(Weight Decay) 这一防止过拟合的重要技术。

二、学习率调度器概述

2.1 为什么需要学习率调度

想象一下你在一座山上寻找最低点:

  • 刚开始:你离谷底还很远,应该大步流星地快速下降——需要大学习率

  • 接近谷底:你已经接近最低点,应该小步慢走,避免一脚跨过最低点——需要小学习率

如果全程使用同一个步长,要么走得慢,要么永远到不了最优点。学习率调度正是为了解决这个问题——随着训练进行,逐步减小学习率。

2.2 PyTorch中的调度器

PyTorch在 torch.optim.lr_scheduler 中提供了多种学习率调度器:

调度器调整策略
StepLR每隔固定步长,学习率乘以gamma
MultiStepLR在指定里程碑处调整学习率
ExponentialLR每个epoch学习率乘以gamma
CosineAnnealingLR余弦退火,周期性调整
ReduceLROnPlateau根据指标变化自动调整
OneCycleLR一个周期内先升后降

代码中展示的是 StepLRReduceLROnPlateau 两种。

三、StepLR——固定步长衰减

3.1 基本用法

scheduler = torch.optim.lr_scheduler.StepLR(
    optimizer, 
    step_size=10,    # 每10个epoch调整一次
    gamma=0.5        # 学习率乘以0.5
)

参数说明

参数含义
optimizer要调整的优化器
step_size调整周期(多少epoch调整一次)
gamma衰减因子,新学习率 = 旧学习率 × gamma

效果

  • Epoch 1-10:lr = 0.1

  • Epoch 11-20:lr = 0.05

  • Epoch 21-30:lr = 0.025

  • Epoch 31-40:lr = 0.0125

  • ...

学习率呈阶梯式下降,故名 "Step" LR。

3.2 如何在训练中使用

调度器需要在每个epoch结束后调用 step()

for epoch in range(epochs):
    train(...)          # 训练一个epoch
    test(...)           # 测试
    scheduler.step()    # 调整学习率

注意StepLRstep() 不需要参数,因为它只依赖于epoch计数。

3.3 StepLR的优缺点

优点

  • 简单直观,易于理解

  • 训练后期学习率变小,有助于精细收敛

缺点

  • 调整时机是预先设定的,无法根据实际训练情况自适应

  • 如果step_size设置不当,可能过早或过晚衰减

四、ReduceLROnPlateau——自适应学习率调整

4.1 基本用法

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer,
    mode='min',              # 监控指标是越小越好(如loss)
    factor=0.1,              # 学习率乘以0.1
    patience=10,             # 连续10次没有改善才调整
    threshold=0.0001,        # 改善小于该值认为没有改善
    threshold_mode='rel',    # 相对模式
    cooldown=0,              # 调整后等待0个epoch再重新监控
    min_lr=0,                # 学习率下限
    eps=1e-08                # 学习率变化的最小值
)

参数详解

参数含义
mode'min'表示监控指标越小越好(如loss),'max'表示越大越好(如accuracy)
factor学习率衰减因子,新学习率 = 旧学习率 × factor
patience"耐心值",连续多少个epoch没有改善才调整
threshold判定"改善"的阈值
threshold_mode'rel'相对模式,'abs'绝对模式
cooldown调整后等待多少个epoch再重新开始监控
min_lr学习率的下限,不会低于此值
eps学习率变化的最小值,小于此值不调整

4.2 核心思想

ReduceLROnPlateau 的核心思想是:当监控指标(如loss)不再下降时,说明当前学习率可能太大了,应该减小学习率

与StepLR的"固定周期调整"不同,ReduceLROnPlateau是自适应的——它会观察指标的变化,只有在真正需要时才调整学习率。

4.3 如何在训练中使用

关键区别:ReduceLROnPlateau的 step() 需要传入监控指标

for epoch in range(epochs):
    train_loss = train(...)
    scheduler.step(train_loss)    # 传入loss,让调度器判断

或者像代码中那样,在训练循环的每个batch后调用:

def train(dataloader, model, loss_fn, optimizer):
    model.train()
    for x, y in dataloader:
        # ... 前向传播、反向传播 ...
        loss_value = loss.item()
        scheduler.step(loss_value)    # 传入当前loss
        print(f"loss:{loss_value:>7f}")

注意:代码中在每个batch后就调用 scheduler.step(loss_value),这意味着调度器会每个batch都检查一次loss。这种方式更细粒度,但可能因为batch间loss波动而导致频繁调整。更常见的做法是每个epoch调用一次,传入该epoch的平均loss。

4.4 ReduceLROnPlateau的优缺点

优点

  • 自适应,根据实际训练情况调整

  • 不需要预先设定调整周期

  • 训练更稳定,避免过早/过晚衰减

缺点

  • 需要传入监控指标,使用稍复杂

  • 参数较多,需要根据任务调整

4.5 两种调度器对比

对比项StepLRReduceLROnPlateau
调整依据固定的epoch数监控指标的变化
是否自适应
step()参数需要传入指标
适用场景训练周期已知训练周期不确定
调参难度

五、权重衰减——防止过拟合

5.1 什么是权重衰减

权重衰减(Weight Decay),也称为L2正则化,是一种防止模型过拟合的技术。

核心思想:在损失函数中添加一个与模型权重平方成正比的惩罚项:

L_{total}=L_{oreginal}+\lambda \sum \omega _{i}^{2}

其中 \lambda 就是权重衰减系数。

为什么能防止过拟合?

  • 过拟合的模型往往权重值很大(对训练数据"死记硬背")

  • 加上权重平方惩罚后,模型会倾向于使用更小的权重值

  • 更小的权重意味着模型更"简单",对噪声不敏感,泛化能力更强

5.2 在优化器中设置权重衰减

optimizer = torch.optim.Adam(
    model.parameters(), 
    lr=0.1,
    weight_decay=1e-4    # 权重衰减系数
)

注意:代码中 weight_decay 参数并没有显式设置,默认为0(即不使用权重衰减)。如果需要启用,可以添加这个参数。

5.3 权重衰减 vs 学习率衰减

很多人容易混淆这两个概念:

概念作用对象目的
学习率衰减优化器的学习率让训练后期更精细,加速收敛
权重衰减模型的权重参数防止过拟合,提升泛化能力

两者是不同维度的技术,可以同时使用。

六、完整训练代码解析

6.1 数据准备

数据准备部分与之前一致,包括数据增强、自定义数据集、DataLoader:

training_data = food_dataset(file_path='./train.txt', 
                             transform=data_transforms['trainda'])
test_data = food_dataset(file_path='./test.txt', 
                         transform=data_transforms['valid'])

train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=True)

6.2 训练函数

def train(dataloader, model, loss_fn, optimizer):
    model.train()
    for x, y in dataloader:
        x, y = x.to(device), y.to(device)
        pred = model.forward(x)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        loss_value = loss.item()
        scheduler.step(loss_value)      # 关键:每个batch后调整学习率
        print(f"loss:{loss_value:>7f}")

与之前代码的区别:多了 scheduler.step(loss_value) 这一行。

6.3 测试函数

acc_s = []
loss_s = []
best_acc = 0

def test(dataloader, model, loss_fn):
    global best_acc
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model.forward(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f"Test result: \n Accuracy: {(100*correct)}%, Avg loss: {test_loss}")
    acc_s.append(correct)     # 记录准确率
    loss_s.append(test_loss)  # 记录损失

新增内容:用 acc_sloss_s 两个列表记录每个epoch的准确率和损失,方便后续绘制学习曲线。

6.4 优化器与调度器配置

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.1)

# 方案一:StepLR
# scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)

# 方案二:ReduceLROnPlateau
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, 
    mode='min', 
    factor=0.1,
    patience=10,
    threshold=0.0001,
    threshold_mode='rel',
    cooldown=0,
    min_lr=0,
    eps=1e-08
)

注意lr=0.1 是一个相对较大的初始学习率,配合调度器逐步衰减,可以让训练初期快速下降,后期精细调整。

6.5 训练循环

epochs = 100
for t in range(epochs):
    print(f"Epoch {t+1}\n-----------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
print("Done!")
test(test_dataloader, model, loss_fn)

注意:这里 epochs = 100,是一个较大的训练轮数。有了学习率调度器,即使训练很多轮,也不会因为学习率过大而震荡,反而能在后期精细收敛。

七、学习率调度效果分析

7.1 学习率变化曲线

使用StepLR时,学习率变化如下:

Epoch 1-10:   lr = 0.1
Epoch 11-20:  lr = 0.05
Epoch 21-30:  lr = 0.025
Epoch 31-40:  lr = 0.0125
...

使用ReduceLROnPlateau时,学习率会在loss不再下降时自动衰减。

7.2 训练效果对比

训练策略收敛速度最终精度稳定性
固定学习率初期快,后期震荡中等
StepLR较快较高
ReduceLROnPlateau自适应最高最好

7.3 学习曲线可视化(扩展)

可以结合 acc_sloss_s 绘制学习曲线:

import matplotlib.pyplot as plt

epochs_range = range(1, len(acc_s) + 1)

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc_s, 'b-', label='Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.title('准确率曲线')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss_s, 'r-', label='Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('损失曲线')
plt.legend()

plt.tight_layout()
plt.show()

八、总结

本篇博客围绕学习率调度权重衰减两大主题,系统讲解了:

知识点核心内容
学习率调度训练过程中动态调整学习率
StepLR每隔固定epoch衰减,简单直观
ReduceLROnPlateau根据指标变化自适应调整
权重衰减L2正则化,防止过拟合
学习曲线记录acc和loss,分析训练过程

更多推荐