高效微调技术:LoRA与PEFT

2908 字
15 分钟
高效微调技术:LoRA与PEFT

大语言模型的全参数微调(Full Fine-tuning)面临着巨大的挑战:一个 7B 参数的模型,全量微调需要消耗超过 140GB 显存(以 FP32 精度计算),这对于大多数研究者和开发者来说都是难以承受的成本。

参数高效微调(Parameter-Efficient Fine-Tuning, PEFT) 技术应运而生。它们的核心思想是:冻结预训练模型的大部分参数,只训练少量额外参数或模型的一小部分,就能达到接近全参数微调的效果。

其中,LoRA(Low-Rank Adaptation) 因其简洁优雅的设计和卓越的性能,已经成为当前最流行的 PEFT 方法。本文将深入剖析 LoRA 的原理,并探讨 PEFT 生态系统中的其他重要方法。


一、为什么需要参数高效微调?#

1.1 全参数微调的困境#

计算成本高昂

  • LLaMA-7B 全量微调需要 ~140GB 显存(FP32)或 ~70GB(FP16)
  • GPT-3 175B 需要数千个 GPU 并行训练
  • 训练时间以天或周为单位

存储压力巨大

  • 每个下游任务都需要保存一份完整模型副本
  • 100 个任务 × 7B 模型 = 700B 参数的存储空间

灾难性遗忘(Catastrophic Forgetting)

  • 在新任务上微调可能破坏预训练知识
  • 需要精心设计训练策略平衡新旧知识

1.2 PEFT 的核心优势#

维度全参数微调PEFT(以LoRA为例)
可训练参数100%0.1% - 1%
显存占用140GB (7B模型)20GB
训练速度基准1.5x - 3x 加速
模型存储13GB/任务10MB/任务
多任务部署困难轻松切换

二、LoRA:低秩自适应的优雅设计#

2.1 核心思想#

LoRA 基于一个关键洞察:大型神经网络的权重更新往往是低秩的

对于预训练权重矩阵 W0Rd×kW_0 \in \mathbb{R}^{d \times k},其微调过程的更新量 ΔW\Delta W 可以分解为:

W=W0+ΔW=W0+BAW = W_0 + \Delta W = W_0 + BA

其中:

  • BRd×rB \in \mathbb{R}^{d \times r}, ARr×kA \in \mathbb{R}^{r \times k}
  • rmin(d,k)r \ll \min(d, k) 是秩(rank),典型值为 4-64
  • 原本需要训练 d×kd \times k 个参数,现在只需 (d+k)×r(d + k) \times r

参数量对比(以 Transformer 中的 WqW_q 矩阵为例):

  • 原始:4096×4096=16,777,2164096 \times 4096 = 16,777,216 参数
  • LoRA (r=8r=8):(4096+4096)×8=65,536(4096 + 4096) \times 8 = 65,536 参数
  • 压缩比:256:1

2.2 前向传播机制#

# 原始 Transformer 层
h = W_0 @ x # 标准线性变换
# LoRA 增强版
h = W_0 @ x + (B @ A) @ x # W_0冻结,只训练B和A
= W_0 @ x + B @ (A @ x) # 先算 A@x 更高效

