Tokenwerk · Das LLM-Lehrbuch

Kapitel 12 · II · Von Zahlen zu Sprache · 5 Minuten

Vom Text zu Token-IDs

Zeichen, Bytes und Subwords: Wie du Text verlustfrei in Indizes verwandelst und deinen eigenen BPE-Tokenizer trainierst.

IDs sind Adressen, keine Größen

Ein Sprachmodell verarbeitet Zahlen. Die ID 42 bedeutet nicht „mehr“ als die ID 17. Sie ist eine Adresse in einer Tabelle. Du könntest alle IDs mit einer Permutation umbenennen und die Tabellen entsprechend umordnen; das Modell würde dasselbe berechnen. Deshalb darfst du die IDs nicht einfach als skalare Zahlen in eine lineare Schicht geben.

Ein Token kann ein Zeichen, ein Byte, ein Wortstück, ein ganzes Wort oder ein Steuerzeichen sein. Tokens sind nicht automatisch Wörter. Die Tokenisierung legt fest, wie der Text in die Positionen aufgeteilt wird, auf denen das Modell später seine Vorhersagen macht.

Zeichen-Tokenizer: schnell verstanden

python
text = 'Hallo Welt!'
vocab = sorted(set(text))
stoi = {c: i for i, c in enumerate(vocab)}
itos = {i: c for c, i in stoi.items()}
ids = [stoi[c] for c in text]
assert ''.join(itos[i] for i in ids) == text

Dieser Tokenizer ist für einen bekannten Korpus einfach. Bei einem neuen Zeichen schlägt er jedoch fehl. Du könntest ein Unknown-Token ergänzen, verlierst damit aber den exakten ursprünglichen Text. Auch Unicode ist wichtig: Ein sichtbarer Buchstabe kann aus mehreren Codepoints bestehen. Verschiedene Normalisierungen müssen bewusst entschieden werden, nicht zufällig entstehen.

Bytes als robustes Fundament

UTF-8 verwandelt jeden gültigen Unicode-Text in Bytes zwischen 0 und 255. Ein Byte-Tokenizer braucht nur 256 normale Tokens und einige Sondertokens. Er kennt keine unbekannten normalen Bytes.

python
raw = 'Grüße 🐈'.encode('utf-8')
ids = list(raw)
text = bytes(ids).decode('utf-8')
assert text == 'Grüße 🐈'

Ein Umlaut kann mehrere Bytes benötigen, ein Emoji ebenfalls. Einzelne generierte Bytes sind nicht zwingend für sich gültiger UTF-8-Text. Beim Dekodieren einer unvollständigen oder beschädigten generierten Folge muss deine Ausgabe das handhaben. Unser Tokenizer rekonstruiert gültige Eingabetexte exakt; bei beliebigen Modelloutputs verwendet er Ersatzzeichen für ungültige UTF-8-Folgen.

Byte Pair Encoding schrittweise

BPE startet in unserem Projekt mit Bytes. Es zählt benachbarte Tokenpaare im Trainingskorpus, wählt das häufigste Paar (bei Gleichstand deterministisch das mit den kleinsten IDs), gibt seiner zusammengefügten Bytefolge eine neue ID und ersetzt Vorkommen dieses Paares. Danach wird neu gezählt. Das Verfahren endet nach einer gewünschten Zahl von Merges oder wenn kein ausreichend häufiges Paar mehr existiert.

Das Wort „hallo“ könnte zuerst l,l zu ll und später h,a zu ha zusammenführen. Die genauen Merges entstehen aus dem Korpus; es gibt keine vorgeschriebene sprachliche Zerlegung. Im Browserexperiment benutzen wir aus Lesbarkeitsgründen Zeichen statt Bytes, aber dieselbe Paarzähl- und Ersetzungslogik. Das ist ausdrücklich ein didaktischer Character-BPE-Durchlauf.

Ein Merge ist nicht das Ersetzen aller Teilstrings im Text. Er wirkt auf die aktuelle Tokenfolge. Für [a,a,a] und den Merge (a,a) ersetzen wir von links nichtüberlappend zu [aa,a]. Würdest du überlappende Paare gleichzeitig ersetzen, wäre das Ergebnis nicht wohldefiniert.

Vokabulargröße ist ein Zielkonflikt

Ein größeres Vokabular verkürzt häufig Sequenzen. Es vergrößert aber Embedding- und Ausgabematrix und kann seltene Tokens erzeugen, für die es wenig Trainingsbeispiele gibt. Ein kleineres Vokabular braucht weniger Tabellenparameter, aber mehr Positionen für denselben Text. Für unser Micro-Modell sind wenige hundert Tokens praktisch, für ein ernsthafteres kleines Modell ist ein größerer Tokenizer oft sinnvoll.

Vergleiche nicht einfach den Token-Loss zweier verschiedener Tokenizer. Ein Token kann beim einen Modell ein Byte und beim anderen ein halbes Wort sein. Für einen solchen Vergleich brauchst du eine gemeinsame Einheit, etwa Bits pro Byte auf identischen Texten, plus einen sauberen Messweg.

Sondertokens sind eigene IDs

Unser Projekt reserviert PAD, BOS, EOS, USER, ASSISTANT und SYSTEM. EOS markiert das Dokument- oder Antwortende. Die Rollenmarker gehören zur späteren Chatformatierung. Normaler Text wird als Bytes verarbeitet und kann keine reservierte ID durch ein zufälliges Stringfragment erzeugen. Einen sichtbaren String wie <assistant> im normalen Text dürfen wir nicht automatisch als Rolle interpretieren.

Beim Vorbereiten der Trainingsdaten fügst du reservierte IDs explizit ein. Bei der Ausgabe kannst du sie überspringen oder als lesbare Marker anzeigen. „EOS“ ist kein automatisch vorkommendes Wort im Dokument, sondern eine strukturelle Entscheidung.

Trainiere den Tokenizer nur auf Trainingsdaten

Teile zuerst Dokumente in Training und Validierung. Lerne dann die Merges nur auf den Trainingsdokumenten. Andernfalls erhält dein Tokenizer Informationen über zurückgehaltene Texte. Das ist eine mildere Form von Datenleckage als direktes Modelltraining auf der Validierung, aber dennoch eine veränderte Versuchsbedingung.

Speichere Bytes und Merges. Die Reihenfolge der Merges ist Teil des Encoders. Beim neuen Text wendet unser Referenzencoder die gelernten Regeln in dieser Reihenfolge an. Dieses naive Verfahren ist langsam, aber leicht zu prüfen. Es ist ein Lehr-Tokenizer, kein Ersatz für hochoptimierte Bibliotheken in einer Produktionspipeline.

Prüfe zuerst den Roundtrip

Teste ASCII, Umlaute, Zeilenumbrüche, Emojis, leere Strings und Texte mit Sondertoken-ähnlichen Strings. decode(encode(text)) == text muss für gültige Eingabetexte gelten, wenn du keine Normalisierung beabsichtigst. Prüfe außerdem, dass Speichern und erneutes Laden denselben Encoder ergeben. Bevor ein Modell lernt, sollte der Weg in die Tokenwelt und zurück funktionieren.