# 實作課程:從入門到進階,訓練你的大型語言模型 **課程目標:** 許多人認為訓練大型語言模型(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 ```