Réseaux de Neurones Récurrents (RNNs)
Introduction aux RNNs
Jusqu'à présent, nous avons travaillé avec des réseaux de neurones feedforward (à propagation avant), où l'information circule dans une seule direction : de l'entrée vers la sortie, en passant par les couches cachées. Ces réseaux sont bien adaptés aux données où chaque exemple est indépendant des autres (comme des images individuelles).
Cependant, de nombreux types de données ont une nature séquentielle :
- Texte : Une phrase est une séquence de mots.
- Séries temporelles : La température, le cours d'une action, etc., sont des séquences de valeurs au fil du temps.
- Son : Un signal audio est une séquence d'amplitudes.
- Vidéo : Une vidéo est une séquence d'images.
- Génome : Séquence ADN
Pour traiter ce type de données, nous avons besoin d'un type de réseau de neurones capable de prendre en compte l'ordre des éléments et de "se souvenir" du passé : les réseaux de neurones récurrents (RNNs).
Qu'est-ce qu'un RNN ?
Un RNN est un réseau de neurones qui a des connexions récurrentes, c'est-à-dire des connexions qui pointent vers l'arrière (vers des couches précédentes, ou vers la même couche à un pas de temps antérieur). Cela crée une forme de "mémoire" interne qui permet au réseau de traiter des séquences de longueur variable et de prendre en compte le contexte.
Intuition : Dérouler le Temps
Imaginez que vous lisiez une phrase, mot par mot. Pour comprendre le sens de la phrase, vous ne pouvez pas traiter chaque mot indépendamment. Vous devez vous souvenir des mots précédents. Un RNN fait quelque chose de similaire : il traite une séquence élément par élément, en conservant un "état caché" qui représente ce qu'il a vu jusqu'à présent.
On peut visualiser un RNN comme un réseau que l'on "déroule" dans le temps :
x_t: L'entrée au temps t (par exemple, un mot dans une phrase).h_t: L'état caché au temps t (la "mémoire" du réseau).y_t: La sortie au temps t (par exemple, la probabilité que le mot suivant soit "chat").[RNN]: Le réseau récurrent (la même cellule RNN est réutilisée à chaque pas de temps).
À chaque pas de temps, le RNN reçoit une entrée (x_t) et son état caché précédent (h_t-1), et il produit une sortie (y_t) et un nouvel état caché (h_t).
Formule Simplifiée
On peut décrire le fonctionnement d'un RNN simple (appelé Vanilla RNN ou Elman RNN) avec les équations suivantes :
Où :
- : Entrée
- : État caché.
- : Sortie
- : Matrice de poids récurrents (qui connecte l'état caché précédent à l'état caché actuel).
- : Matrice de poids des entrées (qui connecte l'entrée à l'état caché).
- : Matrice de poids des sorties (qui connecte l'état caché à la sortie).
- : Biais de l'état caché.
- : Biais de la sortie.
- : Fonction d'activation (tangente hyperbolique).
Les RNNs sont un outil puissant pour traiter les données séquentielles. Ils sont à la base de nombreuses applications de pointe en traitement du langage naturel, en reconnaissance vocale, en génération de texte, etc.
Séquences
Le concept de séquence est fondamental pour comprendre les réseaux de neurones récurrents (RNNs). Une séquence est une suite ordonnée d'éléments. L'ordre est crucial : la séquence [A, B, C] est différente de la séquence [C, B, A].
Exemples de Données Séquentielles
De nombreux types de données peuvent être représentés comme des séquences :
- Texte : Une phrase est une séquence de mots. Un document est une séquence de phrases. Un livre est une séquence de chapitres, etc.
- Séries temporelles :
- Le cours d'une action en bourse est une séquence de prix au fil du temps.
- La température mesurée à intervalles réguliers est une séquence de valeurs.
- Les données d'un capteur (accéléromètre, gyroscope, etc.) sont des séquences de mesures.
- Son :
- Un signal audio brut est une séquence d'amplitudes (échantillonnées à intervalles réguliers).
- Un spectrogramme (représentation temps-fréquence d'un signal audio) est une séquence de vecteurs de fréquences.
- Vidéo : Une vidéo est une séquence d'images.
- Génome : Une séquence d'ADN est une séquence de nucléotides (A, C, G, T).
- Musique: Une partition est une séquence de notes, d'accords
Représentation des Séquences
Pour qu'un RNN puisse traiter une séquence, il faut la représenter sous forme numérique. Les techniques varient selon le type de données :
- Texte :
- One-hot encoding : Chaque mot est représenté par un vecteur de la taille du vocabulaire, avec un 1 à la position correspondant au mot et des 0 ailleurs. (Peu efficace pour de grands vocabulaires.)
- Word embeddings : Chaque mot est représenté par un vecteur dense de petite taille (par exemple, 100, 300 dimensions). Ces vecteurs sont appris par le réseau (ou pré-entraînés sur de grands corpus). Les mots similaires ont des vecteurs proches. (Exemples : Word2Vec, GloVe, FastText).
- Séries temporelles :
- Si la série est monovariée (une seule valeur à chaque pas de temps), chaque valeur peut être représentée par un scalaire.
- Si la série est multivariée (plusieurs valeurs à chaque pas de temps), chaque pas de temps est représenté par un vecteur.
- Son :
- Signal audio brut : Séquence de scalaires (amplitudes).
- Spectrogramme : Séquence de vecteurs (représentant les fréquences à chaque pas de temps).
- Vidéo :
- Séquence de tenseurs (chaque image est un tenseur 2D + canaux de couleur).
Longueur des Séquences
Les séquences peuvent avoir une longueur fixe ou variable :
- Longueur fixe : Toutes les séquences ont le même nombre d'éléments (par exemple, des phrases de longueur fixe, des segments de signal audio de durée fixe).
- Longueur variable : Les séquences peuvent avoir des longueurs différentes (par exemple, des phrases de longueurs différentes, des documents de longueurs différentes).
Les RNNs peuvent gérer les deux, mais le traitement des séquences de longueur variable nécessite des techniques spécifiques (padding, masquage).
Notation
On note souvent :
- : L'élément de la séquence à l'instant t.
- : Autre notation.
- : La longueur de la séquence d'entrée.
- : Le t-ième élément de la séquence du i-ème exemple.
Pourquoi les RNNs sont-ils Adaptés aux Séquences ?
Contrairement aux réseaux feedforward, les RNNs ont une "mémoire" interne (l'état caché) qui leur permet de :
- Prendre en compte l'ordre des éléments.
- "Se souvenir" des informations des pas de temps précédents.
- Traiter des séquences de longueur variable.
C'est ce qui les rend particulièrement adaptés au traitement des données séquentielles.
Mémoire (État Caché)
La "mémoire" est ce qui distingue fondamentalement les réseaux de neurones récurrents (RNNs) des réseaux feedforward. C'est grâce à cette mémoire interne que les RNNs peuvent traiter des séquences et prendre en compte le contexte.
Qu'est-ce que la Mémoire d'un RNN ?
La mémoire d'un RNN, souvent appelée état caché (hidden state), est un vecteur (ou, plus généralement, un tenseur) qui est mis à jour à chaque pas de temps. Ce vecteur représente un résumé de l'information que le RNN a vue jusqu'à présent dans la séquence.
Analogie :
Imaginez que vous lisiez un livre, phrase par phrase. Votre "état mental" après avoir lu chaque phrase n'est pas seulement déterminé par la phrase elle-même, mais aussi par toutes les phrases précédentes. Votre état mental est une sorte de résumé (imparfait) de ce que vous avez lu jusqu'à présent. L'état caché d'un RNN est similaire : c'est une représentation (sous forme de vecteur de nombres) de ce que le RNN a "vu" jusqu'à présent dans la séquence.
Fonctionnement
Rappelons les équations simplifiées d'un RNN "vanilla" :
- est l'état caché (la mémoire) au temps t. C'est un vecteur.
- est l'état caché au temps t-1 (l'état précédent).
L'état caché au temps t est calculé en fonction de :
- L'état caché précédent () : C'est la "mémoire" du passé.
- L'entrée actuelle () : C'est le nouvel élément de la séquence que le RNN est en train de traiter.
- Paramètres appris (, , ) : Ces matrices de poids et ce biais déterminent comment l'état caché est mis à jour.
La fonction d'activation (tanh) introduit une non-linéarité.
L'état caché est donc une combinaison de la mémoire du passé et de l'information actuelle.
Initialisation de l'État Caché
Au début de la séquence (temps t=0), il n'y a pas d'état caché précédent. On initialise généralement l'état caché initial () à un vecteur de zéros, ou parfois à une valeur apprise.
Rôle de l'État Caché
- Propager l'information dans le temps : L'état caché permet à l'information de "voyager" d'un pas de temps à l'autre. Une information présente dans une entrée lointaine peut influencer la sortie du RNN à un temps ultérieur, à condition qu'elle soit "stockée" dans l'état caché.
- Représenter le contexte : L'état caché est une représentation (compressée) du contexte de la séquence jusqu'au temps t.
- Influencer les sorties : La sortie du RNN () est calculée à partir de l'état caché (). Donc, la mémoire influence directement les prédictions du RNN.
Limitations de la Mémoire des RNNs Simples
Les RNNs "vanilla" (ceux décrits par les équations ci-dessus) ont du mal à apprendre des dépendances à long terme dans les séquences. Si une information importante est présente au début d'une longue séquence, son influence peut s'estomper au fur et à mesure que l'état caché est mis à jour à chaque pas de temps (problème du gradient qui s'évanouit/explose, appliqué aux RNNs). C'est pour cela qu'on a conçu des architectures de sequences-to-sequences.
C'est pour résoudre ce problème que des architectures RNN plus avancées, comme les LSTM (Long Short-Term Memory) et les GRU (Gated Recurrent Units), ont été développées. Elles utilisent des mécanismes plus sophistiqués pour contrôler le flux d'information dans l'état caché et préserver l'information sur de plus longues durées.
L'état caché est le cœur du fonctionnement d'un RNN. C'est lui qui permet au réseau de "se souvenir" du passé et de traiter des séquences de manière cohérente. Comprendre le rôle de l'état caché est essentiel pour comprendre les RNNs.
Cellules LSTM et GRU
Nous avons vu que les RNNs "vanilla" (simples) ont des difficultés à apprendre des dépendances à long terme dans les séquences, à cause du problème du gradient qui s'évanouit ou explose. Les cellules LSTM (Long Short-Term Memory) et GRU (Gated Recurrent Unit) sont des architectures de RNN plus avancées, conçues spécifiquement pour résoudre ce problème.
LSTM (Long Short-Term Memory)
Les LSTMs ont été proposées en 1997 par Sepp Hochreiter et Jürgen Schmidhuber. Elles sont beaucoup plus complexes que les RNNs simples, mais l'idée clé est d'introduire des portes (gates) qui contrôlent le flux d'information dans et hors de la cellule.
Composants d'une cellule LSTM :
- État de la cellule () : C'est la "mémoire à long terme" du LSTM. Elle est modifiée de manière additive (contrairement à l'état caché des RNNs simples, qui est recalculé à chaque pas de temps), ce qui permet à l'information de persister plus longtemps.
- État caché () : C'est la "mémoire à court terme" et aussi la sortie de la cellule LSTM.
- Porte d'oubli (forget gate, ) : Décide quelles informations de l'état de la cellule doivent être oubliées. C'est un vecteur de valeurs entre 0 et 1 (produit par une sigmoïde).
- Porte d'entrée (input gate, ) : Décide quelles nouvelles informations doivent être stockées dans l'état de la cellule. C'est aussi un vecteur de valeurs entre 0 et 1.
- Porte de sortie (output gate, ) : Décide quelles parties de l'état de la cellule doivent être utilisées pour calculer l'état caché (et la sortie).
Fonctionnement simplifié :
-
Porte d'oubli : La porte d'oubli calcule un vecteur en fonction de l'état caché précédent () et de l'entrée actuelle (). Ce vecteur indique quelles parties de l'état de la cellule () doivent être oubliées (multiplication terme à terme).
-
Porte d'entrée : La porte d'entrée calcule deux choses :
- Un vecteur (entre 0 et 1) qui indique quelles parties de la nouvelle information candidate doivent être ajoutées à l'état de la cellule.
- Un vecteur qui représente la nouvelle information candidate, calculée à partir de et (en utilisant une tanh).
-
Mise à jour de l'état de la cellule : Le nouvel état de la cellule () est calculé comme suit :
- On oublie une partie de l'ancien état ().
- On ajoute une partie de la nouvelle information candidate ().
-
Porte de sortie : La porte de sortie calcule un vecteur (entre 0 et 1) en fonction de et .
-
Calcul de l'état caché : Le nouvel état caché () est calculé en appliquant une tanh à l'état de la cellule (), puis en multipliant terme à terme par la sortie de la porte de sortie () :
Équations (simplifiées, sans les biais) :
Où :
- est la fonction sigmoïde.
- sont des matrices de poids.
- représente la concaténation des vecteurs et .
*représente la multiplication terme à terme (Hadamard product).
GRU (Gated Recurrent Unit)
Les GRUs, proposées en 2014 par Kyunghyun Cho et al., sont une simplification des LSTMs. Elles ont moins de paramètres et sont souvent plus rapides à entraîner, tout en obtenant des performances similaires, voire meilleures, dans de nombreux cas.
Composants d'une cellule GRU :
- État caché () : Combine les rôles de l'état de la cellule et de l'état caché des LSTMs.
- Porte de mise à jour (update gate, ) : Contrôle la quantité d'information de l'état caché précédent à conserver et la quantité de nouvelle information à ajouter.
- Porte de réinitialisation (reset gate, ) : Contrôle la quantité d'information de l'état caché précédent à oublier.
Fonctionnement simplifié :
-
Porte de réinitialisation : Calcule un vecteur (entre 0 et 1) en fonction de et .
-
Porte de mise à jour : Calcule un vecteur (entre 0 et 1) en fonction de et .
-
Calcul d'un état candidat : Calcule un état candidat en fonction de (modulé par ) et .
-
Mise à jour de l'état caché : Le nouvel état caché () est une combinaison linéaire de l'état caché précédent () et de l'état candidat (), contrôlée par la porte de mise à jour () :
Équations (simplifiées, sans les biais) :
LSTM vs. GRU
| Caractéristique | LSTM | GRU |
|---|---|---|
| Complexité | Plus complexe (plus de paramètres) | Moins complexe (moins de paramètres) |
| Portes | 3 (oubli, entrée, sortie) | 2 (mise à jour, réinitialisation) |
| État | État de la cellule + état caché | Un seul état caché |
| Performance | Très bon, mais plus lent à entraîner | Souvent comparable à LSTM, plus rapide |
| Flexibilité | Plus flexible (contrôle plus fin) | Moins flexible |
Les LSTMs et les GRUs sont des architectures RNN beaucoup plus puissantes que les RNNs simples pour traiter les dépendances à long terme. Elles sont largement utilisées en pratique. Le choix entre LSTM et GRU dépend du problème et des données; il est souvent utile d'essayer les deux.
Applications des RNNs : Analyse de Sentiments, Prédiction de Séries Temporelles, et Autres
Les réseaux de neurones récurrents (RNNs), et en particulier leurs variantes avancées comme les LSTMs et les GRUs, sont utilisés dans une grande variété d'applications impliquant des données séquentielles. Voici quelques exemples marquants :
Analyse de Sentiments (Sentiment Analysis)
- Problème : Étant donné un texte (par exemple, un commentaire de film, un tweet, un avis sur un produit), déterminer si le sentiment exprimé est positif, négatif ou neutre.
- Pourquoi les RNNs sont adaptés :
- Le texte est une séquence de mots.
- L'ordre des mots est important (le sens d'une phrase dépend de l'ordre des mots).
- Le sentiment peut dépendre de relations à long terme entre les mots (par exemple, "pas" suivi de "bon" plusieurs mots plus loin).
- Approche typique :
- Encoder les mots du texte en vecteurs (word embeddings).
- Passer la séquence de vecteurs à un RNN (souvent un LSTM ou un GRU).
- Utiliser l'état caché final du RNN (ou une combinaison des états cachés) comme entrée d'un classifieur (par exemple, une couche Dense avec une activation softmax).
- Variantes : Analyse de sentiments plus fine (par exemple, sur une échelle de 1 à 5 étoiles), détection d'émotions (joie, colère, tristesse, etc.).
Prédiction de Séries Temporelles (Time Series Forecasting)
- Problème : Étant donné une séquence de valeurs mesurées au fil du temps (par exemple, le cours d'une action, la température, la consommation d'électricité), prédire la ou les valeurs futures.
- Pourquoi les RNNs sont adaptés :
- Les séries temporelles sont des séquences.
- Les valeurs futures dépendent souvent des valeurs passées (dépendances temporelles).
- Approche typique :
- Représenter chaque pas de temps comme un vecteur (si la série est multivariée) ou un scalaire (si elle est monovariée).
- Passer la séquence de vecteurs/scalaires à un RNN (souvent un LSTM ou un GRU).
- Utiliser l'état caché final du RNN (ou une combinaison des états cachés) comme entrée d'un prédicteur (par exemple, une couche Dense).
- Variantes : Prédiction à court terme, prédiction à long terme, prédiction multi-étapes.
Traduction Automatique (Machine Translation)
- Problème : Étant donné une phrase dans une langue source, générer la traduction correspondante dans une langue cible.
- Pourquoi les RNNs sont adaptés :
- Les phrases sont des séquences de mots.
- L'ordre des mots est crucial.
- Il existe des dépendances à long terme entre les mots de la phrase source et les mots de la phrase cible.
- Approche typique :
- Modèle encodeur-décodeur (sequence-to-sequence) :
- Un RNN (l'encodeur) traite la phrase source et produit un vecteur d'état caché qui représente le "sens" de la phrase.
- Un autre RNN (le décodeur) prend cet état caché comme entrée et génère la phrase cible, mot par mot.
- Souvent, des mécanismes d'attention sont ajoutés pour améliorer les performances.
- Modèle encodeur-décodeur (sequence-to-sequence) :
- Exemple: Google Traduction
Génération de Texte (Text Generation)
- Problème : Générer du texte de manière automatique (par exemple, compléter une phrase, écrire un poème, générer du code).
- Pourquoi les RNNs sont adaptés :
- Le texte est une séquence.
- Le prochain mot/caractère à générer dépend des mots/caractères précédents.
- Approche typique :
- Entraîner un RNN sur un grand corpus de texte.
- Le RNN apprend à prédire le prochain mot/caractère étant donné une séquence de mots/caractères précédents.
- Pour générer du texte, on donne une séquence initiale au RNN, il prédit le mot suivant, on ajoute ce mot à la séquence, et on répète le processus.
- Variantes : Génération de texte conditionnelle (par exemple, générer une description d'une image), génération de dialogue.
Reconnaissance Vocale (Speech Recognition)
- Problème : Transcrire un signal audio en texte.
- Pourquoi les RNNs sont adaptés :
- Le son est une séquence.
- Le contexte (les sons précédents) est important pour identifier le phonème/mot courant.
- Approche typique :
- Extraire des caractéristiques du signal audio (par exemple, spectrogramme).
- Passer la séquence de caractéristiques à un RNN (souvent bidirectionnel, c'est-à-dire qui traite la séquence dans les deux sens).
- Le RNN produit une séquence de probabilités pour chaque phonème/mot possible.
- Variantes : Reconnaissance vocale en temps réel, reconnaissance vocale multi-locuteurs.
Autres Applications
Les RNNs sont également utilisés dans de nombreux autres domaines, tels que :
- Analyse de séquences biologiques (ADN, protéines).
- Modélisation du langage (prédiction du prochain mot, complétion de phrase).
- Génération de musique.
- Analyse de vidéos (reconnaissance d'actions, légendage de vidéos).
- Robotique (contrôle de robots basé sur des séquences d'observations et d'actions).
- Système de recommandation
Cette liste n'est pas exhaustive, mais elle donne un aperçu de la diversité des applications des RNNs. La clé est que les RNNs sont particulièrement adaptés aux problèmes où les données sont séquentielles et où l'ordre et le contexte sont importants.
Utilisation de Couches LSTM (LSTM) ou GRU (GRU) en Keras
Nous avons vu précédemment les concepts théoriques des cellules LSTM et GRU. Voyons maintenant comment utiliser les couches correspondantes en Keras.
Couches LSTM et GRU
Keras fournit des couches LSTM (tf.keras.layers.LSTM) et GRU (tf.keras.layers.GRU) qui implémentent ces architectures. Elles peuvent être utilisées de la même manière que n'importe quelle autre couche Keras (par exemple, Dense) dans un modèle séquentiel ou fonctionnel.
Paramètres principaux (communs à LSTM et GRU) :
units: Le nombre d'unités (la dimensionnalité de l'état caché). C'est le seul paramètre obligatoire.activation: La fonction d'activation à utiliser pour les calculs internes (par défaut,'tanh'pour LSTM et GRU).recurrent_activation: La fonction d'activation à utiliser pour les portes (par défaut,'sigmoid'pour LSTM et GRU).use_bias: Booléen, indique si la couche utilise un biais (par défaut,True).kernel_initializer,recurrent_initializer,bias_initializer: Initialiseurs pour les différents poids.return_sequences: Booléen (par défaut,False).- Si
False: La couche retourne seulement l'état caché final (un seul vecteur pour toute la séquence). - Si
True: La couche retourne les états cachés pour chaque pas de temps de la séquence (un tenseur 3D).
- Si
return_state: Booléen (par défaut,False).- Si
True: La couche retourne, en plus de sa sortie habituelle, le dernier état caché (h_T) et, pour LSTM, le dernier état de la cellule (c_T).
- Si
go_backwards: Booléen (par défaut,False). SiTrue, la séquence est traitée en sens inverse.stateful: Booléen (par défaut,False). SiTrue, l'état caché calculé pour un batch est réutilisé comme état initial pour le batch suivant. Utile pour les très longues séquences, mais nécessite des tailles de batch fixes et une gestion plus complexe.
Exemple (LSTM) :
import tensorflow as tf
model = tf.keras.Sequential([
tf.keras.layers.LSTM(units=64, input_shape=(None, 128), return_sequences=True), # Retourne les états cachés pour chaque pas de temps
tf.keras.layers.LSTM(units=32), # Retourne seulement l'état caché final
tf.keras.layers.Dense(units=10, activation='softmax')
])
Exemple (GRU) :
model = tf.keras.Sequential([
tf.keras.layers.GRU(units=64, input_shape=(None, 128), return_state=True),
tf.keras.layers.Dense(units=10, activation='softmax')
])
output, final_state = model(inputs) # Si return_state=True, la couche retourne l'état final en plus de la sortie
Différences Principales entre LSTM et GRU en Keras
- Nombre d'états :
LSTMa deux états : l'état caché (h_t) et l'état de la cellule (c_t).GRUa un seul état caché (h_t).
return_state(LSTM) : Sireturn_state=True, la coucheLSTMretourne trois tenseurs : la sortie, l'état caché final (h_T) et l'état de la cellule final (c_T).return_state(GRU) : Sireturn_state=True, la coucheGRUretourne deux tenseurs : la sortie et l'état caché final (h_T).
Quand Utiliser return_sequences=True ?
- Si vous empilez plusieurs couches RNN (LSTM ou GRU), vous devez mettre
return_sequences=Truepour toutes les couches sauf la dernière. Cela permet à chaque couche RNN de recevoir la séquence complète des états cachés de la couche précédente. - Si vous voulez utiliser les états cachés de tous les pas de temps pour une autre opération (par exemple, un mécanisme d'attention, ou une couche de convolution 1D), vous devez mettre
return_sequences=True.
Exemple Complet (Classification de Séquences)
Voici un exemple complet de modèle Keras utilisant une couche LSTM pour la classification de séquences (par exemple, analyse de sentiments) :
import tensorflow as tf
model = tf.keras.Sequential([
# Couche d'embedding (pour convertir des entiers - indices de mots - en vecteurs denses)
tf.keras.layers.Embedding(input_dim=10000, output_dim=64, input_length=100), # input_dim = taille du vocabulaire, output_dim = dimension des embeddings, input_length = longueur maximale des séquences
# Couche LSTM
tf.keras.layers.LSTM(units=128, return_sequences=False), # On ne garde que l'état caché final
# Couche Dense (classification)
tf.keras.layers.Dense(units=1, activation='sigmoid') # Classification binaire
])
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
Dans cet exemple :
- On suppose que les entrées sont des séquences d'entiers (représentant des mots, par exemple).
- La couche
Embeddingtransforme ces entiers en vecteurs denses (word embeddings). - La couche
LSTMtraite la séquence de vecteurs et retourne son état caché final. - La couche
Denseprend cet état caché final et produit une probabilité (classification binaire).
Les couches LSTM et GRU de Keras sont des outils puissants et flexibles pour construire des RNNs. Comprendre leurs paramètres (en particulier units, return_sequences et return_state) est essentiel pour les utiliser correctement.
Implémentation d'un RNN Simple avec Keras
Illustrons l'implémentation d'un RNN simple avec Keras en utilisant un exemple de classification de séquences. Pour simplifier, nous n'utiliserons ni LSTM ni GRU, mais une couche SimpleRNN (qui implémente le RNN "vanilla" dont nous avons parlé précédemment). Gardez à l'esprit que SimpleRNN est rarement utilisé en pratique (à cause du problème du gradient qui s'évanouit), mais il est utile pour comprendre les bases.
Problème : Classification de Séquences Binaires
Nous allons créer un problème très simple :
- Entrée : Une séquence de 0 et de 1 (par exemple,
[0, 1, 1, 0, 1]). - Sortie :
- 1 si la séquence contient plus de 1 que de 0.
- 0 sinon.
Ce n'est pas un problème réaliste, mais il est suffisant pour illustrer le fonctionnement d'un RNN simple.
Code Complet (Keras)
import tensorflow as tf
import numpy as np
# Pour la reproductibilité
np.random.seed(42)
tf.random.set_seed(42)
# 1. Génération des données
def generate_binary_sequences(num_sequences, sequence_length):
X = np.random.randint(0, 2, size=(num_sequences, sequence_length)) # Séquences de 0 et 1
Y = (np.sum(X, axis=1) > sequence_length / 2).astype(np.int32) # 1 si plus de 1 que de 0
return X, Y
num_sequences = 1000
sequence_length = 10 # Séquences de longueur 10
X, Y = generate_binary_sequences(num_sequences, sequence_length)
# Division en ensembles d'entraînement et de test (80/20)
split_ratio = 0.8
split_index = int(split_ratio * num_sequences)
x_train, x_test = X[:split_index], X[split_index:]
y_train, y_test = Y[:split_index], Y[split_index:]
# Ajout d'une dimension pour le canal (car les RNNs attendent des entrées 3D)
x_train = x_train[..., np.newaxis] # (800, 10) -> (800, 10, 1)
x_test = x_test[..., np.newaxis] # (200, 10) -> (200, 10, 1)
# 2. Construction du modèle
model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(units=32, input_shape=(None, 1)), # None pour la longueur variable (mais ici, elle est fixe)
tf.keras.layers.Dense(units=1, activation='sigmoid') # Classification binaire
])
# 3. Compilation
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
# 4. Entraînement
history = model.fit(x_train, y_train, epochs=50, batch_size=32, validation_split=0.2)
# 5. Visualisation
import matplotlib.pyplot as plt
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
# 6. Évaluation
loss, accuracy = model.evaluate(x_test, y_test, verbose=0)
print(f"Perte (test) : {loss:.4f}")
print(f"Exactitude (test) : {accuracy * 100:.2f}%")
# 7. Prédictions (sur quelques exemples)
predictions = model.predict(x_test[:5])
print(f"Prédictions :\n{predictions}")
predicted_classes = (predictions > 0.5).astype(np.int32)
print(f"Classes prédites :\n{predicted_classes}")
print(f"Vraies classes :\n{y_test[:5].reshape(-1, 1)}") # Pour la comparaison
Explications Détaillées
-
Génération des données :
generate_binary_sequences(): Crée des séquences aléatoires de 0 et de 1, et détermine l'étiquette (1 si plus de 1 que de 0, 0 sinon).num_sequences: Le nombre de séquences à générer.sequence_length: La longueur de chaque séquence.split_ratio: On divise les données en un ensemble d'entraînement (80%) et un ensemble de test (20%).- Ajout d'une dimension : Les RNNs en Keras attendent des entrées de forme
(batch_size, timesteps, features). Ici :batch_size: Le nombre de séquences dans un batch (sera déterminé lors de l'entraînement).timesteps: La longueur de la séquence (ici, 10).features: Le nombre de caractéristiques pour chaque pas de temps. Ici, nous n'avons qu'une seule caractéristique (0 ou 1), donc nous ajoutons une dimension avecnp.newaxispour que la forme soit (800, 10, 1) au lieu de (800, 10).
-
Construction du modèle :
tf.keras.layers.SimpleRNN(units=32, input_shape=(None, 1)):SimpleRNN: Nous utilisons la couche RNN la plus simple de Keras.units=32: Nous choisissons 32 unités (la dimensionnalité de l'état caché).input_shape=(None, 1):None: Indique que la longueur de la séquence peut être variable (même si, dans cet exemple, elle est fixe).1: Indique qu'il y a une seule caractéristique par pas de temps.
tf.keras.layers.Dense(units=1, activation='sigmoid'): Couche de sortie pour la classification binaire.
-
Compilation :
optimizer='adam': Optimiseur Adam.loss='binary_crossentropy': Perte d'entropie croisée binaire (adaptée à la classification binaire).metrics=['accuracy']: Exactitude.
-
Entraînement :
model.fit(...): Entraînement du modèle. Nous utilisonsvalidation_split=0.2pour utiliser 20% des données d'entraînement comme ensemble de validation.
-
Visualisation : Courbes de perte et d'exactitude.
-
Évaluation : Évaluation sur l'ensemble de test.
-
Prédictions : Prédictions sur quelques exemples et comparaison avec les vraies valeurs.
Classes prédites :
[[0]
[1]
[0]
[0]
[0]]
Vraies classes :
[[0]
[1]
[0]
[0]
[0]]
Ce code montre comment implémenter un RNN simple avec Keras. Bien que SimpleRNN ne soit pas l'architecture la plus performante en pratique, elle illustre les concepts fondamentaux des RNNs. Pour des problèmes plus complexes, vous utiliseriez des couches LSTM ou GRU. N'hésitez pas à modifier les paramètres pour observer leurs effets.
Vidéo RNN
Exercice : Prédiction de la Prochaine Valeur d'une Série Temporelle avec un RNN (Keras)
Dans cet exercice, vous allez utiliser un RNN (avec une couche LSTM ou GRU, à vous de choisir) pour prédire la prochaine valeur d'une série temporelle simple.
Problème
Nous allons générer une série temporelle sinusoïdale, et l'objectif sera de prédire la valeur suivante dans la séquence, étant donné un certain nombre de valeurs passées.
Ce que Vous Devez Faire
-
Génération des données :
- Voici la fonction
generate_time_series(batch_size, n_steps)qui génère des séries temporelles sinusoïdales. La fonction doit :- Générer
batch_sizeséries temporelles. - Chaque série temporelle aura
n_steps + 1pas de temps (valeurs). - La série temporelle sera de la forme :
sin(t * facteur + phase), oùtvarie de 0 àn_steps,facteurest un nombre aléatoire entre 0.5 et 1, etphaseest un nombre aléatoire entre 0 et . - Ajoutez un peu de bruit gaussien à la série temporelle.
- Retournez deux tableaux NumPy :
X: Un tableau de forme(batch_size, n_steps, 1)contenant lesn_stepspremières valeurs de chaque série temporelle.Y: Un tableau de forme(batch_size, 1)contenant la valeur suivante (n_steps + 1) de chaque série temporelle.
- Générer
import numpy as np
def generate_time_series(batch_size, n_steps):
freq1, freq2, offsets1, offsets2 = np.random.rand(4, batch_size, 1)
time = np.linspace(0, 1, n_steps + 1)
series = 0.5 * np.sin((time - offsets1) * (freq1 * 10 + 10)) # wave 1
series += 0.2 * np.sin((time - offsets2) * (freq2 * 20 + 20)) # + wave 2
series += 0.1 * (np.random.rand(batch_size, n_steps + 1) - 0.5) # + noise
return series[..., np.newaxis].astype(np.float32) - Voici la fonction
-
Création des ensembles d'entraînement, de validation et de test :
- Utilisez la fonction
generate_time_seriespour générer les données. - Divisez les données en ensembles d'entraînement, de validation et de test (par exemple, 70%/15%/15%).
-
Construction du modèle RNN :
- Créez un modèle Keras
Sequential. - Utilisez une couche
LSTMouGRU(à vous de choisir !).units: Choisissez un nombre d'unités (par exemple, 32, 64...).input_shape:(n_steps, 1)(car chaque pas de temps est représenté par une seule valeur, et nous avonsn_stepspas de temps).return_sequences: Réfléchissez : Avez-vous besoin deTrueouFalseici ? Pourquoi ?
- Ajoutez une couche
Densede sortie avec 1 neurone (car nous voulons prédire une seule valeur) et pas de fonction d'activation (car c'est un problème de régression).
- Créez un modèle Keras
-
Compilation du modèle :
- Optimiseur :
Adam(ou un autre optimiseur de votre choix). - Fonction de perte :
mse(erreur quadratique moyenne, car c'est un problème de régression). - Métrique :
mae(erreur absolue moyenne).
- Optimiseur :
-
Entraînement du modèle :
- Utilisez
model.fit(). - Choisissez un nombre d'époques et une taille de batch.
- Utilisez votre ensemble de validation.
- Ajoutez des callbacks (
EarlyStopping).
- Utilisez
-
Évaluation du modèle :
- Évaluez le modèle sur l'ensemble de test (
model.evaluate()). - Affichez la perte et la MAE.
- Évaluez le modèle sur l'ensemble de test (
-
Visualisation :
- Tracez les courbes de perte et de MAE (entraînement et validation).
- Choisissez quelques séries temporelles de l'ensemble de test, et affichez :
- Les
n_stepspremières valeurs (l'entrée du modèle). - La vraie valeur suivante (
n_steps + 1). - La valeur prédite par le modèle.
- Les
Conseils
- Commencez avec un modèle simple (par exemple, une seule couche LSTM/GRU).
- Expérimentez avec :
- Le nombre d'unités dans la couche LSTM/GRU.
- L'ajout de couches LSTM/GRU supplémentaires.
- L'ajout de couches
Denseaprès la couche LSTM/GRU. - Le taux d'apprentissage.
- La taille du batch.
- Le nombre d'époques.
- Le nombre de pas de temps (
n_steps).
L'objectif de cet exercice est de vous familiariser avec l'utilisation des RNNs (LSTM ou GRU) pour traiter des séries temporelles. La prédiction de la prochaine valeur d'une sinusoïde bruitée est un problème simple, mais il illustre les principes de base.
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
def generate_time_series(batch_size, n_steps):
freq1, freq2, offsets1, offsets2 = np.random.rand(4, batch_size, 1)
time = np.linspace(0, 1, n_steps + 1)
series = 0.5 * np.sin((time - offsets1) * (freq1 * 10 + 10)) # wave 1
series += 0.2 * np.sin((time - offsets2) * (freq2 * 20 + 20)) # + wave 2
series += 0.1 * (np.random.rand(batch_size, n_steps + 1) - 0.5) # + noise
return series[..., np.newaxis].astype(np.float32)
n_steps = 50
n_samples = 1000
series = generate_time_series(n_samples, n_steps + 1)
# 2. Création des ensembles
split_ratio1 = 0.7
split_ratio2 = 0.85
train_size = int(n_samples * split_ratio1)
val_size = int(n_samples*split_ratio2) - train_size
X_train, Y_train = series[:train_size, :n_steps], series[:train_size, -1]
X_valid, Y_valid = series[train_size:train_size+val_size, :n_steps], series[train_size:train_size+val_size, -1]
X_test, Y_test = series[train_size+val_size:, :n_steps], series[train_size+val_size:, -1]
# 3. Modèle
model = tf.keras.models.Sequential([
tf.keras.layers.LSTM(20, return_sequences=False, input_shape=[None, 1]),
tf.keras.layers.Dense(1)
])
# 4. Compilation
model.compile(loss="mse", optimizer="adam", metrics=['mae'])
# 5. Callbacks
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
mode='min',
restore_best_weights=True
)
# 6. Entraînement
history = model.fit(X_train, Y_train, epochs=100,
validation_data=(X_valid, Y_valid),
callbacks=[early_stopping_callback])
# 7. Visualisation
plt.plot(history.history['loss'], label="Train Loss")
plt.plot(history.history['val_loss'], label="Validation Loss")
plt.legend()
plt.show()
plt.plot(history.history['mae'], label="Train MAE")
plt.plot(history.history['val_mae'], label="Validation MAE")
plt.legend()
plt.show()
# 8. Évaluation
loss, mae = model.evaluate(X_test, Y_test, verbose=0)
print(f"Loss: {loss}, MAE: {mae}")
# 9. Prédictions et visualisation
X_new = generate_time_series(10, n_steps + 1, seed=43)
Y_pred = model.predict(X_new[:, :n_steps][..., np.newaxis])
plt.figure(figsize=(12, 6))
for i in range(10):
plt.subplot(2, 5, i + 1)
plt.plot(np.arange(n_steps), X_new[i, :n_steps, 0], ".-b", label="Input sequence")
plt.plot(n_steps, Y_pred[i, 0], "xr", markersize=10, label="Prediction")
plt.legend()
plt.title(f"Sample {i}")
plt.show()