# 實作課程:從入門到進階,訓練你的大型語言模型
**課程目標:**
許多人認為訓練大型語言模型(LLM)是一個需要龐大運算資源與數月時間的艱鉅任務。這在「從零開始預訓練 (Pre-training)」時確實如此。然而,在實務上,我們更常使用一種稱為「**微調 (Fine-tuning)**」的技術來客製化模型。
本課程將帶您從兩種不同層次的工具入手,體驗微調的威力:
1. **LLaMA-Factory**:學習使用更為強大、整合度更高的框架,透過圖形介面或命令列,高效微調真正的大型語言模型(如 Llama 3)。
2. **Hugging Face `SFTTrainer`**:學習使用業界標準的工具,以 Python 程式碼驅動,微調一個語言模型模型(如 Phi-4)。
**我們將完成:**
1. **核心觀念**:理解什麼是微調(Fine-tuning)。
2. **環境準備**:安裝所有必要的工具。
3. 使用 LLaMA-Factory 微調一個指令遵從 (Instruction-Following) 模型。
4. 使用 Hugging Face `SFTTrainer` 微調語言模型。
-----
## Part 1: 核心觀念 - 什麼是微調 (Fine-tuning)?
想像一下,一個已經精通多國語言的語言學家(這就是**預訓練模型**),他懂語法、懂語意,知識淵博。現在,你想讓他成為一名專業的「美食評論家」(這就是我們的**特定任務**)。
你不需要重新教他語言基礎,你只需要給他一批美食評論範例(**我們的資料集**),告訴他哪些是好評、哪些是負評。他會很快地運用他已有的語言知識,學會判斷影評的情緒。
這個過程,就是**微調**。它快速、高效,且對硬體資源的要求遠低於從零開始訓練。
-----
## Part 2: 環境準備
我們將分別為兩個階段安裝函式庫。
**1. Hugging Face `SFTrainer` 所需函式庫:**
```bash
pip install transformers datasets accelerate torch
```
**2. LLaMA-Factory 所需函式庫 **
```bash
# 1. 從 GitHub 複製專案
git clone https://github.com/hiyouga/LLaMA-Factory.git
# 2. 進入專案目錄
cd LLaMA-Factory
# 3. 安裝所有依賴項
# bitsandbytes 是實現 QLoRA 的關鍵
pip install -e .[torch,bitsandbytes]
```
-----
## Part 3: 使用 LLaMA-Factory 高效微調
套件來源:https://github.com/hiyouga/LLaMA-Factory?tab=readme-ov-file
官方教學:https://llamafactory.readthedocs.io/zh-cn/latest/getting_started/webui.html
教學網頁:https://the-walking-fish.com/p/llama-factory/
### 什麼是 LLaMA-Factory?
LLaMA-Factory 是一個整合了業界最先進技術的**一站式大型語言模型微調框架**。它將 Hugging Face `transformers`, `peft`, `accelerate` 等工具完美封裝,讓使用者可以透過**圖形介面 (Web UI)** 或**命令列**,輕鬆完成大型模型的微調。
**為什麼要使用 LLaMA-Factory?**
* **極致簡化**:無需撰寫複雜的 Python 程式碼,只需設定好參數即可開始訓練。
* **硬體友善**:內建 LoRA、QLoRA 等**參數效率微調 (PEFT)** 技術,讓您可以在消費級顯卡(如 RTX 3090/4090)上微調數十億參數的模型。
* **模型海量**:支援市面上幾乎所有主流的開源大型模型(Llama, Mistral, Qwen, Yi, Gemma 等)。
* **功能全面**:支援多種微調任務,如指令微調 (SFT)、人類偏好對齊 (DPO/ORPO) 等。
我們將以一個「指令遵從 (Instruction-Following)」任務為例,訓練一個能聽懂繁體中文指令的AI助理。
#### 步驟 1:安裝 LLaMA-Factory
LLaMA-Factory 的安裝比單純的函式庫稍複雜,建議在一個乾淨的 Python 環境中進行。
為了避免套件衝突,建議在一個乾淨的 conda 虛擬環境中進行。
```bash
# 建立一個新的 conda 環境
conda create -n llama-factory python=3.10 -y
conda activate llama-factory
# 1. 從 GitHub 複製專案
git clone https://github.com/hiyouga/LLaMA-Factory.git
# 2. 進入專案目錄
cd LLaMA-Factory
# 3. 安裝所有依賴項
# bitsandbytes 是實現 QLoRA 的關鍵
# flash-attn 是一個能大幅加速訓練的工具
pip install -e .[torch,bitsandbytes,flash-attn]
```
安裝完成目錄:

### 步驟 2:準備資料集 (JSONL 格式)
LLaMA-Factory 偏好使用 JSONL 格式的資料。對於**指令微調 (Supervised Fine-tuning, SFT)** 任務,最常見的格式是包含指示 (instruction) 和輸出 (output) 的對話形式。
讓我們創建一個簡單的問答資料集,教模型扮演一個台灣旅遊專家的角色。
1. 在 `LLaMA-Factory/data` 資料夾下,創建一個名為 `travel_expert_zh.jsonl` 的檔案。
2. 在檔案中貼上以下內容(每一行都是一個獨立的 JSON 物件):
```json
{"instruction": "", "input": "請推薦一個適合家庭旅遊的台灣景點。", "output": "當然!我推薦去花蓮的太魯閣國家公園,那裡有壯麗的峽谷景觀和多條適合全家大小的步道。"}
{"instruction": "", "input": "台灣最有名的小吃是什麼?", "output": "這很難說只有一個,但珍珠奶茶、小籠包和牛肉麵絕對是國際知名的三大代表!"}
{"instruction": "", "input": "去九份老街有什麼必吃的?", "output": "到了九份,您一定不能錯過芋圓和草仔粿,這兩樣都是當地非常有特色的傳統點心。"}
{"instruction": "", "input": "聽起來很棒!那附近還有其他可以逛的地方嗎?", "output": "有的,從九份老街可以順道去金瓜石的黃金博物館,了解當地的採礦歷史,風景也很優美。"}
{"instruction": "", "input": "台北有哪些適合夜市逛街的地方?", "output": "台北有幾個非常受歡迎的夜市,比如士林夜市、饒河街夜市和寧夏夜市,每個夜市都有各自的特色小吃和遊戲攤位。"}
{"instruction": "", "input": "我想去台南體驗當地文化,有什麼推薦的景點嗎?", "output": "台南是台灣的文化古都,推薦您參觀赤崁樓、安平古堡以及孔廟,這些地方都能讓您深入了解台南的歷史與文化。"}
```
3. **註冊資料集**:為了讓 Web UI 能找到我們的資料,打開 `LLaMA-Factory/data/dataset_info.json` 檔案,在最前面加入我們自訂資料集的資訊:
```json
{
"travel_expert_zh": {
"file_name": "travel_expert_zh.jsonl",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output"
}
},
// ... 其他原有的資料集資訊 ...
}
```
### 步驟 3:(選項A) 使用 Web UI 進行訓練 (推薦初學者)
這是 LLaMA-Factory 最吸引人的功能。
1. 在您的終端機中(確保您在 `LLaMA-Factory` 目錄下),啟動 Web UI:
```bash
llamafactory-cli webui
```
2. 打開瀏覽器,訪問 `http://localhost:7860`。

