Modul 09 · Lektion 3

Mehrköpfige Attention und Positionskodierung

Ein Attention-Kopf kann die vorigen Zeichen nur nach einem Maßstab betrachten; mit mehreren Köpfen sieht man mehrere Dinge zugleich. Außerdem kann Attention die Reihenfolge der Zeichen nicht unterscheiden, deshalb muss man ihr die Position mitteilen. Diese Lektion macht beides im Experiment sichtbar – und stößt auf ein unerwartetes Ergebnis.

  • Etwa 40 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.

Die vorige Lektion hat einen Attention-Kopf geschrieben. Um daraus ein GPT zu bauen, fehlen noch zwei Dinge: mehrere Köpfe und Positionsinformation.

python heads_positions.py

Die Experimente dieser Lektion nutzen das Modell aus gpt.py, brauchen aber kein Training; untersucht werden Eigenschaften der Struktur selbst.

Mehrere Köpfe: mehrere Dinge zugleich sehen

Ein Zeichen muss oft mehr als eine Art Information von vorn einsammeln. Beim Schreiben von „尽“ in „白日依山尽“ (die weiße Sonne sinkt hinter die Berge) muss das Modell vielleicht zugleich wissen: welche Szenerie vorher kam (weiße Sonne, Berge), das wievielte Zeichen es ist (im Fünf-Zeichen-Gedicht kommt beim fünften Zeichen Reim oder Zäsur) und welche Wörter die vorige Zeile verwendet hat.

Ein Attention-Kopf hat nur einen Satz q und k, bewertet nur nach einem Maßstab und liefert einen Satz Gewichte. Mehrköpfige Attention geht so vor: Den Vektor in mehrere Abschnitte teilen, auf jedem Abschnitt einzeln Attention ausführen und die Ergebnisse wieder zusammensetzen. Jeder Kopf hat eigene q, k und v und kann lernen, auf anderes zu achten.

== 2. 多头注意力:32 维的向量切成 4 个头,每个头 8 维
  q 的形状 (1, 5, 32)
  拆成多个头之后 (1, 4, 5, 8):(句子数, 头数, 字数, 每个头的维度)
  注意力分数 (1, 4, 5, 5):每个头都有自己的一张 5×5 的表
  一个注意力层的参数:qkv 3168 个,proj 1056 个,头数多少都不影响这个数

In gpt.py braucht die Umsetzung nur ein paar zusätzliche Formänderungen:

q, k, v = self.qkv(x).split(C, dim=2)
# 拆成多个头:(B, T, C) -> (B, 头数, T, 每个头的维度)
q, k, v = (t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in (q, k, v))

self.qkv ist eine lineare Schicht, die q, k und v auf einmal berechnet; split trennt sie. Dann wird der 32-dimensionale Vektor als 4 Abschnitte zu je 8 Dimensionen betrachtet und die Dimension „Kopf“ nach vorn verschoben. Bewerten, Maskieren, Softmax und gewichtete Summe danach sind genau wie in der vorigen Lektion; die Matrixmultiplikation von PyTorch rechnet automatisch für jeden Kopf einzeln und erledigt die 4 Köpfe auf einmal.

Danach werden die Ergebnisse der 4 Köpfe wieder zu 32 Dimensionen zusammengesetzt und durch eine lineare Schicht proj gemischt, damit sich die von den einzelnen Köpfen gesammelten Informationen verbinden können:

out = out.transpose(1, 2).contiguous().view(B, T, C)  # 各个头拼回去
return self.proj(out)

Beachte die letzte Ausgabezeile: Die Zahl der Parameter hängt nicht von der Zahl der Köpfe ab. 4 Köpfe mit je 8 Dimensionen und 1 Kopf mit 32 Dimensionen nutzen gleich große Matrizen, nur anders aufgeteilt. Mehrere Köpfe bringen keine zusätzlichen Parameter und kaum zusätzlichen Rechenaufwand, lassen das Modell aber mehrere verschiedene Arten der Aufmerksamkeit zugleich lernen.

Unser GPT nutzt 4 Köpfe mit je 32 Dimensionen (Vektoren insgesamt 128-dimensional). Große Modelle haben meist Dutzende Köpfe mit je 64 oder 128 Dimensionen.

Attention kennt keine Reihenfolge

