微调大模型为什么只改一小部分参数就够?LoRA低秩分解原理拆解
想在开源模型上做微调,拦在前面的门槛往往不是数据,而是显存。一个70亿参数的模型,全量微调要吃掉多少显存?先按混合精度训练的标准开销算一遍。
权重用fp16存储,70亿乘2字节约14GB;反向传播产生的梯度同样14GB;Adam优化器要给每个参数维护一阶和二阶动量,用fp32存,每参数8字节,又是56GB。三块加起来接近84GB,还没算激活值和临时张量。单张80GB的卡都放不下。
LoRA(Low-Rank Adaptation,低秩适配)给出的方案是:不动原始权重,只训练一小撮新增参数,显存占用能压到原来的零头。这篇文章从它的数学假设讲起,把公式、参数账和代码串一遍。
全量微调到底贵在哪

拆开看,全量微调的成本由三部分构成:要更新的权重、对应的梯度、优化器状态。其中优化器状态这块容易被低估——Adam为每个可训练参数额外存两份fp32的量,这一项的体量是权重的四倍。
所以真正昂贵的不是"前向计算",而是"要更新的参数有多少"。这给了一个明确的优化方向:如果把可训练参数从70亿降到几百万,梯度和优化器状态就同步缩水,显存问题自然缓解。
问题是,只训练很少一部分参数,效果能追得上全量微调吗?
关键假设:微调带来的权重变化是低秩的
LoRA成立的前提,来自一篇关于内在维度的研究。其中有个结论:大模型在适配下游任务时,权重的变化量并不需要占满整个参数空间,它落在一个维度低得多的子空间里就能表达得不错。
换成矩阵的语言:假设某一层的原始权重是矩阵W0,微调后的权重是W0加上一个变化量ΔW。这个ΔW虽然是和W0同样大的矩阵,但它的"有效秩"很低——它携带的信息量,用远小于原维度的空间就能装下。
低秩的直观含义可以这么理解。一张4096×4096的矩阵有1677万个元素,但如果这1677万个数其实都由少数几个方向组合出来,那它本质上没那么"自由"。就像一个班级的考试成绩,看似几十个数,实际可能主要由"整体水平"和"偏科程度"两个因素决定。
ΔW = B·A:把大矩阵拆成两个瘦矩阵
既然ΔW是低秩的,就不必直接去学习这个d×k的大矩阵,而是把它分解成两个小矩阵相乘:
W = W0 + ΔW = W0 + B · A 其中: W0 ∈ R^(d×k) 原始权重,冻结,不参与更新 B ∈ R^(d×r) 可训练 A ∈ R^(r×k) 可训练 r ≪ min(d, k) 秩,取值远小于矩阵维度
这里A负责把输入降到r维的低秩空间,B再把它升回原来的维度。两个瘦矩阵一乘,得到的就是那个大矩阵的变化量。
参数账很直观。原来要学d×k个数,现在只要学r×(d+k)个数。
代入具体数字算一遍。取d = k = 4096,r = 8:
| 方案 | 可训练参数量 | 计算式 |
|---|---|---|
| 全量微调 | 16,777,216(约1677万) | 4096 × 4096 |
| LoRA(r=8) | 65,536(约6.5万) | 8 × (4096 + 4096) |
单个矩阵上,参数量降到0.39%。而实际挂到一个70亿参数的模型上时,因为只有部分层被加上适配器,整体可训练参数占比通常在千分之一量级。梯度显存和优化器状态随之同步下降,这就是显存能被压下来的原因。
缩放因子 α/r 与初始化
前向传播时,LoRA的改动落在哪里?原始计算是h = W0·x,加上适配器之后变成:
h = W0 · x + (α / r) · B · A · x
式子里多了一个缩放系数α/r。它的作用是让不同秩配置下的更新幅度保持可比——r调大了,B·A的数值尺度会变,用α/r拉回来,免得换一次秩就要重新调学习率。
经验上α常取r的1到2倍。r=8配α=16、r=16配α=32,都是见得到的组合。
初始化这一步有个细节很关键:A用随机高斯分布初始化,B直接初始化为零。这样在训练开始的那一刻,B·A整体等于零,模型的输出和微调前完全一致。训练是从原始能力出发平滑推进的,不会因为新增模块引入的随机扰动把已有能力带偏。
落到代码:用 peft 挂一个适配器
from peft import LoraConfig, get_peft_model config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], # 只挂到部分线性层 lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(base_model, config) model.print_trainable_parameters() # trainable params: 4,194,304 # all params: 6,738,415,616 # trainable%: 0.06
输出里的那行统计值得留意:可训练参数419万,占全体67亿的0.06%。这个数量级意味着,原本放不下的训练任务,现在消费级显卡也能跑起来。
target_modules决定适配器挂在哪些层上。只挂注意力的q_proj和v_proj是很常见的做法,参数省;如果任务对能力要求更高,也可以把k_proj、o_proj乃至FFN层的线性层一并加上,代价是参数量上升。
超参数怎么选
| 参数 | 作用 | 常见取值与取舍 |
|---|---|---|
| r(秩) | 决定适配器的表达容量 | 4/8/16/32/64;简单任务8够用,复杂任务往16到64调 |
| lora_alpha | 缩放因子,实际缩放为α/r | 常取r的1到2倍 |
| target_modules | 适配器挂载位置 | 注意力投影层常用;追求效果可加FFN层 |
| lora_dropout | 抑制过拟合 | 0.05到0.1;数据量小可适当提高 |
| learning_rate | 训练步长 | 可比全量微调高一些,1e-4到3e-4量级 |
r不是越大越好。秩提高会增加参数和过拟合风险,而在很多任务上加到某个值之后收益就趋于平缓。稳妥的做法是从r=8起步,观察验证集表现再决定要不要往上调。
推理阶段:适配器还能合并回去
LoRA还有一个工程上的便利。训练完成后,可以把B·A乘出来直接加到W0上:
merged_model = model.merge_and_unload() # 合并后:W = W0 + (α/r) · B · A
合并之后,模型结构跟原始结构完全一致,推理时没有额外计算、没有额外延迟,等于把学到的能力"焊"进了权重里。这一点对部署很友好。
当然也可以选择不合并,保留适配器独立存放。这样同一个基座模型可以挂载多套适配器,按请求切换不同能力,代价是每次前向都要多算一遍B·A。
它的边界在哪
LoRA省的是训练开销,但它不是处处适用。它假设下游任务需要的能力,可以用低秩的方式在原始权重的子空间里表达。如果目标任务跟基座的能力差得太远——比如让一个纯文本模型去理解图像——那么改动落在低秩子空间里的假设就不太成立,效果会打折。
另外,LoRA更新的是既有层上的适配器,它不改变模型的结构,也不新增能力。想让模型掌握全新的模态或全新的知识体系,往往还得考虑更重量级的方案。
至于显存,如果基座本身太大,连把权重装进显存都困难,LoRA也帮不上忙。这时候通常会叠加量化——把基座权重压到4比特存放,适配器仍用fp16训练,也就是QLoRA的思路,让更大规模的模型能在单卡上微调。
小结
把逻辑再捋一遍:全量微调之所以贵,是因为要更新的参数量太大,梯度和优化器状态跟着膨胀。LoRA的依据是"微调带来的权重变化是低秩的"这个假设,于是用ΔW = B·A把一个大矩阵拆成两个瘦矩阵,参数量从d×k降到r×(d+k),降幅可达几个数量级。初始化时让B为零保证起点不变,训练时用α/r控制更新幅度,推理时可以合并回原权重而不增加延迟。
理解了这个推导,再去看各种参数高效微调方案,会发现思路是共通的——都是在"可训练参数总量"和"表达能力"之间找平衡点,LoRA只是其中落地得比较广的一种。
