6. Vooropleiding & Laai modelle

Reading time: 25 minutes

{{#include /banners/hacktricks-training.md}}

Teksgenerasie

Om 'n model op te lei, moet ons hê dat daardie model nuwe tokens kan genereer. Dan sal ons die gegenereerde tokens vergelyk met die verwagte om die model te leer om die tokens wat dit moet genereer te leer.

Soos in die vorige voorbeelde het ons reeds 'n paar tokens voorspel, dit is moontlik om daardie funksie vir hierdie doel te hergebruik.

tip

Die doel van hierdie sesde fase is baie eenvoudig: Oplei die model van nuuts af. Hiervoor sal die vorige LLM-argitektuur gebruik word met 'n paar lusse wat oor die datastelle gaan met behulp van die gedefinieerde verliesfunksies en optimizer om al die parameters van die model op te lei.

Teksevaluasie

Om 'n korrekte opleiding uit te voer, is dit nodig om die voorspellings wat vir die verwagte token verkry is, te meet. Die doel van die opleiding is om die waarskynlikheid van die korrekte token te maksimeer, wat behels dat sy waarskynlikheid relatief tot ander tokens verhoog word.

Om die waarskynlikheid van die korrekte token te maksimeer, moet die gewigte van die model aangepas word sodat daardie waarskynlikheid gemaksimeer word. Die opdaterings van die gewigte word gedoen deur middel van terugpropagasie. Dit vereis 'n verliesfunksie om te maksimeer. In hierdie geval sal die funksie die verskil tussen die uitgevoerde voorspelling en die gewenste een wees.

Echter, in plaas daarvan om met die rou voorspellings te werk, sal dit met 'n logaritme met basis n werk. So as die huidige voorspelling van die verwagte token 7.4541e-05 was, is die natuurlike logaritme (basis e) van 7.4541e-05 ongeveer -9.5042.
Dan, vir elke invoer met 'n kontekslengte van 5 tokens, sal die model 5 tokens moet voorspel, met die eerste 4 tokens die laaste een van die invoer en die vyfde die voorspelde een. Daarom sal ons vir elke invoer 5 voorspellings hê in daardie geval (selfs al was die eerste 4 in die invoer, weet die model nie hiervan nie) met 5 verwagte tokens en dus 5 waarskynlikhede om te maksimeer.

Daarom, nadat die natuurlike logaritme op elke voorspelling uitgevoer is, word die gemiddelde bereken, die minus simbool verwyder (dit word kruisentropieverlies genoem) en dit is die nommer om so naby aan 0 as moontlik te verminder omdat die natuurlike logaritme van 1 0 is:

https://camo.githubusercontent.com/3c0ab9c55cefa10b667f1014b6c42df901fa330bb2bc9cea88885e784daec8ba/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830355f636f6d707265737365642f63726f73732d656e74726f70792e776562703f313233

Nog 'n manier om te meet hoe goed die model is, word perplexity genoem. Perplexity is 'n maatstaf wat gebruik word om te evalueer hoe goed 'n waarskynlikheidsmodel 'n monster voorspel. In taalmodellering verteenwoordig dit die model se onsekerheid wanneer dit die volgende token in 'n reeks voorspel.
Byvoorbeeld, 'n perplexity waarde van 48725 beteken dat wanneer dit nodig is om 'n token te voorspel, dit onseker is oor watter een van die 48,725 tokens in die woordeskat die regte een is.

Voorbeeld van Vooropleiding

Dit is die aanvanklike kode wat voorgestel word in https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb soms effens gewysig

Vorige kode wat hier gebruik is maar reeds in vorige afdelings verduidelik
python
"""
This is code explained before so it won't be exaplained
"""

import tiktoken
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader


class GPTDatasetV1(Dataset):
def __init__(self, txt, tokenizer, max_length, stride):
self.input_ids = []
self.target_ids = []

# Tokenize the entire text
token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})

# Use a sliding window to chunk the book into overlapping sequences of max_length
for i in range(0, len(token_ids) - max_length, stride):
input_chunk = token_ids[i:i + max_length]
target_chunk = token_ids[i + 1: i + max_length + 1]
self.input_ids.append(torch.tensor(input_chunk))
self.target_ids.append(torch.tensor(target_chunk))

