深度學習模型的訓練,特別是大語言模型,需要仔細調整各種引數和技巧才能達到最佳效能。本篇文章將深入探討如何修改InstructionDataset類別和自定義collate_fn函式來實作指令遮蔽,並結合學習率預熱、餘弦衰減等策略最佳化訓練過程。同時,我們也將探討 LoRA 微調的應用,以及如何有效處理批次資料和在 GPU 上加速訓練。這些技術的整合運用,能有效提升模型在自然語言處理任務中的表現,並降低訓練成本。

課程作業7.2解答:修改InstructionDataset類別和自定義拼接函式

為了實作如圖7.13所示的指令遮蔽,我們需要對InstructionDataset類別和custom_collate_fn函式進行輕微的修改。首先,我們修改InstructionDataset類別以收集指令的長度,這些長度將在collate函式中用於定位目標中的指令內容位置。

修改InstructionDataset類別

class InstructionDataset(Dataset):
    def __init__(self, data, tokenizer):
        self.data = data
        self.instruction_lengths = []
        for entry in data:
            # 計算指令的長度
            instruction_length = len(tokenizer.encode(entry['instruction'], return_tensors='pt')[0])
            self.instruction_lengths.append(instruction_length)

修改custom_collate_fn函式

接下來,我們需要修改custom_collate_fn函式,以便它能夠根據指令的長度來遮蔽指令內容。這個函式將會被用於資料載入器(DataLoader),以便在批次處理期間正確地拼接和遮蔽指令。

def custom_collate_fn(batch):
    # 取得批次中的所有資料
    inputs = [entry['input_ids'] for entry in batch]
    attention_masks = [entry['attention_mask'] for entry in batch]
    labels = [entry['labels'] for entry in batch]
    instruction_lengths = [entry['instruction_length'] for entry in batch]
    
    # 將指令內容遮蔽
    for i, length in enumerate(instruction_lengths):
        labels[i][:length] = -100  # 使用-100表示遮蔽
    
    # 將資料轉換為張量
    inputs = torch.tensor(inputs)
    attention_masks = torch.tensor(attention_masks)
    labels = torch.tensor(labels)
    
    return {
        'input_ids': inputs,
        'attention_mask': attention_masks,
        'labels': labels
    }

圖表翻譯:InstructionDataset類別和custom_collate_fn函式

此圖示展示瞭如何修改InstructionDataset類別和custom_collate_fn函式,以實作指令遮蔽。首先,InstructionDataset類別會計算每個指令的長度,並將其儲存為instruction_lengths列表。然後,custom_collate_fn函式會使用這些長度來遮蔽每個批次中的指令內容。最終,批次資料會被轉換為張量,準備進行模型訓練。

內容解密:修改InstructionDataset類別和custom_collate_fn函式

這段程式碼展示瞭如何修改InstructionDataset類別和custom_collate_fn函式,以實作指令遮蔽。首先,InstructionDataset類別會計算每個指令的長度,並將其儲存為instruction_lengths列表。然後,custom_collate_fn函式會使用這些長度來遮蔽每個批次中的指令內容。最終,批次資料會被轉換為張量,準備進行模型訓練。

  flowchart TD
    A[初始化InstructionDataset] --> B[計算指令長度]
    B --> C[儲存指令長度]
    C --> D[修改custom_collate_fn]
    D --> E[遮蔽指令內容]
    E --> F[轉換資料為張量]
    F --> G[傳回批次資料]

程式碼重構:自訂資料集與批次處理

import torch
from torch.utils.data import Dataset, DataLoader

class InstructionDataset(Dataset):
    def __init__(self, data, tokenizer):
        self.data = data
        self.tokenizer = tokenizer
        self.encoded_texts = []
        self.instruction_lengths = []
        
        for entry in data:
            instruction_plus_input = self.format_input(entry)
            response_text = f"\n\n### Response:\n{entry['output']}"
            full_text = instruction_plus_input + response_text
            
            self.encoded_texts.append(self.tokenizer.encode(full_text))
            
            instruction_length = len(self.tokenizer.encode(instruction_plus_input))
            self.instruction_lengths.append(instruction_length)
    
    def format_input(self, entry):
        # 將輸入資料格式化為字串
        return f"### Input:\n{entry['input']}"
    
    def __getitem__(self, index):
        return self.instruction_lengths[index], self.encoded_texts[index]
    
    def __len__(self):
        return len(self.data)

