NLP & fastai | RNN

Pierre Guillou
12 min readNov 11, 2019

--

Image Captioning avec un RNN (Image source: Deep Visual-Semantic Alignments for Generating Image Descriptions)

Ce post concerne les vidéos 10 et 11 du cours fastai de Rachel Thomas sur NLP (A code-first introduction to NLP) ainsi que les vidéos 6 (notes, 2018), 7 (notes, 2018), 11 (notes, 2018), 7 (notes, 2019) et 12 (notes, 2019) du cours de Jeremy Howard (Introduction to Machine Learning for Coders). Son objectif est d’expliquer les concepts clés d’un modèle Recurrent Neural Network (RNN) en NLP présentés dans les slides RNNs.pptx, dans ces vidéos et leurs notebooks associés: lesson6-rnn.ipynb (Jeremy 2018), lesson7-human-numbers.ipynb (Jeremy 2019), 6-rnn-english-numbers.ipynb (Rachel 2019).

Autres posts de la série NLP & fastai: Topic Modeling | Sentiment Classification | Language Model | Transfer Learning | ULMFiT | MultiFiT | French Language Model | Portuguese Language Model | LSTM & GRU | SentencePiece | Sequence-to-Sequence Model (seq2seq) | Attention Mechanism | Transformer Model | GPT-2

Motivation

Les cours sur les modèles d’Intelligence Artificielle et en particulier de Deep Learning débutent le plus souvent avec les modèles de convolution (ConvNet) et leur application la plus classique, la classification d’images.

Appliquées à ces modèles ConvNet, les méthodes modernes d’entraînement (Transfer Learning, Data Augmentation, Cyclical Cosine Annealing Learning Rate avec Discriminative Fine-tuning, régularisation, momentum, Optimizer Adam, weight decay, etc.) permettent d’atteindre des taux de performance proches de 100% sur des données avec corrélation spatiale (à majorité des images). Incroyable mais…

… ces modèles ConvNet ne s’appliquent pas à tous les types de données, en particulier celles avec une corrélation temporelle ou un ordre de lecture imposé. En effet, un modèle ConvNet a besoin d’avoir en entrée des données (images) de taille identique, ce qui ne s’applique pas au traitement de phrases dont la taille en terme de nombre de caractères par exemple n’est pas constante. Par ailleurs, un modèle ConvNet n’a pas besoin de se souvenir d’une donnée pour traiter une autre (le modèle est dit sans mémoire à long terme ou même plus simplement, sans mémoire) alors que les modèles de traduction par exemple ont besoin d’une telle mémoire. Enfin, un ConvNet considère que les données sont indépendantes les unes des autres (comme une série d’images à classifier) alors que l’ordre temporel ainsi que les relations entre les données peuvent avoir une importance comme dans le cas du langage naturel.

A la différence des modèles ConvNet, les modèles Recurrent Neural Network (RNN) inventés au début des années 80 permettent de traiter ce type de données avec corrélation temporelle ou un ordre de lecture imposé dont nous rappelons ci-après les 4 grandes caractéristiques:

  • Variable length sequence: un RNN ne demande pas à avoir en entrée des séquences de taille constante.
  • Long-term dependency: un RNN détecte et codifie les relations entre les tokens d’une séquence depuis le premier.
  • Stateful representation: un RNN crée un vecteur d’état caché (hidden state vector ou hidden state) qui est représentatif de la séquence (en fait, un RNN produit une liste de vecteurs d’état caché dont le nombre correspond au nombre de tokens de la séquence).
  • Memory: un RNN a une mémoire qui correspond au BPTT tokens déjà utilisés pour créer le vecteur d’état caché principal (BPTT — BackPropagation Through Time — est un nombre de tokens).
Pourquoi avons-nous besoin des RNNs? (Source image: fastai lesson 6: Deep Learning 2018)

Applications d’un RNN

Prédiction de séquences

(sources de ce paragraphe: “When to Use MLP, CNN, and RNN Neural Networks”, “Gentle Introduction to Models for Sequence Prediction with RNNs”)

Les modèles RNN ont été conçus pour traiter les problèmes de prédiction de séquences (souvent des séquences de textes mais pas nécessairement puisque un RNN opère sur des vecteurs: il est tout à fait possible d’utiliser un RNN pour traiter des séquences d’images) qui se caractérisent par leurs types d’entrées et de sorties. Voici quelques exemples de problèmes de prédiction de séquence:

  • One-to-Many: une observation en entrée mappée vers une séquence en sortie avec plusieurs composantes de prédiction. Par exemple, une image en entrée à qui le modèle associe une légende textuelle (Image Captioning — Demo: NeuralTalk Sentence Generation Results).
