全书导航
大模型之路:从图灵、感知机到 ChatGPT · 卷 4

第 32 章:微调:把通才变成专才

本章问题:已经有了一个强大的基础模型——如何让它在我自己的特殊任务上表现最佳?


32.1 通才的局限

预训练出的基础模型是一个通才——它读过几十亿页文字,知道世界的大致轮廓。但当你面对一个具体的业务问题时——比如"帮我分类这些客服投诉"——模型的通用能力不够精确。

你的客户投诉里可能有特定的产品名("VX-9000 系列的显影液")、行业的特有缩写("SOP"在制造业和医疗中的含义完全不同)、或者你公司内部的特定格式要求。这些信息不在预训练语料中——模型不可能在"预测下一个词"的阶段自动学到你们公司的产品编号体系。

你需要让模型在已经具备了通用语言理解和生成能力的前提下,进一步学习你特定的领域数据和任务需求。这就是微调(fine-tuning)。

微调 = 拿一个预训练好的通用模型 + 在你自己的标注数据上继续训练少量步数。


32.2 微调为什么有效——冻结的底座和微调的表面

微调能够用极少数据达到极好效果的根本原因,可以通过一个类比理解:

一个精通英文的人要学写法文——需要大量时间。但一个已经精通意大利文和西班牙文的人去学写法文——可能几周就能基本掌握,因为她已经掌握了拉丁语系的语法结构、动词人称变位等"通用基底"。

基础模型在预训练阶段已经学到了:

  • 语言的基本句法结构
  • 大量世界知识和常识
  • 基本推理模式
  • 通用的文本生成能力

当你微调时——你不是在教模型从零学语言。你是在它已经具备的完整语言能力上,教会它在这个具体任务上的映射规则。因此只需少量标注数据——几百到几千条——经常就能得到很好的效果。

在这个框架下看,过去几年的各种"微调变体"其实都在处理同一个问题:在给定的计算和数据预算下,最优地调整模型的哪部分权重?

  • 全参数微调:更新所有参数。效果最强——但需要存整个模型的梯度,GPU 内存消耗大。
  • 冻结特征提取器:只微调最后一层(分类头等)。训练极快但通常效果差于全参数微调——因为高层语义特征也需要一些调整来适配新任务。
  • 部分层微调:只微调最后几层。在全参数和特征提取之间有显著的折中——很多实际应用中只微调最后 2-4 层的效果已经非常接近全参数微调。

32.3 微调数据格式:回到指令三元组

微调数据的格式和指令微调(第 29 章)完全相同——指令(任务描述)+ 输入(材料)+ 输出(期望答案):

json
[  {    "instruction": "将以下客户投诉分为三类:产品质量、物流配送、客服态度。",    "input": "我买的手机屏幕上有两条划痕,包装完好但明显是出厂问题。",    "output": "产品质量"  },  {    "instruction": "将以下客户投诉分为三类:产品质量、物流配送、客服态度。",    "input": "快递等了八天才到,比我预期晚了一周——东西到了我都不需要了。",    "output": "物流配送"  },  {    "instruction": "将以下客户投诉分为三类:产品质量、物流配送、客服态度。",    "input": "打客服电话等了半小时还没人接,气得我直接想退单。",    "output": "客服态度"  }]

每条数据是同一个任务的不同实例。微调时——模型在这个任务上反复看、反复调整权重——最终学会"看到投诉→输出正确类别"的映射,同时保持它预训练学到的所有通用语言能力。


32.4 微调的关键工程直觉

学习率要小。预训练用大的学习率(模型从零开始,需要大步快走)。微调用小的学习率——通常是预训练学习率的 1/10 到 1/100——因为你只是想在"已有知识"的基础上做小的修正,而不是全盘改权重。

不要把预训练知识洗掉。微调一个 epoch 太少,模型还没学会你的任务模式。微调 50 个 epoch 太多——模型会过拟合到你的小数据集上,开始"死记"少数样本,导致泛化性能下降。这就是灾难性遗忘——模型为了在你提供的标注上继续降低损失,开始逐步覆盖预训练阶段学到的通用能力。在教科书例中观察到——微调太久后模型在你的任务上表现完美,但丧失了基本常识推理、语言连贯性和事实回忆能力。

