LoRA 的原理,手寫一遍
微調大模型,要不要把幾十億個參數全訓練一遍?LoRA 凍結原來的參數,只在旁邊加兩個小矩陣。在第 09 模組的小 GPT 上手寫 LoRA,只訓練 1.5% 的參數就改了它的風格。
- 約 45 分鐘
- 難度:深入
- 實測:2026-09-15 torch 2.14,Apple M4 CPU,固定隨機種子
程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。
上一課說,確實需要微調時,首選 LoRA。這一課弄明白它是什麼,並在第 09 模組訓練好的小 GPT 上親手實現一遍。
python lora_from_scratch.py
全量微調的麻煩
微調,就是在訓練好的模型上用新的資料接著訓練。最直接的做法是所有參數都參與訓練,叫全量微調。
對大模型來說,這很貴。訓練時,每個參數除了它本身,還要存它的梯度,AdamW 最佳化器還要為每個參數存兩個額外的數(第 08 模組第 5 課提到過,它記錄每個參數的"動量")。一個 70 億參數的模型,光這些就要上百 GB 的視訊記憶體。
而且每微調一次,就得到一份完整的新模型。為十個客戶各微調一個版本,就要存十份幾十 GB 的檔案。
LoRA 的想法
LoRA(Low-Rank Adaptation,低秩適配)的想法是:微調時,權重需要的改變數其實不需要那麼"複雜"。
一個線性層的權重是一個矩陣 W,比如 128×384。全量微調就是學一個同樣大小的改變數 ΔW,加到 W 上。LoRA 不直接學 ΔW,而是把它拆成兩個很瘦的矩陣相乘:
ΔW = A × B
(128 × 384) (128 × 8) (8 × 384)
49152 个数 1024 个数 + 3072 个数 = 4096 个数
中間那個 8 叫秩(rank,r)。A 和 B 加起來只有 4096 個數,是 ΔW 的十二分之一。r 越小,要學的參數越少,但能表達的改變也越有限。
訓練時,原來的 W 凍結不動,只訓練 A 和 B:
输出 = x × W + x × A × B × 缩放系数
───── ─────────────────────
原来的路 新加的支路
實現
包住一個訓練好的 nn.Linear,只要十幾行:
class LoRALinear(nn.Module):
"""包住一个已经训练好的线性层:原来的权重冻结不动,旁边加一条 A、B 两个小矩阵的"支路"。"""
def __init__(self, base: nn.Linear, rank=8, alpha=16):
super().__init__()
self.base = base
for p in self.base.parameters():
p.requires_grad = False # 原来的参数一个都不训练
self.A = nn.Parameter(torch.randn(base.in_features, rank) * 0.01) # 输入维度 × r
self.B = nn.Parameter(torch.zeros(rank, base.out_features)) # r × 输出维度,初始为 0
self.scale = alpha / rank
def forward(self, x):
# 原来的输出 + 支路的输出。B 一开始是 0,所以刚加上时模型的行为和原来完全一样
return self.base(x) + (x @ self.A @ self.B) * self.scale
有兩個細節。
B 初始化為 0。這樣一開始 A×B 是 0,支路沒有任何作用,模型和原來一模一樣。訓練從原模型出發,而不是從一個被隨機支路打亂的模型出發。A 不能也是 0,否則 A 和 B 的梯度都是 0,永遠學不動。
縮放係數 alpha / r。它控制支路的影響有多大。這樣調整 r 時,不必重新調學習率。r=8、alpha=16 是很常見的一組設定。
然後給模型裡每個注意力層的 qkv 和 proj 都換上 LoRA:
for p in model.parameters():
p.requires_grad = False
for block in model.blocks:
block.attn.qkv = LoRALinear(block.attn.qkv)
block.attn.proj = LoRALinear(block.attn.proj)
任務:讓它專寫五言絕句
第 09 模組訓練的小 GPT,什麼格式的詩都寫。現在只用五言絕句微調它,看它能不能改成專寫五言絕句。
原来的训练数据里,五言绝句占 10.4%(3538 首)
微调前:生成 200 首,其中五言绝句 67 首
模型共 1,639,296 个参数,要训练的只有 24,576 个(1.50%)
刚装上 LoRA 时,输出和原模型一样吗?True
微調前,200 首裡有 67 首是五言絕句。這比訓練資料裡的 10.4% 高不少,可能是因為五言絕句最短、最容易寫完整。
裝上 LoRA 後,要訓練的只有 24576 個參數,是整個模型的 1.5%。4 層,每層兩個 LoRA:qkv 的 A、B 是 128×8 和 8×384,proj 的是 128×8 和 8×128,一層 6144 個,4 層 24576 個。
"剛裝上 LoRA 時輸出一樣嗎?True",驗證了 B 初始化為 0 的效果。
訓練
只用五言絕句作為資料,訓練 300 步:
第 1 步 损失 4.356
第 100 步 损失 4.294
第 200 步 损失 4.335
第 300 步 损失 4.213
训练 300 步用了 23 秒
微调后:生成 200 首,其中五言绝句 191 首。前 5 首:
白头还作雨,一鬓自如霜。何时之饮在,试死势悠扬。
白发勤鸿急,秋贫梦不闲。夜寒休绕郡,秋杀杜陵陂。
才子俭高人,何言亦重重。言知不见人,辄空一一言。
一丛生碧霄,危棹度清湍。沙渚穿花发,苍苍隔鹤闲。
九陌两金阁,一枝归草间。何当重枕簟,归复独裴回。
200 首裡有 191 首是五言絕句,從 33.5% 提高到了 95.5%。只動了 1.5% 的參數,訓練了 23 秒。
有意思的是損失幾乎沒變,從 4.36 到 4.21。這說明模型寫詩的"水平"(預測每個字的準確程度)沒有提高,變的是它的"習慣":寫完四句五言之後,它學會了接著輸出換行,結束這首詩。LoRA 擅長的就是這類改變:調整格式、風格、行為,而不是教給模型大量新的知識。上一課說"微調改的是行為,知識靠 RAG",這裡看得很清楚。
儲存和合並
LoRA 的权重单独存下来只有 96 KB,整个模型是 6.3 MB
把 LoRA 合并回原来的权重之后,模型又变回 1,614,720 个参数,输出和合并前最大差别 5.7e-06
LoRA 的第一個好處:只需要存 A 和 B。這裡是 96 KB,模型本身是 6.3 MB。在大模型上,這個差距更加懸殊:幾十 GB 的模型,LoRA 權重可能只有幾十 MB。為十個客戶各微調一次,就是一份基座模型加十個小檔案。
第二個好處:可以合併。訓練完成後,把 A×B×縮放係數直接加到原來的 W 上,就得到一個普通的線性層:
def merged(self):
"""把支路合并回原来的权重,得到一个普通的线性层:推理时没有任何额外开销。"""
layer = nn.Linear(self.base.in_features, self.base.out_features)
with torch.no_grad():
# nn.Linear 的权重形状是 (输出, 输入),所以要转置
layer.weight.copy_(self.base.weight + (self.A @ self.B).T * self.scale)
layer.bias.copy_(self.base.bias)
return layer
合併後,模型的結構和參數量都和原來完全一樣,推理時不會變慢。輸出和合並前的差別是 5.7e-06,只是浮點數的誤差。
LoRA 為什麼有效
一個直覺上的解釋:預訓練的模型已經學會了大部分東西,微調只是在上面做一些"小調整"。2021 年提出 LoRA 的論文(Hu 等,《LoRA: Low-Rank Adaptation of Large Language Models》)的出發點就是:這種調整所需的改變數,本身是"低秩"的,用很小的 r 就能表達。
我們的實驗也符合這個解釋:讓模型"寫完四句就結束",是一個很簡單的行為改變,1.5% 的參數完全夠用。但如果想教給模型大量全新的知識,比如一整個新領域的內容,LoRA 的效果就不一定比得上全量微調了。
LoRA 還有一個常用的變種叫 QLoRA:把凍結的原模型量化成 4 位元(第 4 課會講量化)來省視訊記憶體,LoRA 的 A、B 仍然用正常的精度訓練。這讓在一張消費級顯示卡上微調幾十億參數的模型成為可能。
練習
- 把
rank改成 1、2、32,微調後五言絕句的比例分別是多少?參數量分別是多少? - 不用 LoRA,直接全量微調(所有參數都訓練,學習率改成 1e-4),同樣訓練 300 步,比較五言絕句的比例和訓練時間。
- 只給前饋網路加 LoRA(
block.mlp[0]和block.mlp[2]),不給注意力加,效果有什麼不同?
自測
1. LoRA 訓練的是哪些參數?原來的參數怎麼處理?
原來的權重全部凍結,不參與訓練。LoRA 在選定的線性層旁邊加兩個小矩陣 A 和 B,只訓練它們。輸出等於原來的輸出加上 x×A×B×縮放係數。
2. 為什麼 B 要初始化為 0?A 能不能也初始化為 0?
B 為 0 時 A×B 為 0,剛裝上 LoRA 的模型和原模型完全一樣,訓練從原模型出發。A 不能也為 0:如果 A 和 B 都是 0,兩者的梯度也都是 0,參數永遠不會更新。
3. LoRA 訓練完之後,推理時會變慢嗎?
可以不變慢。訓練完後把 A×B×縮放係數加到原來的權重上合併,得到的模型結構和參數量都和原模型一樣,推理的速度也一樣。不合並時多了一條支路的計算,會稍慢一點,但好處是可以隨時換用不同的 LoRA。
提問與討論
這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。
提問 +3 點,回答別人 +6 點。內容經審核後公開。
正在載入討論…