全参数微调的问题
微调一个 7B 参数的模型,如果更新全部参数:
- 参数量:7,000,000,000 个 float16 参数
- 存储:约 14 GB(仅参数本身)
- 训练时还需存储梯度 + 优化器状态(Adam 需要 3× 参数量)
- 总显存需求:约 112 GB,需要多张 A100
LoRA(Low-Rank Adaptation,Hu et al. 2021)提供了一个优雅的解决方案:不改变原始权重,只训练两个小矩阵。
低秩分解的数学直觉
假设我们要更新一个权重矩阵 W ∈ ℝ^(d×k)。全参数微调会直接更新这个矩阵。LoRA 的做法是:
全参数微调:
W_new = W + ΔW
其中 ΔW ∈ ℝ^(d×k),参数量 = d × k
LoRA 微调:
W_new = W + ΔW = W + B × A
其中 A ∈ ℝ^(r×k),B ∈ ℝ^(d×r),r << min(d,k)
参数量 = r×k + d×r = r(d+k) << d×k
示例(d=4096, k=4096, r=16):
全参数:4096 × 4096 = 16,777,216 参数
LoRA: 16 × (4096+4096) = 131,072 参数
节省比例:99.2%!
低秩假设(Low-Rank Hypothesis)
论文中的关键假设:预训练模型已经具备了大量知识,微调时的权重变化 ΔW 内在维度很低(低秩的)。这意味着用低秩矩阵来近似 ΔW 是合理的。
初始化策略
训练开始时,A 用随机高斯初始化,B 用零初始化。这确保了初始的 ΔW = B×A = 0,即训练开始时 LoRA 对模型没有任何影响,从原始预训练权重出发。
缩放因子 α/r
实际应用中,ΔW 乘以缩放因子 α/r。这个设计让 α 成为一个类似学习率的超参,便于在不同 rank 之间迁移超参设置。
推理时的合并
训练完成后,LoRA 权重可以合并进原始权重,推理时没有额外开销:
# 训练时:W_effective = W + (alpha/r) * B @ A
# 推理前合并:
W_merged = W + (alpha / r) * B @ A
# 合并后的模型与原始模型推理速度完全相同
关键超参数详解
rank(r)— 最重要的超参
rank 决定了 LoRA 矩阵的"容量":
| rank 值 | 参数量(7B 模型) | 适用场景 |
|---|---|---|
| r = 4 | ~3M | 简单格式/风格调整 |
| r = 8 | ~6M | 多数指令微调任务(默认推荐) |
| r = 16 | ~13M | 专业领域知识注入 |
| r = 64 | ~52M | 复杂任务,接近全参数效果 |
| r = 128+ | ~104M+ | 极少需要,通常过拟合 |
alpha(α)— 缩放学习率
经验法则:alpha = 2 × rank。固定 alpha = 16(不随 rank 变化)也是常见做法。如果你不确定,先用 alpha = rank。
target_modules — 应用到哪些层
from peft import LoraConfig
# 保守选择(只有 attention 的 q/v 投影)
config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"], # 原始 LoRA 论文做法
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 激进选择(所有线性层,效果通常更好)
config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj", # attention
"gate_proj", "up_proj", "down_proj" # FFN
],
lora_dropout=0.0,
bias="none",
task_type="CAUSAL_LM"
)
target_modules 选择经验
对于大多数任务,同时训练 attention 和 FFN 层(7个模块)比只训练 q/v 效果好 10-20%。代价是参数量增加约 3.5 倍,显存增加约 1GB。
完整 LoRA 训练代码
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
# 1. 加载基座模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Meta-Llama-3-8B-Instruct",
torch_dtype="float16",
device_map="auto"
)
# 2. 应用 LoRA 配置
lora_config = LoraConfig(
r=16, lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 41,943,040 || all params: 8,072,495,104 || trainable%: 0.52%
# 3. 训练
trainer = SFTTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
args=TrainingArguments(
output_dir="./lora-output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
warmup_ratio=0.05,
lr_scheduler_type="cosine",
logging_steps=10,
evaluation_strategy="steps",
eval_steps=100,
save_steps=500,
)
)
trainer.train()
本章小结
LoRA 通过低秩分解把微调参数量从亿级降到百万级。关键超参:r 控制容量(推荐 8-16),alpha 控制缩放(推荐 = r 或 2r),target_modules 覆盖所有线性层效果最好。下一章引入 4-bit 量化,进一步降低显存。