NLP & fastai | Transfer Learning

Pierre Guillou
13 min readSep 11, 2019

--

Transfer Learning en NLP (Image Credit: fastai Deep Learning Part 1 v3: Lesson 4)

Ce post concerne les vidéos 8, 9, et 10 du cours fastai de Rachel Thomas sur NLP (A code-first introduction to NLP) ainsi que la vidéo 1 (notes) en entier et les parties des vidéos 3 (notes) et 4 (notes) sur NLP du cours de Jeremy Howard (Introduction to Machine Learning for Coders). Son objectif est d’expliquer les concepts clés du Transfer Learning en NLP présentés dans ces vidéos et leurs notebooks 5-nn-imdb.ipynb, nn-imdb-more.ipynb, review-cv-transfer.ipynb, review-nlp-transfer.ipynb, nn-vietnamese et nn-vietnamese-bwd associés.

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

Motivation

La technique de Transfer Learning (TF) en NLP repose sur la même idée que celle utilisée dans le domaine des images où elle consiste tout d’abord à entraîner un modèle à réaliser une tâche sur un corpus important d’images (comme par exemple entraîner un modèle ConvNet à classifier 1 million d’images issues d’ImageNet en 1000 classes) puis à spécialiser (fine tuning) ce modèle à une tâche spécifique en l’entraînant sur un autre corpus éventuellement plus petit (comme par exemple classer des images de voitures en différentes marques).

Le TF en vision computationnelle fonctionne très bien car le premier modèle a appris dans ses premières couches des caractéristiques générales sur les objets présents dans les images. Nous allons alors importer ces couches avec leurs paramètres appris comme premières couches du second modèle, ce qui nous permettra de ne pas partir de zéro dans l’entraînement du second modèle dont l’objectif est de spécialiser ses dernières couches grâce au second corpus d’entraînement.

Le TF dans le domaine visuel permet donc d’entraîner des modèles spécialisés à partir de corpus éventuellement petits et relativement rapidement.

Transfer Learning en reconnaissance d’images (Image Credit: pinterest)

Depuis 2017, le même principe de Transfer Learning a été utilisé avec succès dans le NLP. Il consiste pour une langue donnée à entraîner un Language Model (LM) Général à partir d’un corpus important de texte (par exemple Wikipedia), puis de spécialiser ce LM Général à un domaine particulier à partir d’un autre corpus (par exemple, les critiques de filmes).

Note: lire ce post pour savoir comment entraîner un LM Général.

Les avantages dans le NLP sont ici les mêmes que ceux du TF dans le domaine visuel: en utilisant des Languages Models Généraux, réussir à entraîner des modèles spécialisés à partir de corpus éventuellement petits et relativement rapidement.

L’objet de ce post est ainsi de présenter ce qu’est le Transfer Learning dans le NLP et comment l’implémenter pour spécialiser un LM Général tel que le présentent Rachel Thomas et Jeremy Howard dans leur cours NLP.

Transfer Learning

L’apprentissage par Transfer Learning (TF) est largement utilisé avec succès en vision par ordinateur depuis 2013 (voir la vidéo 9 et le notebook associé), mais il n’a été appliqué avec succès au NLP que depuis 2017 (en commençant par la publication de ULMFiT de fastai en janvier 2018, puis les Language Model (LM) BERT de Google en septembre 2018 et GPT-2 de OpenAI en février 2019).

Note: c’est d’ailleurs la raison pour laquelle Sebastian Ruder (chercheur en DL NLP collaborant avec fast.ai) a écrit dans The Gradient l’été dernier (2018): NLP’s ImageNet moment has arrived.

Ainsi, toutes les astuces pour entraîner un modèle de DL dans le domaine de la vision par ordinateur en utilisant le Transfer Learning peuvent être utilisées dans le domaine du NLP (voir le notebook review-nlp-transfer.ipynb): il s’agit d’entraîner finement un Language Model (LM) Général à un domaine spécifique (LM fine-tuned) selon les étapes suivantes:

  1. Créer le learner de votre LM Spécialisé à un domaine (ex: critiques de filmes) en important l’architecture et les poids d’un LM Général entraîné dans la langue de votre domaine mais sur un corpus général (ex: articles de Wikipedia). L’architecture de ce LM est soit un RNN ou un Transformer (post pour savoir comment entraîner un LM Général).
  2. Entraîner tout d’abord uniquement les dernières couches de votre learner (freeze) sur le corpus de votre domaine spécialisé (ces couches ont en effet été initialisées avec des valeurs aléatoires).
  3. Débloquer (unfreeze) à présent les premières couches de votre learner pour pouvoir à présent entraîner l’ensemble des couches de votre learner sur le corpus de votre domaine spécialisé avec un Discriminative Learning Rate (cad que la valeur du LR des premières couches est plus petite que celle du LR des dernières couches) afin d’entraîner légèrement (fine-tuning a language model) les premières couches à la différence des dernières.

