iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
AI Engineering

RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 23 篇

Day 23:微調 — 什麼時候該做、LoRA 的帳怎麼算

  • 分享至 

  • xImage
  •  

主軸二的最後一個技術主題。前七天我們讓模型「知道」(RAG)、「能做」(Agent),今天讓它「像我們要的樣子」。

我把微調放在最後,是刻意的——因為它是最後才該考慮的手段。

今天要解決的問題

一個我經常被問的問題,以及一個經常錯誤的答案:

「我們公司有很多內部文件,是不是該微調一個自己的模型?」

答案通常是「不是」。 因為提問者真正想解決的是「讓模型知道公司的事」,而這是 RAG 的工作,不是微調的工作。

微調解決的是另一類問題:讓模型用你要的格式、風格、術語回答。分清楚這兩件事,能省下你三個月的時間與可觀的 GPU 費用。

📊 職缺訊號

「模型微調與客製化」只出現在 13.9% 的生成式 AI 職缺中,是八項任務中最低的。這個數字很誠實地反映了實務:多數應用不需要微調。但它出現時通常在資深職缺,因為它要求你能判斷「該不該做」——這比「會不會做」更難。


一、現象:三種手段的分工

image

圖 23-1:模型表現不佳時的手段選擇(示意決策流程)。微調在最右下角——它是最後一個選項,不是第一個。

手段 解決 不解決 成本
提示工程 格式、簡單風格、單點任務 複雜格式的穩定性 最低
RAG 事實知識、時效性、可追溯 風格、語氣、輸出格式 中
微調 風格、格式、領域術語、任務特化 注入新知識(效果差且不可追溯) 高

「微調不適合注入知識」這句話值得展開。 你可以硬用微調把公司文件塞進模型,但會遇到三個問題:模型會用「機率上合理」的方式重組事實(也就是幻覺)、無法追溯來源、文件更新就要重訓。同樣的需求,RAG 全部都做得更好。

二、原理:LoRA 為什麼可行

如果決定要微調,LoRA 幾乎是唯一合理的起點。它的核心觀念是:模型適應新任務時所需的權重變化,其實是低秩的——換句話說,那個巨大的變化矩陣可以用兩個瘦長的小矩陣相乘來近似。

LoRA 的參數量與顯存效益

圖 23-2:LoRA 的參數量與顯存效益(8B 模型、32 層、4 個投影,依公式精算)。左圖:r=8 時可訓練參數僅約 840 萬,是全參數的 0.1%。右圖:QLoRA 讓 8B 模型的訓練顯存需求落在單張消費級顯卡可負擔的範圍。

具體算一次(這也是面試會考的):

每個 LoRA 模組的參數量 = r × (d_in + d_out)
8B 模型:d = 4096、32 層、對 q/k/v/o 四個投影加 LoRA
→ 模組數 = 32 × 4 = 128
→ r = 8 時:128 × 8 × (4096 + 4096) ≈ 8.4M 個參數
→ 佔全參數比例 = 8.4M / 8.03B ≈ 0.10%

顯存的差別更關鍵:全參數微調要存權重、梯度、以及 Adam 的兩個動量狀態,8B 模型約需 96 GB;LoRA 只需要為那 0.1% 的參數存梯度與動量,加上 4-bit 量化的基底權重(QLoRA),總需求可壓到 10 GB 上下——這就是「單張消費級顯卡能微調 8B 模型」的來源。

三、動手:一次可重現的 LoRA 微調

3.1 資料準備(這一步決定八成成敗)

"""prepare_data.py —— 微調資料的品質遠比數量重要。"""
import json

# 格式必須符合模型的 chat template,不同模型不一樣
examples = [
    {"messages": [
        {"role": "system", "content": "你是法律事務所的書狀助理,"
                                      "用本所慣用格式與用語草擬文件。"},
        {"role": "user", "content": "幫我擬一份租賃契約終止通知"},
        {"role": "assistant", "content": "受文者:○○○ 君\n主旨:..."},   # ← 真實的範例輸出
    ]},
    # ... 50–500 筆
]

with open("train.jsonl", "w", encoding="utf-8") as f:
    for ex in examples:
        f.write(json.dumps(ex, ensure_ascii=False) + "\n")

四個資料準備的原則:

  1. 品質 > 數量。 50 筆高品質、格式一致的資料,勝過 5,000 筆雜訊資料。
  2. 輸出必須是你真正想要的樣子。 模型會忠實地學會你給的樣本——包括它們的缺點。
  3. 保留一份 held-out 集(10–20%),用來與基礎模型對照。
  4. 確認授權與個資。 用客戶文件微調前,先確認合約允許,並完成去識別化(Day 27)。

3.2 訓練

"""train_lora.py —— 用 TRL + PEFT 做 LoRA 監督微調。"""
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from trl import SFTTrainer, SFTConfig
import torch

MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"      # 從小模型開始驗證流程

# QLoRA:以 4-bit 載入基底模型,大幅降低顯存
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
                         bnb_4bit_compute_dtype=torch.bfloat16)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, quantization_config=bnb,
                                             device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

