Das Ziel dieser fünften Phase ist sehr einfach: Entwickeln Sie die Architektur des vollständigen LLM. Fügen Sie alles zusammen, wenden Sie alle Schichten an und erstellen Sie alle Funktionen, um Text zu generieren oder Text in IDs und umgekehrt zu transformieren.
Diese Architektur wird sowohl für das Training als auch für die Vorhersage von Text nach dem Training verwendet.
Eine hochrangige Darstellung kann beobachtet werden in:
Eingabe (Tokenisierter Text): Der Prozess beginnt mit tokenisiertem Text, der in numerische Darstellungen umgewandelt wird.
Token-Embedding- und Positions-Embedding-Schicht: Der tokenisierte Text wird durch eine Token-Embedding-Schicht und eine Positions-Embedding-Schicht geleitet, die die Position der Tokens in einer Sequenz erfasst, was entscheidend für das Verständnis der Wortreihenfolge ist.
Transformer-Blöcke: Das Modell enthält 12 Transformer-Blöcke, jeder mit mehreren Schichten. Diese Blöcke wiederholen die folgende Sequenz:
Maskierte Multi-Head-Attention: Ermöglicht es dem Modell, sich gleichzeitig auf verschiedene Teile des Eingabetextes zu konzentrieren.
Layer-Normalisierung: Ein Normalisierungsschritt zur Stabilisierung und Verbesserung des Trainings.
Feed-Forward-Schicht: Verantwortlich für die Verarbeitung der Informationen aus der Attention-Schicht und für die Vorhersage des nächsten Tokens.
Dropout-Schichten: Diese Schichten verhindern Overfitting, indem sie während des Trainings zufällig Einheiten fallen lassen.
Endausgabeschicht: Das Modell gibt einen 4x50.257-dimensionalen Tensor aus, wobei 50.257 die Größe des Wortschatzes darstellt. Jede Zeile in diesem Tensor entspricht einem Vektor, den das Modell verwendet, um das nächste Wort in der Sequenz vorherzusagen.
Ziel: Das Ziel ist es, diese Embeddings zu nehmen und sie wieder in Text umzuwandeln. Insbesondere wird die letzte Zeile der Ausgabe verwendet, um das nächste Wort zu generieren, das in diesem Diagramm als "vorwärts" dargestellt ist.
Code-Darstellung
import torchimport torch.nn as nnimport tiktokenclassGELU(nn.Module):def__init__(self):super().__init__()defforward(self,x):return0.5* x * (1+ torch.tanh(torch.sqrt(torch.tensor(2.0/ torch.pi)) *(x +0.044715* torch.pow(x, 3))))classFeedForward(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"]),)defforward(self,x):return self.layers(x)classMultiHeadAttention(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 num_heads"self.d_out = d_outself.num_heads = num_headsself.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dimself.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 outputsself.dropout = nn.Dropout(dropout)self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1))defforward(self,x):b, num_tokens, d_in = x.shapekeys = 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 maskattn_scores = queries @ keys.transpose(2, 3)# Dot product for each head# Original mask truncated to the number of tokens and converted to booleanmask_bool = self.mask.bool()[:num_tokens,:num_tokens]# Use the mask to fill attention scoresattn_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_dimcontext_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)context_vec = self.out_proj(context_vec)# optional projectionreturn context_vecclassLayerNorm(nn.Module):def__init__(self,emb_dim):super().__init__()self.eps =1e-5self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))defforward(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.shiftclassTransformerBlock(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"])defforward(self,x):# Shortcut connection for attention blockshortcut = xx = 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 blockshortcut = xx = self.norm2(x)x = self.ff(x)x = self.drop_shortcut(x)x = x + shortcut # Add the original input backreturn xclassGPTModel(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 _ inrange(cfg["n_layers"])])self.final_norm =LayerNorm(cfg["emb_dim"])self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)defforward(self,in_idx):batch_size, seq_len = in_idx.shapetok_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 logitsGPT_CONFIG_124M ={"vocab_size":50257,# Vocabulary size"context_length":1024,# Context length"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)out =model(batch)print("Input batch:\n", batch)print("\nOutput shape:", out.shape)print(out)
GELU Aktivierungsfunktion
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classGELU(nn.Module):def__init__(self):super().__init__()defforward(self,x):return0.5* x * (1+ torch.tanh(torch.sqrt(torch.tensor(2.0/ torch.pi)) *(x +0.044715* torch.pow(x, 3))))
Zweck und Funktionalität
GELU (Gaussian Error Linear Unit): Eine Aktivierungsfunktion, die Nichtlinearität in das Modell einführt.
Glatte Aktivierung: Im Gegensatz zu ReLU, das negative Eingaben auf null setzt, ordnet GELU Eingaben glatt Ausgaben zu und ermöglicht kleine, von null verschiedene Werte für negative Eingaben.
Mathematische Definition:
Das Ziel der Verwendung dieser Funktion nach linearen Schichten innerhalb der FeedForward-Schicht besteht darin, die linearen Daten nicht linear zu gestalten, um dem Modell zu ermöglichen, komplexe, nicht-lineare Beziehungen zu lernen.
FeedForward-Neuronales Netzwerk
Formen wurden als Kommentare hinzugefügt, um die Formen der Matrizen besser zu verstehen:
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classFeedForward(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"]),)defforward(self,x):# x shape: (batch_size, seq_len, emb_dim)x = self.layers[0](x)# x shape: (batch_size, seq_len, 4 * emb_dim)x = self.layers[1](x)# x shape remains: (batch_size, seq_len, 4 * emb_dim)x = self.layers[2](x)# x shape: (batch_size, seq_len, emb_dim)return x # Output shape: (batch_size, seq_len, emb_dim)
Zweck und Funktionalität
Positionsweise FeedForward-Netzwerk: Wendet ein zweilagiges voll verbundenes Netzwerk auf jede Position separat und identisch an.
Schichtdetails:
Erste lineare Schicht: Erweitert die Dimensionalität von emb_dim auf 4 * emb_dim.
GELU-Aktivierung: Wendet Nichtlinearität an.
Zweite lineare Schicht: Reduziert die Dimensionalität zurück auf emb_dim.
Wie Sie sehen können, verwendet das Feed Forward-Netzwerk 3 Schichten. Die erste ist eine lineare Schicht, die die Dimensionen mit 4 multipliziert, indem sie lineare Gewichte (Parameter, die im Modell trainiert werden) verwendet. Dann wird die GELU-Funktion in all diesen Dimensionen verwendet, um nicht-lineare Variationen anzuwenden, um reichhaltigere Darstellungen zu erfassen, und schließlich wird eine weitere lineare Schicht verwendet, um zur ursprünglichen Größe der Dimensionen zurückzukehren.
Multi-Head Attention-Mechanismus
Dies wurde bereits in einem früheren Abschnitt erklärt.
Zweck und Funktionalität
Multi-Head Self-Attention: Ermöglicht es dem Modell, sich auf verschiedene Positionen innerhalb der Eingabesequenz zu konzentrieren, wenn es ein Token kodiert.
Schlüsselkomponenten:
Abfragen, Schlüssel, Werte: Lineare Projektionen der Eingabe, die zur Berechnung der Aufmerksamkeitswerte verwendet werden.
Köpfe: Mehrere Aufmerksamkeitsmechanismen, die parallel laufen (num_heads), jeder mit einer reduzierten Dimension (head_dim).
Aufmerksamkeitswerte: Werden als Skalarprodukt von Abfragen und Schlüsseln berechnet, skaliert und maskiert.
Maskierung: Eine kausale Maske wird angewendet, um zu verhindern, dass das Modell zukünftige Tokens berücksichtigt (wichtig für autoregressive Modelle wie GPT).
Aufmerksamkeitsgewichte: Softmax der maskierten und skalierten Aufmerksamkeitswerte.
Kontextvektor: Gewichtete Summe der Werte, entsprechend den Aufmerksamkeitsgewichten.
Ausgabeprojektion: Lineare Schicht zur Kombination der Ausgaben aller Köpfe.
Das Ziel dieses Netzwerks ist es, die Beziehungen zwischen Tokens im gleichen Kontext zu finden. Darüber hinaus werden die Tokens in verschiedene Köpfe unterteilt, um Überanpassung zu verhindern, obwohl die endgültigen Beziehungen, die pro Kopf gefunden werden, am Ende dieses Netzwerks kombiniert werden.
Darüber hinaus wird während des Trainings eine kausale Maske angewendet, sodass spätere Tokens nicht berücksichtigt werden, wenn die spezifischen Beziehungen zu einem Token betrachtet werden, und es wird auch ein Dropout angewendet, um Überanpassung zu verhindern.
Layer Normalisierung
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04classLayerNorm(nn.Module):def__init__(self,emb_dim):super().__init__()self.eps =1e-5# Prevent division by zero during normalization.self.scale = nn.Parameter(torch.ones(emb_dim))self.shift = nn.Parameter(torch.zeros(emb_dim))defforward(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
Zweck und Funktionalität
Layer Normalization: Eine Technik, die verwendet wird, um die Eingaben über die Merkmale (Einbettungsdimensionen) für jedes einzelne Beispiel in einem Batch zu normalisieren.
Komponenten:
eps: Eine kleine Konstante (1e-5), die zur Varianz hinzugefügt wird, um eine Division durch Null während der Normalisierung zu verhindern.
scale und shift: Lernbare Parameter (nn.Parameter), die es dem Modell ermöglichen, die normalisierte Ausgabe zu skalieren und zu verschieben. Sie werden jeweils mit Einsen und Nullen initialisiert.
Normalisierungsprozess:
Mittelwert berechnen (mean): Berechnet den Mittelwert der Eingabe x über die Einbettungsdimension (dim=-1), wobei die Dimension für das Broadcasting beibehalten wird (keepdim=True).
Varianz berechnen (var): Berechnet die Varianz von x über die Einbettungsdimension und behält ebenfalls die Dimension bei. Der Parameter unbiased=False stellt sicher, dass die Varianz mit dem verzerrten Schätzer berechnet wird (Division durch N anstelle von N-1), was angemessen ist, wenn über Merkmale anstelle von Proben normalisiert wird.
Normalisieren (norm_x): Subtrahiert den Mittelwert von x und dividiert durch die Quadratwurzel der Varianz plus eps.
Skalieren und Verschieben: Wendet die lernbaren scale- und shift-Parameter auf die normalisierte Ausgabe an.
Das Ziel ist es, einen Mittelwert von 0 mit einer Varianz von 1 über alle Dimensionen des gleichen Tokens sicherzustellen. Das Ziel davon ist es, das Training von tiefen neuronalen Netzwerken zu stabilisieren, indem der interne Kovariate Shift reduziert wird, der sich auf die Veränderung der Verteilung der Netzwerkaktivierungen aufgrund der Aktualisierung der Parameter während des Trainings bezieht.
Transformer Block
Größen wurden als Kommentare hinzugefügt, um die Größen der Matrizen besser zu verstehen:
Zusammensetzung der Schichten: Kombiniert Multi-Head-Attention, Feedforward-Netzwerk, Layer-Normalisierung und Residualverbindungen.
Layer-Normalisierung: Vor der Attention- und Feedforward-Schicht angewendet für stabiles Training.
Residualverbindungen (Abkürzungen): Fügen den Eingang einer Schicht zu ihrem Ausgang hinzu, um den Gradientfluss zu verbessern und das Training tiefer Netzwerke zu ermöglichen.
Dropout: Nach der Attention- und Feedforward-Schicht zur Regularisierung angewendet.
Schritt-für-Schritt-Funktionalität
Erster Residualpfad (Selbst-Attention):
Eingang (shortcut): Speichern Sie den ursprünglichen Eingang für die Residualverbindung.
Layer Norm (norm1): Normalisieren Sie den Eingang.
Multi-Head-Attention (att): Wenden Sie Selbst-Attention an.
Dropout (drop_shortcut): Wenden Sie Dropout zur Regularisierung an.
Add Residual (x + shortcut): Kombinieren Sie mit dem ursprünglichen Eingang.
Zweiter Residualpfad (FeedForward):
Eingang (shortcut): Speichern Sie den aktualisierten Eingang für die nächste Residualverbindung.
Layer Norm (norm2): Normalisieren Sie den Eingang.
FeedForward-Netzwerk (ff): Wenden Sie die Feedforward-Transformation an.
Dropout (drop_shortcut): Wenden Sie Dropout an.
Add Residual (x + shortcut): Kombinieren Sie mit dem Eingang vom ersten Residualpfad.
Der Transformer-Block gruppiert alle Netzwerke zusammen und wendet einige Normalisierungen und Dropouts an, um die Trainingsstabilität und -ergebnisse zu verbessern.
Beachten Sie, dass Dropouts nach der Verwendung jedes Netzwerks durchgeführt werden, während die Normalisierung vorher angewendet wird.
Darüber hinaus verwendet er auch Abkürzungen, die darin bestehen, den Ausgang eines Netzwerks mit seinem Eingang zu addieren. Dies hilft, das Problem des verschwindenden Gradienten zu verhindern, indem sichergestellt wird, dass die anfänglichen Schichten "genauso viel" beitragen wie die letzten.
GPTModel
Größen wurden als Kommentare hinzugefügt, um die Größen der Matrizen besser zu verstehen:
Token-Einbettungen (tok_emb): Wandelt Token-Indizes in Einbettungen um. Zur Erinnerung, dies sind die Gewichte, die jeder Dimension jedes Tokens im Vokabular zugewiesen werden.
Positions-Einbettungen (pos_emb): Fügt den Einbettungen Positionsinformationen hinzu, um die Reihenfolge der Tokens zu erfassen. Zur Erinnerung, dies sind die Gewichte, die Tokens entsprechend ihrer Position im Text zugewiesen werden.
Dropout (drop_emb): Wird auf Einbettungen zur Regularisierung angewendet.
Transformer-Blöcke (trf_blocks): Stapel von n_layers Transformer-Blöcken zur Verarbeitung von Einbettungen.
Endgültige Normalisierung (final_norm): Schichtnormalisierung vor der Ausgabeschicht.
Ausgabeschicht (out_head): Projiziert die endgültigen versteckten Zustände auf die Größe des Vokabulars, um Logits für die Vorhersage zu erzeugen.
Das Ziel dieser Klasse ist es, alle anderen genannten Netzwerke zu nutzen, um das nächste Token in einer Sequenz vorherzusagen, was grundlegend für Aufgaben wie die Textgenerierung ist.
Beachten Sie, wie es so viele Transformer-Blöcke wie angegeben verwenden wird und dass jeder Transformer-Block ein Multi-Head-Attention-Netz, ein Feed-Forward-Netz und mehrere Normalisierungen verwendet. Wenn also 12 Transformer-Blöcke verwendet werden, multiplizieren Sie dies mit 12.
Darüber hinaus wird eine Normalisierungsschicht vor der Ausgabe hinzugefügt und eine endgültige lineare Schicht wird am Ende angewendet, um die Ergebnisse mit den richtigen Dimensionen zu erhalten. Beachten Sie, dass jeder endgültige Vektor die Größe des verwendeten Vokabulars hat. Dies liegt daran, dass versucht wird, eine Wahrscheinlichkeit pro möglichem Token im Vokabular zu erhalten.
Anzahl der zu trainierenden Parameter
Nachdem die GPT-Struktur definiert ist, ist es möglich, die Anzahl der zu trainierenden Parameter zu ermitteln:
GPT_CONFIG_124M ={"vocab_size":50257,# Vocabulary size"context_length":1024,# Context length"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}model =GPTModel(GPT_CONFIG_124M)total_params =sum(p.numel() for p in model.parameters())print(f"Total number of parameters: {total_params:,}")# Total number of parameters: 163,009,536
Ein Modell, das das nächste Token wie das vorherige vorhersagt, benötigt lediglich die letzten Token-Werte aus der Ausgabe (da sie die Werte des vorhergesagten Tokens sein werden), was ein Wert pro Eintrag im Vokabular sein wird, und dann die softmax-Funktion verwenden, um die Dimensionen in Wahrscheinlichkeiten zu normalisieren, die sich zu 1 summieren, und dann den Index des größten Eintrags zu erhalten, der der Index des Wortes im Vokabular sein wird.
defgenerate_text_simple(model,idx,max_new_tokens,context_size):# idx is (batch, n_tokens) array of indices in the current contextfor _ inrange(max_new_tokens):# Crop current context if it exceeds the supported context size# E.g., if LLM supports only 5 tokens, and the context size is 10# then only the last 5 tokens are used as contextidx_cond = idx[:,-context_size:]# Get the predictionswith torch.no_grad():logits =model(idx_cond)# Focus only on the last time step# (batch, n_tokens, vocab_size) becomes (batch, vocab_size)logits = logits[:,-1,:]# Apply softmax to get probabilitiesprobas = torch.softmax(logits, dim=-1)# (batch, vocab_size)# Get the idx of the vocab entry with the highest probability valueidx_next = torch.argmax(probas, dim=-1, keepdim=True)# (batch, 1)# Append sampled index to the running sequenceidx = torch.cat((idx, idx_next), dim=1)# (batch, n_tokens+1)return idxstart_context ="Hello, I am"encoded = tokenizer.encode(start_context)print("encoded:", encoded)encoded_tensor = torch.tensor(encoded).unsqueeze(0)print("encoded_tensor.shape:", encoded_tensor.shape)model.eval()# disable dropoutout =generate_text_simple(model=model,idx=encoded_tensor,max_new_tokens=6,context_size=GPT_CONFIG_124M["context_length"])print("Output:", out)print("Output length:", len(out[0]))