Aller au contenu
English

La rétropropagation et autograd

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

Ce que vous saurez faire

Obtenez la pente de la perte pour chaque poids du réseau en une seule propagation arrière. Vous évitez ainsi de modifier légèrement 183 685 poids un à un. C’est ce qui rend l’entraînement d’un réseau possible, et PyTorch le fait pour vous avec un seul appel.

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 propagation avant et arrière d’un neurone, puis vérifie le résultat avec de petites modifications. Il vérifie aussi à la main un gradient du réseau de billets de soutien. Il affiche les formes de tous les gradients et mesure la durée de la propagation arrière. 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 55 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

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

Une usine de biscuits a une chaîne de trois postes : mélange, cuisson et emballage. Un client se plaint : les biscuits sont trop foncés. Qui en est responsable, et dans quelle mesure?

La plainte arrive au bout de la chaîne. Vous partez donc de là et remontez la chaîne. Les responsables de l’emballage disent : « Les biscuits plus foncés qui sortent du four causent plus de plaintes : 2 plaintes supplémentaires par nuance plus foncée. » Les responsables de la cuisson disent : « Chaque minute supplémentaire au four rend les biscuits plus foncés de 3 nuances. » Chaque minute de cuisson supplémentaire donne donc 2 × 3 = 6 plaintes de plus. Chaque poste connaît seulement son effet sur le poste suivant. Multipliez les effets en remontant la chaîne, et chaque poste découvre sa part de responsabilité.

Le problème : mesurer une pente par poids prend trop de temps

Section intitulée « Le problème : mesurer une pente par poids prend trop de temps »

La descente de gradient a besoin de la pente de la perte pour chaque paramètre, à chaque étape. Dans la dernière leçon, vous avez mesuré une pente avec une petite modification. Vous modifiez légèrement un paramètre, exécutez le réseau à nouveau et observez le changement de la perte. Cela coûte une propagation avant supplémentaire par paramètre.

Le réseau de billets a 183 685 paramètres. Dans le carnet, une propagation avant sur les 4 696 billets d’entraînement a pris 0,016 seconde. Modifier légèrement chaque paramètre une fois coûte environ 183 685 × 0,016 s ≈ 48 minutes, pour une seule étape. L’entraînement du réseau dans ce module prend 1 480 étapes. Ces petites modifications prendraient donc environ 49 jours. L’entraînement doit obtenir toutes les pentes pour un coût d’environ une propagation.

L’idée : transmettre la responsabilité à rebours

Section intitulée « L’idée : transmettre la responsabilité à rebours »

La rétropropagation est la chaîne de production d’un réseau. La propagation avant calcule la perte à partir des entrées, une étape à la fois. PyTorch conserve une trace de chaque étape : le graphe de calcul. Ensuite, la propagation arrière parcourt le graphe de la perte vers les entrées. À chaque étape, elle multiplie la responsabilité reçue par la pente locale de l’étape, puis transmet le résultat.

La règle utilisée est la règle de dérivation en chaîne. Quand un changement en cause un autre, leurs pentes se multiplient, comme le 2 × 3 = 6 des biscuits. Le nom « rétropropagation » décrit ce qui se passe : l’erreur se propage à rebours dans le réseau. Autograd, abréviation de « gradients automatiques », est l’outil de PyTorch qui fait ce travail.

Chaque type d’étape a une pente locale connue, et PyTorch les connaît toutes. Cette leçon en utilise seulement quatre :

  • −ln p a une pente locale de −1/p. À p = 0,5 : −2.
  • sigmoïde a une pente locale de p × (1 − p). À p = 0,5 : 0,25.
  • w × x change de x pour chaque unité de w (et de w pour chaque unité de x).
  • ajouter b change de 1 pour chaque unité de b.

Prenez le réseau de quatre billets de La descente de gradient et un billet, C. Il contient « refund » (remboursement) deux fois (x = 2), et c’est un billet de paiement. Prenez w = 0,5 et b = −1. Le schéma montre la propagation avant en haut et la propagation arrière en bas, avec toutes les valeurs réelles.