lora = LoraConfig(
    r=8,                  # 秩:8–16 適合多數風格/格式任務;越大越貴且易過擬合
    lora_alpha=16,        # 慣例是 r 的 2 倍
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    task_type="CAUSAL_LM",
)

trainer = SFTTrainer(
    model=model,
    train_dataset=load_dataset("json", data_files="train.jsonl")["train"],
    peft_config=lora,
    processing_class=tokenizer,
    args=SFTConfig(
        output_dir="./lora-out",
        num_train_epochs=3,          # 小資料集 2–4 輪即可,太多會過擬合
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,          # LoRA 的學習率比全參數微調高一個量級
        logging_steps=10,
        bf16=True,
        seed=42,                     # 可重現(Day 05 的第五根柱子)
    ),
)
trainer.train()
trainer.save_model("./lora-out")     # 只存 adapter,通常只有幾十 MB

💡 沒有 GPU 怎麼辦

用 Google Colab 或 Kaggle 的免費 T4(16 GB)即可跑 1.5B 模型的 QLoRA。先用小模型把整條流程跑通——資料格式、訓練、評估、部署——再考慮換大模型。多數人卡住的是流程,不是算力。

3.3 評估:一定要與基礎模型對照

"""evaluate_lora.py —— 沒有對照組的微調評估沒有意義。"""
cases = load_holdout()      # 訓練時沒看過的資料

for name, mdl in [("基礎模型", base_model), ("微調後", tuned_model)]:
    fmt_ok = sum(check_format(mdl.generate(c["input"])) for c in cases) / len(cases)
    judge  = sum(llm_judge_score(mdl.generate(c["input"]), c["expected"])
                 for c in cases) / len(cases)
    print(f"{name}:格式合規率 {fmt_ok:.1%}、風格評分 {judge:.2f}")

# 也要檢查「災難性遺忘」——微調後是否失去了原本的通用能力
for q in GENERAL_ABILITY_PROBES:      # 一般常識、推理、多語能力的題目
    print(q, "→", tuned_model.generate(q))

災難性遺忘是最常被忽略的檢查。 模型可能把你要的格式學得很好,卻失去了原本的推理或多語能力。微調後一定要跑一組「與微調任務無關」的題目確認沒有退化。

四、取捨:微調 vs 其他手段的決策

情境 建議 理由
要模型知道公司政策 RAG 可更新、可追溯、成本低
要模型用固定的公文格式 先提示 + few-shot,不穩定再微調 提示通常就夠了
要模型使用大量業界術語 微調(或 RAG 提供術語表) 術語是「怎麼說」,屬於風格範疇
要壓低推論成本 微調小模型取代大模型 這是微調最被低估的用途
要模型學會新的推理能力 換模型或拆解任務 微調小資料集無法賦予新能力
資料每週更新 RAG 微調的更新週期跟不上

📌 微調最被低估的用途:成本優化

假設你的任務用大模型每次要 $X,用小模型只要 $X/20 但品質不夠。用大模型的輸出當訓練資料,微調小模型(蒸餾),常常能讓小模型在這個特定任務上達到接近大模型的表現。

對高頻、單一用途的任務(分類、擷取、標準格式生成),這個做法的投資報酬率非常高——而且它與 RAG 不衝突,可以疊加使用。

最後,微調也要 MLOps。 這是雙主軸在此交會的地方:資料集要版本化(Day 09)、訓練要追蹤(Day 10)、模型要註冊(Day 10)、部署要走同一套流程(Day 12–13)。微調不是一次性的實驗,是一條要維護的管線。


今日小結

  • 微調是最後才該考慮的手段。先問問題屬於哪一類:不知道事實 → RAG;不會用工具 → 工具呼叫;格式風格不對 → 先提示,不穩定再微調。
  • 微調不適合注入知識——會產生無法追溯的幻覺,且文件更新就要重訓。
  • LoRA 的原理是「權重變化是低秩的」:r=8 時可訓練參數僅約 0.1%,QLoRA 讓 8B 模型的訓練顯存降到約 10 GB。
  • 資料準備決定八成成敗:品質 > 數量、輸出要是你真正想要的樣子、保留 held-out、確認授權與個資。
  • 評估一定要與基礎模型對照,並檢查災難性遺忘(跑一組與微調任務無關的題目)。
  • 微調最被低估的用途是成本優化:用大模型的輸出微調小模型,在特定任務上以 1/20 的成本達到接近的品質。
  • 微調也要 MLOps:資料版本、實驗追蹤、模型註冊、部署流程——雙主軸在這裡交會。

延伸閱讀

  • 📄 Hu et al., LoRA: Low-Rank Adaptation of Large Language Models (2021)——圖 23-2 那個低秩分解的來源
  • 📄 Dettmers et al., QLoRA: Efficient Finetuning of Quantized LLMs (2023)——4-bit 基底模型的做法,讓消費級顯卡也能微調

上一篇
Day 22:Agent(二) — MCP、多代理與可靠性護欄
下一篇
Day 24:評測體系 — LLM-as-a-Judge 與那個沒人算的 κ 值
系列文
RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言