# 6. Vooropleiding & Laai modelle {{#include ../../banners/hacktricks-training.md}} ## Teksgenerasie Om 'n model op te lei, moet ons hê dat daardie model in staat is om nuwe tokens te 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 in daardie geval hê (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 _cross entropy loss_ 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 is in [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/ch05.ipynb) soms effens aangepas
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 Dit 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 was daar '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, sal slegs die 3 meest waarskynlike tokens 'n waarskynlikheid hê wat verskil van `-inf` voordat 'n besluit geneem word. - 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 een, terwyl 'n temperatuur van `5` byvoorbeeld dit meer plat sal maak. Dit help om die variasie in antwoorde wat ons wil hê die LLM moet hê, te verbeter. - Na die temperatuur toegepas is, word 'n **`softmax`** funksie weer toegepas om al die oorblywende tokens 'n totale waarskynlikheid van 1 te gee. - 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`**](https://en.wikipedia.org/wiki/Top-p_sampling), 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 na 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 is.\ > Anders as greedy search, wat die mees waarskynlike volgende woord by elke stap kies en 'n enkele volgorde bou, **hou beam search die top 𝑘 k hoogste-telling gedeeltelike volgordes** (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** volgorde 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._ ### Verliesfunksies Die **`calc_loss_batch`** funksie bereken die kruisentropie van 'n voorspelling van 'n enkele bondel.\ Die **`calc_loss_loader`** verkry die kruisentropie van al die bondels en bereken die **gemiddelde kruisentropie**. ```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 gradientgroottes in te stel. Wanneer gradienten 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 gradienten 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 laaiers 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 laaiers gebruik dieselfde batchgrootte, maksimum lengte en stapgrootte en aantal werkers (0 in hierdie geval).\ Die hoof verskille 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 kontekste lengte is**, beteken dat daar nie oorvleueling tussen kontekste wat gebruik word om die data op te lei sal wees nie (vermin die 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 valideringsverlies (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 opgedateer 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ëntopdatering, 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 evaluasiefrekwensie: Die frekwensie om `evaluate_model` aan te roep - Die evaluasie-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-kosyn kromme **na die opwarm** fase, wat gewig opdaterings vertraag om **die risiko van oorskry** van die verlies minima te minimaliseer en om opleidings stabiliteit 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](https://github.com/rasbt/LLMs-from-scratch) plaaslik kloon, dan: - Die skrip [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/01_main-chapter-code/gpt_generate.py) sal al die gewigte aflaai en die formate van OpenAI na die formate wat deur ons LLM verwag word, transformeer. Die skrip is ook voorberei met die nodige konfigurasie en met die prompt: "Elke poging beweeg jou" - Die skrip [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch05/02_alternative_weight_loading/weight-loading-hf-transformers.ipynb) laat jou toe om enige van die GPT2 gewigte plaaslik te laai (verander net die `CHOOSE_MODEL` var) en teks te voorspel vanaf sommige prompts. ## Verwysings - [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch) {{#include ../../banners/hacktricks-training.md}}