Modul 09 · Lektion 5

Trainieren: das Modell lernt Tang-Gedichte schreiben

Das GPT der vorigen Lektion mit 34.000 Tang-Gedichten trainieren, sieben Minuten auf der CPU eines Laptops, und zusehen, wie es sich vom Zeichensalat Schritt für Schritt Gedichte mit fünf und sieben Zeichen pro Zeile aneignet. Dann bekommt es nur dreihundert Gedichte, und man sieht, wie es sie auswendig lernt, statt Gedichte schreiben zu lernen.

  • Etwa 50 Minuten
  • Niveau: Vertiefung
  • Getestet: 2026-09-15 torch 2.14, Apple-M4-CPU, fester Zufallsstartwert

Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.

Das Modell steht, jetzt wird es trainiert. Der Code dieser Lektion, train.py, hat nur gut hundert Zeilen und denselben Aufbau wie die Trainingsschleife aus Modul 08: einen Stapel Daten nehmen, Verlust berechnen, Backpropagation, Parameter aktualisieren.

python train.py

Daten: um eine Position verschoben

Die Trainingsdaten eines Sprachmodells müssen nicht von Menschen annotiert werden. Ein Text ist selbst Aufgabe und Antwort:

def get_batch(ids, generator=None):
    """随机截取 BATCH 段长度为 block_size 的文字。目标 y 就是 x 往后错一位:每个位置都要预测下一个字。"""
    starts = torch.randint(len(ids) - cfg.block_size - 1, (BATCH,), generator=generator)
    x = torch.stack([ids[s:s + cfg.block_size] for s in starts])
    y = torch.stack([ids[s + 1:s + cfg.block_size + 1] for s in starts])
    return x, y

Alle Gedichte werden mit Zeilenumbrüchen zu einer langen Zeichenkette verbunden, daraus werden zufällig 32 Abschnitte zu je 128 Zeichen als Eingabe x herausgeschnitten; jeden Abschnitt um eine Position nach hinten verschoben, ergibt die Antwort y. Ist die Eingabe etwa „白日依山尽,“, lautet die Antwort „日依山尽,黄“: Die 1. Position sieht „白“ und muss „日“ raten, die 2. sieht „白日“ und muss „依“ raten. Wie in der vorigen Lektion gesagt, sorgt die kausale Maske dafür, dass keine Position die Antwort abschreiben kann.

Der Zeilenumbruch hat hier eine besondere Bedeutung: Er markiert das Ende eines Gedichts und den Beginn des nächsten. Das Modell lernt „nach dem Punkt folgt ein Zeilenumbruch“, und beim Generieren kann es bei einem Zeilenumbruch aufhören; dann ist ein Gedicht fertig.

Die letzten 1000 Gedichte bleiben als Validierungsmenge zurück und werden beim Training nicht gesehen. So kann man mit der Methode aus Modul 08, Lektion 6 prüfen, ob das Modell lernt oder auswendig lernt.

训练集 34135 首诗,1547255 个词元;验证集 1000 首;词表 6289 个字符
模型:4 层,4 个头,向量维度 128,共 1,614,720 个参数

Einige Details der Trainingsschleife

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.1)
for step in range(1, args.steps + 1):
    for group in optimizer.param_groups:
        group["lr"] = lr_at(step - 1)
    x, y = get_batch(train_ids)
    _, loss = model(x, y)
    optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # 梯度裁剪:防止偶尔一步梯度太大把训练带飞
    optimizer.step()

Verglichen mit Modul 08 kommen zwei Dinge hinzu.

Die Lernrate ändert sich. In den ersten 100 Schritten steigt sie langsam von 0 auf 0,001; das heißt Warm-up: Zu Trainingsbeginn sind die Parameter zufällig und die Richtung der Gradienten sehr unzuverlässig, zu große Schritte führen leicht in die Irre. Danach fällt die Lernrate entlang einer Kosinuskurve langsam auf 0,0001; in der späten Trainingsphase sind die Schritte kleiner, und man findet feiner die Stellen mit niedrigem Verlust.

def lr_at(step, peak=1e-3, warmup=100):
    """学习率:前 100 步从 0 慢慢升上去(预热),然后按余弦曲线降到峰值的十分之一。"""
    if step < warmup:
        return peak * (step + 1) / warmup
    progress = (step - warmup) / max(1, args.steps - warmup)
    return peak * (0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * progress)))

Gradient Clipping. Ist der Gradient eines Schritts besonders groß (bei einem ungewöhnlichen Datenstapel), wird er proportional verkleinert, sodass seine Gesamtgröße 1 nicht überschreitet. Modul 08, Lektion 2 hat gezeigt, dass ein zu großer Schritt das Training direkt divergieren lassen kann.

„Warm-up + Kosinus-Abfall + Gradient Clipping + AdamW“ ist die üblichste Kombination beim Training von Transformern; auch große Modelle werden im Wesentlichen so trainiert, nur mit anderen Zahlenwerten.

Zusehen, wie es Gedichte schreiben lernt

3000 Schritte Training; in Abständen wird der Verlust angesehen, und es schreibt zwei Gedichte:

训练前:训练损失 8.779,验证损失 8.778(随便猜的话是 ln(6289) = 8.747)
  训练前随便写的: 耦呌同捐睢寮办猬囊斵樬劫鞑顷饼遁瞩赴冈譀嗾雅踯牧写禧氛迹樯毁尤平螵蔻郓祐菼灺嵊匦

第 300 步(27 秒):训练损失 5.778,验证损失 5.818
  将南。
  门不独。到李斜,功觅青。谁将风花不可碧,不和山。

第 1000 步(97 秒):训练损失 4.777,验证损失 4.879
  山里怅望两间道,千里长亭寺断肠。曾梳白兰草,试向玉关鱼。
  此日醉前年,相思高至兹。云如汉陵子,暮水九重宫。

第 2000 步(249 秒):训练损失 4.410,验证损失 4.583
  秋江野鸟过高楼,野树猿声怨见人。处处秋风满孤照,沧海无端行处闻。
  春山度滟月,多少漫为秋。药罢已开葬,松阴不道开。

第 3000 步(407 秒):训练损失 4.280,验证损失 4.488
  江山古馆响幽幽,山鸟无人见白头。此时猩猩争得语,又将杯酒醉参差。
  分明人在泪,明月更经过。小谷闲烟树,红潭古石床。归来扶白首,立向卧青山。独有安行处,如何却得还。

训练用了 408 秒,模型存到 .cache/gpt.pt

Auf meinem Rechner (Apple M4, nur CPU) dauerten 3000 Schritte unter 7 Minuten. Ein Schritt verarbeitet 32×128 = 4096 Zeichen, 3000 Schritte sahen insgesamt etwa 12 Millionen Zeichen, also etwa 8 Durchgänge durch die Trainingsmenge.

Was es lernt, erscheint in Etappen:

  • Vor dem Training: eine Folge zufälliger Zeichen, Verlust 8,78, also reines Raten.
  • 300 Schritte: Es hat Satzzeichen gelernt und schreibt häufige Zeichen, aber die Zeilen sind unterschiedlich lang.
  • 1000 Schritte: Die meisten Zeilen haben fünf oder sieben Zeichen, Komma und Punkt wechseln sich ab. Im ersten Gedicht haben die ersten zwei Zeilen aber sieben Zeichen und die letzten zwei fünf; es weiß noch nicht, dass ein Gedicht einheitlich sein muss.
  • 2000 Schritte: Das Format stimmt im Wesentlichen, und es gibt erste Zeilen mit zusammenhängenden Bildern wie „秋江野鸟过高楼,野树猿声怨见人“ (über den Herbstfluss ziehen Wildvögel am hohen Turm vorbei, in wilden Bäumen klagen Affenrufe über Menschen).
  • 3000 Schritte: Das erste ist ein vollständiges Jueju mit sieben Zeichen pro Zeile, „江山古馆响幽幽,山鸟无人见白头“; das zweite ein vollständiges Lüshi mit fünf Zeichen pro Zeile, acht Zeilen.

Was es nicht gelernt hat, ist ebenso deutlich: Der Sinn hängt oft nicht zusammen („此时猩猩争得语“, in diesem Moment streiten Orang-Utans um Worte), und auf Tonmuster und Reim achtet es nicht. Mit 1,61 Millionen Parametern und 7 Minuten Training ist das schon ordentlich.

Während des ganzen Trainings sinken Trainings- und Validierungsverlust und bleiben dicht beieinander (am Ende 4,28 und 4,49). Nach Modul 08, Lektion 6 heißt das: keine deutliche Überanpassung; bei unbekannten Gedichten ist es fast genauso gut.

Schreibt es Gedichte oder sagt es sie auf?

Die vorigen Module dieses Kurses haben es immer wieder betont: Dass Generiertes gut aussieht, heißt nicht, dass das Modell wirklich etwas gelernt hat; es könnte die Trainingsdaten auswendig gelernt haben. Deshalb prüft das Skript nach dem Training: 100 Gedichte generieren und zählen, wie viele Zeilen genau einer Zeile der Trainingsmenge entsprechen.

train_sentences = {s for p in train_poems for s in p.replace("。", ",").split(",") if s}
torch.manual_seed(1)
generated = sample(100)
sentences = [s for p in generated for s in p.replace("。", ",").split(",") if s]
copied = sum(s in train_sentences for s in sentences)
生成 100 首诗,共 576 句,其中 0 句(0%)和训练集里的某一句一模一样
整首和训练集里某一首一模一样的:0 首

576 Zeilen, keine einzige abgeschrieben. Gelernt hat es die „Schreibweise“ von Tang-Gedichten: Format, häufige Zeichen und Wörter, welche Zeichen oft zusammen vorkommen, und daraus setzt es neue Zeilen zusammen.

Nur 300 Gedichte

Modul 08, Lektion 6 hat gesagt, dass ein Modell bei zu wenig Daten auswendig lernt. Jetzt bekommt dasselbe Modell nur 300 Gedichte:

python train.py --poems 300 --steps 1500 --out small.pt
训练集 300 首诗,13612 个词元;验证集 1000 首;词表 6289 个字符

第 150 步(22 秒):训练损失 5.079,验证损失 6.584
第 500 步(70 秒):训练损失 0.421,验证损失 8.864
第 1000 步(145 秒):训练损失 0.103,验证损失 9.817
  雕鹗途程在碧天,彩衣东去复何言。二千宾客旧知己,十二山河新故园。吟看桂生溪月上,醉听鲲化海涛翻。好期圣代重相见,莫学袁生老竹轩。
第 1500 步(230 秒):训练损失 0.071,验证损失 9.984

生成 100 首诗,共 593 句,其中 454 句(77%)和训练集里的某一句一模一样
整首和训练集里某一首一模一样的:40 首

Der Trainingsverlust fällt auf 0,07, viel niedriger als die 4,28 mit allen Daten. Der Validierungsverlust aber steigt stetig auf 9,98, höher als die 8,78 des zufälligen Ratens vor dem Training.

Das Lüshi mit sieben Zeichen pro Zeile bei Schritt 1000 ist wunderbar ebenmäßig, weil es Zeichen für Zeichen aus der Trainingsmenge aufgesagt ist. Die abschließende Prüfung bestätigt das: 77 % der Zeilen sind abgeschrieben, 40 von 100 Gedichten vollständig.

1,61 Millionen Parameter und nur 13.000 Zeichen zu lernen: Das Modell kann sich alle problemlos merken. Auswendiglernen ist der bequemste Weg, den Trainingsverlust zu senken. Bei unbekannten Gedichten ist es sogar schlechter, als hätte es nichts gelernt: Es ist sich bei dem Auswendiggelernten zu „sicher“, und bei anderen Gedichten gibt es der richtigen Antwort eine jämmerlich kleine Wahrscheinlichkeit.

Dasselbe Modell, derselbe Code, nur die Daten von 34.000 auf 300 Gedichte geändert, und das Ergebnis wechselt von „kann Gedichte schreiben“ zu „sagt Gedichte auf“. Das ist das Fazit aus Modul 08, Lektion 6, noch einmal an einem Sprachmodell vorgeführt, und der Grund, warum große Modelle riesige Datenmengen brauchen.

Was ein Verlust von 4,49 bedeutet

Der Validierungsverlust ist am Ende 4,49. Kreuzentropie ist -log(Wahrscheinlichkeit des richtigen Zeichens); im Schnitt gibt das Modell dem richtigen nächsten Zeichen also eine Wahrscheinlichkeit von etwa e hoch -4,49, rund 1,1 %.

Das klingt niedrig, aber bedenke: Beim Gedichteschreiben gibt es für das nächste Zeichen ohnehin viele sinnvolle Möglichkeiten. Nach „白日依山尽,黄河入海“ verteilt das Modell etwas Wahrscheinlichkeit auf „无“, „间“, „多“, „深“ (die genauen Zahlen sehen wir in der nächsten Lektion). Im Original steht „流“ (fließt), aber ein anderes Zeichen ist nicht unbedingt falsch. Der Verlust kann nicht auf 0 sinken; das ist dasselbe wie in Modul 08, Lektion 1: „Die Daten schwanken zufällig, der Verlust sinkt nie auf 0.“

Ein Maßstab ist der Vergleich mit zufälligem Raten: Das wählt gleichmäßig aus 6289 Zeichen, Verlust 8,75. 4,49 entspricht einer Verkleinerung der Auswahl von 6289 Zeichen auf etwa e hoch 4,49, rund 89 Zeichen.

Übungen

  1. Setz --steps auf 6000. Sinkt der Verlust weiter? Werden die generierten Gedichte besser?
  2. Setz die Zahl der Trainingsgedichte auf 1000, 3000 und 10000 (--poems), trainiere jeweils 1500 Schritte, notiere Validierungsverlust und Anteil „abgeschriebener Zeilen“ und stell eine Tabelle auf.
  3. Entferne Warm-up und Abfall der Lernrate (lr_at gibt direkt 0,001 zurück), trainiere ebenfalls 3000 Schritte und vergleiche den abschließenden Validierungsverlust.

Selbsttest

1. Was sind beim Training eines Sprachmodells Eingabe und Antwort?

Aus dem Text wird ein Abschnitt als Eingabe herausgeschnitten, und derselbe Abschnitt um eine Position nach hinten verschoben ist die Antwort. Jede Position muss aus sich selbst und den vorigen Zeichen das nächste Zeichen vorhersagen. Ein Text von 128 Zeichen liefert zugleich 128 Vorhersageaufgaben.

2. Welche Probleme lösen Warm-up der Lernrate und Gradient Clipping jeweils?

Warm-up: Zu Trainingsbeginn sind die Parameter zufällig und die Richtung der Gradienten unzuverlässig; eine große Lernrate von Anfang an führt leicht in die Irre, deshalb beginnt man mit einer kleinen und erhöht sie langsam. Gradient Clipping: Gelegentlich ist der Gradient eines Schritts besonders groß; direkt damit zu aktualisieren ließe die Parameter zu weit springen und das Training womöglich divergieren, deshalb werden zu große Gradienten proportional verkleinert.

3. Warum ist beim Training mit nur 300 Gedichten der Trainingsverlust sehr niedrig, der Validierungsverlust aber höher als beim zufälligen Raten?

Das Modell hat genug Parameter, um alle 300 Gedichte auswendig zu lernen, deshalb ist der Trainingsverlust sehr niedrig. Gelernt hat es aber diese 300 Gedichte selbst, nicht die Regeln des Dichtens. Bei Auswendiggelerntem ist es sich sehr sicher; bei unbekannten Gedichten gibt es der richtigen Antwort eine sehr kleine Wahrscheinlichkeit, und die Kreuzentropie ist höher als beim zufälligen Raten. Dass 77 % der Zeilen der generierten Gedichte abgeschrieben sind, bestätigt das.

Fragen und Diskussion

Hängst du in dieser Lektion fest? Frag hier. Und wenn du die Frage von jemandem beantworten kannst, tu es gern.

Eine Frage bringt 3 Punkte, eine Antwort 6. Beiträge erscheinen nach der Prüfung.

Diskussion wird geladen…