返回文章列表

GPT2模型解碼策略與權過載入

本文探討 GPT-2 模型的解碼策略與權過載入方法。涵蓋 Top-k 取樣、溫度取樣等技術,並提供修改後的文字生成函式,結合不同策略提升生成文字的多樣性。同時,詳細說明如何在 PyTorch 中載入和儲存模型權重,以及如何使用預訓練的 GPT-2 模型權重,包含從 OpenAI

深度學習 自然語言處理

大語言模型的訓練成本高昂,有效載入和使用預訓練模型權重至關重要。本文介紹瞭如何使用 PyTorch 載入和儲存模型的 state_dict,確保模型訓練的延續性。同時,詳細說明瞭如何從 OpenAI 載入預訓練的 GPT-2 模型權重,包含必要的環境設定、程式碼範例和步驟說明。此外,本文也探討了控制隨機性的解碼策略,包含 Top-k 取樣和溫度取樣,並提供了結合兩種策略的程式碼實作,讓開發者能更精確地控制生成文字的多樣性和準確性,並根據不同的應用場景調整引數。

5.3 控制隨機性的解碼策略

在生成文字的過程中,模型會根據輸入的上下文預測下一個詞彙的機率分佈。為了控制生成文字的多樣性和準確性,我們可以採用不同的解碼策略。

5.3.1 Top-k 取樣

Top-k 取樣是一種常見的解碼策略,旨在限制模型在預測下一個詞彙時的選擇範圍。具體來說,就是隻考慮機率最高的 k 個詞彙,而不是所有可能的詞彙。

首先,我們取得 logits 值最高的 top 三個 token 的 logits 值和 token ID,按照降序排列:

Top logits: tensor([6.7500, 6.2800, 4.5100])
Top positions: tensor([3, 7, 0])

接著,我們使用 PyTorch 的 where 函式,將不在 top 三個的 token 的 logit 值設為負無窮大(-inf):

new_logits = torch.where(
    condition=next_token_logits < top_logits[-1],
    input=torch.tensor(float('-inf')),
    other=next_token_logits
)

輸出結果如下:

tensor([4.5100, -inf, -inf, 6.7500, -inf, -inf, -inf, 6.2800, -inf])

最後,我們對這些 logit 值應用 softmax 函式,將它們轉換為機率:

topk_probas = torch.softmax(new_logits, dim=0)

得到的結果是三個非零機率值:

tensor([0.0615, 0.0000, 0.0000, 0.5775, 0.0000, 0.0000, 0.0000, 0.3610, 0.0000])

內容解密:

  1. 取得 Top Logits 和 Token ID:首先,我們根據模型的輸出 logits 值,找出機率最高的 top 三個 token 的 logits 值和對應的 token ID。
  2. 設定非 Top Token 的 Logits 為 -inf:使用 PyTorch 的 where 函式,將不在 top 三個的 token 的 logit 值設為負無窮大,以確保它們在 softmax 後的機率為零。
  3. 應用 Softmax 函式:將處理後的 logits 值透過 softmax 函式轉換為機率,得到最終的機率分佈。

5.3.3 修改文字生成函式

現在,我們結合溫度取樣(temperature sampling)和 top-k 取樣,修改之前的 generate_text_simple 函式,建立一個新的 generate 函式。

def generate(model, idx, max_new_tokens, context_size,
             temperature=0.0, top_k=None, eos_id=None):
    for _ in range(max_new_tokens):
        idx_cond = idx[:, -context_size:]
        with torch.no_grad():
            logits = model(idx_cond)
            logits = logits[:, -1, :]
            
        if top_k is not None:
            top_logits, _ = torch.topk(logits, top_k)
            min_val = top_logits[:, -1]
            logits = torch.where(
                logits < min_val,
                torch.tensor(float('-inf')).to(logits.device),
                logits
            )
            
        if temperature > 0.0:
            logits = logits / temperature
            probs = torch.softmax(logits, dim=-1)
            idx_next = torch.multinomial(probs, num_samples=1)
        else:
            idx_next = torch.argmax(logits, dim=-1, keepdim=True)
            
        if idx_next == eos_id:
            break
            
        idx = torch.cat((idx, idx_next), dim=1)
        
    return idx

內容解密:

  1. generate 函式流程:該函式根據輸入的 idx(token IDs)和模型,生成指定數量(max_new_tokens)的文字。
  2. Top-k 取樣實作:如果指定了 top_k,則只保留機率最高的 k 個 token 的 logits 值,其他設為 -inf。
  3. 溫度取樣實作:如果溫度大於 0,則對 logits 值進行溫度縮放後,透過 softmax 取得機率分佈,並進行多項式取樣;否則,直接選擇機率最高的 token。
  4. 結束標記處理:如果生成的 token 是結束標記(eos_id),則提前終止生成。

測試新的 generate 函式:

torch.manual_seed(123)
token_ids = generate(
    model=model,
    idx=text_to_token_ids("Every effort moves you", tokenizer),
    max_new_tokens=15,
    context_size=GPT_CONFIG_124M["context_length"],
    top_k=25,
    temperature=1.4
)
print("Output text:\n", token_ids_to_text(token_ids, tokenizer))

生成的文字如下:

Output text:
 Every effort moves you stand to work on surprise, a one of us had gone with random-

相比之前的生成結果,這次的輸出更加多樣化。

練習題 5.2

嘗試不同的溫度和 top-k 設定,觀察生成的文字變化。思考在哪些應用場景下需要較低或較高的溫度和 top-k 設定。

5.4 在 PyTorch 中載入和儲存模型權重

為何儲存模型權重?

由於訓練大語言模型(LLM)非常耗時耗資源,因此儲存模型權重以便後續使用或繼續訓練至關重要。

如何儲存和載入模型權重?

PyTorch 提供簡單的方法來儲存和載入模型的 state_dict,即模型的權重引數。

儲存模型權重:

torch.save(model.state_dict(), "model.pth")

載入模型權重到新的 GPTModel 例項:

model.load_state_dict(torch.load("model.pth"))

內容解密:

  1. 儲存模型權重:使用 torch.save 將模型的 state_dict 儲存到檔案中。
  2. 載入模型權重:使用 torch.load 載入儲存的權重,並透過 load_state_dict 方法將權過載入到模型中。

練習題 5.3

思考如何設定 generate 函式的引數,以實作確定性的文字生成(即停用隨機取樣,使輸出固定)。

載入與使用預訓練的GPT-2模型權重

在前一章中,我們訓練了一個小型GPT-2模型,使用的是有限的資料集,即一本短篇故事書。這種方法使我們能夠專注於基本原理,而無需耗費大量時間和計算資源。

儲存與載入模型及最佳化器狀態

為了在稍後繼續預訓練模型,儲存最佳化器狀態是必要的。像AdamW這樣的自適應最佳化器會為每個模型權重儲存額外的引數。AdamW利用歷史資料動態調整每個模型引數的學習率。如果不儲存最佳化器狀態,最佳化器將重置,模型可能會學習得不理想,甚至無法正常收斂,這意味著它將失去生成連貫文字的能力。

torch.save({
    "model_state_dict": model.state_dict(),
    "optimizer_state_dict": optimizer.state_dict(),
}, "model_and_optimizer.pth")

然後,我們可以透過首先使用torch.load載入已儲存的資料,然後使用load_state_dict方法來還原模型和最佳化器的狀態:

checkpoint = torch.load("model_and_optimizer.pth", map_location=device)
model = GPTModel(GPT_CONFIG_124M)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1)
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
model.train()

內容解密:

  1. 儲存模型和最佳化器狀態:使用torch.save儲存模型的狀態字典和最佳化器的狀態字典。
  2. 載入模型和最佳化器狀態:使用torch.load載入已儲存的檢查點,然後分別載入模型和最佳化器的狀態字典。
  3. 設定模型為訓練模式:透過呼叫model.train(),確保模型在載入狀態後處於訓練模式。

從OpenAI載入預訓練權重

幸運的是,OpenAI公開分享了他們的GPT-2模型的權重,從而消除了需要在大型語料函式庫上重新訓練模型的需要。首先,我們需要安裝TensorFlow和tqdm來載入GPT-2模型的權重:

pip install tensorflow>=2.15.0 tqdm>=4.66

下載GPT-2模型的權重

我們可以直接從本章的線上倉函式庫下載gpt_download.py Python模組:

import urllib.request
url = ("https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/main/ch05/01_main-chapter-code/gpt_download.py")
filename = url.split('/')[-1]
urllib.request.urlretrieve(url, filename)

然後,我們可以匯入download_and_load_gpt2函式,該函式將載入GPT-2架構設定(settings)和權重引數(params):

from gpt_download import download_and_load_gpt2
settings, params = download_and_load_gpt2(model_size="124M", models_dir="gpt2")

內容解密:

  1. 安裝必要的函式庫:TensorFlow用於載入GPT-2模型的權重,tqdm用於顯示下載進度。
  2. 下載GPT-2模型的權重:透過執行download_and_load_gpt2函式,下載並載入GPT-2模型的設定和權重。
  3. 檢查下載的內容:列印settingsparams的內容,以驗證下載是否成功。

檢視模型的設定與權重

下載完成後,我們可以檢視settingsparams的內容:

print("Settings:", settings)
print("Parameter dictionary keys:", params.keys())

輸出結果如下:

Settings: {'n_vocab': 50257, 'n_ctx': 1024, 'n_embd': 768, 'n_head': 12, 'n_layer': 12}
Parameter dictionary keys: dict_keys(['blocks', 'b', 'g', 'wpe', 'wte'])

