Aller au contenu
English

La boucle d'entraînement

  • Leçon 10
  • 50 min
  • Module 3 : Comment les réseaux de neurones apprennent

Ce que vous saurez faire

Entraînez réellement le routeur de billets de soutien : répétez cinq étapes, dans un ordre fixe, jusqu’à obtenir une faible perte. Gardez un journal de cette perte. Tout entraînement dans PyTorch suit cette boucle. Une étape manquante ne produit généralement aucun message d’erreur, seulement un réseau qui apprend mal ou pas du tout.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient toutes les cellules de code, les tâches et les vérifications de cette leçon. Il exécute la boucle d’entraînement en lot complet sur les billets et mesure le résultat. Ensuite, il omet chaque étape à tour de rôle et montre les gradients qui s’accumulent sans zero_grad(). L’entraînement prend quelques secondes sur un processeur. Environ 25 minutes.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Rien pour travailler dans le carnet.
  • Pour travailler localement : PyTorch et le dossier ticket-router, de la leçon Installer PyTorch. Ajoutez aussi les lignes de configuration de Les billets sous forme de nombres au début de chaque fichier de code.

Testé avec Python 3.12 et PyTorch 2.14.1 sur un processeur.

Une joueuse de basketball s’exerce aux lancers francs. Chaque lancer suit les mêmes étapes. Elle oublie la dernière correction. Elle lance. Elle observe à quelle distance le ballon a manqué la cible. Elle détermine la correction : un peu plus de force, un peu vers la gauche. Ensuite, elle modifie son lancer et recommence.

L’ordre compte. Si elle modifie son lancer avant de voir où le ballon atterrit, elle corrige sans raison. Et si elle n’oublie pas les corrections précédentes, chacune s’ajoute à la nouvelle. Après quelques lancers, elle corrige beaucoup trop.

Le problème : exécuter les éléments dans l’ordre, des milliers de fois

Section intitulée « Le problème : exécuter les éléments dans l’ordre, des milliers de fois »

Les trois dernières leçons vous ont donné les éléments nécessaires. La perte indique à quel point le réseau se trompe. La descente de gradient modifie les poids, et la rétropropagation fournit les gradients. Pour entraîner le routeur de billets, il faut exécuter ces éléments des centaines ou des milliers de fois. Toujours dans le même ordre.

Chaque élément occupe une ligne de code. Une ligne manquante ou mal placée peut facilement passer inaperçue. Pire encore, PyTorch ne signale aucune erreur pour la plupart de ces problèmes. Le réseau s’entraîne, la perte s’affiche, mais le résultat est simplement mauvais. Il vous faut donc une structure d’entraînement fixe, que vous pouvez vérifier d’un coup d’œil.

L’idée : la boucle d’entraînement en cinq étapes

Section intitulée « L’idée : la boucle d’entraînement en cinq étapes »

La boucle d’entraînement répète cinq étapes. Un cycle de ces cinq étapes constitue un pas d’entraînement : une mise à jour des poids.

Cinq cases dans un cycle : 1 Remettre les gradients à zéro, optimizer.zero_grad(); 2 Propagation avant, scores = model(x); 3 Perte, loss = loss_fn(scores, y); 4 Rétropropagation, loss.backward(); 5 Mettre à jour les poids, optimizer.step(). Une flèche portant l'étiquette « lot suivant » revient de 5 à 1.
Les cinq étapes d'un pas d'entraînement, toujours dans cet ordre. La boucle les répète pour le lot suivant de billets.
Description textuelle du diagramme

Cinq cases dans un cycle. Étape 1, remettre les gradients à zéro : optimizer.zero_grad(). Étape 2, la propagation avant : scores = model(x). Étape 3, la perte : loss = loss_fn(scores, y). Étape 4, la rétropropagation : loss.backward(). Étape 5, mettre à jour les poids : optimizer.step(). Une flèche portant l’étiquette « lot suivant » revient de l’étape 5 à l’étape 1.

  1. Remettre les gradients à zéro. backward() s’ajoute aux gradients déjà présents. Effacez-les donc d’abord : la joueuse oublie les corrections précédentes.
  2. Propagation avant. Le réseau calcule ses scores pour les billets d’entraînement : le lancer.
  3. Perte. Comparez les scores aux bonnes équipes : la distance entre le ballon et la cible.
  4. Rétropropagation. Un gradient pour chaque paramètre : la correction.
  5. Mise à jour. Déplacez chaque paramètre dans le sens opposé à son gradient : modifier le lancer.

