返回文章列表

深度學習模型微調與程式碼解析

本文探討深度學習模型的微調技巧與程式碼解析,涵蓋模型引數調整、資料載入器設定、注意力機制實作、損失函式計算、提示格式調整等關鍵導向,並輔以 GPT 模型微調與程式碼範例,提供實務參考。

深度學習 程式開發

深度學習模型的訓練和微調過程涉及許多細節,從資料預處理到模型架構調整,每個環節都可能影響最終效能。本文針對資料載入、注意力機制、模型引數設定、損失函式計算以及提示工程等方面,提供程式碼解析和實務技巧,並以 GPT 模型的微調為例,示範如何調整模型以適應特定任務。特別關注不同模型大小的資源需求、引數數量計算、以及如何控制不同層的 dropout 率等細節,並探討不同提示格式對模型訓練效率和效能的影響。最後,文章也分析了訓練集和驗證集損失的比較,以及不同微調策略(例如微調整個模型或只微調特定層)對模型效能的影響,提供更全面的模型微調策略參考。

附錄A:深度學習資源

對於希望進一步學習深度學習的讀者,以下是一些推薦資源:

  • Machine Learning with PyTorch and Scikit-Learn(2022)by Sebastian Raschka, Hayden Liu, and Vahid Mirjalili。ISBN 978-1801819312
  • Deep Learning with PyTorch(2021)by Eli Stevens, Luca Antiga, and Thomas Viehmann。ISBN 978-1617295263
  • Lecture 4.1: Tensors in Deep Learning,Sebastian Raschka錄製的15分鐘影片教程(https://www.youtube.com/watch?v=JXfDlgrfOBY)
  • Model Evaluation, Model Selection, and Algorithm Selection in Machine Learning(2018)by Sebastian Raschka(https://arxiv.org/abs/1811.12808)
  • Introduction to Calculus by Sebastian Raschka(https://mng.bz/WEyW)

此外,對於希望瞭解更多關於梯度累積和分散式資料平行性的讀者,可以參考以下資源:

  • Finetuning Large Language Models on a Single GPU Using Gradient Accumulation by Sebastian Raschka(https://mng.bz/8wPD)
  • Introducing PyTorch Fully Sharded Data Parallel (FSDP) API(https://mng.bz/EZJR)

附錄C:練習解答

本文的練習解答程式碼可在補充的GitHub儲存函式庫中找到:https://github.com/rasbt/LLMs-from-scratch。

第二章練習解答

練習2.1

您可以透過每次提示編碼器一個字串來獲得單個標記ID:

print(tokenizer.encode("Ak"))
print(tokenizer.encode("w"))
# ...

這將輸出:

[33901]
[86]
# ...

然後,您可以使用以下程式碼組裝原始字串:

print(tokenizer.decode([33901, 86, 343, 86, 220, 959]))

這將傳回:

'Akwirw ier'

第3章 習題解答

練習 2.2

max_length=2stride=2 時,資料載入器的程式碼如下:

dataloader = create_dataloader(
    raw_text, batch_size=4, max_length=2, stride=2
)

產生的批次格式如下:

tensor([[ 40, 367],
        [2885, 1464],
        [1807, 3619],
        [ 402, 271]])

而當 max_length=8stride=2 時,資料載入器的程式碼如下:

dataloader = create_dataloader(
    raw_text, batch_size=4, max_length=8, stride=2
)

一個範例批次如下:

tensor([[ 40, 367, 2885, 1464, 1807, 3619, 402, 271],
        [2885, 1464, 1807, 3619, 402, 271, 10899, 2138],
        [1807, 3619, 402, 271, 10899, 2138, 257, 7026],
        [402, 271, 10899, 2138, 257, 7026, 15632, 438]])

程式碼解密:

  1. create_dataloader函式用於建立資料載入器。
  2. max_length引數控制每個輸入序列的最大長度。
  3. stride引數控制序列之間的步幅。
  4. max_length=2stride=2 時,序列不重疊;當 max_length=8stride=2 時,序列部分重疊。

練習 3.1

正確的權重分配如下:

sa_v1.W_query = torch.nn.Parameter(sa_v2.W_query.weight.T)
sa_v1.W_key = torch.nn.Parameter(sa_v2.W_key.weight.T)
sa_v1.W_value = torch.nn.Parameter(sa_v2.W_value.weight.T)

程式碼解密:

  1. sa_v2 的權重轉置後指定給 sa_v1 的對應權重。
  2. 這樣做是為了比對兩種不同實作之間的權重格式。

練習 3.2

要實作輸出維度為2,需要將投影維度 d_out 設定為1。

d_out = 1
mha = MultiHeadAttentionWrapper(d_in, d_out, block_size, 0.0, num_heads=2)

程式碼解密:

  1. d_out 設定為1,以實作所需的輸出維度。
  2. 初始化一個具有兩個注意力頭的多頭注意力模組。

練習 3.3

最小的GPT-2模型的初始化如下:

block_size = 1024
d_in, d_out = 768, 768
num_heads = 12
mha = MultiHeadAttention(d_in, d_out, block_size, 0.0, num_heads)

程式碼解密:

  1. 設定序列長度為1024,輸入和輸出維度均為768。
  2. 初始化一個具有12個注意力頭的多頭注意力模組。

第4章 習題解答

練習 4.1

計算前饋網路和注意力模組的引數數量:

block = TransformerBlock(GPT_CONFIG_124M)
total_params = sum(p.numel() for p in block.ff.parameters())
print(f"Total number of parameters in feed forward module: {total_params:,}")
total_params = sum(p.numel() for p in block.att.parameters())
print(f"Total number of parameters in attention module: {total_params:,}")

結果顯示,前饋網路的引數數量約為注意力模組的兩倍。

程式碼解密:

  1. 初始化一個Transformer區塊。
  2. 分別計算前饋網路和注意力模組的引數數量。

練習 4.2

修改組態字典以例項化其他GPT模型大小(以GPT-2 XL為例):

GPT_CONFIG = GPT_CONFIG_124M.copy()
GPT_CONFIG["emb_dim"] = 1600
GPT_CONFIG["n_layers"] = 48
GPT_CONFIG["n_heads"] = 25
model = GPTModel(GPT_CONFIG)

計算引數數量和RAM需求後,結果如下:

  • 總引數數量:1,637,792,000
  • 可訓練引數數量(考慮權重分享):1,557,380,800
  • 模型總大小:6247.68 MB

程式碼解密:

  1. 修改組態字典以比對GPT-2 XL的規格。
  2. 初始化GPT模型並計算相關統計資料。

練習 4.3

修改組態字典和程式碼以分別控制不同層的dropout率:

GPT_CONFIG_124M = {
    "vocab_size": 50257,
    "context_length": 1024,
    "emb_dim": 768,
    "n_heads": 12,
    "n_layers": 12,
    "drop_rate_attn": 0.1,
    "drop_rate_shortcut": 0.1,
    "drop_rate_emb": 0.1,
    "qkv_bias": False
}

修改後的TransformerBlockGPTModel如下:

class TransformerBlock(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.att = MultiHeadAttention(
            d_in=cfg["emb_dim"],
            d_out=cfg["emb_dim"],
            context_length=cfg["context_length"],
            num_heads=cfg["n_heads"],
            dropout=cfg["drop_rate_attn"],
            qkv_bias=cfg["qkv_bias"])
        self.ff = FeedForward(cfg)
        self.norm1 = LayerNorm(cfg["emb_dim"])
        self.norm2 = LayerNorm(cfg["emb_dim"])
        self.drop_shortcut = nn.Dropout(cfg["drop_rate_shortcut"])

    def forward(self, x):
        # 省略實作細節

class GPTModel(nn.Module):
    def __init__(self, cfg):
        super().__init__()
        self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
        self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
        self.drop_emb = nn.Dropout(cfg["drop_rate_emb"])
        # 省略其他實作細節

程式碼解密:

  1. 在組態字典中新增不同的dropout率引數。
  2. 修改TransformerBlockGPTModel以使用這些新的dropout率引數。

第5章 習題解答

練習 5.1

使用print_sampled_tokens函式列印“pizza”標記被取樣的次數。

  • 當溫度為0或0.1時,“pizza”被取樣的次數為0。
  • 當溫度升高到5時,“pizza”被取樣的次數為32次。

預估機率為3.2%,實際機率為4.3%。

程式碼解密:

  1. 使用不同的溫度設定進行取樣。
  2. 統計“pizza”標記的出現次數。

練習 5.2

Top-k取樣和溫度縮放是根據LLM和所需的輸出多樣性和隨機性進行調整的設定。

  • 較小的top-k值(例如小於10)和低於1的溫度會使模型的輸出更具確定性和連貫性。
  • 較大的top-k值(例如20到40)和高於1的溫度則適用於需要更多創造力和多樣性的任務,如創意寫作。

程式碼解密:

  1. 調整top-k值和溫度以控制輸出的隨機性和多樣性。
  2. 不同設定適用於不同的應用場景,如正式檔案、創意寫作等。

第5章練習解答

練習5.3

強制生成函式具有確定性行為有多種方法:

  1. 設定top_k=None並且不應用溫度縮放
  2. 設定top_k=1

練習5.4

本質上,我們需要載入在主章節中儲存的模型和最佳化器:

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

然後,呼叫train_simple_function,設定num_epochs=1來再訓練模型一個epoch。

內容解密:

這段程式碼首先載入了之前儲存的模型和最佳化器的檢查點。然後,它初始化了一個GPT模型,並載入了檢查點中的模型狀態字典。接著,它初始化了一個AdamW最佳化器,並載入了檢查點中的最佳化器狀態字典。最後,它呼叫了train_simple_function來再訓練模型一個epoch。

練習5.5

我們可以使用以下程式碼來計算GPT模型的訓練和驗證集損失:

train_loss = calc_loss_loader(train_loader, gpt, device)
val_loss = calc_loss_loader(val_loader, gpt, device)

對於1.24億引數的模型,得到的損失如下: 訓練損失:3.754748503367106 驗證損失:3.559617757797241

主要觀察結果是,訓練和驗證集的效能在同一水平。這有多種可能的解釋:

  1. 當OpenAI訓練GPT-2時,“The Verdict”並不是預訓練資料集的一部分。因此,模型並沒有明確地過擬合訓練集,並且在訓練和驗證集上的表現相似。
  2. “The Verdict”是GPT-2的訓練資料集的一部分。在這種情況下,我們無法判斷模型是否過擬合訓練資料,因為驗證集也已被用於訓練。

內容解密:

這段程式碼計算了GPT模型在訓練和驗證集上的損失。結果顯示,訓練和驗證集的損失在同一水平,這可能是由於“The Verdict”不是預訓練資料集的一部分,或者是“The Verdict”是GPT-2的訓練資料集的一部分。

練習5.6

在主章節中,我們實驗了最小的GPT-2模型,它只有1.24億個引數。這樣做的原因是為了保持資源需求盡可能低。然而,你可以輕鬆地透過最小的程式碼更改來實驗更大的模型。例如,要載入15.58億引數的模型而不是1.24億引數的模型,我們只需要更改以下兩行程式碼:

hparams, params = download_and_load_gpt2(model_size="1558M", models_dir="gpt2")
model_name = "gpt2-xl (1558M)"

第6章練習解答

練習6.1

我們可以透過在初始化資料集時設定max_length=1024來將輸入填充到模型支援的最大標記數量:

train_dataset = SpamDataset(..., max_length=1024, ...)
val_dataset = SpamDataset(..., max_length=1024, ...)
test_dataset = SpamDataset(..., max_length=1024, ...)

然而,額外的填充導致測試準確率大幅下降到78.33%(相對於主章節中的95.67%)。

內容解密:

這段程式碼將輸入填充到模型支援的最大標記數量。然而,這導致了測試準確率的下降。

練習6.2

我們可以透過刪除以下程式碼來微調整個模型,而不是隻微調最後一個Transformer區塊:

for param in model.parameters():
    param.requires_grad = False

這種修改導致測試準確率提高了1%,達到96.67%(相對於主章節中的95.67%)。

內容解密:

這段程式碼微調了整個模型,而不是隻微調最後一個Transformer區塊。這導致了測試準確率的提高。

練習6.3

我們可以透過將model(input_batch)[:, -1, :]更改為model(input_batch)[:, 0, :]來微調第一個輸出標記,而不是最後一個輸出標記。

正如預期的那樣,由於第一個標記包含的資訊少於最後一個標記,這種更改導致測試準確率大幅下降到75.00%(相對於主章節中的95.67%)。

內容解密:

這段程式碼微調了第一個輸出標記,而不是最後一個輸出標記。這導致了測試準確率的下降。

第7章練習解答

練習7.1

Phi-3提示格式如圖7.4所示,對於給定的示例輸入,看起來如下:

<user>
Identify the correct spelling of the following word: 'Occasion'
<assistant>
The correct spelling is 'Occasion'.

要使用此範本,我們可以按如下方式修改format_input函式:

def format_input(entry):
    instruction_text = (
        f"<|user|>\n{entry['instruction']}"
    )
    input_text = f"\n{entry['input']}" if entry["input"] else ""
    return instruction_text + input_text

最後,我們還需要更新收集測試集回應時提取生成的回應的方式。

使用Phi-3範本微調模型大約快了17%,因為它導致了更短的模型輸入。得分接近50,與之前使用Alpaca樣式提示獲得的分數差不多。

內容解密:

這段程式碼修改了format_input函式以使用Phi-3提示格式。它還更新了收集測試集回應時提取生成的回應的方式。使用Phi-3範本微調模型導致了更短的模型輸入和相似的分數。

練習7.2

要像圖7.13中所示那樣遮蔽指令,我們需要對InstructionDataset類別和custom_collate_fn函式進行一些修改。我們可以修改InstructionDataset類別以收集指令的長度。