Ce LM Spécialisé peut alors être utilisé par exemple pour classifier des textes du domaine spécifique en entraînant un classificateur dont les premières couches (appelées couches de l’encoder du LM) sont celles de ce LM Spécialisé (il s’agit alors de réaliser un second Transfer Learning). Cette technique s’appelle Fine-tuned Classifier.

Implémentation en Python d’un LM Spécialisé

Ressources à consulter en particulier: vidéos 8, 9 et 10 et notebooks 5-nn-imdb.ipynb et nn-imdb-more.ipynb

Le code ci-dessous utilise Python et différentes bibliothèques (dont fastai v1) dans un Jupyter Notebook sur un OS avec GPU (cf. tous les tutoriaux d’installation de fastai v1 sur GPU).

Note: pour l’installation de fastai v1 sur votre ordinateur Windows 10 avec GPU, lire “How to install fastai v1 on Windows 10”. Cette installation est utile pour faire des tests à partir de petits corpus mais si vous souhaitez entraîner un LM sur un gros corpus, il est recommandé de choisir un GPU en ligne du type NVIDIA T4 ou même V100.

Nous n’y utilisons pas toutes les options possibles de codage utilisées par Rachel Thomas et Jeremy Howard afin d’attirer l’attention du lecteur sur les points essentiels.

Les 4 étapes basiques (hors étape d’application) de la création par Transfer Learning d’un Language Model Spécialisé que nous détaillons ci-après sont ainsi les suivantes:

  1. Initialisation (importation des bibliothèques de notre environnement de travail)
  2. Téléchargement du corpus d’entraînement et vérification
  3. Création du DataBunch (tokenisation, puis numérisation)
  4. Création du learner du Language Model Spécialisé par Transfer Learning et entraînement
  5. Application | Génération de textes par un Language Model Spécialisé

Voici à présent des explications détaillées sur ces 4 étapes ainsi que sur une application d’un LM Spécialisé à la génération de textes.

1. Initialisation

Il s’agit de construire notre environnement de travail en important les bibliothèques que nous utiliserons dans le Jupyter Notebook.

Nous allons importer la bibliothèque fastai v1 (et son module fastai.text) qui permet de manipuler des corpus de texte de l’importation du corpus textuel à la création du DataBunch et imposer l’autoreload des fichiers de la bibliothèque ainsi que l’affichage des graphiques matplotlib dans le notebook. Nous définissons également notre batch size en tenant compte de la performance du GPU que nous allons utilisé (ie, batch size de 48 pour GPU de faible performance et de 128 dans le cas contraire), notre chemin path et répertoire pour les données à importer ainsi que la langue (lang = ‘en’ ici pour anglais) de notre corpus spécialisé.

Voici le code correspondant à implémenter:

# import fastai and fastai text
from fastai import *
from fastai.text import * %reload_ext autoreload
%autoreload 2 # reload fastai files when they have been changed
%matplotlib inline # plot graphics in the notebook
bs=128 # choose batch size
torch.cuda.set_device(0) # choose GPU if more than one
data_path = Config.data_path() # path to datalang = 'en' # corpus language
name = f'{lang}wiki'
path = data_path/name
path.mkdir(exist_ok=True, parents=True)
lm_fns = [f'{lang}_wt', f'{lang}_wt_vocab']

2. Téléchargement du corpus d’entraînement et vérification

Il nous faut à présent importer nos données textuelles d’un domaine particulier puis, avant de les manipuler, il s’agit de vérifier leur bonne importation.

Note: comme bonne pratique, il est recommandé d’importer d’abord un échantillon des données afin de développer notre code avant de l’appliquer à l’ensemble des données.

Nous utilisons ici le grand jeu de données de critiques de films d’IMDB (100 000 movie reviews). Nous utiliserons la version hébergée dans le cadre des jeux de données fast.ai sur AWS Open Datasets.

Le jeu de données contient un nombre identique de critiques positives et négatives qui sont les plus polarisées: un avis négatif a un score ≤ 4 sur 10, et un avis positif a un score ≥ 7 sur 10 (les avis neutres ne sont pas inclus dans le jeu de données). Le jeu de données est divisé en 3 datasets dans les répertoires train, test et unsup avec 25 000 critiques étiquetées (négatif/positif) dans train, la même chose dans test et 50 000 critiques non étiquetées dans unsup.

