Series · Transfer Learning · Chapter 9

迁移学习(九):参数高效微调

从低秩适配的数学原理出发,系统讲解 LoRA、Adapter、Prefix-Tuning、Prompt-Tuning、BitFit、QLoRA 等参数高效微调方法,附 LoRA 从零实现与方法选型指南。

GPT-3 有 1750 亿参数,全量微调一次要占 700 GB 显存,再加上梯度和优化器状态,单卡根本放不下;要为 100 个客户分别定制一份模型,光存储就 70 TB 起步。**参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)**给出的答案是:把预训练权重冻住,只训练一份不到 1% 的"增量",单张消费级显卡就能微调几十亿乃至几百亿参数的模型,性能几乎不掉。

本文从低秩假设的数学动因讲起,依次推导 LoRA、Adapter、Prefix-Tuning、Prompt-Tuning、P-Tuning v2、BitFit、QLoRA 的设计逻辑,给出 LoRA 的从零实现,最后用 GLUE 上的实测数据告诉你"什么场景该选什么方法"。

你将学到

  • 为什么"权重更新是低秩的"这个假设站得住脚
  • LoRA 的数学推导、初始化、缩放因子与权重合并
  • Adapter 瓶颈结构的设计与插入位置
  • Prefix-Tuning、Prompt-Tuning、P-Tuning v2 的区别
  • QLoRA 如何把 65B 模型塞进单张 48 GB 显卡
  • 各方法在 GLUE 上的性能对比与选型建议

预备知识

  • Transformer 结构(Attention、FFN、残差与 LayerNorm)
  • 矩阵分解基础(秩、SVD)
  • 迁移学习基础(本系列第 1-6 篇)

全量微调的困境

形式化地,全量微调对所有参数 $\boldsymbol{\theta}$ 做优化:

$$\boldsymbol{\theta}^* = \arg\min_{\boldsymbol{\theta}} \mathcal{L}(\boldsymbol{\theta}).$$

代价是真实的:

  • 显存爆炸:175B 模型 FP32 权重 ~700 GB,加上梯度与 Adam 的两份动量,全量微调需要数 TB 显存。
  • 存储成本:每个任务一份完整 checkpoint,100 个任务就是 70 TB。
  • 灾难性遗忘:所有参数都在动,预训练知识容易被破坏。
  • 服务复杂度:上线时无法用单一基座 + 路由的方式同时服务多个任务。

PEFT 把问题改写成加性分解:

$$\boldsymbol{\theta}^* = \boldsymbol{\theta}_0 + \Delta\boldsymbol{\theta}, \qquad |\Delta\boldsymbol{\theta}| \ll |\boldsymbol{\theta}_0|.$$

预训练权重 $\boldsymbol{\theta}_0$ 冻结、共享,每个任务只训练并存储一份很小的 $\Delta\boldsymbol{\theta}$。“基座 + 增量"的范式让模型部署从奢侈品变成了日用品。

方法可训练参数占比存储节省
全量微调100%0%
LoRA0.1% - 1%99% - 99.9%
Adapter0.5% - 2%98% - 99.5%
Prefix-Tuning~0.1%~99.9%
Prompt-Tuning<0.01%>99.99%
BitFit~0.1%~99.9%

LoRA:低秩适配

LoRA 低秩分解:W = W_0 + (alpha/r) * B * A

一句话讲清楚

LoRA 假设 权重更新 $\Delta\mathbf{W}$ 本身是低秩的,于是用两个"瘦长矩阵"的乘积来参数化它:

$$\mathbf{W}' = \mathbf{W}_0 + \frac{\alpha}{r}\, \mathbf{B}\mathbf{A}, \qquad \mathbf{A} \in \mathbb{R}^{r \times d_{\text{in}}},\; \mathbf{B} \in \mathbb{R}^{d_{\text{out}} \times r},\; r \ll \min(d_{\text{in}}, d_{\text{out}}).$$

