Modul 09 · Lektion 2

Der Attention-Mechanismus

Um seine eigene Bedeutung zu verstehen, muss ein Zeichen sehen, welche Zeichen vor ihm stehen. Vom einfachsten „Mittelwert aller vorigen Zeichen“ aus kommen Schritt für Schritt Query, Key und Value, kausale Maske und Skalierung dazu, bis ein vollständiger Attention-Kopf entsteht, der mit der Implementierung von PyTorch abgeglichen wird.

  • Etwa 50 Minuten
  • Niveau: Vertiefung
  • Getestet: 2026-09-15 torch 2.14, CPU, fester Zufallsstartwert

Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.

Das „明“ in „明月“ (heller Mond) und das „明“ in „明天“ (morgen) bedeuten Verschiedenes. Wie ein Zeichen zu verstehen ist, hängt von den Zeichen um es herum ab.

Das Modell, das wir trainieren wollen, soll an jeder Position das nächste Zeichen vorhersagen. Soll es nach „床前明月“ (vor dem Bett das Licht des hellen Mondes …) „光“ (Licht) schreiben, muss es wissen, dass vorher „明月“ vorkam. Jede Position braucht also einen Weg, Informationen von den vorigen Zeichen einzusammeln.

Attention (attention) ist dieser Weg; sie ist der Kern des Transformers. Diese Lektion beginnt beim einfachsten Ansatz und baut sie Schritt für Schritt auf.

python attention.py

Der einfachste Ansatz: Mittelwert

Jedes Zeichen wird zunächst durch einen Vektor dargestellt (Vektor-Embeddings aus Modul 01, Lektion 5). Die einfachste Art, „Informationen von vorn einzusammeln“, ist der Mittelwert der Vektoren des eigenen und aller vorigen Zeichen.

Das geht mit einer einzigen Matrixmultiplikation. Man baut eine untere Dreiecksmatrix, in der jede Zeile nur an den vorderen Positionen Werte hat, die zusammen 1 ergeben:

x = torch.randn(T, 4)  # 5 个词,每个词一个 4 维向量(先随便取)
weights = torch.ones(T, T).tril()  # 下三角:第 i 行只有前 i+1 个位置是 1
weights = weights / weights.sum(dim=1, keepdim=True)  # 每行除以个数,变成平均
out = weights @ x
== 1. 用一个矩阵乘法,让每个位置得到自己和前面所有位置的平均
tensor([[1.00, 0.00, 0.00, 0.00, 0.00],
        [0.50, 0.50, 0.00, 0.00, 0.00],
        [0.33, 0.33, 0.33, 0.00, 0.00],
        [0.25, 0.25, 0.25, 0.25, 0.00],
        [0.20, 0.20, 0.20, 0.20, 0.20]])
  第 3 个位置('明')的结果 = 前三个向量的平均?True

Zeile i gibt an, wie viel das i-te Zeichen von jeder Position nimmt: Das 1. Zeichen sieht nur sich selbst, das 3. sieht die ersten drei gleichmäßig, das 5. alle fünf gleichmäßig. weights @ x berechnet die Ergebnisse aller Positionen auf einmal.

Merk dir diese Schreibweise als Matrixmultiplikation; auch die spätere Attention hat diese Form: eine Gewichtsmatrix mal eine Menge von Vektoren. Anders ist nur, woher die Gewichte kommen.

Das Problem des Mittelwerts ist offensichtlich: Alle Zeichen werden gleich behandelt. „光“ will wissen, ob vorher ein „月“ (Mond) stand; „床“ (Bett) und „前“ (vor) sind dafür weniger wichtig. Die Gewichte sollten vom Inhalt abhängen.

Query und Key: jedes Zeichen entscheidet, wohin es schaut

Der Ansatz: Jedes Zeichen bekommt zwei Vektoren:

  • Query (query, q): welche Art Information ich suche.
  • Key (key, k): welche Art Information ich habe.

Wie stark ein Zeichen ein anderes „beachtet“, wird mit dem Skalarprodukt aus der eigenen q und der k des anderen bewertet. Je ähnlicher die Richtung zweier Vektoren, desto größer das Skalarprodukt.

Zuerst ein Test mit handgemachten Vektoren. Die Vektoren haben zwei Dimensionen; die erste steht für „hat mit dem Himmel zu tun“, die zweite für „hat mit dem Ort zu tun“. Die Query von „光“ ist [1, 0], es sucht also etwas, das mit dem Himmel zu tun hat:

k = torch.tensor([[0.1, 0.9],   # 床:一个地点
                  [0.0, 1.0],   # 前:一个方位
                  [0.9, 0.1],   # 明:和天空、光有关
                  [1.0, 0.0],   # 月:天空里的东西
                  [0.8, 0.2]])  # 光
q_guang = torch.tensor([1.0, 0.0])  # "光"想找的是:和天空有关的东西
scores = k @ q_guang
== 2. 点积打分:查询(q)和每个键(k)越像,分数越高
  '光' 对每个词的分数: {'床': 0.1, '前': 0.0, '明': 0.9, '月': 1.0, '光': 0.8}
  softmax 之后的权重:   {'床': 0.12, '前': 0.11, '明': 0.26, '月': 0.29, '光': 0.23}
  分数放大 5 倍再 softmax: {'床': 0.01, '前': 0.0, '明': 0.3, '月': 0.5, '光': 0.18}

