從零開始創建大語言模型(Python/pytorch )(四):從零開始實現一個用于生成文本的GPT模型
- 4 從零開始實現一個用于生成文本的GPT模型
- 4.1 編寫 L L M LLM LLM架構
- 4.2 使用層歸一化對激活值進行標準化
- 4.3 使用GELU激活函數實現前饋神經網絡
- GELU vs. ReLU
- GELU激活函數的定義
- 代碼實現
- 4.4 添加捷徑連接
- 4.5 在變換器塊中連接注意力機制和線性層
- 4.6 編寫GPT模型
- GPT-2 模型整體結構
- 最終的輸出層
- 練習 4.1:計算前饋網絡和多頭注意力模塊的參數數量
- 練習 4.2:初始化更大規模的 GPT 模型
- 4.7 生成文本
- 練習 4.3:使用不同的 dropout 參數
- 4.8 總結
前面章節內容會慢慢補上!!!
4 從零開始實現一個用于生成文本的GPT模型
本章內容涵蓋:
- 編寫一個類似GPT的大型語言模型( L L M LLM LLM),該模型可以被訓練以生成類人文本
- 歸一化層激活值以穩定神經網絡訓練
- 在深度神經網絡中添加捷徑連接,以更有效地訓練模型
- 實現Transformer模塊以創建不同規模的GPT模型
- 計算GPT模型的參數數量及存儲需求
在上一章中,你學習并實現了多頭注意力機制,這是大型語言模型的核心組件之一。在本章中,我們將編寫大型語言模型的其他構建模塊,并將它們組裝成一個類似GPT的模型,隨后在下一章中訓練該模型以生成類人文本,如圖4.1所示。
圖4.1 展示了編碼大型語言模型( L L M LLM LLM)的三個主要階段的思維模型,包括在通用文本數據集上對 L L M LLM LLM進行預訓練,并在標注數據集上進行微調。本章重點在于實現 L L M LLM LLM的架構,我們將在下一章對其進行訓練。
L L M LLM LLM的架構(如圖4.1所示)由多個構建模塊組成,我們將在本章中逐步實現這些模塊。在接下來的部分,我們將首先從整體上概述模型架構,然后再詳細介紹各個組件。
from importlib.metadata import versionprint("matplotlib version:", version("matplotlib"))
print("torch version:", version("torch"))
print("tiktoken version:", version("tiktoken"))
matplotlib version: 3.10.0
torch version: 2.5.1
tiktoken version: 0.9.0
4.1 編寫 L L M LLM LLM架構
大型語言模型( L L M LLM LLM),如 G P T GPT GPT(即生成式預訓練變換器),是一種大型深度神經網絡架構,旨在逐詞(或逐標記)生成新文本。然而,盡管這些模型規模龐大,它們的架構并不像你想象的那么復雜,因為許多組件是重復使用的,我們將在后續內容中看到這一點。圖4.2提供了一個類似 G P T GPT GPT的 L L M LLM LLM的整體視圖,并突出顯示了其主要組件。
圖4.2 展示了 G P T GPT GPT模型的思維模型。除了嵌入層外,它由一個或多個包含掩碼多頭注意力模塊的變換器塊組成,而該模塊正是我們在上一章中實現的。
如圖4.2所示,我們已經涵蓋了多個方面,例如輸入tokenization和嵌入,以及掩碼多頭注意力模塊。本章的重點將是實現 G P T GPT GPT模型的核心結構,包括其變換器塊,我們將在下一章訓練該模型以生成類人文本。
在前幾章中,為了簡化概念和示例,使其能夠輕松地適應單頁內容,我們使用了較小的嵌入維度。而在本章,我們將模型規模擴展到一個小型 G P T GPT GPT-2模型的級別,具體來說是 R a d f o r d Radford Radford等人在論文《Language Models are Unsupervised Multitask Learners》中描述的最小版本,該模型具有 1.24 1.24 1.24億個參數。需要注意的是,盡管原始報告中提到 1.17 1.17 1.17億個參數,但這一數值后來被更正。
第6章將重點介紹如何將預訓練權重加載到我們的實現中,并適配到更大規模的 G P T GPT GPT-2模型,包括 3.45 3.45 3.45億、 7.62 7.62 7.62億和 15.42 15.42 15.42億個參數的版本。在深度學習和類似 G P T GPT GPT的 L L M LLM LLM中,“參數”指的是模型的可訓練權重。這些權重本質上是模型的內部變量,在訓練過程中不斷調整和優化,以最小化特定的損失函數,從而使模型能夠從訓練數據中學習。
例如,在一個神經網絡層中,假設權重由一個 2048 × 2048 2048\times2048 2048×2048維矩陣(或張量)表示,那么該矩陣的每個元素都是一個參數。由于該矩陣有 2048 2048 2048行和 2048 2048 2048列,因此該層的總參數數量為 2048 × 2048 = 4194304 2048\times2048=4194304 2048×2048=4194304個參數。
G P T GPT GPT-2與 G P T GPT GPT-3的對比
需要注意的是,我們專注于 G P T GPT GPT-2,因為 O p e n A I OpenAI OpenAI已公開提供了其預訓練模型的權重,我們將在第6章將其加載到我們的實現中。從模型架構的角度來看, G P T GPT GPT-3在本質上與 G P T GPT GPT-2相同,只是將參數規模從 G P T GPT GPT-2的 15 15 15億擴展到 G P T GPT GPT-3的 1750 1750 1750億,并且訓練數據規模更大。截至撰寫本文時, G P T GPT GPT-3的權重尚未公開。
此外, G P T GPT GPT-2也是學習如何實現 L L M LLM LLM的更佳選擇,因為它可以在單臺筆記本電腦上運行,而 G P T GPT GPT-3的訓練和推理則需要 G P U GPU GPU集群。據 L a m b d a L a b s Lambda Labs LambdaLabs估算,使用單塊 V 100 V100 V100數據中心 G P U GPU GPU訓練 G P T GPT GPT-3需要 355 355 355年,而使用消費級 R T X 8000 RTX8000 RTX8000則需要 665 665 665年。
我們通過以下 P y t h o n Python Python字典來指定小型 G P T GPT GPT-2模型的配置,并將在后續代碼示例中使用:
GPT_CONFIG_124M = {"vocab_size": 50257, # Vocabulary size"context_length": 1024, # Context length"emb_dim": 768, # Embedding dimension"n_heads": 12, # Number of attention heads"n_layers": 12, # Number of layers"drop_rate": 0.1, # Dropout rate"qkv_bias": False # Query-Key-Value bias
}
在GPT_CONFIG_124M
字典中,我們使用簡潔的變量名,以提高清晰度并避免代碼行過長:
"vocab_size"
指的是詞匯量大小,共 50257 50257 50257個單詞,與第2章介紹的 B P E BPE BPE標記器使用的詞匯表一致。"context_length"
表示模型可以處理的最大輸入標記數,這通過第2章討論的位置嵌入實現。"emb_dim"
代表嵌入維度,將每個標記轉換為 768 768 768維向量。"n_heads"
指的是多頭注意力機制中的注意力頭數,該機制已在第3章實現。"n_layers"
指定了模型中的變換器塊數量,我們將在接下來的章節中詳細介紹。"drop_rate"
表示隨機失活(dropout)的強度( 0.1 0.1 0.1意味著隱藏單元有 10 % 10\% 10%的概率被丟棄),用于防止過擬合,這在第3章已介紹。"qkv_bias"
決定是否在多頭注意力機制中的查詢、鍵和值的線性層中包含偏置向量。最初,我們將禁用該選項,以遵循現代 L L M LLM LLM的標準做法,但在第6章加載來自 O p e n A I OpenAI OpenAI的預訓練 G P T GPT GPT-2權重時,我們將重新討論這一設置。
基于上述配置,我們將在本節實現一個 G P T GPT GPT占位架構(DummyGPTModel
),如圖4.3所示。這將幫助我們從整體上理解模型的架構,以及在接下來的章節中需要編寫哪些組件,以最終組裝完整的 G P T GPT GPT模型架構。
圖4.3 展示了 G P T GPT GPT架構的編寫順序。在本章中,我們將從 G P T GPT GPT的主干架構(占位架構)開始,然后逐步實現各個核心模塊,并最終將它們整合到變換器塊中,以構建完整的 G P T GPT GPT模型架構。
圖4.3中編號的框表示我們編寫最終 G P T GPT GPT架構時需要依次掌握的各個概念的順序。我們將從第1步開始,實現一個占位的 G P T GPT GPT主干架構,我們稱之為DummyGPTModel
:
代碼清單 4.1 占位 G P T GPT GPT模型架構類
import torch
import torch.nn as nnclass DummyGPTModel(nn.Module):def __init__(self, cfg):super().__init__()'''創建一個詞嵌入層,將輸入的 token 索引映射到一個固定維度的向量空間。'''self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])'''創建一個位置嵌入層,將每個位置映射到一個固定維度的向量。'''self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])'''構造一個 Dropout 層,用于在訓練時隨機丟棄部分神經元,防止過擬合。'''self.drop_emb = nn.Dropout(cfg["drop_rate"])# Use a placeholder for TransformerBlock'''* 操作符的作用是將列表中的元素拆開,為“解包”操作符。將這個列表中的所有元素“解包”,使它們變成 nn.Sequential 的單獨參數。等效于寫成(假設cfg["n_layers"] = 2):self.trf_blocks = nn.Sequential(DummyTransformerBlock(cfg), DummyTransformerBlock(cfg))創建一個由多個 TransformerBlock 組成的序列,然后用 nn.Sequential 將它們串聯在一起。'''self.trf_blocks = nn.Sequential(*[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])])# Use a placeholder for LayerNorm'''創建一個層歸一化層,通常用于穩定訓練,但這里使用的是一個占位實現(DummyLayerNorm),實際不做任何操作。'''self.final_norm = DummyLayerNorm(cfg["emb_dim"])'''創建一個線性層,將最后得到的嵌入映射到詞匯表大小的維度,輸出 logits。'''self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)def forward(self, in_idx):"""定義模型的前向傳播接口,輸入為 token 索引組成的張量 in_idx。"""batch_size, seq_len = in_idx.shape'''將 token 索引轉換為對應的嵌入向量。輸出形狀為 (batch_size, seq_len, emb_dim),例如 (2, 6, 8)。'''tok_embeds = self.tok_emb(in_idx)'''生成位置序列 [0, 1, ..., seq_len-1] 并查找對應的位置信息向量。輸出形狀為 (seq_len, emb_dim),例如 (6, 8)。'''pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))'''將 token 嵌入與位置嵌入相加,融合語義信息和位置信息。pos_embeds 的形狀 (6, 8) 自動擴展到 (2, 6, 8) 與 tok_embeds 相加。1. tok_embeds[0](6,8)+pos_embeds(6, 8) ->[0](6,8)2. tok_embeds[1](6,8)+pos_embeds(6, 8) ->[1](6,8)變成(2, 6, 8)'''x = tok_embeds + pos_embedsx = self.drop_emb(x)'''將 x 依次傳入所有 DummyTransformerBlock 層處理。'''x = self.trf_blocks(x)'''對 x 進行層歸一化操作,穩定訓練。'''x = self.final_norm(x)'''通過線性變換,將每個 token 的 8 維向量映射到詞匯表大小維度(例如 100),得到每個詞的 logits。'''logits = self.out_head(x)return logitsclass DummyTransformerBlock(nn.Module):"""定義一個占位用的 TransformerBlock 模塊。"""def __init__(self, cfg):super().__init__()# A simple placeholderdef forward(self, x):# This block does nothing and just returns its input.return xclass DummyLayerNorm(nn.Module):"""定義一個占位用的 LayerNorm 層,用于模擬接口。"""def __init__(self, normalized_shape, eps=1e-5):super().__init__()# The parameters here are just to mimic the LayerNorm interface.def forward(self, x):# This layer does nothing and just returns its input.return x
pytorch小記(十四):pytorch中 nn.Embedding 詳解
DummyGPTModel
類在這段代碼中定義了一個使用 P y T o r c h PyTorch PyTorch神經網絡模塊(nn.Module
)的簡化版 G P T GPT GPT模型。DummyGPTModel
類的模型架構包括標記嵌入和位置嵌入、隨機失活、若干個變換器塊(DummyTransformerBlock
)、最終的層歸一化(DummyLayerNorm
),以及一個線性輸出層(out_head
)。模型的配置通過 P y t h o n Python Python字典傳入,例如我們之前創建的GPT_CONFIG_124M
字典。
forward
方法描述了數據在模型中的流動過程:它計算輸入索引的標記嵌入和位置嵌入,應用隨機失活,然后將數據傳遞至變換器塊進行處理,接著應用歸一化,并最終通過線性輸出層生成對數概率(logits
)。
上述代碼已經是一個可運行的模型,我們將在本節后續部分準備輸入數據并進行測試。然而,目前需要注意的是,代碼中我們使用了占位符(DummyLayerNorm
和DummyTransformerBlock
)來代替變換器塊和層歸一化,這些組件將在后續部分實現。
接下來,我們將準備輸入數據并初始化一個新的 G P T GPT GPT模型,以演示其使用方式。基于我們在第2章中編寫標記器的內容,圖4.4提供了數據在 G P T GPT GPT模型中流入和流出的整體概覽。
圖4.4 展示了數據流入 G P T GPT GPT模型的全局視圖,包括輸入數據的標記化、嵌入處理,并最終輸入到 G P T GPT GPT模型。需要注意,在我們之前編寫的DummyGPTClass
中,標記嵌入是在 G P T GPT GPT模型內部處理的。在 L L M LLM LLM中,嵌入輸入標記的維度通常與輸出維度相匹配。這里的輸出嵌入代表了我們在第3章討論的上下文向量。
為了實現圖4.4所示的步驟,我們使用第2章介紹的tiktoken
標記器對一批包含兩條文本輸入的數據進行標記化,以供 G P T GPT GPT模型使用。
import tiktokentokenizer = tiktoken.get_encoding("gpt2")batch = []txt1 = "Every effort moves you"
txt2 = "Every day holds a"batch.append(torch.tensor(tokenizer.encode(txt1)))
batch.append(torch.tensor(tokenizer.encode(txt2)))
batch = torch.stack(batch, dim=0)
print(batch)
由此得到的兩個文本的標記 ID 如下:
tensor([[6109, 3626, 6100, 345], #A[6109, 1110, 6622, 257]])
接下來,我們初始化一個具有 1.24 1.24 1.24億參數的DummyGPTModel
實例,并將標記化后的數據批次輸入到模型中:
torch.manual_seed(123)
model = DummyGPTModel(GPT_CONFIG_124M)
logits = model(batch)
print("Output shape:", logits.shape)
print(logits)
模型的輸出通常稱為對數概率(logits),具體如下:
Output shape: torch.Size([2, 4, 50257])
tensor([[[-0.9289, 0.2748, -0.7557, ..., -1.6070, 0.2702, -0.5888],[-0.4476, 0.1726, 0.5354, ..., -0.3932, 1.5285, 0.8557],[ 0.5680, 1.6053, -0.2155, ..., 1.1624, 0.1380, 0.7425],[ 0.0447, 2.4787, -0.8843, ..., 1.3219, -0.0864, -0.5856]],[[-1.5474, -0.0542, -1.0571, ..., -1.8061, -0.4494, -0.6747],[-0.8422, 0.8243, -0.1098, ..., -0.1434, 0.2079, 1.2046],[ 0.1355, 1.1858, -0.1453, ..., 0.0869, -0.1590, 0.1552],[ 0.1666, -0.8138, 0.2307, ..., 2.5035, -0.3055, -0.3083]]],grad_fn=<UnsafeViewBackward0>)
Output shape: torch.Size([2, 4, 50257]):可以理解成2個batch,每個batch有4個單詞(token),每個單詞(token)用50257維來表示
輸出張量包含兩行,對應于兩條文本樣本。每個文本樣本由 4 4 4個標記組成,每個標記是一個 50257 50257 50257維的向量,這與標記器的詞匯表大小相匹配。
嵌入層的維度為 50257 50257 50257,因為這些維度中的每一個都對應于詞匯表中的一個唯一標記。在本章末尾,當我們實現后處理代碼時,我們將把這些 50257 50257 50257維的向量轉換回標記 I D ID ID,然后解碼為單詞。
現在,我們已經從整體上了解了 G P T GPT GPT架構及其輸入和輸出,接下來將在后續部分逐步編寫具體的占位組件,從實現真實的層歸一化類開始,以替換之前代碼中的DummyLayerNorm
。
4.2 使用層歸一化對激活值進行標準化
在訓練具有多個層的深度神經網絡時,可能會遇到梯度消失或梯度爆炸等問題。這些問題會導致訓練過程不穩定,使得網絡難以有效地調整其權重。這意味著學習過程難以找到一組能夠最小化損失函數的參數(權重)。換句話說,網絡難以學習數據中的潛在模式,從而無法做出準確的預測或決策。
(如果你對神經網絡訓練和梯度的概念不熟悉,可以參考附錄A中的A.4節 《自動微分簡明介紹》。然而,理解本教程內容并不需要對梯度有深奧的數學理解。)
在本節中,我們將實現層歸一化,以提高神經網絡訓練的穩定性和效率。
層歸一化的核心思想是調整神經網絡層的激活值(輸出),使其均值為 0 0 0,方差為 1 1 1,也稱為單位方差。這種調整可以加速收斂,使網絡更快地找到有效的權重,并確保訓練過程的穩定性和可靠性。正如我們在上一節基于DummyLayerNorm
占位符所看到的,在 G P T GPT GPT-2及現代變換器架構中,層歸一化通常應用于多頭注意力模塊的前后,以及最終輸出層的前面。
在我們編寫層歸一化的代碼之前,圖4.5提供了層歸一化的可視化概覽。
圖4.5展示了如何對 5 5 5個層的輸出(即激活值)進行歸一化,使其均值為 0 0 0,方差為 1 1 1。
我們可以通過以下代碼重新創建圖4.5所示的示例,其中我們實現了一個具有 5 5 5個輸入和 6 6 6個輸出的神經網絡層,并將其應用于兩個輸入示例:
torch.manual_seed(123)# create 2 training examples with 5 dimensions (features) each
batch_example = torch.randn(2, 5)
print(batch_example)
layer = nn.Sequential(nn.Linear(5, 6), nn.ReLU())
out = layer(batch_example)
print(out)
這將打印出以下張量,其中第一行列出了第一個輸入的層輸出,第二行列出了第二個輸入的層輸出:
tensor([[-0.1115, 0.1204, -0.3696, -0.2404, -1.1969],[ 0.2093, -0.9724, -0.7550, 0.3239, -0.1085]])
tensor([[0.2260, 0.3470, 0.0000, 0.2216, 0.0000, 0.0000],[0.2133, 0.2394, 0.0000, 0.5198, 0.3297, 0.0000]],grad_fn=<ReluBackward0>)
我們編寫的神經網絡層由一個線性層(Linear
)和一個非線性激活函數 R e L U ReLU ReLU(即修正線性單元)組成, R e L U ReLU ReLU是神經網絡中的標準激活函數。如果你對 R e L U ReLU ReLU不熟悉,它的作用是將負輸入截斷為 0 0 0,確保層的輸出僅包含正值,這也解釋了為什么最終的層輸出中不包含任何負值。(需要注意的是,在 G P T GPT GPT模型中,我們將使用另一種更復雜的激活函數,我們將在下一節介紹。)
在對這些輸出應用層歸一化之前,讓我們先檢查其均值和方差:
mean = out.mean(dim=-1, keepdim=True)
var = out.var(dim=-1, keepdim=True)print("Mean:\n", mean)
print("Variance:\n", var)
輸出結果如下:
Mean:tensor([[0.1324],[0.2170]], grad_fn=<MeanBackward1>)
Variance:tensor([[0.0231],[0.0398]], grad_fn=<VarBackward0>)
上方均值張量的第一行包含第一個輸入行的均值,第二行包含第二個輸入行的均值。
在計算均值或方差等操作時,使用keepdim=True
可以確保輸出張量保持與輸入張量相同的形狀,即使操作沿著由dim
參數指定的維度進行降維。例如,如果不使用keepdim=True
,返回的均值張量將是一個 2 2 2維向量[0.1324, 0.2170]
,而不是一個 2 × 1 2\times1 2×1維矩陣[[0.1324], [0.2170]]
。
dim
參數指定在張量的哪個維度上計算統計量(如均值或方差),其作用如圖4.6所示。
圖4.6 展示了在計算張量均值時
dim
參數的作用。例如,如果我們有一個 2 D 2D 2D張量(矩陣),其維度為 [ 行 , 列 ] [行,列] [行,列],那么使用dim=0
將在行方向(縱向,見圖底部)執行操作,得到的輸出聚合了每列的數據。而使用dim=1
或dim=-1
將在列方向(橫向,見圖頂部)執行操作,得到的輸出聚合了每行的數據。
如圖4.6所示,對于 2 D 2D 2D張量(如矩陣),在計算均值或方差等操作時,使用dim=-1
等同于使用dim=1
。這是因為-1
表示張量的最后一個維度,在 2 D 2D 2D張量中對應于列。
在后續為 G P T GPT GPT模型添加層歸一化時,該模型會生成形狀為[batch_size, num_tokens, embedding_size]
的 3 D 3D 3D張量,我們仍然可以使用dim=-1
在最后一個維度上進行歸一化,而無需將dim=1
改為dim=2
。
接下來,讓我們對之前獲得的層輸出應用層歸一化。該操作包括減去均值并除以方差的平方根(即標準差):
out_norm = (out - mean) / torch.sqrt(var)
mean = out_norm.mean(dim=-1, keepdim=True)
var = out_norm.var(dim=-1, keepdim=True)print("Normalized layer outputs:\n", out_norm)
print("Mean:\n", mean)
print("Variance:\n", var)
從結果可以看出,歸一化后的層輸出現在包含了負值,并且均值為 0 0 0,方差為 1 1 1。
Normalized layer outputs:tensor([[ 0.6159, 1.4126, -0.8719, 0.5872, -0.8719, -0.8719],[-0.0189, 0.1121, -1.0876, 1.5173, 0.5647, -1.0876]],grad_fn=<DivBackward0>)
Mean:tensor([[9.9341e-09],[0.0000e+00]], grad_fn=<MeanBackward1>)
Variance:tensor([[1.0000],[1.0000]], grad_fn=<VarBackward0>)
需要注意的是,輸出張量中的值9.9341e-08
是科學計數法的表示方式,相當于 9.9341 × 1 0 ? 9 9.9341\times10^{-9} 9.9341×10?9,即十進制形式的0.0000000099341
。這個值非常接近 0 0 0,但由于計算機表示數值時的有限精度,可能會累積微小的數值誤差,因此不會完全等于 0 0 0。
為了提高可讀性,我們可以關閉科學計數法的顯示方式,只需將sci_mode
設置為False
:
torch.set_printoptions(sci_mode=False)
print("Mean:\n", mean)
print("Variance:\n", var)
輸出結果:
Mean:tensor([[ 0.0000],[ 0.0000]], grad_fn=<MeanBackward1>)
Variance:tensor([[1.0000],[1.0000]], grad_fn=<VarBackward0>)
到目前為止,在本節中,我們已經逐步實現并應用了層歸一化。現在,我們將這一過程封裝到一個 P y T o r c h PyTorch PyTorch模塊中,以便在后續的 G P T GPT GPT模型中使用:
代碼清單 4.2 層歸一化類
class LayerNorm(nn.Module):def __init__(self, emb_dim):super().__init__()self.eps = 1e-5self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))def forward(self, x):mean = x.mean(dim=-1, keepdim=True)var = x.var(dim=-1, keepdim=True, unbiased=False)norm_x = (x - mean) / torch.sqrt(var + self.eps)return self.scale * norm_x + self.shift
這一層歸一化的實現針對輸入張量x
的最后一個維度進行操作,該維度對應于嵌入維度(emb_dim
)。變量eps
是一個很小的常數( ? = 1 0 ? 5 \epsilon=10^{-5} ?=10?5),用于防止歸一化過程中出現除零錯誤。
scale
和shift
是兩個可訓練參數,它們的維度與輸入相同。在 L L M LLM LLM的訓練過程中,模型會自動調整這些參數,以優化其在訓練任務上的表現。這使得模型能夠學習適合其處理數據的最佳縮放和偏移,從而提高訓練效果。
有偏方差計算
在方差計算方法中,我們選擇了一個實現細節,即將unbiased=False
。對于好奇這一點的讀者,在方差計算中,我們使用公式中的樣本數量 n n n作為分母進行計算,而不是使用 n ? 1 n-1 n?1(即貝塞爾校正)。貝塞爾校正通常用于修正樣本方差估計中的偏差,而我們的做法會導致所謂的有偏方差估計。
對于大型語言模型( L L M LLM LLM),由于嵌入維度 n n n通常非常大,使用 n n n和 n ? 1 n-1 n?1之間的差異實際上可以忽略不計。我們選擇這種方式,以確保與 G P T GPT GPT-2模型的歸一化層保持兼容,并且這樣做也符合 T e n s o r F l o w TensorFlow TensorFlow的默認行為( G P T GPT GPT-2的原始實現基于 T e n s o r F l o w TensorFlow TensorFlow)。采用類似的設置可以確保我們的方法與第6章將要加載的預訓練權重兼容。
現在,讓我們在實踐中測試LayerNorm
模塊,并將其應用到批量輸入數據上:
ln = LayerNorm(emb_dim=5)
out_ln = ln(batch_example)mean = out_ln.mean(dim=-1, keepdim=True)
var = out_ln.var(dim=-1, unbiased=False, keepdim=True)print("Mean:\n", mean)
print("Variance:\n", var)
從結果可以看出,層歸一化代碼正常工作,并且歸一化后的兩個輸入數據的均值為 0 0 0,方差為 1 1 1:
Mean:tensor([[ -0.0000],[ 0.0000]], grad_fn=<MeanBackward1>)
Variance:tensor([[1.0000],[1.0000]], grad_fn=<VarBackward0>)
在本節中,我們介紹了 G P T GPT GPT架構所需的一個重要構建模塊,如圖4.7所示。
圖4.7 展示了本章中實現 G P T GPT GPT架構所需的各個構建模塊的思維模型。
在下一節中,我們將介紹 GELU(高斯誤差線性單元)激活函數,它是 L L M LLM LLM中常用的激活函數之一,而不是本節中使用的傳統 R e L U ReLU ReLU函數。
層歸一化 vs. 批量歸一化
如果你熟悉 批量歸一化(Batch Normalization),這是一種常見的神經網絡歸一化方法,那么你可能會好奇它與 層歸一化(Layer Normalization) 的對比。
與批量歸一化不同,批量歸一化是在 批量(batch) 維度 上進行歸一化,而層歸一化則是在 特征(feature) 維度 上進行歸一化。
由于 L L M LLM LLM通常需要大量計算資源,可用的硬件或特定的使用場景可能會影響訓練或推理時的批量大小(batch size)。而層歸一化對每個輸入獨立進行歸一化,不受批量大小的影響,因此在這些情況下提供了更大的靈活性和穩定性。
這種特性在 分布式訓練 或 計算資源受限的環境 中尤其有利,使得模型能夠在不同的設備和計算環境下保持穩定的表現。
4.3 使用GELU激活函數實現前饋神經網絡
在本節中,我們將實現一個小型神經網絡子模塊,該模塊是 L L M LLM LLM中變換器塊的一部分。我們首先實現 GELU(高斯誤差線性單元)激活函數,它在此神經網絡子模塊中起著關鍵作用。(如果需要額外了解如何在 P y T o r c h PyTorch PyTorch中實現神經網絡,請參考附錄A的A.5節 《實現多層神經網絡》。)
GELU vs. ReLU
歷史上,ReLU(修正線性單元)因其簡單性和在各種神經網絡架構中的有效性而被廣泛使用。然而,在 L L M LLM LLM中,除了傳統的 R e L U ReLU ReLU之外,還使用了其他幾種激活函數,其中兩個重要的例子是:
- GELU(高斯誤差線性單元)
- SwiGLU(Sigmoid加權線性單元)
GELU和SwiGLU比 R e L U ReLU ReLU更復雜,它們分別結合了 高斯分布 和 Sigmoid門控線性單元,能夠為深度學習模型提供更好的性能。
GELU激活函數的定義
GELU激活函數可以用多種方式實現,最精確的數學定義如下:
GELU(x) = x ? Φ ( x ) \text{GELU(x)} = \text{x} \cdot \Phi(\text{x}) GELU(x)=x?Φ(x)
其中, Φ ( x ) \Phi(x) Φ(x) 是 標準高斯分布的累積分布函數(CDF)。
但在實際應用中,通常采用計算量更小的近似版本( G P T GPT GPT-2模型的原始訓練也是基于此近似實現):
GELU(x) ≈ 0.5 ? x ? ( 1 + tanh ? ( 2 / π ? ( x + 0.044715 ? x 3 ) ) ) \text{GELU(x)} \approx 0.5 \cdot \text{x} \cdot (1 + \tanh(\sqrt{2/\pi} \cdot (\text{x} + 0.044715 \cdot \text{x}^3))) GELU(x)≈0.5?x?(1+tanh(2/π??(x+0.044715?x3)))
代碼實現
我們可以將此函數實現為 P y T o r c h PyTorch PyTorch模塊,如下所示:
代碼清單 4.3 GELU激活函數的實現
class GELU(nn.Module):def __init__(self):super().__init__()def forward(self, x):return 0.5 * x * (1 + torch.tanh(torch.sqrt(torch.tensor(2.0 / torch.pi)) * (x + 0.044715 * torch.pow(x, 3))))
接下來,為了直觀地了解 GELU 函數的形狀,并與 ReLU 函數進行對比,我們可以將這兩個函數繪制在同一張圖上:
import matplotlib.pyplot as pltgelu, relu = GELU(), nn.ReLU()
x = torch.linspace(-3, 3, 100) # A
y_gelu, y_relu = gelu(x), relu(x)plt.figure(figsize=(8, 3))
for i, (y, label) in enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1):plt.subplot(1, 2, i)plt.plot(x, y)plt.title(f"{label} activation function")plt.xlabel("x")plt.ylabel(f"{label}(x)")plt.grid(True)plt.tight_layout()
plt.show()
從 圖4.8 所示的繪圖結果可以看出,ReLU 是一個分段線性函數:當輸入為正時,它直接輸出輸入值;當輸入為負時,它輸出 0 0 0。
而 GELU 是一個平滑的非線性函數,它近似 R e L U ReLU ReLU,但對于負值輸入仍然保持了非零梯度。
圖4.8 展示了使用
matplotlib
繪制的 GELU 和 ReLU 函數的輸出。橫軸( x x x軸)表示函數輸入,縱軸( y y y軸)表示函數輸出。
GELU 的平滑特性(如圖4.8所示)可以在訓練過程中帶來更好的優化性能,因為它允許對模型參數進行更細微的調整。相比之下,ReLU 在 0 0 0處存在一個尖角,這可能會在優化過程中帶來困難,尤其是在深層網絡或復雜架構中。
此外,與 ReLU 不同,ReLU 對任何負輸入都會輸出 0 0 0,而 GELU 對負輸入仍然允許一個小的非零輸出。這意味著在訓練過程中,即使某些神經元接收到負輸入,它們仍然可以在一定程度上參與學習過程,盡管其貢獻比正輸入要小。
接下來,我們使用 GELU 函數來實現一個小型神經網絡模塊 FeedForward,該模塊將在后續的 L L M LLM LLM變換器塊中使用:
代碼清單 4.4 前饋神經網絡模塊
class FeedForward(nn.Module):def __init__(self, cfg):super().__init__()self.layers = nn.Sequential(nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),GELU(),nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),)def forward(self, x):return self.layers(x)print(GPT_CONFIG_124M["emb_dim"])
768
如上面的代碼所示,FeedForward 模塊是一個由 兩個線性層(Linear layers) 和 GELU激活函數 組成的小型神經網絡。在 具有1.24億參數的 G P T GPT GPT模型 中,該模塊接收 輸入批次(batches),其中每個標記(token)的嵌入維度(embedding size) 為 768,這一配置由GPT_CONFIG_124M
字典中的參數 GPT_CONFIG_124M["emb_dim"] = 768
確定。
圖4.9 展示了當輸入通過該 前饋神經網絡(Feed Forward Neural Network) 時,嵌入維度如何在該小型網絡內部進行變換。
圖4.9 直觀地展示了前饋神經網絡各層之間的連接關系。值得注意的是,該神經網絡可以處理不同的批量大小(batch size) 和 輸入標記數(num_tokens),但每個標記的嵌入維度(embedding size) 在初始化權重時即被確定,并保持固定。
按照 圖4.9 中的示例,我們初始化一個新的 FeedForward 模塊,其 標記嵌入維度(embedding size) 為 768,并向其輸入一個包含 2個樣本,每個樣本包含3個標記 的批量輸入:
ffn = FeedForward(GPT_CONFIG_124M)
x = torch.rand(2, 3, 768) # A
out = ffn(x)
print(out.shape)
如我們所見,輸出張量的形狀與輸入張量相同:
torch.Size([2, 3, 768])
在本節中實現的 FeedForward 模塊在 增強模型的學習能力和泛化能力 方面發揮著重要作用。
盡管該模塊的 輸入和輸出維度相同,但其 內部 通過第一層線性變換(Linear Layer) 將嵌入維度擴展到 更高維度空間,然后經過 GELU非線性激活,最終通過 第二層線性變換 將維度 收縮回原始尺寸,如 圖4.10 所示。
這種設計使得模型能夠探索更豐富的表示空間,進而提升對數據的理解能力。
圖4.10 展示了 前饋神經網絡(Feed Forward Neural Network) 中 層輸出的擴展與收縮過程:1. 第一層線性變換:將輸入 擴展4倍,即 從768維度擴展到3072維度。2. GELU非線性激活:對擴展后的向量進行非線性變換。3. 第二層線性變換:將3072維度壓縮回768維度,以匹配原始輸入的維度。
此外,輸入和輸出維度的一致性 簡化了模型架構,使得我們可以 堆疊多個層(我們將在后續實現),而無需在層之間調整維度,從而提高模型的 可擴展性(scalability)。
如 圖4.11 所示,我們已經實現了 大部分 L L M LLM LLM的核心構建模塊。
圖4.11 展示了本章涵蓋的主題,其中 黑色對勾標記表示我們已經完成的部分。
在下一節中,我們將介紹 捷徑連接(shortcut connections) 的概念。這些連接被插入到神經網絡的不同層之間,對于 提升深度神經網絡架構的訓練性能 至關重要。
4.4 添加捷徑連接
接下來,我們討論 捷徑連接(shortcut connections) 的概念,這種連接也稱為 跳躍連接(skip connections) 或 殘差連接(residual connections)。
最初,捷徑連接 是為 計算機視覺中的深度網絡(尤其是殘差網絡(ResNet))提出的,目的是 緩解梯度消失問題(vanishing gradient problem)。
梯度消失問題 指的是,在訓練過程中,梯度(用于指導權重更新)在向后傳播時逐漸變小,導致模型難以有效訓練 靠近輸入端的早期層,如 圖4.12 所示。
圖4.12 比較了 一個包含5層的深度神經網絡,其中:
- 左側 是 沒有捷徑連接 的網絡。
- 右側 是 添加了捷徑連接 的網絡。
捷徑連接的核心思想是:將某一層的輸入直接加到該層的輸出上,從而 創建一個繞過某些層的額外路徑。圖1.1 中所示的 梯度(gradient) 代表 每一層的平均絕對梯度值,我們將在后續的代碼示例中計算這些梯度。
如 圖4.12 所示,捷徑連接(shortcut connection) 創建了一條 額外的、較短的梯度傳播路徑,使梯度可以繞過一層或多層直接流經網絡。這種 跳躍連接(skip connection) 通過 將前一層的輸出加到后續層的輸出上 實現,因此在訓練的 反向傳播(backward pass) 過程中,它們在 保持梯度流動 方面起到了至關重要的作用。
在下面的代碼示例中,我們實現了 圖4.12 中所示的神經網絡,并探索如何在 前向傳播(forward method) 中添加 捷徑連接:
代碼清單 4.5 實現帶有捷徑連接的深度神經網絡
class ExampleDeepNeuralNetwork(nn.Module):def __init__(self, layer_sizes, use_shortcut):super().__init__()self.use_shortcut = use_shortcutself.layers = nn.ModuleList([# 5 層,每層包含一個線性層(Linear)和GELU激活函數nn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), GELU()),nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), GELU()),nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), GELU()),nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), GELU()),nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), GELU())])def forward(self, x):for layer in self.layers:# 計算當前層的輸出layer_output = layer(x)# 如果啟用了捷徑連接,并且形狀匹配,則應用捷徑連接if self.use_shortcut and x.shape == layer_output.shape:x = x + layer_outputelse:x = layer_outputreturn x
代碼解析
- 該代碼實現了一個 深度神經網絡,包含 5層,每一層由:
- 一個線性層(Linear layer)
- GELU激活函數
- 在 前向傳播(forward pass) 過程中:
- 逐層計算輸入的輸出
- 如果
self.use_shortcut=True
且當前層的輸入與輸出形狀相同,則 應用捷徑連接(x = x + layer_output) - 否則,直接使用當前層的輸出(x = layer_output)
初始化一個不帶捷徑連接的神經網絡
我們首先初始化 一個沒有捷徑連接的神經網絡。這里,每一層都被初始化為:
- 輸入為3個數值
- 輸出為3個數值
- 最后一層輸出1個數值
layer_sizes = [3, 3, 3, 3, 3, 1]
sample_input = torch.tensor([[1., 0., -1.]])torch.manual_seed(123) # 指定隨機種子,以確保可復現性
model_without_shortcut = ExampleDeepNeuralNetwork(layer_sizes, use_shortcut=False
)
這樣,我們創建了一個 不包含捷徑連接的神經網絡,其權重由 隨機種子 設定,以確保實驗結果可復現。
接下來,我們實現一個 計算模型反向傳播梯度 的函數:
def print_gradients(model, x):# 前向傳播output = model(x)target = torch.tensor([[0.]])# 計算損失,衡量模型輸出與目標值(此處設為0)的接近程度loss = nn.MSELoss()loss = loss(output, target)# 反向傳播計算梯度loss.backward()for name, param in model.named_parameters():if 'weight' in name:# 打印權重梯度的平均絕對值print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")
在上述代碼中,我們定義了損失函數,用于計算模型輸出與用戶指定的目標值(此處設為 0)的誤差。然后,調用 loss.backward()
,讓 PyTorch 自動計算模型每一層的梯度。我們可以通過 model.named_parameters()
迭代模型的權重參數 并獲取其梯度信息。假設某一層的權重參數矩陣是 3 × 3 3\times3 3×3,那么該層將會有 3 × 3 3\times3 3×3 個梯度值。為了更方便地比較不同層的梯度,我們 計算并打印這些梯度的平均絕對值。
關于 .backward()
.backward()
方法 是 PyTorch 中的一個便捷函數,可 自動計算損失函數的梯度,無需手動實現梯度計算的數學公式。- 這一方法使得 深度神經網絡的訓練更加高效和便捷。
如果你對 梯度(gradients) 和 神經網絡訓練過程 還不熟悉,建議閱讀 附錄A 中的以下內容:
- A.4: 《自動微分簡明介紹(Auto Differentiation Made Easy)》
- A.7: 《典型訓練循環(A Typical Training Loop)》
現在,我們使用 print_gradients
函數,并將其應用于 不帶跳躍連接(skip connections) 的模型:
print_gradients(model_without_shortcut, sample_input)
輸出如下:
layers.0.0.weight has gradient mean of 0.00020173587836325169
layers.1.0.weight has gradient mean of 0.0001201116101583466
layers.2.0.weight has gradient mean of 0.0007152041653171182
layers.3.0.weight has gradient mean of 0.001398873864673078
layers.4.0.weight has gradient mean of 0.005049646366387606
從 print_gradients
函數的輸出可以看出,梯度值從最后一層(layers.4) 到第一層(layers.0) 逐漸減小,這種現象被稱為 梯度消失問題(vanishing gradient problem)。
創建帶跳躍連接的模型,并進行比較
接下來,我們 實例化一個包含跳躍連接(skip connections) 的模型,并觀察梯度分布的變化:
torch.manual_seed(123)
model_with_shortcut = ExampleDeepNeuralNetwork(layer_sizes, use_shortcut=True
)
print_gradients(model_with_shortcut, sample_input)
輸出如下:
layers.0.0.weight has gradient mean of 0.22169792652130127
layers.1.0.weight has gradient mean of 0.20694106817245483
layers.2.0.weight has gradient mean of 0.32896995544433594
layers.3.0.weight has gradient mean of 0.2665732502937317
layers.4.0.weight has gradient mean of 1.3258541822433472
從 帶跳躍連接的模型 的梯度分布可以看出:
- 梯度不再隨著層數增加而逐漸減小,而是 在各層保持相對較大。
- 這證明了 跳躍連接有效緩解了梯度消失問題,使得梯度能夠更順暢地從 輸出層 傳遞到 輸入層,從而改善深度神經網絡的訓練性能。
這里 “從 輸出層 傳遞到 輸入層” 指的是反向傳播過程!
從輸出結果可以看出,最后一層(layers.4) 的梯度仍然較大,但梯度值在向第一層(layers.0) 傳播的過程中趨于穩定,并且不會縮小到極小的值。
捷徑連接(shortcut connections) 在深度神經網絡中對于克服 梯度消失問題(vanishing gradient problem) 的限制至關重要。捷徑連接是 超大規模模型(如 L L M LLM LLM) 的核心構建模塊,它們在訓練 G P T GPT GPT模型時確保梯度能夠穩定地 在各層之間流動,從而 提高訓練的有效性。
在介紹完 捷徑連接 之后,我們將在 下一節 中 整合 之前討論的所有核心概念:
- 層歸一化(Layer Normalization)
- GELU激活函數
- 前饋模塊(Feed Forward Module)
- 捷徑連接(Shortcut Connections)
最終,我們將 在變換器塊(Transformer Block) 中實現這些組件,這是 G P T GPT GPT架構的最后一個構建模塊。
4.5 在變換器塊中連接注意力機制和線性層
在本節中,我們將實現 變換器塊(Transformer Block),這是 GPT 及其他 L L M LLM LLM架構 的 核心構建模塊。
在 G P T GPT GPT-2(1.24億參數) 的架構 中,該變換器塊被 重復多次,并整合了我們之前介紹的多個概念:
- 多頭注意力機制(Multi-Head Attention)
- 層歸一化(Layer Normalization)
- 隨機失活(Dropout)
- 前饋層(Feed Forward Layers)
- GELU激活函數
這一結構如 圖4.13 所示。
在下一節中,我們將 把該變換器塊與 G P T GPT GPT架構的其余部分連接,完成最終的 完整 G P T GPT GPT模型。
圖4.13 展示了 變換器塊(Transformer Block) 的組成結構:
- 底部 顯示了 輸入標記(tokens),它們已經被 轉換為768維的向量(即 嵌入表示)。
- 每一行(row) 對應 一個標記的向量表示。
- 變換器塊的輸出 與 輸入的維度相同,因此可以進一步輸入到 后續的 L L M LLM LLM層 進行處理。
如 圖4.13 所示,變換器塊(Transformer Block) 結合了多個關鍵組件,包括:
- 第3章介紹的掩碼多頭注意力模塊(Masked Multi-Head Attention Module)
- 第4.3節實現的前饋模塊(FeedForward Module)
當變換器塊處理 輸入序列 時,序列中的每個元素(如單詞或子詞標記) 都會被表示為一個 固定大小的向量(在 圖4.13 中,該向量維度為 768)。變換器塊的操作(包括 多頭注意力機制和前饋層)不會改變輸入向量的維度,而是通過 變換這些向量的表示方式 來增強模型的處理能力。
核心思想
- 多頭自注意力機制(Multi-Head Self-Attention) 負責 識別和分析輸入序列中元素之間的關系。
- 前饋神經網絡(Feed Forward Network, FFN) 在 每個位置單獨修改數據。
這種 組合方式 既能 讓模型更深入地理解輸入序列,又能 增強模型處理復雜數據模式的能力。
代碼實現
我們可以使用以下代碼實現 TransformerBlock
:
代碼清單 4.6 G P T GPT GPT的變換器塊組件
from previous_chapters import MultiHeadAttentionclass 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"],qkv_bias=cfg["qkv_bias"])'''(1,4)@(4,16) -> (1,16)(1,16)@(16,4) -> (1,4)'''self.ff = FeedForward(cfg)self.norm1 = LayerNorm(cfg["emb_dim"])self.norm2 = LayerNorm(cfg["emb_dim"])self.drop_shortcut = nn.Dropout(cfg["drop_rate"])def forward(self, x):# Shortcut connection for attention blockshortcut = xx = self.norm1(x)x = self.att(x) # Shape [batch_size, num_tokens, emb_size]x = self.drop_shortcut(x)x = x + shortcut # Add the original input back# Shortcut connection for feed forward blockshortcut = xx = self.norm2(x)x = self.ff(x)x = self.drop_shortcut(x)x = x + shortcut # Add the original input backreturn x
下載 previous_chapters.py 到相同目錄中
按照圖4.13流程來走,很容易理解!
該代碼定義了 TransformerBlock
類,這是一個 P y T o r c h PyTorch PyTorch 實現的變換器塊,其中包含:
- 多頭注意力機制(
MultiHeadAttention
) - 前饋網絡(
FeedForward
) - 層歸一化(
LayerNorm
) - 隨機失活(
Dropout
)
這些組件的配置基于 提供的配置字典(cfg
),例如 GPT_CONFIG_124M
。
Pre-LayerNorm vs. Post-LayerNorm
- 在 本實現 中,層歸一化 (LayerNorm) 應用在多頭注意力和前饋網絡之前,這種方式被稱為 “Pre-LayerNorm”。
- 在 早期的變換器架構(如原始 T r a n s f o r m e r Transformer Transformer模型) 中,層歸一化是應用在這些模塊之后,這種方式被稱為 “Post-LayerNorm”。
- “Post-LayerNorm” 可能會導致 訓練動態較差,因此現代 L L M LLM LLM通常采用 “Pre-LayerNorm”。
前向傳播 (Forward Pass) 該類還實現了 前向傳播,其中:
- 每個組件 后面都加上捷徑連接(shortcut connection),即 將塊的輸入添加到其輸出。
- 這種 捷徑連接 能夠 幫助梯度在訓練過程中更順暢地傳播,從而 改進深度模型的學習能力(詳見 4.4節)。
代碼示例:實例化變換器塊
使用 GPT_CONFIG_124M
配置,我們創建一個 變換器塊 并向其輸入一些樣本數據:
torch.manual_seed(123)
x = torch.rand(2, 4, 768) # A
block = TransformerBlock(GPT_CONFIG_124M)
output = block(x)print("Input shape:", x.shape)
print("Output shape:", output.shape)
輸出:
Input shape: torch.Size([2, 4, 768])
Output shape: torch.Size([2, 4, 768])
保持輸入輸出形狀不變
從代碼輸出可以看出:
- 變換器塊的輸入和輸出維度相同,即:
Input?shape = Output?shape = ( b a t c h _ s i z e , s e q u e n c e _ l e n g t h , e m b e d d i n g _ s i z e ) \text{Input shape} = \text{Output shape} = (batch\_size, sequence\_length, embedding\_size) Input?shape=Output?shape=(batch_size,sequence_length,embedding_size) - 這表明 變換器架構在處理序列數據時不會改變其形狀,而是在相同的形狀下進行信息轉換。
為什么保持形狀不變很重要?
- 這是 變換器架構的核心設計理念,使其能夠適用于 各種序列到序列(Sequence-to-Sequence)任務。
- 每個輸出向量 直接 對應一個輸入向量,保持 一對一關系。
- 盡管物理維度(sequence length & feature size) 不變,但 輸出向量的內容已經重新編碼,并整合了整個輸入序列的上下文信息(詳見 第3章)。
完成 G P T GPT GPT 架構的所有構建模塊
在本節中,我們成功實現了 變換器塊,至此,我們已經具備了 構建 G P T GPT GPT架構所需的全部組件,如 圖4.14 所示。
圖4.14 展示了 本章已實現的所有核心概念,這些構建模塊將在下一節 完整地組合為 G P T GPT GPT模型。
如 圖4.14 所示,變換器塊(Transformer Block) 結合了多個關鍵組件,包括:
- 層歸一化(Layer Normalization)
- 前饋網絡(Feed Forward Network)(包含 GELU激活函數)
- 捷徑連接(Shortcut Connections)
這些組件我們已經在本章的前面部分進行了介紹和實現。
在下一章中,我們將看到 變換器塊 將作為 G P T GPT GPT架構的主要組成部分,并用于構建完整的 G P T GPT GPT模型。
4.6 編寫GPT模型
本章開始時,我們對 GPT架構 進行了整體概述,并實現了一個 占位版本(DummyGPTModel)。在 DummyGPTModel 代碼實現中,我們展示了 GPT模型的輸入和輸出,但其內部構建模塊仍然是一個 黑盒,使用了 DummyTransformerBlock
和 DummyLayerNorm
作為占位符。
在本節中,我們將 用真實的TransformerBlock
和LayerNorm
類 替換這些 占位符,從而 組裝出完整可運行的 G P T GPT GPT-2模型(1.24億參數版本)。
在 第5章,我們將 預訓練(Pretrain) G P T GPT GPT-2模型,而在 第6章,我們將 加載來自 O p e n A I OpenAI OpenAI的預訓練權重。
GPT-2 模型整體結構
在正式編寫 GPT-2模型代碼 之前,讓我們先看 圖4.15,它展示了 GPT模型的整體架構,并匯總了本章介紹的所有核心概念。
圖4.15:GPT模型架構概覽
- 數據流(data flow) 從底部開始:
- 標記化文本(Tokenized Text) 轉換為 標記嵌入(Token Embeddings)。
- 標記嵌入 再與 位置嵌入(Positional Embeddings) 結合,以編碼序列信息。
- 這些 組合信息 形成 張量(Tensor),并輸入到 變換器塊(Transformer Blocks) 進行處理。
- 中間部分(核心計算)
- 變換器塊(Transformer Blocks) 由 多個層堆疊組成,每個變換器塊包括:
- 多頭注意力(Multi-Head Attention)
- 前饋神經網絡(Feed Forward Neural Network, FFN)
- 層歸一化(Layer Normalization)
- 隨機失活(Dropout)
- 變換器塊被堆疊并重復12次(即 G P T GPT GPT-2的12層架構)。
在接下來的代碼部分,我們將正式實現 完整的 G P T GPT GPT-2架構。
如 圖4.15 所示,在 G P T GPT GPT模型架構 中,我們在 4.5節 編寫的 變換器塊(Transformer Block) 會被多次重復。
- 對于 G P T GPT GPT-2的1.24億參數版本,該 變換器塊重復12次,這一數值由
GPT_CONFIG_124M["n_layers"] = 12
指定。 - 對于 G P T GPT GPT-2的最大版本(15.42億參數),該 變換器塊重復36次。
最終的輸出層
如 圖4.15 所示:
- 最后一個變換器塊的輸出 經過 最終的層歸一化(Layer Normalization)。
- 隨后進入最終的線性輸出層(Linear Output Layer),該層將 變換器的輸出投影到高維空間:
- 在 G P T GPT GPT-2 中,這個維度是 50,257,對應 模型的詞匯表大小(vocabulary size)。
- 最終輸出預測序列中的下一個標記(token)。
現在,我們來 編寫代碼,實現 圖4.15所示的 G P T GPT GPT模型架構:
代碼清單 4.7 G P T GPT GPT模型架構的實現
class GPTModel(nn.Module):def __init__(self, cfg):super().__init__()'''創建一個詞嵌入層,將輸入的 token 索引映射到一個固定維度的向量空間。'''self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])'''創建一個位置嵌入層,將每個位置映射到一個固定維度的向量。'''self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])'''構造一個 Dropout 層,用于在訓練時隨機丟棄部分神經元,防止過擬合。'''self.drop_emb = nn.Dropout(cfg["drop_rate"])# Use a placeholder for TransformerBlock'''* 操作符的作用是將列表中的元素拆開,為“解包”操作符。將這個列表中的所有元素“解包”,使它們變成 nn.Sequential 的單獨參數。等效于寫成(假設cfg["n_layers"] = 2):self.trf_blocks = nn.Sequential(TransformerBlock(cfg), TransformerBlock(cfg))創建一個由多個 TransformerBlock 組成的序列,然后用 nn.Sequential 將它們串聯在一起。'''self.trf_blocks = nn.Sequential(*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])# Use a placeholder for LayerNorm'''創建一個層歸一化層,通常用于穩定訓練,但這里使用的是一個占位實現(LayerNorm),實際不做任何操作。'''self.final_norm = LayerNorm(cfg["emb_dim"])'''創建一個線性層,將最后得到的嵌入映射到詞匯表大小的維度,輸出 logits。'''self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)def forward(self, in_idx):"""定義模型的前向傳播接口,輸入為 token 索引組成的張量 in_idx。"""batch_size, seq_len = in_idx.shape'''將 token 索引轉換為對應的嵌入向量。輸出形狀為 (batch_size, seq_len, emb_dim),例如 (2, 6, 8)。'''tok_embeds = self.tok_emb(in_idx)'''生成位置序列 [0, 1, ..., seq_len-1] 并查找對應的位置信息向量。輸出形狀為 (seq_len, emb_dim),例如 (6, 8)。'''pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))'''將 token 嵌入與位置嵌入相加,融合語義信息和位置信息。pos_embeds 的形狀 (6, 8) 自動擴展到 (2, 6, 8) 與 tok_embeds 相加。1. tok_embeds[0](6,8)+pos_embeds(6, 8) ->[0](6,8)2. tok_embeds[1](6,8)+pos_embeds(6, 8) ->[1](6,8)變成(2, 6, 8)'''x = tok_embeds + pos_embedsx = self.drop_emb(x)'''將 x 依次傳入所有 TransformerBlock 層處理。'''x = self.trf_blocks(x)'''對 x 進行層歸一化操作,穩定訓練。'''x = self.final_norm(x)'''通過線性變換,將每個 token 的 8 維向量映射到詞匯表大小維度(例如 100),得到每個詞的 logits。'''logits = self.out_head(x)return logits
由于我們在 4.5節 實現了 TransformerBlock
類,因此 GPTModel
類的代碼相對緊湊且易于理解。
在 GPTModel
類的 __init__
構造函數 中:
- 標記嵌入層(Token Embedding Layer) 和 位置嵌入層(Positional Embedding Layer) 由傳入的 配置字典(
cfg
) 進行初始化。- 這些 嵌入層 負責 將輸入的標記索引轉換為密集向量,并 添加位置信息(詳見 第2章)。
- 隨后,創建一系列
TransformerBlock
變換器塊,其數量等于cfg["n_layers"]
中指定的層數。 - 在變換器塊之后,應用層歸一化(
LayerNorm
),標準化變換器塊的輸出,以 穩定學習過程。 - 最后,定義一個無偏置(
bias=False
)的線性輸出層,它 將變換器的輸出投影到詞匯空間,用于 計算詞匯表中每個標記的對數概率(logits)。
前向傳播(forward
方法)
- 接收一個批量輸入(
batch
),其中包含標記索引。 - 計算其標記嵌入,并應用位置嵌入。
- 通過變換器塊(
Transformer Blocks
) 處理序列。 - 對最終輸出進行層歸一化。
- 計算對數概率(
logits
),用于預測下一個標記。
在下一節中,我們將 把這些對數概率轉換為實際的標記(tokens) 和文本輸出(text output)。
初始化 1.24 億參數的 GPT 模型
現在,我們使用 GPT_CONFIG_124M
配置字典初始化 G P T GPT GPT-2模型,并輸入我們在本章開始時創建的批量文本數據:
torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
out = model(batch)print("Input batch:\n", batch)
print("\nOutput shape:", out.shape)
print(out)
代碼輸出:
Input batch:
tensor([[ 6109, 3626, 6100, 345], # 文本1的標記ID[ 6109, 1110, 6622, 257]]) # 文本2的標記IDOutput shape: torch.Size([2, 4, 50257])
tensor([[[ 0.1381, 0.0077, -0.1963, ..., -0.0222, -0.1060, 0.1717],[ 0.3865, -0.8408, -0.6564, ..., -0.5163, 0.2369, -0.3357],[ 0.6989, -0.1829, -0.1631, ..., 0.1472, -0.6504, -0.0056],[-0.4290, 0.1669, -0.1258, ..., 1.1579, 0.5303, -0.5549]],[[ 0.1094, -0.2894, -0.1467, ..., -0.0557, 0.2911, -0.2824],[ 0.0882, -0.3552, -0.3527, ..., 1.2930, 0.0053, 0.1898],[ 0.6091, 0.4702, -0.4094, ..., 0.7688, 0.3787, -0.1974],[-0.0612, -0.0737, 0.4751, ..., 1.2463, -0.3834, 0.0609]]],grad_fn=<UnsafeViewBackward0>)
解析輸出
- 輸入 (
batch
) 形狀為[2, 4]
,表示 兩個文本樣本,每個文本包含 4 個標記(token)。 - 輸出 (
out
) 形狀為[2, 4, 50257]
:2
:表示 批量大小(batch size),即兩個輸入樣本。4
:表示 序列長度(sequence length),即每個輸入文本包含 4 個標記。50257
:表示 模型詞匯表大小(vocabulary size),即 每個標記都有 50257 維的對數概率(logits),用于預測 下一個可能的標記。
- 最終輸出的
tensor
:- 每個標記對應一個 大小為
50257
的對數概率向量(logits vector),其中數值最高的索引即為下一個預測的標記。
- 每個標記對應一個 大小為
正如我們所見,輸出張量的形狀為 [2, 4, 50257]
,因為我們輸入了 2 條文本數據,每條包含 4 個標記(tokens)。
- 最后一維(
50257
) 對應于標記器的詞匯表大小,表示 每個標記都有一個大小為50257
的對數概率(logits) 向量。 - 在下一節,我們將 把這些
50257
維的輸出向量轉換回標記(tokens)。
計算 GPT-2 的參數總數
在繼續轉換模型輸出為文本之前,我們先花些時間分析 GPT-2 的模型大小。使用 numel()
方法(全稱 “number of elements”),我們可以計算 模型所有參數的總數:
total_params = sum(p.numel() for p in model.parameters())
print(f"Total number of parameters: {total_params:,}")
輸出結果:
Total number of parameters: 163,009,536
為什么 GPT-2 不是 1.24 億參數,而是 1.63 億參數?
- 我們之前提到 GPT-2 的參數量為 1.24 億,但 實際參數數為 1.63 億,為什么會有差異?
- 這是由于 “權重共享(Weight Tying)” 這一概念,該技術被用于 原始 G P T GPT GPT-2架構。
- 在 GPT-2 中,輸出層復用了標記嵌入層(Token Embedding Layer) 的權重,這意味著實際存儲的參數量要少于模型結構計算出的總參數數。
驗證 Token 嵌入層 和 輸出層 形狀
為了理解 權重共享,我們打印 模型的標記嵌入層(tok_emb
) 和 輸出層(out_head
) 的形狀:
print("Token embedding layer shape:", model.tok_emb.weight.shape)
print("Output layer shape:", model.out_head.weight.shape)
輸出結果:
Token embedding layer shape: torch.Size([50257, 768])
Output layer shape: torch.Size([50257, 768])
- 兩者的形狀完全相同,都為
[50257, 768]
,其中:50257
:標記器的 詞匯表大小。768
:標記嵌入的 維度。
這說明 out_head
(輸出層) 的參數實際上與 tok_emb
(標記嵌入層) 的參數是等價的,可以 共享同一組權重。
計算去除輸出層后,模型的實際可訓練參數量
根據權重共享的原則,我們從總參數數中移除輸出層的參數:
total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())
print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")
輸出結果:
Number of trainable parameters considering weight tying: 124,412,160
現在,模型的總參數數降至 1.24 億
,與原始 GPT-2 模型的大小匹配。
權重共享的意義
- “權重共享(weight tying)” 減少了模型的整體內存占用(memory footprint) 和計算復雜度(computational complexity)。
- 但在實踐中,分開使用獨立的 Token 嵌入層 和 輸出層,通常會得到更好的訓練效果和模型性能。
- 現代 L L M LLM LLM(大型語言模型) 通常不會使用權重共享,而是 使用獨立的標記嵌入層和輸出層,正如我們在
GPTModel
的實現 中所做的那樣。
后續章節
我們將在 第6章 重新討論 “權重共享(weight tying)”,并在加載 O p e n A I OpenAI OpenAI 預訓練的 G P T GPT GPT-2權重 時實現該技術。
練習 4.1:計算前饋網絡和多頭注意力模塊的參數數量
計算并比較:
- 前饋網絡(Feed Forward Module) 中的參數數量。
- 多頭注意力模塊(Multi-Head Attention Module) 中的參數數量。
答案
block = TransformerBlock(GPT_CONFIG_124M)
print(block)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:,}")
TransformerBlock((att): MultiHeadAttention((W_query): Linear(in_features=768, out_features=768, bias=False)(W_key): Linear(in_features=768, out_features=768, bias=False)(W_value): Linear(in_features=768, out_features=768, bias=False)(out_proj): Linear(in_features=768, out_features=768, bias=True)(dropout): Dropout(p=0.1, inplace=False))(ff): FeedForward((layers): Sequential((0): Linear(in_features=768, out_features=3072, bias=True)(1): GELU()(2): Linear(in_features=3072, out_features=768, bias=True)))(norm1): LayerNorm()(norm2): LayerNorm()(drop_shortcut): Dropout(p=0.1, inplace=False)
)
Total number of parameters in feed forward module: 4,722,432
Total number of parameters in attention module: 2,360,064
計算 G P T M o d e l GPTModel GPTModel 的存儲需求
接下來,我們計算 G P T M o d e l GPTModel GPTModel(1.63 億參數)所需的內存:
total_size_bytes = total_params * 4 # A: 每個參數占用 4 字節(32-bit float)
total_size_mb = total_size_bytes / (1024 * 1024) # B: 轉換為 MB
print(f"Total size of the model: {total_size_mb:.2f} MB")
輸出結果:
Total size of the model: 621.83 MB
結論
- 假設每個參數都是 32-bit 浮點數(即 4 字節),則 1.63 億個參數的 G P T M o d e l GPTModel GPTModel 需要 621.83 MB 的存儲空間。
- 這說明,即使是 較小的 L L M LLM LLM,也需要相對較大的存儲空間。
在本節中,我們:
- 實現了 G P T M o d e l GPTModel GPTModel 架構,并觀察到其輸出是形狀為
[batch_size, num_tokens, vocab_size]
的數值張量。 - 計算了模型的存儲需求,并理解了即使是 小規模的 L L M LLM LLM,也需要較大的存儲容量。
- 在 下一節,我們將 編寫代碼,將這些輸出張量轉換為文本。
練習 4.2:初始化更大規模的 GPT 模型
本章中,我們初始化了 1.24 億參數的 G P T GPT GPT模型(GPT-2 Small)。
現在,不修改代碼結構,僅修改配置文件,使用 GPTModel
實現以下更大規模的 GPT-2 版本:
GPT-2 版本 | 嵌入維度 | 變換器塊數量 | 多頭注意力頭數 |
---|---|---|---|
GPT-2 Medium | 1024 | 24 | 16 |
GPT-2 Large | 1280 | 36 | 20 |
GPT-2 XL | 1600 | 48 | 25 |
額外任務:計算 每個 G P T GPT GPT模型的總參數量。
答案
GPT_CONFIG_124M = {"vocab_size": 50257,"context_length": 1024,"emb_dim": 768,"n_heads": 12,"n_layers": 12,"drop_rate": 0.1,"qkv_bias": False
}def get_config(base_config, model_name="gpt2-small"):GPT_CONFIG = base_config.copy()if model_name == "gpt2-small":GPT_CONFIG["emb_dim"] = 768GPT_CONFIG["n_layers"] = 12GPT_CONFIG["n_heads"] = 12elif model_name == "gpt2-medium":GPT_CONFIG["emb_dim"] = 1024GPT_CONFIG["n_layers"] = 24GPT_CONFIG["n_heads"] = 16elif model_name == "gpt2-large":GPT_CONFIG["emb_dim"] = 1280GPT_CONFIG["n_layers"] = 36GPT_CONFIG["n_heads"] = 20elif model_name == "gpt2-xl":GPT_CONFIG["emb_dim"] = 1600GPT_CONFIG["n_layers"] = 48GPT_CONFIG["n_heads"] = 25else:raise ValueError(f"Incorrect model name {model_name}")return GPT_CONFIGdef calculate_size(model): # based on chapter codetotal_params = sum(p.numel() for p in model.parameters())print(f"Total number of parameters: {total_params:,}")total_params_gpt2 = total_params - sum(p.numel() for p in model.out_head.parameters())print(f"Number of trainable parameters considering weight tying: {total_params_gpt2:,}")# Calculate the total size in bytes (assuming float32, 4 bytes per parameter)total_size_bytes = total_params * 4# Convert to megabytestotal_size_mb = total_size_bytes / (1024 * 1024)print(f"Total size of the model: {total_size_mb:.2f} MB")from gpt import GPTModelfor model_abbrev in ("small", "medium", "large", "xl"):model_name = f"gpt2-{model_abbrev}"CONFIG = get_config(GPT_CONFIG_124M, model_name=model_name)model = GPTModel(CONFIG)print(f"\n\n{model_name}:")calculate_size(model)
下載 gpt.py 到相同目錄中。
4.7 生成文本
在本章的最后一節,我們將 實現代碼,將 G P T GPT GPT模型的張量輸出轉換回文本。在開始之前,讓我們簡要回顧 生成式模型(Generative Model)(如 L L M LLM LLM)是如何 逐步生成文本的,如 圖4.16 所示。
圖4.16 展示了 L L M LLM LLM 逐步生成文本的過程:
- 模型一次生成一個標記(token)。
- 以 初始輸入上下文(initial input context) “Hello, I am” 為例:
- 第一步:模型預測 下一個標記,添加 “a”。
- 第二步:模型再預測 下一個標記,添加 “model”。
- 第三步:繼續預測 下一個標記,添加 “ready”。
- 最終,模型逐步構建完整的句子。
圖4.16 直觀地展示了 G P T GPT GPT模型 在給定輸入上下文(如 “Hello, I am”)后,逐步生成文本的過程。
- 每次迭代,輸入上下文都會增長,使得模型能夠 生成連貫且符合上下文的文本。
- 到 第6次迭代,模型已經 構造出完整的句子:
“Hello, I am a model ready to help.”
在 上一節,我們看到 GPTModel
的當前實現 輸出形狀為 [batch_size, num_tokens, vocab_size]
的張量。現在的問題是: G P T GPT GPT模型如何從這些輸出張量生成如圖4.16所示的文本?
文本生成的關鍵步驟: G P T GPT GPT模型從輸出張量到最終生成文本的過程涉及多個步驟,如 圖4.17 所示:
- 解碼輸出張量(將
logits
轉換為概率分布)。 - 基于概率分布選擇標記(可采用貪心搜索、采樣、Top-k采樣等策略)。
- 將選定的標記轉換為可讀文本。
圖4.17 詳細說明了 G P T GPT GPT模型文本生成的內部機制,展示了 單個標記生成過程(iteration):
- 首先,將 輸入文本編碼為標記ID。
- 然后,將標記ID 輸入到 G P T GPT GPT模型。
- 最后,模型的輸出被 轉換回文本,并 追加到原始輸入文本,形成新的上下文。
圖4.17 詳細展示了 GPT模型的“下一個標記生成”過程,即 模型如何在每一步預測下一個標記。
單步標記生成過程: 在 每個生成步驟 中:
- G P T GPT GPT模型輸出一個矩陣,其中包含表示 潛在下一個標記的向量。
- 提取下一個標記對應的向量,并使用 Softmax函數 將其轉換為 概率分布。
- 在概率分布中找到最大值對應的索引,該索引即 預測的標記ID。
- 將該標記ID解碼回文本,得到 序列中的下一個標記。
- 將該標記追加到之前的輸入序列,形成 新的輸入上下文,用于下一次迭代。
通過這種 逐步擴展輸入序列 的方式,模型可以 順序生成文本,從初始上下文構建 連貫的短語和句子。
循環迭代生成完整文本: 在實際應用中,我們會 重復此過程(如 圖4.16 所示),直到:
- 達到用戶設定的標記數(token count)。
- 遇到特殊終止標記(如
<EOS>
)。
我們可以使用以下代碼 實現GPT模型的標記生成過程:
代碼清單 4.8 GPT模型生成文本的函數
def generate_text_simple(model, idx, max_new_tokens, context_size): # A"""- model:GPT 語言模型。 - idx:當前輸入標記序列(張量格式)。 - max_new_tokens:要生成的新標記數。 - context_size:模型最大可接受的上下文窗口。 """for _ in range(max_new_tokens):''' 取輸入序列的最后 context_size 個標記,確保輸入序列不會超出模型最大上下文限制。 '''idx_cond = idx[:, -context_size:] # Bwith torch.no_grad():'''model(idx_cond) 計算所有時間步的 logits,其中:- 形狀為 [batch_size, num_tokens, vocab_size]。 '''logits = model(idx_cond)'''取最后一個token(-1) 的 logits ,即預測下一個標記的概率分布。'''logits = logits[:, -1, :] # C'''將 logits 轉換為概率分布(probas)。 '''probas = torch.softmax(logits, dim=-1) # D'''選取最高概率的標記索引,即下一個預測標記ID。 - 該方法被稱為 “貪心解碼(Greedy Decoding)”。'''idx_next = torch.argmax(probas, dim=-1, keepdim=True) # E'''將新標記追加到輸入序列,用于下一輪預測。'''idx = torch.cat((idx, idx_next), dim=1) # Freturn idx
上面的代碼片段展示了 使用 P y T o r c h PyTorch PyTorch實現的語言模型生成循環。
該代碼 迭代生成指定數量的新標記(tokens),并且:
- 裁剪當前上下文 以適配模型的 最大上下文窗口大小。
- 計算預測(logits) 并選擇 下一個標記。
詳細步驟
-
(A)
generate_text_simple
函數:- 輸入:
model
: G P T GPT GPT 語言模型。idx
:當前 輸入標記序列(張量格式)。max_new_tokens
:要生成的新標記數。context_size
:模型最大可接受的上下文窗口。
- 輸出:擴展后的新標記序列。
- 輸入:
-
(B)
idx[:, -context_size:]
:- 取 輸入序列的最后
context_size
個標記,確保 輸入序列不會超出模型最大上下文限制。
- 取 輸入序列的最后
-
(C)
logits[:, -1, :]
:model(idx_cond)
計算 所有時間步的logits
,其中:- 形狀為
[batch_size, num_tokens, vocab_size]
。
- 形狀為
- 取 最后一個token(
-1
) 的logits
,即 預測下一個標記的概率分布。
-
(D)
torch.softmax(logits, dim=-1)
:- 將
logits
轉換為概率分布(probas)。
- 將
-
(E)
torch.argmax(probas, dim=-1, keepdim=True)
:- 選取 最高概率的標記索引,即 下一個預測標記ID。
- 該方法被稱為 貪心解碼(Greedy Decoding)。
-
(F)
torch.cat((idx, idx_next), dim=1)
:- 將新標記追加到輸入序列,用于下一輪預測。
關于 Softmax 計算的額外說明
在 generate_text_simple
函數中,我們使用 torch.softmax()
將 logits
轉換為概率分布,并用 torch.argmax()
選取最高概率的標記。
但實際上:
- Softmax 是單調函數(monotonic function),即它不會改變輸入的相對大小順序。
- 因此,直接對
logits
應用torch.argmax()
也能得到相同的結果。
盡管如此,我們仍然 顯式地展示 Softmax 計算步驟,以便直觀理解 logits → 概率 → 選取最高概率標記
這一完整流程。此外,在 下一章( G P T GPT GPT訓練代碼部分),我們會引入 更復雜的采樣策略,比如:
- 隨機采樣(Stochastic Sampling)
- 溫度縮放(Temperature Scaling)
- Top-k采樣(Top-k Sampling)
- 核采樣(Nucleus Sampling, Top-p Sampling)
這些策略可以 避免模型始終選擇最高概率標記,從而 提高生成文本的多樣性 和 創造性。
這個 逐步生成標記ID 并將其追加到上下文的過程,使用 generate_text_simple
函數實現,并在 圖4.18 中進一步說明。
(每次迭代中的 標記ID生成過程 詳見 圖4.17。)
圖4.18 展示了 六次迭代的標記預測循環,其中:
- 模型接收初始標記ID序列作為輸入。
- 預測下一個標記 并 將其追加到輸入序列,形成新的輸入序列,用于下一次迭代。
- (為便于理解)標記ID 也被轉換為對應的文本。
如 圖4.18 所示,我們以迭代方式生成標記ID。例如,在 第1次迭代 時:
- 模型接收 代表
"Hello, I am"
的 輸入標記。 - 預測下一個標記(ID為 257,對應
"a"
)。 - 將預測出的標記追加到輸入序列,形成新的輸入。
該過程不斷重復,直到 第6次迭代,模型最終生成完整的句子:
“Hello, I am a model ready to help.”
我們現在嘗試 將 "Hello, I am"
作為模型輸入,并使用 generate_text_simple
進行文本生成,如 圖4.18 所示。
首先,將輸入文本編碼為標記ID:
start_context = "Hello, I am"
encoded = tokenizer.encode(start_context)
print("encoded:", encoded)encoded_tensor = torch.tensor(encoded).unsqueeze(0) # A
print("encoded_tensor.shape:", encoded_tensor.shape)
編碼后的標記ID:
encoded: [15496, 11, 314, 716]
encoded_tensor.shape: torch.Size([1, 4])
接下來,我們將模型置于 eval()
模式(禁用 dropout
等隨機成分,僅用于訓練時),并使用 generate_text_simple
進行文本生成:
model.eval() # A
out = generate_text_simple(model=model,idx=encoded_tensor,max_new_tokens=6,context_size=GPT_CONFIG_124M["context_length"]
)
print("Output:", out)
print("Output length:", len(out[0]))
生成的標記ID如下:
Output: tensor([[15496, 11, 314, 716, 27018, 24086, 47843, 30961, 42348, 7267]])
Output length: 10
將標記ID解碼回文本
decoded_text = tokenizer.decode(out.squeeze(0).tolist())
print(decoded_text)
模型輸出的文本如下:
Hello, I am Featureiman Byeswickattribute argue
為什么生成的文本是無意義的?
從上面的輸出可以看出,模型生成的文本是雜亂無章的,完全不像 圖4.18 中的連貫文本。
原因:模型尚未經過訓練!
- 目前,我們只是實現了 G P T GPT GPT架構,并實例化了一個 G P T GPT GPT模型,但其參數仍然是隨機初始化的。
- 模型訓練是一個龐大的主題,我們將在下一章深入探討如何訓練 G P T GPT GPT模型。
練習 4.3:使用不同的 dropout 參數
在本章的開頭,我們在 GPT_CONFIG_124M
字典中定義了 全局 drop_rate
參數,用于設置 G P T M o d e l GPTModel GPTModel 結構中的所有 dropout
層。
修改代碼,使不同的 dropout
層使用獨立的 dropout
值:
(提示:模型架構中有三個不同的 dropout
層):
- 嵌入層(Embedding Layer) 的 dropout。
- 捷徑連接(Shortcut Layer) 的 dropout。
- 多頭注意力模塊(Multi-Head Attention Module) 的 dropout。
任務:
- 修改
GPT_CONFIG_124M
,為不同dropout
位置指定不同的值。 - 更新
GPTModel
代碼,使其分別使用這些不同的dropout
設置。
答案
GPT_CONFIG_124M = {"vocab_size": 50257,"context_length": 1024,"emb_dim": 768,"n_heads": 12,"n_layers": 12,"drop_rate_emb": 0.1, # NEW: dropout for embedding layers"drop_rate_attn": 0.1, # NEW: dropout for multi-head attention "drop_rate_shortcut": 0.1, # NEW: dropout for shortcut connections "qkv_bias": False
}
import torch.nn as nn
from gpt import MultiHeadAttention, LayerNorm, FeedForwardclass 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"], # NEW: dropout for multi-head attentionqkv_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):# Shortcut connection for attention blockshortcut = xx = self.norm1(x)x = self.att(x) # Shape [batch_size, num_tokens, emb_size]x = self.drop_shortcut(x)x = x + shortcut # Add the original input back# Shortcut connection for feed-forward blockshortcut = xx = self.norm2(x)x = self.ff(x)x = self.drop_shortcut(x)x = x + shortcut # Add the original input backreturn xclass 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"]) # NEW: dropout for embedding layersself.trf_blocks = nn.Sequential(*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])self.final_norm = LayerNorm(cfg["emb_dim"])self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)def forward(self, in_idx):batch_size, seq_len = in_idx.shapetok_embeds = self.tok_emb(in_idx)pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))x = tok_embeds + pos_embeds # Shape [batch_size, num_tokens, emb_size]x = self.drop_emb(x)x = self.trf_blocks(x)x = self.final_norm(x)logits = self.out_head(x)return logits
import torchtorch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
4.8 總結
- 層歸一化(Layer Normalization) 通過確保每一層的輸出具有一致的 均值(mean) 和 方差(variance) 來 穩定訓練過程。
- 捷徑連接(Shortcut Connections) 通過 跳過一個或多個層,將某一層的輸出直接傳遞到更深的層,有助于緩解深度神經網絡(如 L L M LLM LLM)的梯度消失問題(vanishing gradient problem)。
- 變換器塊(Transformer Blocks) 是 G P T GPT GPT模型的核心結構組件,結合了掩碼多頭注意力模塊(masked multi-head attention) 和 使用GELU激活函數的全連接前饋網絡。
- G P T GPT GPT模型是由多個重復的變換器塊組成的 L L M LLM LLM,參數量從百萬到數十億。
- G P T GPT GPT模型有多個不同的規模,例如 124、345、762 和 1542 百萬參數,這些都可以使用相同的
GPTModel
Python 類進行實現。 - G P T GPT GPT類 L L M LLM LLM的文本生成能力 依賴于 逐步預測單個標記(token) 并解碼輸出張量為可讀文本,基于 給定的輸入上下文 進行生成。
- 未經訓練的 G P T GPT GPT模型生成的文本是無意義的,這強調了 模型訓練對生成連貫文本的重要性,這一主題將在后續章節中深入探討。