6. Προεκπαίδευση & Φόρτωση μοντέλων

Reading time: 24 minutes

Δημιουργία Κειμένου

Για να εκπαιδεύσουμε ένα μοντέλο, θα χρειαστεί το μοντέλο αυτό να είναι ικανό να δημιουργεί νέους τόκενς. Στη συνέχεια, θα συγκρίνουμε τους παραγόμενους τόκενς με τους αναμενόμενους προκειμένου να εκπαιδεύσουμε το μοντέλο να μάθει τους τόκενς που χρειάζεται να δημιουργήσει.

Όπως στα προηγούμενα παραδείγματα, έχουμε ήδη προβλέψει κάποιους τόκενς, είναι δυνατόν να επαναχρησιμοποιήσουμε αυτή τη λειτουργία για αυτόν τον σκοπό.

tip

Ο στόχος αυτής της έκτης φάσης είναι πολύ απλός: Εκπαίδευση του μοντέλου από την αρχή. Για αυτό θα χρησιμοποιηθεί η προηγούμενη αρχιτεκτονική LLM με κάποιους βρόχους που θα διατρέχουν τα σύνολα δεδομένων χρησιμοποιώντας τις καθορισμένες συναρτήσεις απώλειας και τον βελτιστοποιητή για να εκπαιδεύσουν όλες τις παραμέτρους του μοντέλου.

Αξιολόγηση Κειμένου

Για να πραγματοποιηθεί σωστή εκπαίδευση, είναι απαραίτητο να μετρηθούν οι προβλέψεις που αποκτήθηκαν για τον αναμενόμενο τόκεν. Ο στόχος της εκπαίδευσης είναι να μεγιστοποιηθεί η πιθανότητα του σωστού τόκεν, που περιλαμβάνει την αύξηση της πιθανότητάς του σε σχέση με άλλους τόκενς.

Για να μεγιστοποιηθεί η πιθανότητα του σωστού τόκεν, τα βάρη του μοντέλου πρέπει να τροποποιηθούν ώστε αυτή η πιθανότητα να μεγιστοποιηθεί. Οι ενημερώσεις των βαρών γίνονται μέσω backpropagation. Αυτό απαιτεί μια συνάρτηση απώλειας προς μεγιστοποίηση. Σε αυτή την περίπτωση, η συνάρτηση θα είναι η διαφορά μεταξύ της εκτελούμενης πρόβλεψης και της επιθυμητής.

Ωστόσο, αντί να δουλεύει με τις ακατέργαστες προβλέψεις, θα δουλεύει με έναν λογάριθμο με βάση n. Έτσι, αν η τρέχουσα πρόβλεψη του αναμενόμενου τόκεν ήταν 7.4541e-05, ο φυσικός λογάριθμος (βάση e) του 7.4541e-05 είναι περίπου -9.5042.
Στη συνέχεια, για κάθε είσοδο με μήκος συμφραζομένων 5 τόκενς, για παράδειγμα, το μοντέλο θα χρειαστεί να προβλέψει 5 τόκενς, με τα πρώτα 4 τόκενς να είναι το τελευταίο της εισόδου και το πέμπτο το προβλεπόμενο. Επομένως, για κάθε είσοδο θα έχουμε 5 προβλέψεις σε αυτή την περίπτωση (ακόμα και αν τα πρώτα 4 ήταν στην είσοδο, το μοντέλο δεν το γνωρίζει αυτό) με 5 αναμενόμενους τόκενς και επομένως 5 πιθανότητες προς μεγιστοποίηση.

Επομένως, μετά την εκτέλεση του φυσικού λογάριθμου σε κάθε πρόβλεψη, υπολογίζεται ο μέσος όρος, αφαιρείται το σύμβολο μείον (αυτό ονομάζεται cross entropy loss) και αυτός είναι ο αριθμός που πρέπει να μειωθεί όσο το δυνατόν πιο κοντά στο 0 γιατί ο φυσικός λογάριθμος του 1 είναι 0:

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

