深度學習模型的訓練和微調過程涉及許多細節,從資料預處理到模型架構調整,每個環節都可能影響最終效能。本文針對資料載入、注意力機制、模型引數設定、損失函式計算以及提示工程等方面,提供程式碼解析和實務技巧,並以 GPT 模型的微調為例,示範如何調整模型以適應特定任務。特別關注不同模型大小的資源需求、引數數量計算、以及如何控制不同層的 dropout 率等細節,並探討不同提示格式對模型訓練效率和效能的影響。最後,文章也分析了訓練集和驗證集損失的比較,以及不同微調策略(例如微調整個模型或只微調特定層)對模型效能的影響,提供更全面的模型微調策略參考。
附錄A:深度學習資源
對於希望進一步學習深度學習的讀者,以下是一些推薦資源:
- Machine Learning with PyTorch and Scikit-Learn(2022)by Sebastian Raschka, Hayden Liu, and Vahid Mirjalili。ISBN 978-1801819312
- Deep Learning with PyTorch(2021)by Eli Stevens, Luca Antiga, and Thomas Viehmann。ISBN 978-1617295263
- Lecture 4.1: Tensors in Deep Learning,Sebastian Raschka錄製的15分鐘影片教程(https://www.youtube.com/watch?v=JXfDlgrfOBY)
- Model Evaluation, Model Selection, and Algorithm Selection in Machine Learning(2018)by Sebastian Raschka(https://arxiv.org/abs/1811.12808)
- Introduction to Calculus by Sebastian Raschka(https://mng.bz/WEyW)
此外,對於希望瞭解更多關於梯度累積和分散式資料平行性的讀者,可以參考以下資源:
- Finetuning Large Language Models on a Single GPU Using Gradient Accumulation by Sebastian Raschka(https://mng.bz/8wPD)
- Introducing PyTorch Fully Sharded Data Parallel (FSDP) API(https://mng.bz/EZJR)
附錄C:練習解答
本文的練習解答程式碼可在補充的GitHub儲存函式庫中找到:https://github.com/rasbt/LLMs-from-scratch。
第二章練習解答
練習2.1
您可以透過每次提示編碼器一個字串來獲得單個標記ID:
print(tokenizer.encode("Ak"))
print(tokenizer.encode("w"))
# ...
這將輸出:
[33901]
[86]
# ...
然後,您可以使用以下程式碼組裝原始字串:
print(tokenizer.decode([33901, 86, 343, 86, 220, 959]))
這將傳回:
'Akwirw ier'
第3章 習題解答
練習 2.2
當 max_length=2 且 stride=2 時,資料載入器的程式碼如下:
dataloader = create_dataloader(
raw_text, batch_size=4, max_length=2, stride=2
)
產生的批次格式如下:
tensor([[ 40, 367],
[2885, 1464],
[1807, 3619],
[ 402, 271]])
而當 max_length=8 且 stride=2 時,資料載入器的程式碼如下:
dataloader = create_dataloader(
raw_text, batch_size=4, max_length=8, stride=2
)
一個範例批次如下:
tensor([[ 40, 367, 2885, 1464, 1807, 3619, 402, 271],
[2885, 1464, 1807, 3619, 402, 271, 10899, 2138],
[1807, 3619, 402, 271, 10899, 2138, 257, 7026],
[402, 271, 10899, 2138, 257, 7026, 15632, 438]])
程式碼解密:
create_dataloader函式用於建立資料載入器。max_length引數控制每個輸入序列的最大長度。stride引數控制序列之間的步幅。- 當
max_length=2且stride=2時,序列不重疊;當max_length=8且stride=2時,序列部分重疊。
練習 3.1
正確的權重分配如下:
sa_v1.W_query = torch.nn.Parameter(sa_v2.W_query.weight.T)
sa_v1.W_key = torch.nn.Parameter(sa_v2.W_key.weight.T)
sa_v1.W_value = torch.nn.Parameter(sa_v2.W_value.weight.T)
程式碼解密:
- 將
sa_v2的權重轉置後指定給sa_v1的對應權重。 - 這樣做是為了比對兩種不同實作之間的權重格式。
練習 3.2
要實作輸出維度為2,需要將投影維度 d_out 設定為1。
d_out = 1
mha = MultiHeadAttentionWrapper(d_in, d_out, block_size, 0.0, num_heads=2)
程式碼解密:
- 將
d_out設定為1,以實作所需的輸出維度。 - 初始化一個具有兩個注意力頭的多頭注意力模組。
練習 3.3
最小的GPT-2模型的初始化如下:
block_size = 1024
d_in, d_out = 768, 768
num_heads = 12
mha = MultiHeadAttention(d_in, d_out, block_size, 0.0, num_heads)
程式碼解密:
- 設定序列長度為1024,輸入和輸出維度均為768。
- 初始化一個具有12個注意力頭的多頭注意力模組。
第4章 習題解答
練習 4.1
計算前饋網路和注意力模組的引數數量:
block = TransformerBlock(GPT_CONFIG_124M)
total_params = sum(p.numel() for p in block.ff.parameters())
print(f"Total number of parameters in feed forward module: {total_params:,}")
total_params = sum(p.numel() for p in block.att.parameters())
print(f"Total number of parameters in attention module: {total_params:,}")
結果顯示,前饋網路的引數數量約為注意力模組的兩倍。
程式碼解密:
- 初始化一個Transformer區塊。
- 分別計算前饋網路和注意力模組的引數數量。
練習 4.2
修改組態字典以例項化其他GPT模型大小(以GPT-2 XL為例):
GPT_CONFIG = GPT_CONFIG_124M.copy()
GPT_CONFIG["emb_dim"] = 1600
GPT_CONFIG["n_layers"] = 48
GPT_CONFIG["n_heads"] = 25
model = GPTModel(GPT_CONFIG)
計算引數數量和RAM需求後,結果如下:
- 總引數數量:1,637,792,000
- 可訓練引數數量(考慮權重分享):1,557,380,800
- 模型總大小:6247.68 MB
程式碼解密:
- 修改組態字典以比對GPT-2 XL的規格。
- 初始化GPT模型並計算相關統計資料。
練習 4.3
修改組態字典和程式碼以分別控制不同層的dropout率:
GPT_CONFIG_124M = {
"vocab_size": 50257,
"context_length": 1024,
"emb_dim": 768,
"n_heads": 12,
"n_layers": 12,
"drop_rate_attn": 0.1,
"drop_rate_shortcut": 0.1,
"drop_rate_emb": 0.1,
"qkv_bias": False
}
修改後的TransformerBlock和GPTModel如下:
class TransformerBlock(nn.Module):
def __init__(self, cfg):
super().__init__()
self.att = MultiHeadAttention(
d_in=cfg["emb_dim"],
d_out=cfg["emb_dim"],
context_length=cfg["context_length"],
num_heads=cfg["n_heads"],
dropout=cfg["drop_rate_attn"],
qkv_bias=cfg["qkv_bias"])
self.ff = FeedForward(cfg)
self.norm1 = LayerNorm(cfg["emb_dim"])
self.norm2 = LayerNorm(cfg["emb_dim"])
self.drop_shortcut = nn.Dropout(cfg["drop_rate_shortcut"])
def forward(self, x):
# 省略實作細節
class GPTModel(nn.Module):
def __init__(self, cfg):
super().__init__()
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
self.drop_emb = nn.Dropout(cfg["drop_rate_emb"])
# 省略其他實作細節
程式碼解密:
- 在組態字典中新增不同的dropout率引數。
- 修改
TransformerBlock和GPTModel以使用這些新的dropout率引數。
第5章 習題解答
練習 5.1
使用print_sampled_tokens函式列印“pizza”標記被取樣的次數。
- 當溫度為0或0.1時,“pizza”被取樣的次數為0。
- 當溫度升高到5時,“pizza”被取樣的次數為32次。
預估機率為3.2%,實際機率為4.3%。
程式碼解密:
- 使用不同的溫度設定進行取樣。
- 統計“pizza”標記的出現次數。
練習 5.2
Top-k取樣和溫度縮放是根據LLM和所需的輸出多樣性和隨機性進行調整的設定。
- 較小的top-k值(例如小於10)和低於1的溫度會使模型的輸出更具確定性和連貫性。
- 較大的top-k值(例如20到40)和高於1的溫度則適用於需要更多創造力和多樣性的任務,如創意寫作。
程式碼解密:
- 調整top-k值和溫度以控制輸出的隨機性和多樣性。
- 不同設定適用於不同的應用場景,如正式檔案、創意寫作等。
第5章練習解答
練習5.3
強制生成函式具有確定性行為有多種方法:
- 設定
top_k=None並且不應用溫度縮放 - 設定
top_k=1
練習5.4
本質上,我們需要載入在主章節中儲存的模型和最佳化器:
checkpoint = torch.load("model_and_optimizer.pth")
model = GPTModel(GPT_CONFIG_124M)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1)
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
然後,呼叫train_simple_function,設定num_epochs=1來再訓練模型一個epoch。
內容解密:
這段程式碼首先載入了之前儲存的模型和最佳化器的檢查點。然後,它初始化了一個GPT模型,並載入了檢查點中的模型狀態字典。接著,它初始化了一個AdamW最佳化器,並載入了檢查點中的最佳化器狀態字典。最後,它呼叫了train_simple_function來再訓練模型一個epoch。
練習5.5
我們可以使用以下程式碼來計算GPT模型的訓練和驗證集損失:
train_loss = calc_loss_loader(train_loader, gpt, device)
val_loss = calc_loss_loader(val_loader, gpt, device)
對於1.24億引數的模型,得到的損失如下: 訓練損失:3.754748503367106 驗證損失:3.559617757797241
主要觀察結果是,訓練和驗證集的效能在同一水平。這有多種可能的解釋:
- 當OpenAI訓練GPT-2時,“The Verdict”並不是預訓練資料集的一部分。因此,模型並沒有明確地過擬合訓練集,並且在訓練和驗證集上的表現相似。
- “The Verdict”是GPT-2的訓練資料集的一部分。在這種情況下,我們無法判斷模型是否過擬合訓練資料,因為驗證集也已被用於訓練。
內容解密:
這段程式碼計算了GPT模型在訓練和驗證集上的損失。結果顯示,訓練和驗證集的損失在同一水平,這可能是由於“The Verdict”不是預訓練資料集的一部分,或者是“The Verdict”是GPT-2的訓練資料集的一部分。
練習5.6
在主章節中,我們實驗了最小的GPT-2模型,它只有1.24億個引數。這樣做的原因是為了保持資源需求盡可能低。然而,你可以輕鬆地透過最小的程式碼更改來實驗更大的模型。例如,要載入15.58億引數的模型而不是1.24億引數的模型,我們只需要更改以下兩行程式碼:
hparams, params = download_and_load_gpt2(model_size="1558M", models_dir="gpt2")
model_name = "gpt2-xl (1558M)"
第6章練習解答
練習6.1
我們可以透過在初始化資料集時設定max_length=1024來將輸入填充到模型支援的最大標記數量:
train_dataset = SpamDataset(..., max_length=1024, ...)
val_dataset = SpamDataset(..., max_length=1024, ...)
test_dataset = SpamDataset(..., max_length=1024, ...)
然而,額外的填充導致測試準確率大幅下降到78.33%(相對於主章節中的95.67%)。
內容解密:
這段程式碼將輸入填充到模型支援的最大標記數量。然而,這導致了測試準確率的下降。
練習6.2
我們可以透過刪除以下程式碼來微調整個模型,而不是隻微調最後一個Transformer區塊:
for param in model.parameters():
param.requires_grad = False
這種修改導致測試準確率提高了1%,達到96.67%(相對於主章節中的95.67%)。
內容解密:
這段程式碼微調了整個模型,而不是隻微調最後一個Transformer區塊。這導致了測試準確率的提高。
練習6.3
我們可以透過將model(input_batch)[:, -1, :]更改為model(input_batch)[:, 0, :]來微調第一個輸出標記,而不是最後一個輸出標記。
正如預期的那樣,由於第一個標記包含的資訊少於最後一個標記,這種更改導致測試準確率大幅下降到75.00%(相對於主章節中的95.67%)。
內容解密:
這段程式碼微調了第一個輸出標記,而不是最後一個輸出標記。這導致了測試準確率的下降。
第7章練習解答
練習7.1
Phi-3提示格式如圖7.4所示,對於給定的示例輸入,看起來如下:
<user>
Identify the correct spelling of the following word: 'Occasion'
<assistant>
The correct spelling is 'Occasion'.
要使用此範本,我們可以按如下方式修改format_input函式:
def format_input(entry):
instruction_text = (
f"<|user|>\n{entry['instruction']}"
)
input_text = f"\n{entry['input']}" if entry["input"] else ""
return instruction_text + input_text
最後,我們還需要更新收集測試集回應時提取生成的回應的方式。
使用Phi-3範本微調模型大約快了17%,因為它導致了更短的模型輸入。得分接近50,與之前使用Alpaca樣式提示獲得的分數差不多。
內容解密:
這段程式碼修改了format_input函式以使用Phi-3提示格式。它還更新了收集測試集回應時提取生成的回應的方式。使用Phi-3範本微調模型導致了更短的模型輸入和相似的分數。
練習7.2
要像圖7.13中所示那樣遮蔽指令,我們需要對InstructionDataset類別和custom_collate_fn函式進行一些修改。我們可以修改InstructionDataset類別以收集指令的長度。