Image Captioning (Image source: Fundamentals of Deep Learning — Introduction to Recurrent Neural Networks)
  • Many-to-One: une séquence à plusieurs composantes en entrée mappée vers une prédiction de classe ou de quantité. Par exemple, un modèle de classification de sentiments (ou de catégories) de textes mis en entrée (Sentiment/Category Classification) ou de prévision du prochain mot (ex: SwiftKey).
Sentiment Classification (Image source: Fundamentals of Deep Learning — Introduction to Recurrent Neural Networks)
  • Many-to-Many: une séquence à plusieurs composantes en entrée mappée vers une séquence en sortie avec plusieurs composantes de prédiction. Par exemple, un modèle de traduction qui en entrée accepte une phrase avec plusieurs mots et qui donne en sortie une phrase dans une autre langue (Machine Translation) ou un modèle de lecture de gauche à droite de nombres qui produit en sortie une animation temporelle de coloriage des nombres lus
Machine Translation (Image source: Fundamentals of Deep Learning — Introduction to Recurrent Neural Networks)
A gauche, un RNN qui apprend à lire des numéro de gauche à droite. A droite, un RNN qui apprend à restituer le nom lu via une animation temporelle de coloriage. Images source: Multiple Object Recognition with Visual Attention (DeepMind)
  • Synchronized Many-to-Many: en plus du fonctionnement des modèles Many-to-Many, les séquences d’entrée et de sortie sont synchronisées. Par exemple, un modèle de classification de video (Video Classification).

Le problème Many-to-Many est souvent appelé séquence-à-séquence, ou seq2seq en abrégé.

Langage naturel

Les RNN en général et les LSTM en particulier sont très utilisés pour les séquences de mots/paragraphes sous forme textuelle ou vocale, généralement appelés traitements de langage naturel (NLP: Natural Language Processing).

Cela inclut à la fois des séquences de texte et des séquences de langage parlé représentées sous forme de série chronologique. Voici quelques exemples possibles de Sequence Labelling Task:

  • Recherche d’entités
  • Effacement de mots ou expressions non désirés
  • Etiquetage MultiLabel d’un texte

Ils sont également utilisés comme modèles génératifs nécessitant une sortie de séquence, non seulement avec du texte, mais également dans des applications telles que la génération d’écriture (manuscrite ou non), de titres, de résumés, de réponses à des questions, etc.

Fonctionnement et code PyTorch d’un RNN

Un modèle RNN est de fait un Fully Connected Network. Le notebook lesson7-human-numbers.ipynb permet de comprendre son fonctionnement à partir d’un exemple: entraîner un modèle de langage à prédire le prochain chiffre à partir des 3 précédents.

Le dataset utilisé s’appelle HUMAN_NUMBERS: le training dataset contient les chiffres de 1 à 7999 (80% des données) et le valid dataset contient les chiffres qui vont de 8000 à 9999 (20% des données). Dans chacun des 2 datasets, les chiffres sont écrits en lettres et séparés par des virgules.

Initialisation

from fastai.text import *bs=64path = untar_data(URLs.HUMAN_NUMBERS)def readnums(d): return [', '.join(o.strip() for o in open(path/d).readlines())]

Données

Les fichiers train.txt and valid.txt sont transformés en une séquence de nombres écrits en anglais:

train_txt = readnums('train.txt')
valid_txt = readnums('valid.txt')

Databunch

Le Databunch de la bibliothèque fastai se construit ici en 2 étapes: tout d’abord, la création de 2 objets TextList (train et valid), puis l’utilisation de ces 2 objets pour créer un Databunch d’un modèle de langage (via la fonction label_for_lm())à partir de la classe ItemLists:

train = TextList(train_txt, path=path)
valid = TextList(valid_txt, path=path)
src = ItemLists(path=path, train=train, valid=valid).label_for_lm()
data = src.databunch(bs=bs)

Le code ci-après permet de vérifier la valeur du BPTT (BackPropagation Through Time) qui représente le nombre de tokens dont le modèle doit se souvenir, le nombre de batch dans le valid dataset, le nombre de tokens dans le valid dataset.

