Training eines kleinen GPTs anhand indischer Volksmärchen: Tokenisierer, Maske und Verlustfunktion
Ein GPT mit 9M Parametern von Grund auf: Warum der Tokenisierer von GPT-2 bei Worten aus Volksmärchen versagt, wie BPE und die kausale Maske funktionieren sowie warum eine Batch-Verarbeitung zu 4.096 Verlusten pro nächsten Token führt.
Quellen: Building LLM, Part 1 — Training: Predicting the Next Token
Ein kleines GPT, das auf indischen Volksmärchen trainiert wurde
Jeder, der bereits einen Vision Transformer entwickelt hat, kennt im Grunde schon 70 Prozent eines Sprachmodells. Embeddings, Attention-Mechanismen, der Residual-Stack sowie der Trainingszyklus sind in beiden Fällen dieselben Komponenten. Was übrig bleibt, ist das Besondere – und dieser Leitfaden zeigt dies, indem er ein kleines GPT von Grund auf erstellt und es mit einer einzigen Laptop-GPU von vorne trainiert. Das Korpus besteht aus indischen Volksmärchen: Jataka-Geschichten, Fabeln im Stil des Panchatantra sowie Erzählungen aus Punjab und Bengalen.
Bereits zu Beginn taucht ein echtes Problem auf. GPT-2s Standard-Tokenisierer zerlegt Wörter wie Brahman und Rajah in Fragmente, weil er sie noch nie gesehen hat; daher wird stattdessen eine Tokenisierer-Struktur aus dem Korpus trainiert. Am Ende steht ein Modell mit 9 Millionen Parametern, das von zufälligem Rauschen zu trainierten Gewichten übergegangen ist, und jeder kann jede dieser Werte erzeugen, indem er den etwa zehn Minuten dauernden Trainingsvorgang ausführt.
Warum die Bildverarbeitung zuerst kam
Das Betrachten von Bildern vor Text mag wie die falsche Reihenfolge erscheinen, da Chatbots mit Wörtern arbeiten. Doch ein Transformer ist unbeeindruckt davon, ob seine Eingabe Bildausschnitte oder Satzteile sind. Die Attention-Heads, LayerNorm, Skip-Connections, MLP sowie der Optimierungszyklus bleiben unverändert. Die bessere Frage ist, wo dieser gemeinsame Mechanismus am einfachsten zu erlernen ist – und Bilder gewinnen aus vier Gründen.
- Ein Token ist im Grunde ein Bildausschnitt, ein festes Raster, das aus dem Bild ausgeschnitten wird – es gibt daher weder ein Vokabular noch einen Tokenisierer und auch kein Problem mit unbekannten Wörtern, das zuerst gelöst werden müsste.
- Es gibt keine kausale Maske. Ein ViT betrachtet das gesamte Bild auf einmal, wodurch reine Aufmerksamkeitsmechanismen Vorrang vor den zusätzlichen sprachlichen Regeln haben, die ein Schauen in die Zukunft verbieten.
- Das Verhalten ist sichtbar. Die Ausschnitte, Aufmerksamkeitskarten sowie die Bereiche, auf die sich das Modell konzentriert, können alle dargestellt werden, und im Vergleich zu Text sind Pixel greifbar.
- Es wird in wenigen Minuten auf einer einzigen GPU von Grund auf trainiert (bei CIFAR-10), sodass jede Zahl eine von Ihnen selbst erzeugte Zahl ist und nicht blind geglaubt werden muss.
Fazit ist, dass der vollständige Transformer bereits vorhanden ist, sobald ein ViT erstellt wurde – er ist lediglich für die Bildverarbeitung konzipiert. Ein Sprachmodell fügt lediglich hinzu, wie Text in Token umgewandelt wird (durch ein Abfragen und nicht durch Berechnungen), die kausale Maske sowie die Vorhersage des nächsten Tokens. Der schwierige, abstrakte Teil – die Sprache selbst – lässt sich viel leichter verstehen, sobald die Maschine damit vertraut ist.
Das Training besteht darin, das nächste Token zu erraten
Zur Trainingszeit reduziert sich alles, was ein Sprachmodell tut, auf das Lesen der bisherigen Tokens und die Vorhersage des folgenden Tokens.
Die Abbildung zeigt die Vorhersage eines nächsten Tokens anhand der vorhergehenden Tokens. Wenn man dem Modell den Satzanfang „In einem bestimmten Dorf lebte ein armer Brahmane“ gibt, erhält man eine Wahrscheinlichkeit für jedes mögliche Folgetoken. Ein Punkt, ein Komma oder Wörter wie wer oder genannt wären alle grammatikalisch möglich. Niemand muss etwas annotieren: Die richtige Antwort ist einfach das Token, das im Text als Nächstes vorkam. Deshalb kann bereits reiner Text ein Sprachmodell trainieren – er bildet das gesamte Lernsignal. Alle anderen Komponenten dienen dazu, diese einzige Vorhersage zu unterstützen, und das ViT liefert bereits die meisten davon.
In Code braucht man dafür nur zwei Zeilen: Zuerst wird das Modell ausgeführt, anschließend werden seine Ergebnisse in eine Verteilung über das nächste Token normalisiert.
logits, _ = model(ids) # ids = the tokens so far, shape (1, T)
probs = F.softmax(logits[0, -1], dim=-1) # one probability per vocabulary token
Der Rest erläutert, wie model erstellt und trainiert wird, damit diese Wahrscheinlichkeiten gut ausfallen.
Warum der Tokenisator wichtig ist
Ein ViT teilt ein Bild in Patches auf, während ein Sprachmodell Text in Tokens aufteilt. Ein Patch wird aus Pixelwerten berechnet, doch ein Texttoken stammt aus einem festen Wortschatz. Jemand muss diesen Wortschatz im Voraus erstellen, und der Ersteller bestimmt, welche Dinge das Modell als einfach empfindet.
Der erste Versuch nutzte r50k_base, den fertiggestellten Tokenisator von GPT-2, dessen Wortschatz seit etwa 2019 aus westlichem Webtext stammt. Die nächste Abbildung zeigt, wie er mit Wörtern aus den Erzählungen umgeht.
Brahman teilt sich in Brah und man, Rajah in Raj und ah auf, und Panchatantra zerfällt in fünf Teile. Keines davon ist unklar. Rajah kommt im Korpus 709 Mal vor, doch der Tokenisierer sah solche Wörter in seinen eigenen Trainingsdaten nie häufig genug, um jedem von ihnen einen eigenen Token zuzuweisen. Die Fragmentierung verlängert die Sequenzen und zwingt das Modell, Brahman aus Brah plus man wieder zusammenzusetzen, wobei man auch mit woman und human gemeinsam verwendet wird. Mit nur etwa einer Million Token an Trainingsdaten ist das ein echter Nachteil.
Die Lösung ist ein Tokenisierer, der mit Hilfe der Byte-Pair-Encodierung (BPE) anhand der Erzählungen trainiert wurde. GPT verwendet denselben Algorithmus, nur angepasst an ein besser passendes Korpus. Das Ausgangsmaterial sind zwölf in der öffentlichen Domain befindliche Sammlungen indischer Volksmärchen aus dem Project Gutenberg, darunter zwei Jataka-Bücher, zwei Bände mit indischen Märchen, „Old Deccan Days“, „Folk-Tales of Bengal“, „The Talking Thrush“, „Tales of the Punjab“, „Twenty-Two Goblins“, zwei Santal-Sammlungen sowie „Tales of the Sun from Southern India“ – insgesamt etwa 809.000 Wörter.
Die Abbildung zeigt die zur Trainierung des Tokenisierers verwendeten Byte-Pair-Fusionen. Bei BPE beginnt man mit den 256 rohen Byte-Werten, sodass nichts unbekannt sein kann; anschließend werden immer wieder die häufigsten benachbarten Paare zu einem neuen Token zusammengeführt. Nach 7.935 Fusionen anhand der Erzählungen umfasst das Vokabular 8.192 Einträge, darunter ganze Begriffe wie Rajah, Brahman und jackal.
Eine Gestaltungsoption verdient eine klare Aussage: Der Tokenisator von GPT-2 wird vollständig ersetzt, nicht erweitert.
Die Abbildung zeigt das benutzerdefinierte Vokabular mit 8.192 Tokenen. Das Hinzufügen weiterer Token zu den bereits vorhandenen 50.257 bei r50k ist ein sinnvoller Ansatz, wenn die Englischfähigkeiten eines großen vorgetrainierten Modells beibehalten werden sollen, wie es Projekte wie OpenHathi tun. Das Training von Grund auf anhand eines kleinen, spezialisierten Korpus ist eine andere Situation – dabei nimmt das Vokabular von r50k aus dem westlichen Internet nur Platz in Anspruch, wobei die behandelten Geschichten lediglich 37 Prozent davon abdecken. Das neue Vokabular mit 8.000 Tokenen ist kleiner, wird zu 96,6 Prozent genutzt und auf die Geschichten abgestimmt, weshalb ein Austausch die beste Lösung darstellt.
Das Training des Tokenisators erfordert nur wenige Zeilen mit der HuggingFace-Bibliothek tokenizers. Das Skript liest jedes Rohtextbuch, entfernt die Header- und Footer-Einträge von Project Gutenberg und trainiert anschließend ein BPE-Modell.
import glob
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
from llm_transformer.prep_tales import strip_gutenberg, RAW_DIR
# read every raw .txt book and strip its Project Gutenberg header/footer,
# leaving just the story text - one big string per book
tales_texts = [strip_gutenberg(open(p, encoding="utf-8", errors="ignore").read())
for p in sorted(glob.glob(f"{RAW_DIR}/*.txt"))] # 12 books, ~809k words
tok = Tokenizer(models.BPE())
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) # start from bytes
trainer = trainers.BpeTrainer(vocab_size=8192, special_tokens=["<|endoftext|>"],
initial_alphabet=pre_tokenizers.ByteLevel.alphabet())
tok.train_from_iterator(tales_texts, trainer) # learn the 7,935 merges
tok.save("tales_bpe.json")
Die ausgegebene .json-Datei ist der fertige Tokenisator, eine feste Liste von Fusionen. Er wird nicht erneut trainiert. Stattdessen wird er geladen und zur Umwandlung von Text in IDs sowie von IDs zurück in Text verwendet. Ein einziger Aufruf von .encode() verknüpft den Rohtext mit dem neuronalen Netzwerk.
tok = Tokenizer.from_file("tales_bpe.json") # reload the trained tokenizer
ids = tok.encode("Once upon a time").ids # -> [412, 987, 15, 733] (integers)
idx = torch.tensor(ids).unsqueeze(0) # (1, 4) — a batch of one sequence
vocab_size = tok.get_vocab_size() # 8192 - sets the NUMBER OF ROWS below
Nur diese Ganzzahlen erreichen das Netzwerk, und die ursprünglichen Wörter existieren für es nicht mehr. Jeder ID ist lediglich eine Position in einer Tabelle, und vocab_size legt fest, wie viele Einträge die Embedding-Schicht reserviert.
Von IDs zu Vektoren
Sobald der Text eine Liste von IDs ist, wird jeder ID ein Vektor zugeordnet. An dieser Stelle ist ein LLM einfacher als ein ViT.
Die Abbildung verbindet einen Tokenisierungsindex mit einem Embedding-Vektor. Bei einem ViT wird durch eine einfache Matrixmultiplikation der Token jedes Patches aus den Pixeln berechnet. Bei einem LLM ist der Token eine diskrete ID, wodurch der Vektor lediglich die Zeilennummer einer gelernten Tabelle darstellt – es handelt sich dabei um eine einfache Abfrage. Der Token 789 ruft Zeile 789 ab, die ein 256-dimensionaler Vektor ist; durch Hinzufügen eines Positional Embeddings, wie es beim ViT der Fall ist, wird die Eingabestufe abgeschlossen.
In Code ist eine nn.Embedding-Schicht die Tabelle, und beim Aufruf erfolgt die Abfrage durch Indexieren statt durch Multiplikation. Ihr .weight ist ein lernbarer Tensor mit den Abmessungen 8192 mal 256, der 2,1 Millionen Gewichte enthält, die mittels Backpropagation trainiert werden; eine bestimmte Zeile ändert sich nur in den Schritten, in denen ihr Token vorkommt.
self.token_embed = nn.Embedding(vocab_size, n_embed) # the 8,192 x 256 table
# .weight IS a learnable (8192, 256) parameter tensor — 2.1M weights, trained by backprop;
# each id's row is nudged only on steps where that token appears in the batch
...
tok_emb = self.token_embed(idx) # idx (B, T) -> vectors (B, T, 256), one row per id
Alles, was danach kommt – also die Aufmerksamkeit, der Reststrom sowie die MLP-Blöcke – ist identisch mit dem Vision Transformer, mit einer Ausnahme.
Die kausale Maske
Ein ViT ermöglicht es jedem Patch, sich auf alle anderen zu konzentrieren. Ein Sprachmodell kann das nicht, denn seine Aufgabe besteht darin, vorherzusagen, was als Nächstes kommt. Wenn die Position t die Position t+1 sehen könnte, würde sie einfach die Antwort kopieren. Die Lösung besteht darin, die Aufmerksamkeit zu maskieren, sodass jeder Token nur sich selbst sowie frühere Tokens wahrnimmt.
Die Maske in der Abbildung ist ein dreieck mit abgeschrägter Basis. Vor dem Softmax erhalten alle zukünftigen Positionen den negativen Unendlichkeitswert, der vom Softmax in ein Gewicht von genau null umgewandelt wird; dadurch tragen die blockierten Token nichts bei. Dieses Dreieck stellt den einzigen architektonischen Unterschied zwischen der Aufmerksamkeit im ViT und der Aufmerksamkeit in LLMs dar. Skalarprodukte, Skalierung durch die Quadratwurzel der Größe des Heads, Softmax sowie die gewichtete Summe der Werte bleiben alle unverändert.
In Code handelt es sich dabei um eine zusätzliche Zeile oberhalb der normalen Aufmerksamkeit. Zunächst wird eine untere dreieckige Matrix aus Einsen erstellt, die Scores werden wie üblich berechnet, und die auf Null gesetzten Positionen werden vor der Softmax-Funktion mit negativer Unendlichkeit gefüllt.
# tril = torch.tril(torch.ones(T, T)) — a lower-triangular matrix of 1s, made once
attn = q @ k.transpose(-2, -1) / math.sqrt(head_size) # scores, (B, T, T)
attn = attn.masked_fill(self.tril[:T, :T] == 0, float('-inf')) # blank out the future
attn = F.softmax(attn, dim=-1) # -inf -> weight 0
out = attn @ v
Ein Durchlauf trainiert jede Kontextlänge
Die Einfachzeilmaske verbirgt etwas, das genauer betrachtet werden sollte. Die Leute neigen dazu, sich ein Sprachmodell so vorzustellen, als würde es Text erzeugen: indem es einen Token zu einem wachsenden Kontext hinzufügt und bei jedem Schritt die Aufmerksamkeit neu berechnet. Das gilt zwar bei der Inferenz, aber nicht während des Trainings. Beim Training ist der Satz vollständig bekannt, sodass Q, K und V für alle T-Token gemeinsam berechnet werden, wodurch in einem einzigen Schritt die gesamte T×T-Aufmerksamkeitsmatrix entsteht.
Der eine Durchlauf ist nur aufgrund der kausalen Maske gültig. Betrachten wir Zeile t der maskierten Matrix: Alle Einträge rechts der Diagonale sind negative Unendlichkeit, wodurch sie nach der softmax-Funktion null werden. Die Berechnung von out = attn @ V liefert somit für Zeile t eine gewichtete Summe nur über die ersten t Wertvektoren. Jede Kontextlänge wird daher gleichzeitig verarbeitet.
Die Abbildung zeigt verschiedene Ausführungen sowie die Form der Maske während des Trainings. Lesen Sie sie Zeile für Zeile:
- Zeile 1 berücksichtigt nur Token 1, wodurch ihre Ausgabe Token 2 aus einem einzelnen Token-Kontext vorhersagt.
- Zeile 3 berücksichtigt die Tokens 1 bis 3 und prognostiziert Token 4 anhand von drei Token des Kontexts.
- Zeile 6 berücksichtigt alle sechs Tokens und berechnet Token 7 aus dem vollständigen Kontext.
Hier ist eine weitere Ansicht. Der sichtbare Kontext jeder Zeile bildet eine dreieckige Matrix, die bei jedem Mal um ein Element erweitert wird – zunächst 1 mal 1 für das erste Token, dann 2 mal 2 und schließlich 3 mal 3. Dies sind die in Orange dargestellten, ineinander verschachtelten Blöcke im oberen linken Bereich der großen Matrix; somit umfasst eine einzige 6 mal 6 Schleife alle kleineren Sequenzen, wobei die untere Leiste diese sequenziell entfaltet. Sie werden alle innerhalb eines attn @ V erzeugt, und in Python wird keine Schleife über die Positionen verwendet. Das erklärt die Trainingseffizienz von Transformern: Eine Sequenz mit T Token entspricht T Trainingsbeispielen, wobei die Kontextlängen von 1 bis T reichen und parallel bewertet werden. Dasselbe dreieckige Maskierungsprinzip, das diese Beispiele liefert, verhindert außerdem, dass eine Position während der Generierung in die Zukunft blickt; die Generierung erfolgt Zeile für Zeile, und jeder Schritt bleibt kostengünstig durch das Caching früherer Schlüssel- und Wertpaare (der KV-Cache).
Reelle Zahlen durch Attention
Bislang bedeutete jede Zelle nur „teilnehmen“ oder „nicht teilnehmen“. Durch die Darstellung in Matrixform werden die tatsächlichen Zahlen für dieselben drei Durchläufe angezeigt, ausgehend vom Block 0, Head 0 des trainierten Modells für den Satz „Once there lived a clever jackal“.
Die Abbildung zeigt die Berechnungen von Q, K und V über die verschiedenen Durchläufe. Innerhalb einer Zeile sind die Aufmerksamkeitsgewichte positiv und summieren sich auf 1, sodass das Ergebnis eine konvexe Mischung der sichtbaren Wertvektoren ist, also ein gewichtetes Mittelwert.
- Im ersten Durchlauf steht ein Token mit dem Gewicht 1,00 zur Verfügung, daher gilt
out1 = V1. Die erste Position kopiert ihren eigenen Wert, da noch nichts gemischt werden muss. - Im dritten Durchlauf werden drei Werte miteinander vermischt, wobei das erste Token ein Gewicht von etwa 0,20, das zweite eines von 0,06 und das dritte eines von 0,74 hat. Der Durchlauf stützt sich hauptsächlich auf sein eigenes Token lived, nimmt aber dennoch ein Fünftel von Once auf – es handelt sich somit um eine echte Mischung.
Das umfasst alles, was Attention leistet: Die Scores aus Abfragen und Schlüsseln legen die Gewichte fest, und die Gewichte mischen die Werte. Das Maskieren stellt sicher, dass die Mischung niemals über den aktuellen Token hinausgeht, sodass jede Zeile eine faire Zusammenfassung dessen ist, was bisher gelesen wurde. Reale Kopfe haben 32 Wertdimensionen, und das Modell verfügt in jeder der sechs Schichten über acht Kopfe. Die Anzeigen zeigen die ersten 8 Dimensionen eines einzelnen Kopfes, obwohl derselbe Mechanismus überall angewandt wird.
T-Vorhersagen, T-Verluste
Die Vorhersage des nächsten Tokens bringt einen Vorteil, den ViT nicht hatte. ViT führte pro Bild nur eine einzige Vorhersage durch – einen Klassenzusatz. Die kausale Maske ermöglicht es einem Sprachmodell, den nächsten Token an allen Positionen gleichzeitig zu erraten, sodass eine Sequenz so viele Lernsignale liefert wie sie Token enthält.
In der Abbildung werden die T Vorhersagen sowie die Verlustwerte für eine Eingabe mit T Token pro Durchlauf durchschnittlich berechnet. Jede Position erhält einen Cross-Entropy-Verlust, der misst, wie überrascht das Modell vom tatsächlichen nächsten Token war. Einige Werte sind gering, wie zum Beispiel a nach lived mit nur 0,58. Andere sind hoch, wie das Adjektiv clever mit 7,17, da viele Wörter hätten passen können. Der Durchschnitt der sechs Werte beträgt 3,59, und die Rückpropagierung senkt diese Zahl mithilfe derselben Kettenregel-Methodik wie ViT.
In der Code-Struktur stammen alle T-Verluste aus einem einzigen Aufruf von cross_entropy. Das (B, T)-Gitter der Positionen wird in eine einzige lange Liste zusammengeflacht, und jeder Eintrag wird mit seinem tatsächlichen nächsten Token verglichen.
logits = self.lm_head(x) # (B, T, vocab)
B, T, C = logits.shape
loss = F.cross_entropy(logits.reshape(B*T, C), # every position ...
targets.reshape(B*T)) # ... vs the token that actually followed
Jeder Verlust ist eine Wortschatzklassifizierung
Es ist wichtig, genau zu wissen, was cross_entropy vergleicht – schließlich sind sowohl die Vorhersage als auch das Zielvektor, nicht einzelne Wörter. An jeder Position erzeugt lm_head einen Logit-Vector mit 8.192 Einträgen, wobei jeder Eintrag einen Wert für ein Wortschatztoken darstellt, und softmax wandelt diesen in eine Wahrscheinlichkeitsverteilung über alle Tokens um. Das Zielvektor ist ein gleich großen One-Hot-Vector, der bei dem tatsächlich folgenden Wort den Wert 1 und an allen anderen Stellen 0 enthält. Die Vorhersage des nächsten Tokens ist somit ein Klassifizierungsproblem, bei dem die Wörter des Wortschatzes als Klassen dienen.
Die Abbildung vergleicht Logits in einem LLM und in einem ViT. Sie macht einen subtilen Punkt deutlich: Die Balken sind aus Lesbarkeitsgründen nach Wahrscheinlichkeit geordnet, während die tatsächliche Ausgabe ein Vektor mit fester Struktur ist, bei dem das Feld i stets dem Token-Id i zugeordnet ist. Das Wort „son“ bleibt bei jeder Vorwärtsberechnung, egal ob beim Training oder bei der Inferenz, am Index 498, und Softmax ändert nur die Höhen der Felder, niemals ihre Reihenfolge. Das erklärt, warum die einzige 1 im one-hot-Zielwert am Index 498 und nicht an einer ersten Position steht: Der Tokenisierer hat jedem Token einmal seine Identität als sein zugehöriges Feld zugewiesen.
Das ist genau der Klassifikatorkopf von ViT, der Softmax auf die Klassenwerte anwendet und diese gegenüber einem one-hot-Label mit Kreuzentropie bewertet. Zwei Dinge unterscheiden sich: die Anzahl der Klassen (8.192 Wortschatz-Token gegenüber etwa 10 Bildlabels) sowie die Häufigkeit (an allen T-Positionen gegenüber einmal pro Bild). Gegenüber einem one-hot-Ziel reduziert sich die Kreuzentropie außerdem auf minus das Logarithm der dem richtigen Wort zugeordneten Wahrscheinlichkeit, weshalb der Verlust pro Position wie ein Überraschungswert aussieht. „son“ mit 9,8 Prozent kostet 2,32 – ein Wort, von dem das Modell stark erwartete, dass es weitaus weniger kosten würde, während ein echter Schock deutlich mehr kosten würde.
Wie eine Batch-Gruppe erstellt wird
reshape(B*T) besagt im Grunde, dass eine Batch-Gruppe aus B Sequenzen besteht, von denen jede T Positionenverluste beiträgt. Woher stammen also diese B Sequenzen, und sind sie alle gleich lang?
Menschen stolpern oft über einen Punkt: Die Vortrainingssequenzen sind nicht gepuffert und haben alle genau die gleiche Kontextlänge. Das Korpus wird zu einem einzigen langen Tokenstrom zusammengefügt, wobei zwischen den Dokumenten ein <|endoftext|>-Token steht, und Fenster fester Länge werden an zufälligen Positionen abgeschnitten. Da alle Fenster aus diesem kontinuierlichen Strom stammen, können sie nicht kurz sein, und es ist kein Puffertoken erforderlich.
Die Abbildung verfolgt den Weg vom Trainingskorpus über die Batchs bis hin zum Verlustwert. Das einzige spezielle Token im Datenstrom ist <|endoftext|> (EOT), das als Trennzeichen statt als Puffer dient und anzeigt, wo eine Volksmärchengeschichte endet und die nächste beginnt. Ein Fenster kann eine solche Grenze überschreiten und mitten darin ein EOT enthalten – so lernt das Modell, dass eine Geschichte zu Ende ist und eine neue beginnt. Puffer werden später beim Feintuning verwendet, wenn die Beispiele in Länge variieren und die gepufferten Positionen aus dem Verlustwert ausgeblendet werden; das ist eine andere Einstellung als beim gepackten Vortrainieren.
Der Verlust ist der zweite Bereich, in dem sich das LLM vom ViT unterscheidet – und dieser Unterschied ist vorteilhaft. Das ViT erzeugte pro Bild eine Vorhersage und einen Verlust. Eine 128-Token-Sequenz erzeugt hier 128 Verluste, die zu dem Verlust dieses Beispiels addiert werden (3,78 für Beispiel 1 in der Abbildung). Die 32 Verluste einer Batch werden wiederum zu einem Batch-Verlust von 3,92 addiert, den backpropagation minimieren soll. Durch diese beiden Schritte der Mittelung sinkt der Durchschnitt der 32 mal 128, also insgesamt 4.096 Verluste pro Schritt erheblich – das bedeutet mehr Informationen pro Schritt als ein einziger Verlust pro Bild.
Beobachtung des sinkenden Verlusts
Verschließen Sie diesen Verlust mit einem Optimierer und wiederholen Sie den Vorgang. Das ist der eigentliche Trainingsprozess am Tales-Korpus.
Der Graph zeigt den Trainingsverlust sowie den für die Validierung zurückgehaltenen Verlust über die Epochen. Der Trainingsverlust beginnt bei 9,2, was ln(8192) entspricht – dem Verlust durch zufälliges Raten in einem Wortschatz mit 8.192 Token – und der beste Verlust für die Validierung erreicht 4,73. Die Kurven bleiben eng beieinander, etwa 1,0 voneinander entfernt, was bedeutet, dass das Modell bei einem eng begrenzten Bereich generalisiert anstatt nur auswendig zu lernen. Das Modell selbst ist relativ klein.
Die Abbildung verknüpft den Trainingsverlauf mit der Anzahl der lernbaren Gewichte. Mit 9,0 Millionen Parametern trainiert das Modell in etwa zehn Minuten auf einer Laptop-GPU mit 6 GB Speicher. Etwas mehr als die Hälfte davon befindet sich in den sechs Transformer-Blöcken, der Rest gehört zur Embedding-Tabelle und zum Ausgabeteil. Das Training besteht ausschließlich darin, diese 9 Millionen Zahlen wiederholt anzupassen, sodass die Wahrscheinlichkeiten für den nächsten Token dem tatsächlich folgenden Token mehr Gewicht verleihen.
In der Code-Struktur ist die Schleife genau so, wie man es erwarten würde, und hat dieselbe Struktur wie bei ViT: Es werden zufällige Kontexte sowie Fenster für die nächsten Token ausgewählt, der Forward-Pass wird ausgeführt, um einen Mittelwert für den Verlust zu ermitteln, die Gradienten werden auf Null gesetzt, es erfolgt die Backpropagation und anschließend ein Schritt im Lernprozess.
for step in range(train_steps):
xb, yb = get_batch(train_data) # random (context, next-token) windows
_, loss = model(xb, yb) # forward: T predictions -> one mean loss
opt.zero_grad(); loss.backward() # backprop the loss into all 9M parameters
opt.step() # nudge them downhill
Der vollständige Code enthält einen Zeitplan für die Anpassung der Lernrate mittels Kosinusfunktion sowie eine Bewertungsmethode.
Was neu ist und was unverändert bleibt
Verglichen mit dem Vision Transformer gibt es beim LLM nur wenige Neuerungen: eine Token-Embedding-Struktur, die als Abfrageschema fungiert, ein kausales Masking sowie einen Verlustfaktor für das nächste Token an jeder Position, der über das gesamte Vokabular klassifiziert. Eine praktische Erkenntnis ist außerdem, dass der Tokenisierer zum Modell gehört – daher lohnt sich die Verwendung eines für das jeweilige Gebiet spezifischen Tokenisierers. Attention-Mechanismen, positionale Embeddings, der Residual-Stream, LayerNorm, die MLP-Blöcke sowie der gesamte Backpropagation- und Optimierer-Zyklus bleiben unverändert.
Reelle Zahlen haben die inneren Abläufe aufgezeigt: wie die kausale Maske es ermöglicht, dass ein einziger Vorwärtslauf alle Kontextlängen zusammen bewertet, was attn @ V erzeugt (jede Ausgabezelle mischt die Wertvektoren, die für diesen Token sichtbar sind), wie Trainingsbatchs durch Komprimieren statt Durchfüllen entstehen und wie der Verlust einer Position, eine Klassifizierung innerhalb des Wortschatzes, mit den anderen zu einem einzigen Durchschnitt zusammengeführt wird.
Die Gewichte sind berechnet, doch ein trainiertes Modell tut nichts, bis es ausgeführt wird. Danach folgt die Inferenz: der Generierungszyklus, der KV-Cache, der dies beschleunigt, und was das kleine Modell schreibt, wenn ihm der Anfang einer Geschichte gegeben wird.