mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
667 lines
28 KiB
Markdown
667 lines
28 KiB
Markdown
# 5. LLM-Architektur
|
|
|
|
## LLM-Architektur
|
|
|
|
> [!TIP]
|
|
> 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.
|
|
|
|
LLM-Architekturbeispiel von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
|
|
|
Eine hochrangige Darstellung kann beobachtet werden in:
|
|
|
|
<figure><img src="../../images/image (3) (1) (1) (1).png" alt="" width="563"><figcaption><p><a href="https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31">https://camo.githubusercontent.com/6c8c392f72d5b9e86c94aeb9470beab435b888d24135926f1746eb88e0cc18fb/68747470733a2f2f73656261737469616e72617363686b612e636f6d2f696d616765732f4c4c4d732d66726f6d2d736372617463682d696d616765732f636830345f636f6d707265737365642f31332e776562703f31</a></p></figcaption></figure>
|
|
|
|
1. **Eingabe (Tokenisierter Text)**: Der Prozess beginnt mit tokenisiertem Text, der in numerische Darstellungen umgewandelt wird.
|
|
2. **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.
|
|
3. **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.
|
|
4. **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.
|
|
5. **Ziel**: Das Ziel ist es, diese Embeddings zu nehmen und sie zurück 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
|
|
```python
|
|
import torch
|
|
import torch.nn as nn
|
|
import tiktoken
|
|
|
|
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 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 num_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.contiguous().view(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 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
|
|
|
|
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
|
|
}
|
|
|
|
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**
|
|
```python
|
|
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
|
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))
|
|
))
|
|
```
|
|
#### **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:**
|
|
|
|
<figure><img src="../../images/image (2) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
|
|
|
> [!NOTE]
|
|
> Das Ziel der Verwendung dieser Funktion nach linearen Schichten innerhalb der FeedForward-Schicht besteht darin, die linearen Daten nicht linear zu machen, 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:_
|
|
```python
|
|
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
|
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):
|
|
# 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 vollständig 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`.
|
|
|
|
> [!NOTE]
|
|
> 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üsselfunktionen:**
|
|
- **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:** Berechnet als das Skalarprodukt von Abfragen und Schlüsseln, 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.
|
|
|
|
> [!NOTE]
|
|
> 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**.
|
|
|
|
### **Schicht** Normalisierung
|
|
```python
|
|
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
|
class LayerNorm(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))
|
|
|
|
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
|
|
```
|
|
#### **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 und nicht über 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.
|
|
|
|
> [!NOTE]
|
|
> 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**
|
|
|
|
_Erklärungen zu den Formen wurden als Kommentare hinzugefügt, um die Formen der Matrizen besser zu verstehen:_
|
|
```python
|
|
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
|
|
|
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):
|
|
# x shape: (batch_size, seq_len, emb_dim)
|
|
|
|
# Shortcut connection for attention block
|
|
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
|
x = self.norm1(x) # shape remains (batch_size, seq_len, emb_dim)
|
|
x = self.att(x) # shape: (batch_size, seq_len, emb_dim)
|
|
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
|
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
|
|
|
# Shortcut connection for feedforward block
|
|
shortcut = x # shape: (batch_size, seq_len, emb_dim)
|
|
x = self.norm2(x) # shape remains (batch_size, seq_len, emb_dim)
|
|
x = self.ff(x) # shape: (batch_size, seq_len, emb_dim)
|
|
x = self.drop_shortcut(x) # shape remains (batch_size, seq_len, emb_dim)
|
|
x = x + shortcut # shape: (batch_size, seq_len, emb_dim)
|
|
|
|
return x # Output shape: (batch_size, seq_len, emb_dim)
|
|
|
|
```
|
|
#### **Zweck und Funktionalität**
|
|
|
|
- **Zusammensetzung der Schichten:** Kombiniert Multi-Head-Attention, Feedforward-Netzwerk, Layer-Normalisierung und Residualverbindungen.
|
|
- **Layer-Normalisierung:** Vor der Attention- und Feedforward-Schicht für stabiles Training angewendet.
|
|
- **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**
|
|
|
|
1. **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.
|
|
2. **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.
|
|
|
|
> [!NOTE]
|
|
> Der Transformer-Block gruppiert alle Netzwerke zusammen und wendet einige **Normalisierungen** und **Dropouts** an, um die Trainingsstabilität und -ergebnisse zu verbessern.\
|
|
> Beachten Sie, wie 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 "so viel" beitragen wie die letzten.
|
|
|
|
### **GPTModel**
|
|
|
|
_Größen wurden als Kommentare hinzugefügt, um die Größen der Matrizen besser zu verstehen:_
|
|
```python
|
|
# From https://github.com/rasbt/LLMs-from-scratch/tree/main/ch04
|
|
class GPTModel(nn.Module):
|
|
def __init__(self, cfg):
|
|
super().__init__()
|
|
self.tok_emb = nn.Embedding(cfg["vocab_size"], cfg["emb_dim"])
|
|
# shape: (vocab_size, emb_dim)
|
|
|
|
self.pos_emb = nn.Embedding(cfg["context_length"], cfg["emb_dim"])
|
|
# shape: (context_length, emb_dim)
|
|
|
|
self.drop_emb = nn.Dropout(cfg["drop_rate"])
|
|
|
|
self.trf_blocks = nn.Sequential(
|
|
*[TransformerBlock(cfg) for _ in range(cfg["n_layers"])]
|
|
)
|
|
# Stack of TransformerBlocks
|
|
|
|
self.final_norm = LayerNorm(cfg["emb_dim"])
|
|
self.out_head = nn.Linear(cfg["emb_dim"], cfg["vocab_size"], bias=False)
|
|
# shape: (emb_dim, vocab_size)
|
|
|
|
def forward(self, in_idx):
|
|
# in_idx shape: (batch_size, seq_len)
|
|
batch_size, seq_len = in_idx.shape
|
|
|
|
# Token embeddings
|
|
tok_embeds = self.tok_emb(in_idx)
|
|
# shape: (batch_size, seq_len, emb_dim)
|
|
|
|
# Positional embeddings
|
|
pos_indices = torch.arange(seq_len, device=in_idx.device)
|
|
# shape: (seq_len,)
|
|
pos_embeds = self.pos_emb(pos_indices)
|
|
# shape: (seq_len, emb_dim)
|
|
|
|
# Add token and positional embeddings
|
|
x = tok_embeds + pos_embeds # Broadcasting over batch dimension
|
|
# x shape: (batch_size, seq_len, emb_dim)
|
|
|
|
x = self.drop_emb(x) # Dropout applied
|
|
# x shape remains: (batch_size, seq_len, emb_dim)
|
|
|
|
x = self.trf_blocks(x) # Pass through Transformer blocks
|
|
# x shape remains: (batch_size, seq_len, emb_dim)
|
|
|
|
x = self.final_norm(x) # Final LayerNorm
|
|
# x shape remains: (batch_size, seq_len, emb_dim)
|
|
|
|
logits = self.out_head(x) # Project to vocabulary size
|
|
# logits shape: (batch_size, seq_len, vocab_size)
|
|
|
|
return logits # Output shape: (batch_size, seq_len, vocab_size)
|
|
```
|
|
#### **Zweck und Funktionalität**
|
|
|
|
- **Embedding-Schichten:**
|
|
- **Token-Embeddings (`tok_emb`):** Wandelt Token-Indizes in Embeddings um. Zur Erinnerung, dies sind die Gewichte, die jeder Dimension jedes Tokens im Vokabular zugewiesen werden.
|
|
- **Positions-Embeddings (`pos_emb`):** Fügt den Embeddings 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 Embeddings zur Regularisierung angewendet.
|
|
- **Transformer-Blöcke (`trf_blocks`):** Stapel von `n_layers` Transformer-Blöcken zur Verarbeitung von Embeddings.
|
|
- **Finale Normalisierung (`final_norm`):** Schichtnormalisierung vor der Ausgabeschicht.
|
|
- **Ausgabeschicht (`out_head`):** Projiziert die finalen versteckten Zustände auf die Größe des Vokabulars, um Logits für die Vorhersage zu erzeugen.
|
|
|
|
> [!NOTE]
|
|
> Das Ziel dieser Klasse ist es, alle anderen genannten Netzwerke zu **benutzen, 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 verwenden wird, wie angegeben** 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 **Normalisierungs**-Schicht **vor** der **Ausgabe** hinzugefügt und eine finale lineare Schicht wird am Ende angewendet, um die Ergebnisse mit den richtigen Dimensionen zu erhalten. Beachten Sie, dass jeder finale Vektor die Größe des verwendeten Vokabulars hat. Dies liegt daran, dass versucht wird, eine Wahrscheinlichkeit pro mögliches 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:
|
|
```python
|
|
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
|
|
```
|
|
### **Schritt-für-Schritt-Berechnung**
|
|
|
|
#### **1. Einbettungsschichten: Token-Einbettung & Positions-Einbettung**
|
|
|
|
- **Schicht:** `nn.Embedding(vocab_size, emb_dim)`
|
|
- **Parameter:** `vocab_size * emb_dim`
|
|
```python
|
|
token_embedding_params = 50257 * 768 = 38,597,376
|
|
```
|
|
- **Schicht:** `nn.Embedding(context_length, emb_dim)`
|
|
- **Parameter:** `context_length * emb_dim`
|
|
```python
|
|
position_embedding_params = 1024 * 768 = 786,432
|
|
```
|
|
**Gesamtzahl der Einbettungsparameter**
|
|
```python
|
|
embedding_params = token_embedding_params + position_embedding_params
|
|
embedding_params = 38,597,376 + 786,432 = 39,383,808
|
|
```
|
|
#### **2. Transformer-Blöcke**
|
|
|
|
Es gibt 12 Transformer-Blöcke, daher berechnen wir die Parameter für einen Block und multiplizieren dann mit 12.
|
|
|
|
**Parameter pro Transformer-Block**
|
|
|
|
**a. Multi-Head Attention**
|
|
|
|
- **Komponenten:**
|
|
- **Query Linear Layer (`W_query`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
|
- **Key Linear Layer (`W_key`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
|
- **Value Linear Layer (`W_value`):** `nn.Linear(emb_dim, emb_dim, bias=False)`
|
|
- **Output Projection (`out_proj`):** `nn.Linear(emb_dim, emb_dim)`
|
|
- **Berechnungen:**
|
|
|
|
- **Jeder von `W_query`, `W_key`, `W_value`:**
|
|
|
|
```python
|
|
qkv_params = emb_dim * emb_dim = 768 * 768 = 589,824
|
|
```
|
|
|
|
Da es drei solcher Schichten gibt:
|
|
|
|
```python
|
|
total_qkv_params = 3 * qkv_params = 3 * 589,824 = 1,769,472
|
|
```
|
|
|
|
- **Output Projection (`out_proj`):**
|
|
|
|
```python
|
|
out_proj_params = (emb_dim * emb_dim) + emb_dim = (768 * 768) + 768 = 589,824 + 768 = 590,592
|
|
```
|
|
|
|
- **Gesamtzahl der Multi-Head Attention-Parameter:**
|
|
|
|
```python
|
|
mha_params = total_qkv_params + out_proj_params
|
|
mha_params = 1,769,472 + 590,592 = 2,360,064
|
|
```
|
|
|
|
**b. FeedForward-Netzwerk**
|
|
|
|
- **Komponenten:**
|
|
- **Erste Linear Layer:** `nn.Linear(emb_dim, 4 * emb_dim)`
|
|
- **Zweite Linear Layer:** `nn.Linear(4 * emb_dim, emb_dim)`
|
|
- **Berechnungen:**
|
|
|
|
- **Erste Linear Layer:**
|
|
|
|
```python
|
|
ff_first_layer_params = (emb_dim * 4 * emb_dim) + (4 * emb_dim)
|
|
ff_first_layer_params = (768 * 3072) + 3072 = 2,359,296 + 3,072 = 2,362,368
|
|
```
|
|
|
|
- **Zweite Linear Layer:**
|
|
|
|
```python
|
|
ff_second_layer_params = (4 * emb_dim * emb_dim) + emb_dim
|
|
ff_second_layer_params = (3072 * 768) + 768 = 2,359,296 + 768 = 2,360,064
|
|
```
|
|
|
|
- **Gesamtzahl der FeedForward-Parameter:**
|
|
|
|
```python
|
|
ff_params = ff_first_layer_params + ff_second_layer_params
|
|
ff_params = 2,362,368 + 2,360,064 = 4,722,432
|
|
```
|
|
|
|
**c. Layer-Normalisierungen**
|
|
|
|
- **Komponenten:**
|
|
- Zwei `LayerNorm`-Instanzen pro Block.
|
|
- Jede `LayerNorm` hat `2 * emb_dim` Parameter (Skalierung und Verschiebung).
|
|
- **Berechnungen:**
|
|
|
|
```python
|
|
layer_norm_params_per_block = 2 * (2 * emb_dim) = 2 * 768 * 2 = 3,072
|
|
```
|
|
|
|
**d. Gesamtparameter pro Transformer-Block**
|
|
```python
|
|
pythonCopy codeparams_per_block = mha_params + ff_params + layer_norm_params_per_block
|
|
params_per_block = 2,360,064 + 4,722,432 + 3,072 = 7,085,568
|
|
```
|
|
**Gesamtparameter für alle Transformatorblöcke**
|
|
```python
|
|
pythonCopy codetotal_transformer_blocks_params = params_per_block * n_layers
|
|
total_transformer_blocks_params = 7,085,568 * 12 = 85,026,816
|
|
```
|
|
#### **3. Finale Schichten**
|
|
|
|
**a. Finale Schichtnormalisierung**
|
|
|
|
- **Parameter:** `2 * emb_dim` (Skalierung und Verschiebung)
|
|
```python
|
|
pythonCopy codefinal_layer_norm_params = 2 * 768 = 1,536
|
|
```
|
|
**b. Ausgabeprojektionsebene (`out_head`)**
|
|
|
|
- **Ebene:** `nn.Linear(emb_dim, vocab_size, bias=False)`
|
|
- **Parameter:** `emb_dim * vocab_size`
|
|
```python
|
|
pythonCopy codeoutput_projection_params = 768 * 50257 = 38,597,376
|
|
```
|
|
#### **4. Zusammenfassung aller Parameter**
|
|
```python
|
|
pythonCopy codetotal_params = (
|
|
embedding_params +
|
|
total_transformer_blocks_params +
|
|
final_layer_norm_params +
|
|
output_projection_params
|
|
)
|
|
total_params = (
|
|
39,383,808 +
|
|
85,026,816 +
|
|
1,536 +
|
|
38,597,376
|
|
)
|
|
total_params = 163,009,536
|
|
```
|
|
## Text generieren
|
|
|
|
Ein Modell, das das nächste Token wie das vorherige vorhersagt, benötigt lediglich die letzten Token-Werte aus der Ausgabe (da dies die Werte des vorhergesagten Tokens sein werden), was ein **Wert pro Eintrag im Vokabular** ist. Dann wird die `softmax`-Funktion verwendet, um die Dimensionen in Wahrscheinlichkeiten zu normalisieren, die sich zu 1 summieren, und anschließend wird der Index des größten Eintrags ermittelt, der der Index des Wortes im Vokabular sein wird.
|
|
|
|
Code von [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch04/01_main-chapter-code/ch04.ipynb):
|
|
```python
|
|
def generate_text_simple(model, idx, max_new_tokens, context_size):
|
|
# idx is (batch, n_tokens) array of indices in the current context
|
|
for _ in range(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 context
|
|
idx_cond = idx[:, -context_size:]
|
|
|
|
# Get the predictions
|
|
with 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 probabilities
|
|
probas = torch.softmax(logits, dim=-1) # (batch, vocab_size)
|
|
|
|
# Get the idx of the vocab entry with the highest probability value
|
|
idx_next = torch.argmax(probas, dim=-1, keepdim=True) # (batch, 1)
|
|
|
|
# Append sampled index to the running sequence
|
|
idx = torch.cat((idx, idx_next), dim=1) # (batch, n_tokens+1)
|
|
|
|
return idx
|
|
|
|
|
|
start_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 dropout
|
|
|
|
out = 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]))
|
|
```
|
|
## Referenzen
|
|
|
|
- [https://www.manning.com/books/build-a-large-language-model-from-scratch](https://www.manning.com/books/build-a-large-language-model-from-scratch)
|