1. 项目概述:从“热身”到模型稳定

在深度学习和机器学习模型的训练过程中,我们常常会遇到一个看似微小却影响深远的问题:模型在训练初期表现极不稳定,损失值剧烈震荡,甚至可能导致梯度爆炸,让整个训练过程在起点就宣告失败。这就像让一个没有经过任何热身的运动员直接以百米冲刺的速度投入比赛,肌肉拉伤、状态低迷几乎是必然的结果。为了解决这个“冷启动”难题, warmup(预热) 作为一种关键的 调优方法 应运而生,并已成为现代优化器配置中不可或缺的一环。

简单来说,warmup就是在训练的最开始阶段,采用一个较小的、逐渐增大的学习率,让模型参数“温和地”进入学习状态,而不是一开始就承受全量学习率的“冲击”。这个策略的核心思想是,在训练初期,模型的参数通常是随机初始化的,它们距离最优解非常遥远。此时,如果使用一个较大的固定学习率,梯度更新可能会非常“莽撞”,导致参数在损失函数的“悬崖峭壁”上横冲直撞,难以收敛到一个好的区域。warmup通过提供一个平缓的“斜坡”,让模型有机会先探索一个相对平滑的损失曲面,逐步建立起对数据分布的基本认知,为后续的稳定、高效训练打下坚实基础。

这个方法尤其适用于大规模预训练模型(如BERT、GPT系列)、使用Adam/AdamW等自适应优化器的场景,以及学习率调度策略复杂的情况。无论是研究员、算法工程师,还是正在入门的学习者,理解并掌握warmup的原理与实现,都能让你在模型调优时多一份从容,少踩一些坑。接下来,我将结合多年的实战经验,为你彻底拆解warmup的里里外外。

2. 核心原理与设计思路拆解

要真正用好warmup,不能只停留在“训练开始时要调小学习率”的模糊认知上。我们需要深入理解其背后的数学原理和设计哲学,明白它究竟解决了优化过程中的哪些具体痛点。

2.1 为什么需要Warmup?—— 优化初期的“悬崖”与“震荡”

想象一下损失函数的等高线图。在最优解附近,地形通常比较平缓;而在远离最优解的随机初始化点,地形可能异常复杂,存在陡峭的峡谷和高耸的山峰。使用大学习率就像给参数更新赋予了巨大的动能,它很可能从一个山坡直接“飞”到另一个山坡,甚至冲出有效的搜索区域,导致损失值剧烈震荡,无法下降。

更具体地说,在训练初期,模型对数据的预测误差很大,计算出的梯度(方向)可能噪声很大且不稳定。同时,对于像Adam这类优化器,它们会维护梯度的一阶矩估计(均值,m)和二阶矩估计(未中心化的方差,v)作为自适应学习率的依据。在初始阶段,由于缺少历史梯度信息,这些矩估计值(尤其是v)接近于零。根据Adam的更新公式 lr_t = global_lr / (sqrt(v_t) + epsilon) ,一个极小的 v_t 会导致此时的有效学习率异常巨大,从而引发更新步长失控。warmup通过在前几步强制使用一个很小的学习率,给了优化器足够的时间来积累稳定、可靠的梯度矩估计,避免了这种因统计量初始化带来的数值不稳定问题。

2.2 Warmup的常见策略与数学表达

Warmup不是一个固定的方法,而是一类策略。最常见的两种实现方式是 线性warmup 和 余弦warmup (通常与余弦退火学习率调度器结合使用)。

线性warmup 是最直观、最常用的方式。假设总训练步数为 T ,warmup步数为 T_warmup ,设定的最大学习率为 lr_max 。那么在第 t 步( t < T_warmup )时,学习率 lr_t 的计算公式为: lr_t = (t / T_warmup) * lr_max 也就是说,学习率从0开始(或一个极小的值,实践中常避免除零错误,设一个如1e-8的底数),随着训练步数线性增长,直到第 T_warmup 步时达到预设的最大学习率 lr_max 。之后,再按照预定的学习率衰减策略(如Step Decay, Cosine Annealing)进行调整。

