nlm · Teil 5 von 5

Warum mein Modell die Wortstellung kennen muss

Eine Token-Embedding-Tabelle allein weiß nicht, ob ein Wort am Satzanfang oder am Satzende steht. Ich habe nlm's echte, trainierte Positions-Embeddings geöffnet und nachgemessen, was sie tatsächlich über "Nähe" gelernt haben ohne dass es dem Modell je gesagt wurde, was Nähe bedeutet.

Inhalt
  1. Die Frage, die die Embedding-Tabelle allein nicht beantworten kann
  2. Wie nlm eine Position tatsächlich codiert
  3. Was diese Positions-Vektoren nach dem Training tatsächlich gelernt haben
  4. Der Preis dieser Entscheidung
  5. Nächster Beitrag

Ich habe bereits gezeigt, dass nlm's Token-Embedding-Tabelle gelernt hat, ähnlich verwendete Wörter nah beieinander zu platzieren: "dog" und "cat" liegen sich im 256-dimensionalen Raum näher (Cosine-Ähnlichkeit +0,55) als "rabbit" und "sad" (−0,04). Aber diese Tabelle hat eine blinde Stelle, die ich bisher übersprungen habe.


Die Frage, die die Embedding-Tabelle allein nicht beantworten kann

Frage: Weiß nlm's Token-Embedding-Tabelle, welcher Satz gemeint ist?

Nimm diese beiden Sätze:

"The dog bites the man."
"The man bites the dog."

Exakt dieselben Wörter, komplett andere Bedeutung.

Hypothese: token_emb[token_ids] schaut für jede Token-ID ausschließlich in einer Tabelle nach, die nichts über die Reihenfolge weiß, in der die Tokens im Satz auftauchen. Jedes Vorkommen von "dog" bekommt exakt denselben 256-dimensionalen Vektor zurück — unabhängig davon, ob es das Subjekt oder das Objekt des Satzes ist.

Experiment: Ein Blick in nlm's echten Tokenizer bestätigt das direkt. "dog" codiert immer zur selben Token-ID:

tok.encode(" dog")   # -> [32, 569]
tok.encode(" man")   # -> [32, 453]
tok.encode(" bites") # -> [32, 1068, 339]

Ergebnis: Die führende 32 ist das Leerzeichen als eigenes Token (die IDs 0 bis 255 sind die Basis-Bytes, und 32 ist das Byte für das Leerzeichen); "dog" selbst ist die 569. Egal an welcher Position im Satz "dog" (Token-ID 569) steht — die Embedding-Tabelle liefert jedes Mal exakt denselben Vektor zurück. Die Information "zuerst kommt der Hund, dann beißt er" oder umgekehrt "zuerst wird gebissen, dann kommt der Hund" geht an dieser Stelle komplett verloren.

Entscheidung: Die Token-Embedding-Tabelle allein reicht nicht. nlm braucht eine zweite, unabhängige Informationsquelle, die ausschließlich die Position eines Tokens im Fenster beschreibt — unabhängig davon, welches Token dort steht.


Wie nlm eine Position tatsächlich codiert

nlm löst das mit einer zweiten Embedding-Tabelle, die nicht nach Token-ID, sondern nach Position nachschlägt:

tok_emb = params["token_emb"][token_ids]  # (batch, seq, d_model)
pos_emb = params["pos_emb"][:seq]         # (seq, d_model)
x = tok_emb + pos_emb

Die erste Zeile schlägt für jedes Token seinen Vektor nach, die zweite für jede Stelle im Text einen Positionsvektor (seq ist die Länge des Textes, batch die Zahl der gleichzeitig verarbeiteten Texte). Die dritte Zeile addiert beides. Danach steckt in jedem Vektor, welches Wort es ist und an welcher Stelle es steht, bevor das Modell damit weiterrechnet.

Zwei komplett getrennte Tabellen, die am Ende einfach addiert werden. Jede der context_length = 512 möglichen Positionen bekommt dabei ihren eigenen, 256-dimensionalen, lernbaren Vektor:

context_length  = 512
d_model         = 256

512 x 256 = 131.072 Parameter

Das ist die Entscheidung "Learned Absolute Positional Embeddings": Statt einer festen, mathematisch vorgegebenen Formel — wie den Sinus/Cosinus-Positionskodierungen aus dem ursprünglichen Transformer-Paper, die sich auf beliebig lange Sequenzen erweitern lassen — bekommt jede der 512 Positionen bei nlm einen eigenen, während des Trainings frei angepassten Vektor. Zusammen mit der Token-Embedding-Tabelle (1.048.576 Parameter) stecken damit knapp 1,18 Millionen von nlm's 10,66 Millionen Parametern — rund 11,1 % des gesamten Modells — allein in diesen beiden Nachschlagetabellen, bevor überhaupt ein einziger Transformer-Block gerechnet hat.


Was diese Positions-Vektoren nach dem Training tatsächlich gelernt haben

Frage: Eine feste Formel (Sinus/Cosinus) würde garantieren, dass nahe Positionen auch im Vektorraum nahe beieinander liegen. Bei frei gelernten Vektoren ist das nicht automatisch so. Hat also nlm diese Eigenschaft trotzdem selbst gelernt?

Hypothese: Wenn Positions-Nähe für das Modell überhaupt nützlich ist (zum Beispiel, um zu erkennen, welche Wörter unmittelbar aufeinander folgen), sollten benachbarte Positionen eine höhere Cosine-Ähnlichkeit zeigen als weit entfernte.

Experiment: Ich habe die Positions-Embedding-Tabelle aus nlm's Checkpoint (Schritt 100.550) geladen und die Cosine-Ähnlichkeit zwischen verschiedenen Positionspaaren berechnet:

pos_emb = checkpoint["params"]["pos_emb"]  # (512, 256)
cos_sim = lambda a, b: np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Eine Position ist dabei kein bestimmtes Wort. Position 250 heißt nur „die 251. Stelle in einem Text“: Mal steht dort „dog“, mal „man“, mal etwas ganz anderes, je nachdem, welchen Trainingstext nlm gerade liest. Der Vektor zu Position 250 kann deshalb nur etwas über die Stelle lernen, nicht über das Wort. Verglichen wird hier also, wie ähnlich sich zwei Stellen sind.

Ergebnis: Reale Zahlen, kein konstruiertes Beispiel:

Position  10 vs Position  11  (Abstand 1):   cos = +0,8098
Position 500 vs Position 511  (Abstand 11):  cos = +0,7499
Position 250 vs Position 251  (Abstand 1):   cos = +0,3211
Position 250 vs Position 260  (Abstand 10):  cos = +0,2806
---------------------------------------------------------
Position  10 vs Position 100  (Abstand 90):  cos = +0,3436
Position  10 vs Position 500  (Abstand 490): cos = -0,3789
Position 250 vs Position 500  (Abstand 250): cos = -0,2290
Position   0 vs Position 511  (Abstand 511): cos = -0,1427

cos misst, wie ähnlich zwei Vektoren sind: +1 heißt fast gleich, 0 heißt unabhängig, negativ heißt gegenläufig. Position 10 und 11 liegen im Text direkt nebeneinander, Position 10 und 500 weit auseinander.

Das Muster ist klar: Je näher zwei Positionen beieinander liegen, desto ähnlicher sind ihre gelernten Vektoren — und je weiter sie auseinander liegen, desto mehr sinkt die Ähnlichkeit, bis hinein in den negativen Bereich. Niemand hat nlm eine Formel für "Nähe" vorgegeben. Das Modell hat diese Eigenschaft rein daraus abgeleitet, dass benachbarte Positionen sich beim Next-Token-Prediction-Training ähnlich nützlich verhalten.

Eine Überraschung dabei: Position 0 verhält sich anders als der Rest. Ihre Norm liegt bei 2,46 — mehr als fünfmal so hoch wie der Durchschnitt aller 512 Positionen (0,43) und auch deutlich über Position 511 (0,63) oder Position 256 (0,34). Die Norm ist die Länge des Vektors, und beim Addieren mit dem Token-Vektor zählt ein längerer Vektor stärker: Der Textanfang ist am Eingang des Modells damit deutlich kräftiger markiert als jede andere Stelle. Die Cosine-Tabelle oben zeigt das nicht, weil sie nur die Richtung vergleicht und nicht die Länge. Der Satzanfang scheint für nlm eine strukturell besondere Rolle zu spielen, die sich von "einfach irgendeine Position" unterscheidet — eine mögliche Erklärung ist, dass Position 0 immer exakt den Beginn eines Trainingsfensters markiert, während alle anderen Positionen je nach Fenster unterschiedlichste Rollen einnehmen können.

Entscheidung: Die Hypothese ist bestätigt: nlm's gelernte Positions-Embeddings kodieren tatsächlich eine Form von Nähe, rein aus den Trainingsdaten heraus — ohne dass diese Eigenschaft im Code jemals vorgeschrieben wurde. Position 0 ist dabei ein auffälliger Sonderfall, der hier nur beobachtet, aber nicht abschließend erklärt wird.


Der Preis dieser Entscheidung

Gelernte, absolute Positions-Embeddings haben einen konkreten Nachteil, der direkt aus ihrer Bauweise folgt: Für Position 600 existiert in einer (512, 256)-Tabelle schlicht keine Zeile. nlm kann deshalb keine Sequenz verarbeiten, die länger als context_length = 512 Tokens ist — eine feste, strukturelle Grenze, keine Performance-Frage. Der Vorteil im Gegenzug: Niemand hat nlm vorgeschrieben, wie Positionen zusammenhängen. Es hat selbst gelernt, was für TinyStories-artige Texte nützlich ist. Die Messung oben zeigt das: Nahe Stellen sind sich ähnlich, ohne dass dafür eine Formel nötig war.


Nächster Beitrag

Jetzt weiß jedes Token, welches Wort es ist, und an welcher Stelle es steht. Was aber noch fehlt: ein Mechanismus, der diese beiden Informationen tatsächlich zwischen Tokens austauscht. Als Nächstes zeige ich, wie Query, Key und Value an einer echten, kleinen Beispielrechnung berechnen, warum "hungry" für das Wort "food" relevant ist und wie aus dieser Relevanz ein neuer, kontextabhängiger Vektor entsteht.

Dies ist mein fünfter Beitrag aus der Serie über den Aufbau von nlm: einem kleinen Sprachmodell, das von Grund auf gebaut, trainiert und kritisch bewertet wird.