冻结的 $\mathbf{W}_0$ 有 $d_{\text{in}} d_{\text{out}}$ 个参数;可训练的增量只有 $r(d_{\text{in}} + d_{\text{out}})$ 个。以 $d=4096, r=8$ 为例,LoRA 引入的参数仅占原矩阵的 0.39%

为什么"低秩"是个好假设

两个经验结果支撑了这个假设:

  1. 本征维度(Aghajanyan 等,2020):微调过程实际上只在权重空间的一个低维子空间里走动。在不少任务上,几百个参数的子空间就足以追平全量微调。
  2. 预训练矩阵的奇异值谱:注意力投影矩阵的奇异值衰减很快,少数方向承载了大部分能量;而"在已经很专业的模型上做任务适配"所需要的更新方向,比这个还要更集中。

直觉上,预训练模型已经几乎什么都会了,下游任务只是让它"换一个角度看问题”,自然不需要动太多自由度。

初始化、缩放与前向

落地 LoRA 时有几个细节决定成败:

  • 初始化:$\mathbf{A} \sim \mathcal{N}(0, \sigma^2)$(Kaiming 风格),$\mathbf{B} = \mathbf{0}$。这样保证了第 0 步时 $\Delta\mathbf{W} = \mathbf{0}$,模型从预训练行为开始训练,不会一步把它打坏。
  • 前向计算:$\mathbf{h} = \mathbf{W}_0 \mathbf{x} + \tfrac{\alpha}{r}\,\mathbf{B}(\mathbf{A}\mathbf{x})$。绝对不要显式构造 $\mathbf{B}\mathbf{A}$——它就是那个 $d_{\text{out}}\times d_{\text{in}}$ 的稠密矩阵,正是我们要避免的。
  • 缩放因子 $\alpha/r$:把"学习率"和"秩"解耦。秩翻倍时,等效学习率不变,因此 $\alpha$ 可以在不同 rank 之间复用。
  • 权重合并:推理时执行 $\mathbf{W}_{\text{merged}} = \mathbf{W}_0 + (\alpha/r)\,\mathbf{B}\mathbf{A}$,把增量合并回原权重。推理时延零额外开销——你交付的依旧是单个权重矩阵。
  • 该装在哪:实测把 LoRA 装在 Attention 的 queryvalue 投影上效果最好;只装其中一个不够,装到所有线性层收益边际明显,性价比反而下降。

体感对比:到底小多少?

不同方法的可训练参数量(对数刻度)

以 GPT-3 175B 为基座,全量微调与 LoRA $r=8$ 的可训练参数差距大约是 五个数量级——但稍后我们会看到,在 GLUE 上得分几乎一样。


Adapter:在 Block 中插入瓶颈模块

Adapter 在 Transformer Block 中的位置

Houlsby 等人(2019)走的是另一条路:不修改原有权重,而是在每个 Transformer Block 里插入小型可训练模块。Adapter 是一个带残差的瓶颈结构:

$$\text{Adapter}(\mathbf{h}) = \mathbf{h} + \mathbf{W}_{\text{up}}\, \sigma(\mathbf{W}_{\text{down}}\, \mathbf{h}), \qquad \mathbf{W}_{\text{down}} \in \mathbb{R}^{m \times d},\; \mathbf{W}_{\text{up}} \in \mathbb{R}^{d \times m},\; m \ll d.$$

“先降维再升维"的把戏和 LoRA 一脉相承,区别在于:Adapter 是新加一层,LoRA 是改写一层。把 $\mathbf{W}_{\text{up}}$ 初始化在零附近,可以让新插入的 Adapter 一开始相当于恒等映射,不会破坏预训练行为。

Adapter vs LoRA

AdapterLoRA
改了什么新加一层重新参数化已有权重
推理时延增加(串行多了一层)无(合并进 $W_0$)
单任务存储每层 ~$2md$ 参数每层 ~$r(d_\text{in}+d_\text{out})$ 参数
适合编码器(BERT 系)生成式(GPT 系)

