第 8 章 微调文本分类实战
学习目标
- 独立完成一次完整的文本分类微调:数据 → 训练 → 评估 → 保存 → 预测
- 理解数据量、学习率、轮数对结果的影响
- 会用
id2label让推理输出可读 - 能识别「小样本实验」与「真实效果」的区别
8.1 任务设定
目标:SST-2 影评情感二分类(negative / positive)。模型:prajjwal1/bert-tiny(438 万参数,几分钟内跑完);数据:训练 2000 条、验证 400 条。
完整流程五步:
加载数据集 → 分词预处理 → 构造模型(带标签映射) → Trainer 训练评估 → 保存并预测8.2 数据与预处理
from datasets import load_dataset
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("google-bert/bert-base-uncased")
def tokenize(examples):
return tokenizer(examples["sentence"], truncation=True, max_length=64)
def prepare(split):
ds = load_dataset("nyu-mll/glue", "sst2", split=split)
return (ds.map(tokenize, batched=True, remove_columns=["sentence", "idx"])
.rename_column("label", "labels"))
train_set = prepare("train[:2000]")
eval_set = prepare("validation[:400]")
print("train:", len(train_set), "| eval:", len(eval_set))输出:
train: 2000 | eval: 400这一步完全复用第 6 章的写法:分词、删列、标签列改名。
8.3 构造模型与标签映射
from transformers import BertConfig, BertForSequenceClassification
config = BertConfig.from_pretrained(
"prajjwal1/bert-tiny",
num_labels=2,
id2label={0: "negative", 1: "positive"},
label2id={"negative": 0, "positive": 1},
)
model = BertForSequenceClassification.from_pretrained(
"prajjwal1/bert-tiny", config=config)
print("参数总数:", sum(p.numel() for p in model.parameters()))输出(LOAD REPORT 省略):
参数总数: 4386178加载时 LOAD REPORT 会显示 classifier.weight / classifier.bias 为 MISSING——分类头是新初始化的,这正是要训练的部分(第 3 章)。
8.4 训练
import numpy as np
from transformers import (TrainingArguments, Trainer,
DataCollatorWithPadding, set_seed)
from evaluate import load as load_metric
set_seed(42)
accuracy = load_metric("accuracy")
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = np.argmax(logits, axis=-1)
return accuracy.compute(predictions=preds, references=labels)
args = TrainingArguments(
output_dir="./results-sst2",
learning_rate=1e-4,
num_train_epochs=2,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
eval_strategy="epoch",
logging_strategy="steps",
logging_steps=20,
save_strategy="no",
report_to=[],
disable_tqdm=True,
fp16=True,
)
collator = DataCollatorWithPadding(tokenizer=tokenizer, return_tensors="pt")
trainer = Trainer(model=model, args=args,
train_dataset=train_set, eval_dataset=eval_set,
data_collator=collator, compute_metrics=compute_metrics)
trainer.train()训练日志(真实运行):
{'loss': '0.6905', 'grad_norm': '2.49', 'learning_rate': '9.24e-05', 'epoch': '0.16'}
{'loss': '0.6776', 'grad_norm': '2.503', 'learning_rate': '8.44e-05', 'epoch': '0.32'}
{'loss': '0.6679', 'grad_norm': '2.791', 'learning_rate': '7.64e-05', 'epoch': '0.48'}
{'loss': '0.6517', 'grad_norm': '1.732', 'learning_rate': '6.84e-05', 'epoch': '0.64'}
{'loss': '0.6578', 'grad_norm': '3.509', 'learning_rate': '6.04e-05', 'epoch': '0.8'}
{'loss': '0.6545', 'grad_norm': '5.742', 'learning_rate': '5.24e-05', 'epoch': '0.96'}
{'eval_loss': '0.6339', 'eval_accuracy': '0.655', 'epoch': '1'}
{'loss': '0.6305', 'grad_norm': '1.565', 'learning_rate': '4.44e-05', 'epoch': '1.12'}
{'loss': '0.6209', 'grad_norm': '2.268', 'learning_rate': '3.64e-05', 'epoch': '1.28'}
{'loss': '0.5851', 'grad_norm': '2.825', 'learning_rate': '2.84e-05', 'epoch': '1.44'}
{'loss': '0.5755', 'grad_norm': '3.991', 'learning_rate': '2.04e-05', 'epoch': '1.6'}
{'loss': '0.5679', 'grad_norm': '3.593', 'learning_rate': '1.24e-05', 'epoch': '1.76'}
{'loss': '0.5947', 'grad_norm': '2.468', 'learning_rate': '4.4e-06', 'epoch': '1.92'}
{'eval_loss': '0.5957', 'eval_accuracy': '0.695', 'epoch': '2'}对比第 7 章的 200 条实验:数据量 ×10 后,loss 从 0.69 稳步降到 0.57,eval_accuracy 从 0.48 升到 0.695。数据量对微调效果的影响比调参更直接。
8.5 评估
print(trainer.evaluate())输出:
{'eval_loss': 0.5957, 'eval_accuracy': 0.695, 'eval_runtime': 0.0943,
'eval_samples_per_second': 4243.2, 'eval_steps_per_second': 137.9, 'epoch': 2}0.695 是 400 条验证集上的准确率。这个数字对 438 万参数的玩具模型是合理的;换更大的模型(如真实 BERT-base)和数据全量,SST-2 可以达到 90%+。
8.6 保存与预测
trainer.save_model("./sst2-bert-tiny")
tokenizer.save_pretrained("./sst2-bert-tiny")目录内容:
sst2-bert-tiny/
├── config.json
├── model.safetensors
├── tokenizer.json
├── tokenizer_config.json
└── training_args.bin用 pipeline 加载并预测新句子:
from transformers import pipeline
classifier = pipeline("text-classification",
model="./sst2-bert-tiny",
tokenizer=tokenizer)
for text in ["This movie is absolutely wonderful and touching.",
"The plot is boring and the acting is terrible.",
"A brilliant story with great performances.",
"I fell asleep halfway through this film."]:
print(text, "->", classifier(text))输出(真实运行):
This movie is absolutely wonderful and touching. -> [{'label': 'positive', 'score': 0.7128987312316895}]
The plot is boring and the acting is terrible. -> [{'label': 'negative', 'score': 0.6286324262619019}]
A brilliant story with great performances. -> [{'label': 'positive', 'score': 0.7736154794692993}]
I fell asleep halfway through this film. -> [{'label': 'negative', 'score': 0.5772325396537781}]四条全对,输出是 positive/negative 而不是 LABEL_0/LABEL_1——这就是 8.3 里 id2label 的功劳。
8.7 如果不写 id2label 会怎样
对比实验(真实运行):同样训练,但 config 不传 id2label,保存后预测:
[{'label': 'LABEL_0', 'score': 0.6261881589889526}]模型预测本身没变,只是类别名退化成编号。id2label 不是模型能力,而是「输出可读性」配置——但它是专业模型的基本素养。
8.8 参数怎么调:一次真实对照
同一份 2000 条数据,只改超参数:
| 配置 | eval_accuracy |
|---|---|
| lr=5e-5, bs=16, 1 epoch | 0.535 |
| lr=1e-4, bs=32, 2 epochs | 0.665 |
| lr=1e-4, bs=16, 2 epochs | 0.695 |
结论:学习率与训练步数不足时,模型几乎没有学习(0.535 接近随机);给足训练后明显提升。调参时一次只改一个变量,并固定种子,才能归因。
动手实践
- 把 8.4 的
learning_rate改成5e-5和2e-4,各跑一次,比较 eval_accuracy。 - 在
compute_metrics里加f1,输出两个指标。 - 用你找到的中文情感模型重复本章流程,输入 3 句中文影评。
常见错误
错误 1:训练集与验证集泄露。
prepare("train[:2000]") 和 prepare("validation[:400]") 来自不同 split。如果都用 train[:N] 训练又评估,数字会虚高。
错误 2:小模型 + 小数据 + 少步数,然后迷信结果。
第 7 章的 0.48 和本章的 0.695 都是真实的,区别只在资源投入。报告效果时务必写清数据规模与超参数。
错误 3:保存时漏掉 tokenizer。
只 trainer.save_model() 不保存 tokenizer,pipeline 加载时会匹配失败。两个都要保存。
章末练习
基础
- 完整复现 8.4 的训练,记录你的 eval_accuracy。
- 用保存的模型预测 5 条新句子,其中故意包含 1 条中性句子,观察模型如何「硬选」。
提高
- 在 2000 条数据上对比
num_train_epochs=1与3,记录 loss/eval_accuracy 变化,解释过拟合迹象。 - 统计验证集上预测错误的样本,各找出 3 条 false positive 与 false negative,总结模型犯错规律。
挑战
- 用
prajjwal1/bert-medium替换 bert-tiny 前,先查清它的真实参数量与显存需求;跑一轮完整微调,对比 bert-tiny 的准确率与耗时,写成实验记录。
章末自测
- 本章使用的模型与数据集分别是什么?
id2label的作用是什么?不写会怎样?- 8.4 中
learning_rate=1e-4相比默认5e-5的效果如何? - 判断:验证集应该从训练集里切,而不是用数据集自带的 validation split。
- 保存模型时需要同时保存哪些文件?
- 小样本实验中 eval_accuracy 不可信的原因是什么?