余弦warmup 则通常嵌入在Cosine Annealing调度器中。其思想是在warmup阶段,学习率从0以某种曲线(如线性或余弦曲线的一部分)增长到初始学习率,然后紧接着开始标准的余弦退火过程。以PyTorch的 CosineAnnealingWarmRestarts 结合线性warmup为例,其过程是先执行线性增长,再执行余弦下降。这种方式使得学习率的变化更加平滑。

选择哪种策略?线性warmup简单粗暴,易于实现和理解,在绝大多数情况下效果已经足够好。余弦或其他曲线方式的warmup可能在某些任务上带来微小的提升,但也会引入额外的超参数(如曲线的形状因子),增加了调优复杂度。我的经验是, 先从线性warmup开始,它是最稳健、最不容易出错的选择 。

2.3 Warmup的关键超参数:步数与峰值学习率

Warmup引入两个核心超参数:

  1. Warmup步数( T_warmup )或比例 :这是warmup策略最重要的参数。它决定了模型“热身”的时长。设置太短,热身不充分,可能无法避免初期的震荡;设置太长,则会浪费计算资源,延缓训练进程。
  2. 峰值学习率( lr_max ) :这是warmup结束后将达到的学习率,也是后续衰减策略的起点。这个值需要与你整体的学习率调度计划协同考虑。

如何设置这些参数?这里没有放之四海而皆准的公式,但有一些经验法则:

  • Warmup步数 :通常设置为总训练步数的5%到10%。例如,如果你计划训练10个epoch,每个epoch有1000个step,总步数 T=10000 ,那么 T_warmup 可以设置在500到1000步之间。对于超大规模预训练(如LLM),warmup比例可能更低(如1%-2%),但绝对步数仍然很大。
  • 峰值学习率 :这个值需要基于你的模型架构、优化器、批次大小等因素通过实验确定。一个常见的做法是, 先在不使用warmup的情况下,通过小范围实验找到一个能稳定训练(不发散)的最大学习率,然后将这个值作为warmup的 lr_max 。因为warmup的存在,有时你甚至可以尝试比原来稍大一点的 lr_max ,因为初期的保护机制降低了风险。

注意 : lr_max 并非一定是整个训练过程中的最大学习率。如果你使用的是类似One Cycle Policy的策略,学习率在warmup后还会继续上升到一个更高的峰值再下降,那么这里的 lr_max 就是指那个最高峰值。

3. 核心细节解析与实操要点

理解了原理,我们来看看在具体实现和应用warmup时,有哪些必须关注的细节和容易踩坑的地方。这些经验往往在官方文档里不会写明,但却决定了调优的成败。

3.1 与优化器的配合:以AdamW为例

如今,AdamW(Adam with decoupled weight decay)是训练Transformer类模型的事实标准。Warmup与AdamW的配合至关重要。在PyTorch中,标准的配合流程如下:

import torch
from torch.optim import AdamW
from torch.optim.lr_scheduler import LambdaLR

# 1. 初始化模型和优化器
model = MyModel()
optimizer = AdamW(model.parameters(), lr=1e-3, betas=(0.9, 0.999), weight_decay=0.01) # 这里的lr是“名义学习率”

# 2. 定义warmup调度函数
def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps, last_epoch=-1):
    def lr_lambda(current_step: int):
        if current_step < num_warmup_steps:
            # 线性增长阶段
            return float(current_step) / float(max(1, num_warmup_steps))
        # 此处可以接其他衰减策略,例如线性衰减到0
        # 这里示例为warmup后保持恒定(实际不常见,通常会衰减)
        return 1.0
        # 更常见的可能是接余弦衰减:
        # progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))
        # return max(0.0, 0.5 * (1.0 + math.cos(math.pi * progress)))

    return LambdaLR(optimizer, lr_lambda, last_epoch=last_epoch)

# 3. 创建调度器
num_warmup_steps = 500
num_training_steps = 10000
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps)

# 4. 训练循环中
for epoch in range(num_epochs):
    for batch in dataloader:
        # ... 前向传播,计算损失,反向传播 ...
        optimizer.step()
        scheduler.step() # 更新学习率!必须在optimizer.step()之后调用
        optimizer.zero_grad()