关键设计细节

  1. 初始化AA 使用高斯初始化,BB 初始化为零
    • 保证训练开始时 ΔW=0\Delta W = 0,模型输出与原模型一致
  2. 缩放因子ΔW\Delta W 乘以 α/r\alpha / r
    • α\alpha 是可调超参数(通常等于 rr
    • 使得不同 rank 的学习率效果一致
  3. 应用位置:通常只对 Attention 层的 Wq,Wk,Wv,WoW_q, W_k, W_v, W_o 应用 LoRA

2.3 代码实现(从零开始)#

import torch
import torch.nn as nn
class LoRALayer(nn.Module):
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
self.rank = rank
self.alpha = alpha
# 冻结的预训练权重
self.weight = nn.Parameter(torch.randn(out_features, in_features))
self.weight.requires_grad = False
# 可训练的低秩矩阵
self.lora_A = nn.Parameter(torch.randn(rank, in_features))
self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
# 初始化
nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
def forward(self, x):
# 原始输出
result = F.linear(x, self.weight)
# LoRA 增量(带缩放)
lora_output = (x @ self.lora_A.T @ self.lora_B.T) * (self.alpha / self.rank)
return result + lora_output
# 应用到 Transformer Attention
class LoRAAttention(nn.Module):
def __init__(self, hidden_size, num_heads, rank=8):
super().__init__()
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
# 使用 LoRA 层替代标准线性层
self.q_proj = LoRALayer(hidden_size, hidden_size, rank)
self.k_proj = LoRALayer(hidden_size, hidden_size, rank)
self.v_proj = LoRALayer(hidden_size, hidden_size, rank)
self.o_proj = LoRALayer(hidden_size, hidden_size, rank)

2.4 训练技巧#

超参数选择

# rank 的经验法则
rank_settings = {
"简单任务(分类)": 4,
"中等任务(摘要、翻译)": 8-16,
"复杂任务(代码生成)": 32-64,
"领域适应(医疗、法律)": 64-128
}
# alpha 通常等于 rank,或设为 2*rank
alpha = rank * 1 # 保守设置
alpha = rank * 2 # 激进设置

学习率设置

  • LoRA 参数可以使用比全参数微调更高的学习率
  • 典型值:1e-43e-4(全参数微调通常是 1e-55e-5

训练稳定性

# 梯度裁剪(防止训练崩溃)
torch.nn.utils.clip_grad_norm_(lora_parameters, max_norm=1.0)
# 使用 warmup(前 3% 步数)
num_warmup_steps = int(0.03 * total_steps)
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps, total_steps
)

三、PEFT 方法大家族#

3.1 Adapter Tuning#

核心思想:在 Transformer 的每一层插入小型”适配器”模块。

class AdapterLayer(nn.Module):
def __init__(self, hidden_size, adapter_size=64):
super().__init__()
# 下投影(降维)
self.down_proj = nn.Linear(hidden_size, adapter_size)
# 上投影(升维)
self.up_proj = nn.Linear(adapter_size, hidden_size)
# 非线性激活
self.activation = nn.GELU()
def forward(self, x):
# Bottleneck 结构
residual = x
x = self.down_proj(x)
x = self.activation(x)
x = self.up_proj(x)
return x + residual # 残差连接
# 插入到 Transformer 层
class TransformerBlockWithAdapter(nn.Module):
def forward(self, x):
# 原始 Attention + FFN
x = x + self.attention(self.norm1(x))
x = x + self.adapter1(x) # Adapter 1
x = x + self.ffn(self.norm2(x))
x = x + self.adapter2(x) # Adapter 2
return x

优缺点

  • ✅ 可训练参数量少(0.5-3%)
  • ✅ 训练稳定,不易过拟合
  • ❌ 增加推理延迟(额外的前向传播)
  • ❌ 每层都需要插入,实现较复杂

3.2 Prefix Tuning#

核心思想:在输入序列前添加可训练的”虚拟 token”。

class PrefixTuning(nn.Module):
def __init__(self, num_layers, num_heads, head_dim, prefix_len=20):
super().__init__()
self.prefix_len = prefix_len
# 为每一层的 K 和 V 创建前缀
# Shape: [num_layers, 2, prefix_len, num_heads * head_dim]
self.prefix_tokens = nn.Parameter(
torch.randn(num_layers, 2, prefix_len, num_heads * head_dim)
)
def forward(self, layer_idx, key, value):
batch_size = key.size(0)
# 提取当前层的前缀
prefix_k, prefix_v = self.prefix_tokens[layer_idx]
# 扩展到 batch
prefix_k = prefix_k.unsqueeze(0).expand(batch_size, -1, -1)
prefix_v = prefix_v.unsqueeze(0).expand(batch_size, -1, -1)
# 拼接到 K, V 序列前
key = torch.cat([prefix_k, key], dim=1)
value = torch.cat([prefix_v, value], dim=1)
return key, value

实际效果

  • 相当于给模型”植入”了任务特定的隐式指令
  • 适合多任务学习(不同任务使用不同 prefix)

3.3 Prompt Tuning#

与 Prefix Tuning 的区别

  • Prefix Tuning:在每一层都添加可训练参数
  • Prompt Tuning:只在输入层添加”软提示”(soft prompt)
class PromptTuning(nn.Module):
def __init__(self, vocab_size, embed_dim, num_prompts=20):
super().__init__()
# 可训练的提示嵌入
self.prompt_embeddings = nn.Parameter(
torch.randn(num_prompts, embed_dim)
)
def forward(self, input_embeds):
batch_size = input_embeds.size(0)
# 扩展提示到 batch
prompts = self.prompt_embeddings.unsqueeze(0).expand(
batch_size, -1, -1
)
# 拼接到输入序列前
return torch.cat([prompts, input_embeds], dim=1)

适用场景

  • 小规模数据微调(few-shot learning)
  • 模型规模越大效果越好(在 GPT-3 规模上接近全参数微调)

3.4 P-Tuning v2#

改进点

  1. 在多层添加可训练前缀(结合 Prefix + Prompt)
  2. 使用 MLP 重参数化(提高表达能力)
class PTuningV2(nn.Module):
def __init__(self, num_layers, hidden_size, prefix_len=20):
super().__init__()
# 使用 MLP 生成前缀
self.prefix_encoder = nn.Sequential(
nn.Linear(hidden_size, hidden_size * 4),
nn.Tanh(),
nn.Linear(hidden_size * 4, num_layers * 2 * hidden_size)
)
# 初始的 prefix tokens
self.prefix_tokens = nn.Parameter(
torch.randn(prefix_len, hidden_size)
)

四、PEFT 库实战指南#

4.1 快速上手:3 行代码微调 LLM#

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import get_peft_model, LoraConfig, TaskType
# 1. 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("gpt2")
# 2. 定义 LoRA 配置
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=8, # LoRA rank
lora_alpha=16, # 缩放因子
lora_dropout=0.1,
target_modules=["c_attn"], # 对哪些模块应用 LoRA
)
# 3. 包装模型
model = get_peft_model(model, peft_config)
# 查看可训练参数
model.print_trainable_parameters()
# 输出: trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.2364%