def __len__(self):
return len(self.input_ids)

def __getitem__(self, idx):
return self.input_ids[idx], self.target_ids[idx]


def create_dataloader_v1(txt, batch_size=4, max_length=256,
stride=128, shuffle=True, drop_last=True, num_workers=0):
# Initialize the tokenizer
tokenizer = tiktoken.get_encoding("gpt2")

# Create dataset
dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)

# Create dataloader
dataloader = DataLoader(
dataset, batch_size=batch_size, shuffle=shuffle, drop_last=drop_last, num_workers=num_workers)

return dataloader


class MultiHeadAttention(nn.Module):
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
super().__init__()
assert d_out % num_heads == 0, "d_out must be divisible by n_heads"

self.d_out = d_out
self.num_heads = num_heads
self.head_dim = d_out // num_heads  # Reduce the projection dim to match desired output dim

self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.out_proj = nn.Linear(d_out, d_out)  # Linear layer to combine head outputs
self.dropout = nn.Dropout(dropout)
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))

def forward(self, x):
b, num_tokens, d_in = x.shape

keys = self.W_key(x)  # Shape: (b, num_tokens, d_out)
queries = self.W_query(x)
values = self.W_value(x)

# We implicitly split the matrix by adding a `num_heads` dimension
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)

# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
keys = keys.transpose(1, 2)
queries = queries.transpose(1, 2)
values = values.transpose(1, 2)

# Compute scaled dot-product attention (aka self-attention) with a causal mask
attn_scores = queries @ keys.transpose(2, 3)  # Dot product for each head

# Original mask truncated to the number of tokens and converted to boolean
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]

# Use the mask to fill attention scores
attn_scores.masked_fill_(mask_bool, -torch.inf)

attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights = self.dropout(attn_weights)

# Shape: (b, num_tokens, num_heads, head_dim)
context_vec = (attn_weights @ values).transpose(1, 2)

# Combine heads, where self.d_out = self.num_heads * self.head_dim
context_vec = context_vec.reshape(b, num_tokens, self.d_out)
context_vec = self.out_proj(context_vec)  # optional projection

return context_vec


class LayerNorm(nn.Module):
def __init__(self, emb_dim):
super().__init__()
self.eps = 1e-5
self.scale = nn.Parameter(torch.ones(emb_dim))
self.shift = nn.Parameter(torch.zeros(emb_dim))

def forward(self, x):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
norm_x = (x - mean) / torch.sqrt(var + self.eps)
return self.scale * norm_x + self.shift


class GELU(nn.Module):
def __init__(self):
super().__init__()

def forward(self, x):
return 0.5 * x * (1 + torch.tanh(
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
(x + 0.044715 * torch.pow(x, 3))
))


class FeedForward(nn.Module):
def __init__(self, cfg):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
GELU(),
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
)

def forward(self, x):
return self.layers(x)


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"],
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"])

def forward(self, x):
# Shortcut connection for attention block
shortcut = x
x = self.norm1(x)
x = self.att(x)   # Shape [batch_size, num_tokens, emb_size]
x = self.drop_shortcut(x)
x = x + shortcut  # Add the original input back

# Shortcut connection for feed-forward block
shortcut = x
x = self.norm2(x)
x = self.ff(x)
x = self.drop_shortcut(x)
x = x + shortcut  # Add the original input back

return 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"])

self.trf_blocks = nn.Sequential(
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])])