內容解密:

  1. settings字典:包含了GPT-2模型的架構設定,如詞彙量、上下文視窗大小、嵌入維度等。
  2. params字典:包含了模型的實際權重張量,可以透過鍵值存取特定的權重。

載入OpenAI預訓練權重於GPT-2模型

在前面的章節中,我們已經下載並載入了最小的GPT-2模型權重,接下來我們需要將這些權重轉移到我們的GPTModel例項中。首先,我們需要定義一個字典來列出不同GPT模型大小之間的差異,如圖5.17所示。

GPT-2模型組態

GPT-2模型有多種不同的大小,包括124M、355M、774M和1558M。這些模型的架構相同,但嵌入維度和重複次數不同。

model_configs = {
    "gpt2-small (124M)": {"emb_dim": 768, "n_layers": 12, "n_heads": 12},
    "gpt2-medium (355M)": {"emb_dim": 1024, "n_layers": 24, "n_heads": 16},
    "gpt2-large (774M)": {"emb_dim": 1280, "n_layers": 36, "n_heads": 20},
    "gpt2-xl (1558M)": {"emb_dim": 1600, "n_layers": 48, "n_heads": 25},
}

更新GPTModel組態

假設我們要載入最小的模型"gpt2-small (124M)",我們可以使用model_configs中的對應設定來更新我們的GPT_CONFIG_124M

model_name = "gpt2-small (124M)"
NEW_CONFIG = GPT_CONFIG_124M.copy()
NEW_CONFIG.update(model_configs[model_name])
NEW_CONFIG.update({"context_length": 1024})
NEW_CONFIG.update({"qkv_bias": True})

組態更新說明:

  1. 上下文長度:原始GPT-2模型使用1024作為上下文長度,因此我們更新NEW_CONFIG以比對此設定。
  2. qkv_bias:OpenAI的原始實作中使用了偏置向量,因此我們將qkv_bias設為True以保持一致性。

初始化GPTModel例項

使用更新後的NEW_CONFIG,我們可以初始化一個新的GPTModel例項。

gpt = GPTModel(NEW_CONFIG)
gpt.eval()

載入權重

預設情況下,GPTModel例項會以隨機權重初始化。為了使用OpenAI的預訓練權重,我們需要覆寫這些隨機權重。

定義assign函式

首先,我們定義一個小型的assign函式,用於檢查兩個張量或陣列的維度是否相同,並傳回右側張量作為可訓練的PyTorch引數。

def assign(left, right):
    if left.shape != right.shape:
        raise ValueError(f"Shape mismatch. Left: {left.shape}, Right: {right.shape}")
    return torch.nn.Parameter(torch.tensor(right))

載入權重函式

接下來,我們定義一個load_weights_into_gpt函式,用於將權重從params字典載入到GPTModel例項中。

import numpy as np

def load_weights_into_gpt(gpt, params):
    gpt.pos_emb.weight = assign(gpt.pos_emb.weight, params['wpe'])
    gpt.tok_emb.weight = assign(gpt.tok_emb.weight, params['wte'])
    
    for b in range(len(params["blocks"])):
        q_w, k_w, v_w = np.split((params["blocks"][b]["attn"]["c_attn"])["w"], 3, axis=-1)
        gpt.trf_blocks[b].att.W_query.weight = assign(gpt.trf_blocks[b].att.W_query.weight, q_w.T)
        gpt.trf_blocks[b].att.W_key.weight = assign(gpt.trf_blocks[b].att.W_key.weight, k_w.T)
        gpt.trf_blocks[b].att.W_value.weight = assign(gpt.trf_blocks[b].att.W_value.weight, v_w.T)
        
        q_b, k_b, v_b = np.split((params["blocks"][b]["attn"]["c_attn"])["b"], 3, axis=-1)
        gpt.trf_blocks[b].att.W_query.bias = assign(gpt.trf_blocks[b].att.W_query.bias, q_b)
        gpt.trf_blocks[b].att.W_key.bias = assign(gpt.trf_blocks[b].att.W_key.bias, k_b)
        gpt.trf_blocks[b].att.W_value.bias = assign(gpt.trf_blocks[b].att.W_value.bias, v_b)
        
        gpt.trf_blocks[b].att.out_proj.weight = assign(gpt.trf_blocks[b].att.out_proj.weight, params["blocks"][b]["attn"]["c_proj"]["w"].T)

載入權重說明:

  1. 位置嵌入和標記嵌入:首先,我們將位置嵌入和標記嵌入的權過載入到GPTModel中。
  2. 多頭注意力機制:對於每個變壓器區塊,我們將查詢、鍵和值的權重和偏置載入到相應的層中。
  3. 輸出投影:最後,我們載入輸出投影層的權重。

透過這些步驟,我們成功地將OpenAI的預訓練權過載入到我們的GPTModel例項中,使其能夠利用預訓練模型的知識進行進一步的微調或推斷。