第 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
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 章完全相同:
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(不保存基座),文件极小:
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:
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」。
动手实践
- 把
r从 8 改成 2 和 32,打印三者的trainable%与 eval_accuracy,画出参数量-效果曲线。 - 把
target_modules改成["query"](只挂 Q),对比完整["query", "value"]。 - 保存两个不同 r 的 adapter,用同一个基座依次加载,验证可以切换。
常见错误
错误 1:target_modules 层名写错。
Target modules ['qq'] not found in the base model...先打印模型结构,确认层名:
for name, _ in base.named_parameters():
print(name)错误 2:直接对 peft 模型再套一层 get_peft_model。
重复包装会报错或产生嵌套 adapter。要换配置就新建一个基座再包装。
错误 3:只保存 adapter,却删了基座。
adapter 是增量,没有基座权重就无法加载。adapter 要配合「基座模型 id + adapter 目录」一起分发。
章末练习
基础
- 解释 LoRA 中
r、lora_alpha、target_modules的含义。 - 写出保存与加载 adapter 的两段代码。
提高
- 在 SST-2 上对比 LoRA(r=8)与全量微调(相同数据/轮数/种子),记录参数量与准确率,写一段结论。
- 验证
print_trainable_parameters()输出的数字:手动统计model.named_parameters()中requires_grad=True的参数总数。
挑战
- 用同一个基座训练两个不同任务的 adapter(如 SST-2 情感与一个自己构造的二分类),演示「同基座、双 adapter 切换」的工作流。
章末自测
- LoRA 的核心思想是什么?
get_peft_model返回的模型可以直接交给 Trainer 吗?- adapter 保存目录里至少包含哪两个文件?
- 加载 adapter 前必须先加载什么?
- 判断:LoRA 在任意规模模型上的效果都优于全量微调。
target_modules的作用是什么?
