查看全课大纲(第 29 / 40 节)
  1. 1.从规则到学习:深度学习解决什么问题
  2. 2.表示学习与端到端训练
  3. 3.张量、计算图与自动微分
  4. 4.训练三要素:数据、模型、目标函数
  5. 5.从感知机到多层感知机
  6. 6.反向传播:手推一个两层网络
  7. 7.激活函数与初始化
  8. 8.优化器:SGD → Momentum → Adam
  9. 9.正则化:权重衰减、Dropout、BN/LN
  10. 10.卷积的直觉与参数共享
  11. 11.池化、感受野与下采样
  12. 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
  13. 13.残差连接与归一化为什么有效
  14. 14.迁移学习与特征可视化
  15. 15.RNN 与 BPTT
  16. 16.LSTM/GRU:门控如何缓解梯度问题
  17. 17.Seq2Seq 与编码器-解码器
  18. 18.注意力前史:Bahdanau 加性注意力
  19. 19.Self-Attention:QKV 与缩放点积
  20. 20.多头注意力:并行的多种关系
  21. 21.位置编码:正弦与可学习
  22. 22.残差、LayerNorm 与前馈层
  23. 23.从零实现一个最小 Transformer block
  24. 24.训练稳定性、复杂度与常见误区
  25. 25.自监督与语言建模目标
  26. 26.BERT:掩码语言模型与双向编码
  27. 27.GPT:自回归生成与缩放定律
  28. 28.指令微调与对齐:SFT → RLHF/DPO
  29. 29.参数高效微调:LoRA 与适配器
  30. 30.自编码器与 VAE
  31. 31.GAN:对抗训练与模式崩塌
  32. 32.扩散模型:前向加噪与反向去噪
  33. 33.扩散模型的应用与采样加速
  34. 34.对比学习与 CLIP
  35. 35.ViT:把图像当序列
  36. 36.多模态架构:对齐、融合与生成
  37. 37.能力边界、幻觉与评测
  38. 38.PyTorch 2.x 训练循环与混合精度
  39. 39.实验管理、评测与复现
  40. 40.推理部署与成本:量化/蒸馏概览

参数高效微调:LoRA 与适配器

小象实战讲义 · 深度学习基础:从神经网络到 Transformer

全参数微调一个 1750 亿参数的模型,每个任务都要存一份完整副本——这条路在工程上走不通。本节看两条参数高效路线:适配器(Adapter)与 LoRA。重点讲清 LoRA 凭什么能用万分之一的参数达到接近全量微调的效果,且不增加推理延迟。

先修:Transformer 的权重矩阵与注意力结构(第 5 章 5.2 节)、SFT 与对齐的基本流程(本章 6.4 节)、Adam 的优化器状态。后续:第 7 章《生成模型》第 1 节《自编码器与 VAE》。

💡 核心导读

  1. 问题本质:全参数微调(Fine-tuning)大模型需要存储和更新海量参数,部署多个任务模型成本极高。
  2. LoRA 核心思想:假设模型在适应新任务时,其权重矩阵的更新(ΔW)具有“低内在秩”。因此,可以用两个小矩阵 B 和 A 的乘积 BA 来近似 ΔW,只训练 B 和 A。
  3. 实现方式:在Transformer层的特定权重矩阵(如 W_q、W_v)旁,并行注入可训练的低秩矩阵 B 和 A。前向传播时,原始输出加上 B*A 的输出。
  4. 关键优势:训练参数量减少数千倍,无推理延迟(训练后可将 BA 合并回原权重),任务切换只需替换小矩阵。
  5. 实践洞察:论文实验表明,即使原始维度 d=12288,极低的秩 r=1 或 2 也能取得很好效果。

🧭 前置知识

  1. 矩阵乘法与形状:(m, n) = (m, r) @ (r, n),其中 r 是秩。
  2. Transformer 权重矩阵:自注意力模块中的 W_q、W_k、W_v、W_o 以及前馈网络中的 W_up、W_down。
  3. 微调(Fine-tuning):在预训练模型基础上,用下游任务数据更新所有权重。
  4. 梯度与优化器状态:Adam等优化器会为每个可训练参数维护动量(momentum)和方差(variance)状态,占用大量显存。

