学习率调度的炼丹心法:从恒定步长到余弦退火,深度学习收敛路径的精准把控
2026/6/16 8:17:06 网站建设 项目流程

学习率调度的炼丹心法:从恒定步长到余弦退火,深度学习收敛路径的精准把控

一、学习率:炼丹炉中最关键的火候

深度学习训练就像炼丹——模型是丹炉,数据是药材,而学习率就是火候。火候太大,丹药炸裂(梯度爆炸、loss 震荡);火候太小,丹药不熟(收敛太慢、陷入局部最优)。古人炼丹讲究"文火慢炖、武火催熟",深度学习的学习率调度也是同样的道理——训练初期用较大的学习率快速探索参数空间,训练后期用较小的学习率精细调整。

我养了一只英短猫叫 Tensor,它的情绪像学习率一样难以捉摸——有时候温顺得像 ReLU 激活,有时候暴躁得像梯度爆炸。跟 Tensor 相处的诀窍是:刚见面时保持距离(大学习率探索),熟悉后慢慢靠近(小学习率微调)。训练模型也一样,学习率调度策略决定了模型能否找到最优解。

本文将系统梳理从恒定学习率到余弦退火、Warm Restart 等高级策略的原理与实现,帮你掌握炼丹的火候。

二、学习率调度技术架构:从基础策略到高级调度

学习率调度的核心思路是:训练初期大步探索 → 中期逐步收敛 → 后期精细微调,必要时周期性重启跳出局部最优。

flowchart TD A[学习率调度策略] --> B[基础策略] A --> C[衰减策略] A --> D[高级策略] B --> B1[Constant: 恒定学习率] B --> B2[Step: 阶梯衰减] B --> B3[MultiStep: 多节点衰减] C --> C1[Exponential: 指数衰减] C --> C2[Polynomial: 多项式衰减] C --> C3[Cosine: 余弦退火] C1 --> C1a[lr = lr0 × gamma^epoch] C2 --> C2a[lr = lr0 × (1 - t/T)^power] C3 --> C3a[lr = lr_min + 0.5×(lr_max-lr_min)×(1+cos(πt/T))] D --> D1[Cosine Annealing + Warm Restart] D --> D2[One-Cycle Policy] D --> D3[Warmup + Cosine Decay] D1 --> D1a[周期性重启跳出局部最优] D1 --> D1b[T_mult 控制周期倍增] D2 --> D2a[先升后降: 探索→收敛] D2 --> D2b[Super-convergence 加速] D3 --> D3a[前 N 步线性升温] D3 --> D3b[Transformer 训练标配] style B fill:#e1f5fe style C fill:#fff3e0 style D fill:#e8f5e9

2.1 学习率调度器实现

