Handgeschriebene Ziffern erkennen
Die erste echte Klassifikationsaufgabe – das Netz soll handgeschriebene Ziffern mit 8×8 Pixeln erkennen. Der Unterschied zwischen Klassifikation und Regression, was Softmax und Kreuzentropie tun, warum man in kleinen Stapeln trainiert, und zum Schluss mit der Konfusionsmatrix sehen, wo es sich irrt.
- Etwa 45 Minuten
- Niveau: Fortgeschritten
- Getestet: 2026-09-14 torch 2.14, scikit-learn 1.9, CPU, fester Zufallsstartwert
Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.
Die Aufgaben der bisherigen Lektionen waren winzig: Hauspreise mit 50 Punkten, XOR mit 4 Punkten. Diese Lektion bearbeitet echte Daten: 1797 Bilder handgeschriebener Ziffern, und das Netz soll bei jedem Bild erkennen, welche Ziffer von 0 bis 9 darauf steht.
Das ist eine Klassifikationsaufgabe. Die Hauspreisvorhersage gibt eine kontinuierliche Zahl aus, das heißt Regression; beim Erkennen von Ziffern wählt man eine von 10 Klassen, und dafür müssen Ausgabeform und Verlustfunktion wechseln. Diese Lektion zeigt, wogegen und warum.
Die Daten stammen aus dem Datensatz handgeschriebener Ziffern, der in scikit-learn enthalten ist; nach der Installation ist er da, ohne Download:
uv add torch scikit-learn
python digit_classifier.py
Wie die Daten aussehen
digits = load_digits()
数据:1797 张图,每张 8×8 像素,像素值 0~16
第一张图,标签是 0:
::**==
**##++##::
..##.. ++==
::** ====
::== ====
::++ **--
..##::++**
--**++
Jedes Bild hat 8×8, also 64 Pixel; jeder Pixel ist eine ganze Zahl von 0 bis 16, je größer, desto dunkler. Das Skript zeichnet die Helligkeitsstufen mit einigen Zeichen nach, und man erkennt eine 0.
Die Eingabe des Netzes sind genau diese 64 Zahlen, zu einer Zeile gestreckt. Geteilt durch 16, also auf 0 bis 1 skaliert, aus demselben Grund wie die Standardisierung in Lektion 2:
X = torch.tensor(digits.data / 16.0, dtype=torch.float32)
y = torch.tensor(digits.target, dtype=torch.long)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
训练集 1347 张,测试集 450 张
Ein Viertel der Bilder wird als Testmenge zurückgehalten und dem Netz beim Training nie gezeigt. Am Ende prüft man damit, ob das Netz wirklich gelernt hat, Ziffern zu erkennen, oder sich nur die Bilder der Trainingsmenge gemerkt hat. Das ist dasselbe Prinzip wie beim Evaluationsdatensatz in Modul 06: Prüfungsfragen dürfen nicht unter den Übungsaufgaben sein.
Das Netz: 10 Punktwerte als Ausgabe
model = nn.Sequential(
nn.Linear(64, 64), # 64 个像素 → 64 个隐藏神经元
nn.ReLU(),
nn.Linear(64, 10), # → 10 个输出,分别对应数字 0~9
)
模型共 4810 个参数
nn.Sequential hängt mehrere Schichten der Reihe nach aneinander, dann muss man keine eigene Klasse schreiben. Die Parameter gezählt: erste Schicht 64×64 Gewichte plus 64 Biases, 4160; zweite Schicht 64×10 plus 10, 650; zusammen 4810.
Zwei Dinge sind anders als bisher.
Die Aktivierungsfunktion ist jetzt ReLU. Sie ist noch einfacher als tanh: Ist die Eingabe größer als 0, wird sie unverändert ausgegeben, sonst 0. Sie ist schnell zu berechnen und bei vielen Schichten leichter zu trainieren als tanh; in heutigen Netzen sind sie und ihre Varianten am verbreitetsten.
Die Ausgabe sind 10 Zahlen, eine pro Ziffer. Sie heißen Punktwerte (Logits), können beliebige positive oder negative Zahlen sein, und die größte bestimmt, welche Ziffer das Netz annimmt.
Softmax: Punktwerte in Wahrscheinlichkeiten verwandeln
10 Punktwerte lassen sich schlecht direkt deuten. softmax macht daraus 10 Wahrscheinlichkeiten: Zuerst wird jeder Punktwert exponenziert (damit er positiv ist), dann durch die Summe aller geteilt (damit alles zusammen 1 ergibt). Je größer der ursprüngliche Punktwert, desto größer die Wahrscheinlichkeit.
probs = torch.softmax(model(X_test[:1]), dim=1)[0]
Nach dem Training die Wahrscheinlichkeiten für das erste Bild der Testmenge:
测试集第一张图(标签 2)的预测概率:
0:0.00 1:0.00 2:1.00 3:0.00 4:0.00 5:0.00 6:0.00 7:0.00 8:0.00 9:0.00
Das Netz ist sich fast hundertprozentig sicher, dass es eine 2 ist, und es ist tatsächlich eine 2.
Genau dasselbe hast du in Modul 01, Lektion 2 gesehen: Ein Sprachmodell gibt bei jedem Schritt für jedes Token des Vokabulars einen Punktwert aus, Softmax macht Wahrscheinlichkeiten daraus, und danach wird das nächste Token gewählt. Der Unterschied ist nur, dass ein Sprachmodell über hunderttausend „Klassen“ hat.
Kreuzentropie: der Verlust für Klassifikation
Regression nutzt die mittlere quadratische Abweichung, Klassifikation die Kreuzentropie (cross entropy). Die Berechnung ist einfach: Man nimmt die Wahrscheinlichkeit, die das Netz der richtigen Antwort gibt, logarithmiert sie und nimmt das Negative.
正确答案的概率 损失 = -log(概率)
1.00 0.00
0.90 0.11
0.50 0.69
0.10 2.30
0.01 4.61
Je näher die Wahrscheinlichkeit der richtigen Antwort an 1 liegt, desto näher liegt der Verlust an 0; je kleiner die Wahrscheinlichkeit, desto größer der Verlust, und er steigt schnell. Ein Netz, das „selbstsicher falsch antwortet“, wird schwer bestraft.
Ein Wert ist es wert, ihn sich zu merken: Zu Beginn des Trainings weiß das Netz nichts, die Wahrscheinlichkeiten der 10 Klassen liegen alle bei etwa 0,1, und der Verlust ist etwa -log(0,1) = 2,30. Trainierst du ein Netz mit 10 Klassen und liegt der Verlust im ersten Schritt weit von 2,3 entfernt, ist vermutlich etwas falsch geschrieben.
nn.CrossEntropyLoss in PyTorch berechnet Softmax und Kreuzentropie zusammen; die letzte Schicht des Modells gibt also direkt Punktwerte aus, und man fügt nicht selbst noch ein Softmax hinzu.
loss_fn = nn.CrossEntropyLoss() # 分类问题用交叉熵
Auch das Training von GPT in Modul 09 nutzt sie; die Klassen sind dann die einzelnen Tokens des Vokabulars.
Training in kleinen Stapeln
In den bisherigen Lektionen wurde bei jedem Schritt mit allen Daten ein Gradient berechnet. Diese Lektion nimmt jedes Mal 64 Bilder:
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 31):
# 每一轮把训练集打乱,每次取 64 张图更新一次参数
order = torch.randperm(len(X_train))
for i in range(0, len(X_train), 64):
idx = order[i:i + 64]
loss = loss_fn(model(X_train[idx]), y_train[idx])
optimizer.zero_grad()
loss.backward()
optimizer.step()
Das heißt Training in kleinen Stapeln (Mini-Batch). 1347 Bilder, 64 pro Stapel, 22 Parameteraktualisierungen pro Runde. Einmal durch alle Trainingsdaten heißt eine Epoche (epoch).
Warum nicht alle Daten auf einmal? Bei großen Datensätzen passen sie gar nicht auf einmal in den Speicher; die Trainingsdaten großer Modelle umfassen Billionen Tokens. Außerdem sind die Gradienten kleiner Stapel zwar etwas verrauscht, aber es wird häufiger aktualisiert, und das lernt oft schneller als eine Aktualisierung pro Runde. Die Reihenfolge wird vor jeder Runde gemischt, damit jeder Stapel eine zufällige kleine Auswahl ist und das Netz sich nicht auf die Anordnung der Daten verlässt.
Auch der Optimierer ist jetzt Adam. Er passt, aufbauend auf SGD, die Schrittweite für jeden Parameter einzeln an: Parameter, die ständig in dieselbe Richtung gehen, bekommen größere Schritte, hin und her pendelnde kleinere. Das Problem aus Lektion 2, dass „eine Lernrate nicht zwei Richtungen bedienen kann“, mildert er automatisch deutlich. Im echten Training sind Adam und seine Variante AdamW die üblichste Standardwahl.
Trainingsergebnis
训练:
第 1 轮 最后一批的损失 0.1804 训练集准确率 87.8% 测试集准确率 87.6%
第 2 轮 最后一批的损失 0.0335 训练集准确率 92.7% 测试集准确率 92.4%
第 5 轮 最后一批的损失 0.3066 训练集准确率 97.6% 测试集准确率 96.7%
第 10 轮 最后一批的损失 0.0285 训练集准确率 98.7% 测试集准确率 96.0%
第 20 轮 最后一批的损失 0.0035 训练集准确率 99.7% 测试集准确率 97.3%
第 30 轮 最后一批的损失 0.0001 训练集准确率 100.0% 测试集准确率 97.8%
Auf meinem Rechner lief das ganze Skript in unter 5 Sekunden.
Zwei Dinge sind bemerkenswert.
Erstens springt der „Verlust des letzten Stapels“ hin und her: in Runde 2 0,03, in Runde 5 dagegen 0,31. Das ist das Rauschen kleiner Stapel: Jeder Stapel hat nur 64 Bilder (der letzte nur 1347 - 21×64 = 3), und erwischt man zufällig ein paar schwer erkennbare, ist der Verlust hoch. Ob gut trainiert wurde, beurteilt man an Kennzahlen über den gesamten Datensatz, nicht am Verlust eines einzelnen Stapels.
Zweitens erreicht die Genauigkeit auf der Trainingsmenge 100 %, auf der Testmenge 97,8 %. Bei bekannten Bildern liegt das Netz immer richtig, bei unbekannten irrt es sich manchmal. Diese Lücke ist Thema der nächsten Lektion.
Wo es sich irrt
Genauigkeit ist nur eine Zahl. Um zu wissen, wo das Netz sich irrt, sieht man sich die Konfusionsmatrix an: Zeilen sind die wahren Ziffern, Spalten die vorhergesagten, auf der Diagonalen die richtigen Antworten, an allen anderen Stellen die falschen.
测试集 450 张里错了 10 张。混淆矩阵(行是真实数字,列是预测数字):
0 1 2 3 4 5 6 7 8 9
0 37 . . . . . . . . .
1 . 42 . . . . . . 1 .
2 . . 44 . . . . . . .
3 . . 1 44 . . . . . .
4 . . . . 38 . . . . .
5 . . . . . 47 . . . 1
6 . 1 . . . . 51 . . .
7 . 1 . . . . . 47 . .
8 . 2 1 . . . . . 45 .
9 . . . . . 1 . 1 . 45
Von 10 Fehlern wurden 4 fälschlich als 1 erkannt (je eine 6 und 7, zwei 8en). Eines davon gezeichnet:
一张判错的图:真实是 7,模型认为是 1
..####::
::++==
**--..
==######++
++####--
##==
##..
..##
Diese 7 hat in der Mitte einen Querstrich, und die untere Hälfte ist ein senkrechter Strich. Bei 8×8 Auflösung ist „1“ gar nicht so abwegig. Die falschen Beispiele anzusehen ist der beste Weg, ein Modell kennenzulernen: Manchmal liegt es am Modell, manchmal sind die Daten selbst unklar, manchmal ist sogar das Label falsch. Das ist dasselbe wie das Ansehen gescheiterter Fälle in der Evaluation von Modul 06.
Übungen
- Ändere die verborgene Schicht von 64 Neuronen auf 16 und auf 256. Wie verändern sich die Genauigkeiten auf Trainings- und Testmenge?
- Ersetze
AdamdurchSGD, ebenfalls mit Lernrate 0,01. Wie hoch ist die Genauigkeit auf der Testmenge nach 30 Runden? Auf welche Lernrate muss man SGD stellen, damit es Adam einholt? - Berechne vor Beginn des Trainings einmal den Verlust über die gesamte Trainingsmenge. Liegt er nahe bei 2,30?
Selbsttest
1. Wie unterscheidet sich die Ausgabe einer Klassifikationsaufgabe von einer Regressionsaufgabe? Was tut Softmax?
Regression gibt eine kontinuierliche Zahl aus. Klassifikation gibt pro Klasse einen Punktwert aus; Softmax exponenziert diese Punktwerte und teilt durch die Summe, sodass Wahrscheinlichkeiten entstehen, die zusammen 1 ergeben, wobei Klassen mit größerem Punktwert eine größere Wahrscheinlichkeit bekommen.
2. Wie berechnet man den Kreuzentropie-Verlust? Wie hoch ist der Verlust eines Netzes mit 10 Klassen zu Beginn des Trainings ungefähr?
Man nimmt die Wahrscheinlichkeit, die das Netz der richtigen Antwort gibt, und berechnet -log(Wahrscheinlichkeit). Je näher sie an 1 liegt, desto näher liegt der Verlust an 0. Zu Beginn liegen die Wahrscheinlichkeiten der 10 Klassen alle bei etwa 0,1, der Verlust ist etwa -log(0,1) ≈ 2,30.
3. Warum schwankt der „Verlust des letzten Stapels“ so stark? Wie beurteilt man den Trainingserfolg?
Jeder Stapel ist nur ein kleiner Teil der Daten; erwischt man zufällig schwierige Beispiele, ist der Verlust hoch, und der letzte Stapel hat nur wenige Bilder. Den Erfolg beurteilt man an Kennzahlen über die gesamte Trainings- und Testmenge, etwa Genauigkeit oder den mittleren Verlust über alle Daten, nicht am Verlust eines einzelnen Stapels.
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…