后续两个变体值得了解。Pfeiffer Adapter 每个 Block 只在 FFN 之后保留一个 Adapter,参数减半但效果几乎不掉。并行 Adapter(He 等,2021)把瓶颈与 FFN 并行计算,避免 GPU 上的串行依赖,把推理延迟拉得很接近 LoRA。


Prefix-Tuning:前置软 Token

Prefix-Tuning:每层都加可学习的虚拟 token

Prefix-Tuning(Li & Liang, 2021)干脆不动模型权重,而是在每一层的 key/value 序列前面,拼接 $m$ 个可学习的"虚拟 token”

$$\bigl[\mathbf{P}_1, \ldots, \mathbf{P}_m,\; \mathbf{x}_1, \ldots, \mathbf{x}_n\bigr] \to \text{Transformer}.$$

只有这些前缀矩阵参与训练,参数总量是 $m \times d \times L \times 2$(key + value,$L$ 层)。两个工程细节:

  • 直接优化前缀向量训练不稳。原论文先把前缀过一个小 MLP 再喂给模型,推理时把 MLP 丢掉、只保留 MLP 输出的前缀向量。
  • 前缀容量有限,硬任务上比 LoRA 差 0.5-1 分。

直觉上,这些前缀就像每层注意力的"调音器"——它们从第一层开始就引导模型对真实 token 的注意力分布,相当于把任务知识压缩在了 K/V 缓存里。


Prompt-Tuning 与 P-Tuning v2

Prompt-Tuning 只调输入层;P-Tuning v2 每层都调

Prompt-Tuning(Lester 等,2021)是极简版本:只在输入层前加 $m$ 个可学习 prompt,参数量仅 $m \times d$,往往只有几千个。

代价是它只在足够大的模型上才好用。Lester 等人的实验显示,模型规模越大,与全量微调的差距越小:1B 以下差距明显,10B 以上几乎可以忽略。直觉上,参数量大的模型自带"翻译任意连续向量"的能力,小模型则需要更局部、更具体的修改。

P-Tuning v2(Liu 等,2022)是 Prompt-Tuning 与 Prefix-Tuning 的折中:在每一层都加可学习 prompt,而不只是输入层。结果是无论模型大小、无论任务难易,都能追平全量微调,可训练参数依然只有 ~0.1%。

BitFit:只调 Bias

最极简的方法:只微调偏置项(Zaken 等,2021)。BERT 的偏置约占总参数的 0.08%,但在小数据 GLUE 任务上能与全量微调打平。原理也直观——偏置改变了每个非线性的输入分布,相当于不改"线性映射本身"的前提下,重新分配激活路径。它当 baseline 极其便宜;当主力方法时,比 LoRA 略弱。


QLoRA:量化 + LoRA

QLoRA:4-bit 基座 + 分页优化器 + bf16 LoRA 适配器

QLoRA(Dettmers 等,2023)让单张 48 GB 显卡微调 65B 模型成为可能。它把三件事组合在一起:

  1. NF4(4-bit NormalFloat)量化冻结的基座权重。NF4 是在"预训练权重近似零均值高斯"的假设下信息论最优的码本——而这个假设大体成立。
  2. 双重量化:把每个 block 的量化常数本身再量化一次,每个参数再省 ~0.4 bit。
  3. 分页优化器:让 Adam 的状态在 CPU 与 GPU 之间通过统一内存分页,长序列时不再 OOM。

关键点是:梯度仍然能穿过量化矩阵传到上面 bf16 的 LoRA 适配器。基座永远不需要 FP16 副本,只有小小的 LoRA 参数需要。右图说明了重量级:65B 全量微调要 ~700 GB 显存,QLoRA 只要 ~50 GB。

QLoRA 在 Vicuna 评测上与全量微调相比性能下降 <2%,硬件成本却下降了一两个数量级。


实现:从零写一遍 LoRA

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F


