Hexo

凡事预则立,不预则废


  • Home

  • Tags

  • Archives

  • Navigation

  • Search

DL——模型训练预热


整体说明

  • 预热(Warm-up)是一种训练技巧:
    • 在模型训练初期采用一些策略,逐步调整超参数(如学习率、 Batch Size 大小等)或模型状态 ,使得训练过程更加稳定、高效的初始化阶段
    • 通过合理预热,可以显著提升训练稳定性、收敛速度和最终性能
  • 预热的核心目的是避免训练初期因参数随机初始化或学习率过高导致的梯度不稳定、收敛困难等问题
  • 常见的预热技术主要包含两类:
    • 学习率预热(Learning Rate Warm-up) :训练初期从极小的学习率(如0)逐步线性或非线性增加到预设值
    • 优化器预热 :
      • Adam 预热阶段可用小学习率,比如正常值的 \(1/10\)(Adam 优化器的自适应动量在初期可能不准确);
      • Adam 在预热阶段启用偏差修正 ,避免初期估计偏差过大
  • 其他预热技术还包括:Batch Size 预热(Batch Size 从小到大),模型参数预热(逐步解冻模型层) 和 混合精度预热等(初期禁用混合精度)
  • 最常见的预热技术是学习率预热,其中 Transformer 常使用 学习率线性预热(比如 BERT 训练中常用 10,000 步线性预热)
  • 术语:warm-up ratio
    • 如 warm-up ratio 等于 0.03,表示 warm-up 阶段(学习率上升阶段)步数占总训练阶段步数的 3%

学习率预热的相关策略

  • 学习率预热(Learning Rate Warm-up)是训练初期逐步增加学习率的策略,旨在稳定训练并提升最终性能。以下是常见的具体方法及其细节:

线性预热(Linear Warm-up)

  • 在预热步数 \(N\) 内,学习率从 \(0\)(或极小值 \(\epsilon\))线性增长到初始学习率 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon + \left(\frac{t}{N}\right) \cdot (lr_{\text{base} } - \epsilon)
    $$
    • 其中 \(t\) 是当前步数,\(t \leq N\)
  • 最常用的方式之一

余弦预热(Cosine Warm-up)

  • 结合余弦函数曲线调整学习率,初期缓慢增长,后期平滑过渡到目标值
    $$
    lr_t = \frac{1}{2} \left(1 + \cos\left(\pi \cdot \left(1 - \frac{t}{N}\right)\right)\right) \cdot lr_{\text{base} }
    $$
  • 注:也可与余弦退火结合,预热后直接进入衰减阶段
  • 更平滑的过渡,减少初期学习率突变
  • 一些大模型中会使用到

指数预热(Exponential Warm-up)

  • 学习率从 \(\epsilon\) 开始指数增长到 \(lr_{\text{base} }\)
    $$
    lr_t = \epsilon \cdot \left(\frac{lr_{\text{base} } }{\epsilon}\right)^{\frac{t}{N} }
    $$
  • 较少使用,因可能过早进入高学习率阶段

阶梯预热(Step Warm-up)

  • 将预热阶段分为多个离散区间,逐步跳跃式增加学习率

附录:torch 自带预热和学习率调度代码示例

  • 一个完整的PyTorch示例:先进行学习率预热,再正常训练模型

  • 以简单的图像分类任务(CIFAR-10)为基础,结合线性预热和余弦退火调度器

  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    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
    from torch.utils.data import DataLoader
    import matplotlib.pyplot as plt

    class SimpleCNN(nn.Module):
    def__init__(self):
    super().__init__()
    self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
    self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
    self.fc = nn.Linear(32 * 8 * 8, 10) # CIFAR-10输入为32x32,经过两次池化后为8x8
    self.pool = nn.MaxPool2d(2, 2)
    self.relu = nn.ReLU()

    def forward(self, x):
    x = self.pool(self.relu(self.conv1(x)))
    x = self.pool(self.relu(self.conv2(x)))
    x = x.view(-1, 32 * 8 * 8)
    x = self.fc(x)
    return x

    transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])
    train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
    train_loader = DataLoader(train_set, batch_size=128, shuffle=True)

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = SimpleCNN().to(device)
    ## 注:学习率包含在优化器 optimizer 中,使用不同的学习率调度器来执行 step,就可以实现不同的学习率调度
    optimizer = optim.AdamW(model.parameters(), lr=0.001) # 初始学习率设为0.001(预热目标值)

    warmup_steps = 500 # 预热步数
    total_steps = 5000 # 总训练步数

    # 线性预热函数
    def warmup_lambda(current_step):
    if current_step < warmup_steps:
    return float(current_step) / float(max(1, warmup_steps))
    else:
    return 1.0 # 预热结束后保持学习率

    # 预热阶段调度器
    warmup_scheduler = LambdaLR(optimizer, lr_lambda=warmup_lambda) # 基于优化器初始化调度器

    # 预热后的余弦退火调度器(从预热结束开始)
    cosine_scheduler = CosineAnnealingLR(
    optimizer, # 与预热阶段调度器初始化相同的优化器
    T_max=total_steps - warmup_steps, # 余弦周期长度
    eta_min=1e-6 # 最小学习率
    )

    criterion = nn.CrossEntropyLoss()
    lr_history = []
    for step in range(total_steps):
    inputs = torch.randn(128, 3, 32, 32).to(device)
    labels = torch.randint(0, 10, (128,)).to(device)

    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    # 更新学习率
    if step < warmup_steps:
    warmup_scheduler.step() # 预热阶段,step 函数会按照 warmup_scheduler 的定义来修改学习率
    else:
    cosine_scheduler.step() # 预热后余弦退火,step 函数会按照 cosine_scheduler 的定义来修改学习率

    # 记录学习率,可打印出来观测
    lr_history.append(optimizer.param_groups[0]['lr'])

    if step % 200 == 0:
    print(f"Step {step}: LR = {optimizer.param_groups[0]['lr']:.6f}, Loss = {loss.item():.4f}")
  • 预热阶段(前500步):学习率从 0 线性增长到初始值 0.001
    $$ lr = \text{base_lr} \times \frac{\text{current_step} }{\text{warmup_steps} } $$

  • 正常训练阶段(500步后):切换为余弦退火调度器(CosineAnnealingLR),学习率从 0.001 逐渐衰减到 1e-6

    • 注: 余弦退火的周期长度 \( T_{\text{max} } \) 设为总步数减去预热步数
  • 总体来说,学习率曲线是先线性上升,后余弦式下降(平滑振荡衰减)的过程


