学习率衰减策略
1. 学习率衰减的基本概念
1.1 什么是学习率衰减?
学习率衰减(Learning Rate Decay)是一种在模型训练过程中逐渐减小学习率的策略。通过在训练初期使用较大的学习率加速收敛,在训练后期使用较小的学习率精细调整参数,学习率衰减能够帮助模型更好地找到全局最优解。
1.2 为什么需要学习率衰减?
固定学习率在训练过程中可能面临以下问题:
- 训练初期:如果学习率过小,收敛速度会很慢;如果学习率过大,可能会导致训练不稳定,甚至发散
- 训练中期:固定学习率可能会导致模型在局部最小值附近振荡
- 训练后期:固定学习率可能无法帮助模型精细调整参数,找到更优的解
1.3 学习率衰减的优势
学习率衰减通过以下方式解决了固定学习率的问题:
- 加速初期收敛:训练初期使用较大的学习率,快速接近最优解区域
- 提高稳定性:随着训练进行,逐渐减小学习率,减少振荡
- 精细调整参数:训练后期使用较小的学习率,精细调整参数,找到更优的解
- 避免过拟合:较小的学习率有助于模型更好地泛化
2. 常见的学习率衰减策略
2.1 指数衰减
指数衰减是一种常见的学习率衰减策略,学习率按指数规律逐渐减小:
$$ \eta_t = \eta_0 \cdot e^{-kt} $$
其中:
- $\eta_t$ 是 $t$ 时刻的学习率
- $\eta_0$ 是初始学习率
- $k$ 是衰减率
- $t$ 是迭代次数或 epoch 数
实现代码:
import numpy as np
class ExponentialDecay:
"""
指数学习率衰减
"""
def __init__(self, initial_lr, decay_rate):
"""
初始化指数衰减
参数:
initial_lr: 初始学习率
decay_rate: 衰减率
"""
self.initial_lr = initial_lr
self.decay_rate = decay_rate
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
return self.initial_lr * np.exp(-self.decay_rate * step)2.2 步长衰减
步长衰减(Step Decay)是一种在固定间隔(step)后将学习率乘以一个衰减因子的策略:
$$ \eta_t = \eta_0 \cdot \gamma^{\lfloor t / s \rfloor} $$
其中:
- $\eta_t$ 是 $t$ 时刻的学习率
- $\eta_0$ 是初始学习率
- $\gamma$ 是衰减因子,通常取值在 0.1 左右
- $s$ 是衰减间隔
- $t$ 是迭代次数或 epoch 数
实现代码:
class StepDecay:
"""
步长学习率衰减
"""
def __init__(self, initial_lr, decay_factor, decay_steps):
"""
初始化步长衰减
参数:
initial_lr: 初始学习率
decay_factor: 衰减因子
decay_steps: 衰减间隔
"""
self.initial_lr = initial_lr
self.decay_factor = decay_factor
self.decay_steps = decay_steps
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
return self.initial_lr * (self.decay_factor ** (step // self.decay_steps))2.3 多项式衰减
多项式衰减(Polynomial Decay)是一种学习率按多项式规律逐渐减小的策略,最终学习率会衰减到一个指定的最小值:
$$ \eta_t = (\eta_0 - \eta_{min}) \cdot (1 - t / T)^{p} + \eta_{min} $$
其中:
- $\eta_t$ 是 $t$ 时刻的学习率
- $\eta_0$ 是初始学习率
- $\eta_{min}$ 是最小学习率
- $t$ 是当前迭代次数
- $T$ 是总迭代次数
- $p$ 是多项式的次数
实现代码:
class PolynomialDecay:
"""
多项式学习率衰减
"""
def __init__(self, initial_lr, min_lr, max_steps, power):
"""
初始化多项式衰减
参数:
initial_lr: 初始学习率
min_lr: 最小学习率
max_steps: 总迭代步数
power: 多项式次数
"""
self.initial_lr = initial_lr
self.min_lr = min_lr
self.max_steps = max_steps
self.power = power
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
if step > self.max_steps:
return self.min_lr
return (self.initial_lr - self.min_lr) * ((1 - step / self.max_steps) ** self.power) + self.min_lr2.4 余弦退火
余弦退火(Cosine Annealing)是一种学习率按余弦函数规律逐渐减小的策略,能够在训练后期实现更精细的参数调整:
$$ \eta_t = \eta_{min} + (\eta_0 - \eta_{min}) \cdot \frac{1 + \cos(\pi \cdot t / T)}{2} $$
其中:
- $\eta_t$ 是 $t$ 时刻的学习率
- $\eta_0$ 是初始学习率
- $\eta_{min}$ 是最小学习率
- $t$ 是当前迭代次数
- $T$ 是总迭代次数
实现代码:
import math
class CosineAnnealing:
"""
余弦退火学习率衰减
"""
def __init__(self, initial_lr, min_lr, max_steps):
"""
初始化余弦退火
参数:
initial_lr: 初始学习率
min_lr: 最小学习率
max_steps: 总迭代步数
"""
self.initial_lr = initial_lr
self.min_lr = min_lr
self.max_steps = max_steps
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
if step > self.max_steps:
return self.min_lr
return self.min_lr + (self.initial_lr - self.min_lr) * (1 + math.cos(math.pi * step / self.max_steps)) / 22.5 线性预热
线性预热(Linear Warmup)是一种在训练初期逐渐增加学习率到初始值的策略,有助于稳定训练开始阶段:
$$ \eta_t = \eta_0 \cdot \frac{t}{W} \quad (t \leq W) $$
$$ \eta_t = \eta_0 \quad (t > W) $$
其中:
- $\eta_t$ 是 $t$ 时刻的学习率
- $\eta_0$ 是初始学习率
- $W$ 是预热步数
- $t$ 是当前迭代次数
实现代码:
class LinearWarmup:
"""
线性预热学习率
"""
def __init__(self, initial_lr, warmup_steps):
"""
初始化线性预热
参数:
initial_lr: 初始学习率
warmup_steps: 预热步数
"""
self.initial_lr = initial_lr
self.warmup_steps = warmup_steps
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
if step < self.warmup_steps:
return self.initial_lr * (step / self.warmup_steps)
return self.initial_lr2.6 预热 + 余弦退火
线性预热与余弦退火的结合是一种常用的学习率调度策略,能够兼顾训练初期的稳定性和后期的精细调整:
实现代码:
class WarmupCosineAnnealing:
"""
预热 + 余弦退火学习率衰减
"""
def __init__(self, initial_lr, min_lr, max_steps, warmup_steps):
"""
初始化预热 + 余弦退火
参数:
initial_lr: 初始学习率
min_lr: 最小学习率
max_steps: 总迭代步数
warmup_steps: 预热步数
"""
self.initial_lr = initial_lr
self.min_lr = min_lr
self.max_steps = max_steps
self.warmup_steps = warmup_steps
self.cosine_annealing = CosineAnnealing(initial_lr, min_lr, max_steps - warmup_steps)
def __call__(self, step):
"""
计算当前步骤的学习率
参数:
step: 当前迭代步数
返回:
当前学习率
"""
if step < self.warmup_steps:
return self.initial_lr * (step / self.warmup_steps)
return self.cosine_annealing(step - self.warmup_steps)3. 在深度学习框架中的实现
3.1 PyTorch 中的实现
PyTorch 提供了多种学习率调度器(Learning Rate Scheduler):
StepLR(步长衰减):
import torch.optim as optim
# 创建优化器
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
# 创建步长衰减调度器
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
# 在训练循环中使用
for epoch in range(100):
# 训练代码
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 更新学习率
scheduler.step()ExponentialLR(指数衰减):
# 创建指数衰减调度器
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.99)CosineAnnealingLR(余弦退火):
# 创建余弦退火调度器
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100, eta_min=0.0001)OneCycleLR(一次性学习率调度):
# 创建一次性学习率调度器
scheduler = optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.1, steps_per_epoch=len(trainloader), epochs=100)3.2 TensorFlow 中的实现
TensorFlow 也提供了多种学习率调度器:
指数衰减:
import tensorflow as tf
# 创建指数衰减学习率
def exponential_decay(epoch, lr):
decay_rate = 0.96
decay_steps = 1000
return lr * (decay_rate ** (epoch / decay_steps))
# 创建学习率调度器
lr_scheduler = tf.keras.callbacks.LearningRateScheduler(exponential_decay)
# 在模型训练中使用
model.fit(train_dataset, epochs=100, callbacks=[lr_scheduler])余弦退火:
# 创建余弦退火学习率调度器
lr_scheduler = tf.keras.callbacks.CosineDecayRestarts(
initial_learning_rate=0.1,
first_decay_steps=1000,
t_mul=2.0,
m_mul=0.9,
alpha=0.0001
)线性预热 + 余弦退火:
# 创建学习率调度器
lr_scheduler = tf.keras.optimizers.schedules.CosineDecay(
initial_learning_rate=0.1,
decay_steps=10000,
alpha=0.0001
)
# 创建优化器
optimizer = tf.keras.optimizers.Adam(learning_rate=lr_scheduler)4. 学习率衰减与优化器的结合
4.1 不同优化器的学习率衰减策略
不同的优化器对学习率衰减的反应不同,需要选择合适的衰减策略:
SGD with Momentum:
- 适合使用较大的初始学习率和较强烈的衰减(如步长衰减,gamma=0.1)
- 余弦退火也能取得良好效果
RMSProp:
- 初始学习率通常较小(如 0.001)
- 适合使用缓慢的衰减策略(如指数衰减,gamma=0.99)
Adam:
- 初始学习率通常为 0.001
- 学习率衰减的效果可能不如 SGD 明显,但仍能带来性能提升
- 适合使用余弦退火或多项式衰减
AdamW:
- 与 Adam 类似,但由于加入了权重衰减,学习率衰减的效果可能更加明显
- 适合使用余弦退火
4.2 学习率衰减的调整策略
初始学习率选择:
- 对于 SGD,通常在 0.01-0.1 之间
- 对于 Adam 等自适应优化器,通常在 0.001 左右
- 可以通过学习率范围测试(LR Range Test)找到最佳初始学习率
衰减策略选择:
- 对于短时间训练(如 100 epochs),步长衰减或余弦退火效果较好
- 对于长时间训练(如 1000 epochs),余弦退火或多项式衰减效果较好
- 对于不稳定的模型,建议使用线性预热
衰减参数调整:
- 步长衰减:step_size 通常设置为总 epochs 的 1/3 或 1/4,gamma 通常为 0.1
- 余弦退火:T_max 通常设置为总 epochs
- 线性预热:warmup_steps 通常设置为总 steps 的 5%-10%
5. 学习率范围测试
5.1 什么是学习率范围测试?
学习率范围测试(LR Range Test)是一种寻找最佳初始学习率的方法,由 Leslie Smith 在 2018 年提出。通过在训练过程中从一个很小的学习率开始,逐渐增加到一个很大的值,观察损失函数的变化,找到损失下降最快的学习率作为初始学习率。
5.2 实现方法
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.optim as optim
import torch.nn as nn
# 学习率范围测试函数
def lr_range_test(model, trainloader, criterion, start_lr=1e-7, end_lr=10, num_iter=100):
"""
学习率范围测试
参数:
model: 模型
trainloader: 训练数据加载器
criterion: 损失函数
start_lr: 起始学习率
end_lr: 结束学习率
num_iter: 测试迭代次数
返回:
lrs: 学习率列表
losses: 损失列表
"""
# 保存原始权重
original_state_dict = model.state_dict()
# 创建优化器
optimizer = optim.SGD(model.parameters(), lr=start_lr, momentum=0.9)
# 学习率调度器
scheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=(end_lr / start_lr) ** (1 / num_iter))
# 记录学习率和损失
lrs = []
losses = []
# 测试循环
model.train()
for i in range(num_iter):
# 获取数据
inputs, labels = next(iter(trainloader))
# 前向传播
outputs = model(inputs)
loss = criterion(outputs, labels)
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 记录
lrs.append(optimizer.param_groups[0]['lr'])
losses.append(loss.item())
# 更新学习率
scheduler.step()
# 恢复原始权重
model.load_state_dict(original_state_dict)
return lrs, losses
# 绘制学习率范围测试结果
def plot_lr_range_test(lrs, losses):
"""
绘制学习率范围测试结果
参数:
lrs: 学习率列表
losses: 损失列表
"""
plt.figure(figsize=(10, 6))
plt.plot(lrs, losses)
plt.xscale('log')
plt.xlabel('学习率 (log scale)')
plt.ylabel('损失')
plt.title('学习率范围测试')
plt.grid(True)
plt.show()
# 使用示例
# lrs, losses = lr_range_test(model, trainloader, criterion)
# plot_lr_range_test(lrs, losses)5.3 结果分析
通过学习率范围测试的结果,我们可以:
- 找到最佳初始学习率:选择损失下降最快的学习率作为初始学习率
- 确定学习率上限:当学习率过大时,损失会突然上升,这是学习率的上限
- 观察模型对学习率的敏感性:损失曲线的陡峭程度反映了模型对学习率的敏感性
6. 实战案例:学习率衰减在图像分类中的应用
6.1 案例背景
在 CIFAR-10 图像分类任务中,我们将比较不同学习率衰减策略的性能差异。
6.2 实现代码
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import time
# 数据准备
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)
# 简单的CNN模型
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
self.bn1 = nn.BatchNorm2d(64)
self.conv2 = nn.Conv2d(64, 64, 3, padding=1)
self.bn2 = nn.BatchNorm2d(64)
self.pool = nn.MaxPool2d(2, 2)
self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
self.bn3 = nn.BatchNorm2d(128)
self.conv4 = nn.Conv2d(128, 128, 3, padding=1)
self.bn4 = nn.BatchNorm2d(128)
self.fc1 = nn.Linear(128 * 8 * 8, 512)
self.fc2 = nn.Linear(512, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = self.relu(self.bn1(self.conv1(x)))
x = self.relu(self.bn2(self.conv2(x)))
x = self.pool(x)
x = self.relu(self.bn3(self.conv3(x)))
x = self.relu(self.bn4(self.conv4(x)))
x = self.pool(x)
x = x.view(-1, 128 * 8 * 8)
x = self.relu(self.fc1(x))
x = self.fc2(x)
return x
# 训练函数
def train_with_scheduler(scheduler_name, optimizer, scheduler):
model = Net()
criterion = nn.CrossEntropyLoss()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
best_acc = 0.0
start_time = time.time()
for epoch in range(100):
running_loss = 0.0
model.train()
for i, data in enumerate(trainloader, 0):
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
# 更新学习率
if scheduler is not None:
scheduler.step()
# 测试
model.eval()
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = 100 * correct / total
if acc > best_acc:
best_acc = acc
current_lr = optimizer.param_groups[0]['lr']
print(f'{scheduler_name} - Epoch {epoch+1}, LR: {current_lr:.6f}, Loss: {running_loss/len(trainloader):.3f}, Acc: {acc:.2f}%, Best Acc: {best_acc:.2f}%')
end_time = time.time()
training_time = end_time - start_time
print(f'{scheduler_name} - 训练时间: {training_time:.2f}秒')
return best_acc, training_time
# 比较不同学习率衰减策略
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 固定学习率
print("=== 固定学习率 ===")
model_fixed = Net()
optimizer_fixed = optim.SGD(model_fixed.parameters(), lr=0.01, momentum=0.9)
best_acc_fixed, time_fixed = train_with_scheduler("固定学习率", optimizer_fixed, None)
# 步长衰减
print("\n=== 步长衰减 ===")
model_step = Net()
optimizer_step = optim.SGD(model_step.parameters(), lr=0.1, momentum=0.9)
scheduler_step = optim.lr_scheduler.StepLR(optimizer_step, step_size=30, gamma=0.1)
best_acc_step, time_step = train_with_scheduler("步长衰减", optimizer_step, scheduler_step)
# 指数衰减
print("\n=== 指数衰减 ===")
model_exp = Net()
optimizer_exp = optim.SGD(model_exp.parameters(), lr=0.1, momentum=0.9)
scheduler_exp = optim.lr_scheduler.ExponentialLR(optimizer_exp, gamma=0.99)
best_acc_exp, time_exp = train_with_scheduler("指数衰减", optimizer_exp, scheduler_exp)
# 余弦退火
print("\n=== 余弦退火 ===")
model_cosine = Net()
optimizer_cosine = optim.SGD(model_cosine.parameters(), lr=0.1, momentum=0.9)
scheduler_cosine = optim.lr_scheduler.CosineAnnealingLR(optimizer_cosine, T_max=100, eta_min=0.0001)
best_acc_cosine, time_cosine = train_with_scheduler("余弦退火", optimizer_cosine, scheduler_cosine)
# OneCycleLR
print("\n=== OneCycleLR ===")
model_onecycle = Net()
optimizer_onecycle = optim.SGD(model_onecycle.parameters(), lr=0.1, momentum=0.9)
scheduler_onecycle = optim.lr_scheduler.OneCycleLR(optimizer_onecycle, max_lr=0.1, steps_per_epoch=len(trainloader), epochs=100)
best_acc_onecycle, time_onecycle = train_with_scheduler("OneCycleLR", optimizer_onecycle, scheduler_onecycle)
# 结果比较
print("\n=== 结果比较 ===")
print(f"固定学习率 - 最佳准确率: {best_acc_fixed:.2f}%, 训练时间: {time_fixed:.2f}秒")
print(f"步长衰减 - 最佳准确率: {best_acc_step:.2f}%, 训练时间: {time_step:.2f}秒")
print(f"指数衰减 - 最佳准确率: {best_acc_exp:.2f}%, 训练时间: {time_exp:.2f}秒")
print(f"余弦退火 - 最佳准确率: {best_acc_cosine:.2f}%, 训练时间: {time_cosine:.2f}秒")
print(f"OneCycleLR - 最佳准确率: {best_acc_onecycle:.2f}%, 训练时间: {time_onecycle:.2f}秒")
print(f"\n步长衰减 vs 固定学习率: {best_acc_step - best_acc_fixed:.2f}%")
print(f"余弦退火 vs 固定学习率: {best_acc_cosine - best_acc_fixed:.2f}%")
print(f"OneCycleLR vs 固定学习率: {best_acc_onecycle - best_acc_fixed:.2f}%")6.3 结果分析
通过上面的实验,我们可以预期以下结果:
- 准确率:OneCycleLR > 余弦退火 > 步长衰减 > 指数衰减 > 固定学习率
- 收敛速度:使用学习率衰减的策略通常比固定学习率收敛更快
- 训练稳定性:余弦退火和 OneCycleLR 的训练过程更加稳定
- 最佳实践:对于大多数深度学习任务,余弦退火或 OneCycleLR 是不错的选择
7. 总结与最佳实践
7.1 总结
学习率衰减是一种重要的模型训练策略,通过在训练过程中逐渐减小学习率,能够:
- 加速初期收敛:训练初期使用较大的学习率,快速接近最优解区域
- 提高稳定性:随着训练进行,逐渐减小学习率,减少振荡
- 精细调整参数:训练后期使用较小的学习率,精细调整参数,找到更优的解
- 避免过拟合:较小的学习率有助于模型更好地泛化
7.2 最佳实践
选择合适的衰减策略:
- 对于短时间训练,步长衰减或余弦退火效果较好
- 对于长时间训练,余弦退火或多项式衰减效果较好
- 对于不稳定的模型,建议使用线性预热
- 对于大多数任务,OneCycleLR 是一个不错的选择
调整衰减参数:
- 初始学习率:通过学习率范围测试找到最佳初始学习率
- 衰减率:对于步长衰减,通常设置为 0.1;对于指数衰减,通常设置为 0.99 左右
- 衰减间隔:对于步长衰减,通常设置为总 epochs 的 1/3 或 1/4
- 最小学习率:通常设置为初始学习率的 1/1000 到 1/100
与优化器结合:
- SGD with Momentum:适合使用较大的初始学习率和较强烈的衰减
- RMSProp:适合使用较小的初始学习率和缓慢的衰减
- Adam/AdamW:学习率衰减的效果可能不如 SGD 明显,但仍能带来性能提升
实现技巧:
- 在 PyTorch 中,使用
torch.optim.lr_scheduler中的各种调度器 - 在 TensorFlow 中,使用
tf.keras.optimizers.schedules中的各种调度器 - 对于自定义学习率调度,可以实现一个学习率调度函数
- 在训练过程中,监控学习率的变化和模型性能,及时调整策略
- 在 PyTorch 中,使用
常见问题与解决方案:
- 学习率过大:导致训练不稳定,甚至发散 → 减小初始学习率
- 学习率过小:导致收敛速度慢 → 增大初始学习率
- 衰减过快:导致模型无法充分学习 → 增大衰减间隔或减小衰减率
- 衰减过慢:导致模型在后期振荡 → 减小衰减间隔或增大衰减率
7.3 未来发展
学习率调度策略是深度学习优化的重要组成部分,未来的发展方向可能包括:
- 自适应学习率调度:根据模型的训练状态自动调整学习率
- 元学习:通过元学习找到最佳的学习率调度策略
- 动态学习率:根据不同层或不同参数组使用不同的学习率调度策略
- 与其他优化技术的结合:如知识蒸馏、模型剪枝等
- 理论基础:加强学习率调度策略的理论基础,更好地理解其工作原理
8. 练习题
- 实现一个自定义的学习率调度器,结合线性预热和余弦退火
- 使用学习率范围测试找到一个适合你的模型的最佳初始学习率
- 在 MNIST 手写数字识别任务中,比较不同学习率衰减策略的性能
- 研究不同批量大小对学习率衰减策略的影响
- 尝试使用 OneCycleLR 训练一个深度学习模型,并观察其效果
- 实现一个基于模型性能的自适应学习率调度器
通过这些练习,你将更深入地理解学习率衰减策略的工作原理和应用方法,为后续的深度学习实践打下基础。