Un graphe de calcul. Propagation avant, de gauche à droite : x = 2 et w = 0,5 entrent dans un nœud de multiplication, qui donne 1,0; avec b = −1, un nœud d'addition donne z = 0,0; la sigmoïde donne p = 0,5; moins ln donne la perte 0,693. Propagation arrière, de droite à gauche, en pointillés : 1 à la perte; −2,0 après moins ln; −0,5 après la sigmoïde; −0,5 après le nœud d'addition; grad b = −0,5; grad w = −1,0.
Un neurone sur le billet C. Propagation avant (traits pleins, valeurs) : z = 0,5 × 2 − 1 = 0, p = 0,5, perte = 0,693. Propagation arrière (pointillés, gradients) : chaque étape multiplie par sa pente locale, jusqu'à grad w = −1,0 et grad b = −0,5.
Description textuelle du diagramme

Propagation avant, de gauche à droite. L’entrée x = 2 et le poids w = 0,5 entrent dans un nœud de multiplication : w × x = 1,0. Le nœud d’addition ajoute le biais b = −1 : z = 0,0. Le nœud sigmoïde donne p = 0,5. Le nœud moins-ln donne la perte, 0,693.

Propagation arrière, de droite à gauche. Elle commence avec 1 à la perte. Le nœud moins-ln multiplie par sa pente locale −1/p = −2, donc −2,0 revient à p. Le nœud sigmoïde multiplie par p(1 − p) = 0,25, donc −0,5 revient à z. Le nœud d’addition multiplie par 1. Ainsi, −0,5 va au nœud de multiplication, et grad b = −0,5. Le nœud de multiplication multiplie par x = 2, donc grad w = −1,0.

Lisez la rangée de propagation arrière en partant de la droite :

  1. Commencez à la perte avec 1 : la perte change de 1 par unité d’elle-même.
  2. L’étape −ln : 1 × (−2) = −2,0. Si p augmente un peu, la perte diminue du double.
  3. L’étape sigmoïde : −2 × 0,25 = −0,5. Si z augmente un peu, la perte diminue de moitié autant.
  4. L’étape d’addition transmet −0,5 à ses deux entrées, donc grad b = −0,5.
  5. L’étape de multiplication multiplie par x = 2 : grad w = −1,0.

Les deux gradients sont négatifs, donc la descente de gradient augmentera w et b. Cela augmente p, la probabilité de paiement, pour un billet de paiement : c’est la bonne direction.

Dans PyTorch, vous marquez chaque paramètre avec requires_grad=True. Cela signifie : « Conservez une trace de chaque étape qui utilise ce nombre pour pouvoir calculer son gradient plus tard. » Ensuite, loss.backward() parcourt cette trace à rebours, et le gradient de chaque paramètre apparaît dans son .grad.

one_neuron.py
import torch
x, y = 2.0, 1.0 # ticket C: "refund" twice, a payment ticket
w = torch.tensor(0.5, requires_grad=True) # the weight: keep a record for its gradient
b = torch.tensor(-1.0, requires_grad=True) # the bias: the same
z = w * x + b
p = torch.sigmoid(z)
loss = -torch.log(p)
print("z =", z)
print("p =", p)
print("loss =", loss)
loss.backward()
print("grad w =", w.grad)
print("grad b =", b.grad)

La sortie :

z = tensor(0., grad_fn=<AddBackward0>)
p = tensor(0.5000, grad_fn=<SigmoidBackward0>)
loss = tensor(0.6931, grad_fn=<NegBackward0>)
grad w = tensor(-1.)
grad b = tensor(-0.5000)

Les mentions grad_fn de la leçon sur la perte prennent maintenant leur sens. Chacune nomme l’étape qui a produit le nombre : une addition, une sigmoïde, une négation. Ensemble, elles forment le graphe de calcul. Les gradients correspondent exactement aux nombres du schéma. Le carnet modifie aussi w de 0,001 de chaque côté, comme dans la dernière leçon, et obtient −0,99996. C’est la même pente, à l’arrondi près.

Le réseau de billets : un gradient vérifié à la main

Section intitulée « Le réseau de billets : un gradient vérifié à la main »