附录:transformers 库的模型训练预热调度示例

  • transformers 库中使用模型训练预热代码(按照初始学习率 1e-4, epochs= )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    import matplotlib.pyplot as plt
    import transformers
    import torch

    initial_lr = 1.0e-4 # 初始学习率
    warmup_ratio = 0.1 # 预热比例

    num_training_steps = 1000 # 总训练 step 数
    num_warmup_steps = int(num_training_steps * warmup_ratio) # 计算 warmup 的 step 数

    optimizer = torch.optim.AdamW([torch.tensor(0.0)], lr=initial_lr) # [torch.tensor(0.0)] 是虚拟的模型参数,可随意设置

    # 使用 transformers 库创建余弦退火学习率调度器
    lr_scheduler = transformers.get_cosine_schedule_with_warmup(
    optimizer=optimizer,
    num_warmup_steps=num_warmup_steps, # warmup step 数
    num_training_steps=num_training_steps, # 训练总 step 数
    # num_cycles=0.5, # 对应 cosine 曲线的周期,默认值是0.5,也就是半周期(递减)
    # last_epoch=-1, # 用于从 checkpoint 启动时恢复训练,设置为 ckpt 对应 step-1 即可
    # 比如从第 500 步的 ckpt启动,设置为499,从第0步启动,设置为-1(默认值)
    )

    learning_rates = []
    for _ in range(num_training_steps):
    learning_rates.append(optimizer.param_groups[0]["lr"])
    lr_scheduler.step() # 更新 optimizer.param_groups[0]["lr"]

    # 设置中文字体
    plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

    # 以下为可视化代码
    plt.figure(figsize=(10, 6))
    plt.plot(learning_rates)
    plt.title('学习率变化曲线')
    plt.xlabel('训练步骤')
    plt.ylabel('学习率')
    plt.grid(True)
    plt.axvline(x=num_warmup_steps, color='r', linestyle='--', label='预热结束')
    plt.legend()

    plt.annotate(f'初始学习率: {initial_lr}', xy=(num_warmup_steps, initial_lr),
    xytext=(num_warmup_steps + 50, initial_lr * 1.5),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'预热起点: 0', xy=(0, 0),
    xytext=(50, initial_lr * 0.2),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.annotate(f'最终学习率: {learning_rates[-1]:.8f}', xy=(num_training_steps-1, learning_rates[-1]),
    xytext=(num_training_steps-200, learning_rates[-1] * 10),
    arrowprops=dict(facecolor='black', shrink=0.05))
    plt.tight_layout()
    plt.savefig('warmup_learning_rate_curve_cycles_0.5.png', dpi=300)
    # plt.show()
  • 可视化结果(半周期余弦 num_cycles=0.5 的结果):

    • warmup 阶段,学习率从 0 开始逐步提升到最大值
    • 正式训练阶段,学习率按照余弦调度器波动
  • 如果设置为 num_cycles=1,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=1.5,则会在指定训练步数内完成两个周期的学习率变化:

  • 如果设置为 num_cycles=2,则会在指定训练步数内完成两个周期的学习率变化:


附录:预热有什么用?

  • 解决梯度不稳定问题 :模型初始阶段参数随机初始化,直接使用高学习率可能导致梯度爆炸或震荡
  • 解决学习率敏感性问题 :过大的初始学习率可能使模型跳过最优解附近区域;过小则导致收敛缓慢
  • 保证优化器适应性 :如 Adam 等自适应优化器在初期需要积累梯度统计量(如动量、方差),预热阶段可为优化器提供更稳定的初始估计

附录:一般预热多少步更合适?

  • 预热步数通常取决于模型规模和数据集大小:
    • 小规模数据:数百到几千步
    • 大规模训练(如LLM):数万步甚至更长(例如 GPT-3 的数千批次预热)
  • 另一种设置方式是:通常为总训练步数的 5-10%(例如 BERT 的 10k 步预热,总步数 100k)

DL——深度学习并行技术总结


整体说明

  • 并行化技术一般在训练大型深度学习模型时使用
  • 并行化技术氛围三种:
    • 数据并行 (Data Parallelism)
    • 模型并行 (Model Parallelism)
    • 流水线并行 (Pipeline Parallelism),有的地方也翻译为管道并行

各种并行方法之间的关系总结

  • 整体可分为 模型并行 (Model Parallelism) 和 数据并行
    • 数据并行 :每个 GPU 都拥有一个完整的模型副本,但处理不同的数据批次
    • 模型并行 :每个 GPU 只负责模型的一部分,所有 GPU 共同处理一个完整的数据批次,包括 张量并行 和 流水线并行 两种具体实现
  • 模型并行的进一步介绍:当模型太大无法放入单个 GPU 时,就需要使用模型并行
    • 将模型的不同部分分配给不同的 GPU
    • 优点是可以训练显存无法容纳的巨大模型
    • 缺点是实现相对复杂,且由于不同 GPU 间的通信和等待,可能会导致 GPU 利用率不高
  • 实际应用中,为了充分利用资源并训练超大模型,通常会结合多种并行化技术,形成 混合并行 策略,例如同时使用数据并行、流水线并行和张量并行

数据并行 (Data Parallelism)

  • 最常见、也最容易理解的并行化方法
  • 数据并行的工作方式 :
    • 训练数据集被分成多个子集(例如,一个 128 张图片的批次被分成 4 个 32 张图片的子批次)
    • 每个 GPU 拥有一个完整的模型,并独立处理一个子批次的数据
  • 数据并行的训练过程 :
    • 1)每个 GPU 计算其子批次的前向传播和反向传播,得到各自的梯度
    • 2)通过 All-Reduce 这样的通信操作,将所有 GPU 的梯度进行汇总和平均
    • 3)每个 GPU 用这个平均后的梯度来更新自己的模型参数,从而确保所有模型副本保持同步
  • 优点是实现简单,对模型结构无特殊要求 ,比如使用 PyTorch 的 DP 类就可以实现
  • 缺点是每个 GPU 都需要存储完整的模型 ,当模型参数量非常大时,会超出单个 GPU 的显存限制 ,此时数据并行就无法使用

