Module 3 — Rétropropagation dans le temps
Le module 2 a introduit le graphe déplié : copies chaînées de la même cellule. Ce module traite du calcul du gradient dans ce graphe, appelé rétropropagation dans le temps (BPTT). On y comprend pourquoi entraîner un récurrent coûte à la fois du calcul et de la mémoire proportionnels à , et pourquoi on triche presque toujours sur ce coût.
Ce que BPTT calcule
Rappelons l'équation de récurrence :
À la fin de la séquence, une perte est calculée à partir de la sortie
finale (ou de toutes les sorties, selon le drapeau return_sequences). Pour
mettre à jour , il faut la dérivée de par rapport à chacun.
Puisque les mêmes matrices apparaissent à chaque pas, la dérivée totale se décompose en une somme sur tous les pas. Pour , par exemple :
Chaque terme dépend d'un produit de jacobiennes sur les pas qui séparent de . C'est cette structure en produit qui cause la disparition ou l'explosion des gradients, sujet du module 4.
BPTT complète : le coût réel
BPTT n'est rien d'autre que la rétropropagation classique appliquée au graphe
déplié. Les cadriciels le font automatiquement dès que la propagation avant
tourne dans un GradientTape (TensorFlow) ou un contexte autograd (PyTorch).
Le prix se paie sur trois axes.
Mémoire. Pour la propagation arrière, il faut avoir gardé les activations de tous les pas : . La mémoire d'entraînement croît donc linéairement en . Une séquence de pas avec unités et un lot de occupe millions d'activations, plusieurs dizaines de mégaoctets par couche, à multiplier par les couches empilées.
Temps. Chaque pas exécute une multiplication matricielle. Un pas de plus, c'est une multiplication de plus, en avant et en arrière. Une séquence deux fois plus longue coûte deux fois plus.
Débit. Le calcul est strictement séquentiel : dépend de , on ne peut pas paralléliser la boucle temporelle. C'est ce qui rend les récurrents lents à entraîner sur accélérateur, et ce qui a motivé les transformeurs du cours 12.
BPTT tronquée : la triche indispensable
Sur des séquences très longues (texte de milliers de tokens, série d'un an à la minute), la BPTT complète devient impraticable. On la tronque : on choisit une longueur (typiquement 50 à 200 pas), on ne rétropropage que sur les derniers pas, et on considère le reste comme figé.
Trois recettes coexistent.
Découpage en fenêtres indépendantes. C'est ce que fait fenetrer du
module 1 : chaque fenêtre est un exemple complet, et la rétropropagation
traverse la fenêtre entière. C'est la solution la plus simple, celle par
défaut pour la consommation électrique où une semaine (168 h) tient
facilement en mémoire.
BPTT tronquée avec état conservé. On enchaîne des fenêtres successives
d'une même séquence en gardant d'une fenêtre comme de la
suivante, mais sans laisser le gradient traverser cette liaison. En Keras,
c'est le mode stateful=True combiné à un appel manuel à reset_states.
Utile pour un texte plus long que la fenêtre mais dont on veut préserver le
contexte.
BPTT tronquée avec chevauchement. On garde un chevauchement entre fenêtres pour ne pas casser les motifs à la frontière. Il coûte un peu plus de calcul mais évite qu'un motif utile ne soit systématiquement tronqué à l'endroit qui compte.
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# stateful : on garde l'etat d'un lot au suivant
modele = keras.Sequential([
layers.Input(batch_shape=(32, 100, 1)), # taille de lot fixe pour stateful
layers.SimpleRNN(64, stateful=True, return_sequences=True),
layers.Dense(1),
])
for lot_entree, lot_cible in jeu: # fenetres successives d'une meme serie
modele.train_on_batch(lot_entree, lot_cible)
modele.reset_states() # a chaque nouvelle serie independante
Bien choisir
Le choix de la longueur de troncature est un équilibre.
Un trop petit empêche le modèle d'apprendre une dépendance longue. Sur la consommation électrique, tronquer à 24 heures interdit d'apprendre la saisonnalité hebdomadaire, qui exige au moins 168 pas.
Un trop grand rétablit tous les coûts qu'on cherchait à éviter, et peut même faire apparaître les problèmes de gradient du module 4 sur des longueurs où LSTM et GRU les évitent naturellement.
En pratique, on prend égal à un ou deux cycles complets du phénomène. Pour la consommation : 168 pas si l'on veut capter l'hebdomadaire, 24 si l'on se limite au quotidien.
Un vérificateur simple : compter les activations gardées
Il est facile de sous-estimer la mémoire prise par BPTT. Un ordre de grandeur
utile : la mémoire d'activation d'une couche récurrente est
octets pour float32. Il faut multiplier par le
nombre de couches, et souvent doubler pour tenir compte des gradients
intermédiaires.
def memoire_activations(B, T, H, n_couches=1, octets_par_val=4):
"""Retourne l'estimation en Mo."""
total = B * T * H * n_couches * octets_par_val
return total / (1024 ** 2)
print(memoire_activations(64, 168, 128, n_couches=2)) # ~11 Mo, largement viable
print(memoire_activations(64, 5000, 512, n_couches=4))) # ~2500 Mo, hors budget
Quand ce calcul dépasse la mémoire disponible, deux leviers agissent avant de tronquer : réduire (peut nuire à la stabilité) et réduire (peut nuire à la capacité). Tronquer est souvent la moins mauvaise option.
La lenteur d'un récurrent est structurelle : rien ne pourra le faire tourner en parallèle sur la dimension temps. Multiplier le nombre d'accélérateurs n'accélère pas un exemple, seulement le nombre d'exemples en parallèle. Si le temps par époque devient rédhibitoire, la vraie réponse est le changement de famille (LSTM au lieu d'un dense à 168 entrées, transformeur pour de très longues séquences), pas plus de matériel.
Un moyen empirique de savoir jusqu'où votre modèle utilise réellement le passé : entraîner deux copies, l'une sur des fenêtres de 24 heures, l'autre sur 168 heures. Si les scores sont identiques, la mémoire longue n'est pas utile pour la tâche ; si le second est meilleur, elle l'est. Cela évite de payer 168 pas de BPTT pour rien.
En résumé
- La BPTT est la rétropropagation classique appliquée au graphe déplié : elle décompose la dérivée en une somme de contributions par pas, chacune contenant un produit de jacobiennes.
- Le coût est linéaire en en temps et en mémoire, et la boucle est strictement séquentielle : on ne parallélise pas sur le temps.
- La BPTT tronquée limite ce coût en n'entraînant que sur les derniers pas ; le choix de doit couvrir au moins un cycle complet du phénomène.
- Estimer la mémoire d'activation ( multiplié par le nombre de couches) évite les explosions de RAM et guide le compromis entre taille de lot, dimension cachée et longueur de fenêtre.
Module suivant : le produit de jacobiennes qui apparaît dans BPTT est aussi ce qui rend les gradients instables ; on regarde pourquoi, et comment y répondre sans changer d'architecture.