Ένας άλλος τρόπος για να μετρηθεί πόσο καλό είναι το μοντέλο ονομάζεται perplexity. Perplexity είναι ένα μέτρο που χρησιμοποιείται για να αξιολογήσει πόσο καλά ένα μοντέλο πιθανότητας προβλέπει ένα δείγμα. Στη γλωσσική μοντελοποίηση, αντιπροσωπεύει την αβεβαιότητα του μοντέλου όταν προβλέπει τον επόμενο τόκεν σε μια ακολουθία.
Για παράδειγμα, μια τιμή perplexity 48725 σημαίνει ότι όταν χρειάζεται να προβλέψει έναν τόκεν, δεν είναι σίγουρο ποιος από τους 48,725 τόκενς στο λεξιλόγιο είναι ο σωστός.

Παράδειγμα Προεκπαίδευσης

Αυτός είναι ο αρχικός κώδικας που προτάθηκε στο https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb μερικές φορές ελαφρώς τροποποιημένος

Προηγούμενος κώδικας που χρησιμοποιείται εδώ αλλά έχει ήδη εξηγηθεί σε προηγούμενες ενότητες
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"
)

Συναρτήσεις για τη μετατροπή κειμένου <--> ids

Αυτές είναι μερικές απλές συναρτήσεις που μπορούν να χρησιμοποιηθούν για τη μετατροπή κειμένων από το λεξιλόγιο σε ids και αντίστροφα. Αυτό είναι απαραίτητο στην αρχή της επεξεργασίας του κειμένου και στο τέλος των προβλέψεων:

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())

Generate text functions

Σε μια προηγούμενη ενότητα, μια συνάρτηση που απλώς έπαιρνε το πιο πιθανό token μετά την απόκτηση των logits. Ωστόσο, αυτό θα σημαίνει ότι για κάθε είσοδο η ίδια έξοδος θα παράγεται πάντα, γεγονός που το καθιστά πολύ ντετερμινιστικό.

Η παρακάτω συνάρτηση generate_text θα εφαρμόσει τις έννοιες top-k, temperature και multinomial.

  • Το top-k σημαίνει ότι θα αρχίσουμε να μειώνουμε σε -inf όλες τις πιθανότητες όλων των tokens εκτός από τα top k tokens. Έτσι, αν k=3, πριν πάρουμε μια απόφαση, μόνο τα 3 πιο πιθανά tokens θα έχουν πιθανότητα διαφορετική από -inf.
  • Η temperature σημαίνει ότι κάθε πιθανότητα θα διαιρείται με την τιμή της θερμοκρασίας. Μια τιμή 0.1 θα βελτιώσει την υψηλότερη πιθανότητα σε σύγκριση με την χαμηλότερη, ενώ μια θερμοκρασία 5, για παράδειγμα, θα την κάνει πιο επίπεδη. Αυτό βοηθά στη βελτίωση της παραλλαγής στις απαντήσεις που θα θέλαμε να έχει το LLM.
  • Μετά την εφαρμογή της θερμοκρασίας, μια συνάρτηση softmax εφαρμόζεται ξανά για να διασφαλιστεί ότι όλα τα υπόλοιπα tokens έχουν συνολική πιθανότητα 1.
  • Τέλος, αντί να επιλέξουμε το token με τη μεγαλύτερη πιθανότητα, η συνάρτηση multinomial εφαρμόζεται για να προβλέψει το επόμενο token σύμφωνα με τις τελικές πιθανότητες. Έτσι, αν το token 1 είχε 70% πιθανότητες, το token 2 20% και το token 3 10%, το 70% των φορών θα επιλεγεί το token 1, το 20% των φορών θα είναι το token 2 και το 10% των φορών θα είναι το token 3.
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

Υπάρχει μια κοινή εναλλακτική στο top-k που ονομάζεται top-p, γνωστή επίσης ως δειγματοληψία πυρήνα, η οποία αντί να παίρνει k δείγματα με την πιο πιθανότητα, οργανώνει όλη τη λεξιλογία κατά πιθανότητες και αθροίζει αυτές από την υψηλότερη πιθανότητα προς τη χαμηλότερη μέχρι να φτάσει σε ένα όριο.

Στη συνέχεια, μόνο αυτές οι λέξεις της λεξιλογίας θα ληφθούν υπόψη σύμφωνα με τις σχετικές πιθανότητές τους.

