Les réseaux de neurones sont devenus l'outil de référence pour de nombreuses applications d'apprentissage automatique, notamment dans la vision par ordinateur (classification d'images, détection d'objets, segmentation) et le traitement automatique du langage (traduction automatique, reconnaissance vocale, modèles de langage). Cet article vise à expliquer en détail le fonctionnement interne des réseaux de neurones récurrents (RNN), en particulier les couches denses et les variantes LSTM, à l'aide d'exemples concrets et d'extraits de code Keras.
Introduction au Deep Learning et aux Réseaux de Neurones
Le deep learning est une branche du machine learning qui utilise des réseaux de neurones profonds pour résoudre des problèmes complexes nécessitant un haut niveau d'abstraction. Contrairement aux méthodes traditionnelles de machine learning qui nécessitent une ingénierie manuelle des caractéristiques (feature engineering), le deep learning peut apprendre directement à partir des données brutes.
Machine Learning : Apprentissage Automatisé
Le machine learning est un ensemble de techniques permettant à un ordinateur d'apprendre à partir de données sans être explicitement programmé pour chaque tâche. Il permet de retirer de l'information de grands ensembles de données. Le deep learning est une des techniques que l’on peut utiliser pour arriver à ce type de résultat.
Deep Learning : Abstraction et Données Continues
Le deep learning fonctionne très bien pour des données "continues" comme des images, du son. Il permet de résoudre des problèmes en utilisant un grand niveau d’abstraction.
Exemples Concrets d'Applications
Le deep learning est utilisé dans de nombreuses applications concrètes, telles que la reconnaissance de chiffres manuscrits (utilisée par les banques pour la lecture automatique des chèques), la reconnaissance faciale, la traduction automatique et la conduite autonome.
Lire aussi: Choisir les meilleures couches de piscine pour bébés
Anatomie d'un Réseau de Neurones Récurrent (RNN)
Architecture Globale d'un RNN
Les RNN sont particulièrement adaptés aux données séquentielles, car ils peuvent tenir compte de l'ordre des informations. Pour illustrer le fonctionnement d'un RNN, nous prendrons l'exemple de la génération de texte caractère par caractère.
Génération de Texte Caractère par Caractère
Afin de générer du texte, nous partons d'une séquence de caractères de taille fixe pour prédire le caractère suivant.
Pré-traitement des Données
Le pré-traitement permet de représenter numériquement le texte brut, le transformant en une matrice dont les dimensions sont nbéchantillons (N) * tailleséquence (T) * nb_variables (M) où :
- nb_échantillons (N) = nombre de séquences d'entraînement (taille du dataset)
- taille_séquence (T) = nombre (fixe) de caractères par séquence (dimension temporelle).
- nb_variables (M) = taille de la représentation vectorielle de chaque caractère.
Par exemple, on peut limiter le vocabulaire aux 26 lettres de l'alphabet + 4 caractères spéciaux. Chaque caractère est alors représenté par un vecteur de taille M=30 grâce au one-hot-encoding.
En résumé, le modèle prend en entrée un tableau de N séquences, chacune de longueur T caractères, où chaque caractère est un vecteur numérique de taille M.
Lire aussi: Comprendre les pleurs nocturnes de bébé
Couche RNN
Une couche de type RNN prend en entrée une séquence de ce tableau (donc une matrice de taille (T x M)) et retourne en sortie un vecteur de taille R qui est une représentation compressée de la totalité de la séquence de caractères. R est un hyperparamètre à choisir judicieusement. C'est en quelque sorte l'équivalent du nombre de neurones dans une couche dense (ou "fully-connected").
Couche Dense et Fonction d'Activation Softmax
Générer le caractère suivant consiste à choisir le caractère le plus pertinent parmi les M caractères possibles. Il faut donc que notre modèle produise une sortie de taille M. Ce vecteur de sortie associe alors à chaque caractère une probabilité d'être le suivant dans la séquence, étant donné les T caractères précédents.
Pour obtenir cette distribution de probabilité, on empile une couche de neurones "classique" (appelée également couche dense) de taille M à la suite de la couche RNN et on utilise une fonction d'activation softmax.
Fonctionnement Interne d'une Couche RNN
Une couche RNN est une succession de T cellules. Chaque cellule a deux entrées :
- l'élément de la séquence lui correspondant (en version one-hot) : la tème cellule est associée au tème caractère de la séquence
- le vecteur en sortie de la cellule précédente. La première cellule, qui n'a pas d'antécédent, prend alors un vecteur initialisé aléatoirement
Dans une couche RNN, on parcourt donc successivement les entrées x1 à xT. À l'instant t, la tème cellule combine l'entrée courante xt avec la prédiction au pas précédent h~t-1 ~ pour calculer une sortie ht de taille R.
Lire aussi: Guide pour une Couche Confortable
Le dernier vecteur calculé h~T ~ (qui est de taille R) est la sortie finale de la couche RNN. Une couche RNN définit donc une relation de récurrence de la forme :
ht = f(xt, ht-1)
Comportement d'une Cellule RNN
La tème cellule n'est rien d'autre qu'une couche dense de taille R, dont l'entrée est la concaténation de xt (de taille M) et ht-1 (de taille R). La fonction d'activation classique utilisée pour les cellules du RNN est la tangente hyperbolique tanh. Plusieurs propriétés intéressantes expliquent ce choix, notamment une convergence plus rapide que la sigmoïde.
La formule devient donc:
ht = tanh( WT * concat (xt, ht-1) + b )
où W et b sont les poids appris par le modèle :
- W est une matrice de taille (R + M) x R
- b un vecteur de taille R
Il est à noter que les poids sont partagés entre toutes les cellules d'une couche RNN. Autrement dit, c'est exactement la même fonction (avec les mêmes poids) qui est appliquée à chaque pas de temps t. Cela permet à la fois de modéliser toute la séquence de façon homogène et d'éviter de multiplier le nombre de poids à apprendre.
Selon les implémentations, il est possible que la matrice W soit séparée en deux parties :
- Whh (correspondant aux connexions entre ht-1 et ht)
- Wxh (correspondant aux connexions entre xt et ht).
Exemple avec Keras
Le code ci-dessous permet de définir un modèle simple à 1 couche RNN, où R=16, qui prend en entrée des séquences de taille T=10 caractères, chacun encodé comme un vecteur de taille M=30.
from keras.models import Sequentialfrom keras.layers import SimpleRNNmodel = Sequential()model.add(SimpleRNN(units=16, input_shape=(10, 30), use_bias=False))print(model.summary())On voit alors que notre modèle a 736 paramètres (ou poids) à apprendre.
Les formes des matrices de poids apprises par le modèle peuvent être récupérées :
print([weight_matrix.shape for weight_matrix in model.get_weights()])Dans ce cas, on constate que le modèle apprend 2 matrices de poids:
- une matrice de taille (30 x 16) = (M x R) qui correspond à Wxh
- une matrice de taille (16 x 16) = (R x R) qui correspond à Whh
Empiler des Couches Récurrentes
Souvent, une seule couche RNN ne suffit pas à capter toute l'information contenue dans les séquences. On peut alors empiler les couches de type RNN afin d'avoir un réseau plus profond, comme on le fait couramment avec des couches denses ou convolutives. Cela permet d'extraire des informations plus complexes à partir des entrées, et ainsi d'avoir une meilleure modélisation de nos données.
Dans la plupart des cas, 2 ou 5 couches récurrentes suffisent; au-delà, la convergence devient difficile. Cette "faible" profondeur est en réalité trompeuse, car les couches RNN sont en quelque sorte profondes par construction, vu qu'une couche unique traverse itérativement de longues séquences.
Pour empiler les couches, il faut donner en entrée de la couche 2 la séquence des sorties intermédiaires h11, h12, …, h1T de la couche 1, et pas seulement la sortie finale.
model = Sequential()model.add(SimpleRNN(units=16, input_shape=(10,30), return_sequences=True))model.add(SimpleRNN(units=4))On peut remarquer dans le code ci-dessus que l'hyperparamètre "units", qu'on a noté R dans notre article, peut être différent d'une couche à l'autre.
Limites des RNN Simples
Bien qu'ils soient construits spécifiquement pour gérer des séquences, les RNN simples ont certaines limites.
Modèle à Mémoire Courte
Une couche RNN est une succession de cellules, chacune prenant en entrée la représentation du caractère courant ainsi que la sortie de la cellule précédente. Ces données d'entrée sont transformées, en passant notamment par une fonction tangente hyperbolique.
La tangente hyperbolique a tendance à écraser les valeurs qu'elle prend en entrée. Ainsi, après un premier passage par la tangente hyperbolique, on obtient une valeur entre -1 et 1. Ensuite, comme tanh(1) = 0.76 et tanh(-1) = -0.76, un deuxième passage par la fonction tanh résulte en un intervalle de valeurs encore plus réduit, et ainsi de suite.
Les informations provenant de x1 passent T fois à travers une tangente hyperbolique, là où celles provenant de xT subissent cette transformation une seule fois. Même si x1 est cruciale pour la tâche de prédiction, son influence est mécaniquement réduite par les passages successifs par la tangente hyperbolique.
Ce problème devient très embêtant lorsqu'on utilise des séquences plus longues, il est donc très compliqué pour un RNN basique de générer un texte cohérent dans la longueur.
Modèle Difficile à Entraîner : Vanishing Gradient
L'une des principales difficultés pour entraîner des réseaux de type RNN est le problème du vanishing gradient.
Un réseau de neurones est un enchaînement de fonctions simples, prenant en entrée les données de notre problème X. L'apprentissage consiste à ajuster les poids W afin de minimiser une erreur, donnée par une fonction de coût L. Cette erreur mesure l'écart entre les labels réels y et les labels prédits ŷ.
L'entraînement a lieu comme il suit :
- Les poids W sont initialisés aléatoirement
- On fait passer Xtrain par notre modèle pour obtenir une prédiction ŷtrain. On calcule ensuite la valeur de la fonction de coût L(ytrain, ŷtrain)
- On calcule le gradient de cette fonction L par rapport aux paramètres W
- On cherche à minimiser notre fonction de coût en mettant à jour les paramètres W. Pour cela, on effectue une descente de gradient grâce au gradient calculé à l'étape précédente : W = W - λ grad L
Un réseau de neurones est une suite de fonctions (couches) appliquées successivement. Calculer son gradient revient donc à dériver des fonctions composées.
Dans le cas d'une couche RNN, le gradient de h3 par rapport à W est proportionnel à la multiplication de dérivées de la fonction tangente hyperbolique. Or, la dérivée de cette fonction se situe dans l'intervalle [0, 1] et prend presque sûrement des valeurs dans [0, 1[.
Plus on multiplie des valeurs entre 0 et 1 entre elles, plus le résultat se rapproche de 0. dL/dW prend donc des valeurs très petites lorsque les séquences sont longues. La mise à jour des paramètres devient donc très lente et l'entraînement du modèle est mis à mal.
Le LSTM : un RNN Amélioré
Plusieurs variantes aux RNN standards ont vu le jour pour remédier aux problèmes évoqués précédemment. Nous allons ici décrire les LSTM, pour Long Short-Term Memory. Ce type de RNN est très utilisé en traitement du langage naturel.
Intuition derrière l'Architecture LSTM
L'idée derrière ce choix d'architecture de réseaux de neurones est de diviser le signal entre ce qui est important à court terme à travers le hidden state (analogue à la sortie d'une cellule de RNN simple), et ce qui l'est à long terme, à travers le cell state. Ainsi, le fonctionnement global d'un LSTM peut se résumer en 3 étapes :
- Détecter les informations pertinentes venant du passé, piochées dans le cell state à travers la forget gate ;
- Choisir, à partir de l'entrée courante, celles qui seront pertinentes à long terme, via l'input gate.
Utilisation de Keras
Keras est une API de haut niveau pour construire et entraîner des modèles de deep learning. Il facilite la création de réseaux de neurones en fournissant des abstractions simples et intuitives.
Modèles Séquentiels
Les modèles séquentiels sont une façon simple de construire des réseaux de neurones en empilant des couches les unes après les autres.
Configuration des Couches
Lors de la création d'un modèle séquentiel, il est important de configurer correctement les couches, en particulier la première couche, qui doit recevoir les caractéristiques de la forme d'entrée (input_shape).
Compilation du Modèle
Avant d'entraîner un modèle Keras, il est nécessaire de le compiler en spécifiant la fonction de coût (loss), l'optimiseur et les métriques à suivre.
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])Entraînement du Modèle
Les modèles Keras sont entraînés sur des tableaux Numpy d'entrées et de labels.
model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))
tags: #couche #dense #keras #explication