self.final_norm = LayerNorm(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  # Shape [batch_size, num_tokens, emb_size]
x = self.drop_emb(x)
x = self.trf_blocks(x)
x = self.final_norm(x)
logits = self.out_head(x)
return logits
python
# Download contents to train the data with
import os
import urllib.request

file_path = "the-verdict.txt"
url = "https://raw.githubusercontent.com/rasbt/LLMs-from-scratch/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()

total_characters = len(text_data)
tokenizer = tiktoken.get_encoding("gpt2")
total_tokens = len(tokenizer.encode(text_data))

print("Data downloaded")
print("Characters:", total_characters)
print("Tokens:", total_tokens)

# Model initialization
GPT_CONFIG_124M = {
"vocab_size": 50257,   # Vocabulary size
"context_length": 256, # Shortened context length (orig: 1024)
"emb_dim": 768,        # Embedding dimension
"n_heads": 12,         # Number of attention heads
"n_layers": 12,        # Number of layers
"drop_rate": 0.1,      # Dropout rate
"qkv_bias": False      # Query-key-value bias
}

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.eval()
print ("Model initialized")


# Functions to transform from tokens to ids and from to ids to tokens
def text_to_token_ids(text, tokenizer):
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
return encoded_tensor

def token_ids_to_text(token_ids, tokenizer):
flat = token_ids.squeeze(0) # remove batch dimension
return tokenizer.decode(flat.tolist())



# Define loss functions
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
logits = model(input_batch)
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
return loss


def calc_loss_loader(data_loader, model, device, num_batches=None):
total_loss = 0.
if len(data_loader) == 0:
return float("nan")
elif num_batches is None:
num_batches = len(data_loader)
else:
# Reduce the number of batches to match the total number of batches in the data loader
# if num_batches exceeds the number of batches in the data loader
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
loss = calc_loss_batch(input_batch, target_batch, model, device)
total_loss += loss.item()
else:
break
return total_loss / num_batches


# Apply Train/validation ratio and create dataloaders
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
train_data = text_data[:split_idx]
val_data = text_data[split_idx:]

torch.manual_seed(123)

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

val_loader = create_dataloader_v1(
val_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=False,
shuffle=False,
num_workers=0
)


# Sanity checks
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the training loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"increase the `training_ratio`")

if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the validation loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"decrease the `training_ratio`")

print("Train loader:")
for x, y in train_loader:
print(x.shape, y.shape)

print("\nValidation loader:")
for x, y in val_loader:
print(x.shape, y.shape)

train_tokens = 0
for input_batch, target_batch in train_loader:
train_tokens += input_batch.numel()

val_tokens = 0
for input_batch, target_batch in val_loader:
val_tokens += input_batch.numel()

print("Training tokens:", train_tokens)
print("Validation tokens:", val_tokens)
print("All tokens:", train_tokens + val_tokens)


# Indicate the device to use
if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")

print(f"Using {device} device.")

model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes



# Pre-calculate losses without starting yet
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader

with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
train_loss = calc_loss_loader(train_loader, model, device)
val_loss = calc_loss_loader(val_loader, model, device)

print("Training loss:", train_loss)
print("Validation loss:", val_loss)


# Functions to train the data
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
eval_freq, eval_iter, start_context, tokenizer):
# Initialize lists to track losses and tokens seen
train_losses, val_losses, track_tokens_seen = [], [], []
tokens_seen, global_step = 0, -1

# Main training loop
for epoch in range(num_epochs):
model.train()  # Set model to training mode

for input_batch, target_batch in train_loader:
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
loss = calc_loss_batch(input_batch, target_batch, model, device)
loss.backward() # Calculate loss gradients
optimizer.step() # Update model weights using loss gradients
tokens_seen += input_batch.numel()
global_step += 1

# Optional evaluation step
if global_step % eval_freq == 0:
train_loss, val_loss = evaluate_model(
model, train_loader, val_loader, device, eval_iter)
train_losses.append(train_loss)
val_losses.append(val_loss)
track_tokens_seen.append(tokens_seen)
print(f"Ep {epoch+1} (Step {global_step:06d}): "
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")

# Print a sample text after each epoch
generate_and_print_sample(
model, tokenizer, device, start_context
)

return train_losses, val_losses, track_tokens_seen


def evaluate_model(model, train_loader, val_loader, device, eval_iter):
model.eval()
with torch.no_grad():
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
model.train()
return train_loss, val_loss


def generate_and_print_sample(model, tokenizer, device, start_context):
model.eval()
context_size = model.pos_emb.weight.shape[0]
encoded = text_to_token_ids(start_context, tokenizer).to(device)
with torch.no_grad():
token_ids = generate_text(
model=model, idx=encoded,
max_new_tokens=50, context_size=context_size
)
decoded_text = token_ids_to_text(token_ids, tokenizer)
print(decoded_text.replace("\n", " "))  # Compact print format
model.train()


# Start training!
import time
start_time = time.time()

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)

