参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南
5209 字
26 分钟
参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南
1. 引言:为什么需要参数高效微调?
1.1 全参数微调的困境
想象一下你要微调一个拥有 700 亿参数的 LLaMA-2 模型:
全参数微调的资源消耗:
模型: LLaMA-2 70B 参数: 70,000,000,000 (700 亿)
存储: - 模型权重: 140 GB (FP16) - 优化器状态: 280 GB (Adam 状态) - 梯度: 140 GB - 总计: ~560 GB 显存
成本: - A100 80GB × 8 = ~$40,000/月(云端) - 或购买 8 张 A100 40GB = ~$160,000
现实: → 个人开发者几乎不可能 → 中小企业也难以承受 → 每训练一个任务都需要复制全量权重1.2 PEFT 的核心思想
参数高效微调(PEFT) 的核心洞察是:微调时不需要更新所有参数,只需要更新一小部分参数。
class PEFTCoreIdea: """ PEFT 的核心思想
全参数微调: θ_new = θ_old - lr * ∇L(θ_old)
PEFT (LoRA): W_new = W_old + ΔW ΔW = B @ A (低秩矩阵)
关键:只训练 A 和 B,W_old 冻结 """
def full_finetuning(self): """ 全参数微调
所有参数都是可训练的: - 70B 参数 → 70B 可训练
优点: 表达能力最强 缺点: 资源消耗巨大 """ return { "trainable_params": "100%", "显存需求": "~560GB (70B 模型)", "适用场景": "有充足资源 + 最高性能要求", }
def lora(self): """ LoRA
只训练低秩适配矩阵: - W ∈ R^(d×k) - A ∈ R^(r×k), B ∈ R^(d×r) - 只训练 r×(d+k) << d×k 参数
70B 模型示例: - r=8, d=4096, k=4096 - 可训练参数: 8 × (4096+4096) = ~65K - 仅为原来的 0.0001% """ return { "trainable_params": "~0.01% - 0.1%", "显存需求": "~few GB", "适用场景": "资源受限 + 快速实验", }1.3 PEFT 方法全景
┌─────────────────────────────────────────────────────────────┐│ PEFT 方法全景 │├─────────────────────────────────────────────────────────────┤│ ││ 1. 适配器方法 (Adapter-based) ││ ├─ Adapter (Houlsby et al., 2019) ││ ├─ Compacter (Mahabadi et al., 2021) ││ └─ AdapterDrop (Rücklé et al., 2020) ││ ││ 2. 增量表示方法 (Additive) ││ ├─ Prefix Tuning (Li & Liang, 2021) ││ ├─ Prompt Tuning (Lester et al., 2021) ││ └─ P-Tuning (Liu et al., 2021) ││ ││ 3. 低秩分解方法 (Low-rank) ││ ├─ LoRA (Hu et al., 2021) ← 主流 ││ ├─ QLoRA (Dettmers et al., 2023) ││ ├─ DoRA (Liu et al., 2024) ││ ├─ LoRA+ (Hayou et al., 2024) ││ └─ PiSSA (Meng et al., 2024) ││ ││ 4. 混合方法 ││ └─ MoL (Mix-of-LoRA) ││ │└─────────────────────────────────────────────────────────────┘1.4 本系列文章关联
| 文章 | 关联 |
|---|---|
| 后训练深度解析 | PEFT 在后训练中的位置 |
| 数据构建深度解析 | 微调数据的构建方法 |
2. LoRA:低秩自适应
2.1 LoRA 的核心原理
LoRA(Low-Rank Adaptation,Hu et al., 2021)的核心发现是:预训练语言模型的权重更新具有低内在秩。
class LoRA原理: """ LoRA 原理
假设:对于预训练模型权重 W₀ ∈ R^(d×k), 微调后的权重更新 ΔW 也可以用低秩矩阵近似。
W_new = W₀ + ΔW = W₀ + BA
其中: - B ∈ R^(d×r) - A ∈ R^(r×k) - r << min(d, k)
关键洞察: ΔW 是低秩的 → 用 BA 近似 ΔW """
def __init__(self, d, k, r): self.d = d # 输出维度 self.k = k # 输入维度 self.r = r # 秩
# 冻结原始权重 self.W0 = None
# 可训练的低秩矩阵 self.A = nn.Parameter(torch.randn(r, k) * 0.01) # 用小随机值初始化 self.B = nn.Parameter(torch.zeros(d, r)) # 用零初始化
def forward(self, x): """ 前向传播
h = W₀x + BAx = W₀x + ΔWx """ return self.W0 @ x + (self.B @ self.A) @ x
def compute_trainable_params(self): """ 计算可训练参数数量 """ trainable = self.r * (self.d + self.k) total = self.d * self.k
ratio = trainable / total * 100
print(f"Total: {total:,} params") print(f"Trainable: {trainable:,} params") print(f"Ratio: {ratio:.4f}%")
return trainable2.2 LoRA 的数学推导
LoRA 的数学推导:
Step 1: 问题形式化
给定预训练权重 W₀ ∈ R^(d×k), 我们想学习一个更新 ΔW,使得:
W_new = W₀ + ΔW
直接学习 ΔW 需要优化 d×k 个参数。
Step 2: 低秩假设
假设 ΔW 的秩 r << min(d, k), 则存在分解:
ΔW = BA, 其中 B ∈ R^(d×r), A ∈ R^(r×k)
可训练参数: r×(d+k) << d×k
Step 3: 为什么低秩有效?
预训练语言模型的知识是"过参数化"的。 微调时,重要的知识改变发生在低秩子空间中。
数学解释: - W₀ 捕获通用语言知识 - ΔW 捕获特定任务知识 - 任务特定知识可以用低秩表示
Step 4: 初始化策略
A 的初始化: 随机小值(~N(0, 0.02)) B 的初始化: 零矩阵
这样初始化确保 ΔW = BA = 0 初始状态。 训练从 W₀ 开始,逐渐学习 ΔW。2.3 LoRA 的实现
class LoRALinear(nn.Module): """ LoRA 线性层实现 """
def __init__(self, in_features, out_features, rank=4, alpha=1.0, dropout=0.0): super().__init__()
self.in_features = in_features self.out_features = out_features self.rank = rank self.alpha = alpha self.scaling = alpha / rank # 缩放因子
# 原始权重(冻结) self.weight = nn.Parameter( torch.randn(out_features, in_features), requires_grad=False ) self.bias = nn.Parameter( torch.zeros(out_features), requires_grad=False )
# LoRA 参数 self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01) self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
# Dropout(可选) self.dropout = nn.Dropout(p=dropout)
# 标记哪些参数是 LoRA 的 self.lora_dropout = nn.Identity()
def forward(self, x): """ 前向传播 """ # 原始输出 output = F.linear(x, self.weight, self.bias)
# LoRA 输出 # h = Wx + (α/r) * BAx lora_output = (self.dropout(x) @ self.lora_A.T @ self.lora_B.T) * self.scaling
return output + lora_output
def merge_weights(self): """ 合并权重
将 LoRA 权重合并到原始权重中 用于推理阶段,消除额外计算 """ # W_new = W + (α/r) * BA merged_weight = self.weight + (self.alpha / self.rank) * (self.lora_B @ self.lora_A)
return merged_weight2.4 哪些层应该应用 LoRA?
class LoRALayerSelection: """ LoRA 层选择策略 """
# 不同配置的实验结果(来自论文) EXPERIMENTS = { "llama_7b": { # 只有 Query 注意力 "q": {"trainable": 0.09, "average": 91.2},
# Query + Value "qkv": {"trainable": 0.27, "average": 93.1},
# 所有注意力层 "qkv_proj": {"trainable": 0.39, "average": 93.5},
# 包含 MLP "all": {"trainable": 1.07, "average": 93.9},
# 包含所有线性层 "all_linear": {"trainable": "3.2%", "average": 94.1}, },
"llama_65b": { "qkv": {"trainable": 0.06, "average": 92.7}, "all_linear": {"trainable": "0.8%", "average": 94.1}, }, }
def recommended_config(self, model_size="7b"): """ 推荐配置 """ configs = { "7b": { "target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"], "rank": 8, "alpha": 16, "dropout": 0.05, }, "13b": { "target_modules": ["q_proj", "v_proj", "k_proj", "o_proj"], "rank": 8, "alpha": 16, "dropout": 0.05, }, "70b": { "target_modules": ["q_proj", "v_proj"], "rank": 8, "alpha": 16, "dropout": 0.05, }, }
return configs.get(model_size, configs["7b"])层选择策略对比:
| 配置 | 可训练参数 | 性能 | 建议 |
|---|---|---|---|
| 仅 Q | ~0.03% | 中等 | 极端资源受限 |
| Q, V | ~0.09% | 良好 | 常用推荐 |
| Q, K, V, O | ~0.39% | 更好 | 推荐 |
| Q, K, V, O + MLP | ~1.07% | 最好 | 有条件时使用 |
3. QLoRA:量化感知的 LoRA
3.1 QLoRA 的核心思想
QLoRA(Quantized LoRA,Dettmers et al., 2023)将量化与 LoRA 结合,在 4-bit 量化基础上微调。
class QLoRA: """ QLoRA: Quantized Low-Rank Adaptation
核心创新: 1. 4-bit NormalFloat (NF4) 量化 2. 双重量化(对量化常数也量化) 3. 分页优化器(处理内存峰值) """
def __init__(self, model, rank=64, alpha=16): self.model = model self.rank = rank self.alpha = alpha
# Step 1: 4-bit 量化基础模型 self.quantized_base = self.quantize_model(model)
# Step 2: 添加 LoRA 适配器(16-bit,精度) self.lora_adapters = self.add_lora_adapters()
def quantize_model(self, model): """ 量化模型
方法:NF4 (4-bit NormalFloat) """ from bitsandbytes import BitsAndBytesConfig
quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, # 双重量化 bnb_4bit_quant_type="nf4", # NormalFloat4 )
quantized_model = model.from_pretrained( model_path, quantization_config=quantization_config, )
return quantized_model
def add_lora_adapters(self): """ 添加 LoRA 适配器
LoRA 参数保持 16-bit,不量化 """ from peft import get_peft_model, LoraConfig
lora_config = LoraConfig( r=self.rank, lora_alpha=self.alpha, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", )
model = get_peft_model(self.quantized_base, lora_config) model.print_trainable_parameters()
return model3.2 NF4 量化原理
class NF4Quantization: """ NF4 (4-bit NormalFloat) 量化
关键思想: 1. 对权重按 abs 值分组 2. 用归一化的分位数量化 3. 保持重要权重(大幅值)的精度 """
def __init__(self, n_bits=4): self.n_bits = n_bits self.num_values = 2 ** n_bits # 16 个离散值
def quantize(self, weights): """ NF4 量化 """ # Step 1: 计算绝对值 abs_weights = torch.abs(weights)
# Step 2: 确定量化常数(分位数) # NF4 使用特殊的分位数量化表 quantiles = self.compute_quantiles(abs_weights, self.num_values)
# Step 3: 量化 quantized = self.assign_quantized_values(weights, quantiles)
return quantized, quantiles
def compute_quantiles(self, values, num_bins): """ 计算分位数
对于 k-bit 量化,我们需要 2^k 个量化值。 NF4 使用归一化的分位数,确保每个 bin 有相同数量的权重。 """ # 展平 flat = values.flatten().float()
# 计算分位数 # 使用特殊的 NF4 分位数量化表 # 这里简化实现 probs = torch.linspace(0, 1, num_bins + 1)[1:-1] quantiles = torch.quantile(flat, probs)
return quantiles
def dequantize(self, quantized, quantiles): """ 反量化 """ # 找到最近的量化值 indices = self.find_nearest_indices(quantized, quantiles)
# 重建 reconstructed = quantiles[indices]
return reconstructed3.3 QLoRA vs LoRA 显存对比
QLoRA 显存节省:
模型: LLaMA-2 65B
全参数微调: - 权重: 130 GB (FP16) - 梯度: 130 GB - 优化器: 260 GB - 总计: 520 GB
LoRA (FP16): - 权重: 130 GB (冻结) - LoRA 参数: ~150 MB - 梯度: ~500 MB - 优化器: ~1 GB - 总计: ~132 GB
QLoRA (4-bit): - 量化权重: 32 GB (NF4) - LoRA 参数: ~150 MB (BF16) - 梯度: ~200 MB - 优化器: ~400 MB - 总计: ~33 GB
节省: 520GB → 33GB (94% 减少!)
可用硬件: - 全参数: 需要 A100 80GB × 8 - LoRA: 需要 A100 40GB × 1 - QLoRA: 需要 A100 40GB × 1 (甚至 3090!)4. LoRA 的变体
4.1 DoRA:权重分解自适应
class DoRA: """ DoRA: Weight-Decomposed Low-Rank Adaptation (Liu et al., 2024)
核心思想: 将权重更新分解为幅度和方向两部分。
原始 LoRA: W_new = W₀ + BA
DoRA: W_new = W₀ + m·(BA / ||BA||_F)
其中 m 是可学习的幅度,BA/||BA||_F 是方向。 """
def __init__(self, in_features, out_features, rank=4): self.in_features = in_features self.out_features = out_features self.rank = rank
# 原始权重(冻结) self.W0 = None
# 方向(LoRA) self.A = nn.Parameter(torch.randn(rank, in_features) * 0.01) self.B = nn.Parameter(torch.zeros(out_features, rank))
# 幅度 self.m = nn.Parameter(torch.ones(out_features, in_features))
def forward(self, x): """ DoRA 前向传播 """ # 原始输出 base_output = F.linear(x, self.W0)
# LoRA 方向 delta_W = self.B @ self.A
# 幅度归一化 normalized_delta = delta_W / (torch.norm(delta_W, p='fro') + 1e-8)
# 加幅度 weighted_delta = self.m * normalized_delta
return base_output + weighted_delta * xDoRA vs LoRA 对比:
| 方面 | LoRA | DoRA |
|---|---|---|
| 更新方式 | 联合更新 BA | 分解为幅度+方向 |
| 可解释性 | 差 | 好 |
| 性能 | 基准 | 更高 |
| 收敛速度 | 中等 | 更快 |
4.2 PiSSA:主成分初始化
class PiSSA: """ PiSSA: Principal Singular Values Adaptation (Meng et al., 2024)
核心思想: 用 SVD 对权重矩阵进行分解, 只对最重要的奇异分量应用 LoRA。
优势: 1. 更好的初始化 2. 更快的收敛 3. 更好的性能 """
def __init__(self, weight, rank=4): self.weight = weight # 原始权重 self.rank = rank
# Step 1: SVD 分解 U, S, Vh = torch.linalg.svd(weight.float(), full_matrices=False)
# Step 2: 选择主成分 # 只对前 r 个奇异分量应用 LoRA self.U_main = U[:, :rank] # 保留主要方向 self.S_main = S[:rank] self.Vh_main = Vh[:rank, :]
# Step 3: 初始化 LoRA # 用主成分的权重初始化 self.A = nn.Parameter(self.Vh_main) self.B = nn.Parameter(self.U_main @ torch.diag(self.S_main))
# 剩余奇异值作为偏置 self.remaining = U[:, rank:] @ torch.diag(S[rank:]) @ Vh[rank:, :]
def forward(self, x): """ PiSSA 前向传播 """ # 主成分部分用 LoRA lora_part = (self.B @ self.A) @ x
# 剩余部分(冻结) remaining_part = self.remaining @ x
# 偏置部分 bias_part = self.weight.bias @ x if hasattr(self.weight, 'bias') else 0
return lora_part + remaining_part + bias_part4.3 LoRA+
class LoRAPlus: """ LoRA+: Different Learning Rates for A and B (Hayou et al., 2024)
核心思想: A 和 B 应该有不同的学习率。
原始 LoRA: lr_A = lr_B = lr
LoRA+: lr_A = lr / λ_A lr_B = lr * λ_B
推荐: λ_A = r, λ_B = 1 """
def configure_optimizers(self, model, base_lr=1e-4, r=8): """ 配置优化器 """ # A 的学习率(通常较小) lr_A = base_lr / r
# B 的学习率(标准) lr_B = base_lr
# 其他参数(LoRA 以外) lr_other = base_lr * 0.1
optimizer_grouped_parameters = [ # LoRA A {"params": model.lora_A, "lr": lr_A}, # LoRA B {"params": model.lora_B, "lr": lr_B}, # 其他(冻结) {"params": model.other_params, "lr": 0}, ]
return torch.optim.AdamW(optimizer_grouped_parameters)4.4 LoRA 变体对比
| 方法 | 核心创新 | 性能提升 | 计算开销 |
|---|---|---|---|
| LoRA | 低秩分解 | 基准 | 低 |
| DoRA | 幅度+方向分解 | +2-5% | 极低 |
| PiSSA | SVD 初始化 | +3-8% | 低 |
| LoRA+ | 自适应学习率 | +2-5% | 极低 |
| QLoRA | 4-bit 量化 | 接近 FP16 | 中等 |
5. 其他 PEFT 方法
5.1 Adapter
class Adapter: """ Adapter (Houlsby et al., 2019)
结构:逐层添加瓶颈网络
h = W₀x + adapter(W₀x)
adapter: d → r → d (bottleneck: r << d) """
def __init__(self, d_model, r=64, alpha=16): self.d_model = d_model self.r = r # bottleneck 维度
# 下投影 self.down = nn.Linear(d_model, r)
# 上投影 self.up = nn.Linear(r, d_model)
# 非线性 self.act = nn.GELU()
# LayerNorm self.layer_norm = nn.LayerNorm(d_model)
# 残差缩放 self.alpha = alpha
def forward(self, x): """ Adapter 前向传播 """ # 原始残差 residual = x
# Adapter adapter_output = self.up(self.act(self.down(x)))
# 残差连接(带缩放) output = self.layer_norm(residual + (self.alpha / self.r) * adapter_output)
return output5.2 Prefix Tuning
class PrefixTuning: """ Prefix Tuning (Li & Liang, 2021)
核心思想: 在每层注意力之前添加可学习的前缀。
不修改模型权重,只添加前缀参数。 """
def __init__(self, num_layers, num_heads, head_dim, prefix_len=10): self.num_layers = num_layers self.num_heads = num_heads self.head_dim = head_dim self.prefix_len = prefix_len
# 可学习的前缀 # 每层有独立的 prefix self.prefixes = nn.ParameterList([ nn.Parameter(torch.randn(prefix_len, num_heads * head_dim) * 0.01) for _ in range(num_layers) ])
def forward(self, layer_idx, q, k, v): """ 应用 Prefix Tuning
在 Key 和 Value 前面拼接可学习的 prefix """ # 获取该层的前缀 prefix = self.prefixes[layer_idx]
# 分割 head prefix_q = prefix[:, :self.num_heads * self.head_dim] prefix_k = prefix[:, :self.num_heads * self.head_dim] prefix_v = prefix[:, :self.num_heads * self.head_dim]
# Reshape batch_size = q.shape[0] prefix_len = prefix.shape[0]
prefix_q = prefix_q.unsqueeze(0).expand(batch_size, -1, -1) prefix_k = prefix_k.unsqueeze(0).expand(batch_size, -1, -1) prefix_v = prefix_v.unsqueeze(0).expand(batch_size, -1, -1)
# 拼接 k_full = torch.cat([prefix_k, k], dim=1) v_full = torch.cat([prefix_v, v], dim=1)
return k_full, v_full5.3 Prompt Tuning
class PromptTuning: """ Prompt Tuning (Lester et al., 2021)
核心思想: 只添加可学习的 soft prompt,完全不修改模型。
参数量:prefix_len × embedding_dim """
def __init__(self, vocab_size, embed_dim, num_tokens=20): self.vocab_size = vocab_size self.embed_dim = embed_dim self.num_tokens = num_tokens
# 可学习的 soft prompt self.prompt = nn.Parameter( torch.randn(1, num_tokens, embed_dim) * 0.01 )
def forward(self, input_ids, embedding_layer): """ 获取带 prompt 的 embedding """ # 输入 embedding inputs_embeds = embedding_layer(input_ids)
# 拼接 prompt batch_size = inputs_embeds.shape[0] prompt = self.prompt.expand(batch_size, -1, -1)
full_embeds = torch.cat([prompt, inputs_embeds], dim=1)
# 记录 prompt 长度(用于 mask) prompt_length = self.num_tokens
return full_embeds, prompt_length5.4 方法对比
| 方法 | 引入参数 | 位置 | 推理开销 | 性能 |
|---|---|---|---|---|
| LoRA | ~0.1-1% | 权重矩阵旁 | 低(可合并) | 高 |
| Adapter | ~0.5-5% | 残差连接中 | 中 | 高 |
| Prefix Tuning | ~0.1% | 输入前 | 中 | 中 |
| Prompt Tuning | ~0.01% | 嵌入层前 | 低 | 较低 |
| QLoRA | ~0.1% | 权重矩阵旁 | 中 | 接近全参数 |
6. 秩的选择与调优
6.1 秩的理论分析
class RankSelection: """ LoRA 秩的选择 """
# 不同秩的实验结果(来自论文) EXPERIMENTS = { "WikiSQL": { "r=1": 74.2, "r=2": 75.6, "r=4": 77.2, "r=8": 78.5, "r=16": 79.3, "r=32": 79.8, "r=64": 80.1, "r=128": 80.2, }, "MNLI": { "r=1": 82.6, "r=4": 84.1, "r=8": 84.8, "r=16": 85.3, "r=64": 85.9, }, }
def analyze_scaling(self): """ 性能-秩关系分析
观察: 1. 性能随秩增加而提升(边际递减) 2. r=8 是一个常用的"甜点" 3. r=64 之后提升很小 """ return { "conclusion": "边际收益递减", "recommended_range": "r ∈ [4, 64]", "sweet_spot": "r=8 for most tasks", "rule_of_thumb": "r = α/2, where α is typical attention dimension ratio", }
def adaptive_rank(self, model, task_complexity): """ 自适应秩选择 """ rank_map = { "simple_classification": 4, "ner": 8, "question_answering": 16, "summarization": 32, "code_generation": 64, "complex_reasoning": 128, }
return rank_map.get(task_complexity, 8)6.2 秩与任务复杂度的关系
秩选择指南:
简单任务(分类、NER): 建议 r = 4-8 理由:任务简单,低秩足以捕捉更新
中等任务(QA、翻译): 建议 r = 8-16 理由:需要更多能力
复杂任务(代码生成、复杂推理): 建议 r = 32-64 理由:任务复杂,需要更高表达能力
极端情况(多任务、微调整个模型): 建议 r = 64-128 理由:最大程度保持性能6.3 缩放因子 α
class AlphaScaling: """ 缩放因子 α 的作用
实际缩放 = α / r """
def __init__(self, rank, alpha=16): self.scaling = alpha / rank
def experiment_results(self): """ 实验结果(来自论文) """ return { "alpha=r": "平衡设置", "alpha=2r": "更保守的更新", "alpha=r/2": "更激进的更新(不推荐)", "recommendation": "α = 2r 是一个好的起点", }7. 实战指南
7.1 使用 Hugging Face PEFT
from peft import ( get_peft_model, LoraConfig, TaskType, PeftType, prepare_model_for_kbit_training,)
def create_lora_model(model, config=None): """ 创建 LoRA 模型 """ # 默认配置 if config is None: config = { "r": 8, "lora_alpha": 16, "target_modules": ["q_proj", "v_proj"], "lora_dropout": 0.05, "bias": "none", "task_type": TaskType.CAUSAL_LM, }
# 创建配置 lora_config = LoraConfig(**config)
# 应用 LoRA model = get_peft_model(model, lora_config)
# 打印可训练参数 model.print_trainable_parameters()
return model
def train_with_peft(model, train_dataset, eval_dataset): """ PEFT 训练 """ from transformers import Trainer, TrainingArguments
training_args = TrainingArguments( output_dir="./output", num_train_epochs=3, per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=1e-4, warmup_ratio=0.03, lr_scheduler_type="cosine", logging_steps=10, save_steps=500, eval_steps=500, evaluation_strategy="steps", fp16=True, optim="paged_adamw_32bit", # 页面优化器 )
trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=data_collator, )
trainer.train()
return model
def merge_and_save(model, output_path): """ 合并权重并保存 """ # 合并 LoRA 权重到基础模型 merged_model = model.merge_and_unload()
# 保存 merged_model.save_pretrained(output_path)7.2 QLoRA 完整示例
from transformers import AutoModelForCausalLM, BitsAndBytesConfigfrom peft import prepare_model_for_kbit_training, get_peft_model, LoraConfig
def load_qloRA_model(model_name): """ 加载 QLoRA 模型 """ # Step 1: 量化配置 bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, )
# Step 2: 加载量化模型 model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto", trust_remote_code=True, )
# Step 3: 准备训练 model = prepare_model_for_kbit_training(model)
# Step 4: 应用 LoRA lora_config = LoraConfig( r=64, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM, )
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 输出: "trainable params: 83,886,080 || all params: 33,杂,杂,杂 || trainable%: 0.2414%"
return model
# 使用示例model = load_qloRA_model("meta-llama/Llama-2-70b-hf")trainer = Trainer(model=model, ...)trainer.train()7.3 多 LoRA 组合
class MultiLoRA: """ 多 LoRA 组合 """
def __init__(self, base_model): self.base_model = base_model self.lora_weights = {}
def add_task_lora(self, task_name, lora_path): """ 添加任务的 LoRA """ from peft import PeftModel
# 加载 LoRA lora_model = PeftModel.from_pretrained( self.base_model, lora_path, adapter_name=task_name, )
self.lora_weights[task_name] = lora_model
def switch_lora(self, task_name): """ 切换到指定任务的 LoRA """ for name, model in self.lora_weights.items(): if name == task_name: model.set_adapter(task_name) else: model.disable_adapter()
def weighted_merge(self, weights): """ 加权合并多个 LoRA
用于多任务学习 """ # 获取所有 LoRA 权重 lora_weights = [] for name in weights.keys(): lora_weights.append(self.lora_weights[name])
# 加权平均 # 实际实现需要更复杂的逻辑 pass8. 高级主题
8.1 权重合并与卸载
class WeightMerging: """ 权重合并与卸载 """
def merge_weights(self, model): """ 合并 LoRA 权重
合并后: - 推理速度更快(无需额外的矩阵乘法) - 模型大小不变 """ # 合并所有 LoRA 适配器 merged_model = model.merge_and_unload()
return merged_model
def merge_and_quantize(self, model): """ 合并后量化
将合并的模型量化以节省空间 """ # 合并 merged = model.merge_and_unload()
# 量化 quantized = self.quantize(merged, bits=4)
return quantized
def unload_and_reload(self, model): """ 卸载 LoRA,保留原始模型 """ # 卸载所有 LoRA 适配器 model.unload()
return model8.2 持续预训练与微调的结合
class ContinualPretraining: """ 持续预训练 + 微调 """
def combined_training(self, base_model, pretrain_data, finetune_data): """ 组合训练策略
阶段 1: 持续预训练(领域适应) 阶段 2: 指令微调(任务适应) """ # 阶段 1: 预训练 pretrain_model = self.pretrain(base_model, pretrain_data)
# 阶段 2: 微调 # 复用同一个 LoRA 或创建新的 finetune_model = self.finetune(pretrain_model, finetune_data)
return finetune_model
def pretrain(self, model, data): """ 持续预训练 """ # LoRA 配置(可以是不同的模块) config = LoraConfig( r=16, target_modules=["q_proj", "v_proj", "k_proj", "mlp"], )
model = get_peft_model(model, config)
# 训练 trainer = Trainer(model=model, data=data, ...) trainer.train()
return model
def finetune(self, model, data): """ 指令微调 """ # 冻结预训练 LoRA for param in model.parameters(): if "lora_" not in param.name: param.requires_grad = False
# 添加新的 LoRA 或继续训练现有 LoRA # ...8.3 分布式训练
class DistributedLoRA: """ 分布式 LoRA 训练 """
def train_distributed(self, model, data): """ 分布式训练 """ # FSDP (Fully Sharded Data Parallel) from torch.distributed.fsdp import ( FullyShardedDataParallel as FSDP, MixedPrecision, ShardingStrategy, )
# 包装模型 model = FSDP( model, sharding_strategy=ShardingStrategy.FULL_SHARD, mixed_precision=MixedPrecision( param_dtype=torch.bfloat16, reduce_dtype=torch.float32, ), )
# 训练 trainer = Trainer(model=model, ...) trainer.train()
def ZeRO_LoRA(self, model, data): """ ZeRO 优化 """ # DeepSpeed ZeRO from transformers import DeepSpeedConfig
ds_config = { "zero_optimization": { "stage": 3, "offload_optimizer": { "device": "cpu", }, }, "bf16": {"enabled": True}, }
# 训练 trainer = Trainer( model=model, args=training_args, ds_config=ds_config, ... )9. 核心公式汇总
9.1 LoRA 权重更新
其中 ,。
9.2 可训练参数比例
9.3 LoRA 梯度
9.4 NF4 量化
其中 是偏移, 是缩放因子。
10. 总结
10.1 方法选择指南
┌─────────────────────────────────────────────────────────────┐│ PEFT 方法选择指南 │├─────────────────────────────────────────────────────────────┤│ ││ 资源情况 ││ │ ││ ├── 消费级 GPU (RTX 3090/4090, 24GB) ││ │ └── QLoRA (4-bit) + r=32-64 ││ │ ││ ├── 单卡 A100 (40-80GB) ││ │ ├── 小模型 (<13B): LoRA + r=8-16 ││ │ └── 大模型 (>=13B): QLoRA + r=64 ││ │ ││ └── 多卡 A100 ││ └── 全参数微调 或 LoRA + r=64 ││ ││ 任务复杂度 ││ │ ││ ├── 简单分类 ││ │ └── LoRA r=4-8 ││ │ ││ ├── NER/QA ││ │ └── LoRA r=8-16 ││ │ ││ ├── 代码生成 ││ │ └── LoRA r=32-64 ││ │ ││ └── 多任务/复杂推理 ││ └── LoRA r=64-128 或 全参数微调 ││ │└─────────────────────────────────────────────────────────────┘10.2 最佳实践
┌─────────────────────────────────────────────────────────────┐│ LoRA 最佳实践 │├─────────────────────────────────────────────────────────────┤│ ││ 1. 模块选择 ││ → 默认: q_proj, v_proj ││ → 更好: + k_proj, o_proj ││ → 最好: + mlp.gate_proj, mlp.up_proj, mlp.down_proj ││ ││ 2. 秩选择 ││ → 常用: r=8 (简单任务), r=64 (复杂任务) ││ → α 通常设为 2r ││ ││ 3. Dropout ││ → 训练时: lora_dropout=0.05 ││ → 推理时: 不使用 dropout ││ ││ 4. 优化器 ││ → AdamW (paged 版本防止峰值) ││ → 学习率: 1e-4 到 3e-4 ││ → Warmup: 3% 到 6% ││ ││ 5. 权重合并 ││ → 推理前合并权重以提高速度 ││ → 如果需要切换 LoRA,保持分开 ││ ││ 6. 持久化 ││ → 保存: 只保存 LoRA 权重 (~10-100MB) ││ → 加载: 需要基础模型 + LoRA 权重 ││ │└─────────────────────────────────────────────────────────────┘10.3 性能对比
┌─────────────────────────────────────────────────────────────┐│ 65B 模型性能对比 │├─────────────────────────────────────────────────────────────┤│ ││ 方法 可训练参数 MMLU HumanEval ││ ───────────────────────────────────────────────────────── ││ 全参数微调 65B 68.0 62.0 ││ LoRA (r=64, all) 666M 67.5 61.5 ││ LoRA (r=8, qkv) 83M 66.8 60.2 ││ QLoRA (r=64, NF4) 666M 67.6 61.8 ││ Adapter (r=64) 666M 67.2 61.0 ││ Prefix Tuning 19M 63.5 58.0 ││ ───────────────────────────────────────────────────────── ││ ││ 结论: LoRA/QLoRA 接近全参数微调性能 ││ │└─────────────────────────────────────────────────────────────┘推荐阅读
- LoRA(Hu et al., 2021)—— 低秩自适应原论文
- QLoRA(Dettmers et al., 2023)—— 量化感知 LoRA
- DoRA(Liu et al., 2024)—— 权重分解自适应
- Adapter(Houlsby et al., 2019)—— 适配器方法
- Prefix Tuning(Li & Liang, 2021)—— 前缀调优
参考资料
- Hu, E. J., et al. (2021). “LoRA: Low-Rank Adaptation of Large Language Models.” ICLR.
- Dettmers, T., et al. (2023). “QLoRA: Efficient Finetuning of Quantized LLMs.” NeurIPS.
- Liu, S., et al. (2024). “DoRA: Weight-Decomposed Low-Rank Adaptation.” arXiv.
- Meng, F., et al. (2024). “PiSSA: Principal Singular Values Adaptation.” arXiv.
- Hayou, S., et al. (2024). “LoRA+: Efficient Low Rank Adaptation of LLMs.” arXiv.
- Houlsby, N., et al. (2019). “Parameter-Efficient Transfer Learning for NLP.” ICML.
- Li, X. L., & Liang, P. (2021). “Prefix-Tuning: Optimizing Continuous Prompts.” ACL.
- Lester, B., et al. (2021). “The Power of Scale for Parameter-Efficient Prompt Tuning.” EMNLP.
- Mahabadi, R. K., et al. (2021). “Compacter: Efficient Low-Rank Hypercomplex Adapter Layers.” NeurIPS.
- Rücklé, A., et al. (2020). “AdapterDrop: On the Efficiency of Adapters in Transformers.” EMNLP.
- Aghajanyan, A., et al. (2020). “Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning.” ACL.
- Lialin, V., et al. (2023). “Hierarchical LoRA for Multi-task Fine-tuning.” arXiv.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
参数高效微调深度解析:从 LoRA 到 QLoRA 的全面指南
https://aiattnstudio.link/posts/peft-lora/
