返回文章列表

PyTorch 指令微調 GPT 模型實戰

本文示範如何使用 PyTorch 和 DataLoader 對 GPT 模型進行指令微調,包含資料載入、模型設定、訓練流程、損失計算及結果分析,並提供使用 Alpaca 資料集的額外練習和硬體資源調配建議。

機器學習 深度學習

本篇探討如何使用 PyTorch 建立資料載入器,並以指令資料集微調預訓練的 GPT 模型。文章涵蓋了 DataLoader 的使用、批次大小設定、自定義整理函式的應用,以及如何載入不同規模的 GPT 模型,例如 124M、355M 引數的版本。同時也示範瞭如何評估預訓練模型的效能,並透過計算訓練和驗證損失來監控微調過程。文章提供完整的程式碼範例,包含模型初始化、權過載入、最佳化器設定以及訓練迴圈的執行。此外,還討論瞭如何處理硬體資源限制,例如透過調整模型大小和批次大小來應對記憶體不足的問題,並建議使用 GPU 加速訓練。最後,文章也介紹瞭如何使用 Alpaca 資料集進行微調,以及如何繪製損失曲線圖來觀察模型的學習過程。

7.4 為指令資料集建立資料載入器

在這一部分,我們將使用 PyTorch 的 DataLoader 來為指令資料集建立資料載入器。首先,我們需要從 torch.utils.data 匯入 DataLoader

from torch.utils.data import DataLoader

num_workers = 0
batch_size = 8
torch.manual_seed(123)

train_dataset = InstructionDataset(train_data, tokenizer)
train_loader = DataLoader(
    train_dataset,
    batch_size=batch_size,
    collate_fn=customized_collate_fn,
    shuffle=True,
    drop_last=True,
    num_workers=num_workers
)

val_dataset = InstructionDataset(val_data, tokenizer)
val_loader = DataLoader(
    val_dataset,
    batch_size=batch_size,
    collate_fn=customized_collate_fn,
    shuffle=False,
    drop_last=False,
    num_workers=num_workers
)

test_dataset = InstructionDataset(test_data, tokenizer)
test_loader = DataLoader(
    test_dataset,
    batch_size=batch_size,
    collate_fn=customized_collate_fn,
    shuffle=False,
    drop_last=False,
    num_workers=num_workers
)

內容解密:

  1. 匯入必要的模組:首先,從 torch.utils.data 匯入 DataLoader,這是用於批次載入資料的工具。
  2. 設定引數:設定 num_workers 為 0(表示不使用多執行緒載入資料),batch_size 為 8,以及固定亂數種子以保證結果的可重現性。
  3. 建立資料集例項:分別為訓練、驗證和測試資料建立 InstructionDataset 例項,並將其與 tokenizer 一起傳入。
  4. 建立 DataLoader:使用 DataLoader 分別為訓練、驗證和測試資料集建立載入器。設定 batch_sizecollate_fn(自定義的批次整理函式)、shuffle(是否打亂資料)和 drop_last(是否丟棄最後一個不完整的批次)。

接著,我們來檢查由訓練載入器產生的輸入和目標批次的維度:

print("Train loader:")
for inputs, targets in train_loader:
    print(inputs.shape, targets.shape)

輸出如下(已截斷以節省空間):

Train loader:
torch.Size([8, 61]) torch.Size([8, 61])
torch.Size([8, 76]) torch.Size([8, 76])
torch.Size([8, 73]) torch.Size([8, 73])
...
torch.Size([8, 74]) torch.Size([8, 74])
torch.Size([8, 69]) torch.Size([8, 69])

內容解密:

  1. 檢查批次維度:遍歷 train_loader,列印每個批次的輸入和目標張量的形狀。
  2. 輸出解析:輸出顯示每個批次的形狀,例如 torch.Size([8, 61]),其中 8 是批次大小,61 是該批次中每個訓練樣本的標記數量。
  3. 自定義整理函式的作用:由於使用了自定義的 collate_fn,不同批次可以具有不同的序列長度,這在處理變長序列時非常有用。

7.5 載入預訓練的大語言模型

在進行指令微調之前,我們需要載入一個預訓練的 GPT 模型。我們之前已經進行過類別似的操作,但這次我們選擇載入中等大小的模型(355M 引數),因為較小的模型(124M 引數)容量有限,難以達到令人滿意的指令跟隨效果。

from gpt_download import download_and_load_gpt2
from chapter04 import GPTModel
from chapter05 import load_weights_into_gpt