关键要点 :

  • 调用顺序 :务必在 optimizer.step() 之后 调用 scheduler.step() 。这是因为学习率调度器需要基于最新的step数来更新学习率。
  • 优化器的 lr 参数 :在创建AdamW时传入的 lr 参数,此时可以理解为“基准学习率”或“峰值学习率”。调度器返回的乘数因子会与优化器中每个参数组的 lr 相乘,得到当前实际的学习率。因此,通常将优化器的 lr 设为 lr_max 。
  • 参数组(param_groups) :如果你的模型不同部分需要不同的学习率策略(例如,预训练骨干网络和随机初始化的分类头),你可以为优化器设置不同的参数组,并为每个参数组配置独立的调度器,实现更精细的控制。

3.2 Batch Size与Learning Rate Scaling对Warmup的影响

当我们在资源允许的情况下增大批次大小(Batch Size)以加速训练时,一个重要的原则是 需要同步增大学习率 。这是因为更大的批次提供了更准确的梯度估计(噪声更小),理论上可以承受更大的更新步长。常见的经验法则是:Batch Size扩大k倍,学习率也相应扩大k倍。

这个原则直接影响warmup的设计:

  • lr_max 需要调整 :如果你因为增大了Batch Size而按比例放大了 lr_max ,那么warmup阶段的学习率增长曲线起点虽然还是接近0,但终点变高了,整个warmup过程中的更新“力度”整体变强。
  • T_warmup 可能需要调整 :更大的学习率意味着模型初期需要更谨慎。有些研究发现,在非常大的Batch Size下(例如数千),适当延长warmup的步数(例如从总步数的10%增加到20%甚至更多)有助于稳定训练,让优化器有更多步数来适应高学习率下的梯度矩估计。但这并非绝对,需要实验验证。

实操建议 :当改变Batch Size时,首先按线性比例缩放你的 lr_max 。然后,在保持warmup比例(如总步数的10%)不变的情况下进行训练,密切关注训练初期几个epoch的损失曲线。如果损失出现异常尖峰或震荡,考虑适当增加warmup步数。

3.3 可视化与监控:看懂训练曲线

不会看训练曲线的调优是盲目的。引入warmup后,你的学习率曲线和损失曲线会呈现出新的特征。

  • 学习率曲线 :你应该能看到一条从0或极低值开始,在warmup阶段平滑上升,之后按照预定策略(如衰减、保持、再上升)变化的曲线。使用TensorBoard、WandB等工具可以轻松绘制这个曲线。确保其变化符合你的预期,是检查调度器是否正确实现的第一步。
  • 训练损失曲线 :一个健康的、使用了恰当warmup的训练,其损失曲线在最初的一些step或epoch里,下降速度应该是 逐渐加快 的,而不是一开始就急速下降或剧烈抖动。如果损失曲线在开始时就“断崖式”下跌然后马上回升震荡,这往往意味着warmup不够充分,或者初始学习率仍然太高。相反,如果损失曲线在很长一段时间内几乎不动,则可能是warmup过长或初始学习率太低。

一个重要的检查点 :在warmup结束、学习率达到 lr_max 的那一刻,观察损失值。它应该处于一个平稳下降的趋势中,而不是一个局部高点或剧烈震荡点。如果此时损失突然飙升,很可能你的 lr_max 设置过大了。

4. 实操过程与核心环节实现

理论说再多,不如动手实现一遍。下面我将以一个具体的图像分类任务(使用ResNet在CIFAR-10上)为例,展示如何从零开始集成warmup策略,并解释每一个核心环节。

4.1 环境准备与模型定义

首先,我们准备基础环境。这里使用PyTorch框架。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import LambdaLR, CosineAnnealingLR
from torchvision import datasets, transforms, models
import math

# 设备定义
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 数据加载与预处理 (CIFAR-10示例)
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])
transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)
testset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)

# 定义一个简单的模型 (这里使用预训练的ResNet18并修改全连接层)
model = models.resnet18(pretrained=False) # 为了示例,不使用预训练权重
model.fc = nn.Linear(model.fc.in_features, 10) # CIFAR-10有10类
model = model.to(device)

4.2 实现带Warmup的余弦退火学习率调度