Αυτό επιτρέπει να μην χρειάζεται να επιλέξετε έναν αριθμό k δειγμάτων, καθώς το βέλτιστο k μπορεί να είναι διαφορετικό σε κάθε περίπτωση, αλλά μόνο ένα όριο.

Σημειώστε ότι αυτή η βελτίωση δεν περιλαμβάνεται στον προηγούμενο κώδικα.

tip

Ένας άλλος τρόπος για να βελτιώσετε το παραγόμενο κείμενο είναι χρησιμοποιώντας Beam search αντί της απληστίας που χρησιμοποιείται σε αυτό το παράδειγμα.
Σε αντίθεση με την απληστία, η οποία επιλέγει τη πιο πιθανή επόμενη λέξη σε κάθε βήμα και δημιουργεί μια μοναδική ακολουθία, η beam search παρακολουθεί τις κορυφαίες 𝑘 k υψηλότερες μερικές ακολουθίες (που ονομάζονται "beams") σε κάθε βήμα. Εξερευνώντας πολλές δυνατότητες ταυτόχρονα, ισορροπεί την αποδοτικότητα και την ποιότητα, αυξάνοντας τις πιθανότητες να βρει μια καλύτερη συνολική ακολουθία που μπορεί να παραληφθεί από την απληστία λόγω πρώιμων, υποβέλτιστων επιλογών.

Σημειώστε ότι αυτή η βελτίωση δεν περιλαμβάνεται στον προηγούμενο κώδικα.

Loss functions

Η calc_loss_batch συνάρτηση υπολογίζει την διασταυρούμενη εντροπία μιας πρόβλεψης ενός μόνο batch.
Η calc_loss_loader παίρνει την διασταυρούμενη εντροπία όλων των batches και υπολογίζει την μέση διασταυρούμενη εντροπία.

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 είναι μια τεχνική που χρησιμοποιείται για να ενισχύσει τη σταθερότητα εκπαίδευσης σε μεγάλα νευρωνικά δίκτυα, θέτοντας ένα μέγιστο όριο για τα μεγέθη των παραγώγων. Όταν οι παράγωγοι υπερβαίνουν αυτό το προκαθορισμένο max_norm, κλιμακώνονται αναλογικά για να διασφαλιστεί ότι οι ενημερώσεις στις παραμέτρους του μοντέλου παραμένουν εντός ενός διαχειρίσιμου εύρους, αποτρέποντας προβλήματα όπως οι εκρηκτικοί παράγωγοι και διασφαλίζοντας πιο ελεγχόμενη και σταθερή εκπαίδευση.

Σημειώστε ότι αυτή η βελτίωση δεν περιλαμβάνεται στον προηγούμενο κώδικα.

Δείτε το παρακάτω παράδειγμα:

Φόρτωση Δεδομένων

Οι συναρτήσεις create_dataloader_v1 και create_dataloader_v1 έχουν ήδη συζητηθεί σε προηγούμενη ενότητα.

Από εδώ σημειώστε πώς ορίζεται ότι το 90% του κειμένου θα χρησιμοποιηθεί για εκπαίδευση ενώ το 10% θα χρησιμοποιηθεί για επικύρωση και τα δύο σύνολα αποθηκεύονται σε 2 διαφορετικούς φορτωτές δεδομένων.
Σημειώστε ότι μερικές φορές μέρος του συνόλου δεδομένων αφήνεται επίσης για ένα σύνολο δοκιμών για να αξιολογηθεί καλύτερα η απόδοση του μοντέλου.

Και οι δύο φορτωτές δεδομένων χρησιμοποιούν το ίδιο μέγεθος παρτίδας, μέγιστο μήκος και βήμα και αριθμό εργαζομένων (0 σε αυτή την περίπτωση).
Οι κύριες διαφορές είναι τα δεδομένα που χρησιμοποιεί ο καθένας, και οι επικυρωτές δεν απορρίπτουν το τελευταίο ούτε ανακατεύουν τα δεδομένα καθώς δεν είναι απαραίτητο για σκοπούς επικύρωσης.