数据质量 >> 数据量。在微调场景下,50 条高质量的、干净标注、覆盖真实边缘情况的样本经常比 5000 条嘈杂的自动标注数据更好。这不是在预训练——你不需要"尽可能多的数据去覆盖语言统计分布"。你需要精确的信号——每条错误的标注都在把模型推向错误的方向。

monitor 验证集 loss。如果你看到训练 loss 在降但验证 loss 在升——停下来,你已经过拟合了,模型已经进入灾难性遗忘的轨道。大部分成功的微调在 1-5 个 epoch 之间停止。


32.5 最小代码:用 HuggingFace 微调一个中文分类模型

以下约 40 行代码,演示微调一个预训练 BERT 做中文文本分类:

python
from transformers import (    AutoTokenizer, AutoModelForSequenceClassification,    Trainer, TrainingArguments)from datasets import Dataset# 1. 加载预训练模型和分词器model_name = "bert-base-chinese"tokenizer = AutoTokenizer.from_pretrained(model_name)model = AutoModelForSequenceClassification.from_pretrained(    model_name, num_labels=3  # 三分类:质量/物流/客服)# 2. 准备数据data = {    "text": [        "手机屏幕有划痕,包装完好但明显是出厂问题。",        "快递等了八天才到,比我预期晚了一周。",        "打客服电话等了半小时还没人接。",        "充电三次电池就鼓包了,质量堪忧。",        "物流信息卡在出库三天没有更新,着急。",    ],    "label": [0, 1, 2, 0, 1],  # 0=质量, 1=物流, 2=客服}dataset = Dataset.from_dict(data)dataset = dataset.map(    lambda x: tokenizer(x["text"], truncation=True, padding="max_length"),    batched=True,)# 3. 训练参数与微调training_args = TrainingArguments(    output_dir="./results",    num_train_epochs=3,    per_device_train_batch_size=4,    learning_rate=2e-5,          # 关键:小学习率    logging_steps=1,    save_strategy="no",)trainer = Trainer(    model=model,    args=training_args,    train_dataset=dataset,)trainer.train()# 4. 推理model.eval()test = tokenizer("耳机用了两天左耳就没声音了", return_tensors="pt")output = model(**test)pred = output.logits.argmax(dim=-1).item()labels = {0: "产品质量", 1: "物流配送", 2: "客服态度"}print(f"预测: {labels[pred]}")

核心观察:

  • num_labels=3 —— 在预训练 BERT 的基础上加了一个新的分类头(随机初始化)。微调会同时更新这个分类头 BERT 的所有 Transformer 层。
  • learning_rate=2e-5 —— 远低于预训练时用的学习率。大型语言模型的微调通常的学习率在 1e-5 到 5e-5 之间。
  • 5 条训练数据显然不够实用——你需要至少 50-100 条来获得可靠的分类。但这段代码的接口对于 500 条和 5 条是一样的。

32.6 全参数微调的代价

对于像 GPT-3(175B 参数)或 LLaMA-70B 这样的模型——全参数微调意味着你需要存储所有 700 亿参数的梯度、优化器状态和中间激活值。对一个 175B 的模型做全参数微调——需要大约 1400 GB GPU 内存。即使你有 8 个 A100(每个 80 GB),也不够在大模型上做全参数微调。

这就是为什么在实际产业应用中,全参数微调往往是"理论上最优但工程上不可行"的——它更适合中小模型(10B 参数以下)或有大量 GPU 预算的团队。对于大部分想要微调大模型的开发者和公司——需要更轻量、更省显存的方法。

但核心训练逻辑没有变:你依然在做同一种事——把模型的通用能力通过少量有监督数据适配到你的具体任务上。只是实现方式需要调整到可以在消费级或单卡 GPU 上跑起来。

这就引出了下一章的核心问题:如何用极少的额外参数和极少的额外计算,达到接近全参数微调的效果?

下一章:LoRA——只训练"变化量"。

SECTION §02 · ENGAGE

Discussion

留言区 · GitHub-powered comments via Giscus