AI 互動教室 ‹ 個人地端大語言模型實作
下載 .py 單獨開啟實驗場 ↗ 留言回報
FINETUNE · 08

微調入門:
LoRA 與 SFT、DPO

上一課你把服務的每個數字攤到儀表板上——延遲、吞吐、佇列,看得見了。 這一課換個方向:不調服務,改模型本身。 想讓模型學你的語氣、你的領域知識,難道要重訓一顆幾十億參數的模型?不用。 把教科書凍起來,只在旁邊貼便利貼——參數量立刻縮小 128 倍,效果幾乎一樣好。 先拉拉看那張便利貼有多小:

W 4096 × 4096 🔒 教科書(凍結不動) + B(4096×r) × A(r×4096) 便利貼:只訓練這兩張

右邊的實驗場是真的 Python(在你的瀏覽器裡跑,不用安裝任何東西)。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。每一格程式碼都能改、能重跑, 改壞了重新整理就復原——這是你的沙盒,盡量玩。

01 · 比喻

凍結教科書,只貼便利貼

你想教模型一件新事情,但不想(也沒本錢)重印整本教科書。 LoRA 的做法是:教科書一個字都不改,只在旁邊貼便利貼

  • 原始模型=教科書:整顆凍結,訓練過程完全不更新。
  • LoRA adapter=便利貼:新增的兩個小矩陣,存下來只有幾 MB。
  • 推論時教科書 + 便利貼一起看:兩者合起來才是最終的權重。
  • 可以備很多張便利貼:客服語氣一張、法遵摘要一張、寫程式一張—— 同一顆底模掛不同 adapter,像換手機殼一樣隨時切換。

便利貼幾乎不輸重印整本書:多數任務上,LoRA 與全參數微調的落差在 5% 以內。 而它帶來的兩個好處在實務上更關鍵——

  • 記憶體可控:全參數微調除了權重,還要為每一個可訓練參數 多帶梯度與優化器狀態,記憶體直接爆炸。LoRA 只要調 r 就能把需求壓到現有的卡裝得下——用「換方法」取代「換硬體」。 (硬要在小記憶體的卡上靠 SSD 卸載撐全參數微調?實測(RTX 4090 24GB)的估計是慢 5–10 倍。)
  • 不容易把模型練壞:全參數微調容易造成災難性遺忘—— 學會新任務,原本會的事情忘了一半,而且參數要調到剛好非常難。 LoRA 動的部分小,模型的底子還在。
02 · 原理

四步看懂,不用數學

  1. W 太大,我們不更新它。Transformer 每層都有 d×d 的方陣, d=4096 就是 16,777,216 個參數——訓練時 W 完全凍結。
  2. 加一個低秩分解。把「要改多少」寫成 ΔW = B × A: B 是 d×r、A 是 r×d,r 遠小於 d(常見 4~16)。
  3. 參數立刻縮小。d=4096、r=16 時,可訓練參數從 16,777,216 掉到 131,072——小 128 倍。連帶梯度與優化器狀態,一層從 192 MiB 變成 1.5 MiB。
  4. 推論零負擔。訓練只更新 A 和 B;上線時把 B×A 加回 W 合成一個矩陣,不增加任何推論延遲

ΔW 不是「裸加」回去的

真正加回去的式子長這樣,前面有一個縮放係數在控制力道:

W′ = W + (α / r) × B·A
  • α 越大 → 越強(分子);r 越小 → 越強(分母)。 α 常見 16 或 32。怕把模型練壞就 α 小、r 大,更新最溫和。
  • 係數裡有 r,意思是「參數多」不等於「更新猛」——rank 改變時幅度會自動調回來。 右邊那格會用真的矩陣算給你看:α 加倍力道就加倍,r 從 16 降到 4 力道也剛好加倍。
  • B 初始化為零:所以掛上 adapter 的第一步 ΔW 每一格都是 0, 模型行為與原本完全一致,不會一掛上就跑掉。
  • 不需要額外的正規化層:W 已經帶著預訓練時的正規化,更新量又小,α/r 就足以控制。

這些參數在程式裡就是這幾行(訓練要在有 GPU 的機器上跑,這堂課的重點是每個參數你都看得懂):

model = FastLanguageModel.get_peft_model( model, r=16, lora_alpha=16, lora_dropout=0, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], use_gradient_checkpointing="unsloth", random_state=3407, )
03 · 直覺

ΔW 憑什麼是低秩的

上一節有個沒說破的賭注:把 ΔW 硬拆成 B×A,資訊不會掉太多嗎? LoRA 的關鍵洞察就是這句——微調帶來的更新是低秩的: 它只往少數幾個方向動,所以「r 個方向」的形狀描述得完。

