---
# System prepended metadata

title: 實作課程：從入門到進階，訓練你的大型語言模型

---

# 實作課程：從入門到進階，訓練你的大型語言模型

**課程目標：**

許多人認為訓練大型語言模型（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]
```

安裝完成目錄：
![image](https://hackmd.io/_uploads/Skd1riUAgl.png =50%x)



### 步驟 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`。
![image](https://hackmd.io/_uploads/BylhKmnIAex.png)

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 技術**：

      * ![image](https://hackmd.io/_uploads/rJ0NXLwAgg.png)

      * **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
```