num_epochs = 10
train_losses, val_losses, tokens_seen = train_model_simple(
model, train_loader, val_loader, optimizer, device,
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
start_context="Every effort moves you", tokenizer=tokenizer
)

end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")



# Show graphics with the training process
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import math
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
fig, ax1 = plt.subplots(figsize=(5, 3))
ax1.plot(epochs_seen, train_losses, label="Training loss")
ax1.plot(
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
)
ax1.set_xlabel("Epochs")
ax1.set_ylabel("Loss")
ax1.legend(loc="upper right")
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax2 = ax1.twiny()
ax2.plot(tokens_seen, train_losses, alpha=0)
ax2.set_xlabel("Tokens seen")
fig.tight_layout()
plt.show()

# Compute perplexity from the loss values
train_ppls = [math.exp(loss) for loss in train_losses]
val_ppls = [math.exp(loss) for loss in val_losses]
# Plot perplexity over tokens seen
plt.figure()
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
plt.xlabel('Tokens Seen')
plt.ylabel('Perplexity')
plt.title('Perplexity over Training')
plt.legend()
plt.show()

epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)


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

Funksies om teks <--> ids te transformeer

Hierdie is 'n paar eenvoudige funksies wat gebruik kan word om van teks uit die woordeskat na ids en omgekeerd te transformeer. Dit is nodig aan die begin van die hantering van die teks en aan die einde van die voorspellings:

python
# Functions to transform from tokens to ids and from to ids to tokens
def text_to_token_ids(text, tokenizer):
encoded = tokenizer.encode(text, allowed_special={'<|endoftext|>'})
encoded_tensor = torch.tensor(encoded).unsqueeze(0) # add batch dimension
return encoded_tensor

def token_ids_to_text(token_ids, tokenizer):
flat = token_ids.squeeze(0) # remove batch dimension
return tokenizer.decode(flat.tolist())

Genereer teks funksies

In 'n vorige afdeling is 'n funksie wat net die meest waarskynlike token gekry het na die logits. Dit sal egter beteken dat vir elke invoer dieselfde uitvoer altyd gegenereer sal word, wat dit baie deterministies maak.

Die volgende generate_text funksie sal die top-k, temperature en multinomial konsepte toepas.

  • Die top-k beteken dat ons sal begin om alle waarskynlikhede van al die tokens na -inf te verminder, behalwe vir die top k tokens. So, as k=3, voordat 'n besluit geneem word, sal slegs die 3 meest waarskynlike tokens 'n waarskynlikheid hê wat verskil van -inf.
  • Die temperature beteken dat elke waarskynlikheid deur die temperatuurwaarde gedeel sal word. 'n Waarde van 0.1 sal die hoogste waarskynlikheid verbeter in vergelyking met die laagste, terwyl 'n temperatuur van 5 byvoorbeeld dit meer plat sal maak. Dit help om die variasie in antwoorde te verbeter wat ons wil hê die LLM moet hê.
  • Na die temperatuur toegepas is, word 'n softmax funksie weer toegepas om te maak dat al die oorblywende tokens 'n totale waarskynlikheid van 1 het.
  • Laastens, in plaas daarvan om die token met die grootste waarskynlikheid te kies, word die funksie multinomial toegepas om die volgende token te voorspel volgens die finale waarskynlikhede. So as token 1 'n 70% waarskynlikheid gehad het, token 2 'n 20% en token 3 'n 10%, sal token 1 70% van die tyd gekies word, token 2 20% van die tyd en token 3 10% van die tyd.
python
# Generate text function
def generate_text(model, idx, max_new_tokens, context_size, temperature=0.0, top_k=None, eos_id=None):

# For-loop is the same as before: Get logits, and only focus on last time step
for _ in range(max_new_tokens):
idx_cond = idx[:, -context_size:]
with torch.no_grad():
logits = model(idx_cond)
logits = logits[:, -1, :]

# New: Filter logits with top_k sampling
if top_k is not None:
# Keep only top_k values
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)

# New: Apply temperature scaling
if temperature > 0.0:
logits = logits / temperature