Les étapes 1 et 5 relèvent de l’optimiseur : l’objet qui conserve la liste des paramètres et les modifie. torch.optim.SGD(model.parameters(), lr=0.5) applique exactement la règle de mise à jour de la leçon sur la descente de gradient. Chaque paramètre moins 0,5 fois son gradient. Le cours utilise torch.optim.Adam avec un taux d’apprentissage de 0,001. Adam utilise aussi la descente de gradient, mais adapte la taille du pas pour chaque paramètre. Taux d’apprentissage et optimiseurs compare les deux. Pour l’instant, voyez Adam comme une « descente de gradient avec laquelle il est plus difficile de se tromper ».

Priya commence par la version la plus simple. Chaque pas utilise les 4 696 billets d’entraînement en même temps. C’est ce qu’on appelle l’entraînement en lot complet. Elle crée l’optimiseur une seule fois, avant la boucle, et consigne la perte tous les 10 pas.

train_full_batch.py
set_seed(0)
model = BagOfWordsNet(len(vocab))
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for step in range(1, 101):
optimizer.zero_grad() # 1. forget the last step's gradients
scores = model(x_train) # 2. forward pass: all 4,696 training tickets
loss = loss_fn(scores, y_train) # 3. how wrong?
loss.backward() # 4. backward pass: a gradient for every parameter
optimizer.step() # 5. update every parameter
if step % 10 == 0:
print(f"step {step:3} loss {loss.item():.4f}")

La sortie :

step 10 loss 1.4113
step 20 loss 1.1342
step 30 loss 0.8550
step 40 loss 0.6181
step 50 loss 0.4454
step 60 loss 0.3313
step 70 loss 0.2573
step 80 loss 0.2076
step 90 loss 0.1724
step 100 loss 0.1460

model.parameters() fournit à l’optimiseur les 183 685 paramètres du réseau. step % 10 == 0 est vrai tous les dix pas, ce qui garde le journal court. L’exécution complète prend environ 1,5 seconde sur un processeur. Le graphique montre la perte à chaque pas, pas seulement tous les dix pas.

La perte d'entraînement à chacun des 100 pas en lot complet : Adam, taux d'apprentissage de 0,001, les 4 696 billets d'entraînement à chaque pas. Exécution réelle, PyTorch 2.14.1 sur un processeur.
Description textuelle du graphique

La perte commence à 1,624, la perte d’une prédiction qui donne la même probabilité à chaque équipe. Elle baisse lentement aux premiers pas, puis rapidement entre les pas 10 et 50 (de 1,41 à 0,45). Ensuite, elle baisse plus lentement encore : 0,26 au pas 70 et 0,146 au pas 100. Elle baisse à chaque pas; la courbe ne présente aucun saut.

La perte baisse à chaque pas, sans saut, contrairement à la descente de gradient simple de la dernière leçon. Priya évalue ensuite le réseau entraîné sur les 600 billets de validation, qu’il n’a jamais utilisés pour s’entraîner :

train_full_batch.py
valid_loss, valid_acc = evaluate(model, x_valid, y_valid)
print(f"validation loss {valid_loss:.4f} accuracy {valid_acc:.4f}")

La sortie :

validation loss 0.1985 accuracy 0.9783

De 18,2 % à 97,8 % en 100 pas. Omar demande ce que cela signifie pour l’équipe de Grace. Environ 98 billets sur 100 arrivent maintenant à la bonne équipe sans qu’une personne les lise. Ce résultat ne dépasse pas encore celui du modèle TF-IDF du cours d’apprentissage automatique. Ce modèle atteint 98,8 % sur les mêmes billets; le cours les compare équitablement à la fin.

Priya effectue quatre entraînements de 20 pas. Une fois avec les cinq étapes, puis une fois sans chacune des étapes 1, 4 et 5. Voici la perte au pas 20 de chaque entraînement, tirée du carnet :

Élément omis Perte au pas 20 Message d’erreur? Ce qui s’est passé
rien 1.1342 non Le réseau apprend.
zero_grad() 1.1745 non Il apprend, mais moins bien : les anciens gradients s’accumulent.
backward() 1.6236 non Aucun gradient, donc step() n’a rien à utiliser. Le réseau reste inchangé.
step() 1.6236 non Les gradients sont calculés, puis ignorés. Le réseau reste inchangé.