全参数微调:不可承受之重

在自然语言处理中,主流范式是先在通用领域数据上进行大规模预训练,再针对特定任务或领域进行适应(Adaptation)(Hu et al., 2021,arXiv:2106.09685)。这种适应通常通过全参数微调实现,即更新预训练模型的所有参数。

对于一个预训练的自回归语言模型 P_Φ(y|x),其参数为 Φ,下游任务数据集为 Z = {(x_i, y_i)}。全参数微调的目标是最大化条件语言建模目标:

max⁡Φ∑(x,y)∈Z∑t=1∣y∣log⁡(PΦ(yt∣x,y<t))\max_{\Phi} \sum_{(x,y) \in \mathcal{Z}} \sum_{t=1}^{|y|} \log(P_{\Phi}(y_t | x, y_{<t}))

模型从预训练权重 Φ_0 开始,通过梯度更新得到 Φ_0 + ΔΦ。问题在于,|ΔΦ| = |Φ_0|。以 GPT-3 175B 为例(Brown et al., 2020,arXiv:2005.14165),|Φ_0| ≈ 1750亿。为每个下游任务部署一个独立的、拥有1750亿参数的微调模型,在存储、计算和部署上都是极其昂贵的。

先看另一条路:适配器(Adapter)

Adapter(Houlsby et al., 2019,arXiv:1902.00751)的思路是在每个 Transformer 子层后面串行插入两个小线性层:先降维到瓶颈(d→rd \to r),再升回 dd,中间夹非线性,外面套残差。只训练这两个小层,参数量同样能压到原模型的几个百分点。代价是推理延迟:新增的层必须按顺序算,无法与原始路径并行,批大小为 1 的在线推理尤其吃亏。LoRA 要解决的正是这个延迟问题。

LoRA:低秩适应

LoRA(Low-Rank Adaptation)的核心假设是:模型在适应新任务时,其权重矩阵的更新 ΔW 具有一个较低的“内在秩”(intrinsic rank)(Hu et al., 2021)。这意味着,尽管 ΔW 本身是一个高维矩阵(例如 d × d),但它可以用一个低秩分解来有效近似。

低秩参数化的更新矩阵

对于一个预训练权重矩阵 W_0 ∈ ℝ^(d×k),LoRA 约束其更新为低秩形式:

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

其中,B ∈ ℝ^(d×r), A ∈ ℝ^(r×k),且秩 r ≪ min(d, k)。在训练期间,W_0 被冻结(不计算梯度、不更新),只有 A 和 B 是可训练的。因此,可训练参数的数量从 d × k 锐减至 r × (d + k)。

前向传播时,对于输入 h,修改后的线性层计算为:

h′=W0h+ΔWh=W0h+B(Ah)h' = W_0 h + \Delta W h = W_0 h + B(A h)

由于 r 很小,计算 A h 和 B (A h) 增加的额外计算量可以忽略不计。

让我们通过一个极简的例子,直观感受形状变化和参数量对比。

import torch
import torch.nn as nn
torch.manual_seed(0)

# 模拟一个Transformer层中的权重矩阵,例如 d_model=64, 内部维度也设为64
d = 64
k = 64
W0 = torch.randn(d, k)  # 预训练权重,冻结

# 全参数微调:需要训练整个 W0 (d x k) 矩阵
full_finetune_params = d * k
print(f"全参数微调参数量: {full_finetune_params}")

# LoRA微调:选择秩 r=4
r = 4
B = torch.zeros(d, r)      # B 初始化为零
A = torch.randn(r, k) * 0.01  # A 用小的随机值;这样初始 ΔW = BA = 0
# 注意:实际LoRA实现中,B和A需要被注册为nn.Parameter并设置requires_grad=True
# 这里仅为演示形状和计算
print(f"LoRA矩阵B形状: {tuple(B.shape)}")
print(f"LoRA矩阵A形状: {tuple(A.shape)}")
lora_params = (d * r) + (r * k)
print(f"LoRA可训练参数量: {lora_params}")
print(f"参数量减少倍数: {full_finetune_params / lora_params:.1f}x")