Επίσης, το γεγονός ότι το βήμα είναι τόσο μεγάλο όσο το μήκος του συμφραζομένου, σημαίνει ότι δεν θα υπάρχει επικάλυψη μεταξύ των συμφραζομένων που χρησιμοποιούνται για την εκπαίδευση των δεδομένων (μειώνει την υπερβολική προσαρμογή αλλά και το σύνολο δεδομένων εκπαίδευσης).

Επιπλέον, σημειώστε ότι το μέγεθος παρτίδας σε αυτή την περίπτωση είναι 2 για να διαιρέσει τα δεδομένα σε 2 παρτίδες, ο κύριος στόχος αυτού είναι να επιτρέψει παράλληλη επεξεργασία και να μειώσει την κατανάλωση ανά παρτίδα.

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
)

Έλεγχοι Σωφροσύνης

Ο στόχος είναι να ελέγξουμε αν υπάρχουν αρκετοί τόκεν για εκπαίδευση, αν οι διαστάσεις είναι οι αναμενόμενες και να αποκτήσουμε κάποιες πληροφορίες σχετικά με τον αριθμό των τόκεν που χρησιμοποιούνται για εκπαίδευση και για επικύρωση:

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)

Επιλογή συσκευής για εκπαίδευση & προϋπολογισμούς

Ο παρακάτω κώδικας απλώς επιλέγει τη συσκευή που θα χρησιμοποιηθεί και υπολογίζει μια απώλεια εκπαίδευσης και μια απώλεια επικύρωσης (χωρίς να έχει εκπαιδευτεί τίποτα ακόμα) ως σημείο εκκίνησης.

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)

Training functions

Η συνάρτηση generate_and_print_sample θα πάρει απλώς ένα πλαίσιο και θα δημιουργήσει μερικούς τόκενς προκειμένου να αποκτήσει μια αίσθηση για το πόσο καλή είναι το μοντέλο σε εκείνο το σημείο. Αυτό καλείται από τη train_model_simple σε κάθε βήμα.

Η συνάρτηση evaluate_model καλείται όσο συχνά υποδεικνύεται στη συνάρτηση εκπαίδευσης και χρησιμοποιείται για να μετρήσει την απώλεια εκπαίδευσης και την απώλεια επικύρωσης σε εκείνο το σημείο της εκπαίδευσης του μοντέλου.

Στη συνέχεια, η μεγάλη συνάρτηση train_model_simple είναι αυτή που πραγματικά εκπαιδεύει το μοντέλο. Αναμένει:

  • Τον φορτωτή δεδομένων εκπαίδευσης (με τα δεδομένα ήδη διαχωρισμένα και προετοιμασμένα για εκπαίδευση)
  • Τον φορτωτή επικύρωσης
  • Τον optimizer που θα χρησιμοποιηθεί κατά τη διάρκεια της εκπαίδευσης: Αυτή είναι η συνάρτηση που θα χρησιμοποιήσει τους βαθμούς και θα ενημερώσει τις παραμέτρους για να μειώσει την απώλεια. Σε αυτή την περίπτωση, όπως θα δείτε, χρησιμοποιείται ο AdamW, αλλά υπάρχουν πολλοί άλλοι.
  • Καλείται το optimizer.zero_grad() για να επαναφέρει τους βαθμούς σε κάθε γύρο ώστε να μην τους συσσωρεύει.
  • Η παράμετρος lr είναι ο ρυθμός μάθησης που καθορίζει το μέγεθος των βημάτων που γίνονται κατά τη διάρκεια της διαδικασίας βελτιστοποίησης όταν ενημερώνονται οι παράμετροι του μοντέλου. Ένας μικρότερος ρυθμός μάθησης σημαίνει ότι ο optimizer κάνει μικρότερες ενημερώσεις στα βάρη, κάτι που μπορεί να οδηγήσει σε πιο ακριβή σύγκλιση αλλά μπορεί να επιβραδύνει την εκπαίδευση. Ένας μεγαλύτερος ρυθμός μάθησης μπορεί να επιταχύνει την εκπαίδευση αλλά κινδυνεύει να υπερβεί το ελάχιστο της συνάρτησης απώλειας (να πηδήξει πάνω από το σημείο όπου ελαχιστοποιείται η συνάρτηση απώλειας).
  • Ο Weight Decay τροποποιεί το βήμα Υπολογισμού Απώλειας προσθέτοντας έναν επιπλέον όρο που επιβαρύνει τα μεγάλα βάρη. Αυτό ενθαρρύνει τον optimizer να βρει λύσεις με μικρότερα βάρη, ισορροπώντας μεταξύ της καλής προσαρμογής στα δεδομένα και της διατήρησης του μοντέλου απλού, αποτρέποντας την υπερβολική προσαρμογή σε μοντέλα μηχανικής μάθησης αποθαρρύνοντας το μοντέλο από το να αποδίδει υπερβολική σημασία σε οποιοδήποτε μεμονωμένο χαρακτηριστικό.
  • Παραδοσιακοί optimizers όπως ο SGD με L2 κανονικοποίηση συνδυάζουν τον weight decay με τον βαθμό της συνάρτησης απώλειας. Ωστόσο, ο AdamW (μια παραλλαγή του Adam optimizer) αποσυνδέει τον weight decay από την ενημέρωση του βαθμού, οδηγώντας σε πιο αποτελεσματική κανονικοποίηση.
  • Η συσκευή που θα χρησιμοποιηθεί για την εκπαίδευση
  • Ο αριθμός των εποχών: Αριθμός φορών που θα περάσει από τα δεδομένα εκπαίδευσης
  • Η συχνότητα αξιολόγησης: Η συχνότητα για να καλέσετε τη evaluate_model
  • Η επανάληψη αξιολόγησης: Ο αριθμός των παρτίδων που θα χρησιμοποιηθούν κατά την αξιολόγηση της τρέχουσας κατάστασης του μοντέλου όταν καλείτε τη generate_and_print_sample
  • Το αρχικό πλαίσιο: Ποια είναι η αρχική πρόταση που θα χρησιμοποιηθεί όταν καλείτε τη generate_and_print_sample
  • Ο 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