# Apply softmax to get probabilities
probs = torch.softmax(logits, dim=-1)  # (batch_size, context_len)

# Sample from the distribution
idx_next = torch.multinomial(probs, num_samples=1)  # (batch_size, 1)

# Otherwise same as before: get idx of the vocab entry with the highest logits value
else:
idx_next = torch.argmax(logits, dim=-1, keepdim=True)  # (batch_size, 1)

if idx_next == eos_id:  # Stop generating early if end-of-sequence token is encountered and eos_id is specified
break

# Same as before: append sampled index to the running sequence
idx = torch.cat((idx, idx_next), dim=1)  # (batch_size, num_tokens+1)

return idx

tip

Daar is 'n algemene alternatief vir top-k genoem top-p, ook bekend as nucleus sampling, wat in plaas daarvan om k monsters met die meeste waarskynlikheid te verkry, organiseer dit al die resulterende vokabulaire volgens waarskynlikhede en som dit op van die hoogste waarskynlikheid tot die laagste totdat 'n drempel bereik word.

Dan sal slegs daardie woorde van die vokabulaire oorweeg word volgens hul relatiewe waarskynlikhede.

Dit maak dit moontlik om nie 'n aantal k monsters te kies nie, aangesien die optimale k in elke geval anders kan wees, maar slegs 'n drempel.

Let daarop dat hierdie verbetering nie in die vorige kode ingesluit is nie.

tip

'n Ander manier om die gegenereerde teks te verbeter, is deur Beam search te gebruik in plaas van die greedy search wat in hierdie voorbeeld gebruik word.
Anders as greedy search, wat die mees waarskynlike volgende woord by elke stap kies en 'n enkele reeks bou, hou beam search die top 𝑘 k hoogste-telling gedeeltelike reekse (genoem "beams") by elke stap dop. Deur verskeie moontlikhede gelyktydig te verken, balanseer dit doeltreffendheid en kwaliteit, wat die kanse verhoog om 'n beter algehele reeks te vind wat dalk deur die greedy benadering gemis kan word weens vroeë, suboptimale keuses.

Let daarop dat hierdie verbetering nie in die vorige kode ingesluit is nie.

Verlies funksies

Die calc_loss_batch funksie bereken die kruis entropie van 'n voorspelling van 'n enkele batch.
Die calc_loss_loader verkry die kruis entropie van al die batches en bereken die gemiddelde kruis entropie.

python
# Define loss functions
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch, target_batch = input_batch.to(device), target_batch.to(device)
logits = model(input_batch)
loss = torch.nn.functional.cross_entropy(logits.flatten(0, 1), target_batch.flatten())
return loss

def calc_loss_loader(data_loader, model, device, num_batches=None):
total_loss = 0.
if len(data_loader) == 0:
return float("nan")
elif num_batches is None:
num_batches = len(data_loader)
else:
# Reduce the number of batches to match the total number of batches in the data loader
# if num_batches exceeds the number of batches in the data loader
num_batches = min(num_batches, len(data_loader))
for i, (input_batch, target_batch) in enumerate(data_loader):
if i < num_batches:
loss = calc_loss_batch(input_batch, target_batch, model, device)
total_loss += loss.item()
else:
break
return total_loss / num_batches

tip

Gradient clipping is 'n tegniek wat gebruik word om opleiding stabiliteit in groot neurale netwerke te verbeter deur 'n maksimum drempel vir gradiëntgroottes in te stel. Wanneer gradiënte hierdie voorafbepaalde max_norm oorskry, word hulle proporsioneel afgeneem om te verseker dat opdaterings aan die model se parameters binne 'n hanteerbare reeks bly, wat probleme soos ontploffende gradiënte voorkom en 'n meer beheerde en stabiele opleiding verseker.

Let daarop dat hierdie verbetering nie in die vorige kode ingesluit is.

Kyk na die volgende voorbeeld:

Laai Data

Die funksies create_dataloader_v1 en create_dataloader_v1 is reeds in 'n vorige afdeling bespreek.