class LoRALayer(nn.Module):
    """W' = W_0 + (alpha / r) * B @ A.

    基座权重冻结;只有 A、B 参与梯度计算。
    """

    def __init__(self, in_features: int, out_features: int,
                 rank: int = 8, alpha: float = 16.0,
                 dropout: float = 0.0):
        super().__init__()
        self.in_features, self.out_features = in_features, out_features
        self.rank, self.alpha = rank, alpha
        self.scaling = alpha / rank

        # 冻结的预训练权重(之后从原 Linear 拷贝进来)
        self.weight = nn.Parameter(
            torch.empty(out_features, in_features), requires_grad=False)
        self.bias = nn.Parameter(
            torch.zeros(out_features), requires_grad=False)

        # 可训练的低秩因子
        self.lora_A = nn.Parameter(torch.empty(rank, in_features))
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
        nn.init.kaiming_uniform_(self.lora_A, a=np.sqrt(5))

        self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
        self._merged = False

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        base = F.linear(x, self.weight, self.bias)
        if self._merged:
            return base
        # 关键:绝不显式构造 B @ A
        delta = (self.dropout(x) @ self.lora_A.T) @ self.lora_B.T
        return base + self.scaling * delta

    @torch.no_grad()
    def merge_weights(self) -> None:
        """把 LoRA 增量合并回原权重,推理零额外开销。"""
        if self._merged:
            return
        self.weight.add_(self.scaling * (self.lora_B @ self.lora_A))
        self.lora_A.zero_(); self.lora_B.zero_()
        self._merged = True


def apply_lora(module: nn.Module, rank: int = 8, alpha: float = 16.0,
               targets: tuple[str, ...] = ("query", "value")) -> None:
    """递归地把目标 nn.Linear 替换成 LoRALayer。"""
    for name, child in module.named_children():
        if isinstance(child, nn.Linear) and any(t in name for t in targets):
            lora = LoRALayer(child.in_features, child.out_features, rank, alpha)
            lora.weight.data.copy_(child.weight.data)
            if child.bias is not None:
                lora.bias.data.copy_(child.bias.data)
            setattr(module, name, lora)
        else:
            apply_lora(child, rank, alpha, targets)

三个值得注意的细节:

  1. 前向永远不构造 $\mathbf{B}\mathbf{A}$——先左乘 $\mathbf{A}$ 得到一个宽度只有 $r$ 的中间张量,再乘 $\mathbf{B}$。
  2. merge_weights 是一次性的、原地的合并。合并之后这个 Layer 与 nn.Linear 在数值上完全等价,部署时延和不用 LoRA 一模一样。
  3. apply_lora 沿模型树递归,只替换名字含 "query" / "value" 的层——这是 LoRA 论文给出的默认推荐。

方法对比:一张图看懂 GLUE 上的取舍

PEFT 方法在 GLUE 上的效率-准确率关系

把 RoBERTa-base 在 GLUE 上的近似结果画成气泡图,整个设计空间就清楚了:

  • LoRA $r=8$:仅训练 0.24% 的参数,与全量微调差距小于 0.1 分,性价比之王。
  • Adapter:与 LoRA 准确率相当,但参数量约 4 倍、推理还有额外延迟。
  • Prefix-Tuning:用很小的精度让步换来极少的参数,适合需要存几百份任务适配器的场景。
  • Prompt-Tuning:在 RoBERTa-base 这种规模上不够看;底座越大它越好用。
  • BitFit:作为最朴素的 baseline,效果好得令人意外。

选型指南

场景首选原因
GPT 类生成模型微调LoRA(q,v)零延迟、易合并、可扩展到 70B+
BERT 类多任务分类Adapter(Pfeiffer)单任务存储极小,工具链成熟
超大基座(>10B),多任务Prompt-Tuning存储开销可忽略,规模越大越好
推理时延零容忍LoRA + 合并最终模型就是单个权重矩阵
小样本(千例量级)Prefix-Tuning 或 BitFit强归纳偏置,过拟合风险低
单卡 24-48 GB,30B+ 模型QLoRANF4 + 分页 Adam 解锁这个尺寸段