Die Punktwerte werden per Softmax zu Gewichten (Softmax aus Modul 08, Lektion 5), die zusammen 1 ergeben. „月“ hat den höchsten Punktwert und bekommt das größte Gewicht.

Beachte die letzte Zeile: Werden alle Punktwerte verfünffacht, sind die Gewichte nach Softmax viel konzentrierter, und „月“ allein bekommt die Hälfte. Die Größe der Punktwerte entscheidet, ob die Attention „gleichmäßig verteilt“ ist oder „nur auf eines schaut“. Das wird bei der Skalierung weiter unten wichtig.

In echten Modellen sind q und k nicht handgemacht, sondern werden mit zwei Matrizen aus dem Vektor jedes Zeichens berechnet; die Parameter der Matrizen werden durch Training gelernt. Das Modell lernt selbst, welche Art Zeichen welche Art Information suchen soll.

Value: der eigentlich übertragene Inhalt

Nach dem Bewerten ist entschieden, wie viel von jeder Position genommen wird. Aber was wird genommen? Ein dritter Vektor: der Value (value, v), der sagt: „Wenn du mich beachtest, bekommst du diese Information.“

Warum nicht direkt den ursprünglichen Vektor nehmen? Weil „die Information zum Abgleichen“ und „die Information zum Übertragen“ nicht dieselbe sein müssen. Trennt man sie, ist das Modell flexibler.

Die drei Vektoren zusammen sind die ganze Attention:

权重 = softmax(q 和每个 k 的点积)
输出 = 用这些权重,把每个位置的 v 加起来

Kausale Maske: nicht nach hinten spähen

Beim Training soll das Modell an jeder Position das nächste Zeichen vorhersagen. Könnte die 3. Position das 4. Zeichen sehen, könnte sie die Antwort direkt „abschreiben“ und würde nichts lernen. Also darf jede Position nur sich selbst und die vorigen Positionen sehen.

Dazu setzt man vor dem Softmax die Punktwerte späterer Positionen auf minus unendlich. Minus unendlich exponenziert ergibt 0, nach dem Softmax ist das Gewicht also 0:

scores = q @ k.T / math.sqrt(8)
mask = torch.ones(T, T, dtype=torch.bool).tril()
scores = scores.masked_fill(~mask, float("-inf"))
== 3. 因果掩码:把后面位置的分数设成负无穷,softmax 之后权重就是 0
tensor([[1.00, 0.00, 0.00, 0.00, 0.00],
        [0.96, 0.04, 0.00, 0.00, 0.00],
        [0.24, 0.26, 0.50, 0.00, 0.00],
        [0.09, 0.39, 0.43, 0.09, 0.00],
        [0.02, 0.14, 0.02, 0.09, 0.73]])

Verglichen mit der Mittelwertmatrix aus dem ersten Abschnitt: dieselbe Form, beides untere Dreiecke, jede Zeile ergibt zusammen 1. Der Unterschied: Die Gewichte jeder Zeile sind nicht mehr gleich, sondern werden von q und k bestimmt. Hier sind q und k zufällig, deshalb wirken die Gewichte regellos; nach dem Training bekommen sie eine Bedeutung.

Wegen dieser Maske heißt diese Attention kausale Self-Attention: Self-Attention heißt, dass q, k und v alle aus demselben Text stammen; kausal heißt, dass nur die Vergangenheit sichtbar ist, nicht die Zukunft. Modelle wie GPT, die „Zeichen für Zeichen weiterschreiben“, nutzen alle diese Form.

Warum man durch die Wurzel der Dimension teilt

Im Code oben steht ein / math.sqrt(8); 8 ist die Dimension von q und k. Dieser Schritt heißt Skalierung. Sehen wir, was ohne ihn passiert:

== 4. 为什么要除以 √d:维度越大,点积的数值越大,softmax 会变得非常极端
  d=   16  点积的标准差   3.89(√d =  4.00)  每行最大权重的平均:不缩放 0.79,除以 √d 后 0.53
  d=   64  点积的标准差   8.17(√d =  8.00)  每行最大权重的平均:不缩放 0.86,除以 √d 后 0.47
  d=  256  点积的标准差  16.08(√d = 16.00)  每行最大权重的平均:不缩放 0.95,除以 √d 后 0.47
  d= 1024  点积的标准差  32.53(√d = 32.00)  每行最大权重的平均:不缩放 0.92,除以 √d 后 0.39

Das Skalarprodukt zweier Zufallsvektoren ist die Summe von d Produkten. Je mehr Summanden, desto stärker schwankt das Ergebnis; die Standardabweichung ist etwa die Wurzel aus d: Bei d=256 hat das Skalarprodukt eine Standardabweichung von 16.