Ein Blick zurück auf die Attention-Formel der vorigen Lektion: Die Bewertung schaut nur auf den Inhalt von q und k, die gewichtete Summe nur auf den Inhalt von v; nirgends kommt vor, „an welcher Position dieses Zeichen steht“.

Das heißt: Für die Attention gibt es zwischen „白日依山尽“ und „山依日白尽“ womöglich keinen Unterschied.

Experiment: Das Modell liest beide Zeilen; beide enden mit „尽“, die ersten vier Zeichen sind vertauscht. Wir vergleichen, wie stark sich die Ausgaben an der letzten Position unterscheiden (also die Punktwerte für das nächste Zeichen):

model.pos_emb.weight.zero_()  # 把位置嵌入清零,等于没有位置信息
a = model(torch.tensor([tok.encode("白日依山尽")]))[0][0, -1]
b = model(torch.tensor([tok.encode("山依日白尽")]))[0][0, -1]
== 1. '白日依山尽' 和 '山依日白尽',最后一个位置的输出差多少
  1 层:没有位置嵌入 3.7e-08,有位置嵌入 2.8e-03
  2 层:没有位置嵌入 1.9e-02,有位置嵌入 1.5e-02

Zuerst die erste Zeile, ein Modell mit nur 1 Schicht. Ohne Positionsinformation unterscheiden sich die Ausgaben beider Zeilen um 3,7e-08, also nur um Rundungsfehler der Gleitkommazahlen: völlig gleich. Die letzte Position hat dieselben fünf Zeichen vor sich, nur in anderer Reihenfolge. Attention bewertet sie und bildet die gewichtete Summe, und das Ergebnis hängt nicht von der Reihenfolge ab.

Für ein Sprachmodell ist das fatal. „白日依山尽“ ist ein Gedicht, „山依日白尽“ ergibt keinen Sinn; „我打你“ (ich schlage dich) und „你打我“ (du schlägst mich) bedeuten das Gegenteil. Das Modell muss die Reihenfolge kennen.

Die unerwartete zweite Zeile

Die zweite Zeile ist ein Modell mit 2 Schichten. Nach der obigen Überlegung müssten die Ausgaben beider Zeilen ohne Positions-Embedding ebenfalls gleich sein. Das Experiment zeigt aber einen Unterschied von 1,9e-02, etwa so groß wie die 1,5e-02 mit Positions-Embedding.

Beim Schreiben dieses Experiments habe ich das auch nicht erwartet. Die Ursache ist die kausale Maske:

  • In Schicht 1 sieht jede Position nur sich selbst und die vorigen Zeichen. Die 2. Position von „白日依山尽“ sieht „白日“, die 2. Position von „山依日白尽“ sieht „山依“. Die Ausgaben von Schicht 1 an den Positionen 2, 3 und 4 unterscheiden sich also zwischen beiden Zeilen.
  • In Schicht 2 schaut die letzte Position auf die Ausgaben von Schicht 1 an den vorigen Positionen, und die unterscheiden sich bereits.

Die kausale Maske selbst verrät also die Reihenfolge: Wie viele Zeichen eine Position sehen kann, sagt ihr indirekt, an welcher Position sie steht. Ab zwei Schichten kann das Modell daraus etwas Positionsinformation gewinnen.

Das ist kein Zufall unseres Codes. 2022 hat ein Paper genau das untersucht (Haviv et al., „Transformer Language Models without Positional Encodings Still Learn Positional Information“) und festgestellt, dass kausale Sprachmodelle ganz ohne Positionskodierung ebenfalls Positionsinformation lernen und nur etwas schlechter abschneiden als mit Positionskodierung.

Die über die Maske „erratene“ Positionsinformation ist aber indirekt und unscharf. Echte Modelle fügen Positionsinformation ausdrücklich hinzu.

Positions-Embedding

Der einfachste Ansatz, den GPT-2 nutzt: Auch jede Position bekommt einen Vektor, der zum Vektor des Zeichens addiert wird.

self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)
...
pos = torch.arange(start, start + T, device=idx.device)
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))

pos_emb ist eine Tabelle mit 128 Zeilen: Zeile 0 ist der Vektor der Position 0, Zeile 1 der von Position 1 und so weiter. Diese Vektoren werden wie alle anderen Parameter durch Training gelernt.