LoRA 秩的选择:默认从 $r=8$ 开始。如果验证集差距超过 0.5 分,提到 16;显存吃紧降到 4。大模型反而更喜欢小 r——任务适配的本征维度并不会随模型尺寸线性增长。


Q&A

相对全量微调,PEFT 会损失多少性能?

模型规模 >10B 时通常 <1 分,常常落在噪声范围内;<1B 时差距可能 2-5 分,特别是数据量小的任务。

LoRA 的学习率怎么设?

比全量微调高 1-2 个数量级。可训练参数从零附近出发,可以承受更大的步长而不发散。常用配置:$\text{lr} = 1\text{e-}4$ 到 $5\text{e-}4$,配 cosine 衰减。

PEFT 能和量化结合吗?

这正是 QLoRA 在做的事——NF4 + 双重量化 + bf16 LoRA,把 65B 模型微调塞进单张 48 GB GPU,性能下降 <2%。

要不要把 LoRA 装到所有线性层?

不必。原论文实测 query+value 已经拿到大部分增益;加 key 和 output 边际很小,参数翻倍。Decoder-only LLM 上有时再加 MLP up/down 投影对长上下文任务略有帮助。

服务端 Adapter vs LoRA?

LoRA 合并后无任何额外延迟,与全量微调推理性能一致;Adapter 因为多了一层串行子层,延迟会增加 1-3%(并行 Adapter 能压到 1% 以下)。


小结

PEFT 的核心思想可以浓缩为一句话:预训练模型已经几乎什么都会了,下游任务只需要一个低维的"调音"

  • LoRA 用低秩分解参数化这个调音,零延迟、易合并,是当前事实上的默认方法
  • Adapter 用瓶颈模块在 Block 内插入调音,存储开销小但有推理延迟
  • Prefix-Tuning / Prompt-Tuning / P-Tuning v2 把调音做成"软 token",参数极少
  • BitFit 极简到只调 bias,作为 baseline 出奇地有用
  • QLoRA 把 4-bit 量化叠加到 LoRA 上,把单卡微调的尺寸上限推到了 65B

下一篇我们进入持续学习,看模型如何在不忘旧知识的前提下不断吸收新任务。

参考文献

  • Hu, E. J., et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models. ICLR.
  • Aghajanyan, A., Gupta, S., & Zettlemoyer, L. (2020). Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning. ACL.
  • Houlsby, N., et al. (2019). Parameter-Efficient Transfer Learning for NLP. ICML.
  • Pfeiffer, J., et al. (2021). AdapterFusion: Non-Destructive Task Composition for Transfer Learning. EACL.
  • He, J., et al. (2021). Towards a Unified View of Parameter-Efficient Transfer Learning. ICLR.
  • Li, X. L., & Liang, P. (2021). Prefix-Tuning: Optimizing Continuous Prompts for Generation. ACL.
  • Lester, B., Al-Rfou, R., & Constant, N. (2021). The Power of Scale for Parameter-Efficient Prompt Tuning. EMNLP.
  • Liu, X., et al. (2022). P-Tuning v2: Prompt Tuning Can Be Comparable to Fine-Tuning Universally Across Scales and Tasks. ACL.
  • Zaken, E. B., Ravfogel, S., & Goldberg, Y. (2021). BitFit: Simple Parameter-Efficient Fine-Tuning for Transformer-Based Masked Language-Models. ACL.
  • Dettmers, T., et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. NeurIPS.

系列导航

部分主题
1基础与核心概念
2预训练与微调
3域适应
4小样本学习
5知识蒸馏
6多任务学习
7零样本学习
8多模态迁移
9参数高效微调(本文)
10持续学习
11跨语言迁移
12工业应用与最佳实践

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub