Startseite / Artikel / Warum Transformer Sinus und Kosinus für das Positional Encoding nutzen

Warum Transformer Sinus und Kosinus für das Positional Encoding nutzen

Eine Schritt-für-Schritt-Erklärung des sinusförmigen Positional Encodings: warum Attention die Wortreihenfolge ignoriert, Uhren mit verschiedenem Tempo, ein Rechenbeispiel mit 12 Dimensionen und Rotation.

1919 Wörter

Warum nutzen Transformer Sinus und Kosinus, um Positionen zu kodieren? Die Wörter einfach mit 1, 2, 3 durchzunummerieren wirkt viel simpler, und doch ist die Frage schwerer sauber zu beantworten, als es klingt.

Attention hat kein Gefühl für Reihenfolge

Als Operation behandelt Attention ihre Eingabe wie einen Sack voller Wörter. Gibt man einem Transformer „Katze frisst Maus“ und danach „Maus frisst Katze“, ganz ohne Positionsinformation, sieht die Attention-Schicht beide Male dieselben drei Vektoren. Sie kann nicht erkennen, was zuerst kam, und die Skalarprodukte Q · K fallen identisch aus. Das ist ein echtes Problem, denn der erste Satz ist ein Abendessen und der zweite eine Überraschung. Die Position muss irgendwo eingespeist werden, bevor die erste Attention-Schicht läuft.

Die naheliegende Lösung und warum sie scheitert

Nehmen wir die Phrase „cat and dog“, drei Tokens: cat auf Position 1, „and“ auf Position 2 und dog auf Position 3. Echte Wort-Embeddings sind nicht zwei oder drei Zahlen lang. Produktive Transformer nutzen Vektoren mit 512, 768 oder noch mehr Dimensionen. Hier sind cat und dog mit 768 Dimensionen, gezeigt werden die ersten sechs Einträge und der letzte, damit die Form erkennbar ist:

cat ≈ [ 0.82, -0.14, 0.36, 0.05, -0.71, 0.29, ... , -0.18 ]   (768 numbers)
dog ≈ [ 0.77, -0.09, 0.41, 0.11, -0.68, 0.24, ... , -0.15 ]   (768 numbers)

Die beiden Vektoren liegen nah beieinander, was Sinn ergibt: Beides sind Tiere, und der Embedding-Raum platziert bedeutungsähnliche Wörter nebeneinander. Doch nichts in einem der Vektoren sagt, wo das Wort im Satz steht. Genau das fehlt.

Als Erstes versuchen die meisten, die Positionsnummer als einfachen Skalar zu jedem Eintrag des Embeddings zu addieren.

cat at position 1 → cat + 1 → [ 1.82, 0.86, 1.36, 1.05, 0.29, 1.29, ... , 0.82 ]

Auf Position 1 sieht das harmlos aus. Stellen Sie sich nun dasselbe Wort auf Position 500 eines langen Dokuments vor:

cat at position 500 → cat + 500 → [ 500.82, 499.86, 500.36, 500.05, 499.29, 500.29, ... , 499.82 ]

Das erste Problem ist das große. Die Werte blähen sich auf, und zwar in jeder Dimension gleich. Was cat erkennbar machte, seine besondere Folge aus 0.82, -0.14, 0.36 und so weiter, also das, was „Katze“ von „Hund“ unterschied, wird neben 500 zum Rundungsfehler. Das Signal geht unter. Durch die maximale Sequenzlänge zu teilen, rettet es nicht, weil diese Länge beim Ausführen des Modells nicht bekannt ist und kurze Sätze und sehr lange Dokumente dann auf unvereinbaren Skalen lägen.

Das zweite Problem ist subtiler. Schon vor dem Aufblähen ist ein einziger gemeinsamer Versatz für alle Dimensionen eine dürftige Art, Position auszudrücken. Das Modell hat nur eine Zahl zur Verfügung, und das ist zu wenig Fläche, um Verhalten mit relativen Positionen aufzubauen, etwa „achte auf das Wort zwei Stellen weiter vorn“.

Die Position sollte also ein eigener Vektor sein, mit derselben Form wie das Wort-Embedding, damit man beide einfach addieren kann, und sie sollte sich ohne äußere Fakten wie die Sequenzlänge gutartig verhalten.

Verkleinern wir das Problem, um es klar zu sehen. Vergessen Sie 768 Dimensionen und stellen Sie sich nur 2 vor. Cat auf Position 1 braucht einen Vektor [x, y], „and“ auf Position 2 einen anderen Vektor [p, q]. Welche Zahlen sollen das sein? Man könnte sie von Hand wählen:

position 1 → [0.2, 0.8]
position 2 → [0.4, 0.6]
position 3 → [0.6, 0.4]

Jede Position bekommt zwar ein eigenes Paar, aber willkürlich. Nichts verbindet Position 4 so mit Position 3, wie sie auf einem Zahlenstrahl verbunden sind. Fragt man nach dem Vektor für Position 4.857, gibt es keine Regel, die man anwenden könnte; jemand hätte ihn vorab festlegen müssen, für jede Position, die je gebraucht werden könnte.

Gebraucht wird eine Funktion. Sie nimmt eine ganzzahlige Position, gibt ein Paar [x, y] zurück, legt benachbarte Positionen nah zusammen und lässt die Zahlen nie explodieren, wie groß die Position auch wird. Die Geometrie hat das längst gelöst. Es heißt Kreis.

Eine Uhr für die Position

Setzen Sie einen Punkt auf einen Kreis und lassen Sie ihn wandern. Seine Koordinaten sind (cos θ, sin θ), also liefert jeder Winkel eine eindeutige 2D-Koordinate, und benachbarte Winkel liefern benachbarte Koordinaten. Die Darstellung ist glatt. Statt „Position 17“ könnte man schreiben:

position 17 → (sin 17, cos 17)

Zwei Dimensionen kodieren eine Position, und das schlägt einen Skalar schon. Die Werte bleiben in [-1, 1], nichts explodiert, und die Positionen 17 und 18 sehen fast gleich aus.

Ein Haken bleibt: Eine einzelne Uhr wiederholt sich. Nach einer vollen Umdrehung sehen Position 1 und Position 1 + 2π gleich aus. Bei einem Satz mit 500 Tokens und einer Uhr mit einer Periode von etwa 6 gibt es überall Kollisionen.

Viele Uhren mit verschiedenem Tempo

Das ganze Schema funktioniert, weil viele Uhren gleichzeitig laufen, jede in ihrem eigenen Tempo. Eine dreht sich vielleicht etwa alle 10 Tokens, eine andere alle 100, eine weitere alle 1.000, noch eine alle 10.000. Für jede Position notiert man alle Anzeigen zusammen. Zwei Positionen können auf der schnellen Uhr übereinstimmen, aber nicht auf allen Uhren gleichzeitig, also wirkt die kombinierte Anzeige wie ein Fingerabdruck. GPS nutzt einen ähnlichen Trick mit mehreren Satelliten, und ein Barcode kodiert eine ID in Streifen unterschiedlicher Breite.

In der echten Formel liefert eine Embedding-Dimension d genau d/2 Uhrenpaare, wobei Paar i mit der Frequenz 1 / 10000^(2i/d) läuft. Ein kleines i ist eine schnelle Uhr, ein großes i eine langsame:

PE(pos, 2i)   = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

Das ist die ganze Formel.

Rechenbeispiel: „cat and dog“ in 12 Dimensionen

Die Mathematik nur abstrakt anzustarren bringt wenig, also rechnen wir. Mit d = 12 gibt es 6 Uhrenpaare mit jeweils eigener Frequenz, Paar 1 ist das schnellste und Paar 6 das langsamste:

pair 1: 10000^0/12  ≈ 1        (fastest)
pair 2: 10000^2/12  ≈ 21.54
pair 3: 10000^4/12  ≈ 464.16
pair 4: 10000^6/12  ≈ 1000
pair 5: 10000^8/12  ≈ 2154.43
pair 6: 10000^10/12 ≈ 4641.59  (slowest)

Nehmen wir Paar 1 und cat auf Position 1: Die Formel ergibt

Eine Spalte ist der vollständige 12-dimensionale PE-Vektor eines Wortes, und in den Zeilen wird es interessant:

  • Zeile 1 schwankt stark zwischen den Wörtern: 0.84, dann 0.91, dann 0.14. Die schnelle Uhr dreht in nur zwei Ticks fast eine ganze Runde.
  • Zeile 6 bewegt sich kaum und zeigt dreimal hintereinander 1.0000. Die langsame Uhr hat noch nicht nennenswert getickt. Sie ist dafür da, weit auseinanderliegende Positionen wie 3 und 3000 zu unterscheiden.
  • Jeder Eintrag ist vollständig deterministisch. Nichts in der Tabelle wird gelernt.

Die Eingabe des Transformers ist dann eine schlichte elementweise Summe:

input = word_embedding(cat) + PE(1)

Gleiche Form, keine raffinierte Verschmelzung. Um die tatsächliche Ausgabe zu sehen, werden die Wort-Embeddings auf die 12 Dimensionen des Spielzeug-PE verkleinert (ein echtes Modell nutzt seine echten 768-dimensionalen Vektoren, und diese Zahlen sind nur erfunden, um den Mechanismus zu zeigen):

cat_embedding ≈ [ 0.20,  0.70, -0.10,  0.40,  0.15, -0.30,  0.55,  0.10, -0.20,  0.05,  0.30, -0.15]
dog_embedding ≈ [ 0.18,  0.68, -0.05,  0.35,  0.20, -0.25,  0.50,  0.15, -0.18,  0.08,  0.28, -0.10]

Wie bei den 768-dimensionalen Versionen liegen cat und dog anfangs nah beieinander, weil beides Tiere sind. Nun addieren wir zu jedem Wort sein PE aus der Tabelle, Dimension für Dimension:

cat_final = cat_embedding + PE(1)
          = [1.0415, 1.2403, -0.0536, 1.3989, 0.1522, 0.7000, 0.5510, 1.1000, -0.1995, 1.0500, 0.3002, 0.8500]
dog_final = dog_embedding + PE(3)
          = [0.3211, -0.3100, 0.0888, 1.3403, 0.2065, 0.7500,
             0.5030, 1.1500, -0.1786, 1.0800, 0.2806, 0.9000]

Diese beiden Vektoren erreichen tatsächlich die erste Attention-Schicht. Vergleichen Sie Dimension 2: Dort waren cat und dog fast gleich (0.70 gegenüber 0.68), nach dem Positionssignal liegen sie bei 1.24 gegenüber -0.31, Welten auseinander. Das ist die schnelle Uhr (Paar 1, Position 1 gegenüber Position 3), die wie gewollt arbeitet. Sie hinterlässt einen so deutlichen Positionsstempel, dass „cat, früh im Satz“ und „dog, später im Satz“ klar verschiedene Vektoren werden, obwohl die Wörter selbst Ähnliches bedeuten.

Woher weiß das Modell, dass [0.84, 0.54, ...] Position 1 bedeutet?

Gar nicht. Keine Dimension in diesem Vektor sagt „1“, und nichts dekodiert ihn. Das Modell muss die Positionsnummer nie zurückgewinnen, denn das ist nicht seine Aufgabe.

Die Aufgabe ist enger. Angenommen, die Query von dog trifft im Skalarprodukt auf den Key von cat. Zeigt der Score, dass die beiden Wörter zwei Plätze auseinanderliegen? Genau das bedeutet „Position verstehen“ auf dieser Schicht. Die Kodierung ist kein Rauschen. Sie ist eine glatte Mischung von Sinuskurven mit bekannten Frequenzen, sodass die gelernten Q- und K-Projektionen diese Muster aufgreifen und darauf reagieren können. Eine Query kann am Ende „bevorzuge den Key, der eine Stelle weiter vorn steht“ bedeuten, ohne dass je eine Variable namens Position beteiligt ist.

Gesichtserkennung bietet einen handlichen Vergleich. Ein 512-dimensionales Gesichts-Embedding hat keine einzelne Dimension mit dem Namen der Person. Der ganze Vektor zusammen ist diese Person. Der 12-dimensionale Vektor PE(1) funktioniert genauso: Als Ganzes ist er Position 1.

Warum Sinus und Kosinus zusammen

Diesen Teil erklärt man meist mit Handwedeln. Der eigentliche Grund ist, dass das Paar zusammen eine Darstellung der Position liefert, die sich gut mit Rotation verträgt. Die Additionstheoreme sagen:

sin(x + Δ) = sin(x)cos(Δ) + cos(x)sin(Δ)
cos(x + Δ) = cos(x)cos(Δ) − sin(x)sin(Δ)

Kennt man [sin(x), cos(x)], lässt sich [sin(x+Δ), cos(x+Δ)] über eine feste lineare Transformation berechnen, die nur von Δ abhängt und nicht von x. Eine Verschiebung um Δ ist also dieselbe Transformation, egal wo man startet. Dadurch kann Attention „schau Δ Tokens zurück“ als eine wiederverwendbare Operation lernen, ohne für jedes Positionspaar eine eigene Regel auswendig zu lernen. Sinus allein schafft das nicht, denn für die vollständige Rotation braucht man beide Hälften des Paares.

Was bei Backpropagation passiert

Werden die Positionskodierungen im Training per Gradientenabstieg angepasst? Bei der sinusförmigen Variante nicht. Sie hat keine lernbaren Parameter, und die Formel ist fest. Die Gradienten fließen durch die Summe (Eingabe = Embedding + PE), ohne auf der PE-Seite etwas zum Aktualisieren zu finden, ähnlich wie bei der Frage, ob die 5 in y = x + 5 trainiert wird.

Bei der Tabelle der Wort-Embeddings ist das anders. Jedes Mal, wenn cat in einem Batch auftaucht, bekommt seine Zeile in der Tabelle einen Gradienten und verschiebt sich.

Eine weitere Variante verdient Erwähnung, weil die meisten modernen Modelle tatsächlich sie nutzen: gelernte Positionen. Da gibt es gar keine Formel. Das Modell hält eine trainierbare Tabelle mit einer Zeile pro Position, bis zur maximalen Sequenzlänge und so breit wie die Modelldimension, und das Training entscheidet, was in Zeile 17 stehen soll. Darüber lässt sich leichter nachdenken, und innerhalb der im Training gesehenen Längen funktioniert es gut, aber auf längere Sequenzen lässt es sich nicht so dehnen wie sinusförmiges PE. Dieser Kompromiss ist, zusammen mit neueren Ansätzen wie ALiBi und RoPE, ein Thema für sich.