Transformers 微调预训练模型实战:数据集处理、Trainer 工作流与原生 PyTorch 训练循环
本文基于 Transformers 仓库的官方教程文档 training.md 展开,系统讲解如何把一个预训练语言模型微调(fine-tuning)到特定任务上:从 Yelp 评论分类数据集的加载与预处理,到使用 Trainer 一行命令完成训练与评估,再到完全手写 PyTorch 原生训练循环的完整实现。读完后,你将掌握微调的标准工作流、TrainingArguments 与 get_scheduler 等关键 API 的实际行为,并能独立搭建不依赖 Trainer 的自定义训练管线。
为什么选择微调而不是从头训练
文档开篇即点明了微调(fine-tuning)的核心价值:
- 降低计算成本:直接复用预训练权重,避免从头训练的巨大开销;
- 减小环境影响:更少的训练算力意味着更低的能耗;
- 快速用上最强基座:无需自己训练,即可使用社区中最新的预训练模型。
使用预训练模型时,你在自己的任务数据集上继续训练它,这个过程就叫"微调"(Arabic 文档中的 الضبط الدقيق)。Transformers 提供了数千个预训练模型供选择,而教程给出的三条路线是:
- 使用
Trainer微调(本文重点); - 在 TensorFlow 中使用 Keras 微调;
- 在原生 PyTorch 中微调(本文重点)。
下面的两条 PyTorch 路线以 Yelp 评论五分类 为贯穿案例,代码均可直接复制运行。
数据集准备:加载与批处理分词
加载 Yelp 评论数据集
微调的第一步是准备训练数据。官方教程使用的是 Yelp Reviews 数据集(load_dataset("yelp_review_full")),其每条样本包含一个 text 字段和一个 label 字段(0–4 共五个星级):
>>> from datasets import load_dataset
>>> dataset = load_dataset("yelp_review_full")
>>> dataset["train"][100]
{'label': 0,
'text': 'My expectations for McDonalds are t rarely high. But for one to still fail so spectacularly...'}
用 map 一步完成批处理分词
模型无法直接消费原始文本,需要 tokenizer 处理,并配合填充(padding)与截断(truncation)策略应对变长序列。教程使用 Datasets 库的 map 方法,把分词函数一次性应用到整个数据集:
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-cased")
>>> def tokenize_function(examples):
... return tokenizer(examples["text"], padding="max_length", truncation=True)
>>> tokenized_datasets = dataset.map(tokenize_function, batched=True)
其中 padding="max_length" 会把所有序列填充到模型的最大长度,truncation=True 负责截断超长序列,batched=True 则让 tokenize_function 接收批量字段(字典而非单条样本),显著提升处理速度。
切出小子集以加速实验
Yelp 全量数据训练耗时较长,教程建议先切出 1000 条样本做快速验证:
>>> small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
>>> small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))
注意固定 seed=42 保证结果可复现。
路线一:使用 PyTorch Trainer 微调
Trainer 是 Transformers 官方封装的训练类,免去了手写训练循环,开箱支持日志记录(logging)、梯度累积(gradient accumulation)、混合精度(mixed precision)等大量训练特性。
加载带分类头的模型
根据 Yelp 数据集的五个星级标签,加载 BERT 基座并指定分类数:
>>> from transformers import AutoModelForSequenceClassification
>>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-cased", num_labels=5)
这里会看到一条关于"部分预训练权重未被使用、部分权重被随机初始化"的警告,这是完全正常的现象:BERT 预训练模型的原始任务头被丢弃,替换为一个随机初始化的序列分类头(num_labels=5)。后续微调会把这个新头与预训练骨干的知识对齐,这正是微调"迁移知识"的机制所在。
训练超参数:TrainingArguments
TrainingArguments 是一个 dataclass,集中管理所有可调整的训练超参数与激活项(输出目录、学习率、批大小、评估策略等)。最小配置只需指定检查点保存位置:
>>> from transformers import TrainingArguments
>>> training_args = TrainingArguments(output_dir="test_trainer")
从源码结构看,TrainingArguments 在 training_args.py 中以 dataclass field 逐一声明默认值,例如评估相关的 eval_strategy 默认为 "no"(不评估),可选 'no'、'steps'、'epoch' 三种取值,见 training_args.py#L1081-L1084。其余超参数(learning_rate、per_device_train_batch_size、num_train_epochs 等)均保留默认值即可启动训练,实际使用时可按需覆盖。
评估指标:logits 转预测再算 accuracy
Trainer 不会自动评估模型,需要你传入一个指标函数。教程使用 Evaluate 库的 accuracy 指标,并强调了一个关键点:所有 Transformers 模型返回的都是原始 logits,必须先转成最终预测:
>>> import numpy as np
>>> import evaluate
>>> metric = evaluate.load("accuracy")
>>> def compute_metrics(eval_pred):
... logits, labels = eval_pred
... predictions = np.argmax(logits, axis=-1)
... return metric.compute(predictions=predictions, references=labels)
若希望每个训练 epoch 结束都看到评估指标,在 TrainingArguments 中打开 eval_strategy:
>>> from transformers import TrainingArguments, Trainer
>>> training_args = TrainingArguments(output_dir="test_trainer", eval_strategy="epoch")
这与源码中的定义一致:eval_strategy 取 "epoch" 时,Trainer 会在每个 epoch 末自动对 eval_dataset 跑一轮评估,并把 compute_metrics 的返回值记入日志。
创建 Trainer 并启动训练
把模型、训练参数、数据与评估函数组装进 Trainer:
>>> trainer = Trainer(
... model=model,
... args=training_args,
... train_dataset=small_train_dataset,
... eval_dataset=small_eval_dataset,
... compute_metrics=compute_metrics,
... )
>>> trainer.train()
trainer.train() 是整条训练管线的总入口。从源码看,Trainer.train 定义了 resume_from_checkpoint、超参数搜索 trial 等参数,内部负责:随机种子设置、梯度检查点激活、优化器/调度器构建、设备与分布式后端协调、检查点保存与评估循环调度——这些在教程视角下都被这一行调用完全封装。
路线二:原生 PyTorch 训练循环
对于偏好完全掌控训练过程的用户,Transformers 模型同样可以直接嵌入手写的 PyTorch 循环。这一路线的所有要素(DataLoader、优化器、调度器、训练循环)都是标准 PyTorch 写法。
释放内存并手工整理数据格式
先清理上一路线的对象释放显存:
del model
del trainer
torch.cuda.empty_cache()
接着对 tokenized_datasets 做三步整理:
-
移除
text列——模型不接受原始文本输入:>>> tokenized_datasets = tokenized_datasets.remove_columns(["text"]) -
重命名
label为labels——模型期望的输入参数名是labels:>>> tokenized_datasets = tokenized_datasets.rename_column("label", "labels") -
设置数据集格式为 torch——让取出的样本直接是 PyTorch 张量而非 Python 列表:
>>> tokenized_datasets.set_format("torch")
之后同样切出 1000 条的小数据集加速实验:
>>> small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
>>> small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))
DataLoader 与设备
为训练/评估集创建 DataLoader,批量大小为 8,训练集打乱顺序:
>>> from torch.utils.data import DataLoader
>>> train_dataloader = DataLoader(small_train_dataset, shuffle=True, batch_size=8)
>>> eval_dataloader = DataLoader(small_eval_dataset, batch_size=8)
重新加载模型(同样指定 num_labels=5):
>>> from transformers import AutoModelForSequenceClassification
>>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-cased", num_labels=5)
优化器与学习率调度器
使用 PyTorch 的 AdamW,学习率 5e-5(BERT 微调的典型取值):
>>> from torch.optim import AdamW
>>> optimizer = AdamW(model.parameters(), lr=5e-5)
再创建与 Trainer 默认行为一致的学习率调度器:
>>> from transformers import get_scheduler
>>> num_epochs = 3
>>> num_training_steps = num_epochs * len(train_dataloader)
>>> lr_scheduler = get_scheduler(
... name="linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps
... )
get_scheduler 是 Transformers 提供的调度器统一 API,见 optimization.py#L960-L984。从源码签名看,它接受调度器名称(如 linear)、优化器、num_warmup_steps 与 num_training_steps 四个核心参数;除 constant 等少数调度器外,其余类型(含 linear)都必须提供 num_warmup_steps,缺失时函数会抛出 ValueError——教程中显式传 num_warmup_steps=0 正是满足这一约束。
最后选择训练设备,有 GPU 时务必使用 GPU,否则 CPU 上训练可能需要数小时而非几分钟:
>>> import torch
>>> device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
>>> model.to(device)
训练循环
用 tqdm 添加进度条,核心循环是教科书式的"前向-反向-更新":
>>> from tqdm.auto import tqdm
>>> progress_bar = tqdm(range(num_training_steps))
>>> model.train()
>>> for epoch in range(num_epochs):
... for batch in train_dataloader:
... batch = {k: v.to(device) for k, v in batch.items()}
... outputs = model(**batch)
... loss = outputs.loss
... loss.backward()
... optimizer.step()
... lr_scheduler.step()
... optimizer.zero_grad()
... progress_bar.update(1)
几个值得注意的细节:
model(**batch)把整理过的数据字典(input_ids、attention_mask、labels)解包为模型输入,模型内部会利用labels自动计算并返回outputs.loss,无需手写损失函数;- 每个 batch 内严格按
loss.backward()→optimizer.step()→lr_scheduler.step()→optimizer.zero_grad()的顺序执行; - 外层
for epoch与内层for batch的双层结构使progress_bar按总步数平滑推进。
评估:用 add_batch 累积后一次性计算
与 Trainer 中"每 epoch 末计算指标"不同,手写循环的惯用做法是用 metric.add_batch 逐批累积预测与标签,循环结束后一次性 compute():
>>> import evaluate
>>> metric = evaluate.load("accuracy")
>>> model.eval()
>>> for batch in eval_dataloader:
... batch = {k: v.to(device) for k, v in batch.items()}
... with torch.no_grad():
... outputs = model(**batch)
... logits = outputs.logits
... predictions = torch.argmax(logits, dim=-1)
... metric.add_batch(predictions=predictions, references=batch["labels"])
>>> metric.compute()
评估时记得 model.eval() 切换模型模式(关闭 dropout 等训练态行为),并用 torch.no_grad() 关闭梯度计算以节省显存。
延伸资源
- 仓库中的 examples/pytorch/text-classification 目录提供了完整可运行的分类脚本:run_classification.py(基于
Trainer)与 run_glue_no_trainer.py(基于Accelerator的手写循环,可对照本文原生 PyTorch 路线查看get_scheduler、DataCollatorWithPadding等 API 在完整脚本中的用法),以及对应的 README。 - examples/pytorch 下还有语言建模、文本生成、问答等其他任务的训练脚本,可作为迁移本文流程到其他任务的参考。
- 若要在 TensorFlow/Keras 路线上微调,可结合仓库文档树中的 TensorFlow 训练章节(英文版训练教程)查看对应写法。
小结
| 环节 | Trainer 路线 | 原生 PyTorch 路线 |
|---|---|---|
| 数据整理 | 自动(DataCollator 处理填充/格式) | 手动 remove_columns / rename_column / set_format |
| 超参数 | TrainingArguments dataclass |
自行管理各组件 |
| 优化器/调度器 | 内部自动构建 | AdamW + get_scheduler 手工创建 |
| 评估 | compute_metrics + eval_strategy |
metric.add_batch 累积后 compute |
| 启动成本 | 一行 trainer.train() |
完整训练循环代码 |
两条路线共享完全相同的数据预处理逻辑与模型加载方式,选择取决于你对训练过程控制粒度的需求:追求效率与特性(日志、断点续训、分布式)选 Trainer;需要深度定制(特殊损失、自定义梯度处理、非标准数据流)时选原生 PyTorch 循环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00