Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Réseaux récurrents

Le chapitre 7 a montré comment le MLP apprend une représentation ϕ(x)\boldsymbol{\phi}(\mathbf{x}) à partir de données tabulaires, puis effectue une prédiction linéaire sur cette représentation. Mais le MLP traite son entrée comme un vecteur plat de taille fixe: chaque dimension est indépendante de ses voisines, et la taille de l’entrée est déterminée à l’avance.

Or beaucoup de données ont une structure séquentielle. Un texte est une suite de mots, un signal audio est une suite d’échantillons, une série temporelle de température est une suite de mesures à intervalles réguliers. Ces données ont deux propriétés que le MLP ne sait pas exploiter: elles sont ordonnées (le mot “pas” a un sens différent au début et à la fin d’une phrase), et elles sont de longueur variable (une phrase peut avoir 5 ou 50 mots).

Dans ce chapitre, nous présentons les réseaux récurrents, une famille d’architectures conçues pour traiter les séquences. Nous commençons par montrer pourquoi l’approche naïve (tout mettre dans un vecteur plat) échoue, puis nous introduisons l’idée d’état caché récurrent, le mécanisme de rétropropagation à travers le temps, et les variantes à portes (LSTM, GRU) qui atténuent la dissolution du gradient.

Le problème des séquences

Supposons que nous voulions prédire le mot suivant dans une phrase, ou classifier le sentiment d’un commentaire. L’entrée est une séquence (x1,x2,…,xT)(\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_T) où chaque xt∈Rd\mathbf{x}_t \in \mathbb{R}^d représente un élément (un mot, une mesure, un échantillon). La longueur TT varie d’un exemple à l’autre.

L’approche la plus directe serait de concaténer tous les éléments en un seul vecteur et d’utiliser un MLP:

xconcat=[x1,x2,…,xT]∈RTd\mathbf{x}_{\text{concat}} = [\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_T] \in \mathbb{R}^{Td}

Cette approche a trois problèmes. Le premier est la taille fixe: le MLP attend un vecteur de dimension TdTd, mais TT varie d’un exemple à l’autre. Pour des phrases de longueurs différentes, il faudrait tronquer ou rembourrer, ce qui est inélégant et gaspille de l’information. Le deuxième est l’absence de partage: les paramètres qui traitent x1\mathbf{x}_1 sont complètement distincts de ceux qui traitent x2\mathbf{x}_2, même si ces deux positions jouent un rôle analogue. Un motif appris en début de séquence (par exemple, reconnaître une négation) ne se transfère pas aux autres positions. Le troisième est l’absence de notion d’ordre: si l’on permute les éléments de l’entrée, un MLP n’a aucune façon de savoir que l’ordre a changé, à moins de l’encoder explicitement dans l’architecture.

Nous avons besoin d’une architecture qui traite la séquence élément par élément, qui partage ses paramètres entre les positions, et qui maintient une forme de mémoire de ce qu’elle a vu jusqu’ici.

L’état caché récurrent

L’idée des réseaux récurrents est de traiter la séquence un élément à la fois, en maintenant un vecteur d’état ht∈Rm\mathbf{h}_t \in \mathbb{R}^m qui résume l’historique de la séquence jusqu’au pas tt. À chaque pas de temps, le réseau lit le nouvel élément xt\mathbf{x}_t, le combine avec l’état précédent ht−1\mathbf{h}_{t-1}, et produit un nouvel état ht\mathbf{h}_t.

La mise à jour prend la forme:

ht=φ(Whh ht−1+Wxh xt+bh)\mathbf{h}_t = \varphi(W_{hh}\, \mathbf{h}_{t-1} + W_{xh}\, \mathbf{x}_t + \mathbf{b}_h)

où Whh∈Rm×mW_{hh} \in \mathbb{R}^{m \times m} et Wxh∈Rm×dW_{xh} \in \mathbb{R}^{m \times d} sont des matrices de poids, bh∈Rm\mathbf{b}_h \in \mathbb{R}^m est un biais, et φ\varphi est une fonction d’activation (typiquement tanh⁡\tanh). L’état initial est h0=0\mathbf{h}_0 = \mathbf{0}.

Cette équation est un MLP appliqué à la concaténation de ht−1\mathbf{h}_{t-1} et xt\mathbf{x}_t. On peut réécrire (2) sous la forme:

ht=φ ⁣([WhhWxh][ht−1xt]+bh)\mathbf{h}_t = \varphi\!\left( \begin{bmatrix} W_{hh} & W_{xh} \end{bmatrix} \begin{bmatrix} \mathbf{h}_{t-1} \\ \mathbf{x}_t \end{bmatrix} + \mathbf{b}_h \right)

Pour produire une sortie à chaque pas de temps (par exemple, prédire le mot suivant), on ajoute une couche de sortie:

yt=Why ht+by\mathbf{y}_t = W_{hy}\, \mathbf{h}_t + \mathbf{b}_y

où Why∈RK×mW_{hy} \in \mathbb{R}^{K \times m} projette l’état caché vers l’espace de sortie.

Les trois paramètres (Whh,Wxh,Why)(W_{hh}, W_{xh}, W_{hy}) et les biais sont les mêmes à chaque pas de temps. C’est le partage de paramètres qui distingue le RNN du MLP: un motif appris à une position fonctionne à toutes les positions.

Déroulement dans le temps

Pour visualiser le calcul, on peut “dérouler” le RNN dans le temps. Le même réseau est copié TT fois, une copie par pas de temps, avec les mêmes poids partout:

Le réseau déroulé ressemble à un MLP très profond (avec TT “couches”), mais avec une différence: toutes les couches partagent les mêmes poids. À chaque couche, un nouvel élément d’entrée xt\mathbf{x}_t est injecté.

Selon la tâche, on utilise l’état caché de différentes façons:

Rétropropagation à travers le temps

Pour entraîner un RNN, nous devons calculer les gradients de la perte par rapport aux paramètres. Le réseau déroulé est un graphe de calcul comme un autre: on peut appliquer la rétropropagation vue au chapitre 7.

Considérons une perte L=∑t=1Tℓt\mathcal{L} = \sum_{t=1}^T \ell_t qui accumule un terme à chaque pas de temps. Le gradient par rapport à WhhW_{hh} fait intervenir la chaîne de dépendances h1→h2→⋯→hT\mathbf{h}_1 \to \mathbf{h}_2 \to \cdots \to \mathbf{h}_T. Par la règle de la chaîne:

∂L∂Whh=∑t=1T∂ℓt∂ht∂ht∂Whh\frac{\partial \mathcal{L}}{\partial W_{hh}} = \sum_{t=1}^T \frac{\partial \ell_t}{\partial \mathbf{h}_t} \frac{\partial \mathbf{h}_t}{\partial W_{hh}}

Le terme ∂ht∂Whh\frac{\partial \mathbf{h}_t}{\partial W_{hh}} dépend de tous les états précédents. En développant:

∂ht∂Whh=∑k=1t(∏j=k+1t∂hj∂hj−1)∂+hk∂Whh\frac{\partial \mathbf{h}_t}{\partial W_{hh}} = \sum_{k=1}^t \left(\prod_{j=k+1}^t \frac{\partial \mathbf{h}_j}{\partial \mathbf{h}_{j-1}}\right) \frac{\partial^+ \mathbf{h}_k}{\partial W_{hh}}

où ∂+hk∂Whh\frac{\partial^+ \mathbf{h}_k}{\partial W_{hh}} désigne la dérivée directe (en traitant hk−1\mathbf{h}_{k-1} comme une constante), et le produit de jacobiennes ∏j=k+1t∂hj∂hj−1\prod_{j=k+1}^t \frac{\partial \mathbf{h}_j}{\partial \mathbf{h}_{j-1}} propage le gradient à travers le temps.

Ce produit de jacobiennes est la source du problème principal des RNN.

Dissolution du gradient

Chaque facteur du produit ∏j=k+1t∂hj∂hj−1\prod_{j=k+1}^t \frac{\partial \mathbf{h}_j}{\partial \mathbf{h}_{j-1}} est la jacobienne de la mise à jour récurrente:

∂hj∂hj−1=diag(φ′(aj)) Whh\frac{\partial \mathbf{h}_j}{\partial \mathbf{h}_{j-1}} = \text{diag}(\varphi'(\mathbf{a}_j))\, W_{hh}

où aj=Whh hj−1+Wxh xj+bh\mathbf{a}_j = W_{hh}\, \mathbf{h}_{j-1} + W_{xh}\, \mathbf{x}_j + \mathbf{b}_h. Pour φ=tanh⁡\varphi = \tanh, la dérivée φ′\varphi' est comprise entre 0 et 1. Si les valeurs propres de WhhW_{hh} sont inférieures à 1 en module, le produit de t−kt - k matrices décroît exponentiellement. Le gradient “disparaît” et le réseau ne peut plus apprendre les dépendances entre des éléments éloignés dans la séquence.

À l’inverse, si les valeurs propres sont supérieures à 1, le produit croît exponentiellement: le gradient “explose”. Ce problème est plus facile à traiter (on peut tronquer la norme du gradient, une technique appelée écrêtage du gradient (gradient clipping)), mais la dissolution du gradient est plus insidieuse, car elle ne produit pas d’erreur visible: l’entraînement semble fonctionner, mais le réseau ignore silencieusement les dépendances à long terme.

C’est le même phénomène que dans les MLP profonds (chapitre 8), mais aggravé par le fait que TT peut être très grand (des centaines ou des milliers de pas de temps).

Source
import numpy as np
import matplotlib.pyplot as plt

%config InlineBackend.figure_format = 'retina'

np.random.seed(42)
m = 50
W = np.random.randn(m, m) * 0.9 / np.sqrt(m)

norms = []
v = np.random.randn(m)
v = v / np.linalg.norm(v)
for t in range(100):
    v = np.tanh(W @ v)  # simplified: tanh(W h)
    scale = np.linalg.norm(np.diag(1 - v**2) @ W, ord=2)
    norms.append(scale)

grad_product = np.cumprod(norms)

fig, ax = plt.subplots(figsize=(7, 3))
ax.semilogy(grad_product, 'C0', lw=1.5)
ax.set_xlabel('Nombre de pas de temps ($t - k$)')
ax.set_ylabel('Norme du produit de jacobiennes')
ax.set_title('Décroissance exponentielle du gradient dans un RNN')
ax.grid(True, alpha=0.3)
plt.tight_layout()
<Figure size 700x300 with 1 Axes>

La figure montre la norme du produit de jacobiennes en fonction du nombre de pas de temps. Après quelques dizaines de pas, le gradient est essentiellement nul: le réseau ne reçoit plus de signal d’apprentissage pour les dépendances à long terme.

LSTM et GRU: des mécanismes de portes

Le LSTM (Long Short-Term Memory) Hochreiter & Schmidhuber (1997) résout la dissolution du gradient en introduisant un état de cellule ct\mathbf{c}_t qui circule d’un pas à l’autre de façon (presque) linéaire. Des portes apprises contrôlent le flux d’information:

ft=σ(Wf[ht−1,xt]+bf)(porte d’oubli)it=σ(Wi[ht−1,xt]+bi)(porte d’entreˊe)c~t=tanh⁡(Wc[ht−1,xt]+bc)(candidat)ct=ft⊙ct−1+it⊙c~t(mise aˋ jour de la cellule)ot=σ(Wo[ht−1,xt]+bo)(porte de sortie)ht=ot⊙tanh⁡(ct)\begin{aligned} \mathbf{f}_t &= \sigma(W_f [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_f) && \text{(porte d'oubli)} \\ \mathbf{i}_t &= \sigma(W_i [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_i) && \text{(porte d'entrée)} \\ \tilde{\mathbf{c}}_t &= \tanh(W_c [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_c) && \text{(candidat)} \\ \mathbf{c}_t &= \mathbf{f}_t \odot \mathbf{c}_{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t && \text{(mise à jour de la cellule)} \\ \mathbf{o}_t &= \sigma(W_o [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_o) && \text{(porte de sortie)} \\ \mathbf{h}_t &= \mathbf{o}_t \odot \tanh(\mathbf{c}_t) \end{aligned}

où ⊙\odot est le produit élément par élément et [ht−1,xt][\mathbf{h}_{t-1}, \mathbf{x}_t] désigne la concaténation.

L’état de cellule ct\mathbf{c}_t agit comme un chemin direct pour le gradient. Quand la porte d’oubli ft≈1\mathbf{f}_t \approx 1 et la porte d’entrée it≈0\mathbf{i}_t \approx 0, l’état de cellule est simplement copié: ct≈ct−1\mathbf{c}_t \approx \mathbf{c}_{t-1}. Le gradient circule sans atténuation à travers cette connexion linéaire, ce qui permet d’apprendre des dépendances sur des centaines de pas de temps.

Le GRU (Gated Recurrent Unit) Cho et al. (2014) simplifie le LSTM en fusionnant l’état de cellule et l’état caché, et en utilisant deux portes au lieu de trois:

zt=σ(Wz[ht−1,xt]+bz)(porte de mise aˋ jour)rt=σ(Wr[ht−1,xt]+br)(porte de reˊinitialisation)h~t=tanh⁡(Wh[rt⊙ht−1,xt]+bh)ht=(1−zt)⊙ht−1+zt⊙h~t\begin{aligned} \mathbf{z}_t &= \sigma(W_z [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_z) && \text{(porte de mise à jour)} \\ \mathbf{r}_t &= \sigma(W_r [\mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_r) && \text{(porte de réinitialisation)} \\ \tilde{\mathbf{h}}_t &= \tanh(W_h [\mathbf{r}_t \odot \mathbf{h}_{t-1}, \mathbf{x}_t] + \mathbf{b}_h) \\ \mathbf{h}_t &= (1 - \mathbf{z}_t) \odot \mathbf{h}_{t-1} + \mathbf{z}_t \odot \tilde{\mathbf{h}}_t \end{aligned}

La porte de mise à jour zt\mathbf{z}_t joue le rôle combiné des portes d’oubli et d’entrée du LSTM. En pratique, LSTM et GRU ont des performances comparables sur la plupart des tâches. Le GRU a moins de paramètres, ce qui peut être un avantage quand les données sont limitées.

Ce qui compte pour l’intuition, c’est le mécanisme commun: les portes permettent au gradient de circuler sans être multiplié par WhhW_{hh} à chaque pas, ce qui atténue la dissolution du gradient.

Limites des réseaux récurrents

Malgré les mécanismes de portes, les réseaux récurrents ont des limitations qui expliquent pourquoi ils ont été largement remplacés par les transformeurs pour beaucoup de tâches.

Le traitement séquentiel est le premier problème. Le calcul de ht\mathbf{h}_t dépend de ht−1\mathbf{h}_{t-1}, qui dépend de ht−2\mathbf{h}_{t-2}, et ainsi de suite. On ne peut pas paralléliser le traitement des différentes positions: il faut les traiter dans l’ordre. Sur du matériel moderne (GPU, TPU), conçu pour le parallélisme massif, cette contrainte est un goulot d’étranglement.

Le goulot d’information est le second problème. Toute l’information sur la séquence passe par le vecteur ht\mathbf{h}_t de dimension fixe mm. Pour une longue séquence, il est difficile de compresser tout le contexte pertinent dans ce vecteur. Même avec LSTM ou GRU, les dépendances à très long terme restent difficiles à capturer.

Ces deux limitations motivent le mécanisme d’attention, que nous verrons au chapitre suivant. L’attention permet à chaque position d’une séquence de consulter directement toutes les autres positions, sans passer par une chaîne d’états cachés, et de manière parallélisable.

Résumé

Les réseaux récurrents étendent le MLP aux données séquentielles en introduisant un état caché ht\mathbf{h}_t qui résume l’historique. Les mêmes paramètres sont partagés à chaque pas de temps, ce qui permet de traiter des séquences de longueur variable. Le réseau déroulé dans le temps est un graphe de calcul profond auquel on applique la rétropropagation standard (BPTT).

Le produit de jacobiennes qui apparaît dans BPTT décroît (ou croît) exponentiellement avec le nombre de pas de temps. Cette dissolution du gradient empêche l’apprentissage de dépendances à long terme. Le LSTM et le GRU introduisent des portes qui créent un chemin linéaire pour le gradient, atténuant ce problème.

Malgré ces progrès, les RNN souffrent d’un traitement séquentiel non parallélisable et d’un goulot d’information dû à la compression de toute la séquence dans un vecteur de taille fixe. Le chapitre suivant introduit le mécanisme d’attention, qui résout ces deux problèmes en permettant l’accès direct entre toutes les positions d’une séquence.

Exercices

Les exercices ★ vérifient la compréhension de base. Les exercices ★★ demandent d’appliquer les concepts à des calculs concrets. Les exercices ★★★ approfondissent le sujet et sont optionnels pour IFT3395.

References
  1. Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735–1780.
  2. Cho, K., van Merriënboer, B., Gulcehre, C., Bahdanau, D., Bougares, F., Schwenk, H., & Bengio, Y. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), 1724–1734.