流水线并行 (Pipeline Parallelism)

  • 流水线并行将模型的不同“层”(或一组层)分配给不同的 GPU,形成一个“流水线”
    • 例如,GPU 1 负责模型的第 1-4 层,GPU 2 负责第 5-8 层,以此类推
  • 数据并行的训练过程 :
    • 一个数据批次被分解成更小的“微批次”(micro-batches)
    • GPU 1 处理第一个微批次,完成后将输出传给 GPU 2
    • 当 GPU 1 开始处理第二个微批次时,GPU 2 就可以同时处理第一个微批次
  • 优点是部分解决了模型过大的问题(单层过大仍然无法解决)
  • 缺点是存在 “流水线气泡”(pipeline bubble) 问题,即流水线开始和结束时,部分 GPU 会处于空闲等待状态,导致 GPU 利用率并非 100%
    • 注:通过流水线的方式(即错位并行处理不同微批次的方式),可以一定程度上重叠不同 GPU 的计算,提高整体效率

张量并行 (Tensor Parallelism)

  • 张量并行 不按层切分模型,而是将模型中某个操作内部的 张量(例如一个大型矩阵)切分到不同的 GPU 上
  • 举例来说:一个 \(A \times B\) 的矩阵乘法,可以把矩阵 B 按列切分,每个 GPU 分别计算,最后再通过通信操作将结果合并
  • 优点是:
    • 可以进一步解决单个层或单个操作的参数过大的问题,从根本上解决了模型过大的问题
    • 因为所有 GPU 都在同一时间处理同一个微批次,所以不会有流水线气泡问题 ,GPU 利用率通常更高
  • 缺点是
    • 对模型结构有要求,通常只能在某些特定操作(如矩阵乘法、线性层)中应用
    • 需要频繁的 GPU 间通信来同步切分后的张量,这要求非常高速的 GPU 互联(例如 NVLink)
1…131132133…352
San Ye

San Ye

Stay Hungry. Stay Foolish.

704 posts
53 tags
© 2026 San Ye
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4