# 模拟前向传播
batch_size = 2
seq_len = 8
h = torch.randn(batch_size, seq_len, d)  # 输入特征
# 原始路径
output_original = torch.matmul(h, W0.T)  # (2, 8, 64)
# LoRA路径
output_lora = torch.matmul(torch.matmul(h, A.T), B.T)  # h @ A^T @ B^T
# 合并输出
output_final = output_original + output_lora
print(f"原始输出形状: {tuple(output_original.shape)}")
print(f"LoRA输出形状: {tuple(output_lora.shape)}")
print(f"最终输出形状: {tuple(output_final.shape)}")
全参数微调参数量: 4096
LoRA矩阵B形状: (64, 4)
LoRA矩阵A形状: (4, 64)
LoRA可训练参数量: 512
参数量减少倍数: 8.0x
原始输出形状: (2, 8, 64)
LoRA输出形状: (2, 8, 64)
最终输出形状: (2, 8, 64)

在上面的小规模示例中,参数量减少了8倍。换到真实尺寸算:d=12288、r=8 时,单个矩阵从 1.5 亿参数降到 19.7 万(约 768 倍);而 GPT-3 175B 整模型按论文配置(r=4,只改 W_q、W_v)叠加后,论文报告可训练参数减少 10,000 倍、GPU 显存需求减少 3 倍(Hu et al., 2021)。

应用于 Transformer

在Transformer架构中,LoRA通常只应用于自注意力模块的四个权重矩阵(W_q, W_k, W_v, W_o),有时也应用于前馈网络(FFN)的两个权重矩阵。论文通过实验发现,仅将LoRA应用于 W_q 和 W_v 就能获得大部分性能增益,且进一步减少参数量。

以下代码演示如何为一个简化的自注意力模块的 W_q 和 W_v 添加LoRA。

import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)

class LoRALayer(nn.Module):
    """为单个线性层添加LoRA。"""
    def __init__(self, linear_layer, rank=4, alpha=8):
        super().__init__()
        self.linear = linear_layer  # 原始的、冻结的线性层
        d, k = linear_layer.weight.shape
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank  # 缩放因子,用于稳定训练

        # 初始化LoRA矩阵
        self.lora_A = nn.Parameter(torch.zeros(rank, k))
        self.lora_B = nn.Parameter(torch.zeros(d, rank))
        # 通常使用Kaiming或Xavier初始化A,B初始化为零
        nn.init.kaiming_uniform_(self.lora_A, a=5**0.5)
        # B初始化为零,保证训练开始时ΔW=0

    def forward(self, x):
        # 原始线性变换
        out_orig = self.linear(x)
        # LoRA变换: x @ A^T @ B^T
        out_lora = (x @ self.lora_A.T @ self.lora_B.T) * self.scaling
        return out_orig + out_lora

# 构建一个简化的自注意力模块,包含W_q和W_v
d_model = 32
num_heads = 4
head_dim = d_model // num_heads

# 原始投影层(模拟预训练权重)
W_q = nn.Linear(d_model, d_model, bias=False)
W_v = nn.Linear(d_model, d_model, bias=False)
# 冻结它们的权重
for param in W_q.parameters():
    param.requires_grad = False
for param in W_v.parameters():
    param.requires_grad = False

# 用LoRALayer包裹它们
lora_rank = 2
lora_W_q = LoRALayer(W_q, rank=lora_rank)
lora_W_v = LoRALayer(W_v, rank=lora_rank)

# 模拟前向传播
batch_size = 2
seq_len = 10
x = torch.randn(batch_size, seq_len, d_model)

# 计算query和value
query = lora_W_q(x)  # 包含原始W_q + LoRA
value = lora_W_v(x)  # 包含原始W_v + LoRA

print(f"输入x形状: {tuple(x.shape)}")
print(f"查询query形状: {tuple(query.shape)}")
print(f"值value形状: {tuple(value.shape)}")
print(f"原始W_q可训练参数: {sum(p.numel() for p in W_q.parameters() if p.requires_grad)}")
print(f"LoRA W_q可训练参数: {sum(p.numel() for p in lora_W_q.parameters() if p.requires_grad)}")
输入x形状: (2, 10, 32)
查询query形状: (2, 10, 32)
值value形状: (2, 10, 32)
原始W_q可训练参数: 0
LoRA W_q可训练参数: 128