Le réseau de billets est une chaîne plus longue qui suit la même idée. La propagation arrière commence à la perte et traverse la couche de sortie, ReLU et la première couche. En chemin, elle laisse un gradient pour chaque paramètre.

Le réseau de billets sur une rangée : fréquences des mots (1, 2864), première couche avec poids (64, 2864) et biais (64), ReLU, couche de sortie avec poids (5, 64) et biais (5), 5 scores (1, 5), perte. Les flèches pleines vont vers l'avant; les flèches en pointillés vont vers l'arrière. Sous la première couche : gradient des poids (64, 2864), gradient des biais (64). Sous la couche de sortie : gradient des poids (5, 64), gradient des biais (5).
Une propagation arrière dans le réseau de billets laisse un gradient pour chaque paramètre, de la même forme que ce paramètre.
Description textuelle du diagramme

Propagation avant, de gauche à droite : les fréquences des mots d’un billet, de forme (1, 2864), entrent dans la première couche. Ses poids ont la forme (64, 2864) et ses biais, (64). Viennent ensuite ReLU et la couche de sortie, avec des poids de forme (5, 64) et des biais de forme (5). La couche de sortie donne 5 scores, de forme (1, 5), puis la perte, un seul nombre. Propagation arrière, de droite à gauche, depuis la perte : la couche de sortie reçoit un gradient pour ses poids, de forme (5, 64), et ses biais, de forme (5). La première couche reçoit un gradient pour ses poids, de forme (64, 2864), et ses biais, de forme (64).

Vous pouvez vérifier un de ces gradients à la main. Pour softmax avec l’entropie croisée, le gradient de chaque score a une forme simple. C’est la probabilité moins 1 pour la bonne équipe, et la probabilité elle-même pour chaque autre équipe. Le biais de sortie d’une équipe s’ajoute au score de cette équipe, donc son gradient est le même nombre. C’est aussi le résultat du neurone ci-dessus : p − 1 = 0,5 − 1 = −0,5 à z.

Le réseau non entraîné donne au premier billet de validation les probabilités 0,172, 0,200, 0,218, 0,201 et 0,209. La bonne équipe est celle des paiements. Les cinq gradients du biais de sortie devraient donc être 0,172, 0,200, 0,218 − 1 = −0,782, 0,201 et 0,209. Priya demande à PyTorch :

check_gradient.py
set_seed(0)
model = BagOfWordsNet(len(vocab))
scores = model(x_valid[:1])
loss = loss_fn(scores, y_valid[:1])
loss.backward()
print("probabilities: ", torch.softmax(scores, dim=1).detach())
print("grad of output bias:", model.layers[3].bias.grad)
for name, weight in model.named_parameters():
print(f"{name:16} weight {str(tuple(weight.shape)):12} grad {tuple(weight.grad.shape)}")

La sortie :

probabilities: tensor([[0.1722, 0.2003, 0.2180, 0.2006, 0.2089]])
grad of output bias: tensor([ 0.1722, 0.2003, -0.7820, 0.2006, 0.2089])
layers.0.weight weight (64, 2864) grad (64, 2864)
layers.0.bias weight (64,) grad (64,)
layers.3.weight weight (5, 64) grad (5, 64)
layers.3.bias weight (5,) grad (5,)

.detach() masque seulement la mention grad_fn dans l’affichage. model.layers[3] est la couche de sortie : la couche 0 est la première nn.Linear. Viennent ensuite ReLU (1), le dropout (désactivation aléatoire de neurones; 2, désactivé dans ce réseau) et la couche de sortie (3). named_parameters() énumère chaque paramètre avec son nom.

La vérification à la main concorde avec PyTorch à quatre décimales. Le biais des paiements a un gradient négatif, donc la descente de gradient l’augmentera, et tous les autres biais diminueront. Le réseau se rapproche des paiements pour ce billet. Et chaque paramètre a un gradient de sa propre forme : 183 685 pentes en une seule propagation arrière.

Combien de temps cela a-t-il pris? Dans le carnet, une propagation avant sur tous les billets d’entraînement a pris 0,016 seconde. Une propagation avant et arrière a pris 0,017 seconde. La propagation arrière coûte à peu près autant qu’une propagation avant supplémentaire, peu importe le nombre de paramètres. Modifier légèrement chaque paramètre prendrait environ 48 minutes.

