一、引言:学习率在深度学习中的作用
在之前的博客中,我们完成了数据增强、模型训练、保存和推理的完整流程。但如果你仔细观察训练过程,会发现一个有趣的现象:损失值在训练初期快速下降,但到了后期却卡住了,无论如何训练都难以继续改善。
这背后的原因",很可能就是学习率(Learning Rate) 设置不当。
学习率控制着模型参数更新的步长,是深度学习中最重要、也最难调的超参数之一:
-
学习率太大:参数更新步子太大,损失值剧烈震荡,甚至发散,永远到不了最优点。
-
学习率太小:参数更新步子太小,训练速度极慢,可能陷入局部最优无法跳出。
-
固定学习率:训练初期合适的学习率,到了后期可能太大;后期合适的学习率,在初期又太小。
那么,有没有一种方法能让学习率在训练过程中自动调整 呢?答案是肯定的------学习率调度器(Learning Rate Scheduler)。
本篇博客将基于完整代码,讲解两种常用的学习率调度策略:StepLR 和 ReduceLROnPlateau ,并介绍权重衰减(Weight Decay) 这一防止过拟合的重要技术。
二、学习率调度器概述
2.1 为什么需要学习率调度
想象一下你在一座山上寻找最低点:
-
刚开始 :你离谷底还很远,应该大步流星 地快速下降------需要大学习率
-
接近谷底 :你已经接近最低点,应该小步慢走 ,避免一脚跨过最低点------需要小学习率
如果全程使用同一个步长,要么走得慢,要么永远到不了最优点。学习率调度正是为了解决这个问题------随着训练进行,逐步减小学习率。
2.2 PyTorch中的调度器
PyTorch在 torch.optim.lr_scheduler 中提供了多种学习率调度器:
| 调度器 | 调整策略 |
|---|---|
StepLR |
每隔固定步长,学习率乘以gamma |
MultiStepLR |
在指定里程碑处调整学习率 |
ExponentialLR |
每个epoch学习率乘以gamma |
CosineAnnealingLR |
余弦退火,周期性调整 |
ReduceLROnPlateau |
根据指标变化自动调整 |
OneCycleLR |
一个周期内先升后降 |
代码中展示的是 StepLR 和 ReduceLROnPlateau 两种。
三、StepLR------固定步长衰减
3.1 基本用法
python
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():
python
for epoch in range(epochs):
train(...) # 训练一个epoch
test(...) # 测试
scheduler.step() # 调整学习率
注意 :StepLR 的 step() 不需要参数,因为它只依赖于epoch计数。
3.3 StepLR的优缺点
优点:
-
简单直观,易于理解
-
训练后期学习率变小,有助于精细收敛
缺点:
-
调整时机是预先设定的,无法根据实际训练情况自适应
-
如果step_size设置不当,可能过早或过晚衰减
四、ReduceLROnPlateau------自适应学习率调整
4.1 基本用法
python
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() 需要传入监控指标:
python
for epoch in range(epochs):
train_loss = train(...)
scheduler.step(train_loss) # 传入loss,让调度器判断
或者像代码中那样,在训练循环的每个batch后调用:
python
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 两种调度器对比
| 对比项 | StepLR | ReduceLROnPlateau |
|---|---|---|
| 调整依据 | 固定的epoch数 | 监控指标的变化 |
| 是否自适应 | 否 | 是 |
| step()参数 | 无 | 需要传入指标 |
| 适用场景 | 训练周期已知 | 训练周期不确定 |
| 调参难度 | 低 | 中 |
五、权重衰减------防止过拟合
5.1 什么是权重衰减
权重衰减(Weight Decay) ,也称为L2正则化,是一种防止模型过拟合的技术。
核心思想:在损失函数中添加一个与模型权重平方成正比的惩罚项:
其中 就是权重衰减系数。
为什么能防止过拟合?
-
过拟合的模型往往权重值很大(对训练数据"死记硬背")
-
加上权重平方惩罚后,模型会倾向于使用更小的权重值
-
更小的权重意味着模型更"简单",对噪声不敏感,泛化能力更强
5.2 在优化器中设置权重衰减
python
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.1,
weight_decay=1e-4 # 权重衰减系数
)
注意 :代码中 weight_decay 参数并没有显式设置,默认为0(即不使用权重衰减)。如果需要启用,可以添加这个参数。
5.3 权重衰减 vs 学习率衰减
很多人容易混淆这两个概念:
| 概念 | 作用对象 | 目的 |
|---|---|---|
| 学习率衰减 | 优化器的学习率 | 让训练后期更精细,加速收敛 |
| 权重衰减 | 模型的权重参数 | 防止过拟合,提升泛化能力 |
两者是不同维度的技术,可以同时使用。
六、完整训练代码解析
6.1 数据准备
数据准备部分与之前一致,包括数据增强、自定义数据集、DataLoader:
python
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 训练函数
python
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 测试函数
python
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_s 和 loss_s 两个列表记录每个epoch的准确率和损失,方便后续绘制学习曲线。
6.4 优化器与调度器配置
python
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 训练循环
python
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时,学习率变化如下:
python
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_s 和 loss_s 绘制学习曲线:
python
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,分析训练过程 |