用 pytorch 從零開始創建大語言模型(四):從零開始實現一個用于生成文本的GPT模型

從零開始創建大語言模型(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)。

上述代碼已經是一個可運行的模型,我們將在本節后續部分準備輸入數據并進行測試。然而,目前需要注意的是,代碼中我們使用了占位符(DummyLayerNormDummyTransformerBlock)來代替變換器塊和層歸一化,這些組件將在后續部分實現。

接下來,我們將準備輸入數據并初始化一個新的 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=1dim=-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),用于防止歸一化過程中出現除零錯誤。

scaleshift是兩個可訓練參數,它們的維度與輸入相同。在 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加權線性單元)

GELUSwiGLU 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繪制的 GELUReLU 函數的輸出。橫軸( 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) 過程中:
    1. 逐層計算輸入的輸出
    2. 如果 self.use_shortcut=True 且當前層的輸入與輸出形狀相同,則 應用捷徑連接(x = x + layer_output)
    3. 否則,直接使用當前層的輸出(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模型的輸入和輸出,但其內部構建模塊仍然是一個 黑盒,使用了 DummyTransformerBlockDummyLayerNorm 作為占位符。

在本節中,我們將 用真實的TransformerBlockLayerNorm 替換這些 占位符,從而 組裝出完整可運行的 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.15GPT模型架構概覽

  • 數據流(data flow) 從底部開始
  1. 標記化文本(Tokenized Text) 轉換為 標記嵌入(Token Embeddings)
  2. 標記嵌入 再與 位置嵌入(Positional Embeddings) 結合,以編碼序列信息。
  3. 這些 組合信息 形成 張量(Tensor),并輸入到 變換器塊(Transformer Blocks) 進行處理。
  • 中間部分(核心計算)
  1. 變換器塊(Transformer Blocks)多個層堆疊組成,每個變換器塊包括:
    • 多頭注意力(Multi-Head Attention)
    • 前饋神經網絡(Feed Forward Neural Network, FFN)
    • 層歸一化(Layer Normalization)
    • 隨機失活(Dropout)
  2. 變換器塊被堆疊并重復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 所示:

  1. 最后一個變換器塊的輸出 經過 最終的層歸一化(Layer Normalization)
  2. 隨后進入最終的線性輸出層(Linear Output Layer),該層將 變換器的輸出投影到高維空間
    • G P T GPT GPT-2 中,這個維度是 50,257,對應 模型的詞匯表大小(vocabulary size)
  3. 最終輸出預測序列中的下一個標記(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>)

解析輸出

  1. 輸入 (batch) 形狀為 [2, 4],表示 兩個文本樣本,每個文本包含 4 個標記(token)
  2. 輸出 (out) 形狀為 [2, 4, 50257]
    • 2:表示 批量大小(batch size),即兩個輸入樣本。
    • 4:表示 序列長度(sequence length),即每個輸入文本包含 4 個標記。
    • 50257:表示 模型詞匯表大小(vocabulary size),即 每個標記都有 50257 維的對數概率(logits),用于預測 下一個可能的標記
  3. 最終輸出的 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 Medium10242416
GPT-2 Large12803620
GPT-2 XL16004825

額外任務:計算 每個 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” 為例:
    1. 第一步:模型預測 下一個標記,添加 “a”
    2. 第二步:模型再預測 下一個標記,添加 “model”
    3. 第三步:繼續預測 下一個標記,添加 “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 所示:

  1. 解碼輸出張量(將 logits 轉換為概率分布)。
  2. 基于概率分布選擇標記(可采用貪心搜索、采樣、Top-k采樣等策略)。
  3. 將選定的標記轉換為可讀文本

在這里插入圖片描述圖4.17 詳細說明了 G P T GPT GPT模型文本生成的內部機制,展示了 單個標記生成過程(iteration)

  • 首先,將 輸入文本編碼為標記ID
  • 然后,將標記ID 輸入到 G P T GPT GPT模型
  • 最后,模型的輸出被 轉換回文本,并 追加到原始輸入文本,形成新的上下文。

圖4.17 詳細展示了 GPT模型的“下一個標記生成”過程,即 模型如何在每一步預測下一個標記

單步標記生成過程:每個生成步驟 中:

  1. G P T GPT GPT模型輸出一個矩陣,其中包含表示 潛在下一個標記的向量
  2. 提取下一個標記對應的向量,并使用 Softmax函數 將其轉換為 概率分布
  3. 在概率分布中找到最大值對應的索引,該索引即 預測的標記ID
  4. 將該標記ID解碼回文本,得到 序列中的下一個標記
  5. 將該標記追加到之前的輸入序列,形成 新的輸入上下文,用于下一次迭代。

通過這種 逐步擴展輸入序列 的方式,模型可以 順序生成文本,從初始上下文構建 連貫的短語和句子

循環迭代生成完整文本: 在實際應用中,我們會 重復此過程(如 圖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),并且:

  1. 裁剪當前上下文 以適配模型的 最大上下文窗口大小
  2. 計算預測(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):

  1. 嵌入層(Embedding Layer) 的 dropout
  2. 捷徑連接(Shortcut Layer) 的 dropout
  3. 多頭注意力模塊(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模型生成的文本是無意義的,這強調了 模型訓練對生成連貫文本的重要性,這一主題將在后續章節中深入探討。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/73916.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/73916.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/73916.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vmware tools灰化

Windows7 32位的某些版本&#xff0c;已經不被vmware支持。下面是解決方法&#xff1a; 安裝kb4474419補丁包&#xff1a;https://www.catalog.update.microsoft.com/Search.aspx?qKB4474419網絡共享。必須要虛擬機和主機可通信。此方法不錯&#xff0c;但是操作起來太麻煩。…

ubuntu高并發內核參數調優 - (壓測客戶端調優)

業務上要求集群提供10w并發&#xff0c;10w并發聽上去不是很難&#xff0c;但10w并發持續1小時呢 在業務上線之前還需要我們自己對業務進行壓測&#xff0c;俗稱benchmark。 壓測的服務器也是需要進行性能調優的&#xff0c;以下列出調優前后的參數對比&#xff0c;更直觀的分析…

html5制作2048游戲開發心得與技術分享

2048游戲開發心得與技術分享 這里寫目錄標題 2048游戲開發心得與技術分享項目概述技術架構1. 核心技術棧2. 項目結構 核心功能實現1. 數據結構設計2. 移動邏輯實現3. 觸摸支持 性能優化1. DOM操作優化2. 事件處理優化 開發心得1. 代碼組織2. 調試技巧3. 用戶體驗優化 項目亮點技…

dify+deepseek聯網搜索:免費開源搜索引擎Searxng使用(讓你的大模型也擁有聯網的功能)

docker安裝SearXng 項目地址:https://github.com/searxng/searxng-docker 第一步 git clone下來 git clone https://github.com/searxng/searxng-docker.git第二步 進入 searxng-docker目錄中修改docker-compose.yaml(直接復制粘貼) cd searxng-dockerdocker-compose.yaml …

docker的anythingllm和open-webui壓縮包分享(國內鏡像拉取,百度云壓縮包分享)

文章目錄 前言第一部分&#xff1a;鏡像獲取&#x1f680; 方式一&#xff1a;切換國內下載鏡像?1. 下載anythingllm? 2. 下載open-webui &#x1f680;方式二&#xff1a;下載我分享的百度云? anythingllm壓縮包百度云鏈接? open-webui壓縮包 第二部分&#xff1a;下載之后…

DeepSeek-R1深度解讀

deepseek提出了一種通過強化學習&#xff08;RL&#xff09;激勵大語言模型&#xff08;LLMs&#xff09;推理能力的方法&#xff0c;個人認為最讓人興奮的點是&#xff1a;通過RL發現了一個叫“Aha Moment”的現象&#xff0c;這個時刻發生在模型的中間版本中。在這個階段&…

從零實現B站視頻下載器:Python自動化實戰教程

一、項目背景與實現原理 1.1 B站視頻分發機制 Bilibili的視頻采用 音視頻分離技術,通過以下方式提升用戶體驗: 動態碼率適配(1080P/4K/HDR) 分段加載技術(基于M4S格式) 內容保護機制(防盜鏈/簽名驗證) 1.2 技術實現路線 graph TDA[模擬瀏覽器請求] --> B[獲取加密…

AJAX的理解和原理還有概念

你想問的可能是 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09; &#xff0c;它并不是一門新的編程語言&#xff0c;而是一種在無需重新加載整個網頁的情況下&#xff0c;能夠與服務器進行異步通信并更新部分網頁的技術。以下從基本概念、原理、優點、使用場景等…

封裝一個分割線組件

最終樣式 Vue2代碼 <template><div class"sep-line"><div class"sep-label"><span class"sep-box-text"><slot>{{ title }}</slot> <!-- 默認插槽內容&#xff0c;如果沒有傳遞內容則使用title -->&…

Redis基本命令手冊——五大類型

目錄 一&#xff1a;基本操作 二&#xff1a;字符串&#xff08;String&#xff09; 三&#xff1a;哈希&#xff08;Hash) 四&#xff1a;列表&#xff08;List&#xff09; 五&#xff1a;集合&#xff08;Set&#xff09; 六&#xff1a;有序集合&#xff08;Zset&…

【C++】動態規劃從入門到精通

一、動態規劃基礎概念詳解 什么是動態規劃 動態規劃&#xff08;Dynamic Programming&#xff0c;DP&#xff09;是一種通過將復雜問題分解為重疊子問題&#xff0c;并存儲子問題解以避免重復計算的優化算法。它適用于具有以下兩個關鍵性質的問題&#xff1a; 最優子結構&…

Qt動態設置樣式,實現樣式實時切換

文章目錄 概要插件實現界面 核心代碼設置樣式 擴展導入樣式導出樣式 概要 最近需要設計界面&#xff0c;但是使用Qt的Designer只能看到每個界面單獨的樣式&#xff0c;程序中有些事需要主界面調用進行組合的界面&#xff0c;因此需要寫一個插件Ui可以直接輸入樣式內容&#xf…

集成學習之隨機森林

目錄 一、集成學習的含義 二、集成學習的代表 三、集成學習的應用 1、分類問題集成。&#xff08;基學習器是分類模型&#xff09; 2、回歸問題集成。&#xff08;基學習器是回歸模型&#xff09; 3、特征選取集成。 四、Bagging之隨機森林 1、隨機森林是有多個決策樹&a…

矩陣期望 E 的含義:概率

矩陣期望 E 的含義:概率 期望的含義 在概率論和統計學中,數學期望(或均值,簡稱期望)是試驗中每次可能結果的概率乘以其結果的總和,是最基本的數學特征之一,它反映隨機變量平均取值的大小。用公式表示,如果離散型隨機變量 X X X 可能取值為 x i x_

Qt Graphics View

Graphics View框架是用來處理大量2D圖形對象的&#xff0c;適合需要高效管理和交互的場景&#xff0c;比如繪圖軟件、地圖編輯或者游戲。它和QPainter的區別在于&#xff0c;Graphics View提供了更高級別的對象管理&#xff0c;而QPainter更偏向于直接繪制。 一、核心組件 ?Q…

卷積神經網絡 - 卷積層(具體例子)

為了更一步學習卷積神經網絡之卷積層&#xff0c;本文我們來通過幾個個例子來加深理解。 一、灰度圖像和彩色圖像的關于特征映射的例子 下面我們通過2個例子來形象說明卷積層中“特征映射”的概念&#xff0c;一個針對灰度圖像&#xff0c;一個針對彩色圖像。 例子 1&#x…

xlsx.utils.json_to_sheet函數詳解

xlsx.utils.json_to_sheet 是 xlsx 庫中的一個實用函數&#xff0c;用于將 JSON 數據轉換為 Excel 工作表對象。這個函數非常有用&#xff0c;尤其是在你需要從數據庫或其他數據源獲取數據并將其導出到 Excel 文件時。 函數簽名 XLSX.utils.json_to_sheet(data, opts)data&am…

2025-03-17 學習記錄--C/C++-PTA 習題4-7 最大公約數和最小公倍數

合抱之木&#xff0c;生于毫末&#xff1b;九層之臺&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、題目描述 ?? 習題4-7 最大公約數和最小公倍數 本題要求兩個給定正整數的最大公約數和最小公倍數。 輸入格式: 輸入在一…

【源碼閱讀】多個函數抽象為類(實現各種類型文件轉為PDF)

目錄 一、原始函數二、類三、轉換過程 一、原始函數 最開始就是寫了幾個函數&#xff08;包括doc、excel、ppt類型的文件&#xff09;轉換為pdf&#xff0c;需要將這些函數形成一個類。相似的一類函數就可以組成一個實現特定功能的類 import subprocess import pandas as pd i…

VSCode擴展工具Copilot MCP使用教程【MCP】

MCP&#xff08;Model Context Protocol&#xff0c;模型上下文協議&#xff09; &#xff0c;2024年11月底&#xff0c;由 Anthropic 推出的一種開放標準&#xff0c;旨在統一大型語言模型&#xff08;LLM&#xff09;與外部數據源和工具之間的通信協議。本文章教你使用VSCode…