Aller au contenu principal

Module 2 — Le neurone récurrent et son état caché

Le module 1 a expliqué pourquoi une séquence exige autre chose qu'un dense. Voici l'objet qui répond : une petite cellule qui lit un pas à la fois, garde une trace de ce qu'elle a vu, et réutilise les mêmes poids à chaque pas.

L'équation de récurrence

Un neurone récurrent maintient un vecteur hth_t appelé état caché. À chaque pas, il combine l'entrée du pas xtx_t et l'état précédent ht1h_{t-1} :

ht=tanh(Wxxt+Whht1+b)h_t = \tanh(W_x x_t + W_h h_{t-1} + b)

Le vecteur hth_t résume tout le passé de la séquence à la date tt, dans une taille fixe choisie à l'avance. C'est le point clé : quelle que soit la longueur TT de la séquence, la mémoire tient toujours dans hth_t de dimension HH.

Trois matrices de paramètres :

  • WxW_x de forme (F,H)(F, H) mélange les variables d'entrée
  • WhW_h de forme (H,H)(H, H) propage l'état d'un pas au suivant
  • bb de forme (H,)(H,) est le biais

Le nombre total de paramètres est FH+HH+HF \cdot H + H \cdot H + H. Pour la consommation électrique avec F=1F = 1 variable d'entrée et H=64H = 64 unités cachées, cela fait 164+6464+64=42241 \cdot 64 + 64 \cdot 64 + 64 = 4224 paramètres. Ce compte est indépendant de la longueur TT.

Poids partagés : la propriété fondamentale

Les mêmes matrices Wx,Wh,bW_x, W_h, b sont utilisées à tous les pas de temps. C'est ce qui rend le modèle capable de généraliser à des séquences plus longues que celles vues à l'entraînement, et c'est ce qui borne le nombre de paramètres.

Comparez avec un dense qui traiterait 1681=168168 \cdot 1 = 168 entrées et 6464 unités : 16864+64=10816168 \cdot 64 + 64 = 10\,816 paramètres, tous spécifiques à leur position. Un motif appris à la position 12 n'aide en rien à la position 24. Un récurrent, lui, apprend une transformation qui s'applique partout.

Le contraste tient dans une image simple : le dense apprend un modèle par heure, le récurrent apprend une règle de mise à jour.

Dépliage dans le temps

Pour le calcul, la cellule est dépliée : on écrit TT copies chaînées, où la sortie de l'une devient l'entrée de la suivante. C'est un graphe plus long, mais qui utilise les mêmes poids partout.

x_1 --> [cellule] --> h_1 --+
|
x_2 --> [cellule] --> h_2 --+
|
x_3 --> [cellule] --> h_3 --+
...
x_T --> [cellule] --> h_T

Le graphe déplié sert autant à la propagation avant qu'à la propagation arrière — c'est de là que vient le nom BPTT du module 3.

Les formes de tenseurs

Toute erreur de forme à ce stade vient d'une confusion sur l'ordre des dimensions. Keras et PyTorch conviennent : par défaut, la forme est (B,T,F)(B, T, F)BB est la taille du lot, TT la longueur en pas, FF le nombre de variables par pas.

ObjetFormeSignification
Entrée d'une couche récurrente(B,T,F)(B, T, F)BB séquences, TT pas, FF variables
Sortie avec return_sequences=False(B,H)(B, H)uniquement le dernier état hTh_T
Sortie avec return_sequences=True(B,T,H)(B, T, H)tous les hth_t
État caché hth_t(B,H)(B, H)une valeur par exemple et par unité

Le drapeau return_sequences est le paramètre le plus source d'erreurs. Une couche récurrente qui suit une autre couche récurrente doit exposer tous ses hth_t, donc return_sequences=True. Une couche récurrente suivie d'une couche Dense de classification doit ne rendre que le dernier, donc return_sequences=False.