Utilisez-le toujours pour l’entraînement. Écrire les gradients à la main est lent et propice aux erreurs. Les petites modifications sont beaucoup trop lentes pour un vrai réseau. Chaque couche de torch.nn conserve sa trace et connaît ses pentes locales.

Ce qu’il coûte :

  • Mémoire. La trace conserve chaque valeur de la propagation avant jusqu’à ce que la propagation arrière l’ait utilisée. Pour un grand modèle, cela représente beaucoup de mémoire. C’est une raison pour laquelle l’entraînement exige beaucoup plus de mémoire que l’utilisation d’un modèle. Les coûts de l’inférence en fait le compte pour un vrai modèle.
  • Les gradients s’additionnent. backward() ajoute chaque nouveau gradient au nombre déjà dans .grad. Exécutez deux fois la propagation avant et arrière du neurone, et w.grad vaut −2,0, pas −1,0. C’est utile dans certains cas, mais dans une boucle d’entraînement, vous devez remettre les gradients à zéro avant chaque étape. La prochaine leçon montre ce qui se passe si vous oubliez.
  • Aucune trace si vous n’en avez pas besoin. Pour utiliser un réseau sans l’entraîner, vous désactivez l’enregistrement. Lots, époques et validation montre comment et pourquoi.

Le billet D contient aussi « refund » deux fois (x = 2), mais il va aux retours. Pour un billet qui ne concerne pas les paiements, la perte est −ln(1 − p). Sa pente locale est 1 ÷ (1 − p).

  1. Utilisez w = 0,5 et b = −1. Calculez la propagation avant : z, p et la perte. Vous devriez obtenir z = 0, p = 0,5 et une perte de 0,693.
  2. Calculez la pente locale de la perte par rapport à p : 1 ÷ (1 − 0,5). Vous devriez obtenir 2.
  3. Multipliez par la pente locale de la sigmoïde, p(1 − p) = 0,25. Vous devriez obtenir 0,5 : la responsabilité à z.
  4. Calculez grad b (× 1) et grad w (× x = 2). Vous devriez obtenir grad b = 0,5 et grad w = 1,0.
  5. Vérifiez dans PyTorch : créez w et b avec requires_grad=True, calculez loss = -torch.log(1 - torch.sigmoid(w * 2.0 + b)), appelez loss.backward() et affichez w.grad et b.grad.
Afficher la réponse

PyTorch affiche tensor(1.) pour w et tensor(0.5000) pour b, comme dans le calcul à la main. Les gradients sont positifs cette fois, donc la descente de gradient diminue w et b. Cela diminue p, la probabilité de paiement, pour un billet de retour : c’est la bonne direction. Les billets C et D tirent w dans des directions opposées avec la même force. C’est pourquoi le meilleur w pour les quatre billets est un compromis.

Le billet B contient « refund » une fois (x = 1) et concerne les paiements. Avec w = 0,5 et b = −1, prédisez d’abord le signe de grad w. Calculez-le ensuite avec autograd, comme dans la section 9 du carnet, puis à la main avec la formule « p − 1 à z ».

Afficher la réponse

z = 0,5 × 1 − 1 = −0,5, donc p = sigmoïde(−0,5) = 0,378, et la perte est −ln 0,378 = 0,974. La responsabilité à z est p − 1 = −0,622. Ensuite, grad b = −0,622 × 1 et grad w = −0,622 × x = −0,622 × 1. PyTorch donne −0,6225 pour les deux. Le signe est négatif : augmentez w et b pour rendre les paiements plus probables pour ce billet de paiement.

Erreur courante

Sans trace, pas de gradient

Symptôme : Tomás crée le poids et le biais comme des tenseurs ordinaires, w = torch.tensor(0.5), et la propagation avant s’exécute. À loss.backward(), PyTorch s’arrête avec : RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn (l’élément 0 des tenseurs ne demande pas de gradient et n’a pas de fonction de gradient).