为什么LoRA有效且高效?

消除推理延迟

与适配器(Adapter)层不同,适配器在模型中添加了额外的、必须串行计算的层,即使参数很少也会增加推理延迟(尤其是在批大小为1的在线推理场景)。LoRA的修改是并行的,其计算 B(Ah) 可以与原始计算 W_0 h 同时进行。更重要的是,在训练完成后,我们可以将低秩更新矩阵合并回原始权重:

W′=W0+BAW' = W_0 + B A

合并后,模型在架构上与原始预训练模型完全相同,因此不会引入任何额外的推理延迟。

import torch
import torch.nn as nn
torch.manual_seed(0)

# 模拟合并过程
d, k = 8, 8
r = 2
W0 = torch.randn(d, k)
B = torch.randn(d, r) * 0.01
A = torch.randn(r, k) * 0.01

# 合并前的前向计算
h = torch.randn(3, k)
output_separate = h @ W0.T + h @ A.T @ B.T

# 合并权重
W_merged = W0 + B @ A
output_merged = h @ W_merged.T

# 检查结果是否一致(允许微小浮点误差)
print(f"合并前后输出差异: {torch.max(torch.abs(output_separate - output_merged)).item():.2e}")
合并前后输出差异: 4.77e-07

高效的任务切换与存储

由于预训练模型 W_0 被冻结且共享,对于不同的下游任务,我们只需要存储和加载对应的、体积很小的 A 和 B 矩阵。这极大地降低了存储开销和任务切换的成本。

优化器状态大幅减少

使用Adam等自适应优化器时,优化器需要为每个可训练参数维护两个状态(动量和方差)。LoRA通过大幅减少可训练参数的数量,也等比例地减少了优化器状态所占用的GPU内存。这正是LoRA能将GPT-3 175B训练的GPU内存需求降低3倍的关键原因之一。

LoRA的实践细节与配置

根据论文中的实验(Hu et al., 2021),我们可以总结出一些实用的配置经验:

  1. 应用位置:在Transformer中,将LoRA仅应用于自注意力模块的 W_q 和 W_v 矩阵,通常就能获得与全参数微调相当甚至更好的性能。增加 W_k、W_o 或FFN层带来的收益递减。
  2. 秩 r 的选择:即使对于 d=12288 这样的大维度,使用极低的秩 r=1、2、4、8 也足够了。更高的秩(如 r=64、r=128)带来的性能提升非常有限。起始尝试可以从 r=8 开始。
  3. 缩放因子:LoRA 的输出通常乘上 α/r。α 取 r 的常数倍(如 α=2r),缩放因子就固定为 2,调整 r 时不必重调学习率。
  4. 初始化:矩阵 A 用小的随机初始化(论文用随机高斯;本节示例代码用的是 Kaiming 均匀初始化,效果等价),矩阵 B 初始化为零。这确保了在训练开始时 ΔW = BA = 0,模型行为与原始预训练模型一致。

📝 动手练一练

问题:假设我们有一个维度 d_model=512 的Transformer模型,计划对其 W_q、W_v、W_k、W_o 四个矩阵应用LoRA,每个矩阵的 d=k=d_model。如果选择秩 r=8,那么LoRA引入的总可训练参数量是多少?如果全参数微调这四个矩阵,参数量又是多少?计算参数量减少的倍数。

👉 点击查看参考答案

思路:每个LoRA模块包含 A ∈ ℝ^(r×d) 和 B ∈ ℝ^(d×r) 两个矩阵。共有4个这样的模块。全参数微调则是更新4个 d×d 的矩阵。

import torch
torch.manual_seed(0)

d = 512
r = 8
num_matrices = 4

# LoRA参数量
lora_params_per_matrix = (r * d) + (d * r)  # A + B
total_lora_params = lora_params_per_matrix * num_matrices

