Query, Key, Value
Drei Lineare Abbildungen von demselben Eingang
Nach dem Embedding (Aktivität 4) trägt jede Position einen 768-dimensionalen Vektor x_t. Attention beginnt damit, drei unterschiedliche Projektionen von x zu erzeugen:
Q (Query): Was möchte diese Position wissen?
K (Schlüssel): Was bietet diese Position anderen Positionen?
V (Wert): Welchen Inhalt liefert diese Position, wenn sie beachtet wird?
Jede Projektion stammt aus einer gelernten Gewichtsmatrix:
Q = x · W_Q # W_Q shape: (d_model, d_k)
K = x · W_K # W_K shape: (d_model, d_k)
V = x · W_V # W_V shape: (d_model, d_k)
Drei Matrizen, alle durch Backpropagation trainiert. Ein Modell lernt: an dieser Position, welche Query holt am besten nützlichen vergangenen Kontext? Welcher Key bewirbt den Inhalt dieser Position gut? Welcher Value wird geliefert, wenn ausgewählt?
Eine Bibliotheks-Analogie
Stellen Sie sich einen Kartenkatalog in einer Bibliothek vor. Sie gehen mit einem Thema im Kopf hinein (Ihre Query). Jede Karte listet Schlüsselwörter auf (ein Key). Wenn Ihr Thema zu den Schlüsselwörtern einer Karte passt, greifen Sie zum Inhalt des Buches (ein Value). Attention macht das für jedes Token parallel: Jede Position fragt jede andere Position ab, bewertet die Übereinstimmung und holt eine gewichtete Kombination aus Value-Vektoren ab.
ANDREA-120M Dimensionen
| Menge | Wert | Hinweise |
|---|---|---|
| d_model | 768 | Vektorgroße an jeder Position |
| n_head | 12 | Parallele Attention-Heads |
| d_k | 64 | Dimension pro Head (= d_model / n_head) |
| T | 1024 | Kontextlänge |
d_k = d_model / n_head = 768 / 12 = 64. Jeder Head sieht einen 64-dimensionalen Ausschnitt eines vollständigen 768-dimensionalen Raums. Activity 6 (grow_a_language_model_multi_head) behandelt die Aufteilung pro Head im Detail.
Berechne d_k
Warum durch sqrt(d_k) teilen
Eine Score-Matrix
Sobald Q & K existieren (jede Form (T, d_k)), berechnet Attention eine Score-Matrix:
scores = Q · K^T # shape: (T, T)
scores[i, j] = wie stark sich die Query der Position i mit dem Key der Position j ausrichtet. Jedes (i, j)-Paar erhält einen Score: 1024 × 1024 = 1.048.576 Scores pro Attention-Head pro Forward-Pass.
Warum teilen
Skalarprodukte von zwei zufälligen d-dimensionalen Einheitsvektoren haben eine Größenordnung von sqrt(d). Ohne Skalierung wachsen die Scores mit d_k:
- d_k = 64: typische Skalarprodukte in der Größenordnung von 8.
- d_k = 256: typische Skalarprodukte in der Größenordnung von 16.
- d_k = 4096: typische Skalarprodukte in der Größenordnung von 64.
Große Scores erzeugen ein spitzes Softmax (eine Position dominiert, Gradienten verschwinden anderswo). Training stockt. Skalierung korrigiert eine Größenordnung:
scaled_scores = (Q · K^T) / sqrt(d_k)
Für ANDREA-120M ist sqrt(d_k) = sqrt(64) = 8. Jeder Score wird durch 8 geteilt. Die Größen bleiben unabhängig von d_k ungefähr im Einheitsmaßstab. Softmax bleibt gut verhalten. Gradienten fließen.
Vaswanis Originalbegründung
Aus Attention Is All You Need (2017): 'Für große Werte von d_k wachsen die Dot-Produkte stark in der Magnitude, was die Softmax-Funktion in Bereiche schiebt, in denen sie extrem kleine Gradienten hat.' Ein Divisor sqrt(d_k) wirkt diesem Wachstum entgegen.
Eine Code-Ansicht
Innerhalb von microgpt_cuda.cu erscheint dieses Scaling als literale Division:
scores[i][j] = dot(Q[i], K[j]) * (1.0f / sqrtf(d_k));
Eine Float-Multiplikation pro Score. Günstig. Kritisch.
Skalierung bei d_model = 4096
Warum Position i Position j > i nicht sehen kann
Eine Einschränkung aus der Generierung
ANDREA generiert ein Token nach dem anderen. Beim Inferenzprozess erzeugt Position 0 ein erstes Token, dann sieht Position 1 das Ausgabe-Token von Position 0 & erzeugt ein zweites Token, & so weiter. Ein Modell hat während der Generierung nie Zugriff auf zukünftige Tokens.
Das Training muss dies widerspiegeln. Wenn während des Trainings Position 5 auf Position 6 achten könnte, würde ein Modell einen Shortcut lernen: „Token 6 vorhersagen, indem man Token 6 liest“. Beim Inferenzprozess verschwindet dieser Shortcut (Token 6 existiert noch nicht). Das Verhalten eines Modells bei Training vs. Inferenz würde katastrophal auseinanderdriften.
Eine Maske
Eine causale Maske blockiert die Aufmerksamkeit von jeder Position i zu jeder Position j > i. Implementierung: scaled_scores[i][j] = -infinity setzen, wo immer j > i. Nach Softmax werden diese Einträge zu exp(-inf) = 0. Die Maske setzt die Aufmerksamkeit auf zukünftige Positionen sauber auf Null.
for i in range(T):
for j in range(T):
if j > i:
scaled_scores[i][j] = -1e9 # effektiv -inf
Nach Softmax (zeilenweise) summiert sich jede Zeile zu 1, aber nur die Einträge [0, i] tragen Wahrscheinlichkeitsmasse. Position i mischt Informationen nur aus vergangenen Positionen.
Visualisierung einer Maske
Eine Score-Matrix der Form (T, T) mit angewendeter Maske sieht wie eine untere Dreieckstruktur aus:
scaled_scores nach Maske, softmax zeilenweise:
Zeile 0: [1.0, 0, 0, 0, ...] # sieht nur sich selbst
Zeile 1: [0.4, 0.6, 0, 0, ...] # sieht Positionen 0, 1
Zeile 2: [0.2, 0.3, 0.5, 0, ...] # sieht 0, 1, 2
Zeile 3: [0.1, 0.2, 0.3, 0.4, ...] # sieht 0, 1, 2, 3
...
Strikt untere Dreieckswahrscheinlichkeitsverteilung pro Zeile. Zukunft bleibt unsichtbar.
Warum ein Decoder-Only Transformer das braucht
Decoder-only-Modelle wie ANDREA, GPT & LLaMA teilen alle ein Ziel: Vorhersage des nächsten Tokens aus der Vergangenheit. Eine causal mask macht dieses Ziel parallel trainierbar: Jede Position berechnet ihre eigene Next-Token-Vorhersage gleichzeitig, & keine Position schummelt, indem sie vorausschaut.
Maske & Geschmack
Von Scores zu Ausgabe
Softmax: Von Scores zu Wahrscheinlichkeiten
Gemaskierte, skalierte Scores liegen immer noch im Bereich reeller Zahlen. Softmax wandelt jede Zeile in eine Wahrscheinlichkeitsverteilung um:
A[i][j] = exp(scaled_scores[i][j]) / sum_k exp(scaled_scores[i][k])
Drei Eigenschaften ergeben sich:
- A[i][j] >= 0 für alle (i, j).
- sum_j A[i][j] = 1 für jede Zeile i.
- Größere Rohwerte erzeugen größere Wahrscheinlichkeiten (monoton).
Der Wahrscheinlichkeitsvektor der Zeile i sagt einem Modell: Wie stark sollte Position i auf jede vorherige Position achten, wenn sie ihren Output berechnet?
Gewichtete V-Summe
Ein finales Attention-Ausgabe für Position i:
output[i] = sum_j A[i][j] · V[j]
Jeder Wertvektor V[j] wird mit der Attention-Wahrscheinlichkeit A[i][j] gewichtet und dann summiert. Die Ausgabe der Position i kombiniert Wertvektoren aus jeder vorherigen Position, gewichtet nach Relevanz.
In Matrixform, alle Positionen auf einmal:
Attention(Q, K, V) = softmax(mask(Q · K^T / sqrt(d_k))) · V
Eine Zeile. Ein ganzer Attention-Mechanismus. Vaswani et al. schrieben diese Zeile 2017; Transformer haben sich seitdem nicht grundlegend verändert.
Ausgabeform pro Head
Ausgabe eines Attention-Heads: Form (T, d_k). Für ANDREA-120M: (1024, 64). Alle 12 Heads werden parallel berechnet; ihre Ausgaben werden zu (1024, 768) verkettet & in eine finale lineare Projektion (W_O) eingespeist, dann weiter zum MLP eines Transformer-Blocks.
Aktivität 6 (grow_a_language_model_multi_head) behandelt eine Multi-Head-Aufteilung. Aktivität 7 (grow_a_language_model_transformer_block) behandelt alles, was die Attention umgibt: Residualverbindungen, Layer Norm, MLP.