右邊把兩個 64×64 的矩陣擺在一起讓你看差別:一個是有結構的更新 (少數幾個方向疊起來加一點雜訊),一個是純亂數矩陣。 只留前 4 個方向重建時——

  • 有結構的更新:相對誤差只有 4.6%(前 4 個方向就吃下 99.8% 的能量)。
  • 純亂數矩陣:還有 88.9% 的誤差,你留多少個方向都還原不了它。

這就是為什麼 r8 或 16 通常就夠,不必給到幾百。 反過來說:任務越是要模型學一整套新東西,這個假設就越吃緊,r 才需要往上加。

04 · 選擇

target_modules:便利貼貼在哪幾頁

一個 layer 裡不只一個線性層。target_modules 決定你在哪幾個矩陣上掛 adapter, 這一行差很多。用 d=4096、MLP 中間層 4d、r=16 來算一層的帳 (該層所有線性層權重合計 268,435,456):

  • ["q_proj","v_proj"]262,144/層(0.10%)—— 原論文做法,最少修改、最省資源。
  • attention 全掛(q,k,v,o):524,288/層(0.20%)——效果好一些,但 MLP 完全沒動。
  • "all-linear"1,507,328/層(0.56%)——主流做法, MLP 對知識儲存很重要,可調空間最大。

注意 gate_projup_projd → 4d 的大矩陣、 down_proj4d → d——光是 MLP 這三個投影就佔 983,040, 比 attention 四個投影加起來(524,288)還多。所以「多掛三個模組」不是多 75%, 是將近三倍

直覺選擇邏輯:資料多、要學新知識 → all-linear; 資料少、只想調語氣風格 → q_proj, v_proj 就夠。

05 · 資料

SFT 一問一答,DPO 一好一壞

LoRA 決定改哪些權重,資料決定往哪個方向改。微調有兩條主線路:

SUPERVISED FINE-TUNING

SFT

標準答案,讓模型模仿。把(指令, 標準回覆)成對餵進去, 最大化標準回覆的機率。教格式、語氣、領域知識、指令跟隨的基礎能力。

限制:模型只知道「什麼是對的」, 不知道「A 比 B 好在哪」——風格偏好、安全性這類相對性目標它學不到。

DIRECT PREFERENCE OPTIMIZATION

DPO

好壞對比,讓模型偏向較好的。同一個 prompt 兩個回覆: chosen(較好)與 rejected(較差),直接用偏好對訓練。

拉高 chosen、壓低 rejected 的相對機率,並用參考模型防止偏離太遠。 一個損失函數就取代了 RLHF 的 Reward Model + PPO 兩階段。

比較面向SFTDPO
資料格式prompt + response(單一標準答案)prompt + chosen + rejected(偏好對)
訓練目標交叉熵:模仿參考答案DPO loss:拉開 chosen / rejected 差距
學到什麼「正確答案長什麼樣」「哪種回答比較好」
額外需求無,單一模型即可需要參考模型(通常是 SFT 後的模型)
標註成本要寫出完整好答案,較貴只需比較兩個回覆,較便宜且一致性高
訓練階段對齊流程的第一步通常接在 SFT 之後,做偏好對齊

資料就是 JSONL,一行一筆,差別只在欄位:

// train_sft.jsonl {"prompt": "用一句話介紹台北", "response": "台北是融合傳統與現代的臺灣首都。"} // train_dpo.jsonl {"prompt": "用一句話介紹台北", "chosen": "台北是融合傳統與現代的臺灣首都。", "rejected": "台北就是個城市。"}

DPO 用 TRL 大約 15 行就能開跑,beta偏離參考模型的懲罰強度—— 右邊那格會讓你拉著它看分布怎麼被拉走、又怎麼被拉回來:

from trl import DPOConfig, DPOTrainer config = DPOConfig(output_dir="dpo-out", beta=0.1) trainer = DPOTrainer(model=model, args=config, train_dataset=dataset, processing_class=tokenizer) trainer.train() # 沒指定 ref model 時,TRL 會自動複製一份當參考模型

建議流程:先用高品質示範資料做 SFT 教會基本能力與格式 → 由人工或模型比較,收集偏好對(chosen / rejected)→ 在 SFT 模型上做 DPO,調風格、安全性、有用性。 兩者是互補不是取代;DPO 幾乎都建立在 SFT 之上——沒有基本能力,偏好對齊也調不出好結果。 最少幾百筆偏好對就看得到效果,重點是 chosen / rejected 的差距要反映你在意的品質面向

06 · 實戰

換你動手

右邊最下面有一格「你的實驗區」。三個挑戰,由易到難(每題都有折疊解答):

LEVEL 1

把 1️⃣ 的 r 從 16 拉到 8,看縮小倍數變成幾倍; 再把 d 拉到 8192,看全參數微調那根柱子跑到哪裡去。

