跳转到内容

6.1 Trainer API 速成——三步微调一个模型

回顾一下我们到目前为止走过的路:第 1 章建立了 PyTorch 的基础认知,第 2 章构建了生产级的数据管道,第 3 章从零手写了一个完整的 Transformer/GPT 模型,第 4 章实现了手写的训练循环和优化策略,第 5 章用 PyTorch Lightning 把训练流程提升到了工程化水平。每一步都让我们离"真正训练一个大语言模型"更近了一步。但如果你现在去问任何一个正在做 LLM 微调的工程师"你用什么工具",十有八九得到的回答是:HuggingFace Transformers 的 Trainer + PEFT(LoRA)。这不是说我们之前学的东西没有价值——恰恰相反,理解底层原理是正确使用高层工具的前提。但不得不承认的事实是:在 LLM 微调这个具体场景中,HF Trainer + PEFT 已经成为了工业界的事实标准,它的生态集成度、开箱即用的功能丰富程度以及社区支持都是其他方案难以比拟的。

这一章的目标就是让你掌握这套工具链。我们从最简单的三步上手开始,逐步深入到 LoRA 原理、量化训练、完整实战对比,最后覆盖模型评估与导出。先别被这些名词吓到——它们背后都有清晰的逻辑链条。

为什么 LLM 微调首选 HF Trainer?

在深入代码之前,先理解 HF Trainer 相对于我们之前学过的两种方式(手写 PyTorch 和 Lightning)的独特优势在哪里。核心原因可以归结为一点:生态集成深度

当你用 HF Trainer 时,你获得的不仅仅是一个训练循环封装器,而是一个与整个 HuggingFace 生态系统无缝对接的工作流:

Model Hub (自动下载/上传)

AutoModel (自动识别架构 + 权重加载)

Trainer (统一训练接口)
    ├── 内置 PEFT/LoRA 支持
    ├── 内置 BitsAndBytes 量化支持
    ├── 内置 DeepSpeed/FSDP 分布式支持
    └── 内置 W&B/TensorBoard 日志

push_to_hub (一键部署)

每一个环节都经过了数千个项目的验证,而且它们之间的配合是天衣无缝的。比如你想用 LoRA 微调一个 Qwen 模型——在 HF 体系中只需要三行配置;如果你想换成全量微调——改一行参数;如果你想加上 4-bit 量化来节省显存——再加两行配置。这种灵活性来自于 HF 团队对 LLM 训练工作流的深刻理解和大量工程投入。

三步上手:最快的方式微调一个 7B 模型

让我们直接看代码。下面是用 HF Trainer + PEFT 在单个 GPU 上微调 Qwen2.5-7B-Instruct 模型的最小可行示例:

python
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling,
)
from peft import LoraConfig, get_peft_model, TaskType


def quick_finetune():
    model_name = "Qwen/Qwen2.5-7B-Instruct"

    print("[Step 1] Loading model and tokenizer...")
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True,
    )
    tokenizer = AutoTokenizer.from_pretrained(model_name)

    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    print(f"[Step 2] Configuring LoRA...")
    lora_config = LoraConfig(
        r=16,
        lora_alpha=32,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM,
    )
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()

    print(f"[Step 3] Training...")
    args = TrainingArguments(
        output_dir="./output/qwen-lora",
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        num_train_epochs=3,
        learning_rate=2e-4,
        warmup_ratio=0.03,
        logging_steps=10,
        save_strategy="epoch",
        bf16=True,
        gradient_checkpointing=True,
        report_to="wandb",
        run_name="qwen-lora-demo",
    )

    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=train_dataset,
        data_collator=DataCollatorForLanguageModeling(
            tokenizer, mlm=False, pad_to_multiple_of=8
        ),
    )

    trainer.train()

    print("Training complete!")


quick_finetune()

就这三步——加载模型、配置 LoRA、启动训练。运行后你会看到类似这样的输出:

[Step 1] Loading model and tokenizer...
[Step 2] Configuring LoRA...
trainable params: 33,554,432 || all params: 7,696,998,912 || trainable%: 0.4359%
[Step 3] Training...
{'train_runtime': 4523.12, 'train_samples_per_second': 1.98, 
 'train_steps_per_second': 0.124, 'train_loss': 2.1234, 'epoch': 3.0}
Training complete!

注意那个 trainable%: 0.4359% —— 这意味着我们在只训练不到 0.5% 参数的情况下完成了对 7B 模型的微调。这就是 LoRA 的威力所在,下一节我们会详细解释它的工作原理。

TrainingArguments 完整参数指南

TrainingArguments 是 HF Trainer 的配置中心,它包含了控制训练行为的所有参数。虽然大多数参数都有合理的默认值,但理解每个参数的含义对于调试问题和优化性能至关重要。我们把参数按功能分组来介绍:

训练控制参数

python
args = TrainingArguments(
    # 基本训练设置
    num_train_epochs=3,              # 总训练轮数
    max_steps=-1,                    # 最大步数(-1 表示由 epoch 决定)
    
    # Batch 相关
    per_device_train_batch_size=4,   # 每 GPU 的训练 batch size
    per_device_eval_batch_size=4,     # 每 GPU 的验证 batch size
    gradient_accumulation_steps=4,   # 梯度累积步数
    
    # 学习率相关
    learning_rate=2e-4,             # 峰值学习率
    weight_decay=0.01,               # 权值衰减
    adam_beta1=0.9,                  # Adam beta1
    adam_beta2=0.999,                # Adam beta2
    adam_epsilon=1e-8,               # Adam epsilon
    
    # 学习率调度
    lr_scheduler_type="cosine",      # "linear", "cosine", "constant", "constant_with_warmup"
    warmup_ratio=0.03,              # warmup 占总步数的比例
    warmup_steps=0,                  # 或指定具体的 warmup 步数
)

