RNN et LSTM
Réseaux récurrents pour les données séquentielles
Les Recurrent Neural Networks (RNN) sont conçus pour traiter des séquences de données où l'ordre et le contexte importent : texte, audio, séries temporelles.
Le problème des séquences
Les MLP et CNN traitent des entrées de taille fixe. Mais de nombreuses données sont des séquences de longueur variable :
- Texte : phrases de longueurs différentes
- Audio : parole, musique
- Vidéo : suite d'images
- Séries temporelles : cours boursiers, capteurs
L'ordre compte ! "Le chat mange la souris" ≠ "La souris mange le chat"
RNN : l'idée de base
Un RNN traite les éléments un par un en maintenant un état caché qui résume l'historique.
Architecture
x₁ x₂ x₃ x₄
↓ ↓ ↓ ↓
┌───┐ ┌───┐ ┌───┐ ┌───┐
h₀ ──→ │RNN│ ──→ │RNN│ ──→ │RNN│ ──→ │RNN│ ──→ h₄
└───┘ └───┘ └───┘ └───┘
↓ ↓ ↓ ↓
y₁ y₂ y₃ y₄
Les mêmes poids sont partagés à chaque pas de temps !Équations
hₜ = tanh(Wₕₓ · xₜ + Wₕₕ · hₜ₋₁ + bₕ)
yₜ = Wᵧₕ · hₜ + bᵧL'état caché hₜ encode l'information des étapes précédentes.
Types de RNN
One-to-One One-to-Many Many-to-One
(Standard) (Génération) (Classification)
┌───┐ ┌───┐→y₁ x₁→┌───┐
x → │ │ → y x → │ │→y₂ x₂→│ │
└───┘ │ │→y₃ x₃→│ │ → y
└───┘ x₄→└───┘
Many-to-Many Many-to-Many
(Aligned) (Seq2Seq)
x₁→┌───┐→y₁ x₁→┌───┐ ┌───┐→y₁
x₂→│ │→y₂ x₂→│Enc│───→│Dec│→y₂
x₃→│ │→y₃ x₃→└───┘ └───┘→y₃
x₄→└───┘→y₄Le problème du Vanishing Gradient
Les RNN simples ont du mal à apprendre les dépendances long terme.
"Le chat qui a mangé la souris qui vivait dans le trou est ___"
Le RNN doit "se souvenir" que le sujet est "chat" sur plusieurs mots.Pourquoi ?
Gradient = ∏ ∂hₜ/∂hₜ₋₁
Si |∂h/∂h| < 1 répété 100 fois → gradient ≈ 0 (vanishing)
Si |∂h/∂h| > 1 répété 100 fois → gradient → ∞ (exploding)LSTM : Long Short-Term Memory
Solution au vanishing gradient proposée en 1997 par Hochreiter & Schmidhuber.
Idée clé : la cellule mémoire
Le LSTM ajoute une cellule mémoire C séparée de l'état caché, avec des portes qui contrôlent le flux d'information.
┌─────────────────────────────────────────────────┐
│ LSTM Cell │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────┐ │
│ Cₜ₋₁ ────→│ × │──────────→ Cₜ │
│ └──┬──┘ │
│ ↑ + │
│ ┌────┴────┐ ┌──┴──┐ │
│ │ Forget │ │ Add │ │
│ │ Gate │ │ │ │
│ └────┬────┘ └──┬──┘ │
│ │ │ │
│ ┌────┴────┐ ┌────┴────┐ │
│ │ σ │ │ tanh │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ [hₜ₋₁,xₜ] [hₜ₋₁,xₜ] │
│ │
└─────────────────────────────────────────────────┘Les 4 portes
# 1. Forget Gate : que garder de l'ancienne mémoire ?
fₜ = σ(Wf · [hₜ₋₁, xₜ] + bf) # Entre 0 et 1
# 2. Input Gate : quoi ajouter de nouveau ?
iₜ = σ(Wi · [hₜ₋₁, xₜ] + bi) # Entre 0 et 1
C̃ₜ = tanh(Wc · [hₜ₋₁, xₜ] + bc) # Candidat
# 3. Cell State : mise à jour de la mémoire
Cₜ = fₜ * Cₜ₋₁ + iₜ * C̃ₜ
# 4. Output Gate : que sortir ?
oₜ = σ(Wo · [hₜ₋₁, xₜ] + bo)
hₜ = oₜ * tanh(Cₜ)Pourquoi ça marche ?
Le flux de gradient à travers C est linéaire (additions/multiplications), évitant le vanishing gradient.
∂Cₜ/∂Cₜ₋₁ = fₜ (proche de 1 si on veut garder)
Le gradient peut "couler" sur de longues séquences.GRU : Gated Recurrent Unit
Version simplifiée du LSTM (2014), souvent aussi performante.
# Reset Gate : que garder du passé ?
rₜ = σ(Wr · [hₜ₋₁, xₜ])
# Update Gate : combien mettre à jour ?
zₜ = σ(Wz · [hₜ₋₁, xₜ])
# Candidat
h̃ₜ = tanh(W · [rₜ * hₜ₋₁, xₜ])
# Nouvel état
hₜ = (1 - zₜ) * hₜ₋₁ + zₜ * h̃ₜLSTM vs GRU
| Aspect | LSTM | GRU |
|---|---|---|
| Paramètres | Plus | Moins |
| Portes | 4 | 2 |
| Performance | Souvent meilleure sur longues séquences | Comparable, plus rapide |
| Mémoire | Cellule séparée | État unique |
Implémentation PyTorch
RNN simple
import torch
import torch.nn as nn
# RNN basique
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=2, batch_first=True)
# Entrée : (batch, seq_len, input_size)
x = torch.randn(32, 50, 10)
h0 = torch.zeros(2, 32, 20) # (num_layers, batch, hidden_size)
output, hn = rnn(x, h0)
# output: (32, 50, 20) - toutes les sorties
# hn: (2, 32, 20) - dernier état cachéLSTM
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=2, batch_first=True)
h0 = torch.zeros(2, 32, 20)
c0 = torch.zeros(2, 32, 20)
output, (hn, cn) = lstm(x, (h0, c0))
# cn: cellule mémoire finaleGRU
gru = nn.GRU(input_size=10, hidden_size=20, num_layers=2, batch_first=True)
output, hn = gru(x, h0)Architectures avancées
Bidirectionnel
Traite la séquence dans les deux sens.
→ → → →
x₁ x₂ x₃ x₄
← ← ← ←
Utile quand le contexte futur compte (NLP).lstm = nn.LSTM(input_size=10, hidden_size=20,
bidirectional=True, batch_first=True)
# Output: (batch, seq_len, hidden_size * 2)Multi-couches (Stacked)
x₁ x₂ x₃
↓ ↓ ↓
┌───┐ ┌───┐ ┌───┐
│L1 │→│L1 │→│L1 │ Couche 1
└───┘ └───┘ └───┘
↓ ↓ ↓
┌───┐ ┌───┐ ┌───┐
│L2 │→│L2 │→│L2 │ Couche 2
└───┘ └───┘ └───┘
↓ ↓ ↓
y₁ y₂ y₃lstm = nn.LSTM(input_size=10, hidden_size=20,
num_layers=3, batch_first=True, dropout=0.2)Seq2Seq avec attention
Base des premiers systèmes de traduction neuronale.
Encoder Decoder
┌───┐ ┌───┐
→ │ │→ → │ │→ "Le"
└───┘ └───┘
┌───┐ ┌───┐
→ │ │→ ═══Context═══> → │ │→ "chat"
└───┘ └───┘
┌───┐ ┌───┐
→ │ │→ → │ │→ "dort"
└───┘ └───┘
"The cat sleeps"Exemple complet : Classification de texte
import torch
import torch.nn as nn
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(embed_dim, hidden_dim,
num_layers=2, batch_first=True,
bidirectional=True, dropout=0.3)
self.fc = nn.Linear(hidden_dim * 2, num_classes)
self.dropout = nn.Dropout(0.5)
def forward(self, x):
# x: (batch, seq_len) - indices de tokens
# Embedding
embedded = self.embedding(x) # (batch, seq_len, embed_dim)
# LSTM
output, (hn, cn) = self.lstm(embedded)
# hn: (num_layers * 2, batch, hidden_dim)
# Concaténer les derniers états forward et backward
hidden = torch.cat([hn[-2], hn[-1]], dim=1)
# hidden: (batch, hidden_dim * 2)
# Classification
out = self.dropout(hidden)
out = self.fc(out)
return out
# Utilisation
model = TextClassifier(vocab_size=10000, embed_dim=128,
hidden_dim=256, num_classes=5)
# Batch de 32 séquences de 100 tokens
x = torch.randint(0, 10000, (32, 100))
output = model(x) # (32, 5)Applications
Traitement du langage naturel
- Classification de texte (sentiment, spam)
- Génération de texte
- Traduction automatique
- Résumé automatique
Séries temporelles
- Prévision météo
- Prédiction boursière
- Détection d'anomalies
- Maintenance prédictive
Audio et parole
- Reconnaissance vocale
- Synthèse vocale
- Transcription musicale
Limites des RNN/LSTM
Malgré leurs succès, les RNN ont des limites :
| Limite | Conséquence |
|---|---|
| Séquentiel | Pas de parallélisation, lent à entraîner |
| Dépendances très longues | LSTM aide mais pas parfait |
| Contexte limité | Difficile de "regarder loin" |
Depuis 2017, les Transformers ont largement remplacé les LSTM pour le NLP grâce à leur parallélisme et leur gestion supérieure du contexte long.
LSTM vs Transformers
| Aspect | LSTM | Transformer |
|---|---|---|
| Parallélisation | Non (séquentiel) | Oui (attention) |
| Contexte long | Difficile | Excellent |
| Entraînement | Lent | Rapide |
| Mémoire | Faible | Élevée |
| Interprétabilité | Difficile | Attention visualisable |
Les LSTM restent utiles pour :
- Séries temporelles courtes
- Ressources limitées
- Applications temps réel sur edge