Cause : sans requires_grad=True, PyTorch ne conserve aucune trace des étapes. Il n’y a donc aucun graphe de calcul à parcourir à rebours. Si seulement certains paramètres l’ont, aucune erreur ne survient. Les autres gardent simplement .grad à None (aucune valeur), et ils n’apprennent jamais.

Solution : créez chaque paramètre avec requires_grad=True, ou utilisez les couches de torch.nn (comme nn.Linear), qui le font pour vous. Pour vérifier, affichez [name for name, p in model.named_parameters() if p.grad is None] après une propagation arrière : la liste devrait être vide.

Ce que cela signifie pour un vrai service d’IA : un réseau peut s’entraîner sans erreur tout en gardant certaines parties figées. Quand un modèle apprend moins que prévu, vérifiez que chaque paramètre que vous vouliez entraîner a un gradient.

Vérifiez votre compréhension

Dans vos mots : que fait la propagation arrière, et pourquoi est-elle beaucoup plus rapide que de modifier légèrement chaque poids?

Afficher la réponse

La propagation arrière parcourt la trace de la propagation avant, de la perte vers les entrées. À chaque étape, elle multiplie la responsabilité reçue par la pente locale de l’étape (la règle de dérivation en chaîne). Elle transmet ensuite le résultat. Chaque paramètre reçoit donc son gradient en chemin. Elle est rapide parce qu’elle réutilise le travail déjà fait. Une propagation arrière coûte environ une propagation avant supplémentaire, peu importe le nombre de paramètres. Les petites modifications exigent une propagation avant supplémentaire par paramètre : 183 685 pour le réseau de billets.

Vérifiez votre compréhension

Prédisez : dans le neurone de cette leçon, l'entrée x passe de 2 à 3, avec les mêmes w et b. Est-ce que grad w change? Expliquez avec la règle de dérivation en chaîne, sans calculer tous les nombres.

Afficher la réponse

Oui. La dernière étape de la propagation arrière multiplie la responsabilité à z par x. Un x plus grand donne donc un grad w plus grand en valeur absolue. Mais z change aussi : z = 0,5 × 3 − 1 = 0,5. Donc p = sigmoïde(0,5) = 0,622 et la responsabilité à z est p − 1 = −0,378. Ainsi, grad w = −0,378 × 3 = −1,13, un peu plus grand en valeur absolue que −1,0. Un paramètre qui reçoit une entrée plus grande reçoit une plus grande part de responsabilité.

Vérifiez votre compréhension

Après loss.backward(), le gradient du biais de sortie des paiements est −0,782 et les autres sont positifs. Que fera la descente de gradient aux cinq biais de sortie, et qu'est-ce que cela change pour ce billet?

Afficher la réponse

La descente de gradient déplace chaque biais dans le sens opposé à son gradient. Le biais des paiements a un gradient négatif, donc il augmente. Les quatre autres ont des gradients positifs, donc ils diminuent. Le score des paiements augmente et les autres scores diminuent. Softmax donne donc une plus grande probabilité aux paiements, la bonne équipe, et la perte de ce billet diminue.

Récapitulatif

  • La propagation avant conserve une trace de chaque étape : le graphe de calcul. La propagation arrière le parcourt depuis la perte, en multipliant les pentes locales (la règle de dérivation en chaîne). Elle laisse un gradient pour chaque paramètre : c’est la rétropropagation.
  • Dans PyTorch, requires_grad=True démarre l’enregistrement, loss.backward() exécute la propagation arrière, et .grad contient chaque gradient. C’est autograd.
  • Un neurone sur le billet C : perte 0,693, grad w = −1,0, grad b = −0,5, à la main et avec PyTorch. Dans le réseau de billets, les gradients des biais de sortie sont les probabilités moins 1 pour la bonne équipe : −0,782 pour les paiements.
  • Une propagation arrière coûte environ une propagation avant. Modifier légèrement les 183 685 paramètres prendrait environ 48 minutes par étape.
  • Les gradients s’additionnent dans .grad. Une boucle d’entraînement doit donc les remettre à zéro avant chaque étape.

Ensuite : réunissez la propagation avant, la perte, la propagation arrière et la mise à jour dans une boucle d’entraînement. Entraînez ensuite le routeur de billets.

Leçon suivante : La boucle d'entraînement