优化与精度参数

python
args = TrainingArguments(
    # 精度
    fp16=False,                      # FP16 混合精度
    bf16=True,                       # BF16 混合精度(推荐)
    tf32=None,                       # TF32(Ampere+ 自动启用)
    
    # 梯度
    max_grad_norm=1.0,               # 梯度裁剪阈值
    
    # 内存优化
    gradient_checkpointing=True,    # 梯度检查点(节省显存 ~30-50%)
    dataloader_num_workers=4,       # 数据加载进程数
    dataloader_pin_memory=True,      # 锁定页内存
    
    # 优化器
    optim="adamw_torch",            # "adamw_hf", "adamw_torch", "adafactor", "adamw_bnb_8bit"
)

日志与保存参数

python
args = TrainingArguments(
    # 日志
    logging_steps=10,                # 每 N 步记录一次日志
    report_to="wandb",               # "wandb", "tensorboard", "none"
    run_name="my-experiment",
    
    # Checkpoint
    save_strategy="epoch",           # "no", "steps", "epoch"
    save_steps=500,                  # save_strategy="steps" 时使用
    save_total_limit=3,              # 最多保留几个 checkpoint
    
    # 验证
    eval_strategy="steps",           # "no", "steps", "epoch"
    eval_steps=100,                  # eval_strategy="steps" 时使用
    metric_for_best_model="eval_loss",
    greater_is_better=False,
    load_best_model_at_end=True,
)

分布式参数

python
args = TrainingArguments(
    # 分布式
    fsdp="",                        # FSDP 配置字符串或 JSON 路径
    deepspeed="",                   # DeepSpeed 配置文件路径
    ddp_find_unused_parameters=False,  # DDP: 是否查找未使用的参数
    
    # 其他
    local_rank=-1,                  # DDP rank(通常不需要手动设置)
    seed=42,                        # 随机种子
    data_seed=None,                 # 数据 shuffle 种子
)

这里有几个容易出错的细节值得特别说明:

optim 参数的选择"adamw_torch" 使用 PyTorch 原生的 AdamW 实现(推荐),"adamw_hf" 使用 HuggingFace 自己的实现(行为略有不同,某些旧模型可能需要)。"adamw_bnb_8bit" 来自 bitsandbytes 库,用于 8-bit 优化器状态以节省显存。"adafactor" 用于超大模型的低内存优化器。

save_strategy vs eval_strategy:这两个参数独立控制 checkpoint 保存和验证执行的频率。你可以每个 epoch 保存一次 checkpoint 但每 100 步做一次验证——这在需要频繁监控 val loss 但不想频繁写磁盘的场景下很有用。

bf16 vs fp16:和第 4 章讨论的一样,优先选择 bf16=True(如果你的 GPU 支持)。它会自动处理所有混合精度的细节,不需要你手动管理 GradScaler。

DataCollator 系列

HF Trainer 提供了几种内置的 DataCollator,针对不同的任务类型做了预配置:

DataCollatorForLanguageModeling — 语言模型训练专用:

python
from transformers import DataCollatorForLanguageModeling

collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=False,                     # False = 因果语言建模 (CLM), True = BERT式 MLM
    pad_to_multiple_of=8,          # padding 到 8 的倍数(提高 Tensor Core 效率)
    return_tensors="pt",
)

mlm=False 时,它基本上就是我们在第 2 章自己实现的 LLMDataCollator 的 HF 版本——做动态 padding、构造 attention_mask、处理 labels。当 mlm=True 时,它会随机 mask 掉一些 token 用于 BERT 式的掩码语言建模任务。

DataCollatorWithPadding — 最简单的 collator,只做 padding:

python
from transformers import DataCollatorWithPadding

collator = DataCollatorWithPadding(
    tokenizer=tokenizer,
    padding="longest",              # "longest" 或 "max_length"
    max_length=512,                 # padding="max_length" 时使用
    return_tensors="pt",
)

DataCollatorForSeq2Seq — 序列到序列任务(翻译、摘要等):

python
from transformers import DataCollatorForSeq2Seq

collator = DataCollatorForSeq2Seq(
    tokenizer=tokenizer,
    max_length=128,
    label_pad_token_id=-100,
    padding=True,
)

对于我们的 LLM 微调任务来说,DataCollatorForLanguageModeling(mlm=False) 几乎总是正确的选择。它自动处理的细节包括:

  • 动态 padding 到 batch 内最长序列
  • 正确构造 attention_mask(区分真实 token 和 padding)
  • labels 的 shift 操作(input_ids 右移一位作为 labels)
  • padding 位置的 label 设为 -100(ignore_index)

这意味着你在准备数据时只需要提供原始的文本或 token IDs,不需要像在第 2 章那样手动处理 input_ids/labels 的对应关系——DataCollator 会帮你搞定一切。

到这里,你已经能够用 HF Trainer 启动一个基本的 LLM 微调任务了。但这只是冰山一角——真正让现代 LLM 微调变得实用的是 LoRA(Low-Rank Adaptation) 技术。下一节我们将深入 LoRA 的数学原理和工程实践,理解为什么只用 0.5% 的可训练参数就能达到接近全量微调的效果。

基于 MIT 许可发布