微调预训练模型
微调是将一个先前在一个数据集上训练好的模型,调整使其适应更专业的数据集/任务的过程。通常,原始数据集非常大且非常通用(例如:抓取互联网上的大部分公共数据),因此模型为了理解所有这些信息也非常大(数十亿甚至更多参数)。
HuggingFace 的 transformers 等库提供了最先进的预训练模型,这些模型可以在 Ludwig 中用作输入特征编码器,使您能够利用这些大型预训练模型,并将其适应于解决您的特定任务,将它们与表格元数据等其他领域特定特征相结合,创建强大的多模态模型架构。
Ludwig 的默认配置旨在快速灵活,因此,当微调预训练模型时,我们建议对默认配置参数进行一些调整。以下部分将展示我们发现能取得良好结果的配置示例,以及每个被覆盖参数背后的理由。
建议配置¶
以下部分配置展示了“完整微调配置”,包括可训练权重、设置为最大化吞吐量的批大小,以及学习率预热/衰减。
defaults:
text:
encoder:
type: bert
trainable: true
trainer:
epochs: 5
batch_size: auto
learning_rate: 0.00001
learning_rate_scheduler:
warmup_fraction: 0.2
decay: linear
optimizer:
type: adamw
use_mixed_precision: true
compile: true
如果您希望从模型中获得最佳性能,并且对训练时间不敏感,这是一个很好的起点。在以下部分中,我们还将介绍一些权衡部分潜在性能以换取大幅提高训练吞吐量的选项。
特征编码器和预处理¶
支持微调的特征¶
文本编码器¶
type: text
encoder:
type: bert
use_pretrained: true
Ludwig 中所有的 HuggingFace 编码器 都可以用于微调,只需在编码器配置中设置 use_pretrained=true
(默认值)。如果您想使用某个特定模型但未在列表中看到,可以将 auto_transformer
编码器与在 pretrained_model_name_or_path
参数中提供模型名称结合使用。
图像编码器¶
type: image
encoder:
type: resnet
use_pretrained: true
Ludwig 中所有的 Torchvision 预训练模型 都可以用于微调,只需在编码器配置中设置 use_pretrained-true
(默认值)。
可训练¶
encoder:
trainable: true
Ludwig 目前支持两种微调变体,通过 trainable
编码器参数配置:
- 修改预训练编码器的权重以使其适应下游任务(
trainable=true
)。 - 保持预训练编码器权重固定,并训练作为组合器和解码器模块下游的一堆密集层(
trainable=false
,默认值)。这有时被区分为迁移学习。
通过设置 trainable=true
允许修改权重可以显著提高下游任务的性能,但这将需要显著更长的训练时间(由于对预训练模型参数进行额外的反向传播)。此外,在选择超参数时,当 trainable=true
时需要更加谨慎,以防止灾难性遗忘,即模型忘记预训练期间学到的所有有价值信息。我们在下面的训练器部分介绍了一些有助于防止这种情况的有用技术,但总而言之:
- 选择较低的
learning_rate
。 - 通过设置
warmup_fraction
使用学习率预热。 - 通过设置
decay
使用学习率衰减。 - 将总训练
epochs
限制在 3 到 10 之间。
保持权重冻结将显著加快训练速度,特别是在缓存编码器嵌入(参见下文)时。
缓存编码器嵌入¶
name: sentence
type: text
encoder:
type: bert
trainable: false
preprocessing:
cache_encoder_embeddings: true
如果您选择设置 trainable=false
以在微调期间保持权重固定,我们强烈建议在特征配置的预处理部分设置 cache_encoder_embeddings=true
,这将把从分词输入生成文本嵌入的编码器前向传播移至 Ludwig 训练工作流的预处理部分,从而完全从训练中移除此步骤。实际上,这可以将微调期间的训练速度提高 50 倍以上。
当嵌入被缓存在预处理数据中时,我们在训练时将 ECD 模型中的相应编码器替换为一个模拟的“跳过编码器”(Skip Encoder),该编码器直接将输入张量数据传递到下游组合器,以便与其他输入特征进行连接。在预测时以及将模型导出到 TorchScript 时,跳过编码器将被原始编码器及其预训练权重替换。
虽然嵌入缓存对于单个模型训练运行很有用,因为它只需为一个训练轮次支付大型预训练模型的前向传播成本,但在后续实验中它可能更有价值。因为 Ludwig 会缓存预处理数据并在新的模型训练实验中重复使用(当预处理参数相同时),所以同一数据集的任何后续训练运行将完全不需要对预训练模型进行前向传播。
训练器¶
训练轮次¶
trainer:
epochs: 5
我们建议在微调时将训练轮次数量保持在 3 到 10 之间。如果您设置了 trainable=true
,我们建议将其设置得更接近 3 到 5 个训练轮次。这是为了帮助防止在后续训练轮次中发生灾难性遗忘,因为当指定的训练轮次数过高时,学习率衰减会启动得太慢。
此外,在单个 GPU 上训练默认的 100 个训练轮次可能非常耗时。
批大小¶
trainer:
batch_size: auto
我们建议保持默认的 batch_size=auto
,以最大化训练期间的 GPU 利用率。如果选择较低的批大小,训练进度将慢得多且成本更高,因为 GPU 周期会被浪费。
实际上,预训练模型对使用更大批大小进行训练的敏感度往往低于许多小型模型架构。然而,如果您在使用更大批大小时遇到收敛问题,我们建议在组合器或解码器中启用幽灵批归一化。
combiner:
type: concat
norm: ghost
学习率¶
基础学习率¶
trainer:
learning_rate: auto
微调时使用非常小的学习率,以避免预训练模型先前学到的所有知识发生灾难性遗忘。
- 当
trainable=true
时,我们建议从低至learning_rate=0.00001
开始。 - 当
trainable=false
时,我们建议从learning_rate=0.00002
开始。
请注意,设置 learning_rate=auto
将根据所选的模型架构自动为您设置上述默认值。
学习率调度¶
trainer:
epochs: 5
learning_rate_scheduler:
warmup_fraction: 0.2
decay: linear
对学习率进行预热(尤其在使用分布式训练时)和衰减以避免灾难性遗忘非常重要。
将 warmup_fraction
设置为 0.2
将导致总训练步骤的 20% 用于将学习率从 0 线性地逐步提高到 trainer.learning_rate
中提供的初始值。这很有用,否则学习过程可能会在训练的早期阶段过度修正预训练模型的权重。
使用 linear
衰减是一种非常激进的衰减策略,它会随着训练接近最后一个训练轮次而将学习率线性地降低到 0。衰减只会在学习率预热期结束后并且学习率设置为初始值时开始。
预热和衰减都受 trainer
配置中设置的总 epochs
影响,因此确保 epochs
设置为足够低的值以使预热和衰减有效非常重要。如果 epochs
保留默认值 100
,那么将花费太多时间进行预热,而衰减将不明显。
学习率缩放¶
trainer:
learning_rate_scaling: sqrt
基础 trainer.learning_rate
将随着分布式训练的训练工作器数量增加而按比例放大。默认情况下,学习率会线性缩放(linear
),但如果您注意到发生了灾难性遗忘,可以放宽此设置,此时可能值得考虑使用更柔和的 learning_rate_scaling=sqrt
设置。
优化器¶
trainer:
optimizer:
type: adamw
我们建议使用 adamw
作为微调的优化器。
AdamW 通常被推荐用于微调,而不是 SGD 或 Adam 等更传统的优化器,因为它在处理权重衰减方面有所改进,这在微调期间对于避免灾难性遗忘尤为重要。
混合精度¶
trainer:
use_mixed_precision: true
强烈建议在微调时设置 use_mixed_precision=true
。根据经验,这可以将训练速度提高约 2.5 倍,同时不会损失模型质量。
模型编译¶
trainer:
compile: true
对于使用 PyTorch v2.0 及以上版本的用户,您可以利用新的模型编译功能,将训练速度提高 20% 以上。模型编译与混合精度训练结合使用时尤其有效。
后端¶
微调大型预训练模型通常受益于分布式训练,而无需大量的额外超参数调整。因此,我们建议使用 Ray 后端,以便利用多 GPU 训练并扩展到大型数据集。
在大多数情况下,ddp
或 horovod
分布式策略会运行良好,但如果模型对于您的 GPU 类型来说太大,那么您应该尝试 deepspeed
策略,该策略允许在多个 GPU 上对模型参数进行分片。