返回文章列表

實作GPT模型生成文字技術詳解

本文探討如何使用 PyTorch 從零開始實作一個類別似 GPT 的模型,並訓練它生成類別似人類的文字。文章涵蓋了模型架構、關鍵元件的實作細節,以及資料前處理和模型初始化等步驟。同時,也討論了 GPT-2 與 GPT-3 的比較,以及層標準化在提高訓練穩定性方面的作用。

深度學習 自然語言處理

深度學習模型的訓練常常會遇到梯度消失或爆炸等問題,尤其是在多層網路結構中。層標準化透過調整神經網路層的啟用值,使其具有零均值和單位方差,有效地解決了這些問題,提高了訓練的穩定性和效率。這對於像 GPT-2 這樣的多層轉換器架構至關重要,因為它包含大量的層和引數,容易受到梯度問題的影響。透過在每個轉換器區塊中應用層標準化,可以確保模型在訓練過程中保持穩定,並有效地學習輸入資料的模式。本文提供的程式碼範例展示瞭如何使用 PyTorch 實作層標準化,並將其整合到 GPT 模型中。

實作GPT模型以生成文字

在前一章中,我們已經學習並實作了多頭注意力機制(multi-head attention mechanism),這是大語言模型(LLM)中的核心元件。現在,我們將實作LLM的其他建構模組,並將它們組合成類別似GPT的模型,接下來我們將訓練這個模型來生成類別似人類的文字。

4.1 編碼LLM架構

像GPT(生成式預訓練變換器)這樣的大語言模型,是設計用來一次生成一個詞(或標記)的深度神經網路架構。雖然它們的規模龐大,但模型架構並不如你想像的那麼複雜,因為它的許多元件都是重複的,如我們稍後會看到的。圖4.2提供了GPT-like LLM的頂層檢視,突出了其主要元件。

圖4.2 GPT-like LLM架構圖

我們已經介紹了LLM架構的幾個方面,例如輸入標記化和嵌入(input tokenization and embedding)以及遮蔽多頭注意力模組(masked multi-head attention module)。現在,我們將實作GPT模型的核心結構,包括其變換器塊(transformer blocks),稍後我們將訓練這些結構來生成類別似人類的文字。

在前面的章節中,為了簡化起見,我們使用了較小的嵌入維度,以確保概念和範例可以舒適地放在一頁上。現在,我們正在擴充套件到小型GPT-2模型的規模,具體來說是具有1.24億引數的最小版本,如Radford等人所述(https://mng.bz/yoBq)。注意,雖然原始報告提到1.17億引數,但後來已被修正。在第6章中,我們將重點載入預訓練權重到我們的實作中,並將其適應於更大的GPT-2模型,分別具有3.45億、7.62億和15.42億引數。

在深度學習和像GPT這樣的LLM背景下,“引數”指的是模型的可訓練權重(trainable weights)。這些權重本質上是模型的內部變數,在訓練過程中會被調整和最佳化,以最小化特定的損失函式。這種最佳化使模型能夠從訓練資料中學習。

練習3.3 初始化GPT-2大小的注意力模組

使用MultiHeadAttention類別,初始化一個具有與最小GPT-2模型相同數量注意力頭(12個注意力頭)的多頭注意力模組。同時,請確保使用與GPT-2相似的輸入和輸出嵌入大小(768維度)。注意,最小的GPT-2模型支援長度為1,024個標記的上下文。

import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiHeadAttention(nn.Module):
    def __init__(self, num_heads, embedding_dim, dropout=0.1):
        super(MultiHeadAttention, self).__init__()
        assert embedding_dim % num_heads == 0, "Embedding dimension must be divisible by the number of heads"
        
        self.num_heads = num_heads
        self.embedding_dim = embedding_dim
        self.dropout = dropout
        
        self.head_dim = embedding_dim // num_heads
        
        # 定義查詢、鍵和值的線性層
        self.query_linear = nn.Linear(embedding_dim, embedding_dim)
        self.key_linear = nn.Linear(embedding_dim, embedding_dim)
        self.value_linear = nn.Linear(embedding_dim, embedding_dim)
        
        # 定義輸出的線性層
        self.output_linear = nn.Linear(embedding_dim, embedding_dim)
        
        # 定義dropout層
        self.dropout_layer = nn.Dropout(dropout)
        
    def forward(self, query, key, value, attention_mask=None):
        # 取得批次大小
        batch_size = query.size(0)
        
        # 線性變換
        query = self.query_linear(query).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        key = self.key_linear(key).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        value = self.value_linear(value).view(batch_size, -1, self.num_heads, self.head_dim).transpose(1, 2)
        
        # 計算注意力得分
        scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.head_dim)
        
        if attention_mask is not None:
            scores = scores + attention_mask
        
        # 計算注意力權重
        attention_weights = F.softmax(scores, dim=-1)
        
        # 應用dropout
        attention_weights = self.dropout_layer(attention_weights)
        
        # 計算輸出
        output = torch.matmul(attention_weights, value).transpose(1, 2).contiguous().view(batch_size, -1, self.embedding_dim)
        
        # 線性變換輸出
        output = self.output_linear(output)
        
        return output

