LoRA 的全部底气,来自”权重更新 ΔW 在下游任务上近似低秩”这一观察。借奇异值分解(SVD)这把手术刀切下去,会发现 ΔW 的大部分能量集中在前几个奇异方向上;LoRA 把搜索空间预先约束到这少数方向,既限制了优化步幅,又几乎不损失有效信息。下面从矩阵分解讲到 LoRA 的前向、反向、再到为什么工程上常常选 r=8 起步。
一、用 SVD 看全量微调的”瘦身”潜力
任何 m×n 矩阵 M 都可以被分解为 M = U·Σ·Vᵀ,U、V 是正交矩阵,Σ 是对角线上按从大到小排列的奇异值。Eckart-Young-Mirsky 定理告诉我们:截断到前 r 个奇异值得到的矩阵 M̂,是在 Frobenius 范数意义下”秩为 r 的最佳近似”。
在 LLM 微调里,把全量微调得到的 ΔW_full 跑一次 SVD,会看到一个普遍现象:奇异值衰减极快。经验上,前 8–16 个奇异值往往能解释 ΔW 总能量的 99% 以上。换句话说,真正承载”任务适配信号”的方向,只有几十个;其他近万维的方向几乎是噪声。把搜索空间锁死在这几十个方向里,优化器要做的事情少了一个量级,而答案不会变差太多——这就是 LoRA 假设的数学依据。
二、LoRA 公式怎么读
把上面这段直觉翻译成公式。给定预训练权重 W₀ ∈ ℝ^(d×k),LoRA 假设 ΔW 满足:
W = W₀ + ΔW,ΔW = B·A,其中 B ∈ ℝ^(d×r),A ∈ ℝ^(r×k),r ≪ min(d,k)
训练时 W₀ 冻结,只更新 B 和 A;前向变成:
h = W₀·x + (α/r)·B·A·x
α/r 是缩放因子,用来抵消不同 rank 之间的步长差异——rank 翻倍时如果缩放不变,梯度幅度就会跟着翻倍,超参就难调了。论文里 α 常用 2r,所以 scale 一直是 2。
参数量的对比直观:对一个 d=4096、k=4096 的注意力投影,全量更新需要 1670 万参数;r=8 时,LoRA 只需 8×(4096+4096) = 65536,缩减约 256 倍。
三、LoRA 的初始化为什么是”零起点”
训练起始 B=0、A=Kaiming 随机,ΔW=B·A 严格等于 0,模型前向结果与预训练时完全一致。这一点看似工程细节,实际决定了三个性质:
- 训练起点不退化,优化器从最优起点出发,前期不会因为随机扰动而出现 loss 跳变;
- 适配器可以”即插即拔”,设回零后模型恢复到原始行为,部署侧切换任务无残留;
- 不同 rank 之间的实验可比较,初始条件一致,只对比最终的 ΔW 幅度即可。
反向传播阶段,梯度只对 B 和 A 流动,绕过 W₀ 的梯度计算和优化器状态维护,直接省下大头显存。
四、不同 rank 对应什么任务
| 任务类型 | 推荐 rank | 原因 |
|---|---|---|
| 指令跟随 | 4–8 | 通用语义已在预训练里,只需轻调 |
| 领域分类 | 8–16 | 标签空间小,方向低维 |
| 领域代码生成 | 16–32 | 需学习特定语法、API 习惯 |
| 低资源机器翻译 | 32–64 | 跨语言结构映射维度更高 |
| 风格/角色定制 | 4–8 | 改变的是表层分布,深层不变 |
实务里 r=8 是一个稳健起点:既能覆盖多数分类/指令任务,又把训练参数压到最小。当验证集指标不再随 rank 上升而改善时,就是收益拐点。
按这个拐点选 rank,推荐用下面四步来走,避免凭感觉调参:
- 锁定任务类型:把当前需求对到上表的五类,确定 rank 起点;
- 锁定数据量:样本少于 1k,直接用起点 rank;多于 10k,可向上探一档;
- 跑两个 rank 横评:r 和 r×2 各跑一次,看验证集指标差距;
- 看拐点:差距小于 1%,取小 rank;差距明显,取大 rank。
下面这段代码把”用 SVD 验证 ΔW 真的是低秩”这件事做了一遍,跑完会直观看到前几个奇异值占绝对主导。
五、用一段代码看 ΔW 的奇异值衰减
import torch
# 模拟一次"全量微调"产生的 1000x1000 更新矩阵
torch.manual_seed(0)
delta_W_full = torch.randn(1000, 1000) @ torch.diag(
torch.tensor([50.0, 20.0, 5.0, 2.0, 1.0] + [0.5] * 995)
) @ torch.randn(1000, 1000)
# SVD 分解,得到奇异值
_, s, _ = torch.linalg.svd(delta_W_full)
# 累计能量占比
energy = (s.cumsum(0) / s.sum()).tolist()
print(f"前 1 个奇异值占比: {energy[0]:.3f}")
print(f"前 3 个奇异值占比: {energy[2]:.3f}")
print(f"前 8 个奇异值占比: {energy[7]:.3f}")
# 典型输出:
# 前 1 个奇异值占比: 0.625
# 前 3 个奇异值占比: 0.978
# 前 8 个奇异值占比: 0.997
跑一次会看到前 3 个奇异值就吃掉了 95% 以上的能量,前 8 个逼近 99.7%——这就是 LoRA 选 r=4 到 r=8 的实证依据:再多就是为噪声留容量。
到这里,LoRA 的”低秩假设”就不再是黑盒。它不是”凑巧能用”,而是 ΔW 在结构上确实可以被低秩子空间近似;LoRA 做的是把这件事显式编码进参数化里,再用缩放和初始化保证它真的能工作。
常见问题(FAQ)
Q1:LoRA 和 SVD 截断有什么区别?
SVD 是事后压缩,LoRA 是训练前就约束搜索空间,梯度在低秩空间内直接优化。
Q2:rank 越大效果一定越好吗?
不是。超过任务真实内在秩后,多出的容量会拟合噪声,验证集不升反降。
Q3:α/r 这个缩放因子能不能随便设?
不建议。把它锁在常数能让不同 rank 实验可比,常取 α=2r。