Van hier af, let op hoe dit gedefinieer is dat 90% van die teks vir opleiding gebruik gaan word terwyl die 10% vir validasie gebruik sal word en albei stelle in 2 verskillende data laders gestoor word.
Let daarop dat 'n deel van die datastel soms ook vir 'n toetsstel gelaat word om die prestasie van die model beter te evalueer.

Albei data laders gebruik dieselfde batchgrootte, maksimum lengte en stapgrootte en aantal werkers (0 in hierdie geval).
Die hoofverskille is die data wat deur elkeen gebruik word, en die validators laat nie die laaste val nie en skud ook nie die data nie, aangesien dit nie vir validasiedoeleindes nodig is nie.

Ook die feit dat stapgrootte so groot soos die kontekslengte is, beteken dat daar geen oorvleueling tussen kontekste wat gebruik word om die data op te lei sal wees nie (vermin oorpassing maar ook die opleidingsdatastel).

Boonop, let daarop dat die batchgrootte in hierdie geval 2 is om die data in 2 batches te verdeel, die hoofdoel hiervan is om parallelle verwerking toe te laat en die verbruik per batch te verminder.

python
train_ratio = 0.90
split_idx = int(train_ratio * len(text_data))
train_data = text_data[:split_idx]
val_data = text_data[split_idx:]

torch.manual_seed(123)

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

val_loader = create_dataloader_v1(
val_data,
batch_size=2,
max_length=GPT_CONFIG_124M["context_length"],
stride=GPT_CONFIG_124M["context_length"],
drop_last=False,
shuffle=False,
num_workers=0
)

Sanity Checks

Die doel is om te verifieer dat daar genoeg tokens is vir opleiding, dat die vorms die verwagte is en om inligting te verkry oor die aantal tokens wat vir opleiding en vir validasie gebruik word:

python
# Sanity checks
if total_tokens * (train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the training loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"increase the `training_ratio`")

if total_tokens * (1-train_ratio) < GPT_CONFIG_124M["context_length"]:
print("Not enough tokens for the validation loader. "
"Try to lower the `GPT_CONFIG_124M['context_length']` or "
"decrease the `training_ratio`")

print("Train loader:")
for x, y in train_loader:
print(x.shape, y.shape)

print("\nValidation loader:")
for x, y in val_loader:
print(x.shape, y.shape)

train_tokens = 0
for input_batch, target_batch in train_loader:
train_tokens += input_batch.numel()

val_tokens = 0
for input_batch, target_batch in val_loader:
val_tokens += input_batch.numel()

print("Training tokens:", train_tokens)
print("Validation tokens:", val_tokens)
print("All tokens:", train_tokens + val_tokens)

Kies toestel vir opleiding & vooraf berekeninge

Die volgende kode kies net die toestel om te gebruik en bereken 'n opleidingsverlies en 'n validasieverlies (sonder om enigiets nog op te lei) as 'n beginpunt.

python
# Indicate the device to use

if torch.cuda.is_available():
device = torch.device("cuda")
elif torch.backends.mps.is_available():
device = torch.device("mps")
else:
device = torch.device("cpu")

print(f"Using {device} device.")

model.to(device) # no assignment model = model.to(device) necessary for nn.Module classes

# Pre-calculate losses without starting yet
torch.manual_seed(123) # For reproducibility due to the shuffling in the data loader

with torch.no_grad(): # Disable gradient tracking for efficiency because we are not training, yet
train_loss = calc_loss_loader(train_loader, model, device)
val_loss = calc_loss_loader(val_loader, model, device)

print("Training loss:", train_loss)
print("Validation loss:", val_loss)

Opleidingsfunksies

Die funksie generate_and_print_sample sal net 'n konteks kry en 'n paar tokens genereer om 'n gevoel te kry oor hoe goed die model op daardie punt is. Dit word deur train_model_simple op elke stap aangeroep.

Die funksie evaluate_model word so gereeld as wat aangedui word aan die opleidingsfunksie aangeroep en dit word gebruik om die opleidingsverlies en die valideringsverlies op daardie punt in die modelopleiding te meet.