Importation

# sample
# path = untar_data(URLs.IMDB_SAMPLE)# full dataset
# full dataset
path = untar_data(URLs.IMDB)
path.ls()# results
[WindowsPath('D:/fastai/data/imdb/imdb.vocab'),
WindowsPath('D:/fastai/data/imdb/lm_databunch'),
WindowsPath('D:/fastai/data/imdb/models'),
WindowsPath('D:/fastai/data/imdb/README'),
WindowsPath('D:/fastai/data/imdb/test'),
WindowsPath('D:/fastai/data/imdb/tmp_clas'),
WindowsPath('D:/fastai/data/imdb/tmp_lm'),
WindowsPath('D:/fastai/data/imdb/train'),
WindowsPath('D:/fastai/data/imdb/unsup')]
(path/'train').ls()# results
[WindowsPath('D:/fastai/data/imdb/train/labeledBow.feat'),
WindowsPath('D:/fastai/data/imdb/train/neg'),
WindowsPath('D:/fastai/data/imdb/train/pos'),
WindowsPath('D:/fastai/data/imdb/train/unsupBow.feat')]

Vérification

# train
!ls {path}/'train/pos' | wc -l # 12500
!ls {path}/'train/neg' | wc -l # 12500
# test
!ls {path}/'test/pos' | wc -l # 12500
!ls {path}/'test/neg' | wc -l # 12500
# unsup
!ls {path}/'unsup' | wc -l # 50000
# Print the first lines of one text
!head -n 4 {(path/'train/pos').ls()[0]}

3. Création du DataBunch (tokenisation, puis numérisation)

Afin de pouvoir manipuler nos données textuelles à des fins d’entraînement d’un modèle de Deep Learning, il nous faut avant toute chose créer un vocabulaire en les tokénisant (un token peut représenter un début ou fin de phrase, un mot, un effet de style comme une ou des majuscules, un symbole de ponctuation, un symbole pour les mots non retenus, etc.) puis les numériser en remplaçant chaque token d’une phrase par son id dans le vocabulaire généré. Ces opérations se font lors de la création du DataBunch du corpus dans la bibliothèque fastai.

Note: par défaut, la bibliothèque fastai utilise le tokenizer SpaCy (vocab de 60 000 caractères par défaut). Depuis juillet 2019, elle dispose aussi de SentencePiece (vocab de 30 000 tokens par défaut).

DataBunch

Nous utilisons ici le Data Block Api de fastai.text pour créer le DataBunch du Language Model.

POINT CLE 1 | Pour entraîner notre LM Spécialisé, nous allons utilisé tous les datasets à notre disposition (train, test, unsup) qu’ils contiennent des critiques étiquetées ou non puisque les labels des données de notre LM sont directement les mots. Nous allons donc lister les 3 datasets dans le databunch ci-dessous, et cette augmentation de données d’entraînement à notre disposition nous permettra alors d’augmenter le nombre d’epochs jusqu’à 10 et cela sans overfitting.

%%time#Inputs: all the text files in path
#We may have other temp folders that contain text files so we only keep what's in train, test and unsup
#We randomly split and keep 10% (10,000 reviews) for validation
#We want to do a language model so we label accordingly
data_lm = (TextList.from_folder(path)
.filter_by_folder(include=['train', 'test', 'unsup'])
.split_by_rand_pct(0.1, seed=42)
.label_for_lm()
.databunch(bs=bs, num_workers=1))
# Save DataBunch
data_lm.save(f'{path}/lm_databunch')

Exploration

# get train and valid datasets tokenized and numericalized by fastai, and the vocab of tokens
train_ds = data_lm.train_ds
valid_ds = data_lm.valid_ds
vocab = data_lm.vocab
# number of texts in train and valid
len(train_ds), len(valid_ds)
# size of vocab
len(vocab.itos), len(vocab.stoi)
# display the 10 first tokens
vocab.itos[:10]
# display the first train review
review = train_ds.x[0]
review
# display the id token list of the train review 0
tokens = review.data
tokens
# from a token list, display the corresponding text
' '.join(np.array(vocab.itos)[tokens])
# Finally, show beginning of a batch
data_lm.show_batch()

Résultat de la tokénisation d’une review:

Text xxbos xxmaj once again xxmaj mr. xxmaj costner has dragged out a movie for far longer than necessary . xxmaj aside from the terrific sea rescue sequences , of which there are very few i just did not care about any of the characters . xxmaj most of us have ghosts in the closet , and xxmaj costner 's character are realized early on , and then forgotten until much later , by which time i did not care .

La tokénisation a en fait inséré des tokens spécifiques dont la liste est en ligne. Par exemple, xxunk (unknown) remplace un mot ou ensemble de caractères non retenu dans le vocabulaire du corpus (du fait, par exemple, de sa faible fréquence d’occurrence).

Note: nous pouvons obtenir un token à partir de son index (liste vocab.itos) et inversement, nous pouvons obtenir l’index du token de tout mot ou ensemble de caractères (dictionnaire vocab.stoi).

4. Création du learner du Language Model Spécialisé par Transfer Learning et entraînement

C’est à cette étape que nous appliquons le Transfer Learning en important dans notre learner l’architecture du Language Model Général pré-entraîné avec ses poids et son vocab.

Il y a ici 2 possibilités selon que le LM Général pré-entraîné est:

  • le modèle AWD-LSTM anglais (ASGD Weight-Dropped LSTM) présent dans la bibliothèque fastai. Il faut alors préciser pretrained=True dans le learner de la manière suivante:
learn_lm = language_model_learner(data_lm, AWD_LSTM, pretrained=True, drop_mult=0.5).to_fp16()

Note: le Language Model AWD-LSTM a été entraîné à partir du corpus WikiText-103 créé par Stephen Merity. Ce corpus est un sous-ensemble de Wikipedia en anglais qui contient plus de 100 millions de tokens dont ceux de ponctuation. AWS-LSTM représente donc un LM Général de la langue anglaise.

  • le modèle AWD-LSTM d’une autre langue que l’anglais et pré-entraîné avec un large corpus issu par exemple de Wikipedia. Il faut alors passer le fichier des poids du modèle et son vocab avec pretrained_fnames=lm_fns dans le learner de la manière suivante:
lang = 'vi' # use the letters that correspond to your LM language
lm_fns = [f'{lang}_wt', f'{lang}_wt_vocab']
learn_lm = language_model_learner(data_lm, AWD_LSTM, pretrained_fnames=lm_fns, drop_mult=0.5).to_fp16()

Note: lire ce post pour savoir comment entraîner un LM Général dans une langue autre que l’anglais.

Dans le premiers cas de figure, voici le code global:

# load DataBunch
data_lm = load_data(path, 'lm_databunch', bs=bs)
# Create LM learner
learn_lm = language_model_learner(data_lm, AWD_LSTM, pretrained=True, drop_mult=0.5).to_fp16()

POINT CLE 2 | Utiliser la méthode to_fp16() sur le LM learner. Cette méthode appelée Mixed Precision (MP) signifie qu’au lieu d’utiliser 32 bits (single precision floats), nous allons utiliser 16 bits (half precision floats). Si la plupart des CPU ne l’accepte pas, les GPU l’acceptent depuis 2 ans, en particulier les GPU NVIDIA (MP s’appelle Tensor Cores chez NVIDIA) et Google, ce qui augmente la vitesse de calcul de 8 à 10 fois quand la Mixed Precision est activée. Cependant, si 16 bits (half precision floats) sont suffisants pour le calcul du gradient, il nous faut 32 bits (single precision floats) pour la multiplication du learning rate avec le gradient (car valeurs très petites qui risquent alors d’être mises à zéro). C’est pour cela que fastai a implémenté dans la méthode to_fp16() la Mixed Precision.

POINT CLE 3 | Régulariser le learner par l’argument drop_mult. Le dropout est le fait de supprimer au hasard à chaque epoch des activations dans les couches, ce qui permet au modèle de mieux généraliser car cela l’oblige à trouver une combinaison des poids qui fonctionne pour le plus grand nombre de cas. L’argument drop_mult dans le language_model_learner(data_lm, AWD_LSTM, pretrained=True, drop_mult=0.5) entraîne la multiplication par sa valeur des valeurs par défaut des 5 dropout du modèle AWD-LSTM (1.0 laisse donc les valeurs par défaut des 5 dropout inchangées). Si votre LM overfit pendant l’entraînement, il vaut mieux augmenter la valeur de drop_mult (jusqu’à 1.0). Sinon, il faut la diminuer.