Aucun des quatre entraînements ne s’arrête avec une erreur. Une perte qui reste à sa valeur initiale (1,6236 ici) indique que les poids ne changent jamais. Cherchez un appel manquant à backward() ou à step(). Omettre l’étape 2 ou 3 n’est pas possible : sans elles, aucune perte ne permet d’appeler backward().

Chaque réseau que vous entraînez dans PyTorch utilise cette boucle. Les bibliothèques de plus haut niveau la cachent dans une fonction fit() ou train(), mais les cinq étapes restent présentes. Le fichier ticketnet.py du cours en contient une, train(), avec les mêmes cinq lignes et les mêmes commentaires. Dès la prochaine leçon, vous pourrez l’utiliser.

L’entraînement en lot complet a deux coûts. D’abord, la mémoire : chaque pas conserve tous les billets d’entraînement et les données de leur propagation avant. Les fréquences des mots seules représentent 4 696 × 2 864 nombres, soit environ 54 Mo. Les jeux de données réels comptent des millions de lignes. Ensuite, la vitesse d’apprentissage : chaque pas exige un passage complet sur les données. Ainsi, 100 pas lisent chaque billet 100 fois. La prochaine leçon règle ces deux problèmes avec de petits lots.

  1. Dans le carnet, allez à la section 4.
  2. Créez un nouveau réseau avec set_seed(0) et BagOfWordsNet(len(vocab)).
  3. Créez l’optimiseur : torch.optim.SGD(model.parameters(), lr=0.5).
  4. Écrivez les cinq étapes dans une boucle de 10 pas, et ajoutez chaque loss.item() à losses.
  5. Exécutez la cellule de vérification. Vous devriez voir Check passed (vérification réussie). Les dix pertes commencent à 1,6236 et finissent à 1,4191.
Afficher un exemple de réponse
train_sgd.py
losses = []
set_seed(0)
model = BagOfWordsNet(len(vocab))
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
for step in range(10):
optimizer.zero_grad()
loss = loss_fn(model(x_train), y_train)
loss.backward()
optimizer.step()
losses.append(loss.item())

Ce sont les mêmes dix pas que la descente de gradient simple sur le réseau de billets dans la dernière leçon. Les pertes sont : 1,624, 1,599, 1,578 … 1,419. torch.optim.SGD applique la règle de mise à jour pour vous. Adam a atteint 1,411 après les mêmes dix pas, une valeur un peu plus faible.

À vous de jouer : un taux d’apprentissage 10 fois plus grand

Section intitulée « À vous de jouer : un taux d’apprentissage 10 fois plus grand »

Faites passer le taux d’apprentissage d’Adam de 0,001 à 0,01. Entraînez de nouveau le réseau pendant 100 pas en lot complet. Avant l’exécution, faites une prédiction : la perte d’entraînement après 100 pas sera-t-elle plus faible ou plus élevée? Et l’exactitude sur les données de validation?

Afficher la réponse

Résultat de Priya : la perte d’entraînement baisse beaucoup plus vite, jusqu’à 0,303 au pas 10 et 0,0008 au pas 100. Elle est presque nulle. Mais le résultat de validation n’est pas meilleur : la perte est de 0,138 et l’exactitude de 96,7 %. Avec 0,001, elles étaient de 0,199 et de 97,8 %. Une perte d’entraînement proche de zéro signifie que le réseau a commencé à mémoriser les billets d’entraînement. La perte d’entraînement seule ne permet pas de savoir comment le réseau se comporte sur de nouveaux billets. La prochaine leçon mesure les deux, après chaque passage.

Erreur courante

Oublier zero_grad()

Symptôme : Tomás supprime optimizer.zero_grad() « parce que le code fonctionne quand même ». C’est vrai : aucune erreur, et la perte s’affiche. Mais le réseau apprend de moins en moins bien. Priya a exécuté sa version avec les petits lots habituels de 64 billets du cours, expliqués dans la prochaine leçon. Après chacun des trois passages sur les données, l’exactitude de validation est de 91,5 %, 89,5 % et 90,0 %. La perte de validation augmente : 0,349, 0,417, 0,489. Avec zero_grad(), le même entraînement donne 97,2 %, 98,7 % et 98,5 %.