def custom_collate_fn(batch, pad_token_id=50256, ignore_index=-100, allowed_max_length=None, device="cpu"):
    """
    自訂批次處理函式。
    
    將批次中的資料進行填充和遮罩,傳回填充後的輸入和目標資料。
    """
    batch_max_length = max(len(item[1]) + 1 for item in batch)
    inputs_lst, targets_lst = [], []
    
    for instruction_length, item in batch:
        new_item = item + [pad_token_id] * (batch_max_length - len(item))
        inputs_lst.append(new_item)
        
        # 遮罩對應的指令程式碼在目標ID列表中
        target_ids = new_item.copy()
        for i in range(instruction_length):
            target_ids[i] = ignore_index
        
        targets_lst.append(target_ids)
    
    inputs_tensor = torch.tensor(inputs_lst, device=device)
    targets_tensor = torch.tensor(targets_lst, device=device)
    
    return inputs_tensor, targets_tensor

# 範例使用
data = [
    {"input": "這是輸入資料", "output": "這是輸出資料"},
    #...
]

tokenizer = torch.hub.load('huggingface/pytorch-transformers', 'tokenizer', 't5-base')
dataset = InstructionDataset(data, tokenizer)
batch_size = 32
dataloader = DataLoader(dataset, batch_size=batch_size, collate_fn=custom_collate_fn)

for batch in dataloader:
    inputs, targets = batch
    print(inputs.shape, targets.shape)

內容解密:

以上程式碼定義了一個自訂資料集 InstructionDataset,用於儲存和編碼指令和輸出資料。custom_collate_fn 函式則負責將批次中的資料進行填充和遮罩,以便於模型訓練。

InstructionDataset 中,我們首先初始化了 encoded_textsinstruction_lengths 列表,用於儲存編碼後的資料和指令長度。然後,我們定義了 format_input 方法,用於格式化輸入資料為字串。

__getitem__ 方法中,我們傳回了指令長度和編碼後的資料。在 __len__ 方法中,我們傳回了資料集的長度。

custom_collate_fn 函式中,我們首先計算了批次中的最大長度。然後,我們建立了 inputs_lsttargets_lst 列表,用於儲存填充後的輸入和目標資料。對於每個批次中的資料,我們進行了填充和遮罩,傳回填充後的輸入和目標資料。

最後,我們建立了一個範例使用,展示瞭如何使用 InstructionDatasetcustom_collate_fn 函式進行批次處理。

處理批次資料的填充和遮罩

在進行自然語言處理任務時,批次資料的長度可能會有所不同。為了方便計算和避免不同長度的序列之間的混淆,我們需要對批次資料進行填充和遮罩。

填充批次資料

首先,我們需要將批次資料中的每個序列填充到相同的長度。這可以透過在序列末尾新增填充token來實作。以下是填充批次資料的示例:

padded = (
    new_item + [pad_token_id] * (batch_max_length - len(new_item))
)

在這個示例中,new_item是需要填充的序列,pad_token_id是填充token的ID,batch_max_length是批次資料中序列的最大長度。

建立輸入和目標張量

接下來,我們需要建立輸入和目標張量。輸入張量是批次資料中每個序列除最後一個token外的所有token,目標張量是批次資料中每個序列除第一個token外的所有token。以下是建立輸入和目標張量的示例:

inputs = torch.tensor(padded[:-1])
targets = torch.tensor(padded[1:])

在這個示例中,padded是填充後的批次資料,inputs是輸入張量,targets是目標張量。

建立遮罩

為了避免填充token對模型訓練的影響,我們需要建立一個遮罩來忽略填充token。以下是建立遮罩的示例:

mask = targets == pad_token_id

在這個示例中,mask是遮罩,targets是目標張量,pad_token_id是填充token的ID。

收集指令長度

在某些情況下,我們需要收集指令長度以便於後續的處理。以下是收集指令長度的示例:

instruction_lengths = []
for instruction in batch:
    instruction_lengths.append(len(instruction))

在這個示例中,batch是批次資料,instruction_lengths是指令長度列表。

傳回指令長度和文字

最終,我們需要傳回指令長度和文字。以下是傳回指令長度和文字的示例:

return instruction_lengths, texts

在這個示例中,instruction_lengths是指令長度列表,texts是文字列表。

圖表翻譯:

  graph LR
    A[批次資料] --> B[填充]
    B --> C[建立輸入和目標張量]
    C --> D[建立遮罩]
    D --> E[收集指令長度]
    E --> F[傳回指令長度和文字]

在這個圖表中,我們可以看到批次資料的處理流程,從填充到傳回指令長度和文字。

指令遮罩方法的實作與評估

在模型微調中,指令遮罩是一種重要的技術,透過遮罩某些輸入和指令程式碼來改善模型的效能。下面是實作指令遮罩的關鍵步驟:

if indices.numel() > 1:
    targets[indices[1:]] = ignore_index
    targets[:instruction_length-1] = -100

在這段程式碼中,indices 是一個包含指令索引的張量,而 targets 是模型的目標輸出。當 indices 的元素數量大於 1 時,程式碼會遮罩指令程式碼和輸入,將 targets 中對應的元素設為 ignore_index-100

此外,程式碼還對輸入和目標進行了截斷,確保其長度不超過允許的最大長度:

if allowed_max_length is not None:
    inputs = inputs[:allowed_max_length]
    targets = targets[:allowed_max_length]

這些步驟對於模型的微調和評估至關重要。

評估結果

使用指令遮罩方法微調的模型在評估中表現略遜於原始模型,大約低 4 個點(使用 Ollama Llama 3 方法)。這與之前的觀察結果一致。

Alpaca 資料集

Alpaca 資料集包含 52,000 個條目,比之前的資料集大 50 倍。這些條目的長度也比之前的資料集長。由於資料集的大小和複雜性,強烈建議在 GPU 上進行訓練。如果遇到記憶體溢位錯誤,可以考慮降低批次大小或允許的最大長度。

LoRA 微調

要使用 LoRA 進行指令微調,可以使用附錄 E 中的相關類別和函式:

from appendix_E import LoRALayer, LinearWithLoRA, replace_linear_with_lora

這些類別和函式提供了一種有效的方法來實作 LoRA 微調。

圖表翻譯:

  graph LR
    A[指令遮罩] --> B[模型微調]
    B --> C[評估]
    C --> D[結果分析]
    D --> E[LoRA 微調]
    E --> F[最終結果]

內容解密:

在上述程式碼中,indicestargets 是兩個關鍵變數。indices 用於儲存指令索引,而 targets 用於儲存模型的目標輸出。透過遮罩某些輸入和指令程式碼,可以改善模型的效能。

此外,程式碼還對輸入和目標進行了截斷,確保其長度不超過允許的最大長度。這些步驟對於模型的微調和評估至關重要。

在評估中,使用指令遮罩方法微調的模型表現略遜於原始模型,大約低 4 個點。這與之前的觀察結果一致。

Alpaca 資料集包含 52,000 個條目,比之前的資料集大 50 倍。這些條目的長度也比之前的資料集長。由於資料集的大小和複雜性,強烈建議在 GPU 上進行訓練。如果遇到記憶體溢位錯誤,可以考慮降低批次大小或允許的最大長度。

最後,LoRA 微調提供了一種有效的方法來實作指令微調。透過使用附錄 E 中的相關類別和函式,可以輕鬆地實作 LoRA 微調。

使用LoRA進行模型微調

在進行模型微調時,使用LoRA(Low-Rank Adaptation)可以有效地減少可訓練引數的數量,從而加速訓練過程。以下是使用LoRA進行模型微調的步驟:

首先,載入預先訓練好的模型,並計算模型中可訓練引數的總數。

total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters before: {total_params:,}")

接下來,將模型中所有引數的requires_grad屬性設為False,然後再次計算可訓練引數的總數。

for param in model.parameters():
    param.requires_grad = False
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable parameters after: {total_params:,}")

然後,使用LoRA替換線性層,並計算新的可訓練引數的總數。

replace_linear_with_lora(model, rank=16, alpha=16)
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Total trainable LoRA parameters: {total_params:,}")

