Skip to content

第 10 章 PEFT 与 LoRA ​

学习目标 ​

  • 理解全量微调与参数高效微调的区别
  • 掌握 LoraConfig 与 get_peft_model
  • 会训练、保存、加载 LoRA adapter
  • 理解 LoRA 在小模型与大模型上的适用性差异

10.1 为什么需要参数高效微调 ​

全量微调(第 8 章)更新所有参数:10 亿参数模型要存 10 亿份梯度与优化器状态,显存开销巨大。参数高效微调(PEFT, Parameter-Efficient Fine-Tuning) 只训练一小部分参数,冻结其余部分,效果接近全量微调。

LoRA(Low-Rank Adaptation) 是其中最流行的方法:不直接更新权重矩阵 W,而是学习两个小矩阵 A、B,用 W + A×B 表示增量。增量矩阵的秩 r 很小(如 8),参数量只有原来的几百分之一。

10.2 用 peft 给模型加 LoRA ​

python
from transformers import BertConfig, BertForSequenceClassification
from peft import LoraConfig, get_peft_model, TaskType

config = BertConfig.from_pretrained("prajjwal1/bert-tiny", num_labels=2)
base = BertForSequenceClassification.from_pretrained(
    "prajjwal1/bert-tiny", config=config)
print("基座参数:", sum(p.numel() for p in base.parameters()))

lora_config = LoraConfig(
    task_type=TaskType.SEQ_CLS,   # 任务类型:序列分类
    r=8,                          # 低秩矩阵的秩
    lora_alpha=16,                # 缩放系数
    target_modules=["query", "value"],  # 对哪些层加 LoRA
    lora_dropout=0.1,
)

model = get_peft_model(base, lora_config)
model.print_trainable_parameters()

输出:

基座参数: 4386178
trainable params: 8,450 || all params: 4,394,628 || trainable%: 0.1923

只训练 8450 个参数(占 0.1923%),其余全部冻结。target_modules 必须写对层名:这里 query/value 是 BERT 注意力里的 Q、V 投影层。

10.3 用 Trainer 训练 LoRA 模型 ​

get_peft_model 返回的模型可以直接交给 Trainer,用法与第 8 章完全相同:

python
from transformers import (TrainingArguments, Trainer,
                          DataCollatorWithPadding, set_seed)
from datasets import load_dataset
from transformers import AutoTokenizer
from evaluate import load as load_metric
import numpy as np

set_seed(42)
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]")

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-lora",
    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=40,
    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()
print(trainer.evaluate())

训练与评估输出(真实运行):

{'loss': '0.6893', 'grad_norm': '2.729', 'learning_rate': '8.44e-05', 'epoch': '0.32'}
{'loss': '0.6842', 'grad_norm': '1.237', 'learning_rate': '6.84e-05', 'epoch': '0.64'}
{'loss': '0.6855', 'grad_norm': '3.665', 'learning_rate': '5.24e-05', 'epoch': '0.96'}
{'eval_loss': '0.6859', 'eval_accuracy': '0.5575', 'epoch': '1'}
{'loss': '0.6876', 'grad_norm': '1.453', 'learning_rate': '3.64e-05', 'epoch': '1.28'}
{'loss': '0.6753', 'grad_norm': '2.351', 'learning_rate': '2.04e-05', 'epoch': '1.6'}
{'loss': '0.6833', 'grad_norm': '0.916', 'learning_rate': '4.4e-06', 'epoch': '1.92'}
{'eval_loss': '0.6851', 'eval_accuracy': '0.56', 'epoch': '2'}
{'eval_loss': 0.6851, 'eval_accuracy': 0.56, ...}

对照第 8 章的全量微调:eval_accuracy 0.56 vs 0.695。LoRA 在小模型上的效果通常弱于全量微调——但在 7B、70B 参数的大模型上,LoRA 的省显存优势压倒精度差距,是事实上的标准做法。

10.4 保存与加载 adapter ​

只保存 adapter(不保存基座),文件极小:

python
model.save_pretrained("./my-lora-adapter")

目录内容:

my-lora-adapter/
├── README.md
├── adapter_config.json       # LoRA 配置:r、alpha、target_modules
└── adapter_model.safetensors # 低秩矩阵权重(几 KB ~ 几 MB)

加载:先加载基座,再挂 adapter:

python
from peft import PeftModel

base = BertForSequenceClassification.from_pretrained(
    "prajjwal1/bert-tiny", config=config)
model = PeftModel.from_pretrained(base, "./my-lora-adapter")
print(type(model).__name__)

输出:

PeftModelForSequenceClassification

要点:

  • adapter 与基座解耦:同一个基座可以挂不同 adapter(多任务切换),只需换 PeftModel.from_pretrained 的目录。
  • 加载后仍可训练或推理;推理时 LoRA 增量自动合并进前向。

10.5 常见变体与选型 ​

方法思想适用
LoRA低秩增量矩阵通用,首选
QLoRA量化基座 + LoRA显存极小(如单卡跑 7B)
DoRA权重分解精度略高,实现略复杂

QLoRA 需要 bitsandbytes 与量化后的基座,本书不展开运行示例;原理上就是「先把基座压成 4/8 位,再在其上加 LoRA」。

动手实践 ​

  1. 把 r 从 8 改成 2 和 32,打印三者的 trainable% 与 eval_accuracy,画出参数量-效果曲线。
  2. 把 target_modules 改成 ["query"](只挂 Q),对比完整 ["query", "value"]。
  3. 保存两个不同 r 的 adapter,用同一个基座依次加载,验证可以切换。

常见错误 ​

错误 1:target_modules 层名写错。

Target modules ['qq'] not found in the base model...

先打印模型结构,确认层名:

python
for name, _ in base.named_parameters():
    print(name)

错误 2:直接对 peft 模型再套一层 get_peft_model。

重复包装会报错或产生嵌套 adapter。要换配置就新建一个基座再包装。

错误 3:只保存 adapter,却删了基座。

adapter 是增量,没有基座权重就无法加载。adapter 要配合「基座模型 id + adapter 目录」一起分发。

章末练习 ​

基础

  1. 解释 LoRA 中 r、lora_alpha、target_modules 的含义。
  2. 写出保存与加载 adapter 的两段代码。

提高

  1. 在 SST-2 上对比 LoRA(r=8)与全量微调(相同数据/轮数/种子),记录参数量与准确率,写一段结论。
  2. 验证 print_trainable_parameters() 输出的数字:手动统计 model.named_parameters() 中 requires_grad=True 的参数总数。

挑战

  1. 用同一个基座训练两个不同任务的 adapter(如 SST-2 情感与一个自己构造的二分类),演示「同基座、双 adapter 切换」的工作流。

章末自测 ​

  1. LoRA 的核心思想是什么?
  2. get_peft_model 返回的模型可以直接交给 Trainer 吗?
  3. adapter 保存目录里至少包含哪两个文件?
  4. 加载 adapter 前必须先加载什么?
  5. 判断:LoRA 在任意规模模型上的效果都优于全量微调。
  6. target_modules 的作用是什么?