Cause : backward() ajoute chaque nouveau gradient aux anciens. Sans zero_grad(), la somme de tous les gradients précédents détermine chaque pas. Ces gradients ne correspondent plus aux poids actuels. Priya a mesuré la taille du gradient de la première couche après chaque passage. Elle passe de 10,7 à 15,7, puis à 25,6. Avec zero_grad(), elle reste petite : 0,66, 0,39, 0,39.

  • Avec zero_grad()
  • Sans zero_grad()
La taille (norme) du gradient de la première couche à la fin de chaque passage, avec et sans zero_grad(). Lots de 64, Adam, taux d'apprentissage de 0,001. Exécutions réelles.
Description textuelle du graphique

Avec zero_grad(), la taille du gradient est de 0,66 après le premier passage et de 0,39 après les deuxième et troisième. Sans zero_grad(), elle est de 10,7, 15,7 et 25,6. Elle augmente à chaque passage, car les anciens gradients s’accumulent.

Correction : appelez optimizer.zero_grad() au début de chaque pas d’entraînement, avant la propagation avant. Pour vérifier une boucle, affichez la taille du gradient d’une couche, model.layers[0].weight.grad.norm(), pendant quelques pas. Elle ne devrait pas augmenter constamment.

Ce que cela signifie pour un vrai service d’IA : une erreur d’entraînement provoque rarement un arrêt. Elle produit un modèle un peu moins bon. Personne ne le remarque à moins de vérifier le journal d’entraînement et les résultats de validation de chaque exécution.

Vérifiez votre compréhension

Énumérez dans l'ordre les cinq étapes d'un pas d'entraînement. Expliquez en quelques mots ce que fait chacune.

Afficher la réponse

1, optimizer.zero_grad() : remettre les gradients à zéro, car backward() s’y ajoute. 2, la propagation avant, model(x) : calculer les scores. 3, la perte, loss_fn(scores, y) : mesurer à quel point les scores sont incorrects. 4, loss.backward() : la rétropropagation, un gradient pour chaque paramètre. 5, optimizer.step() : déplacer chaque paramètre dans le sens opposé à son gradient.

Vérifiez votre compréhension

Faites une prédiction : un journal d'entraînement montre la même perte, 1,6236, à chaque pas. Le code s'exécute sans erreur. Quelles étapes pourraient manquer, et comment le vérifier?

Afficher la réponse

Une perte qui ne bouge jamais signifie que les poids ne changent jamais. Il manque donc loss.backward() ou optimizer.step(). L’optimiseur pourrait aussi avoir été créé avec les mauvais paramètres, par exemple ceux d’un autre modèle. Vérifiez que les deux lignes sont dans la boucle. Ensuite, affichez un paramètre et son .grad avant et après un pas. Le gradient ne devrait pas être None (aucune valeur), et le paramètre devrait changer.

Vérifiez votre compréhension

Tomás déplace optimizer.zero_grad() juste après loss.backward(), avant optimizer.step(). Le code s'exécute. Que se passe-t-il, et pourquoi?

Afficher la réponse

Le réseau cesse d’apprendre. backward() calcule les gradients, zero_grad() les efface aussitôt, puis step() ne voit aucun gradient (zéro ou None). Les poids ne changent donc pas. La perte reste à sa valeur initiale. zero_grad() doit précéder la propagation avant et la rétropropagation d’un pas, et non se placer entre backward() et step().

Récapitulatif

  • Un pas d’entraînement comporte cinq étapes, dans cet ordre : zero_grad(), propagation avant, perte, backward(), step(). La boucle d’entraînement les répète.
  • L’optimiseur conserve les paramètres et les modifie. torch.optim.SGD applique la règle de mise à jour de la descente de gradient; torch.optim.Adam adapte le pas de chaque paramètre.
  • Avec Adam, 100 pas en lot complet ont fait passer la perte sur les billets d’entraînement de 1,624 à 0,146. L’exactitude de validation est passée de 18,2 % à 97,8 %.
  • Une étape manquante produit rarement une erreur. Une perte qui ne bouge jamais indique un appel manquant à backward() ou à step(). Sans zero_grad(), les gradients s’accumulent (10,7 → 15,7 → 25,6).
  • L’entraînement en lot complet lit chaque billet à chaque pas, ce qui coûte de la mémoire et du temps.

Ensuite : entraînez le réseau en petits lots sur plusieurs passages. Évaluez-le sur les billets de validation après chaque passage.

Leçon suivante : Lots, époques et validation