最後,將模型移到指定的裝置(例如GPU)上,並進行微調。

model.to(device)

在實驗中,我們觀察到使用LoRA進行微調可以節省約28%的訓練時間。同時,評估指標也表明LoRA模型的效能與原始模型相當。

神經網路引數計算

給定一個神經網路,其結構為兩個輸入、兩個輸出和兩個隱藏層(分別包含30和20個節點),我們可以透過以下程式碼計算其可訓練引數的總數:

model = NeuralNetwork(2, 2)
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("Total number of trainable model parameters:", num_params)

此外,我們也可以手動計算引數的總數:

  • 第一隱藏層:2個輸入×30個隱藏單元+30個偏置單元
  • 第二隱藏層:30個輸入×20個節點+20個偏置單元
  • 輸出層:20個輸入×2個輸出+2個偏置單元 將每層的引數加起來,可以得到752個可訓練引數。

矩陣乘法效能比較

在使用Google Colab例項連線到V100 GPU的情況下,我們可以比較CPU和GPU上矩陣乘法的效能。

a = torch.rand(100, 200)
b = torch.rand(200, 300)

%timeit a@b

在CPU上,結果為63.8 μs ± 8.7 μs per loop。在GPU上,結果為:

a, b = a.to("cuda"), b.to("cuda")
%timeit a @ b

顯示出GPU上的矩陣乘法效能明顯優於CPU。

神經網路引數計算與GPU加速

在神經網路中,計算模型引數的數量是一項重要的工作。下面,我們將透過一個具體的例子來演示如何計算神經網路中的引數數量。

神經網路結構

假設我們有一個神經網路,其結構如下:

  • 輸入層:2 個節點
  • 隱藏層1:30 個節點
  • 隱藏層2:20 個節點
  • 輸出層:2 個節點

計算引數數量

要計算這個神經網路中的引數數量,我們可以使用以下公式:

  1. 第一隱藏層:2(輸入節點)× 30(隱藏節點)+ 30(偏置項)= 60 + 30 = 90
  2. 第二隱藏層:30(來自第一隱藏層的節點)× 20(隱藏節點)+ 20(偏置項)= 600 + 20 = 620
  3. 輸出層:20(來自第二隱藏層的節點)× 2(輸出節點)+ 2(偏置項)= 40 + 2 = 42

將這些引數加起來,我們得到:

90 + 620 + 42 = 752

因此,這個神經網路中總共有752個可訓練的引數。

使用Python計算引數數量

我們也可以使用Python來計算這個神經網路中的引數數量。以下是示例程式碼:

import torch
import torch.nn as nn

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.fc1 = nn.Linear(2, 30)  # 第一隱藏層
        self.fc2 = nn.Linear(30, 20)  # 第二隱藏層
        self.fc3 = nn.Linear(20, 2)  # 輸出層

    def forward(self, x):
        x = torch.relu(self.fc1(x))  # 啟用第一隱藏層
        x = torch.relu(self.fc2(x))  # 啟用第二隱藏層
        x = self.fc3(x)
        return x

model = NeuralNetwork()
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("總共有{}個可訓練的模型引數".format(num_params))

執行這段程式碼後,我們會得到相同的結果:752。

GPU加速

GPU(圖形處理器)可以大大加速深度學習任務的執行速度。下面,我們將比較在CPU和GPU上執行矩陣乘法的速度。

import torch

# 在CPU上建立兩個隨機矩陣
a = torch.rand(100, 200)
b = torch.rand(200, 300)

# 在CPU上執行矩陣乘法
%timeit a @ b

# 將矩陣移至GPU
a, b = a.to("cuda"), b.to("cuda")

# 在GPU上執行矩陣乘法
%timeit a @ b

在我的實驗中,使用GPU(V100)可以將矩陣乘法的執行時間從63.8 μs減少到13.8 μs,約快了4倍。

這些結果表明,使用GPU可以大大加速深度學習任務的執行速度,從而提高開發效率。

最佳化訓練迴圈