data.bptt, len(data.valid_dl), len(data.valid_ds[0][0].data)# 70, 3, 13017# On retrouve le fait qu'il y a 3 batches par le calcul suivant
# La valeur obtenue n'est pas exactement 3 car le nombre de tokens n'est pas divisible par bs x bptt
13017 / bs / bptt

Le schéma ci-dessous explique comme sont obtenus les batches. La séquence unique d’entrée (du dataset train par exemple) est divisée en 64 blocs (chunks) de même taille (bs = 64 ici). Ces 64 blocs sont alors mis en parallèle tout en gardant leur ordre de découpage. L’hyperparamètre BPTT (70 par défaut) indique alors que les 70 premiers tokens de chacun des 64 blocs vont être passés en parallèle au modèle pour l’entraîner (pas seulement les chiffres mais aussi les virgules et les tokens spéciaux xxunk, xxpad, xxbos, xxeos, xxfld, xxmaj, xxup, xxrep, xxwrep). Au batch suivant, ce seront les 70 tokens suivants pour chaque bloc qui seront donnés au modèle et ainsi de suite.

Comprendre ce que signifie un batch dans un Databunch fastai de NLP (Image source: Deep Learning 2: Part 1 Lesson 7)

Modèle “Single Fully Connected”

Le notebook montre progressivement comment arriver à utiliser PyTorch pour coder un modèle RNN.

Tout d’abord, afin de simplifier les dessins, le BPTT est réduit à 3 (il y aura donc 64 batches dans le cas de notre valid dataset). Cela signifie que nous allons utiliser 3 tokens afin de prédire le quatrième. Le principe présenté dans le schéma ci-après est le suivant:

  1. Vecteurs d’embeddings des premiers tokens — Les premiers tokens de chacun des 64 blocs du premier batch sont transformés en vecteurs d’embeddings par la multiplication par une matrice VERTE suivie d’un ReLU (flèche verte: les paramètres de cette matrice ainsi que ceux des autres sont appris par l’entraînement du modèle).
  2. Vecteurs d’état caché des premiers tokens — Les 64 vecteurs d’embeddings sont transformés en vecteurs d’état caché par la multiplication par une matrice ORANGE suivie d’un ReLU (flèche orange).
  3. Calcul des nouveaux vecteurs d’état caché suite à l’arrivée des seconds tokens— Les seconds tokens de chacun des 64 blocs du premier batch sont transformés en vecteurs d’embeddings par la même matrice VERTE, vecteurs qui sont ensuite additionnés aux vecteurs d’état caché correspondants calculés précédemment. La matrice ORANGE transforme alors les vecteurs résultants en nouveaux vecteurs d’état caché.
  4. Calcul des nouveaux vecteurs d’état caché suite à l’arrivée des troisièmes tokens — Exactement la même opération est effectuée à partir des troisièmes tokens de chacun des 64 blocs du premier batch afin d’obtenir 64 vecteurs d’état caché qui représentent chacun une séquence de 3 tokens.
  5. Prévision des 64 quatrième tokens — La matrice BLEUE vient alors transformer ces 64 vecteurs d’état caché en vecteurs de sortie qui permettront ensuite grâce à un softmax d’obtenir une série de probabilités (une probabilité par token du vocab), ce qui permettra de prédire le quatrième token pour chacun des 64 blocs en prenant les probabilités les plus hautes.

Ces 5 opérations se répètent alors jusqu’à la fin du premier batch, puis pour les 63 batches suivants.

Un RNN déployé pour prédire le 4ième token à partir des 3 précédents de la séquence (Image source: RNNs.pptx)

Voici le code du modèle 0 qui correspond à ce schéma de fonctionnement:

Code du modèle 0 (code source: lesson7-human-numbers.ipynb)

Le même modèle mais avec une boucle

L’intérêt d’utiliser à présent une boucle dans la méthode forward() est que le code n’est plus dépendant du nombre de caractères utilisés avant la prédiction (BPTT). Nous pouvons ainsi utiliser le même code quelque soit la valeur du BPTT.

Le même modèle mais avec une boucle (Image source: RNNs.pptx)
Code du modèle 1 (code source: lesson7-human-numbers.ipynb)

Modèle “Multi Fully Connected”

Dans les 2 modèles précédents, une prévision de token était faite uniquement 1 fois par batch (après BPTT tokens). En émettant une prévision après chaque nouveau token, on profite davantage de nos données d’entraînement, ce qui est symbolisé par le graphe suivant:

Un modèle avec une prévision après chaque nouveau token (Image source: RNNs.pptx)
Code du modèle 2 (code source: lesson7-human-numbers.ipynb)

Nous remarquons que la performance du modèle diminue. En effet, dans le code du modèle 2, le vecteur d’état caché utilisé pour donner infine les probabilités du token à prédire (après multiplication par la matrice BLEUE puis l’application d’un softmax) est remis à zéro au début de chaque nouveau batch. Ainsi, tout l’historique du batch précédent est effacé ce qui rend difficile la prévision des tokens suivants.

Maintenir le vecteur d’état caché

Afin de garder le vecteur d’état caché sur l’ensemble d’une époque, il n’est plus initialisé dans la méthode forward() dans le code du modèle 3 mais dans __init__(). En revanche, il est détaché à la fin de chaque batch pour éviter de devoir backpropagager le gradient de la fonction d’erreur sur l’ensemble des tokens d’un bloc mais seulement sur un nombre BPTT de tokens.

Code du modèle 3 (code source: lesson7-human-numbers.ipynb)

Modèle nn.RNN() de PyTorch

nn.RNN() de PyTorch implémente au niveau de CUDA (au niveau du GPU) la boucle du modèle 3, ce qui permet au modèle 4 de tourner bien plus vite. De plus, nn.RNN() renvoie non plus seulement la liste des différents vecteurs d’état caché mais aussi la liste de toutes les sorties. En revanche, nn.RNN() ne contient pas de BatchNorm. Il faut donc l’appliquer sur la liste des sorties issues de nn.RNN().

RNN (Image source: Fundamentals of Deep Learning — Introduction to Recurrent Neural Networks)
Code du modèle 4 (code source: lesson7-human-numbers.ipynb)

Modèle avec 2 couches de GRU

Lorsque vous avez des échelles de temps longues et des réseaux plus profonds, ceux-ci deviennent impossibles à entraîner. Une façon de résoudre ce problème consiste à ajouter un mini-NN pour décider de la quantité de flèche verte et de la flèche orange à conserver. Ces mini-NN peuvent être des LSTM (Long Short Term Memory) ou GRU (Gated Recurrent Units).

L’utilisation de 2 couches de GRU au lieu d’1 couche de RNN permet de passer de 60% de performance à 80%.

Modèle avec 2 couches de GRU (Image source: RNNs.pptx)

Voici le code du modèle 5:

Code du modèle 5 (code source: lesson7-human-numbers.ipynb)

Annexe

Initialisation d’un RNN

Cette technique d’initalisation des paramètres d’un RNN a été introduitz par Geoffrey Hinton et. Al. en 2015 (A Simple Way to Initialize Recurrent Networks of Rectified Linear Units) après que les modèles RNN existent depuis des décennies.

Perte de gradient d’un RNN (The Vanishing Gradient Problem)

Comme pour tous les Réseaux Neuronaux, la mise à jour des valeurs des paramètres d’un RNN se fait via l’algorithme d’apprentissage à partir du gradient de l’erreur appelé BackPropagation.

Comme toutes les couches d’un RNN sont identiques (il s’agit en fait d’un Fully Connnected Layer a BPTT couches, avec BPTT — BackPropagation Through Time — qui le nombre de tokens de la séquence dont le modèle a gardé la mémoire), chaque paramètre d’un RNN est mis à jour BPTT fois lors de la backpropagation du gradient de l’erreur, cad qu’il est multiplié BPTT fois par un nombre inférieur à un comme le Learning Rate et le gradient de l’erreur. Il finit par conséquent par converger vers zéro: il y a alors perte de gradient et il n’est plus possible de mettre à jour les paramètres du RNN (The Vanishing Gradient Problem).

Dans ce cas, le modèle n’apprend plus. Pour solutionner ce problème, il faut soit diminuer le BPTT (ce qui est dommage), soit utiliser des architectures de RNN un peu plus complexes comme LSTM (Long Short Term Memory) ou GRU (Gated Recurrent Units) dont l’architecture permet de garder à chaque cellule une partie du vecteur d’état précédent (et donc du vecteur d’état initial).

Sources:

Autres ressources sur RNN

À propos de l’auteur: Pierre Guillou est consultant en Intelligence Artificielle au Brésil et en France. Merci de le contacter via son profil Linkedin.

--

--

Pierre Guillou

AI, Generative AI, Deep learning, NLP models author | Europe (Paris, Bruxelles, Liège) & Brazil