Was wir bauen
Jetzt füttern wir den Tokenizer mit echtem Text. Die Datei prepare.py macht in einem Rutsch alles, was Kapitel 21 empfohlen hat:
- Grimms Märchen herunterladen und säubern.
- Den Text in Absätze zerlegen – jeder Absatz ist ein Dokument.
- Erst die Dokumente in Training (90 %) und Validierung (10 %) aufteilen.
- Dann den Tokenizer nur auf den Trainingsdokumenten lernen.
- Beide Teile in Token-IDs übersetzen, jedes Dokument mit
<bos>vorne und<eos>hinten. - Alles als Dateien speichern, damit das Training später sofort loslegen kann.
Die Reihenfolge in Punkt 3 und 4 ist kein Zufall. Würde der Tokenizer auch die Validierungstexte sehen, hätte er beim späteren Prüfen einen kleinen unfairen Vorteil – Datenleckage, Kapitel 21.
Text laden und säubern
# Daten vorbereiten: Text laden, aufteilen, Tokenizer lernen, alles in IDs übersetzen (Kapitel 21)
import json
import random
import re
import urllib.request
from pathlib import Path
import numpy as np
from tokenizer import Tokenizer
URL = "https://www.gutenberg.org/cache/epub/77905/pg77905.txt" # Grimms Märchen, gemeinfrei
DATA = Path("data")
VOCAB_SIZE = 1024Oben stehen die Einstellungen: woher der Text kommt, wohin die Daten sollen und wie groß das Vokabular wird. Path("data") ist einfach der Ordner data neben deinen Dateien.
def load_documents():
raw = DATA / "grimm.txt"
if not raw.exists():
DATA.mkdir(exist_ok=True)
print("Lade Grimms Märchen herunter …")
urllib.request.urlretrieve(URL, raw)
text = raw.read_text(encoding="utf-8")
text = text.split("*** START OF")[1].split("***", 1)[1].split("*** END OF")[0]
text = re.sub(r"\[Illustration[^\]]*\]", "", text).replace("~", "")
# Absätze sind durch Leerzeilen getrennt; Zeilenumbrüche im Absatz werden zu Leerzeichen
paragraphs = [" ".join(p.split()) for p in re.split(r"\n\s*\n", text)]
return [p for p in paragraphs if len(p) > 80 and not plattdeutsch(p)] # Überschriften, Reste, Platt wegWas hier passiert:
- Beim ersten Start lädt
urlretrieveden Text herunter und speichert ihn. Danach liegt er lokal, und das Programm lädt ihn nie wieder. - Project Gutenberg packt vor und hinter jedes Buch einen englischen Lizenztext. Den schneiden wir an den Markierungen
*** START OFund*** END OFab. - Die Ausgabe hat Bildhinweise wie
[Illustration: Marienkind]und markiert gesperrten Text mit~. Beides fliegt raus. - Absätze sind durch Leerzeilen getrennt. Innerhalb eines Absatzes machen wir aus allen Zeilenumbrüchen einfache Leerzeichen.
- Absätze unter 80 Zeichen sind meist Überschriften. Die lassen wir weg – ebenso die plattdeutschen Märchen.
def plattdeutsch(paragraph):
"""Ein paar Märchen sind auf Plattdeutsch. Unser Modell soll Hochdeutsch lernen."""
words = [" un ", " de ", " dat ", " ick ", " wat ", " nich ", " wull ", " hett "]
return sum(paragraph.lower().count(w) for w in words) >= 3Eine einfache Faustregel: Kommen in einem Absatz typische plattdeutsche Wörter wie „un“, „dat“ oder „ick“ mindestens dreimal vor, ist er raus. Nicht perfekt, aber gut genug – und ein schönes Beispiel dafür, dass Datenqualität Handarbeit ist.
Aufteilen, Tokenizer lernen, übersetzen
def main():
docs = load_documents()
random.seed(42)
random.shuffle(docs)
n_valid = len(docs) // 10
valid, train = docs[:n_valid], docs[n_valid:] # erst ganze Dokumente trennen ...
print(f"{len(train)} Trainings- und {len(valid)} Validierungsabsätze")
(DATA / "train_docs.json").write_text(json.dumps(train, ensure_ascii=False), encoding="utf-8")
tok = Tokenizer.train("\n".join(train), VOCAB_SIZE) # ... dann den Tokenizer NUR auf Training lernen
tok.save(DATA / "tokenizer.json")
print("Vokabular:", tok.vocab_size, "Tokens")
bos, eos = tok.special["<bos>"], tok.special["<eos>"]
for name, split in [("train", train), ("valid", valid)]:
ids = []
for doc in split:
ids += [bos] + tok.encode(doc) + [eos] # jedes Dokument mit Start und Ende
np.save(DATA / f"{name}.npy", np.array(ids, dtype=np.int32))
print(f"{name}: {len(ids):,} Tokens")
sample = tok.encode(train[0][:60])
print("Beispiel:", [tok.decode([i]) for i in sample])random.seed(42)sorgt dafür, dass die „zufällige“ Aufteilung bei jedem Start gleich ist (Kapitel 21, Seed).train_docs.jsonmerkt sich die Trainingsabsätze. Die brauchen wir in Schritt 7 noch einmal für den Chat.[bos] + tok.encode(doc) + [eos]rahmt jedes Dokument ein. So lernt das Modell später, wo ein Absatz anfängt und wann es aufhören soll.np.savespeichert die langen ID-Listen als NumPy-Dateien.int32reicht völlig, denn unsere größte ID ist 1023.
Die ganze Datei
# Daten vorbereiten: Text laden, aufteilen, Tokenizer lernen, alles in IDs übersetzen (Kapitel 21)
import json
import random
import re
import urllib.request
from pathlib import Path
import numpy as np
from tokenizer import Tokenizer
URL = "https://www.gutenberg.org/cache/epub/77905/pg77905.txt" # Grimms Märchen, gemeinfrei
DATA = Path("data")
VOCAB_SIZE = 1024
def load_documents():
raw = DATA / "grimm.txt"
if not raw.exists():
DATA.mkdir(exist_ok=True)
print("Lade Grimms Märchen herunter …")
urllib.request.urlretrieve(URL, raw)
text = raw.read_text(encoding="utf-8")
text = text.split("*** START OF")[1].split("***", 1)[1].split("*** END OF")[0]
text = re.sub(r"\[Illustration[^\]]*\]", "", text).replace("~", "")
# Absätze sind durch Leerzeilen getrennt; Zeilenumbrüche im Absatz werden zu Leerzeichen
paragraphs = [" ".join(p.split()) for p in re.split(r"\n\s*\n", text)]
return [p for p in paragraphs if len(p) > 80 and not plattdeutsch(p)] # Überschriften, Reste, Platt weg
def plattdeutsch(paragraph):
"""Ein paar Märchen sind auf Plattdeutsch. Unser Modell soll Hochdeutsch lernen."""
words = [" un ", " de ", " dat ", " ick ", " wat ", " nich ", " wull ", " hett "]
return sum(paragraph.lower().count(w) for w in words) >= 3
def main():
docs = load_documents()
random.seed(42)
random.shuffle(docs)
n_valid = len(docs) // 10
valid, train = docs[:n_valid], docs[n_valid:] # erst ganze Dokumente trennen ...
print(f"{len(train)} Trainings- und {len(valid)} Validierungsabsätze")
(DATA / "train_docs.json").write_text(json.dumps(train, ensure_ascii=False), encoding="utf-8")
tok = Tokenizer.train("\n".join(train), VOCAB_SIZE) # ... dann den Tokenizer NUR auf Training lernen
tok.save(DATA / "tokenizer.json")
print("Vokabular:", tok.vocab_size, "Tokens")
bos, eos = tok.special["<bos>"], tok.special["<eos>"]
for name, split in [("train", train), ("valid", valid)]:
ids = []
for doc in split:
ids += [bos] + tok.encode(doc) + [eos] # jedes Dokument mit Start und Ende
np.save(DATA / f"{name}.npy", np.array(ids, dtype=np.int32))
print(f"{name}: {len(ids):,} Tokens")
sample = tok.encode(train[0][:60])
print("Beispiel:", [tok.decode([i]) for i in sample])
if __name__ == "__main__":
main()Ausführen und prüfen
python prepare.pyDas dauert beim ersten Mal ein paar Sekunden für den Download und etwa 10 Sekunden für den Tokenizer. Du solltest ungefähr das hier sehen:
Lade Grimms Märchen herunter …
358 Trainings- und 39 Validierungsabsätze
Vokabular: 1024 Tokens
train: 139,988 Tokens
valid: 11,964 Tokens
Beispiel: ['»', 'So', ' komm', ' nach', ' Haus', ',«', ' sprach', ' der', ' J', 'unge', ...]Schau dir das Beispiel genau an: Häufige Wörter wie „ sprach“ oder „ der“ sind ein einziges Token, seltenere wie „Junge“ werden zerlegt. Genau so soll BPE arbeiten.
Jetzt ein Blick in die gespeicherten Daten. Starte python und tippe:
import numpy as np
from tokenizer import Tokenizer
tok = Tokenizer.load("data/tokenizer.json")
train = np.load("data/train.npy")
print(len(train))
print(train[:10])
print(tok.decode(train[:30]))139988
[1020 279 944 830 493 600 363 375 285 432]
»So komm nach Haus,« sprach der Junge, faßte sie am Strickchen, führte sie in den StallDie erste Zahl, 1020, ist <bos>: 256 Bytes plus 763 Merges ergeben 1019 normale Tokens (IDs 0 bis 1018), danach kommen <pad> (1019) und <bos> (1020). Der Rest sind die Wörter des ersten Absatzes. So sieht dein Modell die Welt: eine lange Kette von Zahlen.
Wenn etwas schiefgeht
URLErroroder Zeitüberschreitung beim Download – Keine Internetverbindung oder Gutenberg ist kurz nicht erreichbar. Alternativ die Datei im Browser von gutenberg.org herunterladen (eBook #77905, „Plain Text UTF-8“) und alsdata/grimm.txtspeichern.IndexErrorbeisplit("*** START OF")– Die heruntergeladene Datei ist keine Gutenberg-Textdatei, zum Beispiel eine Fehlerseite.data/grimm.txtlöschen und neu starten.- Deutlich andere Zahlen – Leicht andere Werte sind normal, falls Gutenberg den Text überarbeitet hat. Hunderte Absätze und über 100.000 Tokens sollten es aber sein.
Kurz gemerkt
prepare.pylädt den Text, säubert ihn, zerlegt ihn in Absätze und teilt zuerst in Training und Validierung.- Der Tokenizer lernt nur auf den Trainingsdaten. Jedes Dokument wird mit
<bos>und<eos>eingerahmt. - Heraus kommen zwei lange Zahlenketten in
data/: rund 140.000 Tokens zum Lernen und 12.000 zum Prüfen.