在本文中,我們將對第 5 至 7 章中介紹的預訓練和微調訓練過程進行最佳化。具體來說,我們將實作學習率預熱(learning rate warmup)、餘弦衰減(cosine decay)和梯度裁剪(gradient clipping)。然後,我們將這些技術整合到訓練函式中,並使用它們來預訓練一個大語言模型(LLM)。

首先,為了使程式碼自成體系,我們重新初始化了第 5 章中訓練的模型:

import torch
from chapter04 import GPTModel

GPT_CONFIG_124M = {
    "vocab_size": 50257,
    "context_length": 256,
    "emb_dim": 768,
    "n_heads": 12,
    "n_layers": 12,
    "drop_rate": 0.1,
    "qkv_bias": False
}

torch.manual_seed(123)

model = GPTModel(GPT_CONFIG_124M)
model.to(device)
model.eval()

初始化模型後,我們需要初始化資料載入器。首先,我們載入短篇故事「判決」:

# 載入資料
data =...

內容解密:

在上述程式碼中,我們首先匯入了必要的模組,包括 torchGPTModel。然後,我們定義了模型組態 GPT_CONFIG_124M,其中包括了詞彙表大小、上下文長度、嵌入維度、注意力頭數、層數、丟棄率和 QKV 偏差等引數。

接下來,我們設定了隨機種子,以確保程式碼的可重現性。然後,我們初始化了 GPTModel 例項,並將其轉移到指定裝置(例如 GPU)上。最後,我們將模型設定為評估模式。

圖表翻譯:

以下是模型架構的 Mermaid 圖表:

  graph LR
    A[輸入] --> B[嵌入層]
    B --> C[編碼器]
    C --> D[解碼器]
    D --> E[輸出]

在這個圖表中,輸入首先經過嵌入層,然後透過編碼器和解碼器,最終產生輸出。

圖表解釋:

這個圖表展示了模型的基本架構。輸入首先被嵌入到高維空間中,然後透過多層編碼器和解碼器,最終產生輸出。這個過程涉及到多個注意力機制和全連線層,以實作語言模型的功能。

程式碼實作:

以下是實作學習率預熱和餘弦衰減的程式碼:

import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

# 定義學習率預熱函式
def warmup_schedule(optimizer, num_warmup_steps):
    def lr_lambda(current_step):
        if current_step < num_warmup_steps:
            return float(current_step) / float(max(1, num_warmup_steps))
        return 1.0

    return optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)

# 定義餘弦衰減函式
def cosine_decay_schedule(optimizer, num_training_steps):
    def lr_lambda(current_step):
        return 0.5 * (1 + math.cos(math.pi * current_step / num_training_steps))

    return optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)

# 初始化最佳化器和學習率預熱和餘弦衰減函式
optimizer = optim.Adam(model.parameters(), lr=1e-4)
warmup_scheduler = warmup_schedule(optimizer, num_warmup_steps=1000)
cosine_decay_scheduler = cosine_decay_schedule(optimizer, num_training_steps=10000)

# 訓練模型
for epoch in range(10):
    for batch in train_dataloader:
        # 前向傳播
        inputs, labels = batch
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = model(inputs)

        # 反向傳播
        loss = criterion(outputs, labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # 更新學習率
        warmup_scheduler.step()
        cosine_decay_scheduler.step()

在這個程式碼中,我們定義了學習率預熱和餘弦衰減函式,然後初始化最佳化器和學習率預熱和餘弦衰減函式。最後,我們訓練模型,並在每個 epoch 中更新學習率。

圖表翻譯:

以下是學習率預熱和餘弦衰減的 Mermaid 圖表:

  graph LR
    A[學習率] --> B[預熱]
    B --> C[餘弦衰減]
    C --> D[最終學習率]

在這個圖表中,學習率首先經過預熱階段,然後進入餘弦衰減階段,最終產生最終學習率。

圖表解釋:

這個圖表展示了學習率預熱和餘弦衰減的過程。學習率首先在預熱階段中線性增加,然後進入餘弦衰減階段,在這個階段中學習率按照餘弦函式衰減,最終產生最終學習率。這個過程可以幫助模型更好地收斂。

Dropout 率與查詢-金鑰-值偏差

在深度學習模型中,Dropout 率是一個重要的超引數,用於控制模型中神經元的啟用比例。然而,在某些情況下,Dropout 率可能會對模型的效能產生負面影響。另一方面,查詢-金鑰-值偏差(Query-key-value bias)是一種常見的問題,尤其是在自然語言處理任務中。

載入文字資料

為了演示如何解決這些問題,我們首先需要載入一些文字資料。假設我們有一個名為 the-verdict.txt 的檔案,包含了我們想要處理的文字資料。以下是載入文字資料的程式碼:

import os
import urllib.request

file_path = "the-verdict.txt"
url = "main/ch02/01_main-chapter-code/the-verdict.txt"

if not os.path.exists(file_path):
    with urllib.request.urlopen(url) as response:
        text_data = response.read().decode('utf-8')
    with open(file_path, "w", encoding="utf-8") as file:
        file.write(text_data)
else:
    with open(file_path, "r", encoding="utf-8") as file:
        text_data = file.read()

建立資料載入器

接下來,我們需要建立一個資料載入器(Data Loader),用於將文字資料分割成訓練集和測試集。以下是建立資料載入器的程式碼:

from previous_chapters import create_dataloader_v1

train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))