Dan is die groot funksie train_model_simple die een wat eintlik die model oplei. Dit verwag:

  • Die opleidingsdata laaier (met die data reeds geskei en voorberei vir opleiding)
  • Die validator laaier
  • Die optimizer wat tydens opleiding gebruik moet word: Dit is die funksie wat die gradiënte sal gebruik en die parameters sal opdateer om die verlies te verminder. In hierdie geval, soos jy sal sien, word AdamW gebruik, maar daar is baie meer.
  • optimizer.zero_grad() word aangeroep om die gradiënte op elke ronde te reset om te voorkom dat hulle ophoop.
  • Die lr parameter is die leer tempo wat die grootte van die stappe bepaal wat tydens die optimaliseringsproses geneem word wanneer die model se parameters opdateer word. 'n Kleinere leer tempo beteken die optimizer maak kleiner opdaterings aan die gewigte, wat kan lei tot meer presiese konvergensie maar kan opleiding vertraag. 'n Groter leer tempo kan opleiding versnel maar risiko's om oor te skiet van die minimum van die verliesfunksie (spring oor die punt waar die verliesfunksie geminimaliseer word).
  • Gewig Afsak wysig die Verlies Berekening stap deur 'n ekstra term by te voeg wat groot gewigte straf. Dit moedig die optimizer aan om oplossings met kleiner gewigte te vind, wat 'n balans skep tussen die data goed aan te pas en die model eenvoudig te hou om oorpassing in masjienleer modelle te voorkom deur die model te ontmoedig om te veel belang aan enige enkele kenmerk toe te ken.
  • Tradisionele optimizers soos SGD met L2 regulering koppel gewig afsak met die gradiënt van die verliesfunksie. egter, AdamW (n variant van Adam optimizer) ontkoppel gewig afsak van die gradiënt opdatering, wat lei tot meer effektiewe regulering.
  • Die toestel om vir opleiding te gebruik
  • Die aantal epoches: Aantal kere om oor die opleidingsdata te gaan
  • Die evaluering frekwensie: Die frekwensie om evaluate_model aan te roep
  • Die evaluering iterasie: Die aantal bondels om te gebruik wanneer die huidige toestand van die model geëvalueer word wanneer generate_and_print_sample aangeroep word
  • Die begin konteks: Wat die begin sin is om te gebruik wanneer generate_and_print_sample aangeroep word
  • Die tokenizer
python
# Functions to train the data
def train_model_simple(model, train_loader, val_loader, optimizer, device, num_epochs,
eval_freq, eval_iter, start_context, tokenizer):
# Initialize lists to track losses and tokens seen
train_losses, val_losses, track_tokens_seen = [], [], []
tokens_seen, global_step = 0, -1

# Main training loop
for epoch in range(num_epochs):
model.train()  # Set model to training mode

for input_batch, target_batch in train_loader:
optimizer.zero_grad() # Reset loss gradients from previous batch iteration
loss = calc_loss_batch(input_batch, target_batch, model, device)
loss.backward() # Calculate loss gradients
optimizer.step() # Update model weights using loss gradients
tokens_seen += input_batch.numel()
global_step += 1

# Optional evaluation step
if global_step % eval_freq == 0:
train_loss, val_loss = evaluate_model(
model, train_loader, val_loader, device, eval_iter)
train_losses.append(train_loss)
val_losses.append(val_loss)
track_tokens_seen.append(tokens_seen)
print(f"Ep {epoch+1} (Step {global_step:06d}): "
f"Train loss {train_loss:.3f}, Val loss {val_loss:.3f}")

# Print a sample text after each epoch
generate_and_print_sample(
model, tokenizer, device, start_context
)

return train_losses, val_losses, track_tokens_seen


def evaluate_model(model, train_loader, val_loader, device, eval_iter):
model.eval() # Set in eval mode to avoid dropout
with torch.no_grad():
train_loss = calc_loss_loader(train_loader, model, device, num_batches=eval_iter)
val_loss = calc_loss_loader(val_loader, model, device, num_batches=eval_iter)
model.train() # Back to training model applying all the configurations
return train_loss, val_loss


def generate_and_print_sample(model, tokenizer, device, start_context):
model.eval() # Set in eval mode to avoid dropout
context_size = model.pos_emb.weight.shape[0]
encoded = text_to_token_ids(start_context, tokenizer).to(device)
with torch.no_grad():
token_ids = generate_text(
model=model, idx=encoded,
max_new_tokens=50, context_size=context_size
)
decoded_text = token_ids_to_text(token_ids, tokenizer)
print(decoded_text.replace("\n", " "))  # Compact print format
model.train() # Back to training model applying all the configurations