BASE_CONFIG = {
    "vocab_size": 50257, 
    "context_length": 1024, 
    "drop_rate": 0.0, 
    "qkv_bias": True 
}

model_configs = {
    "gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},
    "gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},
    "gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},
    "gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}

CHOOSE_MODEL = "gpt2-medium (355M)"
BASE_CONFIG.update(model_configs[CHOOSE_MODEL])

model_size = CHOOSE_MODEL.split(" ")[-1].lstrip("(").rstrip(")")
settings, params = download_and_load_gpt2(
    model_size=model_size,
    models_dir="gpt2"
)

model = GPTModel(BASE_CONFIG)
load_weights_into_gpt(model, params)
model.eval();

內容解密:

  1. 載入必要的模組和函式:匯入下載和載入 GPT-2 模型的函式,以及 GPTModel 的定義和權過載入函式。
  2. 定義模型組態:定義基礎組態和不同大小的 GPT-2 模型的組態字典。
  3. 選擇模型並更新組態:選擇要載入的模型(這裡是 gpt2-medium (355M)),並更新基礎組態。
  4. 下載並載入模型:根據選擇的模型大小下載並載入相應的 GPT-2 模型權重。
  5. 初始化模型並載入權重:使用更新後的組態初始化 GPTModel,並將下載的權過載入模型。
  6. 設定模型為評估模式:呼叫 model.eval() 將模型設定為評估模式,這會關閉 dropout 等訓練時使用的技術。

微調大語言模型以遵循指令

評估預訓練大語言模型的效能

首先,我們需要評估預訓練的大語言模型(LLM)在某項驗證任務上的表現,以瞭解其在微調前的效能。這將有助於我們理解微調的效果。我們將使用驗證集中的第一個範例進行評估:

torch.manual_seed(123)
input_text = format_input(val_data[0])
print(input_text)

指令內容如下:

Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
Convert the active sentence to passive: 'The chef cooks the meal every day.'

接下來,我們使用與第五章中預訓練模型相同的 generate 函式來生成模型的回應:

from chapter05 import generate, text_to_token_ids, token_ids_to_text
token_ids = generate(
    model=model,
    idx=text_to_token_ids(input_text, tokenizer),
    max_new_tokens=35,
    context_size=BASE_CONFIG["context_length"],
    eos_id=50256,
)
generated_text = token_ids_to_text(token_ids, tokenizer)

內容解密:

  1. torch.manual_seed(123):設定隨機種子以確保結果的可重現性。
  2. format_input(val_data[0]):格式化驗證集中的第一個資料範例。
  3. generate 函式:使用預訓練模型生成回應文字。
    • model=model:指定使用的模型。
    • idx=text_to_token_ids(input_text, tokenizer):將輸入文字轉換為令牌ID。
    • max_new_tokens=35:限制生成的最大令牌數量。
    • context_size=BASE_CONFIG["context_length"]:設定上下文大小。
    • eos_id=50256:指定結束令牌的ID。

為了隔離模型的回應文字,我們需要從生成的文字中減去輸入指令的長度:

response_text = generated_text[len(input_text):].strip()
print(response_text)

輸出結果為:


### Response:
The chef cooks the meal every day.

### Instruction:
Convert the active sentence to passive: 'The chef cooks the

內容解密:

  1. generated_text[len(input_text):].strip():從生成的文字中移除輸入指令部分,並去除前後空白字元。
  2. 輸出結果分析:預訓練模型未能正確遵循指令,將主動句轉換為被動句,而是重複了原始輸入句子和部分指令。

微調大語言模型以遵循指令

現在,我們將對預訓練的大語言模型進行微調,以提高其遵循指令的能力。我們將使用之前準備好的指令資料集對模型進行進一步訓練。

from chapter05 import calc_loss_loader, train_model_simple

在開始訓練之前,讓我們計算初始的訓練和驗證損失:

model.to(device)
torch.manual_seed(123)
with torch.no_grad():
    train_loss = calc_loss_loader(train_loader, model, device, num_batches=5)
    val_loss = calc_loss_loader(val_loader, model, device, num_batches=5)
print("Training loss:", train_loss)
print("Validation loss:", val_loss)

初始損失值如下:

Training loss: 3.825908660888672
Validation loss: 3.7619335651397705

內容解密:

  1. model.to(device):將模型移動到指定的裝置(CPU或GPU)。
  2. calc_loss_loader:計算資料載入器中的損失。
    • train_loaderval_loader:分別是訓練和驗證資料的載入器。
    • num_batches=5:限制計算的批次數量。

處理硬體限制

如果遇到硬體限制,可以透過切換到較小的GPT-2模型(1.24億引數)來解決,方法是更改 CHOOSE_MODEL = "gpt2-medium (355M)"CHOOSE_MODEL = "gpt2-small (124M)"。另外,考慮使用GPU來加速模型訓練。下表提供了在不同裝置上訓練GPT-2模型的參考執行時間。使用相容的GPU不需要更改程式碼,可以顯著加快訓練速度。

7.6 微調大語言模型於指令資料

準備好模型和資料載入器後,我們現在可以開始訓練模型。 清單 7.8 中的程式碼設定了訓練過程,包括初始化最佳化器、設定迭代次數、定義評估頻率和起始上下文,以評估在第一個驗證集指令(val_data[0])上生成的 LLM 回應。

import time
start_time = time.time()
torch.manual_seed(123)
optimizer = torch.optim.AdamW(
    model.parameters(), lr=0.00005, weight_decay=0.1
)
num_epochs = 2
train_losses, val_losses, tokens_seen = train_model_simple(
    model, train_loader, val_loader, optimizer, device,
    num_epochs=num_epochs, eval_freq=5, eval_iter=5,
    start_context=format_input(val_data[0]), tokenizer=tokenizer
)
end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"訓練完成於 {execution_time_minutes:.2f} 分鐘內。")