torch.manual_seed(123)

train_loader = create_dataloader_v1(
    text_data[:split_idx],
    batch_size=2,
    max_length=GPT_CONFIG_124M["context_length"],
    stride=GPT_CONFIG_124M["context_length"],
    drop_last=True,
    shuffle=True,
    num_workers=0
)

在這個例子中,我們使用 create_dataloader_v1 函式建立一個資料載入器,該函式接受文字資料、批次大小、最大長度、步長、是否丟棄最後一個批次、是否打亂資料順序和工作執行緒數等引數。

內容解密:

  • create_dataloader_v1 函式是用於建立資料載入器的,它接受多個引數,包括文字資料、批次大小、最大長度、步長、是否丟棄最後一個批次、是否打亂資料順序和工作執行緒數等。
  • train_ratio 變數用於控制訓練集和測試集的比例,在這個例子中,訓練集佔總資料的 90%。
  • split_idx 變數用於計算訓練集和測試集的分界點。
  • torch.manual_seed(123) 用於設定 PyTorch 的隨機種子,以確保結果的一致性。
  • train_loader 變數是建立的資料載入器,它可以用於將文字資料分割成訓練集和測試集。

圖表翻譯:

  flowchart TD
    A[載入文字資料] --> B[建立資料載入器]
    B --> C[分割訓練集和測試集]
    C --> D[設定 PyTorch 隨機種子]
    D --> E[建立訓練資料載入器]

在這個流程圖中,我們展示了載入文字資料、建立資料載入器、分割訓練集和測試集、設定 PyTorch 隨機種子和建立訓練資料載入器的過程。

學習率預熱(Learning Rate Warmup)

在訓練複雜模型如大語言模型(LLM)時,實施學習率預熱可以穩定訓練過程。這個過程涉及從一個非常低的初始學習率(initial_lr)逐漸增加到一個最大值(peak_lr),這個最大值是由玄貓指定的。以較小的權重更新開始訓練,可以降低模型在訓練初期遇到大幅度、不穩定的更新的風險。

實施學習率預熱

假設我們計劃訓練一個LLM 15個epoch,初始學習率為0.0001,最大學習率為0.01:

n_epochs = 15  # 訓練epoch數
initial_lr = 0.0001  # 初始學習率
peak_lr = 0.01  # 最大學習率
warmup_steps = 20  # 預熱步驟數

預熱步驟數通常設定為總步驟數的0.1%至20%之間,我們可以如下計算:

total_steps = len(train_loader) * n_epochs  # 總步驟數
warmup_steps = int(0.2 * total_steps)  # 預熱步驟數
print(warmup_steps)

這將輸出27,意味著我們有20個預熱步驟來將初始學習率從0.0001增加到0.01,在前27個訓練步驟中。

接下來,我們實施一個簡單的訓練迴圈範本來展示這個預熱過程:

optimizer = torch.optim.AdamW(model.parameters(), weight_decay=0.1)  # 最佳化器
lr_increment = (peak_lr - initial_lr) / warmup_steps  # 學習率增量

global_step = -1  # 全域性步驟數
track_lrs = []  # 跟蹤學習率

