Reconnaître des chiffres manuscrits
La première vraie tâche de classification – faire reconnaître à un réseau des chiffres manuscrits de 8×8 pixels. La différence entre classification et régression, ce que font softmax et l'entropie croisée, pourquoi entraîner par mini-lots, et enfin la matrice de confusion pour voir où il se trompe.
- Environ 45 minutes
- Niveau : Intermédiaire
- Testé : 2026-09-14 torch 2.14, scikit-learn 1.9, CPU, graine aléatoire fixée
Le code et les sorties des programmes sont reproduits tels qu’ils ont tourné : commentaires et sorties sont donc en chinois.
Les tâches des leçons précédentes étaient toutes minuscules : des prix de logements sur 50 points, un XOR sur 4 points. Cette leçon traite de vraies données : 1797 images de chiffres manuscrits, et le réseau doit reconnaître, pour chaque image, lequel des chiffres de 0 à 9 y est écrit.
C'est une tâche de classification. La prédiction du prix d'un logement produit un nombre continu : c'est de la régression. Reconnaître un chiffre, c'est choisir une classe parmi 10, et il faut changer la forme de la sortie comme la fonction de perte. Cette leçon explique par quoi, et pourquoi.
Les données viennent du jeu de chiffres manuscrits fourni avec scikit-learn : il est là dès l'installation, sans téléchargement :
uv add torch scikit-learn
python digit_classifier.py
À quoi ressemblent les données
digits = load_digits()
数据:1797 张图,每张 8×8 像素,像素值 0~16
第一张图,标签是 0:
::**==
**##++##::
..##.. ++==
::** ====
::== ====
::++ **--
..##::++**
--**++
Chaque image fait 8×8, soit 64 pixels, chacun étant un entier de 0 à 16 : plus il est grand, plus c'est foncé. Le script dessine les nuances avec quelques caractères, et l'on reconnaît un 0.
L'entrée du réseau, ce sont simplement ces 64 nombres, mis en une ligne. On les divise par 16 pour les ramener entre 0 et 1, pour la même raison que la standardisation de la leçon 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 张
Un quart des images est mis de côté comme jeu de test, jamais montré au réseau pendant l'entraînement. À la fin, il sert à vérifier si le réseau a vraiment appris à reconnaître des chiffres, ou s'il a seulement mémorisé les images du jeu d'entraînement. C'est la même logique que le jeu d'évaluation du module 06 : les questions de contrôle ne doivent pas figurer parmi les exercices.
Le réseau : 10 scores en sortie
model = nn.Sequential(
nn.Linear(64, 64), # 64 个像素 → 64 个隐藏神经元
nn.ReLU(),
nn.Linear(64, 10), # → 10 个输出,分别对应数字 0~9
)
模型共 4810 个参数
nn.Sequential enchaîne plusieurs couches dans l'ordre, ce qui évite d'écrire sa propre classe. Comptons les paramètres : la première couche a 64×64 poids plus 64 biais, soit 4160 ; la seconde 64×10 plus 10, soit 650 ; au total 4810.
Deux choses diffèrent de ce qui précède.
La fonction d'activation devient ReLU. Elle est encore plus simple que tanh : si l'entrée est positive, elle est renvoyée telle quelle, sinon 0. Elle se calcule vite et s'entraîne mieux que tanh quand il y a beaucoup de couches ; c'est elle, avec ses variantes, qu'on trouve le plus souvent dans les réseaux actuels.
La sortie est de 10 nombres, un par chiffre. On les appelle scores (logits) ; ils peuvent être positifs ou négatifs, et le plus grand désigne le chiffre retenu par le réseau.
softmax : transformer les scores en probabilités
10 scores ne s'interprètent pas directement. softmax les transforme en 10 probabilités : on prend d'abord l'exponentielle de chaque score (pour qu'il soit positif), puis on divise par la somme de tous (pour que le total fasse 1). Plus un score était grand, plus sa probabilité l'est.
probs = torch.softmax(model(X_test[:1]), dim=1)[0]
Après l'entraînement, les probabilités pour la première image du jeu de test :
测试集第一张图(标签 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
Le réseau est presque sûr à cent pour cent que c'est un 2, et c'en est bien un.
Vous avez vu exactement la même chose à la leçon 2 du module 01 : à chaque étape, un grand modèle produit un score pour chaque token du vocabulaire, que softmax transforme en probabilités, avant de choisir le token suivant selon ces probabilités. La seule différence : un grand modèle a plus de cent mille « classes ».
L'entropie croisée : la perte de la classification
La régression utilise l'erreur quadratique moyenne ; la classification utilise l'entropie croisée (cross entropy). Son calcul est simple : on regarde la probabilité que le réseau donne à la bonne réponse, on en prend le logarithme, puis l'opposé.
正确答案的概率 损失 = -log(概率)
1.00 0.00
0.90 0.11
0.50 0.69
0.10 2.30
0.01 4.61
Plus la probabilité de la bonne réponse est proche de 1, plus la perte est proche de 0 ; plus elle est petite, plus la perte est grande, et elle grimpe vite. Un réseau qui « se trompe avec assurance » est lourdement sanctionné.
Une valeur mérite d'être retenue : au début de l'entraînement, le réseau ne sait rien, les probabilités des 10 classes valent toutes environ 0,1, et la perte vaut à peu près -log(0,1) = 2,30. Si vous entraînez un réseau à 10 classes et que la perte de la première étape est loin de 2,3, il y a très probablement une erreur quelque part.
Le nn.CrossEntropyLoss de PyTorch calcule softmax et entropie croisée ensemble ; la dernière couche du modèle produit donc directement les scores, sans softmax ajouté par vous.
loss_fn = nn.CrossEntropyLoss() # 分类问题用交叉熵
L'entraînement de GPT au module 09 l'utilise aussi ; les classes seront alors chacun des tokens du vocabulaire.
L'entraînement par mini-lots
Dans les leçons précédentes, chaque étape calculait le gradient sur toutes les données. Cette leçon prend à chaque fois 64 images :
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()
C'est l'entraînement par mini-lots (mini-batch). 1347 images, 64 par lot, soit 22 mises à jour des paramètres par passe. Un passage sur toutes les données d'entraînement s'appelle une époque (epoch).
Pourquoi ne pas utiliser toutes les données d'un coup ? Avec de grands jeux de données, elles ne tiennent tout simplement pas en mémoire ; les données d'entraînement des grands modèles comptent des milliers de milliards de tokens. Et le gradient d'un mini-lot, même un peu bruité, permet des mises à jour fréquentes, qui apprennent souvent plus vite qu'une seule mise à jour par passe. On mélange l'ordre au début de chaque passe pour que chaque lot soit une petite poignée aléatoire, et que le réseau ne dépende pas de l'ordre des données.
L'optimiseur devient lui aussi Adam. Il ajuste, sur la base de SGD, la taille du pas pour chaque paramètre séparément : les paramètres qui avancent toujours dans la même direction font de plus grands pas, ceux qui oscillent de plus petits. Le problème de la leçon 2, « un seul taux d'apprentissage ne peut pas satisfaire deux directions », il l'atténue beaucoup automatiquement. Dans la pratique, Adam et sa variante AdamW sont les choix par défaut les plus courants.
Résultats de l'entraînement
训练:
第 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%
Sur mon ordinateur, tout le script s'exécute en moins de 5 secondes.
Deux points méritent attention.
D'abord, la « perte du dernier lot » saute d'une valeur à l'autre : 0,03 à la passe 2, mais 0,31 à la passe 5. C'est le bruit des mini-lots : chaque lot ne compte que 64 images (le dernier seulement 1347 - 21×64 = 3), et s'il tombe par hasard sur quelques images difficiles, la perte est élevée. Pour juger si l'entraînement est bon, il faut regarder les indicateurs sur l'ensemble des données, pas la perte d'un lot.
Ensuite, la précision atteint 100 % sur le jeu d'entraînement et 97,8 % sur le jeu de test. Sur les images déjà vues, le réseau a tout juste ; sur les autres, il se trompe un peu. Cet écart fait l'objet de la leçon suivante.
Où il se trompe
La précision n'est qu'un nombre. Pour savoir où le réseau se trompe, on regarde la matrice de confusion : les lignes sont les vrais chiffres, les colonnes les chiffres prédits ; sur la diagonale, les bonnes réponses, ailleurs, les erreurs.
测试集 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
Sur 10 erreurs, 4 sont des chiffres pris pour un 1 (un 6 et un 7, et deux 8). Dessinons l'un d'eux :
一张判错的图:真实是 7,模型认为是 1
..####::
::++==
**--..
==######++
++####--
##==
##..
..##
Ce 7 a une barre au milieu, et sa moitié inférieure n'est qu'un trait vertical. À une résolution de 8×8, y voir un 1 n'est pas si absurde. Regarder les exemples ratés est le meilleur moyen de comprendre un modèle : parfois le problème vient du modèle, parfois les données elles-mêmes sont floues, parfois même l'étiquette est fausse. C'est la même chose que l'examen des cas d'échec de l'évaluation au module 06.
Exercices
- Passez la couche cachée de 64 neurones à 16 puis à 256. Comment évoluent les précisions sur les jeux d'entraînement et de test ?
- Remplacez
AdamparSGD, toujours avec un taux d'apprentissage de 0,01. Quelle est la précision sur le jeu de test après 30 passes ? À quel taux d'apprentissage SGD rattrape-t-il Adam ? - Avant le début de l'entraînement, calculez la perte sur l'ensemble du jeu d'entraînement. Est-elle proche de 2,30 ?
Auto-test
1. En quoi la sortie d'une tâche de classification diffère-t-elle de celle d'une régression ? Que fait softmax ?
Une régression produit un nombre continu. Une classification produit un score par classe ; softmax prend l'exponentielle de ces scores et divise par leur somme, pour en faire des probabilités qui totalisent 1, les classes au score plus élevé ayant une probabilité plus grande.
2. Comment se calcule la perte d'entropie croisée ? Quelle est à peu près la perte d'un réseau à 10 classes au début de l'entraînement ?
On prend la probabilité que le réseau donne à la bonne réponse, et l'on calcule -log(probabilité). Plus elle est proche de 1, plus la perte est proche de 0. Au début de l'entraînement, les probabilités des 10 classes sont toutes autour de 0,1, et la perte vaut à peu près -log(0,1) ≈ 2,30.
3. Pourquoi la « perte du dernier lot » fluctue-t-elle ? Comment juger l'efficacité de l'entraînement ?
Chaque lot n'est qu'une petite partie des données ; s'il contient par hasard des exemples difficiles, la perte est élevée, et le dernier lot ne compte que quelques images. Pour juger, il faut regarder les indicateurs sur l'ensemble des jeux d'entraînement et de test, comme la précision ou la perte moyenne sur toutes les données, et non la perte d'un lot.
Questions et discussion
Bloqué sur cette leçon ? Posez votre question ici. Et si vous pouvez répondre à quelqu'un, n'hésitez pas.
Une question rapporte 3 points, une réponse 6. Les messages paraissent après vérification.
Chargement de la discussion…