4. 3. 在圖形介面中,我們將詳細設定各個參數:
* **模型 (Model)**:
* `Model Name`: 選擇一個強大的繁體中文基礎模型,例如 `MediaTek-Research/Breeze-7B-Instruct-v1_0`。
* `Finetuning Method`: 選擇 `lora`,這是我們主要的 PEFT 方法。
* `Quantization bit`: 選擇 `4`,**這會自動啟用 QLoRA**,大幅降低顯卡記憶體需求。
* **資料集 (Dataset)**:
* `Dataset`: 在下拉選單中找到並勾選我們剛才建立的 `travel_expert_zh`。
* **訓練參數 (Arguments)**:
* `Output folder`: 訓練結果的儲存路徑,例如 `breeze_travel_expert_lora`。
* `Learning rate`: 學習率,通常 `5e-5` 是一個不錯的起始點。
* `Num train epochs`: 訓練輪次,對於指令微調,`3.0` 到 `5.0` 輪通常足夠。
* `Per device train batch size`: 每個 GPU 的批次大小,設為 `1` 或 `2` 以節省記憶體。
* `Gradient accumulation steps`: 梯度累積步數。設為 `4` 或 `8`。**小提示**:`batch_size * grad_acc_steps` 才是你真正的「有效批次大小」。這是在不增加顯卡記憶體的情況下,模擬更大批次訓練的技巧。
* `FP16/BF16 training`: 勾選 `fp16`,這能進一步加速訓練並節省記憶體。
* **LoRA 參數 (LoRA Arguments)**:
* `LoRA rank (r)`: LoRA 矩陣的秩,`8` 或 `16` 是常見的設定,數字越大代表轉接器越複雜,但需要訓練的參數也越多。
* `LoRA alpha`: LoRA 的縮放因子,通常設為 `rank` 的兩倍,例如 `16` 或 `32`。
* `LoRA trainable modules`: 指定要在哪些模組上附加 LoRA 轉接器。可以保持預設值,通常已經包含了注意力層。
4. **啟動訓練**:點擊 **Start** 按鈕,開始您的微調之旅!
會看到訓練日誌開始在頁面上滾動,訓練結束後,您的模型就會儲存在指定的輸出資料夾中。
### 步驟 4:(選項B) 使用命令列進行訓練
對於進階使用者,命令列提供了更高的可重複性。以下指令與 Web UI 的設定等效:
```bash
llamafactory-cli train \
--model_name_or_path MediaTek-Research/Breeze-7B-Instruct-v1_0 \
--dataset travel_expert_zh \
--finetuning_type lora \
--output_dir breeze_travel_expert \
--per_device_train_batch_size 2 \
--gradient_accumulation_steps 4 \
--lr_scheduler_type cosine \
--logging_steps 10 \
--save_steps 100 \
--learning_rate 5e-5 \
--num_train_epochs 3.0 \
--plot_loss \
--fp16
```
### 步驟 5:使用微調後的模型進行對話
訓練完成後,LoRA 的「轉接器」權重會被儲存在您指定的輸出資料夾中。
1. 在 Web UI 頂部切換到 **Chat** 分頁。
2. 在左側的 `Adapter path` 下拉選單中,選擇您剛才訓練好的 `breeze_travel_expert_lora`。
3. 點擊 **Load model**。
4. 現在,您可以在右側的對話框中,與您親手訓練出的旅遊專家進行對話了!
也可以用以下命令啟動一個互動式介面,與您親手訓練出的旅遊專家對話。
```bash
llamafactory-cli chat \
--model_name_or_path MediaTek-Research/Breeze-7B-Instruct-v1_0 \
--adapter_path breeze_travel_expert
```
#### 步驟 6(可選):匯出與合併模型
LoRA 微調產生的是一個輕量的「轉接器」,它需要依附在原始的基礎模型上才能運作。如果您想得到一個獨立的、完整的模型以方便部署,可以執行匯出合併的指令。
在終端機中執行:
```bash
llamafactory-cli export \
--model_name_or_path MediaTek-Research/Breeze-7B-Instruct-v1_0 \
--adapter_path breeze_travel_expert_lora \
--export_dir breeze_travel_expert_merged \
--export_size 7B \
--export_legacy_format False
```
這個指令會將 LoRA 轉接器的權重與基礎模型合併,並在 `breeze_travel_expert_merged` 資料夾中生成一個全新的、可以直接使用的完整模型。
-----
### 高效微調的魔法:LLaMA-Factory 為何如此強大?
LLaMA-Factory 的核心在於它整合了多項**參數效率微調 (Parameter-Efficient Fine-Tuning, PEFT)** 技術,讓硬體資源有限的使用者也能訓練數十億(Billion)參數的大型模型。
1. **PEFT 的核心思想**:
* **凍結 (Freeze)**:固定住龐大的預訓練模型主體(99.9% 的參數不動)。
* **附加 (Attach)**:在模型的特定位置(通常是注意力層)加上一些小型的、可訓練的「**轉接器 (Adapter)**」層。
* **專注訓練**:微調時,只更新這些新增的、極少量的 Adapter 參數。
2. **LoRA / QLoRA 技術**:
* 
* **LoRA (Low-Rank Adaptation)**:這是最主流的 PEFT 方法。它假設模型微調時的參數更新是「低秩」的,因此可以用兩個較小的矩陣來模擬這個更新,大幅減少需要訓練的參數數量(從數十億降至數百萬)。
* **QLoRA (Quantized LoRA)**:LoRA 的終極進化版!它在 LoRA 的基礎上,進一步將被凍結的主模型從 16 位元**量化 (Quantize)** 成 4 位元,讓模型在顯卡中的佔用空間減少約 75%,是目前在消費級顯卡(如 RTX 3090/4090)上微調大型模型的關鍵技術。
LLaMA-Factory 已經將這些複雜技術內建其中,您只需要在介面上點擊幾下就能啟用。
-----
# 實戰課程:使用 QLoRA 高效微調 Phi-4 小型語言模型
**課程目標:**
本課程將帶領您完成一個完整的大型語言模型 (LLM) 高效微調流程。我們將使用 Microsoft 最新發布的高性能小型語言模型 (SLM) `Phi-4`,並採用 **QLoRA**(Quantized Low-Rank Adaptation)技術,在有限的 GPU 資源下(例如單張消費級顯卡),將其微調為一個專門的情感強度分類模型。
**您將學到:**
1. **高效微調 (PEFT)** 的核心概念,特別是 LoRA 與 QLoRA。
2. 如何使用 `BitsAndBytesConfig` 載入 4-bit 量化模型以節省顯存。
3. 如何使用 `AutoTokenizer` 和「聊天模板 (Chat Template)」將資料集格式化為指令。
4. 如何設定 `LoraConfig` 來指定要訓練的「轉接器 (Adapter)」。
5. 如何使用 `trl` 函式庫中的 `SFTTrainer` 輕鬆完成監督式微調 (SFT)。
6. 如何保存訓練好的 LoRA 權重並進行後續評估。
-----
## 核心技術概覽
在開始之前,我們先了解幾個關鍵字:
* **`Microsoft_Phi-4`**:這是一個小型語言模型 (Small Language Model, SLM),它在較小的參數量級(例如 30 億)下,達到了接近大型模型(如 Llama 3 8B)的性能,使其成為在資源受限環境下進行微調的絕佳選擇。
* **Quantization (量化)**:這是一種模型壓縮技術。我們將使用 4-bit 量化(`load_in_4bit=True`)載入模型,這意味著模型權重原本使用 16-bit 浮點數儲存,現在被壓縮到 4-bit 整數。這能讓模型在 GPU 中佔用的顯存(VRAM)大幅減少約 75%!
* **PEFT (參數高效微調)**:這是一種微調策略。我們不去改動原始模型的數十億個參數,而是「凍結」它們,然後在模型的特定層(如注意力機制層)旁邊掛上一些小型的、可訓練的「轉接器 (Adapter)」層。
* **LoRA (Low-Rank Adaptation)**:PEFT 中最流行的一種技術。我們只訓練這些 LoRA 轉接器(通常只佔總參數的不到 1%)。
* **QLoRA**:`Quantization` + `LoRA` 的結合。我們在一個 *4-bit 量化的模型* 上,進行 *LoRA 訓練*。這是目前在單張消費級顯卡上微調大型模型的最強技術。
* **`SFTTrainer` (Supervised Fine-tuning Trainer)**:Hugging Face `trl` 函式庫提供的專用訓練器,它極大地簡化了指令微調 (Instruction Tuning) 的流程,能自動處理資料打包、格式化和訓練循環。
-----
## Part 1: 環境設定與 GPU 準備
在開始訓練之前,我們需要設定好環境變數並確認 GPU 狀態。
```python
# 設定訓練使用的GPU
import os
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ['CUDA_LAUNCH_BLOCKING'] = "1" # 設置為 "1" 有助於在出錯時獲得更明確的 CUDA 錯誤訊息
# os.environ["CUDA_VISIBLE_DEVICES"] = '0,1,2,3,4,5,6,7' # 如果您有多張 GPU,可以在此指定要用哪幾張
os.environ["TOKENIZERS_PARALLELISM"] = "false" # 關閉 tokenizer 的平行處理,避免與 Python 的多進程衝突
# 檢查 CUDA (GPU) 是否可用
import torch
print(f"PyTorch 版本: {torch.__version__}")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"目前使用的設備: {device}")
# 釋放所有未使用的 GPU 記憶體 (好習慣)
if torch.cuda.is_available():
torch.cuda.empty_cache()
```
**說明:**
* 這段程式碼確保我們的腳本會使用 `torch` 偵測到的 GPU。
* `torch.cuda.empty_cache()` 是一個有用的指令,可以在執行前清除可能殘留在 GPU 上的舊資料。
-----
## Part 2: 載入 4-bit 量化模型 (QLoRA 的 "Q")
這是高效微調的第一步:以極低的顯存佔用來載入我們的基礎模型。
```python
import pandas as pd
from datasets import Dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
import bitsandbytes as bnb # 確保 bitsandbytes 已安裝 (pip install bitsandbytes)
# --- 1. 設定 4-bit 量化 (BitsAndBytesConfig) ---
# 這是 QLoRA 的核心配置
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 啟用 4-bit 載入
bnb_4bit_quant_type="nf4", # 使用 NF4 (NormalFloat 4-bit) 量化類型,這是 QLoRA 推薦的
bnb_4bit_use_double_quant=True, # 啟用雙重量化,進一步節省記憶體
bnb_4bit_compute_dtype=torch.float16, # 在計算時(例如前向傳播)使用 bfloat16 (或 float16) 以保持精度和速度
)
# --- 2. 載入模型 ---
cache_dir = "/llm_model" # 設定模型下載和快取的本地資料夾
path = "NyxKrage/Microsoft_Phi-4" # 要微調的基礎模型 (Phi-4 的一個社群版本)
model = AutoModelForCausalLM.from_pretrained(
path,
cache_dir=cache_dir,
use_safetensors=True,
quantization_config=bnb_config, # 關鍵!應用我們剛剛定義的 4-bit 量化設定
trust_remote_code=True,
torch_dtype="auto", # 自動偵測最佳的 dtype
device_map="auto", # 自動將模型分佈到可用的 GPU 上
# token = access_token # 如果是私有模型,需要 token
)
# --- 3. 載入分詞器 (Tokenizer) ---
tokenizer = AutoTokenizer.from_pretrained(path, cache_dir=cache_dir)
# 許多 Causal LM(如 Phi-4)沒有預設的 pad token
# 我們通常將其設置為 eos_token (End-of-Sentence)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right" # 將 padding 加在右側
# --- 4. 準備模型進行 K-bit 訓練 ---
# 關閉快取,這在訓練時是必要的
model.config.use_cache = False
from peft import prepare_model_for_kbit_training
# 這是 PEFT 提供的輔助函數,它會正確設定量化後的模型以進行訓練
model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=False)
```
**說明:**
* `BitsAndBytesConfig` 是我們實現 QLoRA 的關鍵。
* `AutoModelForCausalLM.from_pretrained` 中的 `quantization_config` 和 `device_map="auto"` 參數會自動幫我們完成量化並載入到 GPU。
* `prepare_model_for_kbit_training` 會凍結基礎模型,並準備好讓 LoRA 轉接器「插入」。
-----
## Part 3: 資料集準備與指令格式化 (SFT)
我們將訓練模型執行一個「指令遵從 (Instruction-Following)」任務。這意味著我們需要將原始資料(句子和標籤)轉換成模型能理解的「聊天格式」。
**資料集 (train.csv) 範例格式:**
| id | text | ans |
|---|---|---|
| 1 | "這部電影真是太棒了!" | "Anger: 0, Fear: 0, Joy: 3, Sadness: 0, Surprise: 1" |
| 2 | "我好怕... 門外有聲音" | "Anger: 0, Fear: 2, Joy: 0, Sadness: 0, Surprise: 0" |
```python
# --- 1. 載入原始資料 ---
adjust_train_data = 'train.csv'
df = pd.read_csv(adjust_train_data, encoding='utf-8', index_col=False)
dataset = {'train': Dataset.from_pandas(df.sample(frac=1))} # 轉換為 Dataset 物件並洗牌
print(dataset)
# --- 2. 創建聊天提示模板 (Chat Prompt Template) ---
def create_prompt_formats(sample):
# 系統提示 (System Prompt): 定義 AI 的角色和任務規則
system_prompt = (
"You are an emotion classification assistant. Your task is to predict the intensity of emotions "
"expressed in the input sentences for the following categories: 'Anger', 'Fear', 'Joy', 'Sadness', and 'Surprise'.\n"
"- The intensity levels are: \n"
" - 0: No emotion,\n"
" - 1: Low intensity,\n"
" - 2: Moderate intensity,\n"
" - 3: High intensity.\n"
"- Provide the intensity for each emotion as a number between 0 and 3. \n"
"- Always output the result in the format: 'Anger: X, Fear: X, Joy: X, Sadness: X, Surprise: X' where X is the predicted intensity for each emotion. \n"
"- Ensure your prediction reflects the perceived intensity of the input sentence for all emotions, even if some intensities are 0. \n"
)
# 用戶輸入 (User Message): 包含要分析的句子
message = (
f"input sentences : {sample['text']}\n"
"Output format: Provide the intensity for each emotion in the following format: 'Anger: X, Fear: X, Joy: X, Sadness: X, Surprise: X', where X is a number between 0 and 3.\n\n"
)
# 完整的對話結構
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": message},
{"role": "assistant", "content": sample['ans']} # 這是模型要學習的「標準答案」
]
# 使用 tokenizer 的 apply_chat_template 轉換為模型特定的格式
# 例如 Phi-4 的格式可能是:<|system|>...<|end|><|user|>...<|end|><|assistant|>...<|end|>
formatted_prompt = tokenizer.apply_chat_template(
messages,
tokenize=False, # 我們先不轉換成 token ID,只轉換成字串
)
# SFTTrainer 需要一個固定的欄位名稱,我們用 'text'
sample["text"] = formatted_prompt
return sample
# --- 3. 定義預處理函數 (將格式化後的文字轉換為 Token ID) ---
from functools import partial
def preprocess_batch(batch, tokenizer):
# 將 "text" 欄位中的字串轉換為 input_ids
return tokenizer(
batch["text"],
padding=True,
max_length = 8192, # Phi-4 支援很長的上下文
truncation = True,
)
# --- 4. 執行完整的資料集預處理 ---
def preprocess_dataset(tokenizer, seed, dataset: str):
print("Preprocessing dataset...")
# 步驟 A: 應用聊天模板
dataset = dataset.map(create_prompt_formats)
# 步驟 B: 將文字 Tokenize
_preprocessing_function = partial(preprocess_batch, tokenizer = tokenizer)
dataset = dataset.map(
_preprocessing_function,
batched = True, # 批次處理以加快速度
remove_columns = ["id", "ans"], # 移除原始欄位,因為 SFTTrainer 只需要 'text'
)
# 步驟 C: 過濾掉超長的樣本 (可選,但有助於穩定訓練)
dataset = dataset.filter(lambda sample: len(sample["input_ids"]) < 8192)
# 步驟 D: 再次洗牌
dataset = dataset.shuffle(seed = seed)
return dataset
# --- 5. 執行並切分資料集 ---
seed = 33
from sklearn.model_selection import train_test_split
preprocessed_dataset = preprocess_dataset(tokenizer, seed, dataset['train'])
# 切分為訓練集和驗證集
train_test = preprocessed_dataset.train_test_split(test_size=0.2, seed=seed, shuffle=True)
train_dataset = train_test['train']
eval_dataset = train_test['test']
print(f"訓練集樣本數: {len(train_dataset)}")
print(f"驗證集樣本數: {len(eval_dataset)}")
print("\n--- 格式化後的訓練樣本 (text 欄位) ---")
print(train_dataset[0]['text'])
```
**說明:**
* `create_prompt_formats` 是本課程的核心。它演示了如何建構一個高品質的「指令 (Instruction)」。
* `tokenizer.apply_chat_template` 是 Hugging Face 的標準做法,它能確保您的提示詞被轉換為模型在預訓練時所使用的精確格式。
* `SFTTrainer` 預設會尋找一個名為 `text` 的欄位來進行訓練。
-----
## Part 4: 設定 LoRA 轉接器 (QLoRA 的 "LoRA")
我們已經有了 4-bit 量化的模型 (Q),現在我們要定義 LoRA 轉接器。
```python
from peft import LoraConfig, get_peft_model
# --- 1. LoRA 設定 ---
lora_r = 16 # LoRA 的 "rank" (秩)。越高的 r 表示越強的表達能力,但參數也越多。16 或 32 是常見值。
lora_alpha = 32 # LoRA 的縮放因子 (alpha)。通常設為 r 的 2 倍。
lora_dropout = 0.1 # 在 LoRA 層上應用的 Dropout 機率,防止過擬合。
lora_target_modules = [ # 指定要在模型的哪些模組上附加 LoRA 轉接器。
"q_proj",
"up_proj",
"o_proj",
"k_proj",
"down_proj",
"gate_proj",
"v_proj",
]
peft_config = LoraConfig(
r=lora_r,
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
target_modules=lora_target_modules,
bias="none", # LoRA 通常不訓練 bias
task_type="CAUSAL_LM", # 必須指定任務類型為 Causal LM (文本生成)
)
# --- 2. 應用 PEFT 設定到模型 ---
# get_peft_model 會凍結所有原始參數,並在指定模組上加上可訓練的 LoRA 轉接器
model = get_peft_model(model, peft_config)
# 您可以取消註解以下兩行,來查看可訓練參數的數量 (會非常少,通常 < 1%)
# model.print_trainable_parameters()
```
**說明:**
* `LoraConfig` 允許我們精確控制 LoRA 的強度 (`r`, `alpha`) 和位置 (`target_modules`)。
* `get_peft_model` 執行後,`model` 物件就從一個完全凍結的量化模型,變成了一個可以透過 LoRA 進行微調的模型。
-----
## Part 5: 設定 SFTTrainer 並開始訓練
最後一步是設定 `SFTTrainer`,這是我們的自動化訓練引擎。
```python
from trl import SFTTrainer, SFTConfig
from transformers import TrainingArguments
from datetime import datetime
OUTPUT_DIR = "./finetune_20" # 訓練日誌、檢查點和最終模型的儲存位置
# --- 1. SFTConfig 訓練參數設定 ---
# SFTConfig 繼承自 TrainingArguments,並增加了 SFT 專用的參數
training_args = SFTConfig(
# --- 關鍵 SFT 參數 ---
max_seq_length=2048, # 訓練時的最大序列長度
dataset_text_field="text", # 指定包含格式化提示詞的欄位名稱
# --- 訓練超參數 ---
num_train_epochs=20, # 訓練的總輪次 (Epochs)
per_device_train_batch_size=4, # 每個 GPU 的訓練批次大小
gradient_accumulation_steps=4, # 梯度累積步數。 (有效批次大小 = 4 * 4 = 16)
learning_rate=5e-5, # 學習率
lr_scheduler_type="cosine", # 學習率調度器類型 (cosine 衰減)
warmup_ratio=0.1, # 學習率預熱比例 (前 10% 的步數)
# --- 效能與精度 ---
optim="paged_adamw_32bit", # 專為 QLoRA 優化的分頁式 AdamW 優化器,節省記憶體
bf16=True, # 啟用 bf16 混合精度訓練 (若 GPU 支援)
fp16=False, # (若 GPU 不支援 bf16,可改用 fp16=True)
max_grad_norm=0.3, # 梯度裁剪,防止梯度爆炸
neftune_noise_alpha=5, # 在嵌入層加入少許噪音,可提升泛化能力
# --- 記錄與儲存 ---
output_dir=OUTPUT_DIR,
logging_steps=1, # 每 1 步記錄一次 log
report_to="tensorboard", # 將 log 報告給 TensorBoard
do_eval=True, # 在訓練過程中進行評估
eval_strategy="epoch", # 每個 epoch 結束後評估一次
save_strategy="epoch", # 每個 epoch 結束後儲存一次檢查點
metric_for_best_model="eval_loss", # 依據 eval_loss 來決定最佳模型
load_best_model_at_end=True, # 訓練結束時自動載入最佳模型
# --- 其他 ---
group_by_length=True, # 將相近長度的樣本組在一起,提升訓練效率
remove_unused_columns=True, # 自動移除 SFTTrainer 不需要的多餘欄位
seed=42,
)
# --- 2. 初始化 SFTTrainer ---
trainer = SFTTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
peft_config=peft_config, # 傳入 LoRA 設定
tokenizer=tokenizer,
args=training_args,
)
# --- 3. 開始訓練! ---
print("開始訓練...")
trainer.train()
print("訓練完成!")
# --- 4. 儲存最佳的 LoRA 轉接器 ---
best_model = trainer.model
custom_output_dir = OUTPUT_DIR + "/best_model"
best_model.save_pretrained(custom_output_dir, save_safetensors=True)
tokenizer.save_pretrained(custom_output_dir)
print(f"最佳模型已儲存至: {custom_output_dir}")
```
**說明:**
* `SFTConfig` (或 `TrainingArguments`) 是 Hugging Face 訓練流程的大腦,它控制著一切。
* `gradient_accumulation_steps` 是在小顯存 GPU 上模擬大 Batch Size 的關鍵技巧。
* `optim="paged_adamw_32bit"` 是 QLoRA 訓練的標配。
* `trainer.train()` 一行指令即可啟動所有流程。
* `best_model.save_pretrained(custom_output_dir)` 只會儲存 LoRA 轉接器(`adapter_model.safetensors`)和設定檔,檔案非常小(通常只有幾十 MB),而不是完整的 30 億參數模型。
-----
## 總結與下一步
恭喜您!您已經成功地使用 QLoRA 技術,在有限的資源下微調了一個強大的 Phi-4 模型,使其成為一個專門的情感分類 AI。
**下一步:如何使用您訓練好的模型?**
您需要載入 *原始的 Phi-4 基礎模型*,然後再將您儲存的 *LoRA 轉接器* 附加到它上面,才能開始推論。
```python
# --- 推論 (Inference) 範例 ---
from peft import PeftModel
import torch
# 1. 再次載入 4-bit 基礎模型
base_model = AutoModelForCausalLM.from_pretrained(
path,
cache_dir=cache_dir,
quantization_config=bnb_config,
trust_remote_code=True,
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(path, cache_dir=cache_dir)
tokenizer.pad_token = tokenizer.eos_token
# 2. 載入我們訓練好的 LoRA 轉接器
lora_model_path = "./finetune_20/best_model"
model = PeftModel.from_pretrained(base_model, lora_model_path)
model = model.eval() # 切換到評估模式
# 3. 準備一個新的輸入 (使用相同的聊天模板)
system_prompt = "..." # (省略,同訓練時的 system_prompt)
test_text = "這家餐廳的服務生超級友善,食物也美味到不行!"
message = f"input sentences : {test_text}\nOutput format: Provide the intensity for each emotion in the following format: 'Anger: X, Fear: X, Joy: X, Sadness: X, Surprise: X', where X is a number between 0 and 3.\n\n"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": message}
]
input_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(input_prompt, return_tensors="pt").to(device)
# 4. 進行推論
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=50, # 限制最大生成長度
pad_token_id=tokenizer.eos_token_id
)
# 5. 解碼並顯示結果
response = tokenizer.decode(outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)
print(f"輸入句子: {test_text}")
print(f"模型預測: {response}")
# 預期輸出: Anger: 0, Fear: 0, Joy: 3, Sadness: 0, Surprise: 0
```