for epoch in range(n_epochs):  # 迭代epoch
    for input_batch, target_batch in train_loader:  # 迭代批次
        optimizer.zero_grad()  # 清空梯度
        
        global_step += 1  # 更新全域性步驟數
        
        #... (計算損失、反向傳播等)
        
        # 更新學習率(簡化示例)
        if global_step < warmup_steps:
            current_lr = initial_lr + lr_increment * global_step
            for param_group in optimizer.param_groups:
                param_group['lr'] = current_lr
            track_lrs.append(current_lr)
        
        #... (更新模型引數等)

這個例子展示瞭如何在訓練迴圈中實施學習率預熱,逐步增加學習率直到達到最大值。這樣可以幫助模型更穩定地訓練,特別是在初始階段。

學習率調整與訓練迴圈

在深度學習中,學習率(Learning Rate)是控制模型學習速度的重要引數。一個適當的學習率可以幫助模型更快地收斂到最佳解。下面,我們將探討如何調整學習率以及典型的訓練迴圈。

學習率調整

學習率調整是一種技術,用於在訓練過程中動態調整學習率。這裡,我們使用了一種簡單的線性學習率調整策略。在前 warmup_steps 個步驟中,學習率從 initial_lr 線性增加到 peak_lr。之後,學習率保持不變。

if global_step < warmup_steps:
    lr = initial_lr + global_step * lr_increment
else:
    lr = peak_lr

for param_group in optimizer.param_groups:
    param_group["lr"] = lr

記錄學習率變化

為了視覺化學習率的變化,我們記錄了每一步的學習率。

track_lrs.append(optimizer.param_groups[0]["lr"])

視覺化學習率變化

使用 Matplotlib,我們可以輕鬆地視覺化學習率的變化。

import matplotlib.pyplot as plt

plt.ylabel("Learning rate")
plt.xlabel("Step")
total_training_steps = len(train_loader) * n_epochs
plt.plot(range(total_training_steps), track_lrs);
plt.show()

這個圖表顯示了學習率在訓練過程中的變化,幫助我們瞭解模型如何隨著時間的推移而適應。

20% Warmup

在這個例子中,我們使用了 20% 的 warmup 步驟數。這意味著在前 20% 的訓練步驟中,學習率將線性增加到峰值。

執行典型訓練迴圈

典型的訓練迴圈涉及遍歷訓練資料集的批次,並對每個批次執行前向傳播、損失計算、反向傳播和引數更新。

for epoch in range(n_epochs):
    for batch in train_loader:
        # 前向傳播
        outputs = model(batch)
        loss = criterion(outputs, batch.labels)
        
        # 反向傳播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

這個迴圈將重複執行直到達到指定的 epochs 數量,從而完成模型的訓練。

訓練迴圈的最佳化技巧

在深度學習模型的訓練過程中,訓練迴圈的設計至關重要。一個良好的訓練迴圈可以幫助模型更快地收斂,並提高其效能。在本文中,我們將探討如何最佳化訓練迴圈,以提高模型的訓練效率和效能。

學習率的調整

學習率是訓練迴圈中的一個重要引數,它控制著模型更新的步伐。一個適當的學習率可以幫助模型更快地收斂,並提高其效能。然而,學習率太高或太低都可能對模型的訓練產生負面影響。

為瞭解決這個問題,我們可以使用學習率調整技術。其中一種常用的技術是warmup階段,在這個階段中,學習率從一個低值開始,逐漸增加到最大值。這樣可以幫助模型在初期階段更快地收斂,並避免過度更新。

import math

min_lr = 0.1 * initial_lr
track_lrs = []
lr_increment = (peak_lr - initial_lr) / warmup_steps

餘弦衰減

另一種廣泛採用的技術是餘弦衰減(cosine decay)。這種方法在訓練過程中調整學習率,使其遵循餘弦曲線。這樣可以幫助模型在訓練過程中更慢地更新權重,從而避免過度更新,並提高模型的穩定性。

def cosine_decay(lr, step, max_steps):
    return lr * (1 + math.cos(math.pi * step / max_steps)) / 2

訓練迴圈的最佳化