內容解密:

  1. 時間記錄:使用 time.time() 函式記錄訓練的開始和結束時間,計算訓練所需的總時間。
  2. 隨機種子設定torch.manual_seed(123) 設定 PyTorch 的隨機種子,以確保實驗的可重現性。
  3. 最佳化器選擇:使用 torch.optim.AdamW 最佳化器,並設定學習率為 0.00005,權重衰減為 0.1,以最佳化模型的訓練過程。
  4. 訓練引數:設定迭代次數 num_epochs 為 2,並呼叫 train_model_simple 函式進行訓練,期間記錄訓練損失、驗證損失和已處理的 token 數量。

訓練輸出與結果分析

輸出顯示了模型在兩個迭代過程中的訓練進度,損失值持續下降,表明模型能夠有效地學習並遵循指令生成適當的回應。

Ep 1 (Step 000000): Train loss 2.637, Val loss 2.626
Ep 1 (Step 000005): Train loss 1.174, Val loss 1.103
...
Ep 2 (Step 000230): Train loss 0.300, Val loss 0.657

內容解密:

  1. 損失值下降:訓練和驗證損失隨著迭代的進行而下降,表明模型的效能正在提升,能夠更好地理解和遵循指令。
  2. 生成回應範例:模型在每個迭代結束時生成回應,驗證其在特定任務(如主動句轉換為被動句)上的表現。
  3. 執行時間:不同硬體組態下的執行時間比較,展示了使用 GPU 加速訓練的顯著優勢。

7.7 提取和儲存回應

在對 LLM 進行指令資料微調後,我們現在準備在保留的測試集上評估其效能。首先,我們提取模型為測試資料集中的每個輸入生成的回應,並將其收集起來進行手動分析,然後量化回應的品質。

練習 7.3 在原始 Alpaca 資料集上進行微調

Alpaca 資料集由史丹佛大學的研究人員發布,是最早且最受歡迎的公開指令資料集之一,包含 52,002 個條目。作為本章使用的 instruction-data.json 的替代方案,可以考慮在該資料集上對 LLM 進行微調。

內容解密:

  1. Alpaca 資料集介紹:該資料集包含的條目數量遠多於本章使用的資料集,因此建議使用 GPU 加速訓練,以避免記憶體問題。
  2. 處理記憶體問題:如果遇到記憶體錯誤,可以透過降低 batch_sizeallowed_max_length 的值來解決。

繪製損失曲線圖

使用 plot_losses 函式繪製訓練和驗證損失曲線,以直觀地展示模型的學習過程。

from chapter05 import plot_losses
epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)

內容解密:

  1. 損失曲線分析:透過觀察損失曲線,可以看到模型的訓練和驗證損失在兩個迭代過程中顯著下降,表明模型正在有效地學習。
  2. 曲線趨勢:在初始階段,損失值迅速下降,隨後下降速度放緩,表明模型正在微調其學習到的表示並趨於穩定。

此圖示展示了兩個迭代過程中的訓練和驗證損失趨勢,實線代表訓練損失,虛線代表驗證損失。兩者均呈現出類別似的下降趨勢。