Nach der Addition hat dasselbe Zeichen an verschiedenen Positionen verschiedene Vektoren. „白“ an Position 1 und an Position 4 bekommt zwei verschiedene Vektoren, und die Attention kann die Reihenfolge unterscheiden. Im Experiment oben stieg beim Modell mit 1 Schicht der Unterschied der Ausgaben mit Positions-Embedding von 3,7e-08 auf 2,8e-03: Das Modell kann die beiden Zeilen unterscheiden (das Modell ist noch nicht trainiert, deshalb ist der Unterschied klein und noch bedeutungslos).

Dieser Ansatz hat eine Grenze: Die Tabelle hat nur 128 Zeilen, das Modell kann also höchstens 128 Positionen verarbeiten. Für Positionen, die es beim Training nicht gesehen hat, gibt es keinen Vektor.

Rotary Position Embedding

Stand September 2026 nutzen die meisten gängigen Open-Source-Modelle (Llama, Qwen, DeepSeek und andere) nicht das Positions-Embedding von GPT-2, sondern Rotary Position Embedding (RoPE, aus dem Paper RoFormer von 2021).

Die Idee: Die Position wird nicht zum Vektor des Zeichens addiert, sondern vor der Berechnung der Attention-Punktwerte werden q und k je nach Position um einen Winkel „gedreht“. Je weiter hinten die Position, desto stärker die Drehung. Bilden q und k zweier Positionen dann ein Skalarprodukt, hängt das Ergebnis nur davon ab, wie weit sie auseinanderliegen, nicht von ihren absoluten Positionen.

„月“ steht ein Zeichen hinter „明“; ob diese beiden Zeichen am Anfang oder am Ende eines Gedichts vorkommen, ihre Beziehung ist dieselbe. Relative Positionen entsprechen den Regeln der Sprache besser als absolute und lassen sich leichter auf Texte übertragen, die länger sind als beim Training.

Das GPT dieses Kurses bleibt beim einfachsten Positions-Embedding, weil es in unseren Experimenten völlig ausreicht und leicht zu verstehen ist. In Übung 3 kannst du RoPE selbst ausprobieren.

Übungen

  1. Füg in heads_positions.py eine weitere Gruppe hinzu: Modell mit 3 Schichten ohne Positions-Embedding. Wie stark unterscheiden sich die Ausgaben beider Zeilen?
  2. Ändere das Trainingsmodell in gpt.py von 4 Köpfen auf 1 und auf 8 (n_embd bleibt 128), trainiere mit train.py aus der nächsten Lektion jeweils 1000 Schritte und vergleiche die Validierungsverluste.
  3. Herausforderung: Füg nach dem Paper RoFormer oder dem Code eines Open-Source-Modells CausalSelfAttention Rotary Position Embedding hinzu, entferne pos_emb und sieh dir das Trainingsergebnis an.

Selbsttest

1. Wie funktioniert mehrköpfige Attention? Erhöht sie die Zahl der Parameter?

Der Vektor jedes Zeichens wird in mehrere Abschnitte geteilt, auf jedem Abschnitt wird einzeln Attention ausgeführt (jeweils mit eigenen q, k, v und Gewichten), dann werden die Ergebnisse zusammengesetzt und durch eine lineare Schicht gemischt. Die Zahl der Parameter ist dieselbe wie mit einem Kopf, nur die Matrizen sind anders aufgeteilt; der Vorteil ist, dass jeder Kopf lernen kann, auf anderes zu achten.

2. Warum braucht Attention Positionsinformation?

Bewertung und gewichtete Summe der Attention schauen nur auf den Inhalt der Vektoren, nicht auf die Position. Bei einem Modell mit nur einer Schicht ändert sich die Ausgabe an der letzten Position überhaupt nicht, wenn man die vorigen Zeichen umordnet. Die Bedeutung von Sprache hängt aber von der Reihenfolge ab, also muss Positionsinformation hinzugefügt werden.

3. Warum konnte im Experiment das Modell mit 2 Schichten ohne Positions-Embedding die Reihenfolge der beiden Zeilen trotzdem unterscheiden?

Die kausale Maske lässt jede Position nur sich selbst und die vorigen Zeichen sehen; in der ersten Schicht sehen verschiedene Positionen unterschiedlich viele Zeichen, und die Ausgaben hängen damit von der Reihenfolge ab. Liest die zweite Schicht diese Ausgaben, erhält sie indirekt Positionsinformation. Forschung zeigt, dass kausale Sprachmodelle auch ohne Positionskodierung Positionsinformation lernen, echte Modelle fügen Positionskodierung aber trotzdem ausdrücklich hinzu.

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…