PyTorch无内置LinearWarmupLR,需继承_LRScheduler手动实现:前warmup_steps步线性升lr,之后委托after_scheduler;须用self.base_lrs计算、避免读写param_groups['lr'],并确保step()在optimizer.step()后调用。

PyTorch里没有现成的LinearWarmupLR,得自己写
PyTorch官方调度器(如StepLR、CosineAnnealingLR)不直接支持学习率预热(warmup),必须手动组合或继承torch.optim.lr_scheduler._LRScheduler。别指望调一个函数就完事——预热本质是分段逻辑:前N步线性上升,之后切到主调度策略。
常见错误是用lambda配LambdaLR硬凑,但一旦后续要接ReduceLROnPlateau或重启机制,这种写法立刻崩盘,因为LambdaLR不维护内部step计数,也不兼容load_state_dict。
- 预热阶段必须严格按
optimizer.param_groups逐组更新lr,不能只改optimizer.defaults['lr'] - 预热步数
warmup_steps是全局训练步数(不是epoch),需和train_loader的__len__()对齐 - 务必在
optimizer.step()后、lr_scheduler.step()前调用调度器——否则第一次step()会跳过预热起点
继承_LRScheduler实现可复用的LinearWarmupScheduler
这是最稳妥的方式:控制权全在自己手里,能和任意底层调度器(比如CosineAnnealingLR)嵌套,也支持断点续训。
核心逻辑就两段:self.last_epoch 走线性上升;否则把剩余逻辑委托给传入的<code>after_scheduler。
立即学习“Python免费学习笔记(深入)”;
class LinearWarmupScheduler(torch.optim.lr_scheduler._LRScheduler):
def __init__(self, optimizer, warmup_steps, after_scheduler=None):
self.warmup_steps = warmup_steps
self.after_scheduler = after_scheduler
self.finished = False
super().__init__(optimizer)
<pre class="brush:php;toolbar:false;">def get_lr(self):
if self.last_epoch < self.warmup_steps:
return [
base_lr * float(self.last_epoch + 1) / self.warmup_steps
for base_lr in self.base_lrs
]
if not self.finished:
self.finished = True
if self.after_scheduler is not None:
self.after_scheduler.base_lrs = self.base_lrs
return self.after_scheduler.get_lr() if self.after_scheduler else self.base_lrs-
self.base_lrs是初始化时各参数组的基准学习率,别用optimizer.param_groups[i]['lr']动态读——它已被上一轮调度污染 - 如果
after_scheduler是CosineAnnealingLR,记得先用cosine = CosineAnnealingLR(optimizer, T_max=total_steps - warmup_steps)算好总周期 - 调用
load_state_dict()时,LinearWarmupScheduler会自动同步after_scheduler的状态,但前提是after_scheduler本身支持
get_lr()里不能依赖self.optimizer当前lr值
很多新手在get_lr()里写for group in self.optimizer.param_groups: group['lr'] * ...,这会导致梯度更新和学习率更新耦合错乱。PyTorch调度器的设计契约是:所有计算基于self.base_lrs和self.last_epoch,绝不读写param_groups['lr']。
错误示范:group['lr'] * 0.9 → 正确做法:base_lr * decay_factor。
-
self.last_epoch从0开始计数,每调一次.step()加1,和训练循环里的i一致 - 如果用
torch.cuda.amp.GradScaler,调度器无需额外适配——它只管lr,不管梯度缩放 - 多卡DDP下,只要每个进程独立跑自己的scheduler,
last_epoch天然同步,不用加torch.distributed逻辑
验证预热是否生效:看param_groups[0]['lr']打印值
别信日志里“warmup started”,直接在训练循环里加一行print(optimizer.param_groups[0]['lr']),前5步应该看到从0.0→1e-5→2e-5…严格线性增长(假设base_lr=1e-4、warmup_steps=10)。
如果第1步就是1e-4,说明step()调早了(在optimizer.step()前);如果第11步还是1e-4没降,检查after_scheduler.T_max是否设成了total_steps而非total_steps - warmup_steps。
- 预热结束那步(
last_epoch == warmup_steps)的lr值应等于base_lr,之后由after_scheduler接管 - 用
torch.utils.tensorboard.SummaryWriter记录lr时,记得用scheduler.get_last_lr()[0],而不是读param_groups——后者可能滞后一帧
预热调度器真正难的不是写代码,而是让warmup_steps、T_max、total_epochs三者在分布式训练+梯度累积+不同batch size下始终对齐。这些数值一旦错位,预热就变成幻觉。


