我们将实现一个结合了线性warmup和余弦退火(Cosine Annealing)的调度器。这是目前非常流行且有效的组合。

def create_scheduler_with_warmup(optimizer, num_warmup_steps, num_training_steps, base_lr, min_lr=1e-6):
    """
    创建线性warmup + 余弦退火的学习率调度器。
    
    参数:
        optimizer: 优化器对象
        num_warmup_steps: warmup的总步数
        num_training_steps: 训练的总步数(包括warmup)
        base_lr: warmup结束后达到的峰值学习率,也是余弦退火的起始学习率
        min_lr: 余弦退火最终降到的最低学习率
    """
    def lr_lambda(current_step):
        # 1. Warmup阶段
        if current_step < num_warmup_steps:
            # 线性增长到base_lr
            return float(current_step) / float(max(1, num_warmup_steps))
        # 2. 余弦退火阶段
        # 计算在余弦退火周期内的进度,从0到1
        progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))
        # 将进度限制在[0, 1]范围内,防止超出总步数
        progress = min(progress, 1.0)
        # 余弦衰减公式:lr = min_lr + 0.5 * (base_lr - min_lr) * (1 + cos(progress * pi))
        # 这里返回的是乘数因子,所以公式调整为:
        cosine_decay = 0.5 * (1 + math.cos(math.pi * progress))
        decayed = (1 - min_lr/base_lr) * cosine_decay + min_lr/base_lr
        return decayed
    
    scheduler = LambdaLR(optimizer, lr_lambda)
    # 为了在训练中能获取当前学习率,我们可以存储这些参数
    scheduler.num_warmup_steps = num_warmup_steps
    scheduler.num_training_steps = num_training_steps
    scheduler.base_lr = base_lr
    scheduler.min_lr = min_lr
    return scheduler

4.3 整合训练循环与学习率调度

现在,将模型、损失函数、优化器和我们自定义的调度器整合到完整的训练循环中。

# 超参数配置
EPOCHS = 50
BASE_LR = 0.1  # Warmup后达到的峰值学习率
MIN_LR = 1e-4  # 学习率最低值
WARMUP_RATIO = 0.1  # Warmup步数占总训练步数的比例

# 计算总步数和warmup步数
total_steps = len(trainloader) * EPOCHS
num_warmup_steps = int(total_steps * WARMUP_RATIO)

print(f"总训练步数: {total_steps}")
print(f"Warmup步数: {num_warmup_steps}")

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=BASE_LR, momentum=0.9, weight_decay=5e-4)
# 注意:优化器的lr参数这里设置为BASE_LR,它将被调度器返回的乘数因子缩放。

# 创建调度器
scheduler = create_scheduler_with_warmup(optimizer, num_warmup_steps, total_steps, BASE_LR, MIN_LR)

# 训练循环
global_step = 0
for epoch in range(EPOCHS):
    model.train()
    running_loss = 0.0
    for i, (inputs, labels) in enumerate(trainloader):
        inputs, labels = inputs.to(device), labels.to(device)
        
        # 清零梯度
        optimizer.zero_grad()
        
        # 前向传播
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        
        # 反向传播
        loss.backward()
        
        # 梯度裁剪(可选,对于非常深的网络或RNN有益,防止梯度爆炸)
        # torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        # 更新参数
        optimizer.step()
        
        # 更新学习率!!!关键步骤
        scheduler.step()
        
        global_step += 1
        running_loss += loss.item()
        
        # 每100个batch打印一次信息,包括当前学习率
        if i % 100 == 99:
            current_lr = scheduler.get_last_lr()[0] # 获取当前学习率
            print(f'Epoch [{epoch+1}/{EPOCHS}], Step [{global_step}/{total_steps}], '
                  f'Loss: {running_loss/100:.4f}, LR: {current_lr:.6f}')
            running_loss = 0.0
    
    # 每个epoch结束后在测试集上验证(代码略)
    # ...

