大語言模型的訓練過程複雜且耗時,需要仔細調整引數以確保穩定性和效率。本文介紹了學習率預熱策略,透過逐步提升學習率,避免初始階段的劇烈震盪,並搭配餘弦衰減策略調整學習率曲線,有效控制模型訓練步調。此外,LoRA 技術的應用能大幅減少可訓練引數,降低運算成本並提升訓練效率。文章同時提供了 Stanford Alpaca 資料集的微調方法和詳細的程式碼解析,包含 InstructionDataset 類別、客製化 collate 函式以及 LoRA 的實作,讓讀者能更深入地理解程式碼運作邏輯,並將其應用於實際的 LLM 訓練任務中。
練習題解答
第7章練習題解答
練習7.3
要對原始的Stanford Alpaca資料集進行微調,我們只需要更改檔案URL即可。原始的URL是:
url = "https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch07/01_main-chapter-code/instruction-data.json"
將其更改為:
url = "https://raw.githubusercontent.com/tatsu-lab/stanford_alpaca/main/alpaca_data.json"
Stanford Alpaca資料集包含52,000個條目(比第7章中的資料多50倍),並且條目比第7章中的更長。因此,強烈建議在GPU上執行訓練。如果遇到記憶體不足的錯誤,請考慮將批次大小從8降低到4、2或1。除了降低批次大小外,還可以考慮將allowed_max_length從1024降低到512或256。
練習7.4
要使用LoRA對模型進行指令微調,請使用附錄E中的相關類別和函式:
from appendix_E import LoRALayer, LinearWithLoRA, replace_linear_with_lora
在第7.5節的模型載入程式碼下方新增以下程式碼行:
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters before: {total_params:,}")
for param in model.parameters():
param.requires_grad = False
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters after: {total_params:,}")
replace_linear_with_lora(model, rank=16, alpha=16)
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable LoRA parameters: {total_params:,}")
model.to(device)
在Nvidia L4 GPU上,使用LoRA進行微調大約需要1.30分鐘,而原始程式碼需要1.80分鐘。因此,在這種情況下,LoRA大約快了28%。使用第7章中的Ollama Llama 3方法評估的分數約為50,與原始模型的分數差不多。
附錄A練習題解答
練習A.1
該網路有兩個輸入和兩個輸出。此外,還有兩個隱藏層,分別具有30和20個節點。我們可以透過以下方式計算引數數量:
model = NeuralNetwork(2, 2)
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("Total number of trainable model parameters:", num_params)
這將傳回752。我們也可以手動計算:
- 第一隱藏層:2個輸入×30個隱藏單元 + 30個偏差單元
- 第二隱藏層:30個輸入單元×20個節點 + 20個偏差單元
- 輸出層:20個輸入節點×2個輸出節點 + 2個偏差單元
將所有層的引數相加,結果為2 × 30 + 30 + 30 × 20 + 20 + 20 × 2 + 2 = 752。
練習A.2
執行時間結果將取決於用於此實驗的硬體。在我的實驗中,即使對於像下面這樣的小矩陣乘法,我也觀察到了顯著的加速:
a = torch.rand(100, 200)
b = torch.rand(200, 300)
%timeit a@b
在CPU上,這導致:
63.8 μs ± 8.7 μs per loop
當在GPU上執行時:
a, b = a.to("cuda"), b.to("cuda")
%timeit a @ b
結果是:
13.8 μs ± 425 ns per loop
在這種情況下,在V100上,計算速度大約快了四倍。
練習A.3
該網路有兩個輸入和兩個輸出。此外,還有兩個隱藏層,分別具有30和20個節點。我們可以透過以下方式計算引數數量:
model = NeuralNetwork(2, 2)
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("Total number of trainable model parameters:", num_params)
這將傳回752。我們也可以手動計算:
- 第一隱藏層:2個輸入×30個隱藏單元 + 30個偏差單元
- 第二隱藏層:30個輸入單元×20個節點 + 20個偏差單元
- 輸出層:20個輸入節點×2個輸出節點 + 2個偏差單元
將所有層的引數相加,結果為2 × 30 + 30 + 30 × 20 + 20 + 20 × 2 + 2 = 752。
程式碼解析
InstructionDataset類別實作
class InstructionDataset(Dataset):
def __init__(self, data, tokenizer):
self.data = data
self.instruction_lengths = []
self.encoded_texts = []
for entry in data:
instruction_plus_input = format_input(entry)
response_text = f"\n\n### Response:\n{entry['output']}"
full_text = instruction_plus_input + response_text
self.encoded_texts.append(tokenizer.encode(full_text))
instruction_length = len(tokenizer.encode(instruction_plus_input))
self.instruction_lengths.append(instruction_length)
def __getitem__(self, index):
return self.instruction_lengths[index], self.encoded_texts[index]
def __len__(self):
return len(self.data)
custom_collate_fn函式實作
def custom_collate_fn(
batch,
pad_token_id=50256,
ignore_index=-100,
allowed_max_length=None,
device="cpu"
):
batch_max_length = max(len(item)+1 for instruction_length, item in batch)
inputs_lst, targets_lst = [], []
for instruction_length, item in batch:
new_item = item.copy()
new_item += [pad_token_id]
padded = new_item + [pad_token_id] * (batch_max_length - len(new_item))
inputs = torch.tensor(padded[:-1])
targets = torch.tensor(padded[1:])
mask = targets == pad_token_id
indices = torch.nonzero(mask).squeeze()
if indices.numel() > 1:
targets[indices[1:]] = ignore_index
targets[:instruction_length-1] = -100
if allowed_max_length is not None:
inputs = inputs[:allowed_max_length]
targets = targets[:allowed_max_length]
inputs_lst.append(inputs)
targets_lst.append(targets)
inputs_tensor = torch.stack(inputs_lst).to(device)
targets_tensor = torch.stack(targets_lst).to(device)
return inputs_tensor, targets_tensor
LoRA實作
from appendix_E import LoRALayer, LinearWithLoRA, replace_linear_with_lora
# ...
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters before: {total_params:,}")
for param in model.parameters():
param.requires_grad = False
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters after: {total_params:,}")
replace_linear_with_lora(model, rank=16, alpha=16)
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable LoRA parameters: {total_params:,}")
model.to(device)
#### 內容解密:
- InstructionDataset類別:用於處理指令資料集,將資料轉換為模型可接受的格式。
- custom_collate_fn函式:用於批次處理資料,對輸入和目標進行填充和遮罩處理。
- LoRA實作:使用LoRA技術對模型進行微調,減少可訓練引數數量,提高訓練效率。
增強訓練迴圈的功能
在附錄 D 中,我們將增強第 5 章到第 7 章中涵蓋的預訓練和微調過程的訓練函式。特別是,它涵蓋了學習率預熱、餘弦衰減和梯度裁剪。然後,我們將這些技術納入訓練函式並預訓練一個大語言模型(LLM)。
初始化模型和資料載入器
首先,我們重新初始化在第 5 章中訓練的模型:
import torch
from chapter04 import GPTModel
GPT_CONFIG_124M = {
"vocab_size": 50257,
"context_length": 256,
"emb_dim": 768,
"n_heads": 12,
"n_layers": 12,
"drop_rate": 0.1,
"qkv_bias": False
}
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
model.eval()
內容解密:
這段程式碼初始化了一個 GPT 模型,組態引數來自 GPT_CONFIG_124M。首先,匯入必要的函式庫,包括 PyTorch 和自定義的 GPTModel。然後,定義模型的組態引數,如詞彙大小、上下文長度、嵌入維度、注意力頭數、層數、丟棄率和查詢鍵值偏差。接著,檢查是否有可用的 CUDA 裝置,若有則使用 CUDA,否則使用 CPU。設定隨機種子以確保結果的可重現性,並將模型移動到指定的裝置上,最後將模型設定為評估模式。
載入資料
接下來,我們載入“The Verdict”短篇故事:
import os
import urllib.request
file_path = "the-verdict.txt"
url = (
"https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/"
"main/ch02/01_main-chapter-code/the-verdict.txt"
)
if not os.path.exists(file_path):
with urllib.request.urlopen(url) as response:
text_data = response.read().decode('utf-8')
with open(file_path, "w", encoding="utf-8") as file:
file.write(text_data)
else:
with open(file_path, "r", encoding="utf-8") as file:
text_data = file.read()
內容解密:
這段程式碼負責下載“The Verdict”文字資料。首先,檢查本地是否已經存在該檔案,若不存在,則從指定的 URL 下載文字資料並儲存到本地檔案中。若檔案已存在,則直接讀取本地檔案內容。這樣可以避免重複下載相同的資料。
建立資料載入器
然後,我們將 text_data 載入資料載入器:
from previous_chapters import create_dataloader_v1
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
torch.manual_seed(123)
train_loader = create_dataloader_v1(
text_data[:split_idx],
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=True,
shuffle=True,
num_workers=0
)
val_loader = create_dataloader_v1(
text_data[split_idx:],
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=False,
shuffle=False,
num_workers=0
)
內容解密:
這段程式碼將文字資料分成訓練集和驗證集。首先,設定訓練集的比例,並根據此比例劃分資料索引。然後,使用自定義的 create_dataloader_v1 函式建立訓練和驗證資料載入器。設定批次大小、最大長度、步幅、是否丟棄最後一個不完整的批次、是否打亂資料以及工作執行緒數等引數。
學習率預熱
實作學習率預熱可以穩定大語言模型等複雜模型的訓練過程。這個過程涉及從一個非常低的初始值(initial_lr)逐漸增加學習率到使用者指定的最大值(peak_lr)。
n_epochs = 15
initial_lr = 0.0001
peak_lr = 0.01
warmup_steps = 20
total_steps = len(train_loader) * n_epochs
warmup_steps = int(0.2 * total_steps)
print(warmup_steps)
內容解密:
這段程式碼計算了總的訓練步數和預熱步數。首先,定義了訓練的輪數、初始學習率和峰值學習率。然後,計算總的訓練步數,即訓練資料載入器的長度乘以訓練輪數。接著,計算預熱步數,通常設定為總步數的 0.1% 到 20%。這裡將預熱步數設為總步數的 20%。
簡單的訓練迴圈範本
接下來,我們實作一個簡單的訓練迴圈範本來說明這個預熱過程:
optimizer = torch.optim.AdamW(model.parameters(), weight_decay=0.1)
lr_increment = (peak_lr - initial_lr) / warmup_steps
global_step = -1
track_lrs = []
for epoch in range(n_epochs):
for input_batch, target_batch in train_loader:
optimizer.zero_grad()
global_step += 1
if global_step < warmup_steps:
lr = initial_lr + global_step * lr_increment
else:
lr = peak_lr
for param_group in optimizer.param_groups:
param_group["lr"] = lr
track_lrs.append(optimizer.param_groups[0]["lr"])
內容解密:
這段程式碼實作了一個簡單的訓練迴圈,並在其中進行了學習率預熱。首先,初始化最佳化器和學習率增量。然後,在每個訓練步驟中,檢查是否仍在預熱階段。如果是,則根據當前步驟計算學習率;否則,將學習率設為峰值學習率。最後,將當前的學習率應用於最佳化器,並記錄學習率的變化。
視覺化學習率變化
執行上述程式碼後,我們可以透過視覺化來驗證學習率預熱是否按預期工作:
import matplotlib.pyplot as plt
plt.ylabel("Learning rate")
plt.xlabel("Step")
total_training_steps = len(train_loader) * n_epochs
plt.plot(range(total_training_steps), track_lrs)
plt.show()
內容解密:
這段程式碼繪製了學習率隨訓練步驟變化的曲線。首先,匯入 matplotlib.pyplot 用於繪圖。然後,設定 y 軸和 x 軸的標籤,分別表示學習率和訓練步驟。接著,繪製學習率隨訓練步驟變化的曲線,並顯示圖表。這樣可以直觀地觀察到學習率在訓練過程中的變化情況。
此圖示展示了學習率在初始階段逐漸增加到峰值的過程。 此圖示為:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 大語言模型訓練迴圈增強技術
package "LLM 訓練迴圈增強" {
package "學習率策略" {
component [學習率預熱] as warmup
component [餘弦衰減] as cosine
component [批次大小調整] as batch
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml
此圖示說明瞭學習率預熱的過程,從初始的低學習率逐漸增加到峰值學習率,並在之後保持峰值學習率。
綜上所述,本附錄介紹瞭如何增強訓練迴圈的功能,包括初始化模型和資料載入器、載入資料、建立資料載入器、實作學習率預熱以及視覺化學習率變化等。透過這些技術,可以提高大語言模型的訓練穩定性和效果。