4.2 完整训练流程#

from transformers import TrainingArguments, Trainer
from datasets import load_dataset
# 加载数据集
dataset = load_dataset("imdb")
# Tokenization
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=512
)
tokenized_dataset = dataset.map(tokenize_function, batched=True)
# 训练配置
training_args = TrainingArguments(
output_dir="./lora_model",
num_train_epochs=3,
per_device_train_batch_size=8,
gradient_accumulation_steps=4,
learning_rate=3e-4, # LoRA 可以用更高的学习率
fp16=True, # 混合精度训练
logging_steps=100,
save_strategy="epoch",
evaluation_strategy="epoch",
)
# 训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["test"],
)
trainer.train()
# 保存(只保存 LoRA 参数,约 10MB)
model.save_pretrained("./lora_adapter")

4.3 推理与部署#

# 方式一:加载 LoRA 权重
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("gpt2")
model = PeftModel.from_pretrained(base_model, "./lora_adapter")
# 方式二:合并权重(提高推理速度)
model = model.merge_and_unload()
model.save_pretrained("./merged_model")
# 多任务切换(保持一个 base model,切换不同 adapter)
model.load_adapter("./adapter_task1", adapter_name="task1")
model.load_adapter("./adapter_task2", adapter_name="task2")
# 推理时切换
model.set_adapter("task1")
output1 = model.generate(...)
model.set_adapter("task2")
output2 = model.generate(...)

4.4 高级技巧:QLoRA(量化 + LoRA)#

from transformers import BitsAndBytesConfig
# 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config,
device_map="auto"
)
# 应用 LoRA(在量化模型上训练)
peft_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05)
model = get_peft_model(model, peft_config)
# 现在可以在单张 24GB 显卡上微调 7B 模型!

五、性能对比与最佳实践#

5.1 不同方法的对比#

方法参数量推理延迟训练难度适用场景
LoRA★★★★☆★★★★★★★★★★通用推荐
QLoRA★★★★★★★★★☆★★★★☆显存受限
Adapter★★★☆☆★★★☆☆★★★★☆多任务学习
Prefix-Tuning★★★★☆★★★★☆★★★☆☆小数据量
Prompt-Tuning★★★★★★★★★★★★☆☆☆超大模型

5.2 实验结果(GSM8K 数学推理任务)#

模型:LLaMA-7B,数据:7,473 训练样本
┌────────────────┬──────────┬──────────┬──────────┐
│ 方法 │ 准确率 │ 训练时间 │ 显存占用 │
├────────────────┼──────────┼──────────┼──────────┤
│ Full FT │ 36.4% │ 10h │ 140 GB │
│ LoRA (r=8) │ 35.8% │ 4h │ 25 GB │
│ LoRA (r=64) │ 36.2% │ 5h │ 30 GB │
│ QLoRA (4bit) │ 35.6% │ 6h │ 12 GB │
│ Adapter │ 34.1% │ 4.5h │ 28 GB │
│ Prefix-Tuning │ 32.7% │ 3.5h │ 22 GB │
└────────────────┴──────────┴──────────┴──────────┘

关键结论

  • LoRA 在性能、效率、易用性上达到最佳平衡
  • QLoRA 是显存受限场景的最优选择
  • rank 从 8 增加到 64,性能提升有限但成本增加

5.3 最佳实践建议#

1. 选择合适的 rank

# 根据任务复杂度和数据量选择
def choose_rank(task_complexity, data_size):
if task_complexity == "low" and data_size < 1000:
return 4
elif task_complexity == "medium" or data_size < 10000:
return 8
elif task_complexity == "high" or data_size < 100000:
return 16
else:
return 32 # 最大不超过 64

2. 目标模块选择

# 对于 LLaMA/Mistral 架构
target_modules = [
"q_proj", # Query 投影(必选)
"v_proj", # Value 投影(必选)
"k_proj", # Key 投影(可选,提升效果)
"o_proj", # Output 投影(可选,略增成本)
# "gate_proj", "up_proj", "down_proj" # FFN 层(一般不需要)
]
# 激进设置(追求极致性能)
target_modules = "all-linear" # 对所有线性层应用 LoRA