代码解析与要点 :

  1. 步数计算 : total_steps = len(trainloader) * EPOCHS 。这是基于“每个batch更新一次”的设定。如果你的调度策略是基于epoch的,则需要调整。
  2. 优化器初始学习率 :我们将优化器的 lr 参数设置为 BASE_LR 。在训练中, scheduler.get_last_lr() 返回的是一个列表,包含了每个参数组当前应乘的因子。实际学习率是 optimizer.param_groups[i]['lr'] = initial_lr * factor 。我们的调度器函数 lr_lambda 返回的就是这个因子。
  3. scheduler.step() 的位置 :这是最关键的细节之一。它必须在 optimizer.step() 之后 调用,因为调度器需要知道当前已完成的更新步数( global_step 或内部计数器)。
  4. 学习率监控 :通过 scheduler.get_last_lr() 可以随时获取当前学习率,方便记录和可视化。

4.4 效果对比实验与结果分析

为了直观展示warmup的效果,我们可以设计一个简单的对比实验:

  • 实验A :使用上述带warmup的余弦退火策略。
  • 实验B :不使用warmup,直接以 BASE_LR 开始训练,并应用相同的余弦退火。

分别记录两个实验在训练初期的损失曲线和最终验证集准确率。

预期结果 :

  • 训练初期损失曲线 :实验A(有warmup)的损失曲线在最初几百个step内会平缓启动,然后加速下降,曲线平滑。实验B(无warmup)的损失曲线可能在第一步就急剧下降,但随后可能出现明显的“反弹”或剧烈震荡,稳定性较差。
  • 最终性能 :在大多数情况下,实验A会达到与实验B相当或略高的最终准确率。更重要的是, 实验A的训练过程更加稳定可靠,对超参数(尤其是初始学习率)的鲁棒性更强 。这意味着当你换用不同的模型架构或数据集时,使用warmup能减少调参工作量,更容易复现好的结果。

在我的多次实践中,尤其是在训练Transformer或大型CNN时,省略warmup常常导致需要将初始学习率调低一个数量级才能稳定训练,而这又会显著拖慢收敛速度。正确使用warmup,允许你安全地使用更大的学习率,从而可能达到更好的收敛点和更快的训练速度。

5. 常见问题与排查技巧实录

即使理解了原理,在实战中应用warmup时还是会遇到各种问题。下面是我总结的一些典型“坑”及其解决方法。

5.1 问题一:训练初期损失变成NaN或无限大

现象 :训练刚开始没几步,损失值就变成 nan 或 inf 。 可能原因与排查 :

  1. Warmup步数太短或学习率峰值太高 :这是最常见的原因。模型还没来得及“热身”,就被过大的学习率“冲垮”了。梯度更新步长太大,导致参数值溢出。
    • 解决 :首先,检查你的 BASE_LR 是否设置得过高。可以尝试先不用warmup,找一个能稳定训练的小学习率作为参考。其次,增加 num_warmup_steps ,比如从总步数的5%增加到10%或15%。
  2. 梯度爆炸 :即使有warmup,如果模型本身存在梯度爆炸问题(如非常深的RNN、未做归一化的Transformer),小学习率也可能无法阻止。
    • 解决 :在 optimizer.step() 之前加入梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) 。 max_norm 是一个经验值,通常设置在0.5到5.0之间。
  3. 数据或损失函数问题 :检查输入数据是否有NaN或inf值,检查损失函数在极端输入下的计算是否稳定(例如,交叉熵损失输入logits前是否做了适当的数值处理)。

5.2 问题二:Warmup结束后损失突然上升

现象 :损失曲线在warmup阶段平稳下降,但在warmup结束、学习率达到峰值的那一刻,损失值出现一个明显的向上尖峰。 可能原因与排查 :

  1. 峰值学习率 ( BASE_LR ) 设置过高 :这是最直接的原因。warmup提供的保护在结束时消失,模型突然切换到过高的学习率,导致更新步长过大。
    • 解决 :降低 BASE_LR 。一个实用的技巧是进行 学习率扫描(LR Range Test) :在一个或几个epoch内,让学习率从一个非常小的值(如1e-7)指数增长到一个很大的值(如10),同时记录损失。绘制损失vs学习率的曲线。理想的学习率通常位于损失开始持续下降但尚未剧烈上升的区域。将这个值作为你的 BASE_LR 。
  2. Warmup到衰减策略的过渡不平滑 :如果你自定义的调度器在warmup结束和衰减开始处存在不连续(例如,从线性增长突然切换到阶梯下降),也可能引起震荡。
    • 解决 :确保调度函数是连续的。像我们上面实现的“线性warmup+余弦衰减”组合,在交接点 ( current_step == num_warmup_steps ) 时,返回值都是1.0,是平滑的。