Για να βελτιωθεί ο ρυθμός εκμάθησης, υπάρχουν μερικές σχετικές τεχνικές που ονομάζονται linear warmup και cosine decay.

Linear warmup συνίσταται στον καθορισμό ενός αρχικού ρυθμού εκμάθησης και ενός μέγιστου και στη συνεχή ενημέρωσή του μετά από κάθε εποχή. Αυτό συμβαίνει επειδή η έναρξη της εκπαίδευσης με μικρότερες ενημερώσεις βαρών μειώνει τον κίνδυνο το μοντέλο να συναντήσει μεγάλες, αποσταθεροποιητικές ενημερώσεις κατά τη διάρκεια της φάσης εκπαίδευσης.
Cosine decay είναι μια τεχνική που μειώνει σταδιακά τον ρυθμό εκμάθησης ακολουθώντας μια καμπύλη μισού συνημίτονου μετά τη φάση warmup, επιβραδύνοντας τις ενημερώσεις βαρών για να ελαχιστοποιήσει τον κίνδυνο υπερβολικής εκπαίδευσης των ελαχίστων απωλειών και να διασφαλίσει τη σταθερότητα της εκπαίδευσης σε μεταγενέστερες φάσεις.

Σημειώστε ότι αυτές οι βελτιώσεις δεν περιλαμβάνονται στον προηγούμενο κώδικα.

Start training

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.")

Εκτύπωση εξέλιξης εκπαίδευσης

Με την παρακάτω συνάρτηση είναι δυνατή η εκτύπωση της εξέλιξης του μοντέλου ενώ εκπαιδευόταν.

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)

Αποθήκευση του μοντέλου

Είναι δυνατόν να αποθηκεύσετε το μοντέλο + τον βελτιστοποιητή αν θέλετε να συνεχίσετε την εκπαίδευση αργότερα:

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

Ή απλώς το μοντέλο αν σκοπεύετε να το χρησιμοποιήσετε:

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

Φόρτωση βαρών GPT2

Υπάρχουν 2 γρήγορα σενάρια για να φορτώσετε τα βάρη GPT2 τοπικά. Για και τα δύο μπορείτε να κλωνοποιήσετε το αποθετήριο https://github.com/rasbt/LLMs-from-scratch τοπικά, στη συνέχεια:

Αναφορές