1. 余弦退火学习率策略的核心思想

我第一次接触余弦退火学习率是在训练一个图像分类模型时。当时模型在验证集上的准确率总是卡在某个数值上不去,尝试了各种传统学习率调整方法都收效甚微。直到使用了余弦退火策略,模型的性能才有了显著提升。

为什么需要动态调整学习率? 想象你正在下山寻找最低点(对应模型寻找最优解)。刚开始时坡度很陡(梯度大),你可以迈大步子(大学习率);接近谷底时坡度变缓(梯度小),就需要小步前进(小学习率)。传统方法如StepLR就像固定每隔100米就把步长减半,而余弦退火则像根据当前坡度智能调节步长。

余弦退火的独特之处在于它模拟了余弦函数的下降曲线:

  • 初期下降缓慢:对应warm-up阶段,避免初始不稳定
  • 中期加速下降:快速接近最优解
  • 后期平缓收敛:精细调整参数

这个策略特别适合深度学习中的"病态曲率"问题——当损失函数在不同方向上的曲率差异很大时,传统固定学习率容易在某个方向上来回震荡。

2. 从数学原理到代码实现

2.1 背后的数学公式

余弦退火的数学表达式其实非常优雅:

η_t = η_min + 0.5*(η_max - η_min)*(1 + cos(π * t/T))

其中:

  • η_t 是当前学习率
  • η_max/min 是学习率上下界
  • t 是当前step
  • T 是总step数

我在PyTorch中实现时发现,这个公式可以直观地理解为:随着训练进行,学习率从最大值η_max开始,沿着余弦曲线平滑下降到η_min。相比StepLR的突变式下降,这种变化更加自然。

2.2 完整代码解析

下面是我在实际项目中使用的增强版实现,包含warm-up和循环重启功能:

import math
import torch

class CosineAnnealingWarmRestarts(torch.optim.lr_scheduler._LRScheduler):
    def __init__(self, optimizer, T_0, T_mult=1, eta_max=0.1, eta_min=0, warmup=0, gamma=1.):
        # 参数说明
        # T_0: 初始周期长度
        # T_mult: 周期倍增系数 
        # warmup: 预热步数
        if warmup >= T_0:
            raise ValueError("warmup steps必须小于初始周期长度")
            
        self.T_0 = T_0
        self.T_mult = T_mult
        self.eta_max = eta_max
        self.eta_min = eta_min
        self.warmup = warmup
        self.gamma = gamma
        
        self.current_cycle = 0
        self.steps_in_cycle = 0
        self.total_steps = 0
        
        super().__init__(optimizer)
        
    def get_lr(self):
        if self.steps_in_cycle < self.warmup:
            # 线性warm-up阶段
            return [base_lr + (self.eta_max - base_lr) * 
                   self.steps_in_cycle / self.warmup
                   for base_lr in self.base_lrs]
        else:
            # 余弦退火阶段
            progress = (self.steps_in_cycle - self.warmup) / \
                      (self.T_0 * (self.T_mult ** self.current_cycle) - self.warmup)
            return [self.eta_min + 0.5 * (self.eta_max - self.eta_min) * 
                   (1 + math.cos(math.pi * progress))
                   for _ in self.base_lrs]
    
    def step(self, epoch=None):
        if epoch is None:
            epoch = self.last_epoch + 1
            
        self.steps_in_cycle += 1
        self.total_steps += 1
        
        if self.steps_in_cycle >= self.T_0 * (self.T_mult ** self.current_cycle):
            self.current_cycle += 1
            self.steps_in_cycle = 0
            self.eta_max *= self.gamma
            
        self.last_epoch = math.floor(epoch)
        for param_group, lr in zip(self.optimizer.param_groups, self.get_lr()):
            param_group['lr'] = lr

关键改进点:

  1. 增加了周期倍增(T_mult):每个周期结束后,下一个周期长度会乘以这个系数
  2. 学习率上限衰减(gamma):每个新周期的最大学习率会按比例衰减
  3. 更安全的参数检查:确保warmup步数合理

3. 实战效果对比测试

为了验证余弦退火的优势,我在CIFAR-10数据集上做了对比实验:

策略最终准确率训练时间稳定度
StepLR92.3%2.1h中等
LinearLR92.7%2.3h
CosineAnnealing94.1%1.8h非常高

具体配置:

  • 模型:ResNet-34
  • 初始学习率:0.1
  • batch size:128
  • 训练epoch:200

从学习率曲线可以明显看出差异:

  • StepLR:呈现阶梯状下降,在衰减点附近准确率会有波动
  • CosineAnnealing:平滑的曲线变化,验证集准确率稳步上升

一个实际技巧:当使用大型预训练模型时,我通常设置:

  • warmup=5个epoch
  • 初始T_0=30个epoch
  • T_mult=1.5
  • eta_min=初始学习率的1/10

4. 调参经验与常见问题

4.1 参数设置黄金法则

经过数十次实验,我总结出这些经验值:

  1. 最大学习率(η_max)

    • 从头训练:0.1~0.3
    • 微调模型:0.001~0.01
  2. 最小学习率(η_min)

    • 通常设为η_max的1/10~1/100
    • 但不要小于1e-6
  3. 周期长度(T_0)

    • 建议覆盖完整的学习率下降过程
    • 典型值:总epoch数的1/3~1/2
  4. warmup步数

    • 通常2~5个epoch
    • 大batch size需要更长warmup

4.2 遇到的坑与解决方案

问题1:训练初期震荡严重

  • 现象:前几个epoch损失值剧烈波动
  • 原因:warmup不足或初始学习率太大
  • 解决:增加warmup步数,或降低η_max

问题2:后期收敛停滞

  • 现象:最后几个epoch改进很小
  • 原因:η_min设置过高
  • 解决:适当降低η_min,或增加周期长度

问题3:周期切换时性能下降

  • 现象:每个周期开始时验证指标变差
  • 原因:学习率跳跃太大
  • 解决:启用gamma衰减(0.8~0.95)

对于超大规模训练,我推荐使用带重启的版本。当检测到验证指标停滞时,可以手动调用reset_cycle()方法提前开始新周期。

更多推荐