LEVEL 2

在 2️⃣ 找出一組 (α, r),讓力道跟基準(α=16, r=16)幾乎一樣, 但可訓練參數是基準的 4 倍。這題做完,你就真的懂 α/r 了。

LEVEL 3

把 3️⃣ 那個「有結構的更新」改得更難壓縮(權重全改成 1.0,或把雜訊加大十倍), 再看 r=4 的誤差怎麼變——這對「該用多大的 r」意味著什麼?先猜,再驗證。

07 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

你手上有 300 筆客服對話,想讓一顆 8B 模型講話更像你們公司的客服(稱呼、格式、收尾語)。機器只有一張 24GB 的卡,還得同時跑推論服務。最合理的做法是?

解釋:這是典型的「資料少、只調語氣風格」——原論文的 q_proj, v_proj 正是為此而生,最少修改、最省資源,r=8 一層只有 131,072 個可訓練參數。A 的問題不是時間而是記憶體:全參數微調要為每個參數多帶梯度與優化器狀態,一層一個矩陣就吃掉約 192 MiB,24GB 還要分給推論服務,而且全參數微調容易造成災難性遺忘。C 方向對但過頭:all-linear + r=128 一層就有 12,058,624 個可訓練參數(是 B 的 92 倍),用 300 筆資料去撐這個量只會過擬合。D 順序反了——DPO 需要一個已經會做這件事的參考模型,沒有 SFT 打底,偏好對齊調不出好結果。

Q2 錯誤診斷

同事想讓 adapter「學得更用力」,把 r 從 16 開到 64、α 保持 16。實驗場算出來的更新力道卻只剩一半:

α=16, r=16 → ‖ΔW‖ = 0.4109 ← 基準 α=16, r=64 → ‖ΔW‖ = 0.2039 (0.50 倍)

解釋:加回去的是 W′ = W + (α/r)·B·A,r 在分母。r 從 16 變 64、α 不動,力道就掉一半——這正是「參數多不等於更新猛」的設計:rank 改變時幅度會自動調回來。想更用力,把 α 調大(α=32、r=64 就回到基準的 0.99 倍,而且參數量是 4 倍)。A 描述的機制不存在:LoRA 不需要額外的正規化層,W 已經帶著預訓練時的正規化。C 說反了——B 零初始化只影響第一步(那一刻 ΔW 每一格都是 0),訓練開始後 B 就不再是零,跟 r 大小無關。D 的「梯度被平均」在這裡沒有發生:力道差異完全來自 α/r 這個係數,不是梯度計算。

Q3 錯誤診斷

把 target_modules 從 attention 四個投影換成 all-linear,只是想「多掛三張便利貼」,可訓練參數卻跳了將近三倍:

d = 4096, r = 16, 32 層 attention 全掛(q,k,v,o): 524,288 / 層 all-linear(再加 gate,up,down):1,507,328 / 層 ← 2.875 倍

解釋:關鍵在矩陣的形狀,不在模組數量。attention 的四個投影都是 d×d,每個掛 LoRA 是 2·d·r,四個合計 524,288;而 gate/up 是 d→4d、down 是 4d→d,每個要 5·d·r,三個合計 983,040——比 attention 四個加起來還多。所以 all-linear = 1,507,328,是 attention-only 的 2.875 倍。D 的算法(模組數比例)看起來合理,正是這題要破的迷思。A 錯在次方:參數量是 2·d·r,跟 r 成正比不是平方。C 完全相反——LoRA 的前提就是 W 全程凍結,掛再多模組也不會解凍;真要訓練 W 本身,那叫全參數微調。

Q4 情境題

你已經用 800 筆高品質示範資料做完 SFT,模型現在會照你要的格式回答了。但抽查發現它常給出「技術上正確卻很敷衍」的回覆——你說不出完美答案長怎樣,卻一眼分得出哪個比較好。下一步最合適的是?

解釋:「說不出完美答案、但分得出高下」就是 DPO 的守備範圍——SFT 只教得了「什麼是對的」,教不了「A 比 B 好在哪」,而敷衍程度、風格偏好正是相對性目標。資料只要 prompt / chosen / rejected 三欄,最少幾百筆就看得到效果,而且「比較兩個回覆」比「寫出完整好答案」便宜、標註一致性也更高。你剛做完的 SFT 模型正好可以當參考模型,順序完全對得上。A 不是不行,但要寫出 2000 筆完整好答案成本高,而且模型仍然不知道「比較起來哪個好」。C 換掉的是「改哪些權重」,解決不了「資料教不了相對好壞」這個問題,還多了災難性遺忘的風險。D 只是把同一個學習目標學得更用力(更容易過擬合),方向沒變。

Python 環境載入中(首次約 30–60 秒)…讀完第 1 節它就好了