5.3 问题三:感觉Warmup浪费了训练时间

现象 :觉得warmup阶段学习率很低,模型“学得慢”,拖累了整体训练进度。 分析与解决 : 这是一种常见的误解。我们需要从优化轨迹的角度来看:

  • 没有warmup :模型可能因为初期的大学习率而在损失平面上“跳来跳去”,甚至跳到糟糕的区域,后续需要更多时间修正,或者永远无法收敛到好的解。
  • 有warmup :模型从原点附近开始,沿着一个相对平滑、稳定的方向开始搜索。虽然起始速度慢,但方向更准,避免了走弯路和回头路。

量化对比 :你可以做一个实验,比较“有warmup训练N个epoch”和“没有warmup但调低学习率训练N个epoch”所达到的验证集精度。在大多数情况下,前者会胜出。 Warmup不是浪费时间,而是在投资时间换取训练过程的稳定性和最终性能的上限 。对于动辄训练几天甚至几周的大模型,花费几小时进行warmup是完全值得的。

5.4 问题四:如何为不同的参数组设置不同的Warmup策略?

场景 :在微调(Fine-tuning)预训练模型时,我们通常希望骨干网络(Backbone)以较小的学习率缓慢调整,而新添加的头部(Head)以较大的学习率快速学习。 解决方案 :PyTorch的优化器支持为不同的参数组设置不同的学习率。我们可以为每个组创建独立的调度器,或者在一个调度函数中根据参数组ID进行条件判断。

# 示例:为骨干网络和分类头设置不同的学习率和warmup
backbone_params = []
head_params = []
for name, param in model.named_parameters():
    if 'fc' in name: # 假设分类头层的名字包含'fc'
        head_params.append(param)
    else:
        backbone_params.append(param)

optimizer = optim.AdamW([
    {'params': backbone_params, 'lr': 1e-5}, # 骨干网络小学习率
    {'params': head_params, 'lr': 1e-3},     # 分类头大学习率
], weight_decay=0.01)

# 为两个参数组创建独立的调度器
scheduler_backbone = create_scheduler_with_warmup(optimizer, num_warmup_steps, total_steps, base_lr=1e-5, param_group_index=0)
scheduler_head = create_scheduler_with_warmup(optimizer, num_warmup_steps, total_steps, base_lr=1e-3, param_group_index=1)
# 注意:需要修改create_scheduler_with_warmup函数以支持指定param_group_index,或者使用LambdaLR分别为不同组设置lambda函数。

更简洁的方式是使用一个调度器,但在lambda函数中根据 current_step 和参数组索引返回不同的因子。不过,更常见的做法是直接使用像 transformers 库中 get_linear_schedule_with_warmup 这样的函数,它天然支持多参数组,会对每个组应用相同的调度因子,但每个组会乘以自己独立的初始 lr 。

5.5 高级技巧:重启(Restarts)与Warmup的结合

在诸如SGDR(带热重启的随机梯度下降)或CosineAnnealingWarmRestarts策略中,学习率会在训练过程中周期性地通过余弦函数衰减到最小值,然后突然“重启”到一个较高的值,开始新的衰减周期。这种策略可以帮助模型跳出局部最优。

在这种情况下, 每次重启(restart)时,都应该重新进行warmup 。因为重启后学习率从最小值跳到一个相对较高的值,这类似于训练开始时的“冷启动”状态。PyTorch的 CosineAnnealingWarmRestarts 调度器本身不包含warmup,但我们可以通过组合调度器( ChainedScheduler )或者自定义调度函数来实现。

实现思路是:在每个重启周期(T_cycle)的开始阶段,叠加一个短暂的线性warmup。这需要更精细地控制调度逻辑,但能显著提升带有重启策略的训练稳定性。

更多推荐