Note: Jeremy et son équipe viennent de découvrir (juillet 2019) que plus de dropout (ie, valeur de drop_mult jusqu’à 1.0) rend le LM plus facile à entraîner (plus constant/résilient) et améliore la performance du sentiment classifier qui l’utilisera en Transfer Learning (même si celle du LM à proprement parlé sera moins élevée) (video). A ce stade, l’interprétation pourrait être que plus le LM est entraîné à généraliser (fort dropout) et plus il peut s’adapter à des tâches particulières comme la traduction ou la classification.

IMPORTANT | Focus sur le vocabulaire de notre learner vs celui du modèle AWS-LSTM

A chaque token du vocab de notre DataBunch (provenant des critiques de films IMDB), le learner recherche s’il existe dans le vocab de WikiText-103, cad dans le vocab du modèle AWD-LSTM. Si oui, le vecteur embedding de ce token dans AWS-LSTM va être copié-collé dans celui du même token dans notre learner (ie, Transfer Learning). Si non, le vecteur embedding de ce token sera initialisé avec des valeurs aléatoires, valeurs qui seront ensuite mises à jour lors de l’entraînement du learner.

# get vocab of WikiText-103
wiki_itos = pickle.load(open(Config().model_path()/'wt103-1/itos_wt103.pkl', 'rb'))
# compare size of vocabs
len(wiki_itos), len(vocab.itos)
# get the encoder of our learner
enc = learn_lm.model[0].encoder
enc.weight.size()
# get the embeding vector of a token
enc.weight[vocab.stoi[token]]

Entraînement du learner

Nous pouvons à présent rechercher le meilleur Learning Rate pour entraîner notre learner.

learn_lm.lr_find()
learn_lm.recorder.plot()
lr = 1e-3
lr *= bs/48

Nous allons tout d’abord, entraîner les vecteurs d’embeddings des mots qui n’étaient pas dans le vocab du corpus général. Ces vecteurs ayant été initialisés avec des valeurs aléatoires, il faut d’abord les entraîner (par exemple sur 1 epoch) avant d’entraîner l’ensemble du modèle.

learn_lm.fit_one_cycle(1, lr*10, moms=(0.8,0.7))
learn_lm.save('fit_1')

Nous pouvons ensuite unfreeze() notre learner afin de pouvoir entraîner l’ensemble de notre modèle sur 10 epochs avec un Learning Rate plus faible.

learn_lm.load('fit_1');learn_lm.unfreeze()
learn_lm.fit_one_cycle(10, lr, moms=(0.8,0.7))
# save learner and vocab
mdl_path = path/'models'
mdl_path.mkdir(exist_ok=True)
learn_lm.to_fp32().save(mdl_path/lm_fns[0], with_opt=False)
learn_lm.data.vocab.save(mdl_path/(lm_fns[1] + '.pkl'))

Performance d’un LM Spécialisé

L’objectif quand Wikipedia est utilisé comme corpus est d’obtenir un LM Spécialisé avec une performance autour de 40%.

Application | Génération de textes par un Language Model Spécialisé

A présent que nous avons entraîné notre LM Spécialisé sur le corpus des critiques de filmes, nous pouvons l’utiliser pour créer des applications comme par exemple un générateur de textes ressemblant à des critiques de filmes (attention: un LM n’est pas le meilleur modèle pour générer des textes; son objectif est davantage d’être utilisé pour une application type classification).

# Load databunch and learner with pretrained model + vocab
data_lm = load_data(path, 'lm_databunch', bs=bs)
learn_lm = language_model_learner(data_lm, AWD_LSTM, pretrained_fnames=lm_fns)
TEXT = "I hated this movie" # original text
N_WORDS = 30 # number of words to predict following the TEXT
N_SENTENCES = 2 # number of different predictions
print("\n".join(learn_lm.predict(TEXT, N_WORDS, temperature=0.75) for _ in range(N_SENTENCES)))

Note: l’argument temperature de la fonction predict() est un nombre décimal non nul et inférieur ou égal à 1. Plus sa valeur est petite, moins les mots prédits sont aléatoires.

Annexe | Visual Studio

Visual Studio vous permet localement ou en SSH sur le serveur en ligne de votre GPU de parcourir le code de la bibliothèque de DL utilisée comme fastai. Voici les commandes principales:

  • Palette de commandes (Ctrl-shift-p)
  • Aller au symbole (Ctrl-t)
  • Trouver des références (Shift-F12)
  • Aller à la définition (F12)
  • Retour en arrière (alt-left)
  • Voir la documentation
  • Masquer la barre latérale (Ctrl-b)
  • Mode Zen (Ctrl-k, z)

À 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.

--

--