Un premier récurrent en Keras

Sur le fil rouge de la consommation électrique, on prévoit une seule valeur (la consommation à l'heure suivante) à partir des 168 dernières.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

modele = keras.Sequential([
layers.Input(shape=(168, 1)), # T = 168, F = 1
layers.SimpleRNN(64), # return_sequences=False par defaut
layers.Dense(1), # une valeur predite
])

modele.compile(optimizer="adam", loss="mse", metrics=["mae"])
modele.summary()

La ligne SimpleRNN(64) fabrique une cellule de 64 unités cachées et la déplie automatiquement sur 168 pas. Il n'y a rien à écrire pour la boucle temporelle ; le cadriciel s'en charge.

Pour prédire 24 heures d'un coup, deux voies existent. La plus simple est un dense final à 24 sorties : la cellule résume les 168 heures en un hTh_T, puis un dense projette ce vecteur sur 24 valeurs.

modele = keras.Sequential([
layers.Input(shape=(168, 1)),
layers.SimpleRNN(64),
layers.Dense(24), # 24 heures d'un coup
])

L'autre voie, avec return_sequences=True et une cellule à la sortie, produit les 24 valeurs pas à pas. Elle est plus riche mais plus coûteuse ; on la garde pour l'encodeur-décodeur du module 8.

Ce que l'état caché n'est pas

Trois erreurs de représentation valent d'être nommées.

L'état caché n'est pas la sortie du modèle. Il est interne à la cellule. La sortie visible dépend d'une couche Dense (ou d'une autre cellule) qui consomme hth_t.

Il n'est pas une mémoire infinie. Sa capacité est fixée par la dimension HH ; au-delà d'une certaine longueur, il faut réécrire ce qui est utile plutôt que d'ajouter. C'est exactement ce que fait la porte d'oubli du LSTM au module 5.

Il n'est pas remis à zéro entre exemples d'un lot indépendant, sauf si vous l'exigez. La plupart du temps, chaque exemple d'un lot part avec h0=0h_0 = 0 ; l'option stateful=True change ce comportement pour enchaîner des lots consécutifs, utile uniquement quand on ne peut pas mettre toute la séquence dans une seule fenêtre.

Attention à l'axe des variables

Une série univariée mal préparée arrive en forme (B,T)(B, T) au lieu de (B,T,1)(B, T, 1). Keras lève alors une erreur claire. Une série multivariée dont on aurait mis les variables en tête donne (B,F,T)(B, F, T) et fait converger le modèle vers une soupe : il apprend à voir les variables comme du temps. Vérifier X.shape avant fit prend cinq secondes et sauve une soirée.

Un même modèle, plusieurs longueurs

Puisque les poids sont partagés, remplacer T=168T = 168 par T=336T = 336 à l'inférence ne demande aucun nouvel entraînement, à condition d'utiliser Input(shape=(None, 1)). C'est utile pour tester si allonger l'historique améliore les prévisions sans réécrire l'architecture.

En résumé

  • Un neurone récurrent maintient un état caché hth_t mis à jour par une combinaison de xtx_t et de ht1h_{t-1} via des matrices WxW_x et WhW_h et un biais bb.
  • Les poids sont partagés entre tous les pas : le nombre de paramètres est indépendant de la longueur TT, ce qui permet de généraliser à des séquences plus longues.
  • Les formes clés sont (B,T,F)(B, T, F) en entrée, (B,H)(B, H) en sortie avec return_sequences=False, (B,T,H)(B, T, H) avec True — c'est ce drapeau qui décide si l'on empile une autre récurrente ou une Dense de sortie.
  • L'état caché n'est ni la sortie, ni une mémoire infinie : sa capacité est bornée par HH, ce qui motive les portes d'oubli du LSTM.

Module suivant : comment ces poids apprennent, en dépliant le graphe pour y faire circuler un gradient qui, on va le voir, ne circule pas si bien.