轻量微调与 LoRA:在小显存上用大模型
这一篇聚焦在“模型已经很大了,如何在有限资源下适配具体任务”:从最朴素的全参微调,到 LoRA、Prefix-Tuning 这类轻量方案的直觉和工程用法。
全参微调:效果最好,但最昂贵
最直接的做法是:把预训练 Transformer 当普通网络,所有参数都参与更新。
好处:
- 表达能力完全打开,下游任务如果数据足够多,效果往往最好;
- 理论上没有“适配瓶颈”,能充分利用预训练表示。
代价:
- 显存压力极大:不仅要存下所有参数,还要存优化器状态(如 Adam 的一阶/二阶矩);
- 训练成本高:每个 step 的前向/反向都要对全模型做一遍。
在大多数“纯应用”场景下,全参微调已经不现实,需要更轻量的方式。
冻结主干 + 小头微调:最简单的 Parameter-Efficient Tuning
最朴素的轻量微调思路是:
- 冻结大部分 Transformer 层,只在顶部加一个小头(或几层),只更新这些新加的参数。
这在 BERT 时代非常常见,例如:
- 用预训练 Encoder 编码句子;
- 在上面接一个两层 MLP 做分类,只更新 MLP 的参数。
好处:
- 显存和训练成本都很低;
- 实现简单,不需要改动主干结构。
不足:
- 对某些和预训练分布差别较大的任务,可能“不够解锁”,表现有限;
- 主干参数完全冻结,适配能力靠顶部几层,有时会感觉“瓶颈明显”。
LoRA 的直觉:只学“一个低秩残差”
LoRA(Low-Rank Adaptation)的关键想法是:
- 不直接改原始权重矩阵 $W \in \mathbb{R}^{d_{\text{out}} \times d_{\text{in}}}$;
- 而是在它旁边加一个低秩矩阵 $BA$,只训练 $A, B$:
其中:
- $A \in \mathbb{R}^{r \times d_{\text{in}}}$
- $B \in \mathbb{R}^{d_{\text{out}} \times r}$
- $r$ 很小(比如 4, 8, 16),远小于 $d_{\text{in}}, d_{\text{out}}$。
这样一来:
- 原始大矩阵 $W$ 在微调过程中保持冻结;
- 只新增一个秩为 $r$ 的“补丁” $BA$,参数量大幅减少。
工程上常见的做法是只对某些关键权重应用 LoRA,比如:
- 注意力里的 $W_Q, W_K, W_V, W_O$;
- 或者只对 $W_Q, W_V$ 做 LoRA。
LoRA 在代码层面大概长什么样?
抽象一下,一个原始线性层是:
1class Linear(nn.Module):
2 def __init__(self, in_features, out_features):
3 super().__init__()
4 self.weight = nn.Parameter(torch.empty(out_features, in_features))
5
6 def forward(self, x):
7 return x @ self.weight.T
LoRA 版的大致形态可以是:
1class LoRALinear(nn.Module):
2 def __init__(self, in_features, out_features, r=8, alpha=1.0):
3 super().__init__()
4 self.weight = nn.Parameter(torch.empty(out_features, in_features), requires_grad=False)
5 self.A = nn.Parameter(torch.empty(r, in_features))
6 self.B = nn.Parameter(torch.empty(out_features, r))
7 self.scaling = alpha / r
8
9 def forward(self, x):
10 base = x @ self.weight.T # 冻结的原始权重
11 lora = (x @ self.A.T) @ self.B.T * self.scaling
12 return base + lora
实际实现会更复杂一些(比如支持 merge/unmerge、不同 rank、bias 等),但直觉就是:让“调整空间”局限在一个低秩子空间里。
Prefix / Prompt Tuning:不改模型,只改输入
另一类轻量方法(Prefix-Tuning、Prompt-Tuning)则是:
- 不改模型权重;
- 在输入序列前面加一段“可学习的前缀”或者“可学习的 embedding”。
例如,在 Decoder-only 模型里,可以:
- 为每一层的 Self-Attention 引入若干“虚拟 token”的 K/V,作为“前缀”;
- 这段前缀的向量是可训练参数,下游任务只更新这些前缀向量。
直觉上,相当于:
- 给不同任务各自分配一段“任务专属提示”,这些提示在训练过程中被学出来;
- 主干模型保持不动,只通过不同提示来“切换行为模式”。
这类方法的优势在于:
- 不需要改动模型结构;
- 可以为不同任务保存不同的前缀/提示参数,非常适合多任务场景。
组合与实践:LoRA + 微调头部
在很多实际工程里,会把多种轻量方案组合使用,比如:
- 主干 Attention/FFN 层上加 LoRA;
- 顶部再加一个小的任务头,只更新头部 + LoRA 参数。
这种组合有几个好处:
- 适配空间足够大,下游任务效果通常接近全参微调;
- 新增参数量仍然远小于全参,方便在多任务、多版本之间切换;
- 便于“热插拔”:可以按需加载/卸载某个任务的 LoRA 权重。
什么时候选哪种方案?
可以按资源约束和任务数量来大致划分:
- 显存/算力富裕 + 单一关键任务:全参微调仍然是一个选项;
- 中等资源 + 任务相对简单:冻结主干 + 头部微调就够用;
- 资源紧张 + 多任务 / 多版本切换:LoRA / Prefix-Tuning 一类轻量方案性价比更高。
对日常工程来说,掌握 LoRA 这一类方法,基本就能把“把大模型迁到某个具体业务上”这件事变得可操作,而不是只能停留在“只能调用通用 API”这一层。