tip

Om die leer tempo te verbeter, is daar 'n paar relevante tegnieke genaamd lineêre opwarming en kosyn afname.

Lineêre opwarming bestaan uit die definieer van 'n aanvanklike leer tempo en 'n maksimum een en om dit konsekwent na elke epocha op te dateer. Dit is omdat die begin van die opleiding met kleiner gewig opdaterings die risiko verminder dat die model groot, destabiliserende opdaterings tydens sy opleidingsfase teëkom.
Kosyn afname is 'n tegniek wat geleidelik die leer tempo verminder volgens 'n half-kosynkurwe na die opwarm fase, wat gewig opdaterings vertraag om die risiko van oorskiet van die verlies minima te minimaliseer en om opleidingsstabiliteit in latere fases te verseker.

Let daarop dat hierdie verbeterings nie in die vorige kode ingesluit is nie.

Begin opleiding

python
import time
start_time = time.time()

torch.manual_seed(123)
model = GPTModel(GPT_CONFIG_124M)
model.to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0004, weight_decay=0.1)

num_epochs = 10
train_losses, val_losses, tokens_seen = train_model_simple(
model, train_loader, val_loader, optimizer, device,
num_epochs=num_epochs, eval_freq=5, eval_iter=5,
start_context="Every effort moves you", tokenizer=tokenizer
)

end_time = time.time()
execution_time_minutes = (end_time - start_time) / 60
print(f"Training completed in {execution_time_minutes:.2f} minutes.")

Druk opleidingsontwikkeling

Met die volgende funksie is dit moontlik om die ontwikkeling van die model te druk terwyl dit opgelei is.

python
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import math
def plot_losses(epochs_seen, tokens_seen, train_losses, val_losses):
fig, ax1 = plt.subplots(figsize=(5, 3))
ax1.plot(epochs_seen, train_losses, label="Training loss")
ax1.plot(
epochs_seen, val_losses, linestyle="-.", label="Validation loss"
)
ax1.set_xlabel("Epochs")
ax1.set_ylabel("Loss")
ax1.legend(loc="upper right")
ax1.xaxis.set_major_locator(MaxNLocator(integer=True))
ax2 = ax1.twiny()
ax2.plot(tokens_seen, train_losses, alpha=0)
ax2.set_xlabel("Tokens seen")
fig.tight_layout()
plt.show()

# Compute perplexity from the loss values
train_ppls = [math.exp(loss) for loss in train_losses]
val_ppls = [math.exp(loss) for loss in val_losses]
# Plot perplexity over tokens seen
plt.figure()
plt.plot(tokens_seen, train_ppls, label='Training Perplexity')
plt.plot(tokens_seen, val_ppls, label='Validation Perplexity')
plt.xlabel('Tokens Seen')
plt.ylabel('Perplexity')
plt.title('Perplexity over Training')
plt.legend()
plt.show()

epochs_tensor = torch.linspace(0, num_epochs, len(train_losses))
plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses)

Stoor die model

Dit is moontlik om die model + optimizer te stoor as jy later wil voortgaan met opleiding:

python
# Save the model and the optimizer for later training
torch.save({
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
},
"/tmp/model_and_optimizer.pth"
)
# Note that this model with the optimizer occupied close to 2GB

# Restore model and optimizer for training
checkpoint = torch.load("/tmp/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(); # Put in training mode

Of net die model as jy net van plan is om dit te gebruik:

python
# Save the model
torch.save(model.state_dict(), "model.pth")

# Load it
model = GPTModel(GPT_CONFIG_124M)

model.load_state_dict(torch.load("model.pth", map_location=device))

model.eval() # Put in eval mode

Laai GPT2 gewigte

Daar is 2 vinnige skripte om die GPT2 gewigte plaaslik te laai. Vir albei kan jy die repository https://github.com/rasbt/LLMs-from-scratch plaaslik kloon, dan:

Verwysings

{{#include /banners/hacktricks-training.md}}