Schwanken die Punktwerte stark, wird Softmax extrem, wie beim „Verfünffachen“ im zweiten Abschnitt: Ohne Skalierung liegt das größte Gewicht jeder Zeile im Schnitt bei 0,8 bis 0,95, fast die gesamte Attention geht an eine Position. Zu Beginn des Trainings ist das sehr schlecht: Wo Gewichte nahe 0 oder 1 liegen, ist der Gradient von Softmax sehr klein, und das Modell lernt kaum.

Teilt man durch die Wurzel aus d, liegt die Standardabweichung der Punktwerte wieder bei etwa 1, das größte Gewicht jeder Zeile zwischen 0,4 und 0,5, die Attention ist verteilt, die Gradienten sind normal, und das Modell kann nach und nach lernen, worauf es achten soll.

Ein vollständiger Attention-Kopf

Alles zusammengesetzt:

d_model, d_head = 16, 8
x = torch.randn(T, d_model)
W_q, W_k, W_v = (torch.randn(d_model, d_head) / math.sqrt(d_model) for _ in range(3))
q, k, v = x @ W_q, x @ W_k, x @ W_v
scores = (q @ k.T / math.sqrt(d_head)).masked_fill(~mask, float("-inf"))
out = F.softmax(scores, dim=-1) @ v
== 5. 一个完整的注意力头:q、k、v 都由同一个输入经过不同的矩阵得到
  输入 (5, 16) → q、k、v 各 (5, 8) → 输出 (5, 8)
  和 PyTorch 自带的 scaled_dot_product_attention 比,最大差别 2.4e-07

Fünf Zeilen Code: drei Matrizen machen aus der Eingabe q, k und v; bewerten, skalieren, maskieren; Softmax; gewichtete Summe. Das Ergebnis stimmt mit dem eingebauten scaled_dot_product_attention von PyTorch überein (der Unterschied liegt in der siebten Nachkommastelle, ein Rundungsfehler durch eine andere Reihenfolge der Gleitkommaberechnungen).

W_q, W_k und W_v sind die Parameter, die dieser Attention-Kopf lernt. Beim Training werden sie wie alle anderen Parameter per Backpropagation und Gradientenabstieg aktualisiert.

Diese Formel stammt aus dem Paper „Attention Is All You Need“ von 2017 und lautet mathematisch softmax(QKᵀ/√d)V. Jetzt weißt du, wofür jedes Symbol darin steht.

Der Preis der Attention

Jede Position muss mit allen vorigen Positionen bewertet werden. Bei Sequenzlänge T ist die Tabelle der Punktwerte T×T groß. Verdoppelt sich die Länge, vervierfachen sich Rechenaufwand und Speicherbedarf dieser Tabelle.

Das ist einer der grundlegenden Gründe, warum lange Kontexte teuer sind (Kontext und Kosten aus Modul 01, Lektion 4). Um Kontexte von Hunderttausenden oder Millionen Tokens zu unterstützen, haben die Anbieter die Berechnung der Attention stark optimiert, aber der Grundgedanke sind noch immer diese paar Zeilen.

Übungen

  1. Gib im zweiten Abschnitt „光“ die Query [0, 1] (es sucht etwas, das mit dem Ort zu tun hat). Wie ändern sich die Gewichte?
  2. Entferne im fünften Abschnitt die kausale Maske und vergleiche mit scaled_dot_product_attention(..., is_causal=False).
  3. Ändere im fünften Abschnitt die Sequenzlänge von 5 auf 1000, 2000 und 4000 und miss mit time.time() die Zeit für jede Länge. Wächst sie ungefähr quadratisch?

Selbsttest

1. Welche Rolle spielen Query, Key und Value jeweils?

Die Query steht für „was ich suche“, der Key für „was ich habe“. Das Skalarprodukt aus der Query einer Position und den Keys aller Positionen ergibt ihre Aufmerksamkeitspunktwerte für jede Position, die per Softmax zu Gewichten werden. Der Value steht für „die Information, die man bekommt, wenn man mich beachtet“; die Ausgabe ist die mit diesen Gewichten gebildete Summe der Values aller Positionen.

2. Wie wird die kausale Maske umgesetzt? Wozu braucht man sie?

Vor dem Softmax werden die Punktwerte aller Positionen hinter der jeweiligen Position auf minus unendlich gesetzt; nach dem Softmax ist ihr Gewicht 0. Man braucht sie, weil das Modell lernen soll, das nächste Zeichen vorherzusagen; könnte es spätere Zeichen sehen, würde es die Antwort direkt abschreiben und nichts lernen.

3. Warum teilt man die Attention-Punktwerte durch die Wurzel der Dimension?

Das Skalarprodukt zweier d-dimensionaler Vektoren hat eine Standardabweichung von etwa der Wurzel aus d; je größer die Dimension, desto stärker schwanken die Punktwerte, und die Gewichte nach Softmax konzentrieren sich extrem auf eine Position, die Gradienten werden sehr klein, und das Modell ist schwer zu trainieren. Teilt man durch die Wurzel aus d, liegt die Standardabweichung der Punktwerte wieder bei etwa 1.

Fragen und Diskussion

Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.

Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.

Diskussion wird geladen…