#### 內容解密:
1. **多頭注意力機制初始化**我們定義了一個名為`MultiHeadAttention`的類別用於初始化多頭注意力模組它接收三個引數注意力頭的數量嵌入維度和dropout率
2. **斷言檢查**檢查嵌入維度是否能被注意力頭數量整除以確保每個頭的維度是整數
3. **線性層定義**定義了四個線性層分別用於查詢值和輸出的線性變換
4. **前向傳播**實作了多頭注意力的前向傳播過程包括線性變換注意力得分計算注意力權重計算和輸出計算
5. **注意力掩碼**支援可選的注意力掩碼用於遮蔽未來的標記或填充標記
6. **Dropout**在注意力權重上應用dropout以防止過擬合

# 初始化GPT-2大小的注意力模組
num_heads = 12
embedding_dim = 768

multi_head_attention = MultiHeadAttention(num_heads, embedding_dim)

#### 內容解密:
1. **引數設定**設定了與最小GPT-2模型相比對的引數12個注意力頭和768維的嵌入大小
2. **例項化多頭注意力模組**使用設定的引數例項化了一個`MultiHeadAttention`物件

## 從零開始實作GPT模型以生成文字

本章節將探討如何從零開始實作一個GPT模型以實作文字生成的功能首先我們將介紹GPT模型的基本架構接著逐步實作各個元件最終組裝成完整的GPT模型

### GPT-2與GPT-3的比較

本章節主要關注GPT-2模型因為OpenAI已公開其預訓練模型的權重我們將在後續章節中載入這些權重GPT-3在模型架構上與GPT-2基本相同但規模更大引數量從15億增加到1,750並在更多資料上進行訓練然而GPT-3的權重尚未公開對於學習如何實作大語言模型LLM來說GPT-2是一個更好的選擇因為它可以在單台筆記型電腦上執行而GPT-3則需要GPU叢集進行訓練和推斷

### GPT模型的架構

GPT模型主要由嵌入層embedding layers)、轉換器區塊transformer blocks以及輸出層output layers組成其中轉換器區塊是GPT類別LLM的關鍵元件模型的目標是每次生成一個單詞的文字

#### GPT模型組態