# lr_schedulers.py — 学习率调度器集合 # 设计意图:实现多种学习率调度策略,支持 PyTorch 原生调度器和自定义调度器, # 提供统一的接口和可视化工具 import math from typing import List, Optional from torch.optim.lr_scheduler import _LRScheduler import torch class CosineAnnealingWarmRestarts(_LRScheduler): """ 余弦退火 + 温重启(SGDR) 核心思想:学习率按余弦函数周期性衰减, 每个周期结束时重启到最大值,帮助跳出局部最优 这就像炼丹时的"文武交替"—— 余弦退火是文火慢炖,温重启是武火催熟 Args: optimizer: PyTorch 优化器 T_0: 第一个周期的步数 T_mult: 周期倍增因子(每个新周期 = 上一周期 × T_mult) eta_min: 最小学习率 """ def __init__( self, optimizer: torch.optim.Optimizer, T_0: int, T_mult: int = 2, eta_min: float = 1e-6, last_epoch: int = -1, ): self.T_0 = T_0 self.T_mult = T_mult self.eta_min = eta_min self.current_cycle_length = T_0 super().__init__(optimizer, last_epoch) def get_lr(self) -> List[float]: # 计算当前在哪个周期内 cycle_start = 0 cycle_length = self.T_0 while cycle_start + cycle_length <= self.last_epoch: cycle_start += cycle_length cycle_length *= self.T_mult # 当前周期内的位置 t = self.last_epoch - cycle_start T = cycle_length # 余弦退火公式 lrs = [] for base_lr in self.base_lrs: lr = self.eta_min + 0.5 * (base_lr - self.eta_min) * ( 1 + math.cos(math.pi * t / T) ) lrs.append(lr) return lrs class OneCycleLR(_LRScheduler): """ One-Cycle 策略(Super-convergence) 核心思想:学习率先从低到高(探索阶段),再从高到低(收敛阶段) 整个训练过程只用一个周期 这就像 Tensor 的情绪曲线—— 先从平静到兴奋(探索),再从兴奋到平静(收敛) Args: optimizer: PyTorch 优化器 max_lr: 最大学习率(峰值) total_steps: 总训练步数 pct_start: 升温阶段占比(默认 0.3) anneal_strategy: 退火策略 ('cos' 或 'linear') div_factor: 初始学习率 = max_lr / div_factor final_div_factor: 最终学习率 = max_lr / (div_factor × final_div_factor) """ def __init__( self, optimizer: torch.optim.Optimizer, max_lr: float, total_steps: int, pct_start: float = 0.3, anneal_strategy: str = "cos", div_factor: float = 25.0, final_div_factor: float = 1e4, last_epoch: int = -1, ): self.max_lr = max_lr self.total_steps = total_steps self.pct_start = pct_start self.anneal_strategy = anneal_strategy # 初始学习率和最终学习率 self.initial_lr = max_lr / div_factor self.final_lr = max_lr / (div_factor * final_div_factor) # 阶段分界点 self.step_up = int(total_steps * pct_start) self.step_down = total_steps super().__init__(optimizer, last_epoch) def get_lr(self) -> List[float]: step = self.last_epoch if step <= self.step_up: # 升温阶段:从 initial_lr 线性/余弦升到 max_lr pct = step / self.step_up if self.anneal_strategy == "cos": lr = self.initial_lr + (self.max_lr - self.initial_lr) * ( 1 - math.cos(math.pi * pct) ) / 2 else: lr = self.initial_lr + (self.max_lr - self.initial_lr) * pct else: # 降温阶段:从 max_lr 线性/余弦降到 final_lr pct = (step - self.step_up) / (self.step_down - self.step_up) if self.anneal_strategy == "cos": lr = self.final_lr + (self.max_lr - self.final_lr) * ( 1 + math.cos(math.pi * pct) ) / 2 else: lr = self.max_lr - (self.max_lr - self.final_lr) * pct return [lr for _ in self.base_lrs] class WarmupCosineDecay(_LRScheduler): """ Warmup + 余弦衰减 Transformer 训练的标配策略: 1. 前 warmup_steps 步线性升温(避免初期梯度不稳定) 2. 之后余弦衰减到最小学习率 这就像冬天启动汽车—— 先暖机(warmup),再正常行驶(cosine decay) Args: optimizer: PyTorch 优化器 warmup_steps: 预热步数 total_steps: 总训练步数 eta_min: 最小学习率 """ def __init__( self, optimizer: torch.optim.Optimizer, warmup_steps: int, total_steps: int, eta_min: float = 1e-6, last_epoch: int = -1, ): self.warmup_steps = warmup_steps self.total_steps = total_steps self.eta_min = eta_min super().__init__(optimizer, last_epoch) def get_lr(self) -> List[float]: step = self.last_epoch lrs = [] for base_lr in self.base_lrs: if step < self.warmup_steps: # Warmup 阶段:线性升温 lr = base_lr * step / max(1, self.warmup_steps) else: # 余弦衰减阶段 progress = (step - self.warmup_steps) / max( 1, self.total_steps - self.warmup_steps ) lr = self.eta_min + 0.5 * (base_lr - self.eta_min) * ( 1 + math.cos(math.pi * progress) ) lrs.append(lr) return lrs # ===== 调度器工厂函数 ===== def create_scheduler( optimizer: torch.optim.Optimizer, scheduler_type: str, total_steps: int, **kwargs, ) -> _LRScheduler: """ 学习率调度器工厂函数 Args: optimizer: 优化器 scheduler_type: 调度器类型 - "cosine_warmup": Warmup + Cosine Decay(Transformer 推荐) - "onecycle": One-Cycle Policy(CV 推荐) - "sgdr": Cosine Annealing + Warm Restart - "step": Step Decay - "cosine": 纯余弦衰减 total_steps: 总训练步数 """ if scheduler_type == "cosine_warmup": warmup_steps = kwargs.get("warmup_steps", total_steps // 10) return WarmupCosineDecay( optimizer, warmup_steps=warmup_steps, total_steps=total_steps, eta_min=kwargs.get("eta_min", 1e-6), ) elif scheduler_type == "onecycle": return OneCycleLR( optimizer, max_lr=kwargs.get("max_lr", 3e-4), total_steps=total_steps, pct_start=kwargs.get("pct_start", 0.3), ) elif scheduler_type == "sgdr": return CosineAnnealingWarmRestarts( optimizer, T_0=kwargs.get("T_0", total_steps // 4), T_mult=kwargs.get("T_mult", 2), eta_min=kwargs.get("eta_min", 1e-6), ) elif scheduler_type == "step": return torch.optim.lr_scheduler.StepLR( optimizer, step_size=kwargs.get("step_size", total_steps // 3), gamma=kwargs.get("gamma", 0.1), ) elif scheduler_type == "cosine": return torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=total_steps, eta_min=kwargs.get("eta_min", 1e-6), ) else: raise ValueError(f"未知调度器类型: {scheduler_type}")

2.2 训练循环集成与学习率可视化

