Backpropagation (Rétropropagation)
La backpropagation (rétropropagation du gradient) est l’algorithme qui calcule efficacement le gradient de la fonction de perte par rapport à chaque poids d’un réseau de neurones, en propageant l’erreur de la sortie vers l’entrée grâce à la règle de la chaîne.
Sans backpropagation, le deep learning n’existerait pas. C’est l’algorithme qui rend l’entraînement des réseaux profonds praticable : il calcule en un seul passage arrière les gradients qui serviraient sinon des millions de calculs indépendants. Formalisé par David E. Rumelhart, Geoffrey Hinton et Ronald J. Williams dans leur papier fondateur de 1986 dans Nature, backpropagation est resté depuis l’algorithme d’entraînement standard pour toutes les architectures neuronales, des perceptrons multicouches aux Transformers modernes.
- Nom français
- Rétropropagation du gradient (ou rétropropagation de l’erreur)
- Type
- Algorithme de calcul de gradient (différentiation automatique en mode inverse)
- Auteurs clés
- Rumelhart, Hinton & Williams (1986, popularisation). Précurseurs : Bryson & Ho (1969), Werbos (1974), LeCun (1985)
- Papier
- « Learning representations by back-propagating errors » (Nature, 1986)
- Principe
- Application de la règle de la chaîne couche par couche, de la sortie vers l’entrée
- Implémentation
- Automatique dans PyTorch (
loss.backward()), TensorFlow (tape.gradient()), JAX - Verdict
- Fondamental et irremplaçable. Tout entraînement de réseau de neurones l’utilise.
Le rôle de la backpropagation dans l’entraînement
L’entraînement d’un réseau de neurones repose sur un cycle en quatre étapes qui se répète des milliers, voire des millions de fois. La backpropagation est l’étape 3 de ce cycle :
1. Forward pass (propagation avant). Les données d’entrée traversent le réseau, couche par couche, jusqu’à produire une prédiction en sortie. Chaque couche applique une transformation linéaire (multiplication par les poids + biais) suivie d’une fonction d’activation non linéaire.
2. Calcul de la perte. La fonction de perte (loss function) mesure l’écart entre la prédiction du réseau et la valeur cible. Par exemple, la cross-entropy pour la classification ou le MSE pour la régression.
3. Backward pass (backpropagation). L’algorithme propage l’erreur en sens inverse, de la dernière couche vers la première, en calculant le gradient de la perte par rapport à chaque poids du réseau. C’est ici que la règle de la chaîne entre en jeu.
4. Mise à jour des poids. Un optimiseur (SGD, Adam, etc.) utilise les gradients calculés par la backpropagation pour ajuster les poids dans la direction qui réduit la perte.
L’intuition : attribuer la responsabilité de l’erreur
Imaginez une chaîne de montage avec 100 ouvriers. Le produit final est défectueux. Pour améliorer la qualité, vous devez identifier la contribution de chaque ouvrier au défaut. Vous ne pouvez pas tester chaque ouvrier isolément (trop long). À la place, vous remontez la chaîne depuis le produit final, en mesurant à chaque étape combien le défaut provient de cette étape et combien il était déjà présent avant.
C’est exactement ce que fait la backpropagation. Elle remonte le réseau depuis la sortie vers l’entrée, calculant à chaque couche quelle fraction de l’erreur totale est « due » aux poids de cette couche. Le résultat : un gradient pour chaque poids, qui indique dans quelle direction et avec quelle intensité ajuster ce poids pour réduire l’erreur.
La règle de la chaîne : le moteur mathématique
La backpropagation repose entièrement sur la règle de la chaîne (chain rule) du calcul différentiel. Si une fonction est composée de plusieurs fonctions imbriquées, la dérivée de la composition est le produit des dérivées de chaque fonction.
Un réseau de neurones est précisément une telle composition. Pour un réseau simple à 3 couches :
sortie = f₃(W₃ · f₂(W₂ · f₁(W₁ · x + b₁) + b₂) + b₃)
La perte L dépend de la sortie, qui dépend de la couche 3, qui dépend de la couche 2, qui dépend de la couche 1. Pour trouver comment un poids W₁ affecte la perte, la règle de la chaîne nous dit :
∂L/∂W₁ = (∂L/∂sortie) · (∂sortie/∂couche₃) · (∂couche₃/∂couche₂) · (∂couche₂/∂W₁)
Chaque terme est une dérivée partielle locale, facile à calculer. Le gradient complet est leur produit. C’est cette décomposition en produit de termes locaux qui rend la backpropagation si efficace : chaque couche ne doit connaître que sa propre dérivée et le gradient « reçu » de la couche suivante.
Les étapes détaillées de la backpropagation
Prenons un réseau fully connected (dense) à L couches. À chaque couche l, on calcule :
z⁽ˡ⁾ = W⁽ˡ⁾ · a⁽ˡ⁻¹⁾ + b⁽ˡ⁾ # entrée pondérée
a⁽ˡ⁾ = f(z⁽ˡ⁾) # activation (ReLU, sigmoid, etc.)
Où a⁽⁰⁾ = x (l’entrée), W⁽ˡ⁾ sont les poids de la couche l, b⁽ˡ⁾ les biais, et f la fonction d’activation.
Forward pass
On traverse le réseau de l’entrée à la sortie, en sauvegardant les valeurs intermédiaires (z⁽ˡ⁾ et a⁽ˡ⁾) à chaque couche. Ces valeurs seront nécessaires pour le backward pass.
Backward pass
On commence par la dernière couche et on remonte :
Étape 1 : Gradient de la perte par rapport à la sortie.
δ⁽ᴸ⁾ = ∂L/∂a⁽ᴸ⁾ ⊙ f'(z⁽ᴸ⁾)
Ce terme δ (delta), appelé « erreur locale », combine le gradient de la perte et la dérivée de la fonction d’activation. Le symbole ⊙ représente le produit élément par élément (Hadamard product).
Étape 2 : Propagation arrière de l’erreur.
δ⁽ˡ⁾ = (W⁽ˡ⁺¹⁾)ᵀ · δ⁽ˡ⁺¹⁾ ⊙ f'(z⁽ˡ⁾) # pour chaque couche l = L-1, ..., 1
L’erreur de la couche l est calculée en multipliant l’erreur de la couche l+1 par la transposée des poids de connexion, puis par la dérivée locale de l’activation. C’est cette formule récursive qui donne son nom à l’algorithme : l’erreur se « propage en arrière » (back-propagates) à travers le réseau.
Étape 3 : Calcul des gradients pour chaque poids et biais.
∂L/∂W⁽ˡ⁾ = δ⁽ˡ⁾ · (a⁽ˡ⁻¹⁾)ᵀ # gradient des poids
∂L/∂b⁽ˡ⁾ = δ⁽ˡ⁾ # gradient des biais
Une fois tous les δ calculés, obtenir les gradients des poids et biais de chaque couche est un simple produit matriciel.
Pourquoi la backpropagation est-elle si efficace ?
L’alternative naïve pour calculer les gradients serait la méthode des différences finies : pour chaque poids w, calculer deux forward passes (avec w+ε et w-ε) et estimer le gradient par la différence. Pour un réseau avec N poids, cela nécessiterait 2N forward passes.
Pour un modèle de 7 milliards de paramètres, c’est 14 milliards de forward passes pour un seul gradient, soit des milliards d’heures de calcul.
La backpropagation calcule tous les gradients en un seul backward pass dont le coût est comparable à un seul forward pass (environ 2 à 3 fois le coût du forward). Pour N paramètres, c’est un gain de facteur N. C’est ce qui rend l’entraînement des grands modèles possible.
Cette efficacité vient de la programmation dynamique : la backpropagation réutilise les résultats intermédiaires (les δ) au lieu de recalculer les mêmes dérivées partielles des millions de fois. Chaque dérivée locale n’est calculée qu’une seule fois, puis transmise à la couche suivante.
La backpropagation dans les frameworks modernes
Vous n’avez jamais besoin d’implémenter la backpropagation à la main. Les frameworks modernes l’encapsulent dans leurs systèmes de différentiation automatique (autograd).
PyTorch : autograd
PyTorch construit un graphe de calcul dynamique pendant le forward pass. Chaque opération est enregistrée, et loss.backward() traverse ce graphe en sens inverse pour calculer tous les gradients.
import torch
import torch.nn as nn
# Réseau simple
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10)
)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# Boucle d'entraînement
for x_batch, y_batch in dataloader:
# 1. Forward pass
predictions = model(x_batch)
# 2. Calcul de la perte
loss = criterion(predictions, y_batch)
# 3. Backward pass (backpropagation)
optimizer.zero_grad() # réinitialiser les gradients
loss.backward() # calcule ∂L/∂w pour TOUS les poids
# 4. Mise à jour des poids
optimizer.step() # l'optimiseur utilise les gradients
Après loss.backward(), chaque paramètre du modèle possède un attribut .grad contenant son gradient. C’est l’optimiseur qui décide ensuite quoi en faire.
TensorFlow / Keras : GradientTape
import tensorflow as tf
model = tf.keras.Sequential([
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(10)
])
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
# Boucle d'entraînement
for x_batch, y_batch in dataset:
with tf.GradientTape() as tape:
# 1. Forward pass
predictions = model(x_batch, training=True)
# 2. Calcul de la perte
loss = loss_fn(y_batch, predictions)
# 3. Backward pass (backpropagation)
gradients = tape.gradient(loss, model.trainable_variables)
# 4. Mise à jour des poids
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
Le GradientTape enregistre les opérations du forward pass, puis tape.gradient() calcule les gradients par backpropagation.
Les problèmes classiques liés à la backpropagation
Gradients évanescents (vanishing gradients)
Le problème historique numéro un. Lors du backward pass, le gradient est multiplié par la dérivée de la fonction d’activation à chaque couche. Si ces dérivées sont systématiquement inférieures à 1 (comme avec la sigmoïde, dont la dérivée maximale est 0.25), le gradient diminue exponentiellement en traversant les couches. Dans un réseau de 50 couches avec des sigmoïdes, le gradient peut être réduit d’un facteur 0.25⁵⁰ ≈ 10⁻³⁰ avant d’atteindre les premières couches. Celles-ci ne reçoivent plus aucun signal d’apprentissage.
Solutions :
La fonction ReLU (Rectified Linear Unit) a une dérivée de 1 pour les entrées positives, ce qui empêche le gradient de diminuer dans ces neurones. C’est la raison principale de la transition de la sigmoïde vers ReLU dans les années 2010.
Les connexions résiduelles (skip connections, comme dans ResNet) créent des « autoroutes » pour le gradient, qui peut traverser le réseau sans être multiplié par les dérivées de chaque couche.
La batch normalization et la layer normalization stabilisent les distributions d’activations, maintenant les gradients dans une plage raisonnable.
Gradients explosifs (exploding gradients)
Le problème inverse : si les poids sont grands et les dérivées supérieures à 1, le gradient croît exponentiellement. Les mises à jour deviennent gigantesques, les poids divergent vers des valeurs extrêmes, et la perte explose (souvent signalée par des valeurs NaN).
Solutions :
Le gradient clipping (limiter la norme du gradient à une valeur maximale, typiquement 1.0) est la solution standard. torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) dans PyTorch.
Une initialisation des poids adaptée (Xavier/Glorot pour les sigmoïdes/tanh, He/Kaiming pour ReLU) maintient les gradients dans une plage stable dès le début de l’entraînement.
La batch normalization prévient aussi ce problème en normalisant les activations.
Neurones ReLU « morts »
Un neurone ReLU dont l’entrée pondérée tombe en dessous de zéro produit une activation de 0 et une dérivée de 0. Aucun gradient ne traverse ce neurone lors du backward pass, il ne peut plus apprendre, et il reste « mort » pour toujours. Ce phénomène est plus fréquent avec un learning rate élevé.
Solutions : Leaky ReLU (dérivée petite mais non nulle pour les entrées négatives), réduire le learning rate, ou utiliser des variantes comme GELU (utilisée dans les Transformers).
Consommation mémoire
Le forward pass doit sauvegarder toutes les activations intermédiaires pour que le backward pass puisse calculer les gradients. Pour un réseau profond avec de grands batch sizes, cette mémoire peut devenir le facteur limitant.
Solutions :
Le gradient checkpointing (ou activation checkpointing) : au lieu de stocker toutes les activations, on n’en stocke qu’une fraction et on recalcule les autres pendant le backward pass. C’est un compromis mémoire vs calcul. PyTorch l’implémente via torch.utils.checkpoint.
La précision mixte (FP16/BF16) réduit la taille des activations stockées de moitié.
Exemple pas à pas : backpropagation sur un réseau simple
Prenons un réseau minimaliste : 1 neurone d’entrée, 1 neurone caché (avec sigmoïde), 1 neurone de sortie (avec sigmoïde), et la MSE comme loss.
# Forward pass
x = 0.5 # entrée
w1 = 0.3 # poids couche 1
b1 = 0.1 # biais couche 1
w2 = 0.7 # poids couche 2
b2 = 0.2 # biais couche 2
y_target = 1.0 # cible
z1 = w1 * x + b1 # = 0.25
a1 = 1 / (1 + exp(-z1)) # sigmoïde = 0.5622
z2 = w2 * a1 + b2 # = 0.5935
y_pred = 1 / (1 + exp(-z2)) # sigmoïde = 0.6442
loss = (y_pred - y_target) ** 2 # MSE = 0.1266
# Backward pass
dL_dy = 2 * (y_pred - y_target) # = -0.7116
dy_dz2 = y_pred * (1 - y_pred) # dérivée sigmoïde = 0.2292
delta2 = dL_dy * dy_dz2 # = -0.1631
dL_dw2 = delta2 * a1 # = -0.0917
dL_db2 = delta2 # = -0.1631
dz2_da1 = w2 # = 0.7
da1_dz1 = a1 * (1 - a1) # = 0.2462
delta1 = delta2 * dz2_da1 * da1_dz1 # = -0.0281
dL_dw1 = delta1 * x # = -0.0141
dL_db1 = delta1 # = -0.0281
On obtient les 4 gradients (dL/dw1, dL/db1, dL/dw2, dL/db2) en un seul backward pass. L’optimiseur peut maintenant ajuster les poids : par exemple, avec SGD et un learning rate de 0.1, w2 = w2 - 0.1 * (-0.0917) = 0.7092.
Backpropagation dans les architectures modernes
Réseaux convolutionnels (CNN)
Dans un CNN, la backpropagation calcule les gradients par rapport aux filtres de convolution. Le gradient d’un filtre est la corrélation croisée entre le gradient reçu de la couche suivante et les activations d’entrée. Les couches de pooling propagent le gradient uniquement vers les positions qui ont contribué à la sortie (par exemple, la position du maximum pour le max pooling).
Réseaux récurrents (RNN/LSTM)
Pour les séquences, la backpropagation est « déroulée dans le temps » (Backpropagation Through Time, BPTT). Le réseau récurrent est traité comme un réseau feedforward très profond (une couche par pas de temps). C’est particulièrement sujet aux gradients évanescents, ce qui explique pourquoi les LSTM et GRU (avec leurs « gates ») ont été développés pour maintenir le flux du gradient sur de longues séquences.
Transformers
Dans un Transformer, la backpropagation traverse les mécanismes d’attention (self-attention, cross-attention), les couches feed-forward, et les normalisations. La connexion résiduelle autour de chaque sous-couche (attention + FFN) est cruciale : elle crée un chemin direct pour le gradient, évitant les problèmes de gradients évanescents même dans des modèles très profonds (des dizaines, voire des centaines de couches).
Alternatives et recherche future
Bien que la backpropagation soit universellement utilisée, des alternatives sont explorées :
Forward-mode differentiation : calcule les dérivées dans le sens du forward pass. Efficace quand il y a peu d’entrées et beaucoup de sorties, mais inadapté aux réseaux de neurones (beaucoup de paramètres, une seule loss scalaire).
Feedback alignment : remplace les poids transposés W⁽ˡ⁾ᵀ dans le backward pass par des matrices fixes aléatoires. Étonnamment, cela fonctionne dans certains cas, mais les performances restent inférieures à la backpropagation standard.
Forward-forward algorithm (Geoffrey Hinton, 2022) : remplace le backward pass par deux forward passes (un avec des données positives, un avec des données négatives). Encore expérimental, les performances ne rivalisent pas avec la backpropagation sur les benchmarks actuels.
Backpropagation sur hardware physique : des travaux récents (Stanford, Nature 2022) appliquent la backpropagation à des systèmes physiques (optiques, mécaniques) pour créer des accélérateurs d’entraînement plus économes en énergie.
Pour l’instant, aucune alternative ne remplace la backpropagation pour l’entraînement des grands modèles. Elle reste le seul algorithme capable de calculer efficacement les gradients dans des réseaux de milliards de paramètres.
Bonnes pratiques pour un backward pass sain
1. Surveillez les gradients. Loggez les normes des gradients par couche. Des normes qui explosent ou qui tendent vers zéro signalent un problème. PyTorch permet d’accéder aux gradients via param.grad.norm().
2. Utilisez le gradient clipping. Limitez la norme globale des gradients à 1.0 (standard pour les Transformers et LLMs). C’est une ligne de code dans PyTorch : torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0).
3. Choisissez la bonne initialisation. He/Kaiming pour les réseaux avec ReLU, Xavier/Glorot pour sigmoïde/tanh. Une mauvaise initialisation peut tuer les gradients dès la première itération.
4. Préférez les architectures avec skip connections. Les connexions résiduelles (ResNet, Transformers) facilitent le flux du gradient et permettent d’entraîner des réseaux beaucoup plus profonds.
5. Utilisez le gradient checkpointing pour les grands modèles. Si vous manquez de mémoire GPU, le checkpointing vous permet d’entraîner des modèles plus grands en échange d’un surcoût de calcul d’environ 20-30 %.
6. N’oubliez pas optimizer.zero_grad(). En PyTorch, les gradients s’accumulent par défaut. Si vous oubliez de les remettre à zéro avant chaque backward pass, les gradients de plusieurs batchs se cumulent, ce qui corrompt la mise à jour. (Exception : l’accumulation de gradient intentionnelle, où vous accumulez sur N batchs avant de faire un optimizer.step().)
Questions fréquentes sur la backpropagation
Quelle est la différence entre backpropagation et gradient descent ?
La backpropagation est l’algorithme qui calcule les gradients (les dérivées partielles de la perte par rapport à chaque poids). La descente de gradient est l’algorithme qui utilise ces gradients pour mettre à jour les poids et minimiser la perte. En d’autres termes, la backpropagation répond à la question « de combien et dans quelle direction faut-il ajuster chaque poids ? », tandis que la descente de gradient (ou ses variantes comme Adam) effectue l’ajustement. Les deux travaillent ensemble mais sont des algorithmes distincts.
Faut-il comprendre la backpropagation pour faire du deep learning ?
Vous pouvez entraîner des modèles sans connaître les détails mathématiques : loss.backward() dans PyTorch fait tout le travail. Cependant, comprendre la backpropagation est essentiel pour diagnostiquer les problèmes d’entraînement. Quand votre modèle ne converge pas, produit des NaN, ou que certaines couches ne semblent pas apprendre, savoir ce qui se passe pendant le backward pass vous permet d’identifier et de résoudre le problème (gradients évanescents, mauvaise initialisation, learning rate inadapté).
Pourquoi les gradients « disparaissent » dans les réseaux profonds ?
Pendant le backward pass, le gradient est multiplié par la dérivée de la fonction d’activation à chaque couche traversée. Avec des fonctions comme la sigmoïde (dérivée maximale de 0.25) ou tanh (dérivée maximale de 1), ces multiplications successives réduisent exponentiellement le gradient. Dans un réseau de 50 couches, le gradient peut devenir infinitésimal avant d’atteindre les premières couches. Les solutions modernes : utiliser ReLU (dérivée de 1 pour les entrées positives), ajouter des connexions résiduelles (ResNet), et appliquer la normalisation (batch norm, layer norm).
Combien de temps prend le backward pass par rapport au forward pass ?
En règle générale, le backward pass prend environ 2 à 3 fois le temps du forward pass. C’est parce qu’il doit (1) calculer les gradients par rapport aux activations (similaire à un forward pass inversé) et (2) calculer les gradients par rapport aux poids (un produit matriciel supplémentaire par couche). La mémoire nécessaire est aussi plus importante car toutes les activations intermédiaires du forward pass doivent être stockées. C’est pourquoi l’inférence (forward pass seul) est beaucoup plus rapide et moins gourmande que l’entraînement.
La backpropagation est-elle biologiquement plausible ?
C’est un débat actif en neurosciences computationnelles. La backpropagation standard nécessite la transmission de signaux d’erreur en sens inverse à travers les mêmes connexions synaptiques, ce qui n’a pas d’équivalent biologique direct connu. Des hypothèses comme le feedback alignment, le predictive coding, ou l’algorithme forward-forward de Hinton tentent de modéliser l’apprentissage de façon plus biologiquement réaliste. Certaines études suggèrent que le cerveau pourrait utiliser des mécanismes approximativement équivalents à la backpropagation, mais la question reste ouverte.