我們使用以下Python字典來指定小型GPT-2模型的組態
```python
GPT_CONFIG_124M = {
    "vocab_size": 50257,  # 詞彙大小
    "context_length": 1024,  # 上下文長度
    "emb_dim": 768,  # 嵌入維度
    "n_heads": 12,  # 注意力頭數量
    "n_layers": 12,  # 層數
    "drop_rate": 0.1,  # dropout率
    "qkv_bias": False  # Query-Key-Value偏差
}

這個組態字典定義了模型的各個超引數,包括詞彙大小、上下文長度、嵌入維度、注意力頭數量、層數、dropout率等。

GPT模型的實作

首先,我們實作一個GPT模型的佔位架構(DummyGPTModel),以便了解模型的整體結構。

import torch
import torch.nn as nn

class DummyGPTModel(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"])
        self.trf_blocks = nn.Sequential(
            *[DummyTransformerBlock(cfg) for _ in range(cfg["n_layers"])]
        )
        self.final_norm = DummyLayerNorm(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.shape
        tok_embeds = self.tok_emb(in_idx)
        pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))
        x = tok_embeds + pos_embeds
        x = self.drop_emb(x)
        x = self.trf_blocks(x)
        x = self.final_norm(x)
        logits = self.out_head(x)
        return logits

這個佔位模型定義了GPT模型的基本架構,包括嵌入層、轉換器區塊、層標準化(layer normalization)以及輸出層。

下一步工作

接下來,我們將逐步實作轉換器區塊中的各個元件,包括層標準化、GELU啟用函式、前饋網路(feed forward network)以及捷徑連線(shortcut connections)。最終,我們將組裝這些元件以完成GPT模型的實作。

內容解密:

  1. 嵌入層:嵌入層負責將輸入的token索引轉換為向量表示。
  2. 轉換器區塊:轉換器區塊是GPT模型的核心元件,負責處理輸入序列。
  3. 層標準化:層標準化用於規範化層的輸出,以穩定訓練過程。
  4. GELU啟用函式:GELU是一種啟用函式,用於引入非線性。
  5. 前饋網路:前饋網路用於變換輸入資料,以提取更高層次的特徵。
  6. 捷徑連線:捷徑連線用於殘差連線,以避免梯度消失問題。

這些元件共同構成了GPT模型的基礎架構,為實作文字生成任務奠定了基礎。

實作LLM架構:GPT模型的簡化版本

在前一章節中,我們已經瞭解了GPT模型的基本架構。現在,我們將實作一個簡化的GPT模型版本,使用PyTorch的神經網路模組(nn.Module)。這個簡化的模型將包括標記和位置嵌入、丟棄(dropout)、一系列的轉換器區塊(transformer blocks)、最終的層標準化(layer normalization)以及線性輸出層。

簡化GPT模型的實作

class DummyTransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        
    def forward(self, x):
        return x

class DummyLayerNorm(nn.Module):
    def __init__(self, normalized_shape, eps=1e-5):
        super().__init__()
        
    def forward(self, x):
        return x

class DummyGPTModel(nn.Module):
    # 模型的實作細節...

內容解密:

  1. DummyTransformerBlock類別:這是一個簡單的佔位符類別,將在後續章節中被真實的TransformerBlock取代。目前,它只是簡單地傳回輸入。
  2. DummyLayerNorm類別:同樣,這是一個佔位符類別,未來將被真實的LayerNorm類別取代。它目前只是傳回輸入,模擬LayerNorm的介面。
  3. DummyGPTModel類別:這是簡化的GPT模型的主體。它包含標記和位置嵌入、丟棄、一系列的DummyTransformerBlock、最終的DummyLayerNorm以及線性輸出層。

資料前處理與模型初始化

import tiktoken
tokenizer = 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)

內容解密:

  1. tiktoken的使用:我們使用tiktoken函式庫來對輸入文字進行標記化處理,將文字轉換為模型可以理解的標記ID。
  2. 建立批次資料:將兩個示例文字標記化後,堆積疊成一個批次,作為模型的輸入。

模型輸出

torch.manual_seed(123)
model = DummyGPTModel(GPT_CONFIG_124M)
logits = model(batch)
print("Output shape:", logits.shape)
print(logits)

內容解密:

  1. 模型初始化:使用固定的隨機種子,初始化一個124M引數的DummyGPTModel例項。
  2. 模型輸出:將準備好的批次資料輸入模型,得到輸出logits。輸出的形狀對應於批次大小、序列長度和詞彙大小。

層標準化:提高神經網路訓練的穩定性

訓練具有多層的神經網路可能會因為梯度消失或爆炸等問題而變得困難。層標準化透過調整神經網路層的啟用值,使其具有零均值和單位方差,從而提高訓練的穩定性和效率。

層標準化的實作

class LayerNorm(nn.Module):
    def __init__(self, normalized_shape, eps=1e-5):
        super().__init__()
        self.normalized_shape = normalized_shape
        self.eps = eps
        self.weight = nn.Parameter(torch.ones(normalized_shape))
        self.bias = nn.Parameter(torch.zeros(normalized_shape))
        
    def forward(self, x):
        # 計算均值和方差,並進行標準化
        mean = x.mean(dim=-1, keepdim=True)
        var = x.var(dim=-1, keepdim=True, unbiased=False)
        x_normalized = (x - mean) / torch.sqrt(var + self.eps)
        return x_normalized * self.weight + self.bias

內容解密:

  1. LayerNorm類別:實作了層標準化的邏輯,包括初始化和前向傳播方法。
  2. 標準化過程:在前向傳播中,計算輸入的均值和方差,然後進行標準化,最後應用可學習的權重和偏差。

透過層標準化,我們可以提高神經網路訓練的穩定性和效率,這對於像GPT-2這樣的大型轉換器架構尤為重要。