深度學習模型的訓練,特別是大語言模型,需要仔細調整各種引數和技巧才能達到最佳效能。本篇文章將深入探討如何修改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_texts
和 instruction_lengths
列表,用於儲存編碼後的資料和指令長度。然後,我們定義了 format_input
方法,用於格式化輸入資料為字串。
在 __getitem__
方法中,我們傳回了指令長度和編碼後的資料。在 __len__
方法中,我們傳回了資料集的長度。
在 custom_collate_fn
函式中,我們首先計算了批次中的最大長度。然後,我們建立了 inputs_lst
和 targets_lst
列表,用於儲存填充後的輸入和目標資料。對於每個批次中的資料,我們進行了填充和遮罩,傳回填充後的輸入和目標資料。
最後,我們建立了一個範例使用,展示瞭如何使用 InstructionDataset
和 custom_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[最終結果]
內容解密:
在上述程式碼中,indices
和 targets
是兩個關鍵變數。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 個節點
計算引數數量
要計算這個神經網路中的引數數量,我們可以使用以下公式:
- 第一隱藏層:2(輸入節點)× 30(隱藏節點)+ 30(偏置項)= 60 + 30 = 90
- 第二隱藏層:30(來自第一隱藏層的節點)× 20(隱藏節點)+ 20(偏置項)= 600 + 20 = 620
- 輸出層: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 =...
內容解密:
在上述程式碼中,我們首先匯入了必要的模組,包括 torch
和 GPTModel
。然後,我們定義了模型組態 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微調以及學習率調整策略等一系列技術。透過修改InstructionDataset
與custom_collate_fn
,我們可以更精確地控制指令遮蔽的範圍,提升模型訓練效果。雖然指令遮蔽方法在Ollama Llama 3模型上的評估結果略遜於原始模型,但與先前觀察一致,這顯示了在特定模型架構下,指令遮蔽策略仍需進一步調整。LoRA微調技術的引入,有效降低了模型訓練的引數量和時間成本,成為大語言模型訓練的利器。此外,學習率預熱和餘弦衰減策略的應用,有效平衡了模型訓練初期的穩定性與後期的收斂速度。對於Alpaca等大型資料集,GPU加速訓練和調整批次大小或最大長度等引數,是應對記憶體溢位錯誤的有效策略。展望未來,隨著模型架構和訓練技術的持續發展,我們預見更精細的指令遮蔽策略、更高效的微調技術和更智慧的學習率調整策略將持續湧現,推動大語言模型的訓練效能不斷提升。對於追求極致效能的開發者而言,持續關注這些技術的發展趨勢至關重要。