# 全参数微调参数量
full_finetune_params_per_matrix = d * d
total_full_params = full_finetune_params_per_matrix * num_matrices

reduction_ratio = total_full_params / total_lora_params

print(f"每个LoRA模块参数量: {lora_params_per_matrix}")
print(f"LoRA总可训练参数量: {total_lora_params}")
print(f"全参数微调总参数量: {total_full_params}")
print(f"参数量减少倍数: {reduction_ratio:.1f}x")
每个LoRA模块参数量: 8192
LoRA总可训练参数量: 32768
全参数微调总参数量: 1048576
参数量减少倍数: 32.0x

结论:在此配置下,LoRA仅需训练约3.3万个参数,而全参数微调需要约105万个参数,参数量减少了32倍。在真实的大模型(d=12288)中,这个倍数会达到数千倍。

常见错误

错误理解或写法现象或报错原因正确做法
忘记冻结原始权重 W_0训练时原始权重也被更新,失去了参数高效的优势,且可能破坏预训练知识。未设置 W_0.requires_grad = False 或未将其从优化器参数列表中排除。在构建LoRA层时,确保原始线性层的 weight 的 requires_grad 属性为 False。
将LoRA矩阵 A 和 B 初始化得过大训练初期模型行为剧烈偏离预训练模型,导致不稳定或难以收敛。ΔW = BA 在初始时就有很大的范数。A 用小的随机初始化(如Kaiming初始化),B 初始化为零,保证 ΔW 初始近似为零。
误以为LoRA一定会降低训练速度训练速度可能变化不大,甚至在某些情况下更快。LoRA的主要收益在于显存和存储,而非单步计算量。前向传播增加了 B(Ah) 的计算。但显存降低允许使用更大的批次,优化器状态减少也节省了内存和通信开销。理解LoRA的核心优势是显存效率和参数效率,并利用其进行多任务部署。
合并权重后忘记保存/加载正确状态部署时加载了未合并的权重和LoRA模块,导致模型架构不匹配或性能异常。训练保存的checkpoint包含原始权重和LoRA权重,部署时需要明确是使用合并后的权重还是分离的权重。训练后显式执行合并操作 W_merged = W0 + B @ A,并保存 W_merged。或者,在部署框架中明确支持动态加载LoRA权重。

本章小结

  1. 全参数微调大模型(如GPT-3)成本过高,催生了参数高效微调(PEFT)技术。
  2. LoRA的核心假设是模型适应新任务时的权重更新 ΔW 具有低内在秩。
  3. LoRA的实现是冻结预训练权重 W_0,并注入可训练的低秩矩阵 B 和 A,使得 W = W_0 + BA。
  4. LoRA的优势包括:极大减少可训练参数和GPU内存、无推理延迟(权重可合并)、便于多任务切换与存储。
  5. 实践建议:通常将LoRA应用于Transformer的 W_q 和 W_v 矩阵,秩 r 选择个位数(如4, 8)即可,初始化时令 ΔW 从零开始。
  6. 与适配器(Adapter)的区别:LoRA是并行附加,无延迟;适配器是串行插入,会增加延迟。

📋 行动清单

  • 修改上面第二个代码示例,尝试将LoRA也应用于 W_o 矩阵,观察总可训练参数量的变化。
  • 将第一个代码示例中的秩 r 从4改为1和16,分别计算参数量减少倍数,理解 r 对参数效率的影响。
  • (选做)查阅PyTorch代码,思考如何实现一个通用的 LoRAWrapper,能够方便地将任何 nn.Linear 层转换为LoRA层。

延伸阅读

  1. LoRA: Low-Rank Adaptation of Large Language Models(Hu et al., 2021,arXiv:2106.09685)—— 本节核心论文,阐述了LoRA的动机、方法与实验。
  2. Attention Is All You Need(Vaswani et al., 2017,arXiv:1706.03762)—— Transformer架构原论文,理解 W_q、W_v 等权重矩阵的位置。
  3. Language Models are Few-Shot Learners(Brown et al., 2020,arXiv:2005.14165)—— GPT-3论文,了解全参数微调大模型的具体挑战。

—— 小象教研组