# train_with_scheduler.py — 带学习率调度的训练循环 # 设计意图:将学习率调度器集成到训练循环中, # 支持梯度累积、学习率记录和训练曲线可视化 import torch import torch.nn as nn from torch.utils.data import DataLoader from typing import Dict, List, Optional import logging import json from lr_schedulers import create_scheduler logger = logging.getLogger(__name__) class TrainerWithScheduler: """带学习率调度的训练器""" def __init__( self, model: nn.Module, train_loader: DataLoader, val_loader: Optional[DataLoader] = None, learning_rate: float = 3e-4, scheduler_type: str = "cosine_warmup", weight_decay: float = 0.01, gradient_accumulation_steps: int = 1, max_grad_norm: float = 1.0, device: str = "cuda", ): self.model = model.to(device) self.train_loader = train_loader self.val_loader = val_loader self.device = device self.gradient_accumulation_steps = gradient_accumulation_steps self.max_grad_norm = max_grad_norm # 优化器:AdamW(带解耦权重衰减) self.optimizer = torch.optim.AdamW( model.parameters(), lr=learning_rate, weight_decay=weight_decay, betas=(0.9, 0.999), eps=1e-8, ) # 计算总训练步数 total_steps = len(train_loader) * gradient_accumulation_steps logger.info(f"总训练步数: {total_steps}") # 创建学习率调度器 self.scheduler = create_scheduler( self.optimizer, scheduler_type=scheduler_type, total_steps=total_steps, max_lr=learning_rate, warmup_steps=total_steps // 10, ) # 记录学习率变化 self.lr_history: List[Dict] = [] def train_epoch(self, epoch: int) -> Dict: """训练一个 epoch""" self.model.train() total_loss = 0.0 num_batches = 0 for batch_idx, batch in enumerate(self.train_loader): # 前向传播 inputs = {k: v.to(self.device) for k, v in batch.items()} outputs = self.model(**inputs) loss = outputs.loss / self.gradient_accumulation_steps # 反向传播 loss.backward() # 梯度累积 if (batch_idx + 1) % self.gradient_accumulation_steps == 0: # 梯度裁剪 torch.nn.utils.clip_grad_norm_( self.model.parameters(), self.max_grad_norm ) # 参数更新 self.optimizer.step() self.scheduler.step() self.optimizer.zero_grad() # 记录学习率 current_lr = self.scheduler.get_last_lr()[0] self.lr_history.append({ "step": len(self.lr_history), "lr": current_lr, "loss": loss.item() * self.gradient_accumulation_steps, }) total_loss += loss.item() num_batches += 1 avg_loss = total_loss / num_batches current_lr = self.scheduler.get_last_lr()[0] logger.info( f"Epoch {epoch}: loss={avg_loss:.4f}, lr={current_lr:.2e}" ) return {"epoch": epoch, "loss": avg_loss, "lr": current_lr} def save_lr_history(self, path: str = "lr_history.json"): """保存学习率变化历史""" with open(path, "w") as f: json.dump(self.lr_history, f, indent=2) logger.info(f"学习率历史已保存到 {path}")

四、边界分析与架构权衡

Warmup 步数的权衡:Warmup 步数太少(<1% 总步数),初期梯度不稳定可能导致模型崩溃;Warmup 步数太多(>20% 总步数),浪费训练预算。经验值:Transformer 模型 warmup 步数为总步数的 1-10%,大模型(>1B 参数)倾向更长的 warmup(5-10%),小模型可以用较短的 warmup(1-3%)。

余弦衰减 vs 线性衰减:余弦衰减在训练末期学习率下降更平缓,给模型更多时间在低学习率下微调;线性衰减末期学习率下降更陡峭,可能导致训练末期 loss 震荡。实践中余弦衰减几乎总是优于线性衰减,这也是 Transformer 训练的标配选择。

SGDR 的重启周期:T_0 设置太小(如 1 个 epoch),重启太频繁,模型来不及收敛就被打断;T_0 设置太大(如整个训练的 1/2),重启次数太少,失去跳出局部最优的意义。建议 T_0 = 总步数的 1/4 到 1/8,T_mult = 2(每个新周期翻倍)。

One-Cycle 的超参敏感性:One-Cycle 的 max_lr 是最关键的超参数——太大导致训练不稳定,太小导致收敛慢。建议先用 LR Range Test(从 1e-7 到 10 线性增长学习率,观察 loss 曲线)找到 loss 下降最快的学习率作为 max_lr。

五、总结

学习率调度是深度学习训练中最关键的超参数策略——恒定学习率是最低效的基线,Step Decay 是简单粗暴的阶梯,余弦衰减是优雅的曲线,Warm Restart 是跳出局部最优的利器,One-Cycle 是 Super-convergence 的捷径。落地建议:Transformer 训练用 Warmup + Cosine Decay,warmup 步数 1-10%;CV 模型用 One-Cycle,max_lr 通过 LR Range Test 确定;需要跳出局部最优时用 SGDR,T_0 = 总步数/4,T_mult = 2。记住,学习率调度就像炼丹的火候——没有万能的丹方,只有不断调整的火候。Tensor 的情绪我摸了三年才摸透,学习率的脾气你也得耐心摸索。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询