3. 学习率调度

# 推荐配置
optimizer = AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=100, # warmup 3% 的步数
num_training_steps=total_steps
)

4. 防止过拟合

lora_config = LoraConfig(
r=8,
lora_alpha=16,
lora_dropout=0.1, # Dropout 防止过拟合
bias="none", # 不训练 bias
target_modules=["q_proj", "v_proj"],
)
# 早停策略
trainer = Trainer(
...,
early_stopping_patience=3, # 验证集 3 个 epoch 不提升则停止
)

六、前沿进展与未来方向#

6.1 LoRA 的变体#

AdaLoRA(Adaptive LoRA):

  • 动态调整不同层、不同模块的 rank
  • 重要层分配更高 rank,不重要层自动剪枝

DoRA(Weight-Decomposed LoRA):

  • 将权重分解为幅度和方向两部分
  • 在 LLaMA 上比标准 LoRA 提升 1-2%

LoRA+

  • 对矩阵 A 和 B 使用不同学习率
  • B 的学习率设为 A 的 16 倍,训练更稳定

6.2 与其他技术结合#

# LoRA + Flash Attention + Gradient Checkpointing
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-13b-hf",
use_flash_attention_2=True, # 加速 Attention
torch_dtype=torch.bfloat16,
)
model.gradient_checkpointing_enable() # 节省显存
# LoRA + DeepSpeed ZeRO
training_args = TrainingArguments(
...,
deepspeed="ds_config_zero3.json", # ZeRO-3 优化
)

6.3 开源生态#

推荐资源


七、总结#

参数高效微调技术,尤其是 LoRA,正在重新定义大语言模型的应用范式:

  1. 民主化 AI:个人开发者也能在消费级硬件上微调 7B-13B 模型
  2. 灵活部署:一个 base model + 多个轻量级 adapter,实现多任务服务
  3. 快速迭代:从数据准备到模型上线,可压缩到几小时内完成

核心要点

  • LoRA 通过低秩分解实现参数压缩,是当前最流行的 PEFT 方法
  • QLoRA 结合量化技术,可在单卡 24GB 显存上微调 70B 模型
  • 选择 rank=8-16 作为起点,根据效果调整
  • 优先对 Attention 的 Q、V 投影应用 LoRA

实践建议

  1. 从 PEFT 库的官方示例开始
  2. 在小数据集上快速验证配置
  3. 使用 QLoRA 降低硬件门槛
  4. 监控验证集性能,避免过拟合
  5. 合并权重后部署,减少推理延迟

参考资源#

论文

代码与工具

教程

下一篇文章将探讨提示工程最佳实践,敬请期待!

🧠 文章思维导图

mindmap
一、为什么需要参数高效微调?#
  1.1 全参数微调的困境#
  1.2 PEFT 的核心优势#
二、LoRA:低秩自适应的优雅设计#
  2.1 核心思想#
  2.2 前向传播机制#
  2.3 代码实现(从零开始)#
  2.4 训练技巧#
三、PEFT 方法大家族#
  3.1 Adapter Tuning#
  3.2 Prefix Tuning#
  3.3 Prompt Tuning#
  3.4 P-Tuning v2#
四、PEFT 库实战指南#
  4.1 快速上手:3 行代码微调 LLM#
  4.2 完整训练流程#
  4.3 推理与部署#
  4.4 高级技巧:QLoRA(量化 + LoRA)#
五、性能对比与最佳实践#
  5.1 不同方法的对比#
  5.2 实验结果(GSM8K 数学推理任务)#
  5.3 最佳实践建议#
六、前沿进展与未来方向#
  6.1 LoRA 的变体#
  6.2 与其他技术结合#
  6.3 开源生态#
七、总结#
参考资源#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
高效微调技术:LoRA与PEFT
https://meiluosi.github.io/posts/2024-09-22-高效微调技术lora与peft/
作者
枫语
发布于
2024-09-22
许可协议
CC BY-NC-SA 4.0

引用本文

@article{2024-09-22-高效微调技术lora与peft,
  author  = {枫语},
  title   = {高效微调技术:LoRA与PEFT},
  journal = {枫语},
  year    = {2024},
  month   = {sep},
  url     = {https://meiluosi.github.io/posts/2024-09-22-%E9%AB%98%E6%95%88%E5%BE%AE%E8%B0%83%E6%8A%80%E6%9C%AFlora%E4%B8%8Epeft/}
}

评论区

Profile Image of the Author
枫语
LLM 训练/推理 · 分布式系统 · RLHF
公告
LLM 训练/推理 · 分布式系统 · RLHF — 用 RL 的方法让大模型更聪明
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
33
分类
9
标签
15
总字数
75,837
运行时长
0
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录