透過以上的技術,我們可以最佳化訓練迴圈,以提高模型的訓練效率和效能。以下是最佳化後的訓練迴圈範例:

for epoch in range(max_epochs):
    for step in range(max_steps):
        # 更新學習率
        lr = cosine_decay(initial_lr, step, max_steps)
        optimizer.lr = lr
        
        # 更新模型
        optimizer.step()
        
        # 記錄學習率
        track_lrs.append(lr)

圖表翻譯:

以下是學習率變化的圖表:

  flowchart TD
    A[初始學習率] --> B[warmup階段]
    B --> C[餘弦衰減]
    C --> D[學習率衰減]
    D --> E[最終學習率]

這個圖表展示了學習率在訓練過程中的變化,從初始學習率開始,經過warmup階段和餘弦衰減,最終衰減到一個較低的值。

內容解密:

在上面的範例中,我們使用了餘弦衰減技術來調整學習率。這種方法可以幫助模型在訓練過程中更慢地更新權重,從而避免過度更新,並提高模型的穩定性。同時,我們也記錄了學習率的變化,以便於之後的分析和最佳化。

學習率預熱(Learning Rate Warmup)技術

在深度學習中,學習率預熱是一種常用的技術,用於在訓練初期逐漸增加學習率,以避免模型引數發生劇烈的更新。這種方法可以提高模型的穩定性和收斂速度。

學習率預熱的實作

以下是學習率預熱的實作過程:

global_step = -1

for epoch in range(n_epochs):
    for input_batch, target_batch in train_loader:
        optimizer.zero_grad()
        
        global_step += 1
        
        if global_step < warmup_steps:
            # 預熱階段,學習率線性增加
            lr = initial_lr + global_step * lr_increment
        else:
            # 預熱階段結束,學習率保持不變
            progress = ((global_step - warmup_steps) / 
                       (total_training_steps - warmup_steps))
            lr = initial_lr + (max_lr - initial_lr) * progress
            
        # 更新學習率
        optimizer.param_groups[0]['lr'] = lr

在上述程式碼中,warmup_steps 是預熱階段的步數,initial_lr 是初始學習率,lr_increment 是學習率的增量,max_lr 是最大學習率。

學習率預熱的優點

學習率預熱有以下優點:

  • 提高模型的穩定性:透過逐漸增加學習率,可以避免模型引數發生劇烈的更新,從而提高模型的穩定性。
  • 加速模型的收斂:學習率預熱可以加速模型的收斂速度,因為它可以讓模型在初期就開始學習到有用的特徵。

圖表翻譯:

  flowchart TD
    A[開始] --> B[初始化學習率]
    B --> C[預熱階段]
    C --> D[學習率線性增加]
    D --> E[預熱階段結束]
    E --> F[學習率保持不變]
    F --> G[更新模型引數]

上述流程圖展示了學習率預熱的過程。首先,初始化學習率,然後進入預熱階段,在預熱階段中,學習率線性增加,直到預熱階段結束。最後,學習率保持不變,並更新模型引數。

從模型訓練效能最佳化的角度來看,本文探討了修改InstructionDataset類別、自定義拼接函式、指令遮蔽、LoRA微調以及學習率調整策略等一系列技術。透過修改InstructionDatasetcustom_collate_fn,我們可以更精確地控制指令遮蔽的範圍,提升模型訓練效果。雖然指令遮蔽方法在Ollama Llama 3模型上的評估結果略遜於原始模型,但與先前觀察一致,這顯示了在特定模型架構下,指令遮蔽策略仍需進一步調整。LoRA微調技術的引入,有效降低了模型訓練的引數量和時間成本,成為大語言模型訓練的利器。此外,學習率預熱和餘弦衰減策略的應用,有效平衡了模型訓練初期的穩定性與後期的收斂速度。對於Alpaca等大型資料集,GPU加速訓練和調整批次大小或最大長度等引數,是應對記憶體溢位錯誤的有效策略。展望未來,隨著模型架構和訓練技術的持續發展,我們預見更精細的指令遮蔽策略、更高效的微調技術和更智慧的學習率調整策略將持續湧現,推動大語言模型的訓練效能不斷提升。對於追求極致效能的開發者而言,持續關注這些技術的發展趨勢至關重要。