Deep Learning Avec Keras Et TensorFlow - 3e Édition - Aurélien Géron (2024)
Deep Learning Avec Keras Et TensorFlow - 3e Édition - Aurélien Géron (2024)
Deep Learning Avec Keras Et TensorFlow - 3e Édition - Aurélien Géron (2024)
avec
Keras et TensorFlow
Mise en œuvre et cas concrets
3e édition
Aurélien Géron
Traduction de l’anglais par Hervé Soulard
Actualisation par Anne Bohy pour la 3 e édition
Authorized French translation of material from the English edition of
Hands-on Machine Learning with Scikit-Learn, Keras, and TensorFlow, 3E
ISBN 9781098125974
© 2023 Aurélien Géron.
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
Avant-propos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . VII
Le mot de la n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 601
Avant-propos
production, repérer les tentatives de fraude, segmenter une base de clients an de
mieux cibler les offres, prévoir les ventes (ou toute autre série temporelle), classer
automatiquement les prospects à appeler en priorité, optimiser le nombre de conseil-
lers de clientèle en fonction de la date, de l’heure et de mille autres paramètres, etc.
La liste d’applications s’agrandit de jour en jour.
Cette diffusion rapide du Machine Learning est rendue possible en particulier par
trois facteurs :
• Les entreprises sont pour la plupart passées au numérique depuis longtemps :
elles ont ainsi des masses de données facilement disponibles, à la fois en interne
et via Internet.
• La puissance de calcul considérable nécessaire pour l’apprentissage automatique
est désormais à la portée de tous les budgets, en partie grâce à la loi de Moore1 , et
en partie grâce à l’industrie du jeu vidéo : en effet, grâce à la production de masse de
cartes graphiques puissantes, on peut aujourd’hui acheter pour un prix d’environ
1 000 € une carte graphique équipée d’un processeur GPU capable de réaliser
des milliers de milliards de calculs par seconde2. En l’an 2000, le superordinateur
ASCI White d’IBM avait déjà une puissance comparable… mais il avait coûté
110 millions de dollars ! Et bien sûr, si vous ne souhaitez pas investir dans du
matériel, vous pouvez facilement louer des machines virtuelles dans le cloud.
• Enn, grâce à l’ouverture grandissante de la communauté scientique, toutes
les découvertes sont disponibles quasi instantanément pour le monde entier,
notamment sur https://arxiv.org. Dans combien d’autres domaines peut-on voir
une idée scientique publiée puis utilisée massivement en entreprise la même
année ? À cela s’ajoute une ouverture comparable chez les GAFA : chacun
s’efforce de devancer l’autre en matière de publication de logiciels libres, en
partie pour soigner son image de marque, en partie pour que ses outils dominent
et que ses solutions de cloud soient ainsi préférées, et, qui sait, peut-être aussi
par altruisme (il n’est pas interdit de rêver). Il y a donc pléthore de logiciels
libres d’excellente qualité pour le Machine Learning.
Dans ce livre, nous utiliserons TensorFlow, développé par Google et passé en open
source n 2015. Il s’agit d’un outil capable d’exécuter toutes sortes de calculs de façon
distribuée, et particulièrement optimisé pour entraîner et exécuter des réseaux de
neurones articiels. Comme nous le verrons, TensorFlow contient notamment une
excellente implémentation de l’API Keras, qui simplie grandement la création et
l’entraînement de réseaux de neurones articiels.
1. Une loi vériée empiriquement depuis 50ans et qui afrme que la puissance de calcul des processeurs
double environ tous les 18mois.
2. Par exemple, 64 téraFLOPS pour la carte GeForce RTX 4080 de NVidia. Un téraFLOPS égale mille
milliards de FLOPS. Un FLOPS est une opération à virgule ottante par seconde.
Avant-propos IX
Objectif et approche
Pourquoi ce livre ? Quand je me suis mis au Machine Learning, j’ai trouvé plusieurs
livres excellents, de même que des cours en ligne, des vidéos, des blogs, et bien d’autres
ressources de grande qualité, mais j’ai été un peu frustré par le fait que le contenu était
d’une part complètement éparpillé, et d’autre part généralement très théorique, et il
était souvent très difcile de passer de la théorie à la pratique.
J’ai donc décidé d’écrire le livre Hands-On Machine Learning with Scikit-Learn,
Keras, and TensorFlow (ou HOML), avec pour objectif de couvrir les principaux
domaines du Machine Learning, des simples modèles linéaires aux SVM en passant
par les arbres de décision et les forêts aléatoires, et bien sûr aussi le Deep Learning et
même l’apprentissage par renforcement (Reinforcement Learning, ou RL). Je voulais
que le livre soit utile à n’importe quelle personne ayant un minimum d’expérience de
X Deep Learning avec Keras et TensorFlow
Exemples de code
Tous les exemples gurant dans ce livre sont en open source et disponibles sous https://
github.com/ageron/handson-ml3 en tant que notebooks Jupyter: il s’agit de documents
interactifs comportant du texte, des images et des fragments de code exécutable (en
Python dans notre cas). La façon la plus simple et la plus rapide de commencer est
d’exécuter ces notebooks en utilisant Google Colab, un service gratuit vous permet-
tant d’exécuter directement en ligne n’importe quel notebook Jupyter, sans avoir à
installer quoi que ce soit sur votre machine. Vous aurez seulement besoin d’un navi-
gateur internet et d’un compte Google.
Dans ce livre, je supposerai que vous utilisez Google Colab, mais j’ai aussi testé
les notebooks sur d’autres plateformes en ligne comme Kaggle ou Binder, que vous
pouvez donc utiliser si vous préférez. Sinon, vous pouvez aussi installer les biblio-
thèques et outils requis (ou l’image Docker de ce livre) et exécuter les notebooks
directement sur votre propre ordinateur: reportez-vous aux instructions gurant sur
la page https://homl.info/install.
Prérequis
Bien que ce livre ait été écrit plus particulièrement pour les ingénieurs en informa-
tique, il peut aussi intéresser toute personne sachant programmer et ayant quelques
bases mathématiques. Il ne requiert aucune connaissance préalable sur le Machine
Learning mais il suppose les prérequis suivants :
3. J’ai choisi le langage Python d’une part parce que c’est mon langage de prédilection, mais aussi parce
qu’il est simple et concis, ce qui permet de remplir le livre de nombreux exemples de code. En outre, il s’agit
actuellement du langage le plus utilisé en Machine Learning.
4. Cette bibliothèque a été créée par David Cournapeau en 2007, et le projet est maintenant dirigé par une
équipe de chercheurs à l’Institut national de recherche en informatique et en automatique (Inria).
Avant-propos XI
Plan du livre
• Le chapitre1 reprend les éléments du livre Machine Learning avec Scikit-Learn
qui sont indispensables pour comprendre le Deep Learning. Il présente d’abord
Google Colab, qui est l’interface en ligne gratuite et recommandée pour
exécuter facilement tous les exemples de code de ce livre sans avoir à installer
quoi que ce soit sur votre ordinateur (des instructions d’installation sont
disponibles sur github.com/ageron/handson-ml3 si vous préférez exécuter le code
sur votre machine). Puis il présente les bases du Machine Learning, comment
entraîner divers modèles linéaires à l’aide de la descente de gradient, pour des
tâches de régression et de classication, et il présente quelques techniques de
régularisation.
• Le chapitre2 introduit les réseaux de neurones articiels et montre comment
les mettre en œuvre avec Keras.
• Le chapitre 3 montre comment résoudre les difcultés particulières que l’on
rencontre avec les réseaux de neurones profonds.
• Le chapitre4 présente l’API de bas niveau de TensorFlow, utile lorsque l’on
souhaite personnaliser les rouages internes des réseaux de neurones.
• Le chapitre 5 montre comment charger et transformer efficacement de
gros volumes de données lors de l’entraînement d’un réseau de neurones
artificiels.
• Le chapitre6 présente les réseaux de neurones convolutifs et leur utilisation
pour la vision par ordinateur.
• Le chapitre 7 montre comment analyser des séries temporelles à l’aide de
réseaux de neurones récurrents, ou avec des réseaux de neurones convolutifs.
XII Deep Learning avec Keras et TensorFlow
Remerciements
Jamais, dans mes rêves les plus fous, je n’aurais imaginé que la deuxième édition de ce
livre rencontrerait un public aussi vaste. J’ai reçu de nombreux messages de lecteurs,
avec beaucoup de questions, certains signalant gentiment des erreurs et la plupart
m’envoyant des mots encourageants. Je suis extrêmement reconnaissant envers tous
ces lecteurs pour leur formidable soutien. Merci beaucoup à vous tous ! N’hésitez pas
à me contacter si vous voyez des erreurs dans les exemples de code ou simplement
pour poser des questions (https://homl.info/issues3) ! Certains lecteurs ont également
expliqué en quoi ce livre les avait aidés à obtenir leur premier emploi ou à résoudre
un problème concret sur lequel ils travaillaient. Ces retours sont incroyablement
motivants. Si vous trouvez ce livre utile, j’aimerais beaucoup que vous puissiez par-
tager votre histoire avec moi, que ce soit en privé (par exemple, via https://linkedin.
Avant-propos XIII
Je souhaite également remercier Jean-Luc Blanc, des éditions Dunod, pour avoir
soutenu ce projet, et pour la gestion et les relectures attentives des deux premières
éditions. Merci également à Matthieu Daniel, qui a pris la suite de Jean-Luc Blanc
pour cette troisième édition. Je tiens aussi à remercier vivement Hervé Soulard pour
la traduction des deux premières éditions et Anne Bohy pour la traduction de la troi-
sième. Enn, je remercie chaleureusement Brice Martin, des éditions Dunod, pour
sa relecture extrêmement rigoureuse, ses excellentes suggestions et ses nombreuses
corrections.
Pour nir, je suis inniment reconnaissant à ma merveilleuse épouse, Emmanuelle,
et à nos trois enfants, Alexandre, Rémi et Gabrielle, de m’avoir encouragé à tra-
vailler dur pour ce livre. Leur curiosité insatiable fut inestimable : expliquer certains
des concepts les plus difciles de ce livre à ma femme et à mes enfants m’a aidé à
clarier mes pensées et à améliorer directement de nombreuses parties de ce livre. J’ai
même eu droit à des biscuits et du café, qui pourrait en demander davantage ?
1
Les fondamentaux
du Machine Learning
Avant de partir à l’assaut du mont Blanc, il faut être entraîné et bien équipé. De
même, avant d’attaquer le Deep Learning avec TensorFlow et Keras, il est indispen-
sable de maîtriser les bases du Machine Learning. Si vous avez lu le livre Machine
Learning avec Scikit-Learn (A.Géron, Dunod, 3eédition, 2023), vous êtes prêt(e) à
passer directement au chapitre 2. Dans le cas contraire, ce chapitre vous donnera les
bases indispensables pour la suite5 .
Nous commencerons par découvrir Google Colab, un service web gratuit qui est
bien utile pour exécuter du code Python en ligne, sans avoir à installer quoi que ce
soit sur votre machine.
Ensuite, nous étudierons la régression linéaire, l’une des techniques d’apprentis-
sage automatique les plus simples qui soient. Cela nous permettra au passage de rap-
peler ce qu’est le Machine Learning, ainsi que le vocabulaire et les notations que
nous emploierons tout au long de ce livre. Nous verrons deux façons très différentes
d’entraîner un modèle de régression linéaire : premièrement, une méthode analytique
qui trouve directement le modèle optimal (c’est-à-dire celui qui s’ajuste au mieux au
jeu de données d’entraînement) ; deuxièmement, une méthode d’optimisation itéra-
tive appelée descente de gradient (en anglais, gradient descent ou GD), qui consiste à
modier graduellement les paramètres du modèle de façon à l’ajuster petit à petit au
jeu de données d’entraînement.
Nous examinerons plusieurs variantes de cette méthode de descente de gra-
dient que nous utiliserons à maintes reprises lorsque nous étudierons les réseaux de
5. Ce premier chapitre reprend en grande partie le chapitre4 du livre Machine Learning avec Scikit-Learn
(3e édition, 2023), ainsi que quelques éléments essentiels des chapitres1 à 3 de ce dernier.
2 Chapitre 1. Les fondamentaux du Machine Learning
Cellules de texte :
cliquer deux fois
pour modifier
Cellule de code :
cliquer pour modifier
Un notebook Jupyter est constitué d’une suite de cellules. Chaque cellule contient
soit du code exécutable, soit du texte. Essayez de double-cliquer sur la première cellule
de texte (qui contient la phrase « Welcome to Machine Learning Housing Corp.! »).
Ceci ouvrira la cellule en mode Mise à jour. Notez que les notebooks Jupyter com-
portent des balises de formatage (p. ex. **gras**, *italiques*, # Titre, [texte du lien]
(URL), etc.). Essayez de modier ce texte, puis appuyez sur Maj+Entrée pour voir le
résultat.
Ensuite, créez une nouvelle cellule comportant du code en sélectionnant Insérer
→Cellule de code dans le menu. Vous pouvez également utiliser le bouton +Code dans
4 Chapitre 1. Les fondamentaux du Machine Learning
la barre d’outils, ou placer le curseur de votre souris sur le bas de la cellule pour faire appa-
raître les options +Code et +Texte puis cliquer sur +Code. Saisissez du code Python
dans la nouvelle cellule de code, comme par exemple print("Bonjour !"), puis
appuyez sur Maj+Entrée pour l’exécuter (ou cliquez sur le boutonprès du bord gauche
de la cellule).
Si vous n’êtes pas encore connecté à votre compte Google, il vous sera demandé
de le faire maintenant (si vous n’avez pas encore de compte Google, il vous faudra
en créer un). Une fois connecté, vous verrez apparaître un avertissement de sécurité
indiquant que ce notebook n’a pas été créé par Google. Une personne mal inten-
tionnée pourrait créer un notebook tentant de vous leurrer pour vous faire saisir
votre mot de passe Google lui permettant d’accéder à vos données personnelles, c’est
pourquoi, avant d’exécuter un notebook, vous devez toujours vous assurer que vous
pouvez faire conance à son auteur (ou vérier soigneusement ce que va faire chaque
cellule de code avant de l’exécuter). Si vous me faites conance (ou si vous comptez
vérier chaque cellule de code), vous pouvez cliquer maintenant sur « Exécuter
malgré tout ».
Colab vous allouera un nouvel environnement d’exécution (ou runtime) : il s’agit
d’une machine virtuelle gratuite, hébergée sur les serveurs de Google et incluant un
grand nombre d’outils et de bibliothèques Python, en particulier tout ce qu’il vous
faut pour la plupart des chapitres (dans quelques-uns d’entre eux, vous devrez exé-
cuter une commande pour installer des bibliothèques supplémentaires). Cela prendra
quelques secondes. Après quoi, Colab vous connectera automatiquement à ce run-
time et l’utilisera pour exécuter le code de votre nouvelle cellule. Chose importante,
le code s’exécute dans ce runtime, et non sur votre machine. La sortie associée à
votre code s’afchera sous la cellule. Félicitations, vous venez d’exécuter du code
Python dans Colab!
Pour insérer une nouvelle cellule de code, vous pouvez aussi taper Ctrl-M (ou
Cmd-M sur macOS) suivi de A (comme « Above », pour insérer au-dessus de
la cellule active) ou B (comme « Below », pour insérer en dessous). Vous dis-
posez de nombreux autres raccourcis clavier: vous pouvez les visualiser et les
modifier en tapant Ctrl-M (ou Cmd-M) puis H. Si vous choisissez d’exécuter
les notebooks dans Kaggle ou sur votre propre machine en utilisant Jupy-
terLab ou un environnement de développement tel que Visual Studio Code
avec l’extension Jupyter, vous constaterez quelques différences mineures (les
runtimes sont appelés kernels, l’interface utilisateur et les raccourcis clavier sont
légèrement différents, etc.), mais il n’est pas trop difficile de passer d’un envi-
ronnement Jupyter à un autre.
Fichier → Enregistrer une copie dans Drive. Vous pouvez aussi télécharger le note-
book sur votre ordinateur en sélectionnant Fichier → Télécharger → Télécharger le
chier .ipynb. Vous pourrez par la suite vous rendre sur https://colab.research.google.
com et rouvrir le notebook (soit à partir de Google Drive, soit en le rechargeant à
partir de votre ordinateur).
Google Colab n’est conçu que pour un usage interactif : vous pouvez vous
amuser dans les notebooks et modifier le code à votre idée, mais vous ne
pouvez pas demander aux notebooks de tourner pendant longtemps sans
intervention de votre part, car dans un tel cas le runtime serait interrompu
et toutes ses données seraient perdues.
Si le notebook génère des données importantes pour vous, veillez à télécharger ces
données avant la fermeture de votre runtime. Pour cela, cliquez sur l’icône Fichiers
(voir gure1.3, étape1), trouvez le chier que vous voulez télécharger, cliquez sur
la barre verticale pointillée à côté de celui-ci (étape2), puis cliquez sur Télécharger
(étape3). Vous pouvez aussi monter votre drive Google sur le runtime, ce qui permet
au notebook de lire et écrire des chiers directement sur Google Drive comme s’il
s’agissait d’un dossier local. Pour cela, cliquez sur l’icône Fichiers (étape1), puis cli-
quez sur l’icône Google Drive (entourée d’un cercle sur la gure1.3) et suivez les
instructions à l’écran.
2
1
Figure 1.3 – Téléchargement d’un fichier à partir du runtime de Google Colab (étapes 1 à 3),
ou montage de votre drive Google (icone encerclée)
Par défaut, votre drive Google sera monté sur /content/drive/MyDrive. Si vous voulez
sauvegarder un chier de données, copiez-le simplement sous ce dossier en exécu-
tant : !cp /content/mon_super_modele /content/drive/MyDrive.
Toute commande débutant par un caractère «! » (parfois appelé caractère bang) est
interprétée comme une commande système et non comme une commande Python :
dans cet exemple, cp est la commande Linux permettant de copier un chier vers un
autre emplacement. Remarquez que les runtimes de Colab s’exécutent sous Linux (et
plus précisément, sa distribution Ubuntu).
6 Chapitre 1. Les fondamentaux du Machine Learning
Si vous obtenez une erreur que vous ne comprenez pas, essayez de redé-
marrer le runtime (en sélectionnant, dans le menu, Exécution → Redémarrer
l’environnement d’exécution) puis ré-exécutez toutes les cellules depuis le
début du notebook. Ceci résout souvent le problème. Sinon, il est probable
que l’une de vos modifications a introduit une erreur majeure dans le note-
book : revenez simplement au notebook d’origine et essayez à nouveau. En
cas de nouvel échec, signalez le problème sur le projet GitHub.
pas très différent : juste un petit peu plus modulaire, avec davantage de tests et de
gestion d’erreurs.
OK! Maintenant que vous êtes familiarisé avec Colab et que vous pouvez exé-
cuter du code Python, vous êtes prêt à apprendre les bases du Machine Learning.
Etiquette
Observation
Nouvelle
observation
Jeu d’entraînement
Une autre tâche très commune pour un système d’auto-apprentissage est la tâche
de « régression », c’est-à-dire la prédiction d’une valeur. Par exemple, on peut cher-
cher à prédire le prix de vente d’une maison en fonction de divers paramètres (sa
supercie, le revenu médian des habitants du quartier…). Tout comme la classica-
tion, il s’agit d’une tâche d’apprentissage supervisé : le jeu de données d’entraînement
doit posséder, pour chaque observation, la valeur cible. Pour mesurer la performance
du système, on peut par exemple calculer l’erreur moyenne commise par le système
(ou, plus fréquemment, la racine carrée de l’erreur quadratique moyenne, comme
nous le verrons dans un instant).
Il existe également des tâches de Machine Learning pour lesquelles le jeu
d’entraînement n’est pas étiqueté. On parle alors d’apprentissage non supervisé. Par
exemple, si l’on souhaite construire un système de détection d’anomalies (p. ex.
pour détecter les produits défectueux dans une chaîne de production, ou pour
détecter des tentatives de fraudes), on ne dispose généralement que de très peu
d’exemples d’anomalies, donc il est difcile d’entraîner un système de classica-
tion supervisé. On peut toutefois entraîner un système performant en lui donnant
des données non étiquetées (supposées en grande majorité normales), et ce sys-
tème pourra ensuite détecter les nouvelles observations qui sortent de l’ordinaire.
Un autre exemple d’apprentissage non supervisé est le partitionnement d’un jeu de
données, par exemple pour segmenter les clients en groupes semblables, à des ns
demarketing ciblé (voir gure 1.5). Enn, la plupart des algorithmes de réduction de
dimension, dont ceux dédiés à la visualisation des données, sont aussi des exemples
d’algorithmes d’apprentissage non supervisé.
1.3 Comment le système apprend-il ?
Variable 2
Variable 1
prix = 0 + 1
× supercie + 2
× revenu médian
Dans cet exemple, le modèle a trois paramètres : θ 0 , θ1 et θ2. Le premier est le terme
constant, et les deux autres sont les coefcients de pondération (ou poids) des variables
d’entrée. La phase d’entraînement de ce modèle consiste à trouver la valeur de ces
paramètres qui minimise l’erreur du modèle sur le jeu de données d’entraînement.6
6. Le nom « terme constant » peut être un peu trompeur dans le contexte du Machine Learning car il s’agit
bien de l’un des paramètres du modèle que l’on cherche à optimiser, et qui varie donc pendant l’apprentissage.
Toutefois, dès que l’apprentissage est terminé, ce terme devient bel et bien constant. Le nom anglais bias porte lui
aussi à confusion car il existe une autre notion de biais, sans aucun rapport, présentée plus loin dans ce chapitre.
10 Chapitre 1. Les fondamentaux du Machine Learning
Une fois les paramètres réglés, on peut utiliser le modèle pour faire des prédictions
sur de nouvelles observations : c’est la phase d’inférence (ou de prédiction). L’espoir
est que si le modèle fonctionne bien sur les données d’entraînement, il fonction-
nera également bien sur de nouvelles observations (c’est-à-dire pour prédire le prix
de nouvelles maisons). Si la performance est bien moindre, on dit que le modèle a
« surajusté » le jeu de données d’entraînement. Cela arrive généralement quand le
modèle possède trop de paramètres par rapport à la quantité de données d’entraî-
nement disponibles et à la complexité de la tâche à réaliser. Une solution est de
réentraîner le modèle sur un plus gros jeu de données d’entraînement, ou bien de
choisir un modèle plus simple, ou encore de contraindre le modèle, ce qu’on appelle
la régularisation (nous y reviendrons dans quelques paragraphes). À l’inverse, si le
modèle est mauvais sur les données d’entraînement (et donc très probablement aussi
sur les nouvelles données), on dit qu’il « sous-ajuste » les données d’entraînement. Il
s’agit alors généralement d’utiliser un modèle plus puissant ou de diminuer le degré
de régularisation.
Formalisons maintenant davantage le problème de la régression linéaire.
ˆy = θ0 + θ1 x1 + θ2 x2 + … + θn xn
Dans cette équation :
• ŷ est la valeur prédite,
• n est le nombre de variables,
• xi est la valeur de la ième variable,
• θj est le jème paramètre du modèle (terme constant θ0 et coefcients de
pondération des variables θ1, θ2 , …, θn).
Ceci peut s’écrire de manière beaucoup plus concise sous forme vectorielle:
• x est le vecteur des valeurs d’une observation, contenant les valeurs x 0 à xn, où x0
est toujours égal à 1.
• θ ⋅ x est le produit scalaire de θ et de x, qui est égal à θ0x0 + θ1x1 + … + θ nxn, et
que l’on notera dans ce livre θ Tx.
• hθ est la fonction hypothèse, utilisant les paramètres de modèle θ.
Pour réaliser simultanément une prédiction pour toutes les observations, on peut
alors simplement utiliser l’équation suivante :
• Notez que, pour alléger les notations, la fonction d’hypothèse est désormais
notée h plutôt que hθ , mais il ne faut pas oublier qu’elle est paramétrée par le
vecteur θ. De même, nous écrirons simplement RMSE(X) par la suite, même
s’il ne faut pas oublier que la RMSE dépend de l’hypothèse h.
• m est le nombre d’observations dans le jeu de données.
Pour entraîner un modèle de régression linéaire, il s’agit de trouver le vecteur θ qui
minimise la RMSE. En pratique, il est un peu plus simple et rapide de minimiser l’er-
reur quadratique moyenne (MSE, simplement le carré de la RMSE), et ceci conduit
au même résultat, parce que la valeur qui minimise une fonction positive minimise
aussi sa racine carrée.
Nous avons utilisé la fonction y = 4 + 3x1 + bruit gaussien pour générer les don-
nées. Voyons ce que l’équation a trouvé:
>>> theta_best
array([[4.21509616],
[2.77011339]])
7. Notez que Scikit-Learn sépare le terme constant (intercept_) des coefcients de pondération des
variables (coef_).
1.4 Régression linéaire 15
Cette pseudo-inverse est elle-même calculée à l’aide d’une technique très clas-
sique de factorisation de matrice nommée décomposition en valeurs singulières (en
anglais, singular value decomposition ou SVD). Cette technique parvient à décom-
poser le jeu d’entraînement X en produit de trois matrices U, Σ et V T (voir numpy.
linalg.svd()). La pseudo-inverse se calcule ensuite ainsi : X +=VΣ+ UT. Pour
calculer la matrice Σ+, l’algorithme prend Σ et met à zéro toute valeur plus petite
qu’un seuil minuscule, puis il remplace les valeurs non nulles par leur inverse, et
enn il transpose la matrice. Cette approche est bien plus rapide que de calculer
l’équation normale, et elle gère bien les cas limites : en effet, l’équation normale
ne fonctionne pas lorsque la matrice X TX n’est pas inversible (notamment lorsque
m < n ou quand certains attributs sont redondants), alors que la pseudo-inverse est
toujours dénie.
Par ailleurs, une fois votre modèle de régression linéaire entraîné (en utilisant
l’équation normale ou n’importe quel autre algorithme), obtenir une prédiction
est extrêmement rapide: la complexité de l’algorithme est linéaire par rapport au
nombre d’observations sur lesquelles vous voulez obtenir des prédictions et par rap-
port au nombre de variables. Autrement dit, si vous voulez obtenir des prédictions sur
deux fois plus d’observations (ou avec deux fois plus de variables), le temps de calcul
sera grosso modo multiplié par deux.
16 Chapitre 1. Les fondamentaux du Machine Learning
Coût
Pas d’apprentissage
Minimum
�
Valeur de départ
choisie aléatoirement �^
Coût
�
Départ
Côut
�
Départ
De plus, toutes les fonctions de coût n’ont pas la forme d’une jolie cuvette régu-
lière. Il peut y avoir des trous, des crêtes, des plateaux et toutes sortes de reliefs irré-
guliers, ce qui complique la convergence vers le minimum. La gure 1.11 illustre les
deux principaux pièges de la descente de gradient. Si l’initialisation aléatoire démarre
l’algorithme sur la gauche, alors l’algorithme convergera vers un minimum local, qui
n’est pas le minimum global. Si l’initialisation commence sur la droite, alors il lui faut
très longtemps pour traverser le plateau. Et si l’algorithme est arrêté prématurément,
vous n’atteindrez jamais le minimum global.
Coût
Plateau
�
Minimum Minimum
local global
Coût
�2 �2
�1 �1
Figure 1.12 – Descente de gradient avec (à gauche) et sans (à droite) réduction des variables
Une fois le modèle entraîné, il est important de bien penser à appliquer exacte-
ment la même transformation aux nouvelles observations (en utilisant la moyenne et
l’écart-type mesurés sur le jeu d’entraînement). Si vous utilisez Scikit-Learn, il s’agit
de réutiliser le même StandardScaler et d’appeler sa méthode transform(),
et non sa méthode fit_transform().
La gure 1.12 illustre aussi le fait que l’entraînement d’un modèle consiste à
rechercher une combinaison de paramètres du modèle minimisant une fonction
de coût (sur le jeu d’entraînement). C’est une recherche dans l’espace de para-
mètres du modèle: plus le modèle comporte de paramètres, plus l’espace comporte
20 Chapitre 1. Les fondamentaux du Machine Learning
de dimensions et plus difcile est la recherche: rechercher une aiguille dans une
botte de foin à 300 dimensions est plus compliqué que dans un espace à trois
dimensions. Heureusement, sachant que la fonction de coût est convexe dans le
cas d’une régression linéaire, l’aiguille se trouve tout simplement au fond de la
cuvette.
MSE ( )
0
MSE ( ) 2 T
MSE ( ) = 1 = X (X y)
m
MSE( )
n
1.5 Descente de gradient 21
Notez que cette formule implique des calculs sur l’ensemble du jeu d’en-
traînement X, à chaque étape de la descente de gradient ! C’est pourquoi
l’algorithme s’appelle batch gradient descent en anglais (descente de gra-
dient groupée) : il utilise l’ensemble des données d’entraînement à chaque
étape. À vrai dire, full gradient descent (descente de gradient complète)
serait un nom plus approprié. De ce fait, il est extrêmement lent sur les
jeux d’entraînement très volumineux (mais nous verrons plus loin des al-
gorithmes de descente de gradient bien plus rapides). Cependant, la des-
cente de gradient s’accommode bien d’un grand nombre de variables :
entraîner un modèle de régression linéaire lorsqu’il y a des centaines de
milliers de variables est bien plus rapide avec une descente de gradient
qu’avec une équation normale ou une décomposition en valeurs singu-
lières (SVD).
Une fois que vous avez le vecteur gradient (qui pointe vers le haut), il vous
suft d’aller dans la direction opposée pour descendre. Ce qui revient à soustraire
∇θMSE(θ) de θ. C’est ici qu’apparaît le taux d’apprentissage η 10: multipliez le vec-
teur gradient par η pour déterminer le pas de la progression vers le bas:
Équation 1.9 – Pas de descente de gradient
(étape suivante)
= – MSE( )
Voyons une implémentation rapide de cet algorithme:
eta = 0.1 # taux d’apprentissage
n_epochs = 1000
m = len(X_b) # nombre d’observations
np.random.seed(42)
theta = np.random.randn(2, 1) # init. aléatoire des paramètres du modèle
Ce n’était pas trop difcile ! Chacune des itérations sur le jeu d’entraînement
s’appelle une époque (en anglais, epoch). Voyons maintenant le theta qui en résulte :
>>> theta
array([[4.21509616],
[2.77011339]])
C’est exactement ce que nous avions obtenu avec l’équation normale! La des-
cente de gradient a parfaitement fonctionné. Mais que se serait-il passé avec un taux
d’apprentissage différent? La gure 1.13 présente les 20premiers pas de la descente
de gradient en utilisant trois taux d’apprentissage différents. La ligne au bas de chaque
graphique représente le point de départ choisi aléatoirement, puis les époques succes-
sives sont représentées par des lignes de plus en plus sombres.
�2
Coût
�1
Lorsque la fonction de coût est très irrégulière (comme sur la gure 1.11), ceci
peut en fait aider l’algorithme à sauter à l’extérieur d’un minimum local, et donc la
descente de gradient stochastique a plus de chances de trouver le minimum global
que la descente de gradient ordinaire.
Par conséquent, cette sélection aléatoire est un bon moyen d’échapper à un
minimum local, mais n’est pas satisfaisante car l’algorithme ne va jamais s’arrêter
au minimum. Une solution à ce dilemme consiste à réduire progressivement le taux
d’apprentissage: les pas sont grands au début (ce qui permet de progresser rapide-
ment et d’échapper aux minima locaux), puis ils réduisent progressivement, per-
mettant à l’algorithme de s’arrêter au minimum global. Ce processus est semblable
à l’algorithme portant en anglais le nom de simulated annealing (ou recuit simulé)
parce qu’il est inspiré du processus métallurgique consistant à refroidir lentement
un métal en fusion. La fonction qui détermine le taux d’apprentissage à chaque
itération s’appelle l’échéancier d’apprentissage (en anglais, learning schedule). Si le
taux d’apprentissage est réduit trop rapidement, l’algorithme peut rester bloqué sur
un minimum local ou même s’immobiliser à mi-chemin du minimum. Si le taux
d’apprentissage est réduit trop lentement, l’algorithme peut sauter pendant long-
temps autour du minimum et nir au-dessus de l’optimum si vous tentez de l’arrêter
trop tôt.
Ce code implémente une descente de gradient stochastique en utilisant un calen-
drier d’apprentissage très simple:
n_epochs = 50
t0, t1 = 5, 50 # hyperparam. d’échéancier d’apprent.
24 Chapitre 1. Les fondamentaux du Machine Learning
def learning_schedule(t):
return t0 / (t + t1)
np.random.seed(42)
theta = np.random.randn(2, 1) # init. aléatoire
Par convention, nous effectuons des cycles de m itérations: chacun de ces cycles
s’appelle une époque (en anglais, epoch). Alors que le code de la descente de gradient
ordinaire effectue 1000 fois plus d’itérations sur l’ensemble du jeu d’apprentissage, ce
code-ci ne parcourt le jeu d’entraînement qu’environ 50fois et aboutit à une solution
plutôt satisfaisante:
>>> theta
array([[4.21076011],
[2.74856079]])
Remarquez qu’étant donné que les observations sont choisies au hasard, certaines
d’entre elles peuvent être sélectionnées plusieurs fois par époque, alors que d’autres
ne le seront jamais. Si vous voulez être sûr que l’algorithme parcoure toutes les obser-
vations durant chaque époque, vous pouvez adopter une autre approche qui consiste
à mélanger le jeu d’entraînement comme on bat un jeu de cartes (en prenant soin
de mélanger conjointement les variables d’entrée et les étiquettes), à le parcourir
1.5 Descente de gradient 25
Pour effectuer avec Scikit-Learn une régression linéaire par descente de gradient
stochastique (ou SGD), vous pouvez utiliser la classe SGDRegressor qui par
défaut optimise la fonction de coût du carré des erreurs (MSE). Le code suivant
effectue au maximum 1000 époques (max_iter) ou tourne jusqu’à ce que la perte
devienne inférieure à 10–5 (tol) durant 100époques (n_iter_no_change). Il
commence avec un taux d’apprentissage de 0,1 (eta0), en utilisant le calendrier
d’apprentissage par défaut (différent de celui ci-dessus). À la n, il n’effectue aucune
régularisation (penalty=None, expliqué un peu plus loin).
Là encore, vous obtenez une solution assez proche de celle donnée par l’équation
normale:
>>> sgd_reg.intercept_, sgd_reg.coef_
(array([4.21278812]), array([2.77270267]))
Hors- Normali-
m n Hyper- Scikit-
Algorithme mémoire sation
grand grand paramètres Learn
possible ? requise ?
11. Si l’équation normale ne s’applique qu’à la régression linéaire, les algorithmes de descente de gradient
peuvent, comme nous le verrons, permettre d’entraîner de nombreux autres modèles.
28 Chapitre 1. Les fondamentaux du Machine Learning
Il est clair qu’une ligne droite n’ajustera jamais correctement ces données.
Utilisons donc la classe PolynomialFeatures de Scikit-Learn pour transformer
nos données d’apprentissage, en ajoutant les carrés (polynômes du second degré) des
variables aux variables existantes (dans notre cas, il n’y avait qu’une variable):
>>> from sklearn.preprocessing import PolynomialFeatures
>>> poly_features = PolynomialFeatures(degree=2, include_bias=False)
>>> X_poly = poly_features.fit_transform(X)
>>> X[0]
array([-0.75275929])
>>> X_poly[0]
array([-0.75275929, 0.56664654])
Ce n’est pas mal ! Le modèle donne l’estimation ŷ = 0,56 x12 + 0,93 x1 + 1,78 alors
que la fonction d’origine était y = 0,5 x1 2 + 1,0 x1 + 2,0 + aléa gaussien.
Notez que lorsqu’il y a des variables multiples, la régression polynomiale est
capable de mettre en évidence des relations entre ces variables (ce que ne peut pas
faire un modèle de régression linéaire simple). Ceci est rendu possible par le fait que
PolynomialFeatures ajoute toutes les combinaisons de variables jusqu’à un cer-
tain degré. Si vous avez par exemple deux variables a et b, PolynomialFeatures
avec degree=3 ne va pas seulement ajouter les variables a2, a 3, b 2 et b3, mais aussi
les combinaisons ab, a2b et ab 2.
Plutôt que de réserver des données pour le jeu de validation, on peut ef-
fectuer ce qu’on appelle une validation croisée : on découpe le jeu de don-
nées d’entraînement en k morceaux (ou k folds en anglais), et on entraîne
le modèle sur tous les morceaux sauf le premier, que l’on utilise ensuite
pour évaluer le modèle. Puis on réinitialise ce modèle et on l’entraîne à
nouveau, mais cette fois-ci sur tous les morceaux sauf le deuxième, que l’on
utilise pour évaluer le modèle, et ainsi de suite. Pour chaque combinaison
d’hyperparamètres, on obtient ainsi k évaluations. On peut alors choisir la
combinaison qui produit la meilleure évaluation en moyenne. Voir les classes
GridSearchCV et RandomizedSearchCV de Scikit-Learn.
Scikit-Learn dispose d’une fonction bien utile pour cela : elle entraîne et évalue
le modèle en utilisant la validation croisée. Par défaut, elle réentraîne le modèle sur
des sous-ensembles croissants du jeu d’entraînement, mais, si le modèle permet un
apprentissage incrémental, lorsque vous appelez learning_curve() vous pouvez
spécier exploit_incremental_learning=True pour qu’à la place elle
entraîne le modèle incrémentalement. La fonction renvoie les tailles de jeu d’entraî-
nement pour lesquelles elle a évalué le modèle, ainsi que les scores d’entraînement et
de validation mesurés pour chaque taille et pour chaque passe de validation croisée.
Utilisons cette fonction pour obtenir les courbes d’apprentissage du modèle de régres-
sion linéaire ordinaire (voir gure1.20) :
from sklearn.model_selection import learning_curve
d’autre part parce que ce n’est pas linéaire du tout. C’est pourquoi l’erreur sur le
jeu d’entraînement augmente jusqu’à atteindre un plateau: à partir de là, l’ajout
de nouvelles observations au jeu d’entraînement ne modie plus beaucoup l’er-
reur moyenne, ni en bien ni en mal. Voyons maintenant l’erreur de validation:
lorsque le modèle est entraîné à partir de très peu d’observations, il est incapable
de généraliser correctement, c’est pourquoi l’erreur de validation est relativement
importante au départ. Puis le modèle s’améliore à mesure qu’il reçoit davantage
d’exemples d’entraînement, c’est pourquoi l’erreur de validation diminue lente-
ment. Cependant, il n’est toujours pas possible de modéliser correctement les don-
nées à l’aide d’une ligne droite, c’est pourquoi l’erreur nit en plateau, très proche
de l’autre courbe.
Ces courbes d’apprentissage sont caractéristiques d’un modèle qui sous-ajuste: les
deux courbes atteignent un plateau, elles sont proches et relativement hautes.
polynomial_regression = make_pipeline(
PolynomialFeatures(degree=10, include_bias=False),
LinearRegression())
12. Cette notion de biais ne doit pas être confondue avec le terme constant (ou biais) du modèle linéaire.
34 Chapitre 1. Les fondamentaux du Machine Learning
n
2
J ( ) = MSE( ) + i
m
i=1
Notez que le terme constant θ0 n’est pas régularisé (la somme commence à i=1,
et non 0). Si nous définissons w comme le vecteur de pondération des variables
(θ 1 à θ n), alors le terme de régularisation est simplement égal à (║w║2) 2 /m, où
║w║2 représente la norme ℓ2 du vecteur de pondération w (voir l’encart sur les
normes ℓk, ci-dessous).
Pour une descente de gradient ordinaire, ajoutez simplement α w à la partie du
vecteur gradient de la MSE correspondant aux pondérations des variables, sans
rien ajouter au terme constant (voir équation 1.8).
13. Il est courant d’utiliser la notation J(θ) pour les fonctions de coût ne disposant pas d’un nom court:
j’utiliserai souvent cette notation dans la suite de ce livre. Le contexte expliquera clairement de quelle
fonction de coût il s’agit.
1.8 Modèles linéaires régularisés 35
Les normes ℓk
Il existe diverses mesures de distance ou normes :
• Celle qui nous est la plus familière est la norme euclidienne, également
appelée norme ℓ 2 . La norme ℓ2 d’un vecteur v, notée ∥v∥2 (ou tout sim-
plement ∥v∥) est égale à la racine carrée de la somme des carrés de ses
éléments. Par exemple, la norme d’un vecteur contenant les éléments 3 et
4 est égale à la racine carrée de 3 2 + 42 = 25, c’est-à-dire 5. Une ville située
3 km à l’est et 4 km au sud se situe à 5 km à vol d’oiseau.
• La norme ℓ1 d’un vecteur v, notée ∥v∥ 1, est égale à la somme de la valeur
absolue des éléments de v. On l’appelle parfois norme de Manhattan car
elle mesure la distance entre deux points dans une ville où l’on ne peut
se déplacer que le long de rues à angle droit. Par exemple si vous avez
rendez-vous à 3 blocs vers l’est et 4 blocs vers le sud, vous devrez par-
courir 4 + 3 = 7 blocs.
• Plus généralement, la norme ℓ k d’un vecteur v contenant n éléments est
1
définie par la formule : v k
= ( | v0 | k + | v1 | k + … + | v n |k) k . La norme ℓ0
La gure 1.22 présente différents modèles de régression ridge entraînés sur des
données linéaires à bruit très important avec différentes valeurs α . À gauche,
on a effectué des régressions ridge ordinaires, ce qui conduit à des prédic-
tions linéaires. À droite, les données ont été tout d’abord étendues en utilisant
PolynomialFeatures(degree=10), puis centrées-réduites en utilisant
StandardScaler, et enn on a appliqué aux variables résultantes un modèle
ridge, correspondant donc à une régression polynomiale avec régularisation ridge.
Notez comme en accroissant α on obtient des prédictions plus lisses, moins extrêmes,
plus raisonnables14: ceci correspond à une réduction de la variance du modèle, mais
à un accroissement de son biais.
Tout comme la régression linéaire, la régression ridge peut s’effectuer soit en résol-
vant une équation (solution analytique), soit en effectuant une descente de gradient.
Les avantages et inconvénients sont les mêmes. L’équation 1.11 présente la solution
analytique, où A est la matrice identité15 (n+1) × (n+1), à l’exception d’une valeur 0
dans la cellule en haut à gauche, correspondant au terme constant.
–1
= XT X + A X Ty
Voici comment effectuer une régression ridge par la méthode analytique avec
Scikit-Learn (il s’agit d’une variante de la solution ci-dessus utilisant une technique
de factorisation de matrice d’André-Louis Cholesky):
>>> from sklearn.linear_model import Ridge
>>> ridge_reg = Ridge(alpha=1, solver="cholesky")
>>> ridge_reg.fit(X, y)
>>> ridge_reg.predict([[1.5]])
array([[1.55325833]])
15. Une matrice carrée remplie de zéros, à l’exception des 1 sur la diagonale principale (d’en haut à gauche
à en bas à droite).
16. Vous pouvez aussi utiliser la classe Ridge avec solver="sag". La descente de gradient moyenne
stochastique (stochastic average GD ou SAG) est une variante de la descente de gradient stochastique SGD.
Pour plus de détails, reportez-vous à la présentation de Mark Schmidt et al., University of British Colum-
bia: https://homl.info/12.
1.8 Modèles linéaires régularisés 37
La classe RidgeCV effectue aussi une régression ridge, mais elle ajuste auto-
matiquement les hyperparamètres au moyen d’une validation croisée. C’est
à peu près équivalent à l’utilisation de GridSearchCV, mais optimisé pour
la régression ridge et beaucoup plus rapide. Plusieurs autres estimateurs
(principalement linéaires) ont des variantes CV (c’est-à-dire à validation croi-
sée) très efficaces, comme par exemple LassoCV et ElasticNetCV.
La gure 1.23 présente les mêmes résultats que la gure 1.22, mais en remplaçant
les régressions ridge par des régressions lasso et en utilisant des valeursα différentes.
Une caractéristique importante de la régression lasso est qu’elle tend à éliminer les
poids des variables les moins importantes (elle leur donne la valeur zéro). Voyez par
exemple la ligne à tirets du graphique de droite de la gure 1.23 (avec α = 10–7) qui
ressemble grosso modo à une fonction du 3e degré : tous les coefcients de pondéra-
tion des variables polynomiales de haut degré sont nuls. Autrement dit, la régression
lasso effectue automatiquement une sélection des variables et produit un modèle creux
(sparse, en anglais), avec seulement quelques coefcients de pondération non nuls.
L’observation de la gure 1.24 vous permettra de comprendre intuitivement pour-
quoi: les axes représentent deux paramètres du modèle et les courbes de niveau en
arrière-plan représentent différentes fonctions de perte. Sur le graphique en haut à
gauche, les courbes de niveau correspondent à la perte ℓ1 (|θ1 | + |θ2 |), qui décroît
linéairement lorsque vous vous rapprochez de l’un des axes. Ainsi, si vous initialisez
les paramètres du modèle à θ 1 = 2 et θ 2= 0,5, effectuer une descente de gradient
fera décroître de la même manière les deux paramètres (comme le montre la ligne
pointillée), et par conséquent θ2 atteindra en premier la valeur 0 (étant donné qu’il
était plus proche de 0 au départ). Après quoi, la descente de gradient suit la rigole
jusqu’à atteindre θ1=0 (par bonds successifs étant donné que les gradients de ℓ 1 ne
sont jamais proches de zéro, mais valent soit –1, soit 1 pour chaque paramètre). Sur
le graphique en haut à droite, les courbes de niveau représentent la fonction de coût
de la régression lasso (c.-à-d. une fonction de coût MSE plus une perte ℓ1 ). Les petits
cercles blancs matérialisent le chemin suivi par la descente de gradient pour opti-
miser certains paramètres du modèle initialisés aux alentours de θ 1 =0,25 et θ 2=–1 :
remarquez à nouveau que le cheminement atteint rapidement θ 2 = 0, puis suit la
rigole et nit par osciller aux alentours de l’optimum global (représenté par le carré).
Pénalité ℓ1 Lasso
1.5
1.0
0.5
θ2 0.0
– 0.5
– 1.0
– 1.5
Pénalité ℓ2 Ridge
1.5
1.0
0.5
θ2 0.0
– 0.5
– 1.0
– 1.5
– 1.0 – 0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0 – 1.0 – 0.5 0.0 0.5 1.0 1.5 2.0 2.5 3.0
θ1 θ1
Lorsque vous utilisez une régression lasso, pour éviter que la descente de
gradient ne rebondisse à la fin aux alentours de l’optimum, vous devez
diminuer graduellement le taux d’apprentissage durant l’entraînement (l’al-
gorithme continuera à osciller autour de l’optimum, mais le pas sera de plus
en plus petit, et donc il convergera).
signe( 1)
1 si i < 0,
signe( 2 )
g ,J = MSE +2 où signe i = 0 si i = 0,
+1 si i > 0.
signe( n )
17. Vous pouvez considérer qu’un vecteur de sous-gradient en un point non différentiable est un vecteur
intermédiaire entre les vecteurs de gradient autour de ce point. Par exemple, la fonction f(x)=|x| n’est
pas dérivable en 0, mais sa dérivée est –1 pour x<0 et + 1 pour x>0, donc toute valeur comprise entre –1
et +1 est une sous-dérivée de f(x) en 0.
40 Chapitre 1. Les fondamentaux du Machine Learning
Alors quand devriez-vous effectuer une régression linéaire simple (c.-à-d. sans
régularisation), ou au contraire utiliser une régularisation ridge, lasso ou elastic net?
Il est pratiquement toujours préférable d’avoir au moins un petit peu de régularisa-
tion, c’est pourquoi vous devriez éviter en général d’effectuer une régression linéaire
simple. La régression ridge est un bon choix par défaut, mais si vous soupçonnez que
seules quelques variables sont utiles, vous devriez préférer une régression lasso ou
elastic net, car elles tendent à réduire les coefcients de pondération des variables
inutiles, comme nous l’avons expliqué. En général, elastic net est préférée à lasso,
étant donné que lasso peut se comporter de manière erratique lorsque le nombre de
variables est supérieur au nombre d’observations du jeu d’entraînement ou lorsque
plusieurs des variables sont fortement corrélées.
Voici un court exemple Scikit-Learn utilisant ElasticNet (l1_ratio cor-
respond au ratio de mélange r):
>>> from sklearn.linear_model import ElasticNet
>>> elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5)
>>> elastic_net.fit(X, y)
>>> elastic_net.predict([[1.5]])
array([1.54333232])
preprocessing = make_pipeline(
PolynomialFeatures(degree=90, include_bias=False), StandardScaler())
X_train_prep = preprocessing.fit_transform(X_train)
X_valid_prep = preprocessing.transform(X_valid)
sgd_reg = SGDRegressor(penalty=None, eta0=0.002, random_state=42)
n_epochs = 500
best_valid_rmse = float('inf')
ˆp = h ( x) = ( Tx)
La fonction (notée σ ()) est une fonction sigmoïde (c’est-à-dire en forme de «S»)
qui renvoie des valeurs comprises entre 0 et 1. Elle est dénie par l’équation1.16 et
la gure1.26:
Équation 1.16 – Fonction sigmoïde
1
σ (t ) =
1 + exp ( – t)
1.9 Régression logistique 43
⎧⎪
⎪
⎪ – log(pˆ ) si y = 1
c (θ ) = ⎨
⎪
⎪ – log(1 – pˆ ) si y = 0
⎩⎪
44 Chapitre 1. Les fondamentaux du Machine Learning
Cette fonction de coût fonctionne parce que – log(t) devient très grand lorsque
t s’approche de 0, par conséquent le coût sera grand si le modèle estime une pro-
babilité proche de 0 pour une observation positive, et il sera également très grand
si le modèle estime une probabilité proche de 1 pour une observation négative.
Par ailleurs, – log(t) est proche de 0 lorsque t est proche de 1, et par conséquent le
coût sera proche de 0 si la probabilité estimée est proche de 0 pour une observation
négative ou proche de 1 pour une observation positive, ce qui est précisément ce
que nous voulons.
La fonction de coût sur l’ensemble du jeu d’entraînement est le coût moyen sur
l’ensemble de ses observations. Elle peut s’écrire sous forme d’une simple expression,
nommée perte logistique (en anglais, log loss):
m
1
J( )= – y(i)log( p̂(i) ) + (1 – y(i) )log( 1 – p̂(i))
m i=1
La mauvaise nouvelle, c’est qu’il n’existe pas de solution analytique connue pour
calculer la valeur de θ qui minimise cette fonction de coût (il n’y a pas d’équivalent
de l’équation normale). Mais la bonne nouvelle, c’est que cette fonction de coût est
convexe, c’est pourquoi un algorithme de descente de gradient (comme tout autre algo-
rithme d’optimisation) est assuré de trouver le minimum global (si le taux d’appren-
tissage n’est pas trop grand et si vous attendez sufsamment longtemps). La dérivée
partielle de la fonction de coût par rapport au jème paramètre du modèle θj se calcule
comme suit:
18. Photos reproduites à partir des pages Wikipédia correspondantes. Photo d’Iris virginica de Frank
Mayeld (Creative Commons BY-SA 2.0), photo d’Iris versicolor de D. Gordon E. Robertson (Creative
Commons BY-SA 3.0), photo d’Iris setosa dans le domaine public.
46 Chapitre 1. Les fondamentaux du Machine Learning
log_reg = LogisticRegression(random_state=42)
log_reg.fit(X_train, y_train)
Examinons les probabilités estimées par le modèle pour les eurs ayant des tailles
de pétales comprises entre 0cm et 3cm (gure1.28)19 :
X_new = np.linspace(0, 3, 1000).reshape(-1, 1) # transformer en vecteur
# colonne
y_proba = log_reg.predict_proba(X_new)
decision_boundary = X_new[y_proba[:, 1] >= 0.5][0, 0]
19. La fonction reshape() de NumPy permet d’avoir une dimension égale à –1, ce qui équivaut à
«automatique » : sa valeur est déduite de la longueur du tableau et des autres dimensions.
1.9 Régression logistique 47
La largeur des pétales des eurs d’Iris virginica (représentées par des triangles) s’étage
de 1,4cm à 2,5cm, tandis que les autres eurs d’iris (représentées par des carrés) ont
en général une largeur de pétales inférieure, allant de 0,1cm à 1,8 cm. Remarquez
qu’il y a un léger recouvrement. Au-dessus de 2cm, le classicateur estime avec une
grande conance que la eur est un Iris virginica (il indique une haute probabilité pour
cette classe), tandis qu’en dessous de 1cm, il estime avec une grande conance que la
eur n’est pas un Iris virginica (haute probabilité pour la classe « Iris non-virginica»).
Entre ces deux extrêmes, le classicateur n’a pas de certitude. Cependant, si vous lui
demandez de prédire la classe (en utilisant la méthode predict() plutôt que la
méthode predict_proba()), il renverra la classe la plus probable, et par consé-
quent il y a une frontière de décision aux alentours de 1,6cm où les deux probabilités
sont égales à 50%: si la largeur de pétale est supérieure à 1,6cm, le classicateur pré-
dira que la eur est un Iris virginica, sinon il prédira que ce n’en est pas un (même s’il
n’est pas vraiment sûr de cela):
>>> decision_boundary
1.6516516516516517
>>> log_reg.predict([[1.7], [1.5]])
array([True, False])
La gure 1.29 est une autre représentation graphique du même jeu de données,
obtenue cette fois-ci en croisant deux variables: la largeur des pétales et leur longueur.
Une fois entraîné, le classicateur de régression logistique peut estimer la probabilité
qu’une nouvelle eur soit un Iris virginica en se basant sur ces deux variables. La ligne
en pointillé représente les points où le modèle estime une probabilité de 50%: c’est
la frontière de décision du modèle. Notez que cette frontière est linéaire20 . Chaque
ligne parallèle matérialise les points où le modèle estime une probabilité donnée, de
15% (en bas à gauche), jusqu’à 90% (en haut à droite). D’après le modèle, toutes
les eurs au-dessus de la ligne en haut à droite ont plus de 90% de chances d’être
des Iris virginica.
20. C’est l’ensemble des points x tels que θ0 + θ 1 x1 + θ 2 x2 = 0, ce qui dénit une ligne droite.
48 Chapitre 1. Les fondamentaux du Machine Learning
Tout comme les autres modèles linéaires, les modèles de régression logistique
peuvent être régularisés à l’aide de pénalités ℓ1 ou ℓ2 . En pratique, Scikit-Learn ajoute
une pénalité ℓ2 par défaut.
(k) T
sk (x) = ( ) x
Notez que chaque classe possède son propre vecteur de paramètres θ(k). Tous ces
vecteurs constituent les lignes de la matrice de paramètres Θ.
Une fois que vous avez calculé le score de chaque classe pour l’observationx, vous
pouvez estimer la probabilité p̂k que cette observation appartienne à la classe k en
transformant ces scores par la fonction softmax. La fonction calcule l’exponentielle
de chaque score puis effectue un recalibrage (en divisant par la somme de toutes les
exponentielles). Les scores sont souvent appelés logits ou log-odds (bien qu’il s’agisse
en fait de log-odds non normalisés).
1.9 Régression logistique 49
exp( s k ( x))
pˆk = σ ( s ( x ))k = K
∑ exp( s ( x))
j= 1
j
ŷ = argmax
k
(s(x))k = argmax sk (x) = argmax (
k k
( (k) T
) )
x
L’opérateur argmax renvoie la valeur d’une variable qui maximise une fonction. Dans
cette équation, il renvoie la valeur de k qui maximise la probabilité estimée σ(s(x))k .
Maintenant que vous savez comment ce modèle estime les probabilités et fait des pré-
dictions, intéressons-nous à l’entraînement. L’objectif est d’avoir un modèle qui estime
une forte probabilité pour la classe ciblée (et par conséquent de faibles probabilités pour
les autres classes). Minimiser la fonction de coût suivante, appelée entropie croisée (en
anglais, cross entropy), devrait aboutir à ce résultat car le modèle est pénalisé lorsqu’il
estime une faible probabilité pour la classe ciblée. On utilise fréquemment l’entropie
croisée pour mesurer l’adéquation entre un ensemble de probabilités estimées d’apparte-
nance à des classes et les classes ciblées.
Équation 1.24 – Fonction de coût d’entropie croisée
m K
1
J (Θ ) = –
m i=1
∑ ∑y ( ) log(pˆ( ))
k= 1
k
i
k
i
Dans cette équation, y k(i) est la probabilité cible que la ième observation appartienne
à la classek. En général, cette probabilité cible est soit 1, soit 0, selon que l’observa-
tion appartient ou non à la classe k.
50 Chapitre 1. Les fondamentaux du Machine Learning
Notez que lorsqu’il n’y a que deux classes (K = 2), cette fonction de coût est équi-
valente à celle de la régression logistique (log loss, équation 1.19).
Entropie croisée
L’entropie croisée trouve son origine dans la théorie de l’information de Claude
Shannon. Supposons que vous vouliez transmettre quotidiennement et de
manière efficace des informations météorologiques. S’il y a 8 possibilités (ensoleillé,
pluvieux, etc.), vous pouvez encoder chacune de ces options sur 3 bits (puisque
23 = 8). Cependant, si vous pensez que le temps sera ensoleillé pratiquement tous
les jours, il serait plus efficace de coder « ensoleillé » sur un seul bit, et les 7 autres
options sur 4 bits (en commençant par un 1). L’entropie croisée mesure le nombre
moyen de bits que vous transmettez pour chaque option. Si les suppositions que
vous faites sur le temps sont parfaites, l’entropie croisée sera égale à l’entropie
des données météorologiques elles-mêmes (à savoir leur caractère imprévisible
intrinsèque). Mais si vos suppositions sont fausses (p. ex. s’il pleut souvent), l’en-
tropie croisée sera supérieure, d’un montant supplémentaire appelé divergence de
Kullback-Leibler.
L’entropie croisée entre deux distributions de probabilités p et q est définie par
H(p,q) = –∑ x p(x) log q(x) (du moins lorsque les distributions sont discrètes).
Pour plus de détails, consultez ma vidéo sur le sujet : https://homl.info/xentropy.
Le vecteur gradient par rapport à θ(k) de cette fonction de coût se dénit comme suit:
Vous pouvez maintenant calculer le vecteur gradient de chaque classe, puis utiliser
une descente de gradient (ou un autre algorithme d’optimisation) pour trouver la
matrice des paramètres Θ qui minimise la fonction de coût.
Utilisons la régression softmax pour répartir les eurs d’iris en trois classes. Le
classicateur LogisticRegression de Scikit-Learn utilise automatiquement
une régression softmax (dans le cas où solver="lbfgs", ce qui est la valeur
par défaut) lorsque vous l’entraînez sur plus de deux classes. Il applique aussi par
défaut une régularisation ℓ2, que vous pouvez contrôler à l’aide de l’hyperparamètre C,
comme mentionné précédemment :
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = iris["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
Par conséquent, la prochaine fois que vous trouverez un iris ayant des pétales de
5cm de long et de 2cm de large et que vous demanderez à votre modèle de vous dire
1.9 Régression logistique 51
de quel type d’iris il s’agit, il vous répondra Iris virginica (classe 2) avec une probabilité
de 96% (ou Iris versicolor avec une probabilité de 4%):
>>> softmax_reg.predict([[5, 2]])
array([2])
>>> softmax_reg.predict_proba([[5, 2]]).round(2)
array([[0. , 0.04, 0.96]])
1.10 Exercices
1.10 EXERCICES
1. Quel algorithme d’entraînement de régression linéaire pouvez-vous
utiliser si vous avez un jeu d’entraînement comportant des millions
de variables?
2. Supposons que les variables de votre jeu d’entraînement aient des
échelles très différentes. Quels algorithmes peuvent en être affectés,
et comment? Comment pouvez-vous y remédier?
3. Une descente de gradient peut-elle se bloquer sur un minimum local
lorsque vous entraînez un modèle de régression logistique?
4. Tous les algorithmes de descente de gradient aboutissent-ils au même
modèle si vous les laissez s’exécuter sufsamment longtemps?
5. Supposons que vous utilisiez une descente de gradient ordinaire, en
représentant graphiquement l’erreur de validation à chaque cycle
(ou époque): si vous remarquez que l’erreur de validation augmente
régulièrement, que se passe-t-il probablement? Comment y remédier?
6. Est-ce une bonne idée d’arrêter immédiatement une descente de
gradient par mini-lots lorsque l’erreur de validation augmente?
7. Parmi les algorithmes de descente de gradient que nous avons
étudiés, quel est celui qui arrive le plus vite à proximité de la solution
optimale? Lequel va effectivement converger? Comment pouvez-
vous faire aussi converger les autres?
8. Supposons que vous utilisiez une régression polynomiale. Après avoir
imprimé les courbes d’apprentissage, vous remarquez qu’il y a un écart
important entre l’erreur d’entraînement et l’erreur de validation. Que
se passe-t-il? Quelles sont les trois manières de résoudre le problème?
9. Supposons que vous utilisiez une régression ridge. Vous remarquez
que l’erreur d’entraînement et l’erreur de validation sont à peu
près identiques et assez élevées : à votre avis, est-ce le biais ou la
variance du modèle qui est trop élevé(e) ? Devez-vous accroître
l’hyperparamètre de régularisation α ou le réduire?
10. Qu’est-ce qui pourrait vous inciter à choisir une régression…
– ridge plutôt qu’une simple (c.-à-d. sans régularisation) ?
– lasso plutôt que ridge?
– elastic net plutôt que lasso?
11. Supposons que vous vouliez classer des photos en extérieur/intérieur
et jour/nuit. Devez-vous utiliser deux classicateurs de régression
logistique ou un classicateur de régression softmax?
12. Implémentez une descente de gradient ordinaire avec arrêt précoce
pour une régression softmax sans utiliser Scikit-Learn, mais
uniquement NumPy. Utilisez-la pour une tâche de classication
telle que celle sur le jeu Iris.
Les solutions de ces exercices sont données à l’annexeA.
2
Introduction aux
réseaux de neurones
articielsavecKeras
Les oiseaux nous ont donné l’envie de voler, la bardane est à l’origine du Velcro, et
bien d’autres inventions se sont inspirées de la nature. Il est donc naturel de s’ins-
pirer du fonctionnement du cerveau pour construire une machine intelligente. Voilà
la logique à l’origine des réseaux de neurones articiels : il s’agit d’un modèle d’ap-
prentissage automatique inspiré des réseaux de neurones biologiques que l’on trouve
dans notre cerveau. Cependant, même si les avions ont les oiseaux pour modèle, ils
ne battent pas des ailes pour voler. De façon comparable, les réseaux de neurones
articiels sont progressivement devenus assez différents de leurs cousins biologiques.
Certains chercheurs soutiennent même qu’il faudrait éviter totalement l’analogie
biologique, par exemple en parlant d’unités au lieu de neurones, de peur que nous ne
limitions notre créativité aux systèmes biologiquement plausibles21.
Les réseaux de neurones articiels sont au cœur de l’apprentissage profond. Ils
sont polyvalents, puissants et extensibles, ce qui les rend parfaitement adaptés aux
tâches d’apprentissage automatique extrêmement complexes, comme la classication
de milliards d’images (p.ex., Google Images), la reconnaissance vocale (p. ex., Apple
Siri), la recommandation de vidéos auprès de centaines de millions d’utilisateurs
(p.ex., YouTube) ou l’apprentissage nécessaire pour battre le champion du monde du
jeu de go (AlphaGo de DeepMind).
La première partie de ce chapitre est une introduction à ces réseaux de neurones arti-
ciels, en commençant par une description rapide des toutes premières architectures.
Nous présenterons ensuite les perceptrons multicouches, qui sont largement employés
aujourd’hui (d’autres architectures seront détaillées dans les chapitres suivants).
21. Nous pouvons garder le meilleur des deux mondes en restant ouverts aux sources d’inspiration natu-
relles, sans craindre de créer des modèles biologiques irréalistes, tant qu’ils fonctionnent bien.
54 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
22. Warren S. McCulloch et Walter Pitts, « A Logical Calculus of the Ideas Immanent in Nervous Acti-
vity », The Bulletin of Mathematical Biology, 5, n°4 (1943), 115-113 : https://homl.info/43.
23. Voir le chapitre5 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition, 2023).
2.1 Du biologique à l’artificiel 55
2.1.1 Neuronesbiologiques
Avant d’aborder les neurones articiels, examinons rapidement un neurone bio-
logique (voir la gure 2.1). Il s’agit d’une cellule à l’aspect inhabituel que l’on
trouve principalement dans les cerveaux des animaux. Elle est constituée d’un
corps cellulaire, qui comprend le noyau et la plupart des éléments complexes de
la cellule, ainsi que de nombreux prolongements appelés dendrites et un très long
prolongement appelé axone. L’axone peut être juste un peu plus long que le corps
cellulaire, mais aussi jusqu’à des dizaines de milliers de fois plus long. Près de son
extrémité, il se décompose en plusieurs ramications appelées télodendrons, qui se
terminent par des structures minuscules appelées synapses terminales (ou simple-
ment synapses) et reliées aux dendrites ou directement aux corps cellulaire d’autres
neurones24 . Les neurones biologiques produisent de courtes impulsions électriques
appelées potentiels d’action (PA, ou signaux), qui voyagent le long des axones et
déclenchent, au niveau des synapses, la libération de signaux chimiques appelés
neurotransmetteurs. Lorsqu’un neurone reçoit en quelques millisecondes un nombre
sufsant de ces neurotransmetteurs, il déclenche ses propres impulsions électriques
(en réalité cela dépend des neurotransmetteurs, car certains d’entre eux inhibent
ce déclenchement).
24. En réalité, elles ne sont pas reliées, juste sufsamment proches pour échanger très rapidement des
signaux chimiques.
56 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Corps cellulaire
Axone Télodendrons
Noyau
Cône
d’émergence Synapses
Appareil de Golgi
Réticulum
endoplasmique
Mitochondrie Dendrite
Ramifications dendritiques
2.1.2 Calculslogiquesavecdesneurones
McCulloch et Pitts ont proposé un modèle très simple de neurone biologique,
c’est-à-dire le premier neurone articiel : il présente une ou plusieurs entrées binaires
(active/inactive) et une sortie binaire. Le neurone articiel active simplement sa
sortie lorsque le nombre de ses entrées actives dépasse un certain seuil. Ils ont montré
que, malgré la simplicité de ce modèle, il est possible de construire un réseau de neu-
rones articiels pouvant calculer n’importe quelle proposition logique. Par exemple,
nous pouvons construire des réseaux de neurones articiels qui effectuent différents
calculs logiques (voir la gure2.3), en supposant qu’un neurone est activé lorsqu’au
moins deux de ses connexions d’entrée le sont.
Neurones Connexion ∧ = ET
∨ = OU
¬ = NON
C C C C
A A B A B A B
C=A C = A ∧B C=A∨B C = A ∧ ¬B
2.1.3 Le perceptron
Le perceptron, inventé en 1957 par Frank Rosenblatt, est l’une des architectures de
réseau de neurones articiels les plus simples. Il se fonde sur un neurone articiel
58 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
légèrement différent (voir la gure 2.4), appelé unité logique à seuil (en anglais,
threshold logic unit ou TLU) ou parfois unité linéaire à seuil (linear threshold unit). Les
entrées et la sortie sont à présent des nombres (à la place de valeurs binaires, actif/
inactif) et chaque connexion en entrée possède un poids. La TLU calcule une
somme pondérée des entrées (z = w 1 x1 + w 2 x2 + … +w nx n+b = x T w + b), puis elle
applique une fonction échelon (step function) à cette somme et produit le résultat :
hw(x) = step(z) = step(xTw). Ceci ressemble beaucoup à une régression logistique, à
ceci près qu’on applique une fonction échelon au lieu de la fonction sigmoïde (voir
chapitre1). Tout comme dans la régression logistique, les paramètres du modèle sont
les poids d’entrée w et le terme constant (en anglais, bias)b.
w1 w2 w3 Poids
x1 x2 x 3 Entrées
Figure2.4 – Une unité logique à seuil : un neurone artificiel qui calcule une somme pondérée
de ses entrées plus un terme constant b, puis lui applique une fonction échelon
–1 si z < 0
0 si z < 0
heaviside(z) = sgn(z) = 0 si z = 0
1 si z ≥ 0
+1 si z > 0
Une seule TLU peut être employée pour une classication binaire linéaire simple.
Il calcule une fonction linéaire de ses entrées et, si le résultat dépasse un seuil, pré-
sente en sortie la classe positive, sinon la classe négative. Ceci peut évoquer pour
vous une régression logistique (voir chapitre1) ou un classicateur SVM linéaire 28.
Vous pourriez, par exemple, utiliser une seule TLU pour classer les iris en fonction de
la longueur et de la largeur des pétales. L’entraînement d’une telle TLU consisterait
à trouver les valeurs appropriées pour les poids w1, w2 et b2 (l’algorithme d’entraîne-
ment sera présenté plus loin).
28. Voir le chapitre 5 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition, 2023).
2.1 Du biologique à l’artificiel 59
Sorties
TLU Couche
� � � de sortie
x1 x2 Couche
Entrées d’entrée
29. Le terme perceptron est parfois employé pour désigner un minuscule réseau constitué d’une seule TLU.
60 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
w(éta
i, j
pe suivante)
= wi, j + η(y j – yˆ j )x i
Dans cette équation:
• wi,j correspond au poids de la connexion entre le i ème neurone d’entrée et le
jème neurone de sortie ;
• xi est la ième valeur d’entrée de l’instance d’entraînement courante ;
yj • ŷj est la sortie du jème neurone de sortie pour l’instance d’entraînement courante ;
• yj est la sortie souhaitée pour le jème neurone de sortie pour l’instance
d’entraînement courante ;
• η est le taux d’apprentissage (voir chapitre1).
Puisque la frontière de décision de chaque neurone de sortie est linéaire, les per-
ceptrons sont incapables d’apprendre des motifs complexes (tout comme les classi-
cateurs à régression logistique). Cependant, si les instances d’entraînement peuvent
être séparées de façon linéaire, Rosenblatt a montré que l’algorithme converge forcé-
ment vers une solution30. Il s’agit du théorème de convergence du perceptron.
Scikit-Learn fournit une classe Perceptron qui implémente un réseau de TLU.
Nous pouvons l’employer très facilement, par exemple sur le jeu de données Iris (pré-
senté au chapitre1):
30. Notez que cette solution n’est pas unique : lorsque les points peuvent être séparés de façon linéaire, il
existe une innité d’hyperplans qui peuvent les séparer.
2.1 Du biologique à l’artificiel 61
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import Perceptron
iris = load_iris(as_frame=True)
X = iris.data[["petal length (cm)", "petal width (cm)"]].values
y = (iris.target == 0) # Iris setosa
per_clf = Perceptron(random_state=42)
per_clf.fit(X, y)
x2
–½
�
1
–1 1
TLU –3 ⁄2 –½
� �
0 x1 1 1
0 1 1 1
x1 x2
2.1.4 Perceptronmulticoucheetrétropropagation
Un perceptron multicouche est constitué d’une couche d’entrée (de transfert uni-
quement), d’une ou plusieurs couches de TLU appelées couches cachées et d’une
dernière couche de TLU appelée couche de sortie (voir la gure2.7). Les couches
proches de la couche d’entrée sont généralement appelées les couches basses, tandis
que celles proches des sorties sont les couches hautes.
Couche
� � � de sortie
Couche
� � � � cachée
Couche
x1 x2 d’entrée
31. Par exemple, quand les entrées sont (0, 1) le neurone en bas à gauche calcule 0 × 1 + 1 × 1 – 3 / 2 = –1 / 2,
qui est une valeur négative, et sa sortie vaut donc 0. Le neurone en bas à droite calcule 0 × 1 +1 × 1 – 1 / 2 = 1 / 2,
qui est une valeur positive, et sa sortie vaut donc 1. Le neurone de sortie reçoit en entrée les sorties des deux
neurones précédents et calcule 0 × (–1) + 1 × 1 – 1 / 2 = 1 / 2. Cette valeur étant positive, il produit en sortie1.
2.1 Du biologique à l’artificiel 63
Puisque le signal va dans une seule direction (des entrées vers les sor-
ties), cette architecture est un exemple de réseau de neurones à pro-
pagation avant (en anglais, feedforward neural network) ou réseau de
neurones non bouclé.
32. Dans les années 1990, un réseau possédant plus de deux couches cachées était considéré profond.
Aujourd’hui, il n’est pas rare qu’ils comportent des dizaines de couches, voire des centaines. Ladénition
de « profond » est donc relativement oue.
64 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
33. David Rumelhart et al., Learning Internal Representations by Error Propagation (rapport technique du
Defense Technical Information Center, septembre1985).
2.1 Du biologique à l’artificiel 65
34. Étant donné que les neurones biologiques semblent mettre en œuvre une fonction d’activation de type
sigmoïde (en forme de « S »), les chercheurs se sont longtemps bornés à des fonctions de ce type. Mais, en
général, on constate que la fonction d’activation ReLU convient mieux aux réseaux de neurones articiels.
Voilà l’un des cas où l’analogie avec la nature a pu induire en erreur.
66 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Vous savez à présent d’où viennent les réseaux neuronaux, quelle est leur archi-
tecture et comment leurs sorties sont calculées. Vous avez également découvert
l’algorithme de rétropropogation. Mais à quoi pouvons-nous réellement employer
ces réseaux ?
2.1.5 Perceptronmulticouchederégression
Tout d’abord, les perceptrons multicouches peuvent être utilisés pour des tâches de
régression. Si nous souhaitons prédire une seule valeur (par exemple le prix d’une
maison en fonction de ses caractéristiques), nous n’avons besoin que d’un seul neu-
rone de sortie : sa sortie sera la valeur prédite. Pour une régression multivariable
(c’est-à-dire prédire de multiples valeurs à la fois), nous avons besoin d’un neurone
de sortie poue chaque dimension de sortie. Par exemple, pour localiser le centre d’un
objet dans une image, nous devons prédire des coordonnées en 2D et avons donc
besoin de deux neurones de sortie. Pour placer un cadre d’encombrement autour de
l’objet, nous avons besoin de deux valeurs supplémentaires : la largeur et la hauteur
de l’objet. Nous arrivons donc à quatre neurones de sortie.
Scikit-Learn comporte une classe MLPRegressor (MLP pour multi-layer per-
ceptron) que nous allons utiliser pour construire un perceptron multi-couches com-
portant trois couches cachées de 50 neurones chacune et l’entraîner sur le jeu de
données immobilières de Californie. Pour simplier, nous utiliserons la fonction
fetch_california_housing() de Scikit-Learn pour charger les données. Ce
jeu de données est plus simple que celui utilisé au chapitre2 de l’ouvrage Machine
Learning avec Scikit-Learn (Aurélien Géron, 3eédition, 2023), car il ne comporte que
des variables quantitatives (il ne comporte plus de variable ocean_proximity)
et il n’y a plus de données manquantes. Le code qui suit récupère le jeu de données
2.1 Du biologique à l’artificiel 67
et le partage, puis il crée un pipeline qui va normaliser les variables avant de les
transmettre à MLPRegressor. C’est très important pour les réseaux de neurones,
car ils sont entraînés à l’aide d’une descente de gradient et nous avons vu au cha-
pitre1 que la convergence est moins efcace lorsque les variables ont des échelles
très différentes. Enn, le code entraîne le modèle et évalue son erreur de validation.
Le modèle utilise la fonction d’activation ReLU dans les couches cachées, et il uti-
lise une variante de la descente de gradient dénommée Adam (voir chapitre3) pour
minimiser l’erreur quadratique moyenne, avec un petit peu de régularisation ℓ2 (que
vous pouvez contrôler à l’aide de l’hyperparamètre alpha) :
from sklearn.datasets import fetch_california_housing
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
housing = fetch_california_housing()
X_train_full, X_test, y_train_full, y_test = train_test_split(
housing.data, housing.target, random_state=42)
X_train, X_valid, y_train, y_valid = train_test_split(
X_train_full, y_train_full, random_state=42)
Nous obtenons une RMSE de validation d’environ 0,505, ce qui est comparable à
ce que nous obtiendrions avec un classicateur de forêt aléatoire. Pas trop mal pour
un premier essai !
Remarquez que ce perceptron multicouche n’utilise aucune fonction d’activation
pour la couche de sortie, ce qui le rend libre de fournir toute valeur qu’il souhaite.
En général, cela ne pose pas de problème, mais si vous voulez garantir que la sortie
soit toujours positive, alors vous devriez utiliser la fonction d’activation ReLU dans
la couche de sortie, ou la fonction d’activation softplus, qui est une variante
lissée de ReLU: softplus(z) = log(1 + exp(z)). Softplus donne une valeur proche de
0 lorsque z est négatif, et proche de z lorsque celui-ci est positif. Enn, si vous voulez
garantir que les prédictions tombent toujours dans une plage de valeurs donnée, vous
pouvez utiliser la fonction sigmoïde ou la tangente hyperbolique, puis recalibrer les
valeurs cibles dans la plage appropriée: 0 à 1 pour la fonction sigmoïde, et –1 à 1 pour
la tangente hyperbolique. Malheureusement, la classe MLPRegressor ne propose
pas de fonction d’activation dans la couche de sortie.
Hyperparamètre Valeurtype
Dépend du problème, mais en général entre
Nombre de couches cachées
1 et 5
Dépend du problème, mais en général entre
Nombre de neurones par couche cachée
10 et 100
Nombre de neurones de sortie 1 par dimension de prédiction
Fonction d’activation de la couche cachée ReLU
Aucune ou ReLU/softplus (pour des sorties
Fonction d’activation de la sortie positives) ou sigmoid/tanh (pour des sorties
bornées)
Fonction de perte MSE ou Huber en cas de valeurs aberrantes
2.1.6 Perceptronmulticouchedeclassication
Les perceptrons multicouches peuvent également être employés pour des tâches
de classification. Dans le cas d’un problème de classification binaire, nous avons
besoin d’un seul neurone de sortie avec la fonction d’activation sigmoïde : la
sortie sera une valeur entre 0 et 1, que nous pouvons interpréter comme la pro-
babilité estimée de la classe positive. La probabilité estimée de la classe négative
est égale à 1 moins cette valeur
Ils sont également capables de prendre en charge les tâches de classication
binaire à étiquettes multiples35. Par exemple, nous pouvons disposer d’un système
de classication des courriers électroniques qui prédit si chaque message entrant est
un courrier sollicité (ham) ou non sollicité (spam), tout en prédisant s’il est urgent
ou non. Dans ce cas, nous avons besoin de deux neurones de sortie, tous deux avec
la fonction d’activation sigmoïde : le premier indiquera la probabilité que le courrier
soit non sollicité, tandis que le second indiquera la probabilité qu’il soit urgent. Plus
généralement, nous affectons un neurone de sortie à chaque classe positive. Notez
35. Voir le chapitre3 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition, 2023).
2.1 Du biologique à l’artificiel 69
que le total des probabilités de sortie ne doit pas nécessairement être égal à 1. Cela
permet au modèle de sortir toute combinaison d’étiquettes : nous pouvons avoir du
courrier sollicité non urgent, du courrier sollicité urgent, du courrier non sollicité
non urgent et même du courrier non sollicité urgent (mais ce cas sera probablement
une erreur).
Lorsque chaque instance ne peut appartenir qu’à une seule classe, parmi trois
classes possibles ou plus (par exemple, les classes 0 à 9 pour la classication d’image
de chiffres), nous avons besoin d’un neurone de sortie par classe et nous pouvons uti-
liser la fonction d’activation softmax pour l’intégralité de la couche de sortie (voir la
gure2.9). Cette fonction36 s’assurera que toutes les probabilités estimées sont com-
prises entre 0 et 1 et que leur somme est égale à 1 (ce qui est obligatoire si les classes
sont exclusives). Comme nous l’avons vu au chapitre1, il s’agit d’une classication
multiclasse.
En ce qui concerne la fonction de perte, étant donné que nous prédisons des distri-
butions de probabilités, la perte d’entropie croisée (encore appelée x-entropie ou perte
logistique, voir chapitre1) est en général un bon choix.
Softmax
Couche
� � � de sortie
ReLU
Couche
� � � � cachée
Couche
x1 x2 d’entrée
Tableau2.2–Architecturetyped’unperceptronmulticouchedeclassication
Nombre de
De 1 à 5 couches en général, selon la tâche
couches cachées
Nombre de
1 1 par étiquette binaire 1 par classe
neurones de sortie
Fonction d’activation
de la couche de Sigmoïde Sigmoïde Softmax
sortie
Fonction de perte Entropie croisée Entropie croisée Entropie croisée
Avant d’aller plus loin, nous vous conseillons de faire l’exercice 1 donné
à la fin de ce chapitre. Il vous permettra de jouer avec diverses architec-
tures de réseaux de neurones et de visualiser leurs sorties avec TensorFlow
Playground. Ainsi, vous comprendrez mieux les perceptrons multicouches,
y compris les effets de tous les hyperparamètres (nombre de couches et de
neurones, fonctions d’activation, etc.).
À présent que les concepts ont été établis, nous pouvons commencer à implé-
menter des perceptrons multicouches avec Keras!
37. Projet ONEIROS (Open-ended Neuro-Electronic Intelligent Robot Operating System). François Chollet a
rejoint Google en 2015, où il continue à diriger le projet Keras.
2.2 Implémenter un perceptron multicouche avec Keras 71
Keras est une API de haut niveau et n’effectue pas elle-même les opérations
de bas niveau : elle fait appel pour cela à une bibliothèque sous-jacente (ou
backend). Par le passé, plusieurs backends étaient disponibles : TensorFlow,
PlaidML, Theano, ou encore Microsoft Cognitive Toolkit (CNTK). Cepen-
dant, la plupart de ces bibliothèques étant devenues obsolètes, Keras s’est
focalisée uniquement sur TensorFlow. En parallèle, TensorFlow a intégré sa
propre implémentation de l’API Keras, nommée tf.keras! Heureusement, les
choses se sont simplifiées depuis TensorFlow 2.0, car tf.keras est devenue
un simple alias vers la bibliothèque Keras officielle. Ouf ! Pour finir, la ver-
sion 3.0 de Keras, parue en décembre 2023, supporte à nouveau plu-
sieurs backends : TensorFlow, PyTorch 38, ou JAX. Si vous préférez utiliser
le backend PyTorch ou JAX, vous devez installer la bibliothèque corres-
pondante, définir la variable d’environnement KERAS_BACKEND (dont la
valeur doit être "tensorflow", "torch" ou "jax"), et utiliser keras
plutôt que tf.keras dans votre code. La plupart des exemples de code
Keras devraient fonctionner sans autre modification (voir https://keras.io
pour plus de détails).
TensorFlow est en général importé sous le nom tf, et l’API Keras est dispo-
nible via tf.keras.
Pour une question de simplicité, nous allons réduire les intensités de pixels à
la plage 0-1 en les divisant par 255,0 (cela les convertit également en nombres à
virgule flottante) :
X_train, X_valid, X_test = X_train / 255., X_valid / 255., X_test / 255.
Avec MNIST, lorsque l’étiquette est égale à 5, cela signie que l’image représente
le chiffre manuscrit 5. Facile. En revanche, avec Fashion MNIST, nous avons besoin
de la liste des noms de classes pour savoir ce que nous manipulons :
class_names = ["T-shirt/top", "Trouser", "Pullover", "Dress", "Coat",
"Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"]
Détaillons ce code :
• Tout d’abord, dénissons le germe (en anglais, seed) du générateur de nombres
aléatoires de TensorFlow an de pouvoir reproduire les résultats : de cette
manière, les poids aléatoires des couches cachées et de la couche de sortie seront
les mêmes à chaque fois que vous exécuterez votre notebook. Vous pourriez aussi
choisir d’utiliser la fonction tf.keras.utils.set_random_seed(),
qui permet de dénir commodément les germes aléatoires pour TensorFlow,
Python et NumPy.
• La ligne suivante crée un modèle Sequential. C’est le modèle Keras le plus
simple pour les réseaux de neurones : il est constitué d’une seule pile de couches
connectées de façon séquentielle. Il s’agit de l’API de modèle séquentiel, ou API
séquentielle.
• Ensuite, nous construisons la première couche (une couche d’entrée, de type
Input) et l’ajoutons au modèle. Nous spécions sa forme (shape), qui
n’inclut pas la taille du lot mais uniquement la forme des instances. Keras a
besoin de la forme des entrées pour déterminer la forme de la matrice des poids
de connexion de la première couche cachée.
• Nous ajoutons alors une couche Flatten, dont le rôle est de convertir chaque
image d’entrée en un tableau à une dimension : à titre d’exemple, si elle reçoit un
lot de forme [32, 28, 28], elle le reformatera en un tableau [32, 784]. Autrement
74 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
dit, si elle reçoit une donnée d’entrée X, elle calcule X.reshape(-1, 784).
Cette couche ne possède aucun paramètre et a pour seule fonction d’effectuer
un prétraitement simple.
• Puis, nous ajoutons une couche cachée Dense constituée de 300 neurones. Elle
utilisera la fonction d’activation ReLU. Chaque couche Dense gère sa propre
matrice de poids, qui contient tous les poids des connexions entre les neurones
et leurs entrées. Elle gère également un vecteur de termes constants (un par
neurone). Lorsqu’elle reçoit des données d’entrée, elle calcule l’équation2.2.
• Une deuxième couche cachée Dense de 100 neurones est ensuite ajoutée, elle
aussi avec la fonction d’activation ReLU.
• Enn, nous ajoutons une couche de sortie Dense avec 10 neurones (un
par classe) en utilisant la fonction d’activation softmax car les classes sont
exclusives.
40. Vous pouvez aussi utiliser tf.keras.utils.plot_model() pour générer une image du modèle.
2.2 Implémenter un perceptron multicouche avec Keras 75
L’état global de tout ce qui est géré par Keras est conservé dans une session
Keras ; vous pouvez l’effacer en utilisant tf.keras.backend.clear_
session(). Ceci réinitialise en particulier les compteurs de noms.
Vous pouvez aisément obtenir la liste des couches du modèle grâce à son attribut
layers, ou utiliser la méthode get_layer() pour accéder à une couche de nom
donné :
>>> model.layers
[<keras.layers.core.flatten.Flatten at 0x7fa1dea02250>,
<keras.layers.core.dense.Dense at 0x7fa1c8f42520>,
<keras.layers.core.dense.Dense at 0x7fa188be7ac0>,
<keras.layers.core.dense.Dense at 0x7fa188be7fa0>]
>>> hidden1 = model.layers[1]
>>> hidden1.name
'dense'
>>> model.get_layer('dense') is hidden1
True
76 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Tous les paramètres d’une couche sont accessibles à l’aide des méthodes get_
weights() et set_weights(). Dans le cas d’une couche Dense, cela comprend
à la fois les poids des connexions et les termes constants :
>>> weights, biases = hidden1.get_weights()
>>> weights
array([[ 0.02448617, -0.00877795, -0.02189048, ..., 0.03859074, -0.06889391],
[ 0.00476504, -0.03105379, -0.0586676 , ..., -0.02763776, -0.04165364],
...,
[ 0.07061854, -0.06960931, 0.07038955, ..., 0.00034875, 0.02878492],
[-0.06022581, 0.01577859, -0.02585464, ..., 0.00272203, -0.06793761]],
dtype=float32)
>>> weights.shape
(784, 300)
>>> biases
array([0., 0., 0., 0., 0., 0., 0., 0., 0., ..., 0., 0., 0.], dtype=float32)
>>> biases.shape
(300,)
La couche Dense initialise les poids des connexions de façon aléatoire (indispen-
sable pour briser la symétrie, comme nous l’avons vu précédemment) et les termes
constants à zéro, ce qui convient parfaitement. Pour employer une méthode d’initia-
lisation différente, il suft de xer kernel_initializer (kernel, ou noyau, est
un autre nom pour la matrice des poids des connexions) ou bias_initializer
au moment de la création de la couche. Nous reviendrons sur les initialiseurs au cha-
pitre3, mais vous en trouverez la liste complète à l’adresse https://keras.io/api/layers/
initializers.
Nous lui fournissons les caractéristiques d’entrée (X_train) et les classes cibles
(y_train), ainsi que le nombre d’époques d’entraînement (dans le cas contraire, il
n’y en aurait qu’une, ce qui serait clairement insufsant pour converger vers une bonne
solution). Nous passons également un jeu de validation (facultatif). Keras calculera la
perte et les métriques supplémentaires sur ce jeu à la n de chaque époque, ce qui se
révélera très utile pour déterminer les performances réelles du modèle. Si les perfor-
mances sur le jeu d’entraînement sont bien meilleures que sur le jeu de validation, il est
probable que le modèle surajuste le jeu d’entraînement ou qu’il y a une erreur, comme
une différence entre les données du jeu d’entraînement et celles du jeu de validation.
Il est fréquent d’obtenir des performances légèrement moins bonnes sur le jeu
de test que sur le jeu de validation41. En effet, les hyperparamètres sont ajustés non
pas sur le jeu de test mais sur le jeu de validation (cependant, dans cet exemple,
puisque les hyperparamètres n’ont pas été afnés, l’exactitude inférieure est juste
due à la malchance). Résistez à la tentation d’ajuster les hyperparamètres sur le jeu
de test car votre estimation de l’erreur de généralisation sera alors trop optimiste.
Pour chaque instance, le modèle estime une probabilité par classe, de la classe0
à la classe 9. C’est semblable à la sortie de la méthode predict_proba() des
classicateurs de Scikit-Learn. Par exemple, pour la première image, il estime que la
probabilité de la classe9 (bottine) est de 96%, que celle de la classe7 (sneaker) est
de 2%, que celle de la classe 5(sandale) est de 1%, et que celles des autres classes
sont négligeables. Autrement dit, il « croit » que la première image est une chaus-
sure, probablement une bottine, mais éventuellement une sandale ou une basket. Si
nous nous intéressons uniquement à la classe dont la probabilité estimée est la plus
élevée (même sicelle-ci est relativement faible), nous pouvons utiliser à la place la
méthode argmax() pour obtenir pour chaque instance l’index de la classe ayant la
plus grande probabilité:
>>> import numpy as np
>>> y_pred = y_proba.argmax(axis=-1)
>>> y_pred
array([9, 2, 1])
>>> np.array(class_names)[y_pred]
array(['Ankle boot', 'Pullover', 'Trouser'], dtype='<U11')
Le classicateur réalise une classication correcte des trois images (elles sont
représentées à la gure2.12) :
>>> y_new = y_test[:3]
>>> y_new
array([9, 2, 1], dtype=uint8)
41. Voir le chapitre2 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3eédition, 2023).
82 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Vous savez à présent utiliser l’API séquentielle pour construire, entraîner, évaluer et
utiliser un perceptron multicouche de classication. Mais qu’en est-il de la régression ?
2.2.3 Construireunmodèlecomplexeavecl’APIfonctionnelle
Un réseau de neurones Wide & Deep est un exemple de réseau non séquentiel. Cette
architecture a été présentée en 2016 dans un article publié par Heng-Tze Cheng etal.42
Elle connecte tout ou partie des entrées directement à la couche de sortie (voir la
gure2.13). Grâce à cette architecture, le réseau de neurones est capable d’apprendre
à la fois les motifs profonds (en utilisant le chemin profond) et les règles simples (au
travers du chemin court)43. À l’opposé, un perceptron multicouche classique oblige
toutes les données à passer par l’intégralité des couches, et cette suite de transforma-
tions peut nir par déformer les motifs simples présents dans les données.
Couche de sortie
Concaténation
Couche cachée 2
Wide Deep
Couche cachée 1
Normalisation
Couche d’entrée
42. Heng-Tze Cheng et al., « Wide & Deep Learning for Recommender Systems », Proceedings of the First
Workshop on Deep Learning for Recommender Systems (2016), 7-10 : https://homl.info/widedeep.
43. Le chemin court peut également servir à fournir au réseau de neurones des caractéristiques préparées
manuellement.
84 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
input_ = tf.keras.layers.Input(shape=X_train.shape[1:])
normalized = normalization_layer(input_)
hidden1 = hidden_layer1(normalized)
hidden2 = hidden_layer2(hidden1)
concat = concat_layer([normalized, hidden2])
output = output_layer(concat)
Globalement, les cinq premières lignes créent l’ensemble des couches du modèle,
les six lignes suivantes utilisent ces couches comme des fonctions pour passer de l’en-
trée à la sortie, et la dernière ligne crée un objet Keras Model pointant vers l’entrée
et la sortie. Étudions maintenant ce code de manière plus détaillée:
• Nous créons d’abord cinq couches : une couche Normalization pour
centrer et réduire les données d’entrée, deux couches Dense de 30 neurones
chacune utilisant la fonction d’activation ReLU, une couche Concatenate,
et enn une nouvelle couche Dense avec un seul neurone en tant que couche
de sortie, sans aucune fonction d’activation.
• Puis nous créons un objet Input (nous utilisons le nom de variable input_
pour éviter tout conit de nom avec la fonction intégrée input() de Python).
Il spécie le type de l’entrée qui sera fournie au modèle, y compris sa forme
(shape) et son type (dtype). En réalité, un modèle peut avoir plusieurs
entrées, comme nous le verrons plus loin.
• Ensuite, nous utilisons la couche Normalization comme une fonction, en
lui transmettant l’objet Input. Voilà pourquoi cette méthode de construction
porte le nom d’API fonctionnelle. Nous indiquons simplement à Keras comment
il doit connecter les couches ; aucune donnée réelle n’est encore traitée, étant
donné que l’objet Input n’est qu’une spécication de données. Autrement
dit, c’est une entrée symbolique. La sortie de cet appel de fonction est aussi
symbolique : normalized ne stocke aucune donnée véritable mais sert
simplement à construire le modèle.
• De même, nous transmettons alors normalized à hidden_layer1, qui
produit en sortie hidden1, que nous transmettons à hidden_layer2, qui
renvoie en sortie hidden2.
• Jusqu’ici nous avons connecté les couches séquentiellement, mais nous
utilisons ensuite concat_layer pour concaténer l’entrée à la sortie de la
seconde couche cachée. Là encore, il n’y a aucune concaténation effective
pour l’instant : il ne s’agit que d’une opération symbolique, pour construire le
modèle.
2.2 Implémenter un perceptron multicouche avec Keras 85
Couche de sortie
Concaténation
Couche cachée 2
Couche cachée 1
Normalisation Normalisation
Couche Couche
d’entrée Wide d’entrée Deep
norm_layer_wide.adapt(X_train_wide)
norm_layer_deep.adapt(X_train_deep)
history = model.fit((X_train_wide, X_train_deep), y_train, epochs=20,
validation_data=((X_valid_wide, X_valid_deep), y_valid))
mse_test = model.evaluate((X_test_wide, X_test_deep), y_test)
y_pred = model.predict((X_new_wide, X_new_deep))
tâches. Par exemple, nous pouvons effectuer une classication multitâche sur
des images de visages en utilisant une sortie pour classer l’expression faciale
de la personne (sourire, surprise, etc.) et une autre pour déterminer si elle
porte des lunettes.
• La technique de la régularisation (c’est-à-dire une contrainte d’entraînement
dont l’objectif est de réduire le surajustement et d’améliorer ainsi la capacité de
généralisation du modèle) constitue un autre cas d’utilisation. Par exemple, nous
pourrions souhaiter ajouter des sorties supplémentaires dans une architecture
de réseau de neurones (voir la gure2.15) an que la partie sous-jacente du
réseau apprenne par elle-même quelque chose d’intéressant sans s’appuyer sur
le reste du réseau.
Concaténation
Couche cachée 2
Couche cachée 1
Normalisation Normalisation
Couche Couche
d’entrée Wide d’entrée Deep
Figure2.15 – Prise en charge des sorties multiples, dans ce cas pour ajouter une sortie
supplémentaire dans un but de régularisation
plus d’importance à la sortie principale qu’à la sortie auxiliaire (qui ne sert qu’à la
régularisation), nous voulons donner un poids supérieur à la perte de la sortie prin-
cipale. Heureusement, il est possible de dénir tous les poids des pertes lors de la
compilation du modèle :
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss=("mse", "mse"), loss_weights=(0.9, 0.1),
optimizer=optimizer, metrics=["RootMeanSquaredError"])
Comme vous pouvez le constater, vous pouvez construire toutes sortes d’architec-
tures avec l’API fonctionnelle. Voyons ensuite une dernière façon de construire des
modèles Keras.
2.2.4 Construireunmodèledynamiqueavecl’APIdesous-classement
L’API séquentielle et l’API fonctionnelle sont toutes deux déclaratives : nous
commençons par déclarer les couches à utiliser, ainsi que leurs connexions, et
ensuite seulement nous alimentons le modèle en données à des fins d’entraîne-
ment ou de prédiction.
Cette approche présente plusieurs avantages : le modèle peut facilement être sau-
vegardé, cloné et partagé; sa structure peut être afchée et analysée ; le framework
étant capable de déduire des formes et de vérier des types, les erreurs peuvent être
identiées précocement (c’est-à-dire avant que des données ne traversent le modèle).
Elle permet également un débogage facile, puisque l’intégralité du modèle est un
graphe statique de couches. En revanche, son inconvénient est justement son carac-
tère statique. Certains modèles demandent des boucles, des formes variables, des
branchements conditionnels et d’autres comportements dynamiques. Dans de tels
cas, ou simplement si un style de programmation plus impératif vous convient mieux,
vous pouvez vous tourner vers l’API de sous-classement de modèle ou API de sous-clas-
sement (en anglais, subclassing API).
Avec cette approche, il suft de créer une sous-classe de la classe Model, de créer
les couches requises dans le constructeur et de les utiliser ensuite dans la méthode
call() pour effectuer les calculs nécessaires. Par exemple, la création d’une ins-
tance de la classe WideAndDeepModel suivante donne un modèle équivalent à
celui construit précédemment à l’aide de l’API fonctionnelle:
class WideAndDeepModel(tf.keras.Model):
Cet exemple ressemble au précédent, excepté que nous séparons la création des
couches44 dans le constructeur de leur usage dans la méthode call(). Nous n’avons
pas besoin de créer d’objets Input: nous pouvons utiliser l’argument input de la
méthode call().
Maintenant que nous avons une instance de modèle, nous pouvons la compiler,
adapter ses couches de normalisation (en utilisant model.norm_layer_wide.
adapt(...) et model.norm_layer_deep.adapt(...)), l’ajuster, l’éva-
luer et l’utiliser pour faire des prédictions, exactement comme nous l’avons fait avec
l’API fonctionnelle.
La grande différence avec cette API est que nous pouvons inclure quasiment tout
ce que nous voulons dans la méthode call(): boucles for, instructions if, opéra-
tions TensorFlow de bas niveau –la seule limite sera votre imagination (voir le cha-
pitre4)! Cette API convient donc à ceux qui souhaitent expérimenter de nouvelles
idées, en particulier aux chercheurs. Cependant, cette souplesse supplémentaire a
un prix : l’architecture du modèle est cachée dans la méthode call(). Keras ne
peut donc pas facilement l’inspecter ; le modèle ne peut pas être cloné à l’aide de
tf.keras.models.clone_model() ; et, lors de l’appel à summary(), nous
n’obtenons qu’une liste de couches, sans information sur leurs interconnexions. Par
ailleurs, Keras étant incapable de vérier préalablement les types et les formes, les
erreurs sont faciles. En conséquence, à moins d’avoir réellement besoin de cette exi-
bilité, il est préférable de se cantonner à l’API séquentielle ou à l’API fonctionnelle.
Les modèles Keras peuvent être utilisés comme des couches normales.
Vous pouvez donc facilement les combiner pour construire des architec-
tures complexes.
2.2.5 Enregistreretrestaurerunmodèle
Enregistrer un modèle Keras entraîné peut difcilement être plus simple :
model.save("my_keras_model", save_format="tf")
44. Des modèles Keras disposant d’un attribut output, nous ne pouvons pas nommer ainsi la couche de
sortie principale. Nous choisissons donc main_output.
45. C’est à l’heure actuelle l’option par défaut, mais l’équipe Keras travaille sur un nouveau format qui
pourrait devenir l’option par défaut dans les versions futures, c’est pourquoi je préfère dénir explicitement
le format pour préserver l’avenir.
2.2 Implémenter un perceptron multicouche avec Keras 91
l’utiliser en production : SavedModel est sufsant (nous verrons comment cela fonc-
tionne au chapitre4). Le chier keras_metadata.pb contient des informations supplé-
mentaires dont Keras a besoin. Le sous-répertoire variables contient toutes les valeurs
des paramètres (y compris les poids des connexions, les termes constants, les données
de normalisation et les paramètres de l’optimiseur) partagés éventuellement entre
plusieurs chiers si le modèle est de très grande taille. Enn, le répertoire assets peut
contenir d’autres chiers, tels que des échantillons de données, des noms de variables,
des noms de classe, etc. Par défaut, le répertoire assets est vide. Étant donné que l’opti-
miseur est aussi sauvegardé, y compris ses hyperparamètres et ses états éventuels, vous
pouvez recharger le modèle et poursuivre l’entraînement si vous le souhaitez.
En général, vous aurez un script qui entraîne un modèle et l’enregistre, ainsi qu’un
ou plusieurs autres scripts (ou des services web) qui chargent le modèle et l’utilisent
pour l’évaluer ou effectuer des prédictions. Charger le modèle est tout aussi facile que
de l’enregistrer :
model = tf.keras.models.load_model("my_keras_model")
y_pred_main, y_pred_aux = model.predict((X_new_wide, X_new_deep))
2.2.6 Utiliserdesrappels
La méthode fit() accepte un argument callbacks permettant de spécier la
liste des objets que Keras appellera au début et à la n de l’entraînement, au début et
à la n de chaque époque, et même avant et après le traitement de chaque lot. Par
exemple, le rappel ModelCheckpoint enregistre des points de reprise du modèle
à intervalle régulier au cours de l’entraînement, par défaut à la n de chaque époque :
checkpoint_cb = tf.keras.callbacks.ModelCheckpoint("my_checkpoints",
save_weights_only=True)
history = model.fit([...], callbacks=[checkpoint_cb])
92 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Si nous avons besoin d’un plus grand contrôle, nous pouvons aisément écrire nos
propres rappels. Par exemple, le rappel personnalisé suivant afche le rapport entre
la perte de validation et la perte d’entraînement au cours de l’entraînement (par
exemple, pour détecter un surajustement) :
class PrintValTrainRatioCallback(tf.keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
ratio = logs["val_loss"] / logs["loss"]
print(f"Epoch={epoch}, val/train={ratio:.2f}")
2.2.7 UtiliserTensorBoardpourlavisualisation
TensorBoard est un excellent outil interactif de visualisation que nous pouvons
employer pour examiner les courbes d’apprentissage pendant l’entraînement, com-
parer les courbes et les métriques de plusieurs exécutions, visualiser le graphe de
calcul, analyser des statistiques d’entraînement, afcher des images générées par
notre modèle, visualiser des données multidimensionnelles complexes projetées en
3D et regroupées automatiquement pour nous, analyser votre réseau (c.-à-d. mesurer
son débit pour identier les goulots d’étranglement), etc. TensorBoard est installé en
même temps que TensorFlow. Cependant, il vous faudra une extension (ou plug-in)
de TensorBoard pour visualiser les données de prolage. Si vous avez suivi les instruc-
tions d’installation données sur https://homl.info/install de manière à tout exécuter
localement, alors l’extension est déjà installée, mais si vous utilisez Colab, alors vous
devez exécuter la commande suivante:
%pip install -q -U tensorboard-plugin-profile
Pour utiliser TensorBoard, nous devons modier notre programme an qu’il place
les données à visualiser dans des chiers de journalisation binaires spéciques appelés
chiers d’événements (en anglais, event les). Chaque enregistrement de données est
appelé résumé (summary). Le serveur TensorBoard surveille le répertoire de jour-
nalisation et récupère automatiquement les modications an de mettre à jour les
visualisations: nous pouvons ainsi visualiser des données dynamiques (avec un court
délai), comme les courbes d’apprentissage pendant l’entraînement. En général, nous
indiquons au serveur TensorBoard un répertoire de journalisation racine et congu-
rons notre programme de sorte qu’il écrive dans un sous-répertoire différent à chaque
exécution. De cette manière, la même instance de serveur TensorBoard nous permet
de visualiser et de comparer des données issues de plusieurs exécutions du programme,
sans que tout se mélange.
Appellons le répertoire de journalisation racine my_logs et dénissons également
une petite fonction qui générera un chemin de sous-répertoire en fonction de la date
et de l’heure courantes an que le nom soit différent à chaque exécution :
from pathlib import Path
from time import strftime
def get_run_logdir(root_logdir="my_logs"):
return Path(root_logdir) / strftime("run_%Y_%m_%d_%H_%M_%S")
Ce n’est pas plus compliqué que cela ! Dans cet exemple, il effectuera un prolage
du réseau entre les lots 100 et 200 durant la première époque. Pourquoi 100 et 200 ?
À vrai dire, il faut souvent un certain nombre de lots avant que le réseau de neurones
se « réveille », et donc vous ne voulez pas effectuer le prolage trop tôt. De plus, ce
prolage consomme des ressources, c’est pourquoi il est préférable de ne pas l’effec-
tuer pour chaque lot.
Ensuite, essayez de passer le taux d’apprentissage de 0,001 à 0,002 et ré-exécutez le
code, avec un nouveau sous-répertoire de journalisation. Vous obtiendrez une struc-
ture de répertoire semblable à celle-ci:
my_logs
├── run_2022_08_01_17_25_59
│ ├── train
│ │ ├── events.out.tfevents.1659331561.my_host_name.42042.0.v2
│ │ ├── events.out.tfevents.1659331562.my_host_name.profile-empty
│ │ └── plugins
│ │ └── profile
│ │ └── 2022_08_01_17_26_02
│ │ ├── my_host_name.input_pipeline.pb
│ │ └── [...]
│ └── validation
│ └── events.out.tfevents.1659331562.my_host_name.42042.1.v2
└── run_2022_08_01_17_31_12
└── [...]
disponible dont le numéro est supérieur ou égal à 6006 (à moins que vous n’ayez
choisi un port spécique en utilisant l’option --port).
%load_ext tensorboard
%tensorboard --logdir=./my_logs
Si vous exécutez tout sur votre propre machine, il est possible de démar-
rer TensorBoard en exécutant tensorboard --logdir=./my_logs
dans un terminal. Vous devez d’abord activer l’environnement Conda dans
lequel vous avez installé TensorBoard, puis vous rendre dans le réper-
toire handson-ml3. Une fois le serveur démarré, rendez-vous sur http://
localhost:6006.
Actualiser
( ) en haut à droite pour que TensorBoard rafraîchisse les données, ou cliquer sur le
bouton Paramètres ( ) pour activer le rafraîchissement automatique et en spécier
la fréquence.
Par ailleurs, TensorFlow offre aussi une API de bas niveau dans le package
tf.summary. Le code suivant crée un SummaryWriter à l’aide de la fonc-
tion create_file_writer() puis l’utilise comme contexte Python pour la
journalisation des valeurs scalaires, des histogrammes, des images, de l’audio et
du texte, autant d’éléments qui peuvent être visualisés avec TensorBoard :
test_logdir = get_run_logdir()
writer = tf.summary.create_file_writer(str(test_logdir))
with writer.as_default():
for step in range(1, 1000 + 1):
tf.summary.scalar("my_scalar", np.sin(step / 10), step=step)
texts = ["The step is " + str(step), "Its square is " + str(step ** 2)]
tf.summary.text("my_text", texts, step=step)
Vous pouvez partager vos résultats en ligne en les publiant sous https://
tensorboard.dev. Pour cela, exécutez simplement !tensorboard dev
upload --logdir ./my_logs. La première fois, vous devrez accep-
ter les conditions d’utilisation et vous authentifier. Après quoi vos fichiers
de journalisation seront chargés sur le serveur et vous obtiendrez un lien
permanent vous permettant de visualiser vos résultats dans une interface
TensorBoard.
Faisons le point sur ce que vous avez appris jusqu’ici dans ce chapitre. Vous savez
maintenant d’où viennent les réseaux de neurones, ce qu’est un perceptron multi-
couche et comment l’utiliser pour la classication et la régression, comment utiliser
l’API séquentielle de tf.keras pour construire un perceptron multicouche et com-
ment employer l’API fonctionnelle ou l’API de sous-classement pour construire des
modèles à l’architecture encore plus complexe (y compris des modèles Wide & Deep,
2.3 Régler précisément les hyperparamètres d’un réseau de neurones 97
ainsi que des modèles à entrées et sorties multiples). Nous avons expliqué comment
enregistrer et restaurer un modèle, utiliser les rappels pour les sauvegardes intermé-
diaires, l’arrêt précoce, et d’autres fonctions. Enn, nous avons présenté la visualisa-
tion avec TensorBoard. Avec ces connaissances, vous pouvez déjà utiliser les réseaux
de neurones pour vous attaquer à de nombreux problèmes! Mais vous vous demandez
peut-être comment choisir le nombre de couches cachées, le nombre de neurones
dans le réseau et tous les autres hyperparamètres. C’est ce que nous allons voir à
présent.
2.3 RÉGLERPRÉCISÉMENTLESHYPERPARAMÈTRES
D’UNRÉSEAUDENEURONES
La souplesse des réseaux de neurones constitue également l’un de leurs principaux
inconvénients : de nombreux hyperparamètres doivent être ajustés. Il est non seu-
lement possible d’utiliser n’importe quelle architecture de réseau imaginable mais,
même dans un simple perceptron multicouche, nous pouvons modier le nombre
de couches, le nombre de neurones par couche, le type de fonction d’activation
employée par chaque couche, la logique d’initialisation des poids, le type d’optimi-
seur à utiliser, son taux d’apprentissage, la taille des lots, etc. Dans ce cas, comment
pouvons-nous connaître la combinaison d’hyperparamètres la mieux adaptée à une
tâche ?
Une solution consiste à convertir votre modèle Keras en un estimateur Scikit-
Learn, puis à utiliser GridSearchCV ou RandomizedSearchCV pour régler
les hyperparamètres47 . Vous pouvez utiliser pour cela une classe enveloppe (en
anglais, wrapper class) de la bibliothèque SciKeras telle que KerasRegressor
et KerasClassifier (pour plus de détails, consultez https://github.com/adriangb/
scikeras). Cependant, il existe un meilleur moyen : vous pouvez utiliser la biblio-
thèque Keras Tuner, qui est une bibliothèque de réglage des hyperparamètres pour les
modèles Keras. Elle offre différentes stratégies d’optimisation, est hautement adap-
table et dispose d’une excellente intégration avec TensorBoard. Voyons comment
l’utiliser.
Si vous avez suivi les instructions d’installation de https://homl.info/install pour
tout exécuter localement, alors Keras Tuner est déjà installé; mais si vous utilisez
Colab, vous devrez exécuter %pip install -q -U keras-tuner. Ensuite,
importez keras tuner, en général sous le nom kt, puis écrivez une fonction qui
construit, compile et renvoie un modèle Keras. La fonction doit recevoir en argu-
ment un objet kt.HyperParameters, qu’elle pourra utiliser pour dénir les
hyperparamètres (entiers, ottants, chaînes, etc.) avec leurs intervalles de variation ;
ces hyperparamètres seront utilisés pour construire et compiler le modèle. À titre
d’exemple, la fonction suivante construit et compile un perceptron multicouche
pour classier les images Fashion MNIST, en utilisant des hyperparamètres tels que
le nombre de couches cachées (n_hidden), le nombre de neurones par couche
47. Voir le chapitre2 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3eédition, 2023).
98 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
def build_model(hp):
n_hidden = hp.Int("n_hidden", min_value=0, max_value=8, default=2)
n_neurons = hp.Int("n_neurons", min_value=16, max_value=256)
learning_rate = hp.Float("learning_rate", min_value=1e-4, max_value=1e-2,
sampling="log")
model = tf.keras.Sequential()
model.add(tf.keras.layers.Flatten())
for _ in range(n_hidden):
model.add(tf.keras.layers.Dense(n_neurons, activation="relu"))
model.add(tf.keras.layers.Dense(10, activation="softmax"))
model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,
metrics=["accuracy"])
return model
random_search_tuner = kt.RandomSearch(
build_model, objective="val_accuracy", max_trials=5, overwrite=True,
directory="my_fashion_mnist", project_name="my_rnd_search", seed=42)
random_search_tuner.search(X_train, y_train, epochs=10,
validation_data=(X_valid, y_valid))
Chaque tuner est guidé par un prétendu oracle : avant chaque essai, le tuner
demande à l’oracle ce que devrait être le prochain essai. Le tuner RandomSearch
utilise un RandomSearchOracle plutôt simpliste qui choisit simplement l’essai
suivant au hasard, comme nous l’avons vu précédemment. Sachant que l’oracle garde
trace de tous les essais, vous pouvez lui demander de vous donner le meilleur et vous
pouvez afcher un résumé de cet essai :
>>> best_trial = random_search_tuner.oracle.get_best_trials(num_trials=1)[0]
>>> best_trial.summary()
Trial summary
Hyperparameters:
n_hidden: 5
n_neurons: 70
learning_rate: 0.00041268008323824807
optimizer: adam
Score: 0.8736000061035156
Dans certains cas, vous pouvez vouloir ajuster les hyperparamètres de prétraite-
ment des données, ou les arguments de model.fit() tels que la taille de lot. Pour
cela, vous devrez utiliser une technique légèrement différente: au lieu d’écrire une
fonction build_model(), vous devez dénir une sous-classe de kt.HyperModel
avec deux méthodes, build() et fit(). La méthode build() fait exactement la
même chose que la fonction build_model(). La méthode fit() reçoit en argu-
ments un objet HyperParameters et un modèle compilé, ainsi que tous les argu-
ments de model.fit(), puis ajuste le modèle et renvoie l’objet History. Point
essentiel, la méthode fit() peut utiliser des hyperparamètres pour décider comment
prétraiter les données, choisir la taille de lot, etc. À titre d’exemple, la classe qui suit
construit le même modèle que précédemment, avec les mêmes hyperparamètres, mais
elle utilise aussi un hyperparamètre booléen "normalize" pour contrôler s’il faut
ou non centrer et réduire les données d’entraînement avant d’ajuster le modèle:
class MyClassificationHyperModel(kt.HyperModel):
def build(self, hp):
return build_model(hp)
Vous pouvez alors transmettre une instance de cette classe au tuner de votre choix, au
lieu de lui transmettre la fonction build_model. Construisons par exemple un tuner
kt.Hyperband basé sur une instance de MyClassificationHyperModel:
hyperband_tuner = kt.Hyperband(
MyClassificationHyperModel(), objective="val_accuracy", seed=42,
max_epochs=10, factor=3, hyperband_iterations=2,
overwrite=True, directory="my_fashion_mnist", project_name="hyperband")
48. Voir le chapitre2 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition, 2023).
49. L’algorithme Hyperband est en fait un peu plus sophistiqué que les partages en deux successifs de Halving,
voir sur https://homl.info/hyperband la publication de Lisha Li et al., «Hyperband: A Novel Bandit-Based
Approach to Hyperparameter Optimization», Journal of Machine Learning Research, 18avril 2018, 1-52.
2.3 Régler précisément les hyperparamètres d’un réseau de neurones 101
2.3.1 Nombredecouchescachées
Pour bon nombre de problèmes, nous pouvons commencer avec une seule couche
cachée et obtenir des résultats raisonnables. Un perceptron multicouche doté d’une
seule couche cachée peut théoriquement modéliser les fonctions même les plus
complexes pour peu qu’il possède sufsamment de neurones. Mais, pour des pro-
blèmes complexes, les réseaux profonds ont une efcacité paramétrique beaucoup plus
élevée que les réseaux peu profonds. Ils peuvent modéliser des fonctions complexes
avec un nombre de neurones exponentiellement plus faible que les réseaux super-
ciels, ce qui leur permet d’atteindre de meilleures performances avec la même quan-
tité de données d’entraînement.
An de comprendre pourquoi, faisons une analogie. Supposons qu’on vous
demande de dessiner une forêt à l’aide d’un logiciel de dessin, mais qu’il vous soit
interdit d’utiliser le copier-coller. Cela prendrait énormément de temps : il faudrait
alors dessiner chaque arbre individuellement, branche par branche, feuille par feuille.
Si, à la place, vous pouvez dessiner une feuille, la copier-coller pour dessiner une
branche, puis copier-coller cette branche pour créer un arbre, et nalement copier-
coller cet arbre pour dessiner la forêt, vous aurez terminé en un rien de temps. Les
données du monde réel ont souvent une telle structure hiérarchique et les DNN
en tirent automatiquement prot. Les couches cachées inférieures modélisent des
50. Max Jaderberg et al., « Population Based Training of Neural Networks » (2017) : https://homl.info/pbt.
2.3 Régler précisément les hyperparamètres d’un réseau de neurones 103
structures de bas niveau (par exemple, des traits aux formes et aux orientations
variées). Les couches cachées intermédiaires combinent ces structures de bas niveau
pour modéliser des structures de niveau intermédiaire (par exemple, des carrés et des
cercles). Les couches cachées supérieures et la couche de sortie associent ces struc-
tures intermédiaires pour modéliser des structures de haut niveau (par exemple, des
visages).
Cette architecture hiérarchique accélère non seulement la convergence des DNN
vers une bonne solution, mais elle améliore également leur capacité à accepter de
nouveaux jeux de données. Par exemple, si vous avez déjà entraîné un modèle an
de reconnaître les visages sur des photos et si vous souhaitez à présent entraîner un
nouveau réseau de neurones pour reconnaître des coiffures, vous pouvez démarrer
l’entraînement avec les couches inférieures du premier réseau. Au lieu d’initialiser
aléatoirement les poids et les termes constants des quelques premières couches du
nouveau réseau de neurones, vous pouvez les xer aux valeurs des poids et des termes
constants des couches basses du premier. De cette manière, le réseau n’aura pas besoin
de réapprendre toutes les structures de bas niveau que l’on retrouve dans la plupart
des images. Il devra uniquement apprendre celles de plus haut niveau (par exemple,
les coupes de cheveux). C’est ce que l’on appelle le transfert d’apprentissage.
En résumé, pour de nombreux problèmes, vous pouvez démarrer avec juste une
ou deux couches cachées, pour de bons résultats. Par exemple, vous pouvez faci-
lement atteindre une exactitude supérieure à 97% sur le jeu de données MNIST
en utilisant une seule couche cachée et quelques centaines de neurones, mais une
exactitude de plus de 98 % avec deux couches cachées et le même nombre total
de neurones, pour un temps d’entraînement quasi identique. Lorsque les problèmes
sont plus complexes, vous pouvez augmenter progressivement le nombre de couches
cachées, jusqu’à ce que vous arriviez au surajustement du jeu d’entraînement. Les
tâches très complexes, comme la classication de grandes images ou la reconnais-
sance vocale, nécessitent en général des réseaux constitués de dizaines de couches
(ou même des centaines, mais non intégralement connectées, comme nous le ver-
rons au chapitre6) et d’énormes quantités de données d’entraînement. Cependant,
ces réseaux ont rarement besoin d’être entraînés à partir de zéro. Il est beaucoup
plus fréquent de réutiliser des parties d’un réseau préentraîné qui effectue une tâche
comparable. L’entraînement en devient beaucoup plus rapide et nécessite moins de
données (nous y reviendrons au chapitre3).
2.3.2 Nombredeneuronesparcouchecachée
Le nombre de neurones dans les couches d’entrée et de sortie est évidemment déter-
miné par le type des entrées et des sorties nécessaires à la tâche. Par exemple, la tâche
MNIST exige 28×28=784 entrées et 10 neurones de sortie.
Une pratique courante consiste à dimensionner les couches cachées de façon
à former un entonnoir, avec un nombre de neurones toujours plus faible à chaque
couche. La logique est que de nombreuses caractéristiques de bas niveau peuvent se
fondre dans un nombre de caractéristiques de haut niveau moindre. Par exemple,
un réseau de neurones type pour MNIST peut comprendre trois couches cachées,
104 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
2.3.3 Tauxd’apprentisage,tailledelotetautreshyperparamètres
Le nombre de couches cachées et le nombre de neurones ne sont pas les seuls hyperpa-
ramètres que vous pouvez ajuster dans un perceptron multicouche. En voici quelques
autres parmi les plus importants, avec des conseils pour le choix de leur valeur:
• Taux d’apprentissage
Le taux d’apprentissage est probablement l’hyperparamètre le plus important.
En général, sa valeur optimale est environ la moitié du taux d’apprentissage
maximal (c’est-à-dire, le taux d’apprentissage au-dessus duquel l’algorithme
d’entraînement diverge 51). Une bonne manière de déterminer un taux
d’apprentissage approprié consiste à entraîner le modèle sur quelques
centaines d’itérations, en commençant avec un taux d’apprentissage très
bas (par exemple 10−5 ) et de l’augmenter progressivement jusqu’à une valeur
très grande (par exemple 10). Pour cela, on multiplie le taux d’apprentissage
par un facteur constant à chaque itération (par exemple (10/10−5 ) 1/500 de
façon à aller de 10 −5 à 10 en 500 itérations). Si la perte est une fonction
du taux d’apprentissage (en utilisant une échelle logarithmique pour le taux
52. Dominic Masters et Carlo Luschi, « Revisiting Small Batch Training for Deep Neural Networks » (2018) :
https://homl.info/smallbatch.
53. Elad Hoffer et al., « Train Longer, Generalize Better: Closing the Generalization Gap in Large Batch
Training of Neural Networks», Proceedings of the 31st International Conference on Neural Information Proces-
sing Systems (2017), 1729-1739 : https://homl.info/largebatch.
54. Priya Goyal et al., Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour (2017) : https://homl.
info/largebatch2.
106 Chapitre 2. Introduction aux réseaux de neurones artificiels avec Keras
Leslie Smith a publié en 2018 un excellent article55 qui regorge de bonnes pra-
tiques concernant le réglage précis des hyperparamètres d’un réseau de neurones.
N’hésitez pas à le consulter.
Voilà qui conclut notre introduction aux réseaux de neurones articiels et à leur
mise en œuvre avec Keras. Dans les prochains chapitres, nous présenterons des
techniques d’entraînement de réseaux très profonds. Nous verrons également com-
ment personnaliser les modèles avec l’API de bas niveau de TensorFlow et comment
charger et prétraiter efcacement des données avec l’API tf.data. Nous détaillerons
d’autres architectures de réseaux de neurones répandues: réseaux de neurones convo-
lutifs pour le traitement des images, réseaux de neurones récurrents et transformeurs
pour les données séquentielles et le texte, autoencodeurs pour l’apprentissage des
représentations et réseaux antagonistes génératifs pour la modélisation et la généra-
tion de données56 .
2.4 EXERCICES
1. TensorFlow Playground (https://playground.tensorow.org/) est un
simulateur de réseaux de neurones très intéressant développé par
l’équipe de TensorFlow. Dans cet exercice, vous entraînerez plusieurs
classicateurs binaires en seulement quelques clics et ajusterez
l’architecture du modèle et ses hyperparamètres an de vous
familiariser avec le fonctionnement des réseaux de neurones et le
rôle de leurs hyperparamètres. Prenez le temps d’explorer les aspects
suivants :
55. Leslie N. Smith, « A Disciplined Approach to Neural Network Hyper-Parameters: Part1 – Learning Rate,
Batch Size, Momentum, and Weight Decay» (2018) : https://homl.info/1cycle.
56. Quelques autres architectures de réseaux de neurones articiels sont présentées dans l’annexeC.
2.4 Exercices 107
Au chapitre2, vous avez construit, entraîné et ajusté vos premiers réseaux de neu-
rones articiels. Il s’agissait de réseaux assez peu profonds, avec seulement quelques
couches cachées. Comment devons-nous procéder dans le cas d’un problème très
complexe, comme la détection de centaines de types d’objets dans des images en
haute résolution? Nous aurons alors probablement à entraîner un réseau de neurones
beaucoup plus profond, avec peut-être des dizaines de couches, chacune contenant
des centaines de neurones, reliés par des centaines de milliers de connexions. Ce
n’est plus du tout une promenade de santé. Voici quelques-uns des problèmes que
nous pourrions rencontrer:
• Nous pourrions être confrontés au problème des gradients qui deviennent de
plus en plus petits ou de plus en plus grands au cours de la rétropropagation à
travers le réseau. Dans les deux cas, cela complique énormément l’entraînement
des couches inférieures.
• Nous pourrions manquer de données d’entraînement pour un réseau aussi vaste,
ou leur étiquetage pourrait être trop coûteux.
• L’entraînement pourrait être extrêmement lent.
• Un modèle comprenant des millions de paramètres risquera fort de conduire au
surajustement du jeu d’entraînement, en particulier si nous n’avons pas assez
d’instances d’entraînement ou si elles comportent beaucoup de bruit.
Dans ce chapitre, nous allons examiner chacun de ces problèmes et proposer des
techniques pour les résoudre. Nous commencerons par explorer le problème d’insta-
bilité des gradients et présenterons quelques-unes des solutions les plus répandues.
Nous aborderons ensuite le transfert d’apprentissage et le préentraînement non
supervisé, qui peuvent nous aider à traiter des problèmes complexes même lorsque
112 Chapitre 3. Entraînement de réseaux de neurones profonds
les données étiquetées sont peu nombreuses. Puis nous examinerons différents opti-
miseurs qui permettent d’accélérer énormément l’entraînement des grands modèles.
Enn, nous verrons quelques techniques de régularisation adaptées aux vastes réseaux
de neurones.
Armés de ces outils, nous serons en mesure d’entraîner des réseaux très profonds:
bienvenue dans le monde du Deep Learning!
57. Xavier Glorot et Yoshua Bengio, « Understanding the Difculty of Training Deep Feedforward Neural
Networks», Proceedings of the 13th International Conference on Articial Intelligence and Statistics (2010),
249-256 : https://homl.info/47.
3.1 Problèmes d’instabilité des gradients 113
1.2
1
σ ( z) =
1.0 1 + e –z
0.8
Saturation
0.6
0.4
Saturation
0.2 Linéaire
0.0
–0.2
–4 –4 0 2 4
Z
58. Voici une analogie. Si l’amplicateur d’un microphone est réglé près de zéro, le public n’entendra pas
votre voix. S’il est réglé trop près du maximum, votre voix sera saturée et le public ne comprendra pas ce
que vous direz. Imaginons à présent une série de tels amplicateurs. Ils doivent tous être réglés correcte-
ment pour que votre voix arrive parfaitement claire et audible à l’extrémité de la chaîne. Elle doit sortir de
chaque amplicateur avec la même amplitude qu’en entrée.
114 Chapitre 3. Entraînement de réseaux de neurones profonds
la couche59). Ils ont cependant proposé un bon compromis, dont la validité a été mon-
trée en pratique: les poids des connexions doivent être initialisés de façon aléatoire,
comme dans l’équation3.1, où fanmoyen = (fanentrée + fan sortie) . Cette stratégie d’initiali-
2
sation est appelée initialisation de Xavier ou initialisation de Glorot, d’après le nom du
premier auteur de l’article.
Équation 3.1 – Initialisation de Glorot (si la fonction d’activation sigmoïde est employée)
1
Distribution normale avec une moyenne de 0 et une variance σ 2 =
fanmoyen
3
Ou une distribution uniforme entre −r et +r, avec r =
fanmoyen
Si vous remplacez fanmoyen par fanentrée dans l’équation3.1, vous obtenez la stratégie
d’initialisation proposée dans les années 1990 par Yann LeCun, qu’il a nommée ini-
tialisation de LeCun. Genevieve Orr et Klaus-Robert Müller l’ont même conseillée
dans leur ouvrage Neural Networks: Tricks of the Trade publié en 1998 (Springer).
L’initialisation de LeCun est équivalente à l’initialisation de Glorot lorsque
fanentrée=fan sortie. Il a fallu aux chercheurs plus d’une dizaine d’années pour réaliser
l’importance de cette astuce. Grâce à l’initialisation de Glorot, l’entraînement est
considérablement accéléré et elle représente l’une des pratiques qui ont mené au
succès du Deep Learning.
Certains articles récents60 ont proposé des stratégies comparables pour d’autres
fonctions d’activation. Elles diffèrent uniquement par l’échelle de la variance
et par l’utilisation ou non de fanmoyen ou de fanentrée (voir le tableau 3.1) ; pour la
distribution uniforme, on calcule simplement r = 3σ 2 . La stratégie d’initialisation
pour la fonction d’activation ReLU et ses variantes est appelée initialisation de
He ou initialisation de Kaiming (du nom du premier auteur de la publication) (voir
https://homl.info/48). Pour la fonction d’activation SELU, utilisez la méthode
d’initialisation de LeCun, de préférence avec une fonction de distribution nor-
male. Toutes ces fonctions d’activation seront présentées un peu plus loin.
59. Fan signiant « éventail » en anglais, fan-in correspond à l’éventail des connexions entrantes, et fan-out
à l’éventail des connexions sortantes.
60. Par exemple, Kaiming He et al., « Delving Deep into Rectiers: Surpassing Human-Level Performance
on ImageNet Classication», Proceedings of the 2015 IEEE International Conference on Computer Vision
(2015), 1026-1034.
3.1 Problèmes d’instabilité des gradients 115
Keras choisit par défaut l’initialisation de Glorot avec une distribution uni-
forme. Lors de la création d’une couche, nous pouvons opter pour l’initialisation
de He en précisant kernel_initializer="he_uniform" ou kernel_
initializer="he_normal" :
import tensorflow as tf
dense = tf.keras.layers.Dense(50, activation="relu",
kernel_initializer="he_normal")
Si vous souhaitez une autre intialisation, qu’elle gure dans le tableau3.1 ou non,
utilisez l’initialiseur VarianceScaling. Par exemple, pour une initialisation de
He avec une distribution uniforme fondée sur fanmoyen plutôt que sur fanentrée , vous
pouvez utiliser le code suivant :
he_avg_init = tf.keras.initializers.VarianceScaling(scale=2., mode="fan_avg",
distribution="uniform")
dense = tf.keras.layers.Dense(50, activation="sigmoid",
kernel_initializer=he_avg_init)
Leaky RELU
La fonction d’activation Leaky ReLU (leaky signie « qui fuit ») se dénit comme
LeakyReLUα (z) = max(αz, z) (voir la gure 3.2). L’hyperparamètre α dénit le
61. Un neurone mort peut parfois ressusciter si ses entrées évoluent au cours du temps et nissent par
prendre des valeurs pour lesquelles la fonction d’activation ReLU renverra à nouveau une valeur positive.
Ceci peut par exemple se produire lorsque la descente de gradient ajuste des neurones dans les couches
inférieures à celle du neurone mort.
116 Chapitre 3. Entraînement de réseaux de neurones profonds
1
Fuite
0
–1
–4 –2 0 2 4
Z
Figure 3.2 – Fonction Leaky ReLU : comme ReLU, mais avec une petite pente
pour les valeurs négatives
62. Bing Xu et al., « Empirical Evaluation of Recti ed Activations in Convolutional Network » (2015):
https://homl.info/49.
3.1 Problèmes d’instabilité des gradients 117
Vous pouvez aussi utiliser LeakyReLU en tant que couche séparée de votre
modèle ; ceci ne fait pas de différence pour l’entraînement et les prédictions:
model = tf.keras.models.Sequential([
[...] # autres couches
tf.keras.layers.Dense(50, kernel_initializer="he_normal"),
# pas d’activation
tf.keras.layers.LeakyReLU(alpha=0.2), # activation en tant
# que couche séparée
[...] # autres couches
])
Pour PReLU, remplacez LeakyReLU par PReLU. Il n’y a pas pour l’instant d’im-
plémentation ofcielle de RReLU dans Keras, mais vous pouvez l’implémenter assez
facilement par vous-même (reportez-vous pour cela aux exercices gurant à la n du
chapitre4).
ReLU, Leaky ReLU et PreLU souffrent toutes d’un même défaut: elles ne sont
pas « lissées », en ce sens que leur dérivée change brutalement en z=0. Comme nous
l’avons vu au chapitre1 pour la régression lasso, cette discontinuité de la dérivée peut
entraîner un rebond de la descente de gradient autour de l’optimum et ralentir la
convergence. C’est pourquoi nous allons maintenant nous intéresser à des variantes
lissées (c’est-à-dire continûment dérivables) de la fonction d’activation ReLU, à
commencer par ELU et SELU.
ELU et SELU
En 2015, un article63 publié par Djork-Arné Clevert et al. a proposé une nouvelle
fonction d’activation appelée ELU (exponential linear unit), qui s’est montrée bien
plus performante que toutes les variantes de ReLU dans leurs expérimentations. Le
temps d’entraînement diminuait et le réseau de neurones se comportait mieux sur le
jeu de test. L’équation3.2 en donne la dénition.
3
ELU α( z ) = α (e z – 1) si z < 0, sinon z
SELU( z ) = 1.05 ELU 1.67 ( z )
2
–1
–2
–4 –2 0 2 4
Z
63. Djork-Arné Clevert et al., « Fast and Accurate Deep Network Learning by Exponential Linear Units
(ELUs) », Proceedings of the International Conference on Learning Representations (2016) : https://homl.info/50.
118 Chapitre 3. Entraînement de réseaux de neurones profonds
α (exp ( z) – 1) si z < 0
ELU α (z ) =
z si z ≥ 0
64. Günter Klambauer et al., « Self-Normalizing Neural Networks », Proceedings of the 31st International
Conference on Neural Information Processing Systems (2017), 972-981 : https://homl.info/selu.
3.1 Problèmes d’instabilité des gradients 119
GELU(z) = zΦ(z)
Comme vous pouvez le voir sur la gure3.4, GELU ressemble à ReLU: elle se
rapproche de 0 lorsque son entrée z est très négative, et se rapproche de z lorsque
son entrée z est très positive. Cependant, alors que toutes les fonctions d’activation
dont nous avons parlé jusqu’ici étaient à la fois convexes et monotones66 , la fonc-
tion d’activation GELU n’est ni l’une ni l’autre: de gauche à droite, elle commence
de manière rectiligne, puis s’incurve vers le bas, passe par un minimum d’environ
– 0,17 (pour z voisin de 0,75), et rebondit nalement vers le haut en se dirigeant
vers l’angle supérieur droit. Sa forme plutôt complexe et le fait qu’elle soit incurvée
en tout point pourrait expliquer pourquoi elle fonctionne si bien, en particulier pour
les tâches compliquées: la descente de gradient peut s’adapter plus aisément à des
modèles complexes. En pratique, elle donne souvent de meilleurs résultats que toutes
65. Dan Hendrycks et Kevin Gimpel, « Gaussian Error Linear Units (GELUs) », arXiv preprint
arXiv:1606.08415 (2016).
66. Une fonction est convexe si le segment de droite reliant deux points quelconques de la courbe est
toujours au-dessus de la courbe. Une fonction monotone est toujours croissante, ou toujours décroissante.
120 Chapitre 3. Entraînement de réseaux de neurones profonds
les autres fonctions d’activation dont nous avons parlé jusqu’ici. Cependant, elle
requiert davantage de temps de calcul, et le gain de performance qu’elle procure ne
suft pas toujours à justier le coût additionnel. Ceci dit, on peut montrer qu’elle
est à peu près égale à z σ(1,702z), où σ est la fonction sigmoïde: l’utilisation de
cette approximation fonctionne également très bien et présente l’avantage d’être
plus rapide à calculer.
67. Prajit Ramachandran et al., « Searching for Activation Functions », arXiv preprint arXiv:1710.05941
(2017).
68. Diganta Misra, « Mish: A Self Regularized Non-Monotonic Activation Function », arXiv preprint
arXiv:1908.08681 (2019).
3.1 Problèmes d’instabilité des gradients 121
Tout comme GELU et Swish, il s’agit d’une variante de ReLU à la fois lissée, non
convexe et non monotone, et de nouveau l’auteur a effectué de nombreux tests et
trouvé que Mish donne généralement de meilleurs résultats que les autres fonctions
d’activation, y compris Swish et GELU, mais avec une faible marge dans ce dernier
cas. La gure 3.4 présente les fonctions GELU, Swish (à la fois avec la valeur par
défaut β = 1 et avec la valeur β = 0,6), et enn Mish. Comme vous pouvez le voir,
Mish se superpose presque parfaitement avec Swish lorsque z est négatif, et presque
parfaitement avec GELU lorsque z est positif.
Quelle fonction d’activation doit-on donc utiliser dans les couches cachées
des réseaux de neurones ? ReLU reste un bon choix par défaut pour les
tâches simples : elle fonctionne souvent aussi bien que les fonctions d’ac-
tivation plus sophistiquées, elle est plus rapide à calculer, et bon nombre
de bibliothèques et d’accélérateurs matériels disposent d’optimisation spé-
cifiques à ReLU. Toutefois, pour des tâches plus complexes, Swish constitue
probablement un meilleur choix par défaut, avec même la possibilité, pour
les tâches les plus complexes, d’essayer Swish paramétré avec un para-
mètre β pouvant être appris. Mish pourrait vous donner des résultats légè-
rement meilleurs, moyennant un peu plus de temps de calcul. Si le temps
d’exécution importe beaucoup, alors vous préférerez peut-être Leaky ReLU,
ou Leaky RELU paramétrée pour des tâches plus complexes. Pour les per-
ceptrons multicouches profonds, essayez SELU, mais vérifiez bien si les
conditions indiquées ci-dessus sont respectées. S’il vous reste du temps et
des ressources informatiques, vous pouvez exploiter la validation croisée
pour évaluer également d’autres fonctions d’activation.
69. Sergey Ioffe et Christian Szegedy, « Batch Normalization: Accelerating Deep Network Training by
Reducing Internal Covariate Shift », Proceedings of the 32nd International Conference on Machine Learning
(2015), 448-456: https://homl.info/51.
122 Chapitre 3. Entraînement de réseaux de neurones profonds
1.
2.
3.
4.
humains ». Enn, cerise sur le gâteau, la normalisation par lots agit également comme
un régulariseur, diminuant le besoin de recourir à d’autres techniques de régularisa-
tion (comme celle par abandon décrite plus loin).
La normalisation par lots ajoute néanmoins une certaine complexité au modèle
(même si elle permet d’éviter la normalisation des données d’entrée, comme nous
l’avons expliqué précédemment). Par ailleurs, elle implique également un coût à
l’exécution : le réseau de neurones fait ses prédictions plus lentement en raison des
calculs supplémentaires réalisés dans chaque couche. Heureusement, il est souvent
possible de fusionner la couche BN à la couche précédente, après l’entraînement,
évitant ainsi le coût à l’exécution. Pour cela, il suft d’actualiser les poids et les
termes constants de la couche précédente an qu’elle produise directement des sor-
ties ayant l’échelle et le décalage appropriés. Par exemple, si la couche précédente
calcule XW+b, alors la couche BN calculera γ ⊗ (XW + b − µ)/σ + β (en ignorant
le terme de lissage ε dans le dénominateur). Si nous dénissons W’ = γ ⊗ W/σ etb’=
γ ⊗(b − µ)/σ + β, l’équation se simplie en XW’ + b’. Par conséquent, si nous rempla-
çons les poids et les termes constants de la couche précédente (W et b) par les poids
et les termes constants actualisés (W’ et b’), nous pouvons nous débarrasser de la
couche BN (le convertisseur de TFLite le fait automatiquement; voir le chapitre11).
Il est possible que l’entraînement soit relativement lent car, lorsque la nor-
malisation par lots est mise en œuvre, chaque époque prend plus de temps.
Ce comportement est généralement contrebalancé par le fait que la conver-
gence est beaucoup plus rapide avec la normalisation par lots et qu’il faut
moins d’époques pour arriver aux mêmes performances. Globalement, le
temps écoulé (le temps mesuré par l’horloge accrochée au mur) sera en
général plus court.
70. Cependant, puisqu’ils sont calculés au cours de l’entraînement à partir des données d’entraînement,
objectivement ils sont entraînables. Dans Keras, «non entraînable » signie en réalité « non touché par la
rétropropagation ».
126 Chapitre 3. Entraînement de réseaux de neurones profonds
dépendre de la tâche : vous pouvez faire vos propres tests pour voir celle qui convient
à votre jeu de données. Pour ajouter les couches BN avant les fonctions d’activa-
tion, nous devons retirer celles-ci des couches cachées et les ajouter en tant que
couches séparées après les couches BN. Par ailleurs, puisqu’une couche de normali-
sation par lots comprend un paramètre de décalage par entrée, nous pouvons retirer
le terme constant de la couche précédente (lors de sa création, il suft d’indiquer
use_bias=False). Enn, vous pouvez en général laisser tomber la première
couche BN pour éviter que la première couche cachée se trouve prise en sandwich
entre deux couches BN. Voici le code modié:
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation("relu"),
tf.keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation("relu"),
tf.keras.layers.Dense(10, activation="softmax")
])
La couche BatchNormalization est devenue l’une des plus utilisées dans les
réseaux de neurones profonds, à tel point qu’elle est souvent omise dans les schémas
d’architecture, car on suppose que la normalisation par lots est ajoutée après chaque
couche. Voyons maintenant une dernière technique permettant de stabiliser les gra-
dients durant l’entraînement: l’écrêtage de gradient.
71. Razvan Pascanu et al., « On the Difculty of Training Recurrent Neural Networks », Proceedings of the
30th International Conference on Machine Learning (2013), 1310-1318: https://homl.info/52.
128 Chapitre 3. Entraînement de réseaux de neurones profonds
verrons comment en trouver). Si vous trouvez un tel réseau de neurones, vous pouvez
en général réutiliser la plupart de ces couches, à l’exception des couches supérieures.
Cette technique de transfert d’apprentissage (transfer learning) va non seulement accé-
lérer considérablement l’entraînement, mais permettra d’obtenir de bonnes perfor-
mances avec des jeux de données d’entraînement assez petits.
Par exemple, supposons que nous ayons accès à un réseau de neurones profond (ou
DNN) qui a été entraîné pour classer des images en 100 catégories différentes (ani-
maux, plantes, véhicules et tous les objets du quotidien). Nous souhaitons à présent
entraîner un autre DNN pour classer des types de véhicules particuliers. Ces deux
tâches sont très similaires et nous devons essayer de réutiliser des éléments du premier
réseau (voir la gure3.5).
Couche de sortie
Poids
Couche cachée 4 Couche cachée 4
entraînables
Réutilisation
Couche cachée 3 Couche cachée 3
Si les images d’entrée pour la nouvelle tâche n’ont pas la même taille que
celles utilisées dans la tâche d’origine, il faudra rajouter une étape de pré-
traitement pour les redimensionner à la taille attendue par le modèle d’ori-
gine. De façon plus générale, le transfert d’apprentissage ne peut fonction-
ner que si les entrées ont des caractéristiques de bas niveau comparables.
Plus les tâches sont similaires, plus le nombre de couches réutilisables pour-
ra être élevé (en commençant par les couches inférieures). Pour des tâches
très proches, essayez de conserver toutes les couches cachées et remplacez
uniquement la couche de sortie.
Commencez par ger toutes les couches réutilisées (autrement dit, leurs poids
sont rendus non entraînables an que la descente de gradient ne les modie pas),
puis entraînez le modèle et examinez ses performances. Essayez ensuite de libérer
une ou deux des couches cachées supérieures pour que la rétropropagation les ajuste
et voyez si les performances s’améliorent. Plus la quantité de données d’entraîne-
ment est importante, plus le nombre de couches que vous pouvez libérer augmente.
Il est également utile de diminuer le taux d’apprentissage lorsque des couches réu-
tilisées sont libérées: cela évite de mettre à mal leurs poids qui ont été ajustés
nement.
Si les performances sont toujours mauvaises et si les données d’entraînement sont
limitées, vous pouvez retirer la (voire les) couche(s) supérieure(s) et ger de nouveau
les couches cachées restantes. Répétez la procédure jusqu’à trouver le bon nombre de
couches à réutiliser. Si vous avez beaucoup de données d’entraînement, vous pouvez
remplacer les couches cachées supérieures au lieu de les supprimer, et même ajouter
d’autres couches cachées.
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001)
model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer,
metrics=["accuracy"])
Après avoir figé ou libéré des couches, le modèle doit toujours être compilé.
Nous pouvons à présent entraîner le modèle sur quelques époques, puis libérer les
couches réutilisées (ce qui implique une nouvelle compilation du modèle) et pour-
suivre l’entraînement an d’ajuster précisément les couches réutilisées à la tâche B.
Après la libération des couches réutilisées, il est généralement conseillé d’abaisser le
taux d’apprentissage, de nouveau pour éviter d’endommager les poids réutilisés:
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=4,
validation_data=(X_valid_B, y_valid_B))
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001)
model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer,
metrics=["accuracy"])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=16,
validation_data=(X_valid_B, y_valid_B))
3.2 Réutiliser des couches préentraînées 131
Quel est le résultat nal ? Sur ce modèle, l’exactitude de test est de 93,85 %, soit
deux points de mieux que 91,85 %. Le transfert d’apprentissage a donc fait baisser le
taux d’erreur de près de 25 % :
>>> model_B_on_A.evaluate(X_test_B, y_test_B)
[0.2546142041683197, 0.9384999871253967]
Êtes-vous convaincu ? Vous ne devriez pas, car j’ai triché ! J’ai testé de nombreuses
congurations jusqu’à trouver celle qui conduise à une forte amélioration. Si vous
changez les classes ou le germe aléatoire, vous constaterez généralement que l’amélio-
ration baisse, voire disparaît ou s’inverse. En réalité, nous avons « torturé les données
jusqu’à leur faire dire ce qu’on attend ». Lorsqu’un article semble trop positif, vous
devez devenir soupçonneux : il est possible que la nouvelle super technique apporte
en réalité peu d’aide (elle peut même dégrader les performances), mais les auteurs
ont essayé de nombreuses variantes et rapporté uniquement les meilleurs résultats
(peut-être dus à une chance inouïe), sans mentionner le nombre des échecs. La plu-
part du temps, c’est sans intention malveillante, mais cela explique en partie pour-
quoi de nombreux résultats scientiques ne peuvent jamais être reproduits.
Pourquoi ai-je triché? En réalité, le transfert d’apprentissage ne fonctionne pas
très bien avec les petits réseaux denses, probablement parce que les petits réseaux
apprennent peu de motifs et que les réseaux denses apprennent des motifs très spéci-
ques sans doute peu utiles dans d’autres tâches. Le transfert d’apprentissage est mieux
adapté aux réseaux de neurones convolutifs profonds, qui ont tendance à apprendre
des détecteurs de caractéristiques beaucoup plus généraux (en particulier dans les
couches basses). Nous reviendrons sur le transfert d’apprentissage au chapitre6, en
utilisant les techniques que nous venons de présenter (cette fois-ci sans tricher).
Couche de sortie
Données Données
non étiquetées étiquetées
Figure 3.6 – Dans un entraînement non supervisé, un modèle est entraîné sur
toutes les données, y compris celles qui ne sont pas étiquetées en utilisant une technique
d’apprentissage non supervisé, puis il est ajusté plus finement à la tâche finale sur
les seules données étiquetées en utilisant une technique d’apprentissage supervisé ;
la partie non supervisée peut entraîner une couche à la fois, comme illustré ici,
ou directement l’intégralité du modèle.
Il s’agit de la technique que Geoffrey Hinton et son équipe ont employée avec
succès en 2006 et qui a conduit à la renaissance des réseaux de neurones et au succès
du Deep Learning. Jusqu’en 2010, le préentraînement non supervisé, en général
avec des machines de Boltzmann restreintes (restricted Boltzmann machines, ou RBM ;
voir l’annexeC), constituait la norme pour les réseaux profonds. Ce n’est qu’après
avoir résolu le problème de disparition des gradients que l’entraînement uniquement
supervisé des réseaux de neurones profonds est devenu plus fréquent. Le préentraîne-
ment non supervisé (aujourd’hui plutôt avec des autoencodeurs ou des GAN qu’avec
des RBM) reste une bonne approche lorsque la tâche à résoudre est complexe, qu’il
n’existe aucun modèle comparable à réutiliser, et que les données d’entraînement
étiquetées sont peu nombreuses contrairement aux données d’entraînement non éti-
quetées.
Aux premiers jours du Deep Learning, il était difcile d’entraîner des modèles
profonds. On employait donc une technique de préentraînement glouton par couche
(greedy layer-wise pretraining), illustrée à la gure3.6. Un premier modèle non super-
visé était entraîné avec une seule couche, en général une machine de Boltzmann
restreinte. Cette couche était ensuite gée et une autre couche était ajoutée par-
dessus. Le modèle était de nouveau entraîné (seule la nouvelle couche était donc
concernée), puis la nouvelle couche était gée et une autre couche était ajoutée
3.2 Réutiliser des couches préentraînées 133
72. Boris T. Polyak, « Some Methods of Speeding Up the Convergence of Iteration Methods », USSR
Computational Mathematics and Mathematical Physics, 4, n°5 (1964), 1-17 : https://homl.info/54.
3.3 Optimiseurs plus rapides 135
Vous pouvez facilement vérier que si le gradient reste constant, la vélocité nale
(c’est-à-dire la taille maximale des mises à jour des poids) est égale à ce gradient multi-
plié par le taux d’apprentissage η multiplié par 1/(1 – β ) (sans tenir compte du signe).
Par exemple, si β = 0,9, alors la vélocité nale est égale à 10 fois le gradient fois le
taux d’apprentissage. L’optimisation avec inertie permet ainsi d’aller jusqu’à dix fois
plus rapidement que la descente de gradient! Elle peut donc sortir des zones de faux
plat plus rapidement que la descente de gradient. En particulier, lorsque les entrées
ont des échelles très différentes, la fonction de coût va ressembler à un bol allongé73 :
la descente de gradient arrive en bas de la pente abrupte assez rapidement, mais il lui
faut ensuite beaucoup de temps pour descendre la vallée. En revanche, l’optimisation
avec inertie va avancer de plus en plus rapidement vers le bas de la vallée, jusqu’à
l’atteindre (l’optimum). Dans les réseaux de neurones profonds qui ne mettent pas en
œuvre la normalisation par lots, les couches supérieures nissent souvent par recevoir
des entrées aux échelles très différentes. Dans ce cas, l’optimisation avec inertie est
d’une aide précieuse. Elle permet également de sortir des optima locaux.
En raison de l’inertie, l’optimiseur peut parfois aller un peu trop loin, puis
revenir, puis aller de nouveau trop loin, en oscillant ainsi à plusieurs reprises,
avant de se stabiliser sur le minimum. Voilà notamment pourquoi il est bon
d’avoir un peu de frottement dans le système : il réduit ces oscillations et
accélère ainsi la convergence.
Implémenter l’optimisation avec inertie dans Keras ne pose aucune difculté : il
suft d’utiliser l’optimiseur SGD et de xer son hyperparamètre momentum, puis
d’attendre tranquillement!
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
� Coût
2
Point
de départ Mise à jour
ordinaire de l’inertie
–��1
–��1
�m
–��2
Mise à jour
de Nesterov
�
1
3.3.3 AdaGrad
Considérons à nouveau le problème du bol allongé : la descente de gradient commence
par aller rapidement vers le bas de la pente la plus abrupte, qui ne pointe pas direc-
tement vers l’optimum global, puis elle va lentement vers le bas de la vallée. Il serait
préférable que l’algorithme revoie son orientation plus tôt pour se diriger un peu
plus vers l’optimum global. L’algorithme AdaGrad75 met cette correction en place en
75. John Duchi et al., « Adaptive Subgradient Methods for Online Learning and Stochastic Optimiza-
tion», Journal of Machine Learning Research, 12 (2011), 2121-2159 : https://homl.info/56.
3.3 Optimiseurs plus rapides 137
diminuant le vecteur de gradient le long des dimensions les plus raides (voirl’équa-
tion3.7) :
La première étape accumule les carrés des gradients dans le vecteur s (rappelons
que le symbole ⊗ représente la multiplication terme à terme). Cette forme vecto-
risée équivaut au calcul de si ← si + ( ∂ J(θ)/∂θi )2 pour chaque élément s i du vecteurs.
Autrement dit, chaque s i accumule les carrés de la dérivée partielle de la fonction
de coût en rapport avec le paramètre θ.i Si la fonction de coût présente une pente
abrupte le long de la ième dimension, alors si va augmenter à chaque itération.
La seconde étape est quasi identique à la descente de gradient, mais avec une diffé-
rence importante: le vecteur de gradient est réduit d’un facteur s + ε . Le symbole
représente la division terme à terme et ε est un terme de lissage qui permet d’éviter
la division par zéro (sa valeur est généralement xée à 10–10). Cette forme vecto-
risée équivaut au calcul simultané de θi ← θi − η∂ J(θ)/∂θi/ s i + ε pour tous les para-
mètresθi .
En résumé, cet algorithme abaisse progressivement le taux d’apprentissage, mais
il le fait plus rapidement sur les dimensions présentant une pente abrupte que sur
celles dont la pente est plus douce. Nous avons donc un taux d’apprentissage adaptatif.
Cela permet de diriger plus directement les mises à jour résultantes vers l’optimum
global (voir la gure3.8). Par ailleurs, l’algorithme exige un ajustement moindre de
l’hyperparamètre η pour le taux d’apprentissage.
(Dimension
abrupte)
Coût
�2
AdaGrad
Descente
de gradient
�1 (Dimension
plus plate)
Figure 3.8 – AdaGrad contre descente de gradient : la première méthode peut corriger
plus tôt sa direction pour pointer vers l’optimum
avant d’atteindre l’optimum global. Par conséquent, même si Keras propose l’opti-
miseur Adagrad, il est préférable de ne pas l’employer pour entraîner des réseaux
de neurones profonds (cet optimiseur peut rester toutefois efcace pour des tâches
plus simples comme la régression linéaire). Il n’en reste pas moins que l’étude du
fonctionnement d’AdaGrad peut aider à comprendre les autres optimiseurs de taux
d’apprentissage.
3.3.4 RMSProp
Comme nous l’avons vu, AdaGrad risque de ralentir un peu trop rapidement et de ne
jamais converger vers l’optimum global. L’algorithme RMSProp76 corrige ce problème
en cumulant uniquement les gradients issus des itérations les plus récentes, plutôt que
tous les gradients depuis le début l’entraînement. Pour cela, il utilise une moyenne
mobile exponentielle au cours de la première étape (voir l’équation3.8).
Le taux de décroissance ρ77 est en général xé à 0,9. Il s’agit encore d’un nouvel
hyperparamètre, mais cette valeur par défaut convient souvent et nous avons rare-
ment besoin de l’ajuster.
Sans surprise, Keras dispose d’un optimiseur RMSprop:
optimizer = tf.keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
Excepté sur les problèmes très simples, cet optimiseur afche des performances
quasi toujours meilleures qu’AdaGrad. D’ailleurs, il s’agissait de l’algorithme d’opti-
misation préféré de nombreux chercheurs jusqu’à l’arrivée de l’optimisation Adam.
3.3.5 Adam
Adam78, pour Adaptive Moment Estimation, réunit les idées de l’optimisation avec
inertie et de RMSProp. Il maintient, à l’instar de la première, une moyenne mobile
exponentielle des gradients antérieurs et, à l’instar de la seconde, une moyenne
mobile exponentielle des carrés des gradients antérieurs (voir l’équation3.9) 79.
76. Geoffrey Hinton et Tijmen Tieleman ont créé cet algorithme en 2012, et Geoffrey Hinton l’a présen-
té lors de son cours sur les réseaux de neurones (les diapositives sont disponibles à l’adresse https://homl.
info/57, la vidéo à l’adresse https://homl.info/58). Puisque les auteurs n’ont jamais rédigé d’article pour le
décrire, les chercheurs le citent souvent sous la référence « diapositive29 du cours6 ».
77. ρ est la lettre grecque rho.
78. Diederik P. Kingma et Jimmy Ba, « Adam: A Method for Stochastic Optimization » (2014) : https://
homl.info/59.
79. Il s’agit d’estimations de la moyenne et de la variance (non centrée) des gradients. La moyenne est
souvent appelée premier moment, tandis que la variance est appelée second moment, d’où le nom donné à
l’algorithme.
3.3 Optimiseurs plus rapides 139
Si vous commencez à vous sentir submergé par toutes ces différentes tech-
niques et vous demandez comment choisir celles qui convienent à votre
tâche, pas de panique : vous trouverez des conseils pratiques à la fin de ce
chapitre.
3.3.6 AdaMax
La publication ayant introduit Adam comportait aussi une présentation d’Adamax.
À l’étape2 de l’équation3.8, Adam cumule dans s les carrés des gradients (avec un
140 Chapitre 3. Entraînement de réseaux de neurones profonds
poids supérieur pour les gradients les plus récents). À l’étape 5, si nous ignorons ε et
les étapes 3 et 4 (qui sont de toute manière des détails techniques), Adam effectue
la mise à jour des paramètres puis les divise par la racine carrée de s, c’est-à-dire par
la norme ℓ 2 des gradients réduits au l des itérations (rappelons que la norme ℓ2 est la
racine carrée de la somme des carrés).
AdaMax remplace la norme ℓ2 par la norme ℓ ∞ (ce qui est une autre façon de
dire le maximum). Plus précisément, il remplace l’étape 2 de l’équation 3.8 par
s ← max(β2 s, abs(∇θJ(θ))), supprime l’étape4 et, dans l’étape 5, effectue la mise
à jour des paramètres en les réduisant ensuite d’un facteur s qui est simplement le
maximum des gradients réduits au l des itérations.
En pratique, cette modication peut rendre AdaMax plus stable qu’Adam, mais
cela dépend du jeu de données, et en général Adam afche de meilleures perfor-
mances. Il ne s’agit donc que d’un autre optimiseur que vous pouvez essayer si vous
rencontrez des problèmes avec Adam sur une tâche.
3.3.7 Nadam
L’optimisation Nadam correspond à l’optimisation Adam complétée de l’astuce de
Nesterov. Elle convergera donc souvent plus rapidement qu’Adam. Dans la publica-
tion80 présentant cette technique, Timothy Dozat compare de nombreux optimiseurs
sur diverses tâches et conclut que Nadam donne en général de meilleurs résultats
qu’Adam mais qu’il est parfois surpassé par RMSProp.
3.3.8 AdamW
AdamW81 est une variante d’Adam intégrant une technique de régularisation
appelée décroissance des poids (en anglais, weight decay). Elle consiste à réduire les
poids du modèle à chaque itération d’entraînement en les multipliant par un facteur
de décroissance, par exemple 0,99. Ceci peut vous rappeler la régularisation ℓ2 (pré-
sentée au chapitre1), qui a aussi pour but de conserver des poids réduits: effective-
ment, on peut démontrer mathématiquement que la régularisation ℓ 2 est équivalente
à la décroissance des poids lorsqu’on utilise SGD. Cependant, lorsqu’on utilise Adam
ou ses variantes, la régularisation ℓ2 et la décroissance des poids ne sont pas équiva-
lentes: en pratique, la combinaison d’Adam et d’une régularisation ℓ2 aboutit à des
modèles qui souvent ne se généralisent pas aussi bien que ceux produits par SGD.
AdamW résoud ce problème en combinant correctement Adam et la décroissance
des poids.
80. Timothy Dozat, « Incorporating Nesterov Momentum into Adam » (2016) : https://homl.info/nadam.
81. Ilya Loshchilov et Frank Hutter, « Decoupled Weight Decay Regularization », arXiv preprint
arXiv:1711.05101 (2017): https://homl.info/adamw.
3.3 Optimiseurs plus rapides 141
82. Ashia C. Wilson et al., « The Marginal Value of Adaptive Gradient Methods in Machine Learning »,
Advances in Neural Information Processing Systems, 30 (2017), 4148-4158 : https://homl.info/60.
83. Voir la régression lasso au §1.9.2.
84. https://homl.info/tfmot
142 Chapitre 3. Entraînement de réseaux de neurones profonds
86. Leslie N. Smith, « A Disciplined Approach to Neural Network Hyper-Parameters: Part 1 –Learning
Rate, Batch Size, Momentum, and Weight Decay» (2018): https://homl.info/1cycle.
87. Andrew Senior et al., « An Empirical Study of Learning Rates in Deep Neural Networks for Speech
Recognition », Proceedings of the IEEE International Conference on Acoustics, Speech, and Signal Processing
(2013), 6724-6728 : https://homl.info/63.
3.4 Planifier le taux d’apprentissage 145
Si vous préférez ne pas xer en dur η0 et s, vous pouvez créer une fonction qui
retourne une fonction congurée :
def exponential_decay(lr0, s):
def exponential_decay_fn(epoch):
return lr0 * 0.1 ** (epoch / s)
return exponential_decay_fn
un taux d’apprentissage très élevé, qui risque d’endommager les poids du modèle.
Une solution consiste à préciser manuellement l’argument initial_epoch de la
méthode fit() an que epoch commence avec la bonne valeur.
Pour les taux d’apprentissage décroissant par paliers successifs, vous pouvez
employer une fonction de planication semblable à la suivante (comme précédem-
ment, vous pouvez dénir une fonction plus générale si nécessaire; un exemple
est donné dans la section « Piecewise Constant Scheduling » du notebook 89), puis
créer un rappel LearningRateScheduler avec cette fonction et le passer à la
méthode fit(), exactement comme nous l’avons fait pour la décroissance expo-
nentielle :
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.001
batch_size = 32
n_epochs = 25
n_steps = n_epochs * math.ceil(len(X_train) / batch_size)
scheduled_learning_rate = tf.keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate=0.01, decay_steps=n_steps, decay_rate=0.1)
optimizer = tf.keras.optimizers.SGD(learning_rate=scheduled_learning_rate)
3.5.1 Régularisation ℓ1 et ℓ2
Vous pouvez employer la régularisation ℓ2 pour contraindre les poids des connexions
d’un réseau de neurones et/ou la régularisation ℓ1 si vous voulez un modèle creux
(avec de nombreux poids égaux à zéro)91 . Voici comment appliquer la régularisation
ℓ2 au poids des connexions d’une couche Keras, en utilisant un facteur de régularisa-
tion égal à 0,01 :
layer = tf.keras.layers.Dense(100, activation="relu",
kernel_initializer="he_normal",
kernel_regularizer=tf.keras.regularizers.l2(0.01))
RegularizedDense = partial(tf.keras.layers.Dense,
activation="relu",
kernel_initializer="he_normal",
kernel_regularizer=tf.keras.regularizers.l2(0.01))
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
RegularizedDense(100),
RegularizedDense(100),
RegularizedDense(10, activation="softmax")
])
92. Geoffrey E. Hinton et al., « Improving Neural Networks by Preventing Co-Adaptation of Feature
Detectors» (2012) : https://homl.info/64.
93. Nitish Srivastava et al., « Dropout: A Simple Way to Prevent Neural Networks from Overtting »,
Journal of Machine Learning Research, 15 (2014), 1929-1958 : https://homl.info/65.
3.5 Éviter le surajustement grâce à la régularisation 149
Désactivé
x1 x2
Figure 3.10 – Dans la régularisation par abandon, certains neurones choisis aléatoirement
au sein d’une ou plusieurs couches (excepté la couche de sortie) sont désactivés
à chaque étape d’entraînement ; ils produisent 0 en sortie pendant cette itération
(ce qui correspond aux flèches en pointillés)
moins sensibles aux légers changements en entrée. Nous obtenons à terme un réseau
plus robuste, avec une plus grande capacité de généralisation.
Pour bien comprendre la puissance de l’abandon, il faut réaliser que chaque étape
d’entraînement génère un réseau de neurones unique. Puisque chaque neurone
peut être présent ou absent, il existe un total de 2N réseaux possibles (où N est le
nombre total de neurones éteignables). Ce nombre est tellement énorme qu’il est
quasi impossible que le même réseau de neurones soit produit à deux reprises. Après
10 000étapes d’entraînement, nous avons en réalité entraîné 10 000 réseaux de neu-
rones différents, chacun avec une seule instance d’entraînement. Ces réseaux de
neurones ne sont évidemment pas indépendants, car ils partagent beaucoup de leurs
poids, mais ils n’en restent pas moins tous différents. Le réseau de neurones résultant
peut être vu comme un ensemble moyen de tous ces réseaux de neurones plus petits.
Il reste un petit détail technique qui a son importance. Supposons que p=75 %,
c’est-à-dire que, en moyenne, seulement 25 % de l’ensemble des neurones sont actifs
à chaque étape durant l’entraînement. Ceci signie que, après l’entraînement, un
neurone sera connecté à quatre fois plus de neurones d’entrée qu’il ne l’a été au cours
de l’entraînement. Pour compenser cela, il faut multiplier les poids des connexions
d’entrée de chaque neurone par 4 après l’entraînement. Dans le cas contraire, le
réseau de neurones ne fonctionnera pas correctement du fait que les données qu’il
reçoit sont différentes pendant et après l’entraînement. Plus généralement, après
l’entraînement, il faut diviser les poids des connexions d’entrée par la probabilité de
conservation (keep probability) 1−p utilisée durant l’entraînement.
Pour implémenter la régularisation par abandon avec Keras, nous pouvons mettre
en place la couche tf.keras.layers.Dropout. Pendant l’entraînement, elle
désactive aléatoirement certaines entrées (en les xant à 0) et divise les entrées restantes
par la probabilité de conservation. Après l’entraînement, elle ne fait plus rien ; elle se
contente de passer les entrées à la couche suivante. Le code suivant applique la régulari-
sation par abandon avant chaque couche Dense, en utilisant un taux d’abandon de 0,2:
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(100, activation="relu",
kernel_initializer="he_normal"),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(100, activation="relu",
kernel_initializer="he_normal"),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(10, activation="softmax")
])
[...] # compilation et entraînement du modèle
3.5 Éviter le surajustement grâce à la régularisation 151
94. Yarin Gal et Zoubin Ghahramani, « Dropout as a Bayesian Approximation: Representing Model
Uncertainty in Deep Learning », Proceedings of the 33rd International Conference on Machine Learning
(2016), 1050-1059 : https://homl.info/mcdropout.
95. Plus précisément, ils ont notamment montré que l’entraînement d’un réseau avec abandon est mathé-
matiquement équivalent à une inférence bayésienne approchée dans un type spécique de modèle proba-
biliste appelé processus gaussien profond (deep Gaussian process).
152 Chapitre 3. Entraînement de réseaux de neurones profonds
Si vous trouvez que cela semble trop beau pour être vrai, examinez le code suivant.
Cette implémentation complète de l’abandon MC améliore le modèle d’abandon
entraîné précédemment sans l’entraîner de nouveau:
import numpy as np
Le modèle est sûr (à 84,4 %) que cette image appartient à la classe9 (bottine).
Comparez avec la prédiction avec abandon de Monte Carlo:
>>> y_proba[0].round(3)
array([0. , 0. , 0. , 0. , 0. , 0.067, 0. , 0.209, 0.001,
0.723], dtype=float32)
Le modèle semble toujours préférer la classe9 (bottine), mais son taux de conance
a chuté à 72,3 %, tandis que les probabilités estimées pour les classes 5 (sandale) et 7
(sneaker) ont augmenté, ce qui est logique étant donné qu’il s’agit aussi de chaussures.
L’abandon de Monte Carlo tend à améliorer la abilité des probabilités estimées
du modèle. Ceci signie qu’il est moins probable qu’il ait conance tout en ayant tort,
ce qui peut constituer un danger: imaginez une voiture sans conducteur ignorant
un panneau stop. De plus, il est intéressant de connaître exactement quelles autres
classes sont les plus probables. De plus, vous pouvez aussi jeter un œil à l’écart-type
des estimations de probabilité (https://xkcd.com/2110):
>>> y_std = y_probas.std(axis=0)
>>> y_std[0].round(3)
array([0. , 0. , 0. , 0.001, 0. , 0.096, 0. , 0.162, 0.001,
0.183], dtype=float32)
3.5 Éviter le surajustement grâce à la régularisation 153
96. Cette classe MCDropout est compatible avec toutes les API Keras, y compris l’API Sequen-
tial. Si vous vous intéressez uniquement aux API Functional ou Subclassing, il est inutile de créer une
classe MCDropout. Vous pouvez simplement créer une couche Dropout normale et l’appeler avec
training=True.
154 Chapitre 3. Entraînement de réseaux de neurones profonds
Tableau 3.4 – Configuration par défaut d’un réseau de neurones profond autonormalisant
• Si vous avez besoin d’un modèle à faible temps de réponse (s’il doit effectuer
des prédictions en un éclair), il vous faudra peut-être utiliser un nombre plus
faible de couches, utiliser une fonction d’activation rapide telle que ReLU ou
Leaky ReLU, et intégrer les couches de normalisation par lots dans les couches
précédentes après l’entraînement. Un modèle creux pourra également être
utile. Enn, vous pouvez réduire la précision des nombres à virgule ottante de
32 à 16, voire 8 bits (voir §11.2). Là aussi, envisagez l’utilisation de TF-MOT.
• Si vous développez une application sensible ou si le temps d’attente des
prédictions n’est pas très important, vous pouvez utiliser l’abandon deMonte
Carlo pour augmenter les performances et obtenir des estimations de probabilités
plus ables, avec des estimations d’incertitude.
Avec ces conseils, vous êtes paré pour entraîner des réseaux très profonds! J’espère
que vous êtes à présent convaincu que vous pouvez aller très loin en utilisant simple-
ment l’API très pratique de Keras. Toutefois, le moment viendra probablement où
vous aurez besoin d’un contrôle plus important, par exemple pour écrire une fonction
de perte personnalisée ou pour adapter l’algorithme d’entraînement. Dans de tels cas,
vous devrez vous tourner vers l’API de bas niveau de TensorFlow, qui sera décrite au
prochain chapitre.
3.7 EXERCICES
1. Quel problème l’initialisation de Glorot et l’initialisation de He
cherchent-elles à résoudre ?
2. Peut-on donner la même valeur initiale à tous les poids si celle-ci est
choisie au hasard avec l’initialisation de He?
3. Peut-on initialiser les termes constants à zéro ?
4. Dans quels cas utiliseriez-vous chacune des fonctions d’activation
présentées dans ce chapitre ?
5. Que peut-il se passer lorsqu’un optimiseur SGD est utilisé et que
l’hyperparamètre momentum est trop proche de 1 (par exemple,
0,99999) ?
6. Donnez trois façons de produire un modèle creux.
7. La régularisation par abandon ralentit-elle l’entraînement ?
Ralentit-elle l’inférence (c’est-à-dire les prédictions sur de nouvelles
instances)? Qu’en est-il de l’abandon MC ?
8. Effectuez l’entraînement d’un réseau de neurones profond sur le jeu
de données d’images CIFAR10:
a. Construisez un réseau de neurones profond constitué de 20couches
cachées, chacune avec 100 neurones (c’est trop, mais c’est la
raison de cet exercice). Utilisez l’initialisation de He et la fonction
d’activation Swish.
b. En utilisant l’optimisation Nadam et l’arrêt précoce, entraînez-
le sur le jeu de données CIFAR10. Vous pouvez le charger avec
tf.keras.datasets.cifar10.load_data(). Le jeu de
3.7 Exercices 157
Depuis le début de cet ouvrage, nous avons employé uniquement l’API de haut niveau
de TensorFlow, Keras. Elle nous a permis d’aller déjà assez loin : nous avons construit
plusieurs architectures de réseaux de neurones, notamment des réseaux de régression
et de classication, des réseaux Wide & Deep et des réseaux autonormalisants, en
exploitant différentes techniques, comme la normalisation par lots, l’abandon et les
échéanciers de taux d’apprentissage.
En pratique, 95% des cas d’utilisation que vous rencontrerez n’exigeront rien de
plus que tf.keras (et tf.data; voir le chapitre5). Toutefois, il est temps à présent de
plonger au cœur de TensorFlow et d’examiner son API Python de bas niveau (https://
homl.info/tf2api). Elle devient indispensable lorsque nous avons besoin d’un contrôle
supplémentaire pour écrire des fonctions de perte personnalisées, des métriques
personnalisées, des couches, des modèles, des initialiseurs, des régulariseurs, des
contraintes de poids, etc. Il peut même arriver que nous ayons besoin d’un contrôle
total sur la boucle d’entraînement, par exemple pour appliquer des transformations
ou des contraintes particulières sur les gradients (au-delà du simple écrêtage) ou pour
utiliser plusieurs optimiseurs dans différentes parties du réseau.
Dans ce chapitre, nous allons examiner tous ces cas et nous verrons également
comment améliorer nos modèles personnalisés et nos algorithmes d’entraînement
en exploitant la fonctionnalité TensorFlow de génération automatique d’un graphe.
Mais commençons par un rapide aperçu de TensorFlow.
160 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
97. Cependant, la bibliothèque PyTorch de Facebook est à l’heure actuelle la plus utilisée dans le milieu
universitaire: on trouve davantage de publications citant PyTorch que TensorFlow ou Keras. Néanmoins,
la bibliothèque JAX de Google prend un bel essor, tout particulièrement dans ce secteur universitaire.
98. TensorFlow possédait une autre API de Deep Learning appelée Estimators API, désormais obsolète.
4.1 Présentation rapide de TensorFlow 161
tf.lookup
tf.math Mathématiques tf.nest
y compris algèbre Structures
tf.linalg tf.ragged de données
tf.signal linéaire et tf.sets
tf.random traitement tf.sparse spéciales
tf.bitwise du signal tf.strings
tf.audio tf.experimental
tf.data Entrées-sorties tf.config Divers
tf.image et prétraitement ...
tf.io
tf.queue
Visualisation
tf.summary avec Tensorboard
Au niveau le plus bas, chaque opération TensorFlow est implémentée par du code
C++ extrêmement efcace99.
De nombreuses opérations disposent de plusieurs implémentations appelées
noyaux (kernels) : chaque noyau est dédié à un type de processeur spécique, qu’il
s’agisse de CPU, de GPU ou même de TPU (tensor processing unit). Les GPU sont
capables d’accélérer considérablement les calculs en les découpant en portions plus
petites et en les exécutant en parallèle sur de nombreux threads de GPU. Les TPU
sont encore plus efcaces : il s’agit de circuits intégrés ASIC spécialisés dans les opé-
rations de Deep Learning100 (le chapitre11 explique comment utiliser TensorFlow
avec des GPU et des TPU).
99. Si jamais vous en aviez besoin, mais ce ne sera probablement pas le cas, vous pouvez écrire vos propres
opérations à l’aide de l’API C++.
100. Pour de plus amples informations sur les TPU et leur fonctionnement, consultez la page https://homl.
info/tpus.
162 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
TensorFlow s’exécute non seulement sur les ordinateurs sous Windows, Linux et
macOS, mais également sur les appareils mobiles, en version TensorFlow Lite, aussi
bien sous iOS que sous Android (voir le chapitre11). Si vous ne souhaitez pas uti-
liser l’API Python, il existe des API C++, Java et Swift. Il existe même une version
JavaScript appelée TensorFlow.js qui vous permet d’exécuter vos modèles directe-
ment dans votre navigateur.
TensorFlow ne se limite pas à sa bibliothèque. Il est au cœur d’un vaste écosys-
tème de bibliothèques. On trouve en premier lieu TensorBoard pour la visualisa-
tion (voir le chapitre2). TensorFlow Extended (TFX) (https://tensorow.org/tfx) est
un ensemble de bibliothèques développées par Google pour la mise en production
des projets TensorFlow : il fournit des outils de validation des données de prétraite-
ment, d’analyse des modèles et de service (avec TF Serving ; voir le chapitre11).
TensorFlow Hub de Google apporte une solution simple de téléchargement et de
réutilisation de réseaux de neurones préentraînés. Vous pouvez également trouver
de nombreuses architectures de réseaux de neurones, certains préentraînés, dans
le « jardin » de modèles TensorFlow (https://github.com/tensorow/models/). D’autres
projets fondés sur TensorFlow sont disponibles sur le site TensorFlow Resources
(https://www.tensorow.org/resources) ainsi que sur https://github.com/jtoy/awesome-
tensorow. Des centaines de projets TensorFlow sont présents sur GitHub et
permettent de trouver facilement du code existant correspondant à ce que vous
souhaitez faire.
4.2 Utiliser TensorFlow comme NumPy 163
Mais le plus important est que des opérations de toutes sortes sur les tenseurs sont
possibles :
>>> t + 10
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[11., 12., 13.],
[14., 15., 16.]], dtype=float32)>
>>> tf.square(t)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ 1., 4., 9.],
[16., 25., 36.]], dtype=float32)>
>>> t @ tf.transpose(t)
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[14., 32.],
[32., 77.]], dtype=float32)>
Un tenseur peut aussi contenir une valeur scalaire. Dans ce cas, sa forme (en
anglais, shape) est vide:
>>> tf.constant(42)
<tf.Tensor: shape=(), dtype=int32, numpy=42>
101. On notera toutefois une exception notable, tf.math.log(), qui est couramment utilisée mais ne
possède pas d’alias tf.log(), car cela pourrait prêter à confusion avec tf.logging.
4.2 Utiliser TensorFlow comme NumPy 165
Toutes les opérations mathématiques de base dont nous avons besoin (tf.
add(), tf.multiply(), tf.square(), tf.exp(), tf.sqrt(), etc.) et
la plupart des opérations existantes dans NumPy (par exemple, tf.reshape(),
tf.squeeze(), tf.tile()) sont disponibles. Certaines fonctions n’ont pas le
même nom que dans NumPy ; par exemple, tf.reduce_mean(), tf.reduce_
sum(), tf.reduce_max() et tf.math.log() sont les équivalents de np.
mean(), np.sum(), np.max() et np.log(). Lorsque les noms diffèrent, c’est
d’ordinaire pour une bonne raison. Par exemple, dans TensorFlow, nous devons écrire
tf.transpose(t)et non pas simplement t.T comme dans NumPy. En effet, la
fonction tf.transpose() ne fait pas exactement la même chose que l’attribut
T de NumPy. Dans TensorFlow, un nouveau tenseur est créé avec sa propre copie
des données permutées, tandis que, dans NumPy, t.T n’est qu’une vue permutée
sur les mêmes données. De façon comparable, l’opération tf.reduce_sum() se
nomme ainsi car son noyau GPU (c’est-à-dire son implémentation pour processeur
graphique) utilise un algorithme de réduction qui ne garantit pas l’ordre dans lequel
les éléments sont ajoutés : en raison de la précision limitée des nombres à virgule
ottante sur 32bits, le résultat peut changer très légèrement chaque fois que nous
appelons cette opération. C’est la même chose pour tf.reduce_mean() (mais
tf.reduce_max()est évidemment déterministe).
>>> np.square(t)
array([[ 1., 4., 9.],
[16., 25., 36.]], dtype=float32)
Notez que NumPy utilise par défaut une précision sur 64 bits, tandis que
celle de TensorFlow se limite à 32 bits. En effet, une précision sur 32 bits est
généralement suffisante pour les réseaux de neurones, sans compter que
l’exécution est ainsi plus rapide et l’encombrement mémoire plus faible. Par
conséquent, lorsque vous créez un tenseur à partir d’un tableau NumPy,
n’oubliez pas d’indiquer dtype=tf.float32.
Au premier abord, cela peut sembler quelque peu gênant, mais il faut se rappeler
que c’est pour la bonne cause ! Et, bien entendu, nous pouvons toujours utiliser tf.
cast()si nous avons également besoin de convertir des types :
>>> t2 = tf.constant(40., dtype=tf.float64)
>>> tf.constant(2.0) + tf.cast(t2, tf.float32)
<tf.Tensor: id=136, shape=(), dtype=float32, numpy=42.0>
4.2.4 Variables
Les valeurs tf.Tensor que nous avons vues jusqu’à présent sont immuables :
elles ne sont pas modiables. Cela signie que les poids d’un réseau de neurones ne
peuvent pas être représentés par des tenseurs normaux car la rétropropagation doit
être en mesure de les ajuster. Par ailleurs, d’autres paramètres doivent pouvoir évoluer
au l du temps (par exemple, un optimiseur à inertie conserve la trace des gradients
antérieurs). Nous avons donc besoin d’un tf.Variable :
>>> v = tf.Variable([[1., 2., 3.], [4., 5., 6.]])
>>> v
<tf.Variable 'Variable:0' shape=(2, 3) dtype=float32, numpy=
array([[1., 2., 3.],
[4., 5., 6.]], dtype=float32)>
si nous créons un tel tenseur à partir d’une chaîne de caractères Unicode (par
exemple, une chaîne Python3 normale comme "café"), elle sera encodée
automatiquement au format UTF-8 (par exemple, b"caf\xc3\xa9").
Nous pouvons également représenter des chaînes de caractères Unicode en
utilisant des tenseurs de type tf.int32, où chaque élément représente un
point de code Unicode (par exemple, [99, 97, 102, 233]). Le package
tf.strings (avec un s) dispose d’opérations pour les deux types de chaînes
de caractères, et pour les conversions de l’une à l’autre. Il est important de noter
qu’un objet tf.string est atomique, c’est-à-dire que sa longueur n’apparaît
pas dans la forme du tenseur. Après qu’il a été converti en un tenseur Unicode
(autrement dit, un tenseur de type tf.int32 contenant des points de code),
la longueur est présente dans la forme.
• Ensembles
Ils sont représentés sous forme de tenseurs normaux (ou de tenseurs creux).
Par exemple, tf.constant([[1, 2], [3, 4]]) représente les deux
ensembles {1, 2} et {3, 4}. Plus généralement, chaque ensemble est représenté
par un vecteur dans le dernier axe du tenseur. Les ensembles se manipulent à
l’aide des opérations provenant du package tf.sets.
• Files d’attente
Les les d’attente stockent des tenseurs au cours de plusieurs étapes. TensorFlow
propose différentes sortes de les d’attente: FIFO (rst in, rst out) simples
(FIFOQueue), les permettant de donner la priorité à certains éléments
(PriorityQueue), de mélanger leurs éléments (RandomShuffleQueue),
et de compléter des éléments de formes différentes par remplissage
(PaddingFIFOQueue). Toutes ces classes sont fournies par le package
tf.queue.
Grâce aux tenseurs, aux opérations, aux variables et aux différentes structures de
données, vous pouvez à présent personnaliser vos modèles et entraîner des algorithmes!
Il est également possible de retourner la perte moyenne au lieu des pertes des
instances prises individuellement, mais ce n’est pas recommandé car il serait alors
impossible d’utiliser des poids de classe ou des poids d’instance en fonction des
besoins (voir le chapitre2).
Vous pouvez à présent utiliser cette fonction de perte de Huber lors de la compila-
tion du modèle Keras, puis entraîner celui-ci comme d’habitude:
model.compile(loss=huber_fn, optimizer="nadam")
model.fit(X_train, y_train, [...])
model.compile(loss=create_huber(2.0), optimizer="nadam")
Examinons ce code :
• Le constructeur accepte **kwargs et les passe au constructeur parent, qui
s’occupe des hyperparamètres standard : le nom de la perte (name) et l’algorithme
(reduction) à utiliser pour agréger les pertes des instances individuelles.
Par défaut, il s’agit de "AUTO" qui équivaut à "SUM_OVER_BATCH_SIZE".
4.3 Personnaliser des modèles et entraîner des algorithmes 171
Autrement dit, la perte sera la somme des pertes des instances, pondérées par
leurs poids, le cas échéant, et divisée par la taille du lot (et non par la somme
des poids ; ce n’est donc pas la moyenne pondérée) 103. Il y a d’autres valeurs
possibles, comme "SUM" et "NONE".
• La méthode call() reçoit les étiquettes et des prédictions, calcule toutes les
pertes d’instance et les retourne.
• La méthode get_config() retourne un dictionnaire qui associe chaque
nom d’hyperparamètre à sa valeur. Elle commence par invoquer la méthode
get_config()de la classe parent, puis ajoute les nouveaux hyperparamètres
à ce dictionnaire104 .
Vous pouvez alors utiliser une instance de cette classe lors de la compilation du
modèle :
model.compile(loss=HuberLoss(2.), optimizer="nadam")
103. L’utilisation d’une moyenne pondérée n’est pas une bonne idée. En effet, deux instances de même
poids mais provenant de lots différents auraient alors un impact différent sur l’entraînement, en fonction
du poids total de chaque lot.
104. La syntaxe {**x, [...]} a été ajoutée en Python3.5, pour intégrer tous les couples clés/valeur
d’un dictionnaire x dans un autre dictionnaire. Depuis Python3.9, vous pouvez utiliser à la place la syntaxe
bien plus agréable x | y (où x et y sont deux dictionnaires).
172 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
l1(0.01)) et d’une contrainte qui s’assure que tous les poids sont positifs (équivalente
à tf.keras.constraints.nonneg() ou tf.nn.relu()) :
def my_softplus(z):
return tf.math.log(1.0 + tf.exp(z))
def my_l1_regularizer(weights):
return tf.reduce_sum(tf.abs(0.01 * weights))
Vous devez implémenter la méthode call() pour les pertes, les couches(ycompris
les fonctions d’activation) et les modèles, ou la méthode __call__() pour les
4.3 Personnaliser des modèles et entraîner des algorithmes 173
régulariseurs, les initialiseurs et les contraintes. Le cas des métriques est légèrement
différent, comme nous allons le voir.
Pendant l’entraînement, Keras calculera cette métrique pour chaque lot et conser-
vera une trace de sa moyenne depuis le début de l’époque. La plupart du temps,
c’est précisément ce que nous souhaitons. Mais pas toujours! Prenons, par exemple,
la précision d’un classicateur binaire. La précision correspond au nombre de vrais
positifs divisé par le nombre de prédictions positives (comprenant les vrais et les faux
positifs)106. Supposons que le modèle ait effectué cinq prédictions positives dans le
premier lot, quatre d’entre elles étant correctes : la précision est de 80%. Supposons
que le modèle ait ensuite effectué trois prédictions positives dans le second lot, mais
que toutes étaient incorrectes : la précision est alors de 0 % sur le second lot. Si
nous calculons simplement la moyenne des deux précisions, nous obtenons 40%.
Cependant, il ne s’agit pas de la précision du modèle sur ces deux lots ! En réalité, il
y a eu un total de quatre vrais positifs (4 + 0) sur huit prédictions positives (5 +3).
La précision globale est donc non pas de 40%, mais de 50%. Nous avons besoin
d’un objet capable de conserver une trace du nombre de vrais positifs et du nombre
de faux positifs, et de calculer sur demande la précision à partir de ces valeurs. C’est
précisément ce que réalise la classe tf.keras.metrics.Precision:
>>> precision = tf.keras.metrics.Precision()
>>> precision([0, 1, 1, 1, 0, 1, 0, 1], [1, 1, 0, 1, 0, 1, 0, 1])
<tf.Tensor: shape=(), dtype=float32, numpy=0.8>
>>> precision([0, 1, 0, 0, 1, 0, 1, 1], [1, 0, 1, 1, 0, 0, 0, 0])
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>
105. Cependant, la perte de Huber est rarement utilisée comme métrique: la préférence va à la MAE ou
à la MSE.
106. Voir le chapitre3 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition,
2023).
174 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
Dans cet exemple, nous créons un objet Precision, puis nous l’utilisons comme
une fonction en lui passant les étiquettes et les prédictions du premier lot puis du
second (nous pourrions également passer des poids d’échantillonnage). Nous utili-
sons le même nombre de vrais et de faux positifs que dans l’exemple décrit. Après le
premier lot, l’objet retourne une précision de 80%, et de 50% après le second (elle
correspond non pas à la précision du second lot mais à la précision globale). Il s’agit
d’une métrique en continu (streaming metric), ou d’une métrique à états (stateful metric),
qui est actualisée progressivement, lot après lot.
Nous pouvons appeler la méthode result()à n’importe quel moment de façon
à obtenir la valeur courante de la métrique. Nous pouvons également examiner
ses variables (suivre le nombre de vrais et de faux positifs) au travers de l’attribut
variables. Et nous pouvons les réinitialiser en invoquant la méthode reset_
states():
>>> precision.result()
<tf.Tensor: shape=(), dtype=float32, numpy=0.5>
>>> precision.variables
[<tf.Variable 'true_positives:0' [...], numpy=array([4.], dtype=float32)>,
<tf.Variable 'false_positives:0' [...], numpy=array([4.], dtype=float32)>]
>>> precision.reset_states() # les deux variables sont remises à 0.0
Si vous avez besoin de dénir votre propre métrique en continu, il suft de créer
une sous-classe de keras.metrics.Metric. Voici un exemple simple, qui garde
trace de la perte totale de Huber et du nombre d’instances rencontrées. Sur demande
du résultat, elle retourne le rapport, qui correspond à la perte moyenne de Huber:
class HuberMetric(tf.keras.metrics.Metric):
def __init__(self, threshold=1.0, **kwargs):
super().__init__(**kwargs) # traiter les arguments de base (comme dtype)
self.threshold = threshold
self.huber_fn = create_huber(threshold)
self.total = self.add_weight("total", initializer="zeros")
self.count = self.add_weight("count", initializer="zeros")
def update_state(self, y_true, y_pred, sample_weight=None):
sample_metrics = self.huber_fn(y_true, y_pred)
self.total.assign_add(tf.reduce_sum(sample_metrics))
self.count.assign_add(tf.cast(tf.size(y_true), tf.float32))
def result(self):
return self.total / self.count
def get_config(self):
base_config = super().get_config()
return {**base_config, "threshold": self.threshold}
107. Cette classe n’est là qu’à titre d’illustration. Il serait préférable et plus simple de créer une sous-classe
de keras.metrics.Mean; voir l’exemple donné dans la section « Streaming metrics » du notebook
(voir «12_custom_models_and_training_with_tensorow.ipynb » sur https://homl.info/colab3).
4.3 Personnaliser des modèles et entraîner des algorithmes 175
Lorsque nous dénissons une métrique à l’aide d’une simple fonction, Keras
l’appelle automatiquement pour chaque lot et conserve la trace de la moyenne sur
chaque époque, comme nous l’avons fait manuellement. Le seul avantage de notre
classe HuberMetric réside donc dans la sauvegarde de threshold. Mais, évi-
demment, certaines métriques, comme la précision, ne peuvent pas être obtenues
par une simple moyenne sur l’ensemble des lots. Dans de tels cas, il n’y a pas d’autre
option que d’implémenter une métrique en continu.
Maintenant que nous savons construire une métrique en continu, la création
d’une couche personnalisée sera une promenade de santé !
Cette couche personnalisée peut ensuite être utilisée comme n’importe quelle
autre couche, que ce soit avec l’API séquentielle, fonctionnelle ou de sous-
classement. Vous pouvez également vous en servir comme fonction d’activation
ou utiliser activation=tf.exp. L’exponentielle est parfois employée dans la
couche de sortie d’un modèle de régression lorsque les échelles des valeurs à pré-
dire sont très différentes (par exemple, 0,001, 10, 1 000). La fonction exponentielle
faisant partie des fonctions d’activation standard dans Keras, il vous suft d’utiliser
activation="exponential".
Vous l’avez probablement deviné, pour construire une couche personnalisée avec
état (c’est-à-dire une couche qui possède des poids), vous devez créer une sous-classe
de tf.keras.layers.Layer. Par exemple, la classe suivante implémente une
version simpliée de la couche Dense :
class MyDense(tf.keras.layers.Layer):
def __init__(self, units, activation=None, **kwargs):
super().__init__(**kwargs)
self.units = units
self.activation = tf.keras.activations.get(activation)
def get_config(self):
base_config = super().get_config()
return {**base_config, "units": self.units,
"activation": tf.keras.activations.serialize(self.activation)}
Étudions ce code :
• Le constructeur prend tous les hyperparamètres en arguments (dans cet exemple,
units et activation) et, plus important, il prend également un argument
**kwargs. Il invoque le constructeur parent, en lui passant les kwargs ;
cela permet de prendre en charge les arguments standard, comme input_
shape, trainable et name. Il enregistre ensuite les hyperparamètres sous
forme d’attributs, en convertissant l’argument activation en une fonction
d’activation appropriée grâce à la fonction tf.keras.activations.
get() (elle accepte des fonctions, des chaînes de caractères standard comme
"relu" ou "swish", ou juste None).
• La méthode build() a pour rôle de créer les variables de la couche en
invoquant la méthode add_weight() pour chaque poids. build() est
4.3 Personnaliser des modèles et entraîner des algorithmes 177
Pour créer une couche ayant de multiples entrées (par exemple, Concatenate),
l’argument de la méthode call()doit être un n-uplet qui contient toutes les entrées.
Pour créer une couche avec de multiples sorties, la méthode call()doit retourner
la liste des sorties. Par exemple, la couche suivante possède deux entrées et retourne
trois sorties :
class MyMultiLayer(tf.keras.layers.Layer):
def call(self, X):
X1, X2 = X
return [X1 + X2, X1 * X2, X1 / X2]
Cette couche peut à présent être utilisée comme n’importe quelle autre couche,
mais, évidemment, uniquement avec l’API fonctionnelle et l’API de sous-classement.
Elle est incompatible avec l’API séquentielle, qui n’accepte que des couches ayant
une entrée et une sortie.
Si la couche doit afcher des comportements différents au cours de l’en-
traînement et des tests (par exemple, si elle utilise les couches Dropout ou
BatchNormalization), nous devons ajouter un argument training à la
méthode call() et nous en servir pour décider du comportement approprié. Par
108. L’API de Keras nomme cet argument input_shape, mais, puisqu’il comprend également la dimension
du lot, je préfère l’appeler batch_input_shape. Il en va de même pour compute_output_shape().
178 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
exemple, créons une couche qui ajoute un bruit gaussien pendant l’entraînement
(pour la régularisation), mais rien pendant les tests (Keras dispose d’une couche
ayant ce comportement, tf.keras.layers.GaussianNoise) :
class MyGaussianNoise(tf.keras.layers.Layer):
def __init__(self, stddev, **kwargs):
super().__init__(**kwargs)
self.stddev = stddev
Avec tous ces éléments, vous pouvez construire n’importe quelle couche person-
nalisée! Voyons à présent comment créer des modèles personnalisés.
Dense
ResidualBlock +
ResidualBlock Dense
×3
Dense Dense
Figure 4.3 – Exemple de modèle personnalisé : un modèle quelconque avec une couche
personnalisée ResidualBlock qui contient une connexion de saut
109. En général, « API de sous-classement » fait référence uniquement à la création de modèles personna-
lisés en utilisant l’héritage. Mais, comme nous l’avons vu dans ce chapitre, bien d’autres structures peuvent
être créées de cette manière.
4.3 Personnaliser des modèles et entraîner des algorithmes 179
Les entrées passent par une première couche dense, puis par un bloc résiduel
constitué de deux couches denses et d’une opération d’addition (nous le verrons
au chapitre6, un bloc résiduel additionne ses entrées à ses sorties), puis de nou-
veau par ce bloc résiduel à trois reprises, puis par un second bloc résiduel, et le
résultat final traverse une couche de sortie dense. Notez que ce modèle n’a pas
beaucoup de sens; il s’agit simplement d’un exemple qui nous permet d’illus-
trer la facilité de construction de n’importe quel modèle, même ceux contenant
des boucles et des connexions de saut. Pour l’implémenter, il est préférable de
commencer par créer une couche ResidualBlock, car nous allons créer deux
blocs identiques (et nous pourrions souhaiter réutiliser un tel bloc dans un autre
modèle) :
class ResidualBlock(tf.keras.layers.Layer):
def __init__(self, n_layers, n_neurons, **kwargs):
super().__init__(**kwargs)
self.hidden = [tf.keras.layers.Dense(n_neurons, activation="relu",
kernel_initializer="he_normal")
for _ in range(n_layers)]
Nous créons les couches dans le constructeur et les utilisons dans la méthode
call(). Ce modèle peut être employé comme n’importe quel autre modèle : com-
pilation, ajustement, évaluation et exécution pour effectuer des prédictions. Pour
que nous puissions l’enregistrer avec la méthode save() et le charger avec la
fonction tf.keras.models.load_model(), nous devons implémenter la
180 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
Examinons-le:
• Le constructeur crée le réseau de neurons profond constitué de cinq couches
cachées denses et d’une couche de sortie dense. Nous créons aussi une
métrique continue Mean qui gardera trace de l’erreur de reconstruction durant
l’entraînement.
• La méthode build() crée une couche dense supplémentaire qui servira à
reconstruire les entrées du modèle. Sa création doit se faire à cet endroit car son
nombre d’unités doit être égal au nombre d’entrées et cette valeur est inconnue
jusqu’à l’invocation de la méthode build() 110 .
• La méthode call()traite les entrées au travers des cinq couches cachées, puis
passe le résultat à la couche de reconstruction, qui produit la reconstruction.
• Ensuite, la méthode call() calcule la perte de reconstruction (l’écart
quadratique moyen entre la reconstruction et les entrées) et l’ajoute à la liste
110. Si l’on se réfère au défaut TensorFlow n° 46858, l’appel à super().build() peut échouer dans
ce cas, à moins que le problème ait été corrigé depuis. Sinon, vous devez remplacer cette ligne par self.
built = True.
182 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
Dans la plupart des cas, tout ce que nous venons de présenter sufra à implémenter
le modèle dont vous avez besoin, même pour des architectures, des pertes et des
métriques complexes. Toutefois, pour certaines architectures telles que les GAN (voir
chapitre9), vous aurez besoin de personnaliser la boucle d’entraînement elle-même.
Avant d’en expliquer la procédure, nous devons décrire le fonctionnement du calcul
automatique des gradients dans TensorFlow.
111. Nous pouvons également appeler add_loss() sur n’importe quelle couche interne au modèle, car
celui-ci collecte de façon récursive les pertes depuis toutes ses couches.
4.3 Personnaliser des modèles et entraîner des algorithmes 183
Cela semble plutôt bien ! C’est le cas, et la solution est facile à implémenter. Cela
dit, il ne s’agit que d’une approximation et, plus important encore, nous devons
appeler f() au moins une fois par paramètre (non pas deux, car nous pouvons cal-
culer f(w1, w2) juste une fois). En raison de cette obligation, cette approche
n’est pas envisageable avec les grands réseaux de neurones. À la place, nous devons
employer la différentiation automatique en mode inverse. Grâce à TensorFlow, c’est
assez simple :
w1, w2 = tf.Variable(5.), tf.Variable(3.)
with tf.GradientTape() as tape:
z = f(w1, w2)
Nous dénissons tout d’abord les variables w1 et w2, puis nous créons un contexte
tf.GradientTape qui enregistrera automatiquement chaque opération impli-
quant une variable, et nous demandons à cet enregistrement de calculer les gradients
du résultat z en rapport avec les deux variables [w1, w2]. Examinons les gradients
calculés par TensorFlow :
>>> gradients
[<tf.Tensor: shape=(), dtype=float32, numpy=36.0>,
<tf.Tensor: shape=(), dtype=float32, numpy=10.0>]
Parfait ! Non seulement le résultat est précis (la précision est uniquement
limitée par les erreurs d’arrondi des calculs en virgule ottante), mais la méthode
gradient()est également passée une seule fois sur les calculs enregistrés (en ordre
inverse), quel que soit le nombre de variables existantes. Elle est donc incroyable-
ment efcace. C’est magique !
S’il nous faut invoquer gradient()plus d’une fois, nous devons rendre l’enre-
gistrement persistant et le supprimer chaque fois que nous n’en avons plus besoin de
façon à libérer ses ressources112 :
with tf.GradientTape(persistent=True) as tape:
z = f(w1, w2)
Par défaut, l’enregistrement ne conserve que les opérations qui impliquent des
variables. Si nous essayons de calculer le gradient de z par rapport à autre chose
qu’une variable, le résultat est None :
c1, c2 = tf.constant(5.), tf.constant(3.)
with tf.GradientTape() as tape:
z = f(c1, c2)
Cette possibilité se révélera parfois utile, par exemple pour implémenter une
perte de régularisation qui pénalise les activations qui varient beaucoup alors que
les entrées varient peu : la perte sera fondée sur le gradient des activations en lien
avec les entrées. Puisque les entrées ne sont pas des variables, nous devons indiquer à
l’enregistrement de les surveiller.
Le plus souvent, un enregistrement de gradients sert à calculer les gradients d’une
seule valeur (en général la perte) par rapport à un ensemble de valeurs (en général les
paramètres du modèle). C’est dans ce contexte que la différentiation automatique en
mode inverse brille, car elle doit uniquement effectuer une passe en avant et une passe
en arrière pour obtenir tous les gradients en une fois. Si nous calculons les gradients
d’un vecteur, par exemple un vecteur qui contient plusieurs pertes, TensorFlow va cal-
culer des gradients de la somme du vecteur. Par conséquent, si nous avons besoin d’ob-
tenir des gradients individuels (par exemple, les gradients de chaque perte en lien avec
les paramètres du modèle), nous devons appeler la méthode jabobian()de l’enregis-
trement. Elle effectuera une différentiation automatique en mode inverse pour chaque
perte du vecteur (par défaut, toutes exécutées en parallèle). Il est même possible de
calculer des dérivées partielles de deuxième ordre (les hessiens, c’est-à-dire les dérivées
112. Si l’enregistrement n’est plus accessible, par exemple lorsque la fonction qui l’a utilisé se termine, le
ramasse-miettes de Python le supprimera pour nous.
4.3 Personnaliser des modèles et entraîner des algorithmes 185
partielles des dérivées partielles), mais, en pratique, ce besoin est rare (un exemple est
donné dans la section « Computing Gradients with Autodiff » du notebook113).
Parfois, il faudra arrêter la rétropropagation des gradients dans certaines par-
ties du réseau de neurones. Pour cela, nous devons utiliser la fonction tf.stop_
gradient(). Au cours de la passe en avant, elle retourne son entrée (comme
tf.identity()), mais, pendant la rétropropagation, elle ne laisse pas passer les
gradients (elle agit comme une constante) :
def f(w1, w2):
return 3 * w1 ** 2 + tf.stop_gradient(2 * w1 * w2)
Pour résoudre ce problème, il est souvent bon d’ajouter une petite valeur à x (telle
que 10–6) lorsqu’on calcule sa racine carrée.
La fonction exponentielle est aussi fréquemment source de problèmes du fait de
sa croissance extrêmement rapide. À titre d’exemple, la façon dont nous avons déni
précédemment la fonction my_softplus() n’est pas stable numériquement. Si
vous calculez my_softplus(100.0), vous obtiendrez pour résultat infinity,
plutôt que la valeur correcte qui est voisine de 100. Mais il est possible de réécrire la
fonction pour la rendre stable numériquement: la fonction softplus est dénie par
log(1 + exp(z)), qui est aussi égal à log(1 + exp(–|z|)) + max(z, 0) (pour la preuve
mathématique, reportez-vous au notebook) et l’avantage de la deuxième forme est
que le terme exponentiel ne peut pas exploser. Voici donc une meilleure implémen-
tation de la fonction my_softplus:
def my_softplus(z):
return tf.math.log(1 + tf.exp(-tf.abs(z))) + tf.maximum(0., z)
Dans quelques rares cas, une fonction numériquement stable peut avoir néanmoins
des gradients numériquement instables. Vous devrez alors indiquer à TensorFlow
quelle équation utiliser pour les gradients, plutôt que de le laisser utiliser autodiff.
Pour cela, vous devez utiliser le décorateur @tf.custom.gradient lorsque vous
Si vous connaissez les bases du calcul différentiel, vous trouverez que la dérivée
de log(1 + exp(z)) est exp(z) / (1 + exp(z)). Mais cette forme n’est pas stable: pour
les grandes valeurs de z, on aboutira à calculer une valeur innie divisée par une
valeur innie, ce qui renvoie « NaN ». Cependant, avec quelques transformations,
vous pouvez montrer que c’est égal à 1 – 1 / (1 + exp(z)), qui est stable. La fonction
utilise cette formule pour calculer les gradients. Notez que cette fonction recevra
en entrée les gradients qui ont été rétropropagés jusque-là, jusqu’à la fonction my_
softplus(), et que, selon la règle de dérivation en chaîne, nous devons les multi-
plier par les gradients de cette fonction.
À présent, le calcul des gradients de la fonction my_softplus()nous donne le
résultat correct, même pour les grandes valeurs d’entrée.
Félicitations! Vous savez désormais calculer les gradients de n’importe quelle
fonction (à condition qu’elle soit différentiable au point du calcul), bloquer la rétro-
propagation au besoin, et écrire vos propres fonctions de gradient! Cette souplesse
va probablement bien au-delà de ce dont vous aurez besoin, même si vous construisez
vos propres boucles d’entraînement personnalisées. C’est ce que nous allons voir
maintenant.
Créons ensuite une petite fonction qui sélectionne de façon aléatoire un lot d’ins-
tances à partir du jeu d’entraînement (au chapitre5 nous verrons l’API tf.data, qui
offre une bien meilleure solution):
def random_batch(X, y, batch_size=32):
idx = np.random.randint(len(X), size=batch_size)
return X[idx], y[idx]
Ce code n’a pas besoin d’explication, sauf si le formatage des chaînes de caractères
dans Python vous est inconnu: {m.result():.4f} afche la métrique (nombre
à virgule ottante) avec quatre chiffres après la virgule, et la combinaison de \r
(retour-chariot) et de end="" garantit que la barre d’état sera toujours afchée sur
la même ligne. Revenons au sujet principal ! Tout d’abord, nous devons dénir les
divers paramètres et choisir l’optimiseur, la fonction de perte et les métriques (dans
cet exemple, uniquement la MAE):
n_epochs = 5
batch_size = 32
n_steps = len(X_train) // batch_size
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
loss_fn = tf.keras.losses.mean_squared_error
mean_loss = tf.keras.metrics.Mean(name="mean_loss")
metrics = [tf.keras.metrics.MeanAbsoluteError()]
Vous le voyez, pour que tout fonctionne correctement, il faut faire attention à beau-
coup de choses et il est facile de se tromper. En revanche, vous obtenez un contrôle total.
Puisque vous savez désormais personnaliser tous les éléments de vos modèles 114
et de vos algorithmes d’entraînement, voyons comment utiliser la fonctionnalité
TensorFlow de génération automatique d’un graphe. Elle peut accélérer considérable-
ment le code personnalisé et le rendre portable sur toutes les plateformes reconnues
par TensorFlow.
Nous pouvons évidemment appeler cette fonction en lui passant une valeur
Python, comme un entier ou réel, ou bien un tenseur :
>>> cube(2)
8
>>> cube(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>
114. À l’exception des optimiseurs, mais rares sont les personnes qui les personnalisent ; un exemple est
donné dans la section «Custom Optimizers » du notebook (voir « 12_custom_models_and_training_with_
tensorow.ipynb » sur https://homl.info/colab3).
190 Chapitre 4. Modèles personnalisés et entraînement avec TensorFlow
115. Dans notre exemple simple, le graphe de calcul est trop petit pour qu’une optimisation quelconque
puisse être mise en place. tf_cube() s’exécute donc plus beaucoup lentement que cube().
4.4 Fonctions et graphes TensorFlow 191
Par défaut, une fonction TF génère un nouveau graphe pour chaque ensemble
unique de formes et de types de données d’entrée, et le met en cache pour les appels
suivants. Par exemple, si nous appelons tf_cube(tf.constant(10)), un
graphe est généré pour des tenseurs de type int32 et de forme []. Si, par la suite,
nous appelons tf_cube(tf.constant(20)), ce même graphe est réutilisé. En
revanche, si nous appelons tf_cube(tf.constant([10, 20])), un nouveau
graphe est généré pour des tenseurs de type int32 et de forme [2]. C’est de cette
manière que les fonctions TF prennent en charge le polymorphisme (c’est-à-dire
les arguments de type et de forme variables). Toutefois, cela ne concerne que les
arguments qui sont des tenseurs. Si nous passons des valeurs Python numériques à
une fonction TF, un nouveau graphe est généré pour chaque valeur distincte. Par
exemple, appeler tf_cube(10) et tf_cube(20) produit deux graphes.
AutoGraph
Tenseur
Sortie
Graphe
(abrégé)
Traçage
Entrée
tf.autograph.to_code(sum_squares.python_function) af-
fiche le code source de la fonction générée. Le code n’est peut-être pas joli,
mais il peut parfois aider au débogage.
4.5 EXERCICES
1. Comment décririez-vous TensorFlow en quelques mots ? Quelles
sont ses principales fonctionnalités ? Pouvez-vous nommer d’autres
bibliothèques de Deep Learning connues ?
2. TensorFlow est-il un remplaçant direct de NumPy ? Quelles sont
leurs principales différences ?
3. Les appels tf.range(10) et tf.constant(np.arange(10))
produisent-ils le même résultat ?
4. Hormis les tenseurs classiques, nommez six autres structures de
données disponibles dans TensorFlow.
5. Une fonction de perte personnalisée peut être dénie en écrivant
une fonction ou une sous-classe de keras.losses.Loss. Dans
quels cas devez-vous employer chaque option ?
6. De façon comparable, une métrique personnalisée peut être dénie
dans une fonction ou une sous-classe de tf.keras.metrics.
Metric. Dans quels cas devez-vous utiliser chaque option ?
7. Quand devez-vous créer une couche personnalisée plutôt qu’un
modèle personnalisé ?
8. Donnez quelques cas d’utilisation qui nécessitent l’écriture d’une
boucle d’entraînement personnalisée.
9. Les composants Keras personnalisés peuvent-ils contenir du code
Python quelconque ou doivent-ils être convertibles en fonctions TF ?
10. Quelles sont les principales règles à respecter si une fonction doit
être convertible en une fonction TF ?
4.5 Exercices 195
116. Voir le chapitre2 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3e édition, 2023)
198 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
L’API tf.data est une API de lecture en continu (ou streaming API) : elle est
très efficace pour effectuer une itération sur l’ensemble des éléments d’un
dataset mais n’est pas conçue pour l’utilisation d’indices ou de tranches.
from_tensor
_slices(x) repeat(3) batch(7)
6
0
5
x3
1
4
x 2 6 5 4 3 2 1 0 9 8 7
3
2
1
9 7
0
Dans cet exemple, nous commençons par appliquer la méthode repeat() sur le
dataset d’origine. Elle renvoie un nouveau dataset qui contient trois répétitions des
éléments du dataset initial. Bien évidemment, toutes les données ne sont pas copiées
trois fois en mémoire ! (Si nous appelons cette méthode sans argument, le nouveau
dataset répète indéniment les données du dataset source. Dans ce cas, le code qui
parcourt le dataset doit décider quand s’arrêter.)
Ensuite nous appliquons la méthode batch() sur ce nouveau dataset, et obte-
nons encore un nouveau dataset. Celui-ci répartit les éléments du précédent en lots
de sept éléments. Enn, nous itérons sur les éléments de ce dataset nal. La méthode
batch() a été obligée de produire un lot nal dont la taille est non pas 7 mais 2.
Si nous préférons écarter ce lot nal de sorte qu’ils aient tous la même taille, nous
pouvons invoquer cette méthode avec drop_remainder=True.
La méthode map() est celle que vous appellerez pour appliquer un prétraite-
ment à vos données. Celle-ci générera parfois des calculs intensifs, comme le chan-
gement de forme d’une image ou sa rotation. Dans ce cas, il vaudra mieux lancer
plusieurs threads pour accélérer son exécution: il suft d’indiquer dans l’argument
num_parallel_calls le nombre de threads à créer ou tf.data.AUTOTUNE.
La fonction passée à la méthode map() doit être convertible en fonction TF (voir
le chapitre4).
Pour ltrer le dataset, il suft simplement d’utiliser la méthode filter(). Cet
exemple crée un jeu de données ne contenant que les lots dont la somme des élé-
ments est supérieure à 50 :
>>> dataset = dataset.filter(lambda x: tf.reduce_sum(x) > 50)
>>> for item in dataset:
... print(item)
...
tf.Tensor([14 16 18 0 2 4 6], shape=(7,), dtype=int32)
tf.Tensor([ 8 10 12 14 16 18 0], shape=(7,), dtype=int32)
tf.Tensor([ 2 4 6 8 10 12 14], shape=(7,), dtype=int32)
5.1 L’API tf.data 201
Si vous appelez repeat() sur un dataset mélangé, elle générera par défaut
un nouvel ordre à chaque itération. Cela correspond en général au compor-
tement souhaité. Mais, si vous préférez réutiliser le même ordre à chaque
itération (par exemple pour les tests ou le débogage), vous pouvez indiquer
reshuffle_each_iteration=False.
Nous pourrions également utiliser des noms de chiers génériques, par exemple
train_filepaths = "datasets/housing/my_train_*.csv". Nous
créons à présent un dataset qui ne contient que ces chemins de chiers :
filepath_dataset = tf.data.Dataset.list_files(train_filepaths, seed=42)
Pour que l’entrelacement soit efficace, il est préférable que les fichiers soient
de la même longueur. Dans le cas contraire, la fin du fichier le plus long ne
sera pas entrelacée.
Cela correspond aux premières lignes (la ligne d’en-tête étant ignorée) de cinq
chiers CSV choisis aléatoirement. C’est plutôt pas mal !
204 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
def parse_csv_line(line):
defs = [0.] * n_inputs + [tf.constant([], dtype=tf.float32)]
fields = tf.io.decode_csv(line, record_defaults=defs)
return tf.stack(fields[:-1]), tf.stack(fields[-1:])
def preprocess(line):
x, y = parse_csv_line(line)
return (x – X_mean) / X_std, y
Parcourons ce code :
• Tout d’abord, le code suppose que nous avons calculé auparavant la moyenne et
l’écart-type de chaque variable du jeu d’entraînement. X_mean et X_std sont
de simples tenseurs à une dimension (ou des tableaux NumPy) qui contiennent
huit nombres à virgule ottante, un par caractéristique d’entrée. Ceci peut être
réalisé en utilisant un StandardScaler de Scikit-Learn sur un échantillon
aléatoire signicatif du jeu de données. Dans la suite de ce chapitre, nous
utiliserons à la place une couche de prétraitement Keras.
• La fonction parse_csv_line() prend une ligne CSV et l’analyse.
Pour cela, elle se sert de la fonction tf.io.decode_csv(), qui attend
deux arguments : le premier est la ligne à analyser, le second est un tableau
contenant la valeur par défaut de chaque colonne du chier CSV. Ce tableau
defs indique à TensorFlow non seulement la valeur par défaut de chaque
colonne, mais également le nombre de colonnes et leurs types. Dans cet
exemple, nous précisons que toutes les colonnes contiennent des nombres
à virgule ottante et que les valeurs manquantes doivent être xées à 0.
Toutefois, pour la dernière colonne (la cible), nous fournissons un tableau
vide de type tf.float32 comme valeur par défaut. Ceci indique à
TensorFlow que cette colonne contient des nombres à virgule ottante, sans
aucune valeur par défaut. Une exception sera donc lancée en cas de valeur
manquante.
• La fonction tf.io.decode_csv() retourne une liste de tenseurs de
type scalaire (un par colonne), alors que nous devons retourner un tableau
de tenseurs à une dimension. Nous appelons donc tf.stack() sur tous
5.1 L’API tf.data 205
les tenseurs à l’exception du dernier (la cible) : cette opération empile les
tenseurs dans un tableau à une dimension. Nous faisons ensuite de même pour
la valeur cible (elle devient un tableau de tenseurs à une dimension avec une
seule valeur à la place d’un tenseur de type scalaire). La fonction tf.io.
decode_csv() a terminé : elle renvoie les caractéristiques d’entrée et la
cible.
• Enfin, la fonction personnalisée preprocess() se contente d’appeler
la fonction parse_csv_line(), normalise les caractéristiques d’entrée
et renvoie un n-uplet qui contient les caractéristiques normalisées et la
cible.
Testons cette fonction de prétraitement:
>>> preprocess(b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782')
(<tf.Tensor: shape=(8,), dtype=float32, numpy=
array([ 0.16579159, 1.216324 , -0.05204564, -0.39215982, -0.5277444 ,
-0.2633488 , 0.8543046 , -1.3072058 ], dtype=float32)>,
<tf.Tensor: shape=(1,), dtype=float32, numpy=array([2.782], dtype=float32)>)
list_files() interleave()
“a.csv”
“d.csv”
TextLineDataset
“b.csv”
“f.csv”
“c.csv” TextLineDataset
“d.csv” “b.csv”
TextLineDataset
... Cycle
Emplacement
actuel CSV CSV CSV
preprocess()
119. En général, la lecture anticipée d’un lot suft. Mais, dans certains cas, il peut être bon de lire à
l’avance un plus grand nombre de lots. Il est également possible de laisser TensorFlow décider automatique-
ment en transmettant tf.data.AUTOTUNE à prefetch().
120. Jetez un œil à la fonction expérimentale tf.data.experimental.prefetch_to_device(),
qui est capable de précharger directement des données dans le GPU. Une fonction ou une classe TensorFlow
comportant le mot experimental dans son nom risque d'être modiée sans préavis dans les versions
futures. Si une fonction expérimentale échoue, essayez de supprimer le mot experimental: elle a peut-
être été intégrée dans l’API de base. Si ce n’est pas le cas, consultez le notebook car je m’efforcerai d’y
maintenir le code à jour.
5.1 L’API tf.data 207
Sans prélecture
Utilisation 1 Utilisation 2
Temps
Avec prélecture
Si le dataset est sufsamment petit pour tenir en mémoire, nous pouvons accélérer
l’entraînement en utilisant sa méthode cache() : elle place son contenu dans un
cache en mémoire RAM. Cette opération se fait généralement après le chargement
et le prétraitement des données, mais avant leur mélange, leur répétition, leur mise
en lot et leur lecture anticipée. De cette manière, chaque instance ne sera lue et
prétraitée qu’une seule fois (au lieu d’une fois par époque), mais les données resteront
mélangées différemment à chaque époque, et le lot suivant sera toujours préparé à
l’avance.
Vous savez à présent construire un pipeline d’entrée efcace pour charger et
prétraiter des données provenant de plusieurs chiers texte. Nous avons décrit
les méthodes les plus utilisées sur un dataset, mais quelques autres méritent
votre attention : concatenate(), zip(), window(), reduce(), shard(),
flat_map(), apply(), unbatch() et padded_batch(). Par ailleurs,
quelques méthodes de classe supplémentaires, comme from_generator() et
208 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
Il ne nous reste plus qu’à construire et à entraîner un modèle Keras avec ces data-
sets. Lorsque nous appelons la méthode fit(), nous lui transmettons train_
set à la place de X_train, y_train, et lui transmettons validation_
data=valid_set à la place de validation_data=(X_valid, y_valid).
La méthode fit() se chargera de transmettre le dataset d’entraînement à chaque
époque, dans un ordre différent à chaque fois:
model = tf.keras.Sequential([...])
model.compile(loss="mse", optimizer="sgd")
model.fit(train_set, validation_data=valid_set, epochs=5)
En réalité, il est même possible de créer une fonction TF (voir le chapitre4) qui
entraîne le modèle durant toute une époque. Ceci peut réellement accélérer l’entraî-
nement :
@tf.function
def train_one_epoch(model, optimizer, loss_fn, train_set):
for X_batch, y_batch in train_set:
with tf.GradientTape() as tape:
y_pred = model(X_batch)
main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))
loss = tf.add_n([main_loss] + model.losses)
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01)
loss_fn = tf.keras.losses.mean_squared_error
for epoch in range(n_epochs):
print("\rEpoch {}/{}".format(epoch + 1, n_epochs), end="")
train_one_epoch(model, optimizer, loss_fn, train_set)
Si les fichiers CSV (ou de tout autre format) vous conviennent parfaitement,
rien ne vous oblige à employer le format TFRecord. Comme le dit le dicton,
« si ça marche, il ne faut pas y toucher ! » Les fichiers TFRecord seront utiles
lorsque le goulot d’étranglement pendant l’entraînement réside dans le char-
gement et l’analyse des données.
string name = 1;
int32 id = 2;
repeated string email = 3;
}
En bref, nous importons la classe d’accès Person produite par protoc, nous en
créons une instance, que nous manipulons et afchons, et dont nous lisons et modions
certains champs, puis nous la sérialisons avec la méthode SerializeToString().
Nous obtenons des données binaires au format Protobuf, prêtes à être enregistrées
ou transmises sur le réseau. Lors de la lecture ou de la réception de ces données
121. Puisque les objets protobuf sont destinés à être sérialisés et transmis, ils sont appelés messages.
212 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
122. Ce chapitre présente le strict minimum sur les protobufs dont vous avez besoin pour utiliser des
chiers TFRecord. Pour de plus amples informations, consultez le site https://homl.info/protobuf.
5.2 Le format TFRecord 213
Pourquoi avoir défini Example s’il ne contient rien de plus qu’un objet
Features ? La raison est simple : les développeurs de TensorFlow pour-
raient décider un jour de lui ajouter d’autres champs. Tant que la nouvelle
définition Example contient le champ features, avec le même identi-
fiant, elle maintient une compatibilité ascendante. Cette extensibilité est l’une
des grandes caractéristiques du format Protobuf.
person_example = Example(
features=Features(
feature={
"name": Feature(bytes_list=BytesList(value=[b"Alice"])),
"id": Feature(int64_list=Int64List(value=[123])),
"emails": Feature(bytes_list=BytesList(value=[b"[email protected]",
b"[email protected]"]))
}))
Ce code est un tantinet verbeux et répétitif, mais il est facile de l’emballer dans une
petite fonction utilitaire. Nous disposons à présent d’un protobuf Example qui nous
permet de sérialiser ses données en invoquant sa méthode SerializeToString()
et de les écrire dans un chier TFRecord. Écrivons ces données cinq fois, comme s’il
y avait plusieurs contacts:
with tf.io.TFRecordWriter("my_contacts.tfrecord") as f:
for _ in range(5):
f.write(person_example.SerializeToString())
Normalement, nous devrions écrire bien plus que cinq Example ! En effet, nous
voudrons généralement créer un script de conversion qui lit les éléments dans leur
format actuel (par exemple, des chiers CSV), crée un protobuf Example pour
chacun d’eux, les sérialise et les enregistre dans plusieurs chiersTFRecord, idéa-
lement en les mélangeant au passage. Puisque cela demande un peu de travail,
demandez-vous si c’est absolument nécessaire (il est possible que votre pipeline fonc-
tionne parfaitement avec des chiers CSV).
Essayons à présent de charger ce chier TFRecord qui contient plusieurs objets
Example sérialisés.
def parse(serialized_example):
return tf.io.parse_single_example(serialized_example, feature_description)
dataset = tf.data.TFRecordDataset(["my_contacts.tfrecord"]).map(parse)
for parsed_example in dataset:
print(parsed_example)
Les caractéristiques de taille xe sont analysées comme des tenseurs normaux,
tandis que les caractéristiques de taille variable sont analysées comme des ten-
seurs creux. La conversion d’un tenseur creux en un tenseur dense se fait avec
tf.sparse.to_dense(), mais, dans ce cas, il est plus simple d’accéder directe-
ment à ses valeurs:
>>> tf.sparse.to_dense(parsed_example["emails"], default_value=b"")
<tf.Tensor: [...] dtype=string, numpy=array([b'[email protected]', b'[email protected]'], [...])>
>>> parsed_example["emails"].values
<tf.Tensor: [...] dtype=string, numpy=array([b'[email protected]', b'[email protected]'], [...])>
dataset = tf.data.TFRecordDataset(["my_contacts.tfrecord"])
dataset = dataset.batch(2).map(parse)
for parsed_examples in dataset:
print(parsed_examples) # deux exemples à la fois
Puisque vous savez à présent comment stocker, charger, analyser et prétraiter les
données à l’aide de l’API tf.data, des chiers TFrecord et des protobufs, il est mainte-
nant temps de s’intéresser aux couches de prétraitement de Keras.
norm_layer,
tf.keras.layers.Dense(1)
])
model.compile(loss="mse",
optimizer=tf.keras.optimizers.SGD(learning_rate=2e-3))
norm_layer.adapt(X_train) # calcul de la moyenne et de la variance
# de chaque variable
model.fit(X_train, y_train, validation_data=(X_valid, y_valid), epochs=5)
Couche(s)
de prétraitement
Nouvelles
données (brutes)
Maintenant nous pouvons entraîner un modèle sur les données normalisées, sans
couche Normalization cette fois :
model = tf.keras.models.Sequential([tf.keras.layers.Dense(1)])
model.compile(loss="mse",
optimizer=tf.keras.optimizers.SGD(learning_rate=2e-3))
model.fit(X_train_scaled, y_train, epochs=5,
validation_data=(X_valid_scaled, y_valid))
Couche(s)
de prétraitement Époques
d’entraînement Déploiement
Données Production
Réseau
d’entraînement Données de neurones
(brutes) prétraitées
Nouvelles
données (brutes)
Maintenant nous avons le meilleur des deux mondes : l’entraînement est rapide
parce que nous ne prétraitons les données qu’une seule fois avant le début de l’en-
traînement, et le modèle nal peut prétraiter ses entrées à la volée sans aucun risque
d’incohérence dans le prétraitement.
De plus, les couches de prétraitement de Keras s’accordent bien avec l’API tf.data.
Il est possible, par exemple, de transmettre un tf.data.Dataset à la méthode
adapt() de la couche de prétraitement. Il est aussi possible d’appliquer une
couche de prétraitement Keras à un tf.data.Dataset en utilisant sa méthode
5.3 Couches de prétraitement de Keras 219
Enn, si jamais vous avez besoin de plus de fonctionnalités que n’en proposent
les couches de prétraitement de Keras, vous pouvez toujours écrire votre propre
couche Keras, comme nous l’avons vu au chapitre 4. Si par exemple la couche
Normalization n’existait pas, vous pourriez obtenir un résultat similaire en uti-
lisant la couche personnalisée suivante :
import numpy as np
class MyNormalization(tf.keras.layers.Layer):
def adapt(self, X):
self.mean_ = np.mean(X, axis=0, keepdims=True)
self.std_ = np.std(X, axis=0, keepdims=True)
Voyons maintenant une autre couche de prétraitement de Keras pour les variables
numériques (dites quantitatives) : la couche Discretization.
Dans cet exemple, nous avons fourni les limites souhaitées pour chacune des moda-
lités. Vous pouvez aussi indiquer le nombre de modalités souhaitées, puis appelez la
méthode adapt() de la couche pour lui laisser déterminer les bornes appropriées
en se basant sur les centiles. Ainsi, si nous choisissons num_bins=3, alors les fron-
tières entre modalités seront situées juste avant les valeurs correspondant au 33e et au
66ecentile, soit ici les valeurs 10 et 37) :
>>> discretize_layer = tf.keras.layers.Discretization(num_bins=3)
>>> discretize_layer.adapt(age)
220 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
Si vous tentez d’encoder plusieurs variables qualitatives en même temps (ce qui n’a
de sens que si elles ont toutes les mêmes modalités), la classe CategoryEncoding
effectuera un encodage multi-hot par défaut : le tenseur de sortie contiendra un 1
pour chaque modalité présente dans une quelconque des variables d’entrée. À titre
d’exemple :
>>> two_age_categories = np.array([[1, 0], [2, 2], [2, 0]])
>>> onehot_layer(two_age_categories)
<tf.Tensor: shape=(3, 3), dtype=float32, numpy=
array([[1., 1., 0.],
[0., 0., 1.],
[1., 0., 1.]], dtype=float32)>
[0., 1., 0., 1., 0., 0.]. Vous pouvez obtenir le même résultat en modi-
ant légèrement les indices des modalités pour éviter tout doublon. Par exemple:
>>> onehot_layer = tf.keras.layers.CategoryEncoding(num_tokens=3 + 3)
>>> onehot_layer(two_age_categories + [0, 3]) # ajoute 3 à la seconde variable
<tf.Tensor: shape=(3, 6), dtype=float32, numpy=
array([[0., 1., 0., 1., 0., 0.],
[0., 0., 1., 0., 0., 1.],
[0., 0., 1., 1., 0., 0.]], dtype=float32)>
Dans cette sortie, les trois premières colonnes correspondent à la première variable,
et les trois suivantes correspondent à la seconde variable. Ceci permet au modèle
de distinguer les deux variables. Cependant ceci accroît également le nombre de
variables transmises au modèle et augmente par conséquent le nombre de paramètres
du modèle. Il est difcile de savoir par avance ce qui fonctionnera le mieux, entre un
encodage multi-hot est un encodage one-hot variable par variable : cela dépend de la
tâche et il vous faudra parfois tester les deux options.
Maintenant, vous pouvez appliquer à des variables qualitatives à modalités entières
un encodage one-hot ou multi-hot, mais qu’en est-il des variables qualitatives conte-
nant du texte? Pour cela, vous pouvez utiliser la couche StringLookup.
Nous commençons par créer une couche StringLookup, puis nous l’adaptons
aux données : celle-ci découvre qu’il y a trois modalités distinctes. Puis nous uti-
lisons la couche pour encoder quelques villes. Par défaut, elles sont encodées sous
forme d’entiers. Les modalités inconnues ont été associées à 0, c’est le cas ici pour
"Montreal". Les modalités connues sont numérotées à partir de 1, de la plus fré-
quente à la moins fréquente.
De façon commode, si vous spéciez output_mode="one_hot" lors de la
création de la couche StringLookup, la sortie comportera un vecteur one-hot par
modalité, au lieu d’un entier :
>>> str_lookup_layer = tf.keras.layers.StringLookup(output_mode="one_hot")
>>> str_lookup_layer.adapt(cities)
>>> str_lookup_layer([["Paris"], ["Auckland"], ["Auckland"], ["Montreal"]])
<tf.Tensor: shape=(4, 4), dtype=float32, numpy=
array([[0., 1., 0., 0.],
[0., 0., 0., 1.],
[0., 0., 0., 1.],
[1., 0., 0., 0.]], dtype=float32)>
222 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
L’avantage de cette couche, c’est qu’elle n’a besoin d’aucune adaptation, ce qui
peut parfois être utile, en particulier lorsque le jeu de données est trop volumineux
pour tenir en mémoire. Cependant, cette fois encore nous obtenons une collision
de hachage : « Tokyo » et « Montreal » sont associées au même identiant, ce qui ne
permet pas au modèle de les distinguer. Par conséquent, il est en général préférable
de s’en tenir à la couche StringLookup.
Voyons maintenant une autre façon d’encoder des modalités: les plongements
entraînables.
Plongements de mots
Les plongements seront généralement des représentations utiles pour la tâche en
cours. Mais, assez souvent, ces mêmes plongements pourront également être réuti-
lisés avec succès dans d’autres tâches. L’exemple le plus répandu est celui des plonge-
ments de mots (c’est-à-dire, des plongements de mots individuels) : lorsqu’on travaille
sur un problème de traitement automatique du langage naturel, il est souvent préfé-
rable de réutiliser des plongements de mots préentraînés plutôt que d’entraîner nos
propres plongements.
L’idée d’employer des vecteurs pour représenter des mots remonte aux années
1960 et de nombreuses techniques élaborées ont été mises en œuvre pour générer
des vecteurs utiles, y compris à l’aide de réseaux de neurones. Mais l’approche a
réellement décollé en 2013, lorsque Tomáš Mikolov et d’autres chercheurs chez
Google ont publié un article 124 décrivant une technique d’apprentissage des plonge-
ments de mots avec des réseaux de neurones qui surpassait largement les tentatives
précédentes. Elle leur a permis d’apprendre des plongements sur un corpus de
texte très large : ils ont entraîné un réseau de neurones pour qu’il prédise les mots
proches de n’importe quel mot donné et ont obtenu des plongements de mots stu-
péfiants. Par exemple, les synonymes ont des plongements très proches et des mots
liés sémantiquement, comme France, Espagne et Italie, finissent par être regroupés.
Toutefois, ce n’est pas qu’une question de proximité. Les plongements de mots sont
également organisés selon des axes significatifs dans l’espace des plongements. Voici
un exemple bien connu : si nous calculons Roi – Homme + Femme (en ajoutant et
en soustrayant les vecteurs des plongements de ces mots), le résultat sera très proche
du plongement du mot Reine (voir la figure 5.7). Autrement dit, les plongements
de mots permettent d’encoder le concept de genre ! De façon comparable, le calcul
de Madrid – Espagne + France donne un résultat proche de Paris, ce qui semble
montrer que la notion de capitale a également été encodée dans les plongements.
Homme
Roi
Femme
Très
proche
Reine
Figure 5.7 – Pour des mots similaires, les plongements de mots ont tendance à se
rapprocher, et certains axes semblent encoder des concepts significatifs
124. Tomáš Mikolov et al., « Distributed Representations of Words and Phrases and Their Compositio-
nality », Proceedings of the 26th International Conference on Neural Information Processing Systems 2 (2013),
3111-3119 : https://homl.info/word2vec.
5.3 Couches de prétraitement de Keras 225
Keras propose une couche Embedding construite autour d’une matrice de plon-
gement: cette matrice comporte une ligne par modalité en entrée, et une colonne
par composante de plongement. Par défaut, elle est initialisée aléatoirement. Pour
convertir un identiant de modalité en un plongement, la couche Embedding
recherche et renvoie la ligne correspondant à cette modalité. Et c’est tout ! Initialisons
par exemple une couche Embedding à 5 lignes pour effectuer des plongements 2D
et utilisons-la pour encoder quelques modalités :
>>> tf.random.set_seed(42)
>>> embedding_layer = tf.keras.layers.Embedding(input_dim=5, output_dim=2)
>>> embedding_layer(np.array([2, 4, 2]))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[-0.04663396, 0.01846724],
[-0.02736737, -0.02768031],
[-0.04663396, 0.01846724]], dtype=float32)>
Comme vous pouvez le voir, la modalité 2 est encodée (deux fois) sous forme
d’un vecteur 2D [-0.04663396, 0.01846724], tandis que la modalité 4 est
encodée sous la forme [-0.02736737, -0.02768031]. Étant donné que la
couche n’est pas encore entraînée, ces encodages sont des valeurs choisies au hasard.
Une couche Embedding étant initialisée aléatoirement, cela n’a donc pas
de sens de l’utiliser en dehors d’un modèle en tant que couche de prétrai-
tement indépendante, à moins de l’initialiser avec des poids préentraînés.
125. Malvina Nissim et al., « Fair is Better than Sensational: Man is to Doctor as Woman is to Doctor »
(2019) : https://homl.info/fairembeds.
226 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
...
>>> lookup_and_embed(np.array([["<1H OCEAN"], ["ISLAND"], ["<1H OCEAN"]]))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[-0.01896119, 0.02223358],
[ 0.02401174, 0.03724445],
[-0.01896119, 0.02223358]], dtype=float32)>
En combinant tout cela, nous pouvons créer un modèle Keras capable de traiter
une variable textuelle en même temps que des variables quantitatives et d’apprendre
un plongement pour chaque modalité (casiers OOV compris):
X_train_num, X_train_cat, y_train = [...] # charger le jeu d’entraînement
X_valid_num, X_valid_cat, y_valid = [...] # et le jeu de validation
Maintenant que nous avons vu comment encoder des variables qualitatives, il est
temps de nous intéresser au prétraitement de texte.
homonymes, elle ne sait pas indiquer à votre modèle qu’il existe une relation entre
les mots « evolution » et « evolutionary », etc. Si vous utilisez un encodage multi-hot
du nombre d’occurrences, ou TF-IDF, alors l’ordre des mots est perdu. Quelles sont
donc les autres options ?
Une solution consiste à utiliser la bibliothèque TensorFlow Text (https://tensorow.
org/text), qui propose des fonctionnalités de prétraitement de texte plus avancées
que celles de la couche TextVectorization. On y trouve par exemple plusieurs
utilitaires de découpage (ou tokenizers) permettant de découper du texte en entités
plus petites que des mots, ce qui permet au modèle de détecter plus aisément que
«evolution » et « evolutionary » ont quelque chose en commun (nous reparlerons de
ce découpage en petites entités, ou tokens, au chapitre8).
Une autre solution consiste à utiliser des composants de modèle linguistique
préentraînés. C’est ce que nous allons voir maintenant.
126. TensorFlow Hub n’est pas directement intégré dans TensorFlow, mais si vous travaillez sous Colab
ou si vous avez suivi les instructions d’installation (sous https://homl.info/install), alors il sera déjà installé.
230 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
caractères en entrée et encode chacune d’elles sous forme d’un seul vecteur (de
dimension50 dans ce cas). En interne, il analyse la chaîne (la découpant en mots
au niveau des caractères d’espacement) et effectue un plongement de chaque mot
en utilisant une matrice de plongement entraînée au préalable sur un corpus très
volumineux : le corpus Google News 7B (sept milliards de mots !). Puis il calcule
la moyenne de tous les plongements de mots et le résultat constitue le plongement
de la phrase127.
Il vous suft d’inclure cette couche hub_layer dans votre modèle, et vous
voilà prêt. Notez cependant que ce modèle linguistique particulier a été entraîné sur
des textes anglais, mais de nombreuses autres langues sont proposées, ainsi que des
modèles multilingues.
Enn et surtout, l’excellente bibliothèque open source Transformers de Hugging
Face (https://huggingface.co/docs/transformers) simplie l’inclusion de composants
de modèles linguistiques très puissants dans vos propres modèles. Vous pouvez
parcourir Hugging Face Hub (https://huggingface.co/models), choisir le modèle qui
vous convient et utiliser les exemples de code fournis en tant que point de départ.
Cette bibliothèque ne proposait au départ que des modèles linguistiques, mais elle
s’est développée depuis et comporte maintenant des modèles traitant des images et
autres.
Nous reviendrons sur le traitement du langage naturel au chapitre8. Intéressons-
nous maintenant aux couches Keras de prétraitement d’images.
127. Pour être précis, le plongement de la phrase est égal à la moyenne des plongements de mots multipliée
par la racine carrée du nombre de mots dans la phrase. Ceci compense le fait que la norme de la moyenne
de n vecteurs aléatoires a tendance à diminuer lorsque n croît.
5.4 Le projet TensorFlow Datasets 231
Pillow, qui devrait déjà être installée si vous utilisez Colab ou si vous avez suivi les
instructions d’installation) :
from sklearn.datasets import load_sample_images
images = load_sample_images()["images"]
crop_image_layer = tf.keras.layers.CenterCrop(height=100, width=100)
cropped_images = crop_image_layer(images)
datasets = tfds.load(name="mnist")
mnist_train, mnist_test = datasets["train"], datasets["test"]
Vous pouvez alors appliquer la transformation de votre choix (le plus souvent
mélange, mise en lot et lecture anticipée) et passer à l’entraînement du modèle:
for batch in mnist_train.shuffle(10_000, seed=42).batch(32).prefetch(1):
images = batch["image"]
labels = batch["label"]
[...] # utiliser les images et étiquettes
Mais il est plus simple de laisser la fonction load() s’en charger à notre place, en
indiquant as_supervised=True (évidemment, cela ne vaut que pour les jeux de
données étiquetés).
Enn, TFDS offre un moyen commode de partager les données en utilisant l’argu-
ment split. Si vous souhaitez par exemple utiliser 90 % du jeu d’entraînement pour
l’entraînement, les 10 % restants pour la validation et l’ensemble du jeu de test pour
le test, il vous suft de spécier split=["train[:90%]", "train[90%:]",
"test"]. La fonction load() renverra les trois jeux de données. Voici un exemple
complet qui charge et partage le jeu de données MNIST à l’aide de TFDS, puis utilise
ces trois jeux pour entraîner et évaluer un modèle Keras simple:
train_set, valid_set, test_set = tfds.load(
name="mnist",
split=["train[:90%]", "train[90%:]", "test"],
as_supervised=True
)
train_set = train_set.shuffle(buffer_size=10_000, seed=42)
train_set = train_set.batch(32).prefetch(1)
5.5 Exercices 233
valid_set = valid_set.batch(32).cache()
test_set = test_set.batch(32).cache()
tf.random.set_seed(42)
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam",
metrics=["accuracy"])
history = model.fit(train_set, validation_data=valid_set, epochs=5)
test_loss, test_accuracy = model.evaluate(test_set)
Félicitations, vous êtes venus à bout de ce contenu plutôt technique ! Cela vous
paraît peut-être un peu loin de la beauté abstraite des réseaux de neurones. Il n’en
reste pas moins que le Deep Learning implique souvent de grandes quantités de
données et qu’il est indispensable de savoir comment les charger, les analyser et les
prétraiter efcacement. Dans le chapitre suivant, nous étudierons les réseaux de neu-
rones convolutifs, qui font partie des architectures parfaitement adaptées au traite-
ment d’images et à de nombreuses autres applications.
5.5 EXERCICES
1. Pourquoi voudriez-vous utiliser l’API tf.data ?
2. Quels sont les avantages du découpage d’un jeu de donnés volumineux
en plusieurs chiers?
3. Pendant l’entraînement, comment pouvez-vous savoir que le pipeline
d’entrée constitue le goulot d’étranglement ? Comment pouvez-vous
corriger le problème ?
4. Un fichier TFRecord peut-il contenir n’importe quelles données
binaires ou uniquement des données sérialisées au format
Protobuf ?
5. Pourquoi vous embêter à convertir toutes vos données au format
Example ? Pourquoi ne pas utiliser votre propre format Protobuf ?
6. Avec les chiers TFRecord, quand faut-il activer la compression ?
Pourquoi ne pas le faire systématiquement?
7. Les données peuvent être prétraitées directement pendant l’écriture
des chiers de données, au sein du pipeline tf.data ou dans des
couches de prétraitement à l’intérieur du modèle. Donnez quelques
avantages et inconvénients de chaque approche.
8. Nommez quelques techniques classiques d’encodage des variables
qualitatives à modalités entières. Qu’en est-il du texte ?
9. Chargez le jeu de données Fashion MNIST (présenté au chapitre2).
Séparez-le en un jeu d’entraînement, un jeu de validation et un jeu
de test. Mélangez le jeu d’entraînement. Enregistrez les datasets
dans plusieurs chiers TFRecord. Chaque enregistrement doit être
un protobuf Example sérialisé avec deux caractéristiques : l’image
sérialisée (utilisez pour cela tf.io.serialize_tensor()) et
234 Chapitre 5. Chargement et prétraitement de données avec TensorFlow
128. Pour les grandes images, vous pouvez utiliser à la place tf.io.encode_jpeg(). Vous économise-
rez ainsi beaucoup d’espace, mais vous perdrez un peu en qualité d’image.
6
Vision par ordinateur
et réseaux de neurones
convolutifs
traitement automatique du langage naturel (TALN) ; nous nous focaliserons sur les
applications visuelles.
Dans ce chapitre, nous présenterons l’origine des CNN, les éléments qui les consti-
tuent et leur implémentation avec Keras. Puis nous décrirons certaines des meilleures
architectures de CNN, ainsi que d’autres tâches visuelles, notamment la détection
d’objets (la classication de plusieurs objets dans une image et leur délimitation par
des rectangles d’encadrement) et la segmentation sémantique (la classication de
chaque pixel en fonction de la classe de l’objet auquel il appartient).
Figure 6.1 – Les neurones biologiques du cortex visuel répondent à des motifs spécifiques
dans de petites régions du champ visuel appelées champs récepteurs; au fur et à mesure
que le signal visuel traverse les modules cérébraux consécutifs, les neurones répondent
à des motifs plus complexes dans des champs récepteurs plus larges
129. David H. Hubel, « Single Unit Activity in Striate Cortex of Unrestrained Cats », The Journal of
Physiology, 147 (1959), 226-238 : https://homl.info/71.
130. David H. Hubel et Torsten N. Wiesel, « Receptive Fields of Single Neurons in the Cat’s Striate
Cortex », The Journal of Physiology, 148 (1959), 574-591 : https://homl.info/72.
131. David H. Hubel et Torsten N. Wiesel, « Receptive Fields and Functional Architecture of Monkey
Striate Cortex », The Journal of Physiology, 195 (1968), 215-243 : https://homl.info/73.
6.2 Couches de convolution 237
Ils ont également montré que certains neurones réagissent uniquement aux images de
lignes horizontales, tandis que d’autres réagissent uniquement aux lignes ayant d’autres
orientations (deux neurones peuvent avoir le même champ récepteur mais réagir à
des orientations de lignes différentes). Ils ont remarqué que certains neurones ont des
champs récepteurs plus larges et qu’ils réagissent à des motifs plus complexes, corres-
pondant à des combinaisons de motifs de plus bas niveau. Ces observations ont conduit
à l’idée que les neurones de plus haut niveau se fondent sur la sortie des neurones voi-
sins de plus bas niveau (sur la gure6.1, chaque neurone est connecté uniquement aux
neurones voisins de la couche précédente). Cette architecture puissante est capable de
détecter toutes sortes de motifs complexes dans n’importe quelle zone du champ visuel.
Ces études du cortex visuel sont à l’origine du neocognitron132 , présenté en 1980,
qui a progressivement évolué vers ce que nous appelons aujourd’hui réseau de neu-
rones convolutif. Un événement majeur a été la publication d’un article133 en 1998
par Yann LeCun et al., dans lequel les auteurs ont présenté la célèbre architecture
LeNet-5, désormais largement utilisée par les banques pour reconnaître les chiffres
manuscrits sur les chèques. Nous connaissons déjà quelques éléments de cette archi-
tecture, comme les couches intégralement connectées (FC, fully connected) et les
fonctions d’activation sigmoïdes, mais elle en ajoute deux nouveaux : les couches de
convolution et les couches de pooling. Étudions-les.
132. Kunihiko Fukushima, « Neocognitron: A Self-Organizing Neural Network Model for a Mechanism of
Pattern Recognition Unaffected by Shift in Position», Biological Cybernetics, 36 (1980), 193-202 : https://
homl.info/74.
133. Yann LeCun et al., « Gradient-Based Learning Applied to Document Recognition », Proceedings of the
IEEE, 86, n°11 (1998), 2278-2324 : https://homl.info/75.
134. Une convolution est une opération mathématique qui fait glisser une fonction par-dessus une autre
et mesure l’intégrale de leur multiplication ponctuelle. Ses liens avec les transformées de Fourier et de
Laplace sont étroits. Elle est souvent employée dans le traitement du signal. Les couches de convolution
utilisent en réalité des corrélations croisées, qui sont très similaires aux convolutions (pour de plus amples
informations, voir https://homl.info/76).
238 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
chaque pixel de l’image d’entrée (comme c’était le cas dans les chapitres précédents),
mais uniquement aux pixels dans leurs champs récepteurs (voir la gure6.2). À leur
tour, les neurones de la deuxième couche de convolution sont chacun connectés uni-
quement aux neurones situés à l’intérieur d’un petit rectangle de la première couche.
Cette architecture permet au réseau de se focaliser sur des caractéristiques de bas
niveau dans la première couche cachée, puis de les assembler en caractéristiques de
plus haut niveau dans la couche cachée suivante, etc. Cette structure hiérarchique
est récurrente dans les images réelles et c’est l’une des raisons des bons résultats des
CNN pour la reconnaissance d’images.
Couche
de convolution 2
Couche
de convolution 1
Couche d’entrée
Figure 6.2 – Couches d’un CNN avec des champs récepteurs locaux rectangulaires
Jusque-là, tous les réseaux de neurones multicouches que nous avons exa-
minés avaient des couches constituées d’une longue suite de neurones et
nous devions donc aplatir les images d’entrée en une dimension avant de
les transmettre au réseau. Dans un CNN, chaque couche étant représentée
par un tableau à deux dimensions, il est plus facile de faire correspondre les
neurones aux entrées associées.
Un neurone situé en ligne i et colonne j d’une couche donnée est connecté aux
sorties des neurones de la couche précédente situés aux lignes i à i + fh − 1 et aux
colonnes j à j + fw − 1, où fh et fw sont la hauteur et la largeur du champ récepteur (voir
la gure6.3). Pour qu’une couche ait les mêmes hauteur et largeur que la couche
précédente, on ajoute des zéros autour des entrées (marge), comme l’illustre la gure.
Cette opération se nomme remplissage par zéros (zero padding), mais nous dirons plutôt
ajout d’une marge de zéros pour éviter toute confusion (car seule la marge est remplie
par des zéros).
6.2 Couches de convolution 239
fh = 3
fw = 3 Marge de zéros
Il est également possible de connecter une large couche d’entrée à une couche
plus petite en espaçant les champs récepteurs (voir la gure 6.4). Cela permet de
réduire énormément la complexité des calculs du modèle. Le décalage horizontal ou
vertical entre deux champs récepteurs consécutifs est appelé pas (stride en anglais).
Sur la gure, une couche d’entrée 5×7 (plus la marge) est connectée à une couche
3×4, en utilisant des champs récepteurs 3×3 et un pas de 2 (dans cet exemple, le pas
est identique dans les deux directions, mais ce n’est pas obligatoire). Un neurone
situé en ligne i et colonne j dans la couche supérieure est connecté aux sorties des
neurones de la couche précédente situés aux lignes i × sh à i × sh + f h − 1 et colonnes
j× sw à j × s w + fw − 1, où shetsw sont les pas vertical et horizontal.
Sh = 2
Sw = 2
6.2.1 Filtres
Les poids d’un neurone peuvent être représentés sous la forme d’une petite image
de la taille du champ récepteur. Par exemple, la gure6.5 montre deux ensembles
de poids possibles, appelés ltres (ou noyaux de convolution, voire tout simplement
240 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
noyaux). Le premier est un carré noir avec une ligne blanche verticale au milieu (il
s’agit d’une matrice 7×7 remplie de 0, à l’exception de la colonne centrale, pleine
de 1). Les neurones qui utilisent ces poids ignoreront tout ce qui se trouve dans
leur champ récepteur, à l’exception de la ligne verticale centrale (puisque toutes les
entrées seront multipliées par zéro, excepté celles sur la ligne verticale centrale). Le
second ltre est un carré noir traversé en son milieu par une ligne blanche horizon-
tale. Les neurones qui utilisent ces poids ignoreront tout ce qui se trouve dans leur
champ récepteur, hormis cette ligne horizontale centrale.
Carte Carte
de caractéristiques 1 de caractéristiques 2
Entrée
Figure 6.5 – Application de deux filtres différents pour obtenir deux cartes de caractéristiques
Si tous les neurones d’une couche utilisent le même ltre à ligne verticale (ainsi
que le même terme constant) et si nous fournissons en entrée du réseau l’image illus-
trée à la gure6.5 (image du bas), la couche sortira l’image située en partie supérieure
gauche. Les lignes blanches verticales sont mises en valeur, tandis que le reste devient
ou. De façon comparable, l’image supérieure droite est obtenue lorsque tous les neu-
rones utilisent le ltre à ligne horizontale. Les lignes blanches horizontales sont amé-
liorées, tandis que le reste est ou. Une couche remplie de neurones qui utilisent
le même ltre nous donne ainsi une carte de caractéristiques (feature map) qui fait
ressortir les zones d’une image qui se rapprochent le plus du ltre. Évidemment, nous
n’avons pas à dénir des ltres manuellement. À la place, au cours de l’entraînement,
la couche de convolution apprend automatiquement les ltres qui seront les plus
utiles à sa tâche, et les couches supérieures apprennent à les combiner dans des motifs
plus complexes.
6.2 Couches de convolution 241
Carte 2
Filtres
Couche de convolution 1
Carte 1
Carte 2
Couche d’entrée
Canaux
Rouge
Vert
Bleu
Équation 6.1 – Calcul de la sortie d’un neurone dans une couche de convolution
fh – 1 f w–1 fn –1
i =i sh + u
z i, j,k = b k + xi , j ,k w u,v,k ,k avec
u=0 v=0 k =0
j =j sw + v
Dans cette équation :
• z i,j,k correspond à la sortie du neurone situé en ligne i et colonne j dans la carte
de caractéristiques k de la couche de convolution (couchel).
• Comme nous l’avons expliqué, sh et sw sont les pas vertical et horizontal, fh et fw
sont la hauteur et la largeur du champ récepteur, et fn’ est le nombre de cartes
de caractéristiques dans la couche précédente (couche l − 1).
• xi’,j’,k’ correspond à la sortie du neurone situé dans la couche l − 1, ligne i’,
colonne j’, carte de caractéristiques k’ (ou canal k’ si la couche précédente est
la couche d’entrée).
• bk est le terme constant de la carte de caractéristiques k (dans la couche l).
Il peut être vu comme un réglage de la luminosité globale de la carte de
caractéristiques k.
• w u,v,k’,k correspond au poids de la connexion entre tout neurone de la carte
de caractéristiques k de la couche l et son entrée située ligne u, colonne v
(relativement au champ récepteur du neurone) dans la carte de caractéristiquesk’.
Voyons comment créer et utiliser une couche de convolution avec Keras.
6.2 Couches de convolution 243
images = load_sample_images()["images"]
images = tf.keras.layers.CenterCrop(height=70, width=120)(images)
images = tf.keras.layers.Rescaling(scale=1 / 255)(images)
aucune marge par défaut, ce qui signie que nous perdons 6 pixels horizontalement
et 6 pixels verticalement (c’est-à-dire trois pixels sur chaque bord).
Ces deux options de remplissage sont illustrées sur la gure6.7. Pour simplier,
seule la dimension horizontale est présentée ici, mais bien sûr la même logique s’ap-
plique pour la dimension verticale.
Si le pas est supérieur à 1 (dans n’importe quelle direction), alors la taille de la sortie
ne sera pas égale à la taille de l’entrée, même si padding="same". Si par exemple
vous dénissez strides=2 (ou, de manière équivalente, strides=(2, 2)),
alors la carte des caractéristiques de sortie sera de format 35×60, soit une réduction
de moitié à la fois verticalement et horizontalement. La gure6.8 montre ce qui se
produit lorsque strides=2, avec les deux options de remplissage.
Marge de zéros
0 0 0 0 0 0
padding="valid", padding="same",
kernel_size=7, strides=1 kernel_size=7, strides=1
Ignoré
Marge de zéros
0 0 0 0 0
padding="valid", padding="same",
kernel_size=7, strides=2 kernel_size=7, strides=2
Figure 6.8 – Lorsque le pas est supérieur à 1, la sortie est beaucoup plus petite,
même avec un remplissage de type "same"
(et le remplissage "valid" ignore certaines entrées).
Le tableau kernels est un tableau 4D, et sa forme est [hauteur noyau, largeur
noyau, canaux entrée, canaux sortie]. Le tableau biases contenant les termes
constants est un tableau 1D, de forme [canaux sortie]. Le nombre de canaux de sortie
est égal au nombre de cartes de caractéristiques de sortie, qui est aussi égal au nombre
de ltres.
246 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
Le plus important, c’est que la hauteur et la largeur des images d’entrée n’ap-
paraissent pas dans la forme du noyau : c’est parce que tous les neurones des cartes
de caractéristiques de sortie partagent les mêmes poids, comme expliqué précédem-
ment. Ceci signie que vous pouvez alimenter cette couche avec des images de toutes
tailles, à condition qu’elles soient au moins aussi larges que les noyaux et qu’elles
aient le bon nombre de canaux (trois dans ce cas).
Enn, vous choisirez en général une fonction d’activation (telle que ReLU) lors
de la création d’une couche Conv2D, et vous spécierez également l’initialiseur de
noyau correspondant (comme l’initialisation de He). Ceci pour la même raison que
pour les couches Dense: une couche de convolution réalise une opération linéaire,
par conséquent si vous empiliez plusieurs couches de convolution sans fonction d’ac-
tivation, ceci serait équivalent à une seule couche de convolution, et ce réseau serait
dans l’incapacité d’apprendre quoi que ce soit de réellement complexe.
Comme vous pouvez le voir, les couches de convolution ont relativement
peu d’hyperparamètres : filters, kernel_size, padding, strides,
activation, kernel_initializer, etc. Comme toujours, vous pouvez
utiliser une validation croisée pour déterminer les bonnes valeurs des hyperpara-
mètres, mais cela prend beaucoup de temps. Nous étudierons les architectures de
réseaux de neurones convolutifs les plus courantes dans la suite de ce chapitre an
de vous donner une idée des valeurs d’hyperparamètres qui fonctionnent le mieux
en pratique.
135. Pour produire des sorties de même taille, une couche intégralement connectée aurait besoin de 200 ×
150 × 100 neurones, chacun connecté à l’ensemble des150×100 × 3 entrées. Elle aurait donc 200 × 150×
100 × (150 × 100 × 3 + 1) ≈ 135 milliards de paramètres!
136. Dans le système international d’unités (SI), 1Mo = 1 000ko = 1 000 × 1 000octets = 1 000 × 1 000 ×8bits.
Et 1 MiB = 1,024 kiB = 1,024 × 1,024 octets. Donc 12 MB ≈ 11,44 MiB.
6.3 Couche de pooling 247
137. Les noyaux de pooling sont simplement des fenêtres glissantes sans état. Ils n’ont pas de poids,
contrairement aux autres noyaux que nous avons présentés jusqu’ici.
248 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
5
Max
1 5
3 2
A B C
Pour créer une couche de pooling moyen (average pooling), il suft de remplacer
MaxPool2D par AveragePooling2D, alias AvgPool2D. Elle opère exactement
comme une couche de pooling maximum, excepté qu’elle calcule la moyenne à la
place du maximum. Les couches de pooling moyen ont connu leur moment de gloire,
mais elles sont aujourd’hui supplantées par les couches de pooling maximum, qui
afchent généralement de meilleures performances. Cela peut sembler surprenant
250 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
car calculer la moyenne conduit à une perte d’informations moindre que calculer
le maximum. Mais le pooling maximum conserve uniquement les caractéristiques
les plus fortes, écartant les moins pertinentes. La couche suivante travaille donc
sur un signal plus propre. Par ailleurs, le pooling maximum offre une invariance de
translation plus importante que le pooling moyen et demande des calculs légère-
ment moins intensifs.
Le pooling maximum et le pooling moyen peuvent également être appliqués sur
la profondeur à la place de la hauteur et de la largeur, mais cette utilisation est plus
rare. Elle permet néanmoins au CNN d’apprendre à devenir invariant à diverses
caractéristiques. Par exemple, il peut apprendre plusieurs ltres, chacun détectant
une rotation différente du même motif (comme les chiffres manuscrits; voir la
gure6.11), et la couche de pooling maximum en profondeur garantit que la sortie
reste la même quelle que soit la rotation. De manière comparable, le CNN pour-
rait apprendre à devenir invariant à d’autres caractéristiques : épaisseur, luminosité,
inclinaison, couleur, etc.
Couche de pooling
maximum
en profondeur
max
Carte de
caractéristiques
Couche Filtres
de convolution appris
Image d’entrée
Keras n’offre pas de couche de pooling maximum en profondeur, mais il n’est pas
très difcile d’implémenter une couche personnalisée pour cela:
class DepthPool(tf.keras.layers.Layer):
def __init__(self, pool_size=2, **kwargs):
super().__init__(**kwargs)
self.pool_size = pool_size
Cette couche réorganise ses entrées pour partager les canaux en groupes (ou pools)
de la taille désirée (pool_size), puis elle utilise tf.reduce_max() pour cal-
culer le maximum de chaque groupe. Cette implémentation suppose que le pas est
égal à la taille du groupe, ce qui est en général ce que vous voulez. Une autre solution
consiste à utiliser l’opération tf.nn.max_pool() de TensorFlow et à l’emballer
dans une couche Lambda pour l’utiliser à l’intérieur d’un modèle Keras, cependant
cette opération n’implémente hélas pas le pooling en profondeur pour les GPU, mais
uniquement pour les CPU.
Dans les architectures modernes, vous rencontrerez souvent un dernier type de
couche de pooling: la couche de pooling moyen global. Elle fonctionne très différem -
ment, en se limitant à calculer la moyenne sur l’intégralité de chaque carte de carac-
téristiques (cela équivaut à une couche de pooling moyen qui utilise un noyau de
pooling ayant les mêmes dimensions spatiales que les entrées). Autrement dit, elle
produit simplement une seule valeur par carte de caractéristiques et par instance.
Bien que cette approche soit extrêmement destructrice (la majorité des informations
présentes dans la carte de caractéristiques est perdue), elle peut se révéler utile juste
avant la couche de sortie, comme nous le verrons plus loin dans ce chapitre. Pour
créer une telle couche, utilisez simplement la classe GlobalAveragePooling2D,
alias GlobalAvgPool2D :
global_avg_pool = tf.keras.layers.GlobalAvgPool2D()
Cela équivaut à la couche Lambda suivante, qui calcule la moyenne sur les
dimensions spatiales (hauteur et largeur):
global_avg_pool = tf.keras.layers.Lambda(
lambda X: tf.reduce_mean(X, axis=[1, 2]))
Si par exemple nous appliquons cette couche aux images d’entrée, nous obtenons
l’intensité moyenne de rouge, vert et bleu pour chaque image :
>>> global_avg_pool(images)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0.64338624, 0.5971759 , 0.5824972 ],
[0.76306933, 0.26011038, 0.10849128]], dtype=float32)>
Puisque vous connaissez à présent tous les blocs qui permettent de construire des
réseaux de neurones convolutifs, voyons comment les assembler.
Une erreur fréquente est d’utiliser des noyaux de convolution trop grands.
Par exemple, au lieu d’utiliser une couche de convolution avec un noyau
5×5, mieux vaut empiler deux couches ayant des noyaux 3×3 : la charge
de calcul et le nombre de paramètres seront bien inférieurs, et les perfor-
mances seront généralement meilleures. L’exception concerne la première
couche de convolution : elle a typiquement un grand noyau (par exemple,
5×5), avec un pas de 2 ou plus. Cela permet de diminuer la dimension spa-
tiale de l’image sans perdre trop d’informations, et, puisque l’image d’en-
trée n’a en général que trois canaux, les calculs ne seront pas trop lourds.
Détaillons ce modèle :
• Nous utilisons la fonction functools.partial()(présentée au chapitre3)
pour dénir DefaultConv2D, qui fonctionne comme Conv2D mais avec
des arguments par défaut différents : un petit noyau de taille3, un remplissage
de type "same", la fonction d’activation ReLU et l’initialiseur de He qui lui
est associé.
• Ensuite, nous créons le modèle Sequential. La première couche est une
DefaultConv2D avec 64 ltres assez larges (7×7). Elle utilise le pas par défaut
de1, car les images d’entrée ne sont pas très grandes. Elle précise également
input_shape=[28, 28, 1], car les images font 28×28 pixels, avec un
seul canal de couleur (elles sont en niveaux de gris). Lorsque vous chargez le
jeu de données Fashion MNIST, vériez que chaque image a bien cette forme :
il vous faudra peut-être utiliser np.reshape() ou np.expanddims()
pour ajouter la dimension des canaux. Sinon, vous pouvez utiliser une couche
Reshape en tant que première couche du modèle.
• Ensuite, nous ajoutons une couche de pooling maximum, qui utilise un pool de
taille2 (la valeur par défaut) et qui divise donc chaque dimension spatiale par
un facteur2.
• Nous répétons ensuite deux fois la même structure : deux couches de convolution
suivies d’une couche de pooling maximum. Pour des images plus grandes, nous
pourrions répéter cette structure un plus grand nombre de fois (ce nombre est
un hyperparamètre ajustable).
• Le nombre de ltres augmente à mesure qu’on se rapproche de la couche de
sortie du CNN (il est initialement de 64, puis de 128, puis de 256) : cette
augmentation a un sens, car le nombre de caractéristiques de bas niveau est
souvent assez bas (par exemple, de petits cercles, des lignes horizontales), mais
il existe de nombreuses manières différentes de les combiner en caractéristiques
de plus haut niveau. La pratique courante consiste à doubler le nombre de
ltres après chaque couche de pooling : puisqu’une couche de pooling divise
chaque dimension spatiale par un facteur 2, nous pouvons doubler le nombre
de cartes de caractéristiques de la couche suivante sans craindre de voir
exploser le nombre de paramètres, l’encombrement mémoire ou la charge de
calcul.
• Vient ensuite le réseau intégralement connecté, constitué de deux couches
cachées denses et d’une couche de sortie dense. Étant donné qu’il s’agit d’une
tâche de classication à 10 classes, la couche de sortie a 10 éléments et utilise
la fonction d’activation softmax. Nous devons aplatir les entrées juste avant la
première couche dense, car cette dernière attend un tableau de caractéristiques
à une dimension pour chaque instance. Pour réduire le surajustement, nous
ajoutons également deux couches d’abandon (dropout), chacune avec un taux
d’abandon de 50 %.
Si vous compilez ce modèle en utilisant "sparse_categorical_
crossentropy" comme perte et que vous l’ajustez au jeu de données Fashion
MNIST, vous devriez obtenir une exactitude de 92 % sur le jeu de test. Sans être
254 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
extraordinaire, le résultat est plutôt bon, en tout cas bien meilleur que celui obtenu
au chapitre2 avec des réseaux denses.
Au l des années, des variantes de cette architecture fondamentale sont apparues,
conduisant à des avancées impressionnantes dans le domaine. Pour bien mesurer
cette progression, il suft de prendre en référence le taux d’erreur obtenu dans dif-
férentes compétitions, comme le dé ILSVRC ImageNet (https://image-net.org).
Dans cette compétition, le taux d’erreur top-5 pour la classication d’images est passé
de plus de 26% à un peu moins de 2,3% en six ans. Le taux d’erreur top-5 correspond
au nombre d’images test pour lesquelles les cinq premières prédictions du système
ne comprenaient pas la bonne réponse. Les images sont plutôt grandes (hautes de
256pixels) et il existe 1 000 classes, certaines d’entre elles étant réellement subtiles
(par exemple, distinguer 120 races de chiens). Pour mieux comprendre le fonction-
nement des CNN et la façon dont la recherche dans ce domaine progresse, nous
allons regarder l’évolution des propositions gagnantes.
Nous examinerons tout d’abord l’architecture LeNet-5 classique (1998), puis trois
des gagnants du dé ILSVRC : AlexNet (2012), GoogLeNet (2014), ResNet (2015)
et SENet (2017). Au passage, nous étudierons quelques architectures supplémen-
taires telles que Xception, ResNeXt, DenseNet, MobileNet, CSPNet et EfcientNet.
6.5.1 LeNet-5
L’architecture LeNet-5138 est probablement la plus connue des architectures de CNN.
Nous l’avons indiqué précédemment, elle a été créée en 1998 par Yann LeCun et elle
est largement utilisée pour la reconnaissance des chiffres écrits à la main (MNIST).
Elle est constituée de plusieurs couches, énumérées au tableau6.1.
Taille
Couche Type Cartes Taille Pas Activation
de noyau
Intégralement
Out – 10 – – RBF
connectée
Intégralement
F6 – 84 – – tanh
connectée
C5 Convolution 120 1×1 5×5 1 tanh
S4 Pooling moyen 16 5×5 2×2 2 tanh
C3 Convolution 16 10×10 5×5 1 tanh
S2 Pooling moyen 6 14×14 2×2 2 tanh
C1 Convolution 6 28×28 5×5 1 tanh
In Entrée 1 32×32 – – –
Comme vous pouvez le voir, ceci ressemble beaucoup à notre modèle Fashion
MNIST : une pile de couches de convolution et de couches de pooling, suivie par
138. Yann LeCun et al., « Gradient-Based Learning Applied to Document Recognition », Proceedings of the
IEEE, 86, n°11 (1998), 2278-2324 : https://homl.info/lenet5.
6.5 Architectures de CNN 255
un réseau dense. La principale différence avec les CNN de classication plus récents
réside probablement dans les fonctions d’activation : de nos jours, on utiliserait
plutôt ReLU que tanh, et softmax au lieu de RBF. Il existe quelques autres différences
mineures sans grande importance, qui sont néanmoins décrites (en anglais) dans le
notebook de ce chapitre, sur https://homl.info/colab3.
Le site web de Yann LeCun (http://yann.lecun.com/exdb/lenet/index.html) propose
des démonstrations de la classication des chiffres avec LeNet-5.
6.5.2 AlexNet
En 2012, l’architecture de CNN AlexNet139 a remporté le dé ILSVRC avec une
bonne longueur d’avance. Elle est arrivée à un taux d’erreur top-5 de 17%, tandis
que le deuxième n’a obtenu que 26% ! Elle a été développée par Alex Krizhevsky
(d’où son nom), Ilya Sutskever et Geoffrey Hinton. Elle ressemble énormément à
LeNet-5, en étant plus large et profonde, et a été la première à empiler des couches
de convolution directement les unes au-dessus des autres, sans intercaler des couches
de pooling. Le tableau6.2 résume cette architecture.
Taille
Nom Type Cartes Taille Pas Remplissage Activation
de noyau
Out Intégralement
– 1 000 – – – Softmax
connectée
F10 Intégralement
– 4 096 – – – ReLU
connectée
F9 Intégralement
– 4 096 – – – ReLU
connectée
S8 Pooling
256 6×6 3×3 2 valid –
maximum
C7 Convolution 256 13×13 3×3 1 same ReLU
C6 Convolution 384 13×13 3×3 1 same ReLU
C5 Convolution 384 13×13 3×3 1 same ReLU
S4 Pooling
256 13×13 3×3 2 valid –
maximum
C3 Convolution 256 27×27 5×5 1 same ReLU
S2 Pooling
96 27×27 3×3 2 valid –
maximum
C1 Convolution 96 55×55 11×11 4 same ReLU
In Entrée 3 (RVB) 227×227 – – – –
139. Alex Krizhevsky et al., « ImageNet Classication with Deep Convolutional Neural Networks », Procee-
dings of the 25th International Conference on Neural Information Processing Systems, 1 (2012), 1097-1105 :
https://homl.info/80.
256 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
r
j haut
– j haut = min i + ,fn – 1
2
b i = ai k + a j2 avec
j = j bas r
j bas = max 0, i –
2
L’augmentation des données est utile également lorsque le jeu de données est désé-
quilibré : elle vous permet de générer davantage d’exemples correspondant aux
classes peu fréquentes. C’est ce qu’on appelle la technique de suréchantillonnage des
observations minoritaires par synthèse (synthetic minority oversampling technique, ou
SMOTE).
ZF Net140 , une variante d’AlexNet développée par Matthew Zeiler et Rob Fergus,
a remporté le dé ILSVRC en 2013. Il s’agit essentiellement d’une version d’AlexNet
avec quelques hyperparamètres ajustés (nombre de cartes de caractéristiques, taille
du noyau, pas, etc.).
6.5.3 GoogLeNet
L’architecture GoogLeNet (https://homl.info/81) a été proposée par Christian
Szegedy et al. de Google Research141 . Elle a gagné le dé ILSVRC en 2014, en
repoussant le taux d’erreur top-5 sous les 7%. Cette excellente performance vient
principalement du fait que le réseau était beaucoup plus profond que les CNN pré-
cédents (comme vous le verrez à la gure 6.15). Cela a été possible grâce à la pré-
sence de sous-réseaux nommés modules Inception142, qui permettent à GoogLeNet
d’utiliser les paramètres beaucoup plus efcacement que dans les architectures pré-
cédentes. GoogLeNet possède en réalité dix fois moins de paramètres qu’AlexNet
(environ 6millions à la place de 60millions).
La gure6.14 présente l’architecture d’un module Inception. La notation «3×3
+ 1(S) » signie que la couche utilise un noyau 3×3, un pas de 1 et le remplissage
"same". Le signal d’entrée est tout d’abord transmis à quatre couches différentes en
parallèle. Toutes les couches de convolution utilisent la fonction d’activation ReLU.
Les couches de convolution du haut utilisent des tailles de noyau différentes (1×1,
3×3 et 5×5), ce qui leur permet de détecter des motifs à des échelles différentes.
Puisque chaque couche utilise également un pas de 1 et un remplissage "same"
(même la couche de pooling maximum), leurs sorties conservent la hauteur et la
largeur de leurs entrées. Cela permet de concaténer toutes les entrées dans le sens
de la profondeur au sein de la dernière couche, appelée couche de concaténation en
profondeur (elle empile les cartes de caractéristiques des quatre couches de convo-
lution sur lesquelles elle repose). Cette couche de concaténation peut être implé-
mentée à l’aide de la couche Concatenate de Keras, en choisissant la valeur par
défaut axis=-1.
140. Matthew D. Zeiler et Rob Fergus, « Visualizing and Understanding Convolutional Networks »,
Proceedings of the European Conference on Computer Vision (2014), 818-833 : https://homl.info/zfnet.
141. Christian Szegedy et al., « Going Deeper with Convolutions », Proceedings of the IEEE Conference on
Computer Vision and Pattern Recognition (2015), 1-9.
142. Dans le lm Inception, de 2001, les personnages vont de plus en plus profond dans les multiples strates
des rêves, d’où le nom de ces modules.
6.5 Architectures de CNN 259
Concaténation
en profondeur
Pourquoi les modules Inception ont-ils des couches de convolution avec des noyaux
1×1? À quoi peuvent-elles servir puisque, en n’examinant qu’un seul pixel à la fois, elles
ne peuvent capturer aucune caractéristique? En réalité, ces couches ont trois objectifs :
• Même si elles ne peuvent pas capturer des motifs spatiaux, elles peuvent
capturer des motifs sur la profondeur (c’est-à-dire entre les canaux).
• Elles sont congurées pour produire moins de cartes de caractéristiques que leurs
entrées et servent donc de couches de rétrécissement qui réduisent la dimension.
Cela permet d’abaisser la charge de calcul et le nombre de paramètres, et donc
d’accélérer l’entraînement et d’améliorer la généralisation.
• Chaque couple de couches de convolution ([1×1, 3×3] et [1×1, 5×5]) agit comme
une seule couche de convolution puissante, capable de capturer des motifs
plus complexes. Une couche de convolution équivaut à balayer l’image avec
une couche dense (en chaque point elle examine uniquement un petit champ
récepteur), alors qu’un couple de couches de convolution équivaut à balayer
l’image avec un réseau de neurones à deux couches.
En résumé, le module Inception peut être vu comme une couche de convolution
survitaminée, capable de sortir des cartes de caractéristiques qui identient des motifs
complexes à différentes échelles.
Étudions à présent l’architecture du CNN GoogLeNet (voir la gure 6.15). Le
nombre de cartes de caractéristiques produites par chaque couche de convolution et
chaque couche de pooling est indiqué avant la taille du noyau. L’architecture est si
profonde que nous avons dû la représenter sur trois colonnes, mais GoogLeNet est en
réalité une grande pile, comprenant neuf modules Inception (les rectangles accompa-
gnés d’une toupie). Les six valeurs données dans les modules Inception représentent
le nombre de cartes de caractéristiques produites par chaque couche de convolution
260 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
dans le module (dans le même ordre qu’à la gure6.14). Toutes les couches de convo-
lution sont suivies de la fonction d’activation ReLU.
Softmax
1 000 unités
Pooling maximum
intégralement
connectées
Normalisation Abandon
de réponse locale
Convolution Pooling moyen global
Convolution
Convolution
Entrée
Module Inception
6.5.4 VGGNet
L’autre naliste du dé ILSVRC 2014 était VGGNet143 , développé par Karen
Simonyan et Andrew Zisserman du laboratoire de recherche VGG (Visual Geometry
Group) à l’université d’Oxford. Leur architecture classique très simple se fondait sur
une répétition de blocs de deux ou trois couches de convolution et d’une couche
de pooling (pour atteindre un total de 16 ou 19 couches de convolution selon les
variantes), ainsi qu’un réseau nal dense de deux couches cachées et la couche de
sortie. Elle utilisait de petits ltres 3×3, mais en grand nombre.
6.5.5 ResNet
En 2015, Kaiming He et son équipe ont gagné le dé ILSVRC en proposant un réseau
résiduel 144 (residual network, ou ResNet) dont le taux d’erreur top-5 stupéant a été
inférieur à 3,6%. La variante gagnante se fondait sur un CNN extrêmement profond
143. Karen Simonyan et Andrew Zisserman, « Very Deep Convolutional Networks for Large-Scale Image
Recognition » (2014) : https://homl.info/83.
144. Kaiming He et al., « Deep Residual Learning for Image Recognition » (2015) : https://homl.info/82.
262 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
h(x)
h(x) +
Connexion de saut
Couche 2 Couche 2
Couche 1 Couche 1
Entrée Entrée
Lorsque vous initialisez un réseau de neurones ordinaire, ses poids sont proches de
zéro et il produit donc des valeurs proches de zéro. Si vous ajoutez une connexion de
saut, le réseau obtenu produit une copie de ses entrées. Autrement dit, il modélise
initialement la fonction identité. Si la fonction cible est assez proche de la fonction
identité (ce qui est souvent le cas), l’entraînement s’en trouve considérablement
accéléré.
Par ailleurs, en ajoutant de nombreuses connexions de saut, le réseau peut com-
mencer à faire des progrès même si l’apprentissage de plusieurs couches n’a pas encore
débuté (voir la gure6.17). Grâce aux connexions de saut, un signal peut aisément
cheminer au travers de l’intégralité du réseau. Le réseau résiduel profond peut être vu
comme une pile d’unités résiduelles, chacune étant un petit réseau de neurones avec
une connexion de saut.
Étudions à présent l’architecture ResNet (voir la gure6.18). Elle est en fait éton-
namment simple. Elle commence et se termine exactement comme GoogLeNet (à
l’exception d’une couche d’abandon absente), et, au milieu, ce n’est qu’une pile très
profonde d’unités résiduelles. Chaque unité résiduelle est constituée de deux couches
de convolution (sans couche de pooling!), avec une normalisation par lots (BN,
6.5 Architectures de CNN 263
batch normalization) et une activation ReLU, utilisant des noyaux 3×3 et conservant
les dimensions spatiales (pas de 1, remplissage "same").
Unités
résiduelles
Couche bloquant
la rétropropagation
Softmax
1 000 unités
intégralement
connectées
Pooling moyen
global 1024 Saut
Normalisation
Profond ! par lots
Pooling maximum
Unité résiduelle
Entrée
les entrées sont passées au travers d’une couche de convolution 1×1 avec un pas de2
et le nombre approprié de cartes de caractéristiques en sortie (voir la gure6.19).
6.5.6 Xception
Xception146 est une variante très intéressante de l’architecture GoogLeNet. Proposée
en 2016 par François Chollet (le créateur de Keras), elle a surpassé largement
Inception-v3 sur une grande tâche de traitement d’images (350millions d’images et
17 000 classes). À l’instar d’Inception-v4, elle fusionne les idées de GoogLeNet et de
ResNet, mais elle remplace les modules Inception par une couche spéciale appelée
145. Pour décrire un réseau de neurones, il est fréquent de compter uniquement les couches qui possèdent
des paramètres.
146. François Chollet, « Xception: Deep Learning with Depthwise Separable Convolutions » (2016) :
https://homl.info/xception.
6.5 Architectures de CNN 265
Puisque les couches de convolution séparable ne disposent que d’un ltre spatial
par canal d’entrée, vous devez éviter de les placer après des couches qui possèdent
trop peu de canaux, comme la couche d’entrée (d’accord c’est le cas sur la gure6.20,
mais elle n’est là qu’à titre d’illustration). Voilà pourquoi l’architecture Xception
commence par deux couches de convolution ordinaires et le reste de l’architecture
n’utilise que des convolutions séparables (34 en tout), plus quelques couches de
pooling maximum et les couches nales habituelles (une couche de pooling moyen
global et une couche de sortie dense).
Vous pourriez vous demander pourquoi Xception est considérée comme une
variante de GoogLeNet alors qu’elle ne comprend aucun module Inception. Nous
147. Ce nom peut parfois être ambigu, car les convolutions séparables spatialement sont souvent elles aussi
appelées « convolutions séparables ».
266 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
6.5.7 SENet
En 2017, SENet (squeeze-and-excitation network)148 a été l’architecture gagnante du
dé ILSVRC. Elle étend les architectures existantes, comme les réseaux Inception et
les ResNet, en améliorant leurs performances. Son approche lui a permis de gagner la
compétition avec un taux d’erreur top-5 stupéant de 2,25% ! Les versions étendues
des réseaux Inception et des ResNet sont nommées, respectivement, SE-Inception et
SE-ResNet. Les améliorations proviennent d’un petit réseau de neurones, appelé bloc
SE, ajouté par un SENet à chaque module Inception ou unité résiduelle de l’archtec-
ture d’origine, comme l’illustre la gure6.21.
Bloc SE Bloc SE
148. Jie Hu et al., « Squeeze-and-Excitation Networks », Proceedings of the IEEE Conference on Computer
Vision and Pattern Recognition (2018), 7132-7141 : https://homl.info/senet.
6.5 Architectures de CNN 267
Bloc SE
Cartes de caractéristiques Cartes de caractéristiques
recalibrées
Un bloc SE n’est constitué que de trois couches : une couche de pooling moyen
global, une couche cachée dense avec une fonction d’activation ReLU et une couche
de sortie dense avec la fonction d’activation sigmoïde (voir la gure6.23).
Sigmoïde
Dense
ReLU
Dense
149. Saining Xie et al., « Aggregated Residual Transformations for Deep Neural Networks », arXiv preprint
arXiv:1611.05431 (2016) : https://homl.info/resnext.
150. Gao Huang et al., « Densely Connected Convolutional Networks », arXiv preprint arXiv:1608.06993
(2016) : https://homl.info/densenet.
151. Andrew G. Howard et al., « MobileNets: Efcient Convolutional Neural Networks for Mobile Vision
Applications », arXiv preprint arxiv:1704.04861 (2017) : https://homl.info/mobilenet.
6.5 Architectures de CNN 269
152. Chien-Yao Wang et al., « CSPNet: A New Backbone That Can Enhance Learning Capability of
CNN », arXiv preprint arXiv:1911.11929 (2019) : https://homl.info/cspnet.
153. Mingxing Tan et Quoc V. Le, « EfcientNet: Rethinking Model Scaling for Convolutional Neural
Networks », arXiv preprint arXiv:1905.11946 (2019) : https://homl.info/efcientnet.
270 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
modèle (si par exemple vous voulez le déployer sur un téléphone portable) ? La vitesse
d’inférence sur un CPU, ou sur un GPU ?
Le tableau6.3 donne la liste des meilleurs modèles préentraînés actuellement dis-
ponibles dans Keras, triés par taille de modèle (nous verrons plus loin dans ce cha-
pitre comment les utiliser). Vous trouverez la liste complète sous https://keras.io/
api/applications. Pour chaque modèle, le tableau indique le nom de la classe Keras
(du package tf.keras.applications) à utiliser, la taille du modèle en Mo, les
exactitudes top-1 et top-5 obtenues en validation sur le jeu de données ImageNet,
le nombre de paramètres (en millions) et le temps d’inférence sur CPU et GPU en
ms, en utilisant des lots de 32 images sur un matériel relativement puissant154. Dans
chaque colonne, la meilleure valeur est en gras. Comme vous pouvez le voir, les grands
modèles obtiennent généralement de meilleurs résultats, mais pas systématiquement :
EfcientNetB2, par exemple, fait mieux qu’InceptionV3 tant par la taille que l’exac-
titude. Je n’ai conservé InceptionV3 dans la liste que parce qu’il est près de deux fois
plus rapide qu’EfcientNetB2 sur un CPU. De la même façon, InceptionResNetV2
est rapide sur un CPU, tandis que ResNet50V2 et ResNet101V2 sont rapides comme
l’éclair sur un GPU.
J’espère que vous avez apprécié ce plongeon dans les principales architectures de
CNN ! Voyons comment implémenter l’une d’entre elles avec Keras.
154. Un processeur AMD EPYC à 92 cœurs avec IBPB, 1,7To de RAM et un GPU Nvidia Tesla A100.
6.6 Implémenter un CNN ResNet-34 avec Keras 271
class ResidualUnit(tf.keras.layers.Layer):
def __init__(self, filters, strides=1, activation="relu", **kwargs):
super().__init__(**kwargs)
self.activation = tf.keras.activations.get(activation)
self.main_layers = [
DefaultConv2D(filters, strides=strides),
tf.keras.layers.BatchNormalization(),
self.activation,
DefaultConv2D(filters),
tf.keras.layers.BatchNormalization()
]
self.skip_layers = []
if strides > 1:
self.skip_layers = [
DefaultConv2D(filters, kernel_size=1, strides=strides),
tf.keras.layers.BatchNormalization()
]
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation("relu"),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding="same"),
])
prev_filters = 64
for filters in [64] * 3 + [128] * 4 + [256] * 6 + [512] * 3:
strides = 1 if filters == prev_filters else 2
model.add(ResidualUnit(filters, strides=strides))
prev_filters = filters
model.add(tf.keras.layers.GlobalAvgPool2D())
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(10, activation="softmax"))
Dans ce cas, la seule petite difculté réside dans la boucle qui ajoute les couches
ResidualUnit au modèle : les trois premières unités résiduelles comprennent
64 ltres, les quatre suivantes en ont 128, etc. À chaque itération, nous xons le
pas à1 si le nombre de ltres est identique à celui de l’unité résiduelle précédente,
sinon à 2. Puis nous ajoutons la couche ResidualUnit et terminons en actuali-
sant prev_filters.
Il est surprenant de voir qu’à peine 40 lignes de code permettent de construire le
modèle qui a gagné le dé ILSVRC en 2015! Cela montre parfaitement l’élégance
du modèle ResNet et la capacité d’expression de l’API de Keras. L’implémentation
des autres architectures de CNN est un peu plus longue, mais guère plus complexe.
De plus, Keras fournit déjà plusieurs de ces architectures en standard, alors pourquoi
ne pas les employer directement?
C’est tout ! Elle crée un modèle ResNet-50 et télécharge des poids préentraînés sur
le jeu de données ImageNet. Pour l’exploiter, vous devez commencer par vérier que
les images ont la taille appropriée. Puisqu’un modèle ResNet-50 attend des images
de 224×224 pixels (pour d’autres modèles, la taille peut être différente, par exemple
299×299), nous utilisons la couche Resizing de Keras (présentée au chapitre5) pour
redimensionner deux images (après les avoir rognées aux proportions ciblées) :
images = load_sample_images()["images"]
images_resized = tf.keras.layers.Resizing(height=224, width=224,
crop_to_aspect_ratio=True)(images)
Les modèles préentraînés supposent que les images sont prétraitées de manière
spécique. Dans certains cas, ils peuvent attendre que les entrées soient redimen-
sionnées entre 0 et 1, ou entre –1 et 1, etc. Chaque modèle fournit une fonction
6.7 Utiliser des modèles préentraînés de Keras 273
Nous pouvons à présent utiliser le modèle préentraîné pour effectuer des prédic-
tions :
>>> Y_proba = model.predict(inputs)
>>> Y_proba.shape
(2, 1000)
Comme d’habitude, la sortie Y_proba est une matrice constituée d’une ligne par
image et d’une colonne par classe (dans ce cas, nous avons 1 000 classes). Pour afcher
les K premières prédictions, accompagnées du nom de la classe et de la probabilité
estimée pour chaque classe prédite, utilisez la fonction decode_predictions().
Pour chaque image, elle retourne un tableau contenant les K premières prédictions,
où chaque prédiction est représentée sous forme d’un tableau qui contient l’identi-
ant de la classe155, son nom et le score de conance correspondant :
top_K = tf.keras.applications.resnet50.decode_predictions(Y_proba, top=3)
for image_index in range(len(images)):
print(f"Image #{image_index}")
for class_id, name, y_proba in top_K[image_index]:
print(f" {class_id} – {name:12s} {y_proba:.2%}")
Les classes correctes sont palace et dahlia, donc le modèle a trouvé le bon
résultat pour la première image mais s’est trompé pour la seconde. Toutefois, c’est
parce que dahlia ne fait pas partie des 1 000 classes ImageNet. Compte tenu de
ce fait, vase est une supposition raisonnable (la eur est peut-être dans un vase ?)
et daisy (marguerite) n’est pas un mauvais choix non plus, étant donné que les
dahlias et les marguerites font tous deux partie de la famille des Composées.
Vous le constatez, un modèle préentraîné permet de créer très facilement un
classicateur d’images plutôt bon. Comme vous l’avez vu dans le tableau 6.3, il
existe de nombreux autres modèles pour le traitement d’images dans tf.keras.
applications, des modèles légers et rapides jusqu’aux modèles de très grande
taille et précision.
155. Dans le jeu de données ImageNet, chaque image est associée à un mot du jeu de données WordNet
(https://wordnet.princeton.edu) : l’identiant de classe est simplement un identiant WordNet ID.
274 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
Est-il possible d’utiliser un classicateur d’images pour des classes d’images qui ne
font pas partie d’ImageNet? Oui, en protant des modèles préentraînés pour mettre
en place un transfert d’apprentissage.
Les trois jeux de données contiennent des images individuelles. Nous devons
les partager en lots, mais nous devons d’abord vérier qu’elles ont toutes la
mêmetaille, faute de quoi le partage en lots échouerait. Nous pouvons utiliser une
coucheResizing pour cela. Nous devons aussi appeler la fonction tf.keras.
applications.xception.preprocess_input()pour prétraiter les images
d’une manière appropriée au modèle Xception. Enn, nous allons aussi mélanger le
jeu d’entraînement et utiliser la lecture anticipée :
batch_size = 32
preprocess = tf.keras.Sequential([
tf.keras.layers.Resizing(height=224, width=224, crop_to_aspect_ratio=True),
tf.keras.layers.Lambda(tf.keras.applications.xception.preprocess_input)
])
train_set = train_set_raw.map(lambda X, y: (preprocess(X), y))
train_set = train_set.shuffle(1000, seed=42).batch(batch_size).prefetch(1)
valid_set = valid_set_raw.map(lambda X, y: (preprocess(X), y))
valid_set = valid_set.batch(batch_size)
test_set = test_set_raw.map(lambda X, y: (preprocess(X), y)).batch(batch_size)
6.8 Modèles préentraînés pour un transfert d’apprentissage 275
Maintenant chaque lot contient 32 images, chacune de 224×224 pixels, avec des
valeurs de pixels allant de –1 à 1. Parfait !
Le jeu de données n’étant pas très grand, un peu d’augmentation de données sera
certainement utile. Créons un modèle d’augmentation de données qui sera intégré
dans notre modèle nal. Durant l’entraînement, il va aléatoirement retourner les
images horizontalement, les faire pivoter un petit peu et modier les contrastes:
data_augmentation = tf.keras.Sequential([
tf.keras.layers.RandomFlip(mode="horizontal", seed=42),
tf.keras.layers.RandomRotation(factor=0.05, seed=42),
tf.keras.layers.RandomContrast(factor=0.2, seed=42)
])
La classe tf.keras.preprocessing.image.ImageDataGenerator
simplifie le chargement des images à partir du disque et leur augmentation
de diverses manières: vous pouvez décaler chaque image, la faire pivoter,
la redimensionner, la retourner horizontalement ou verticalement, l’incli-
ner, ou lui appliquer toute autre fonction de transformation souhaitée. Elle
est très pratique pour des projets simples. Toutefois, un pipeline tf.data n’est
pas beaucoup plus compliqué à construire et il est en général plus rapide.
De plus, si vous avez un GPU et si vous incluez les couches de prétraite-
ment et d’augmentation de données dans votre modèle, elles bénéficieront
de l’accélération apportée par le GPU durant l’entraînement.
Chargeons ensuite un modèle Xception, préentraîné sur ImageNet. Nous excluons
la partie supérieure du réseau en indiquant include_top=False ; c’est-à-dire la
couche de pooling moyen global et la couche de sortie dense. Nous ajoutons ensuite
notre propre couche de pooling moyen global (en l’alimentant avec la sortie du modèle
de base), suivie d’une couche de sortie dense avec une unité par classe et la fonction
d’activation softmax. Pour nir, nous emballons le tout dans un Model Keras :
base_model = tf.keras.applications.xception.Xception(weights="imagenet",
include_top=False)
avg = tf.keras.layers.GlobalAveragePooling2D()(base_model.output)
output = tf.keras.layers.Dense(n_classes, activation="softmax")(avg)
model = tf.keras.Model(inputs=base_model.input, outputs=output)
Nous l’avons expliqué au chapitre3, il est préférable de ger les poids des couches
préentraînées, tout au moins au début de l’entraînement :
for layer in base_model.layers:
layer.trainable = False
N’oubliez pas de compiler le modèle lorsque vous gez ou libérez des couches.
Veillez également à utiliser un taux d’apprentissage beaucoup plus petit pour éviter
d’endommager les poids préentraînés:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.9)
model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,
metrics=["accuracy"])
history = model.fit(train_set, validation_data=valid_set, epochs=10)
model = tf.keras.Model(inputs=base_model.input,
outputs=[class_output, loc_output])
model.compile(loss=["sparse_categorical_crossentropy", "mse"],
loss_weights=[0.8, 0.2], # dépend de ce qui importe
optimizer=optimizer, metrics=["accuracy"])
Mais nous avons un problème : le jeu de données des eurs ne contient aucun rec-
tangle d’encadrement autour des eurs. Nous devons donc les ajouter nous-mêmes.
Puisque obtenir les étiquettes est souvent l’une des parties les plus difciles et les plus
coûteuses d’un projet de Machine Learning, mieux vaut passer du temps à rechercher
des outils appropriés. Pour annoter des images avec des rectangles d’encadrement,
vous pouvez utiliser un outil open source d’étiquetage des images comme VGG Image
Annotator, LabelImg, OpenLabeler ou ImgLab, ou bien un outil commercial, comme
LabelBox ou Supervisely.
Vous pouvez également envisager d’avoir recours à des plateformes de crowdsour-
cing, comme Amazon Mechanical Turk si le nombre d’images à annoter est très grand.
Cependant, la mise en place d’une telle plateforme demande beaucoup de travail,
avec la préparation du formulaire à envoyer aux intervenants, leur supervision et la
vérication de la qualité des rectangles d’encadrement produits. Assurez-vous que
cela en vaut la peine. Adriana Kovashka et al. ont rédigé un article156 très pratique
sur le crowdsourcing dans la vision par ordinateur. Je vous conseille de le lire, même si
vous ne prévoyez pas d’employer cette solution.
S’il n’y a que quelques centaines à quelques milliers d’images à étiqueter et si vous
ne prévoyez pas de le faire fréquemment, il peut être préférable de le faire vous-même :
avec les bons outils, cela ne vous prendra que quelques jours et vous y gagnerez une
meilleure connaissance de votre jeu de données et de la tâche à effectuer.
Supposons que vous ayez pu obtenir les rectangles d’encadrement de chaque
image dans le jeu de données des eurs (pour le moment, nous supposerons que
chaque image possède un seul rectangle). Vous devez alors créer un dataset dont les
éléments seront des lots d’images prétraitées accompagnées de leur étiquette de classe
et de leur rectangle d’encadrement. Chaque élément doit être un n-uplet de la forme
(image, (étiquette_de_classe, rectangle_d’encadrement)).
Ensuite, il ne vous reste plus qu’à entraîner votre modèle!
156. Adriana Kovashka et al., « Crowdsourcing in Computer Vision », Foundations and Trends in Computer
Graphics and Vision, 10, n°3 (2014), 177-243 : https://homl.info/crowd.
278 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
à prédire les rectangles d’encadrement. Dans ce cas, la métrique la plus répandue est
l’indice de Jaccard (également appelé intersection over union, IoU) : l’aire de l’inter-
section entre le rectangle d’encadrement prédit et le rectangle d’encadrement cible,
divisée par l’aire de leur union (voir la gure6.24). Dans tf.keras, cette métrique est
implémentée par la classe tf.keras.metrics.MeanIoU.
Étiquette
Intersection
Prédiction
Union
Classier et localiser un seul objet est intéressant, mais que pouvons-nous faire si
les images contiennent plusieurs objets (comme c’est souvent le cas dans le jeu de
données des eurs) ?
Au lieu d’un score de présence d’objet, une classe « aucun objet » était
parfois ajoutée, mais en général cela ne fonctionnait pas aussi bien : il est
préférable en effet de répondre séparément aux questions « L’objet est-il
présent ? » et « Quel type d’objet est-ce ? ».
6.10 Détection d’objets 279
L’approche à base de CNN glissant est présentée sur la gure6.25. Dans notre
exemple, l’image est découpée en une grille 5×7, le CNN (le rectangle noir au
contour épais) est déplacé sur l’ensemble des zones 3×3 et il effectue des prédictions
à chaque étape.
Figure 6.25 – Détection de plusieurs objets par déplacement d’un CNN sur l’image
Sur cette gure, le CNN a déjà effectué des prédictions pour trois de ces
régions3×3 :
• Dans la région 3×3 en haut à gauche (dont le centre est situé à l’intersection
de la deuxième ligne et de la deuxième colonne), le CNN a détecté la rose de
gauche. Remarquez que le rectangle d’encadrement prédit dépasse les limites
decette région 3×3. Cela convient bien : même si le CNN n’a pas pu voir le bas
de la rose, il a pu faire une supposition raisonnable quant à son emplacement. Il
a également prédit des probabilités de classe, en accordant une haute probabilité
à la classe « rose ». Enn, le score de présence d’objet est relativement élevé,
étant donné que le centre du rectangle d’encadrement se trouve à l’intérieur de
la cellule centrale de la grille (sur cette gure, le score de présence d’objet est
matérialisé par l’épaisseur du trait du rectangle d’encadrement).
• Pour la région 3×3 suivante, centrée sur une cellule plus à droite dans la grille, il
n’a détecté aucune eur centrée dans cette région et a par conséquent fourni un
score de présence d’objet très bas. Par conséquent, le rectangle d’encadrement
et les probabilités de classe peuvent être ignorés sans problème. Vous pouvez
vérier que le rectangle d’encadrement prédit n’était de toute façon pas bon.
• Enn, dans la région 3×3 suivante, centrée sur une cellule plus à droite, le
CNN a détecté la rose du dessus, bien qu’imparfaitement : cette rose n’est pas
bien centrée dans la région et le score de présence d’objet n’est pas très bon.
280 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
157. Jonathan Long et al., « Fully Convolutional Networks for Semantic Segmentation », Proceedings of
the IEEE Conference on Computer Vision and Pattern Recognition (2015), 3431-3440 : https://homl.info/fcn.
6.10 Détection d’objets 281
158. À une exception près : une couche de convolution qui utilise le remplissage "valid" manifestera
son mécontentement si la taille de l’entrée est inférieure à celle du noyau.
282 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
simplement copier les poids des couches denses vers les couches de convolution !
Une autre solution pourrait être de convertir le CNN en FCN avant l’entraînement.
Supposons à présent que la dernière couche de convolution située avant la
couche de sortie (également appelée couche de rétrécissement) produise des cartes
de caractéristiques 7×7 lorsque le réseau reçoit une image 224×224 (voir la partie
gauche de la gure 6.26). Si nous passons au FCN une image 448×448 (voir la
partie droite de la gure6.26), la couche de rétrécissement génère alors des cartes
de caractéristiques 14×14159 . Puisque la couche de sortie dense a été remplacée par
une couche de convolution qui utilise dix ltres de taille 7×7, avec un remplissage
"valid" et un pas de 1, la sortie sera constituée de dix cartes de caractéristiques,
chacune de taille 8×8 (puisque 14 − 7 + 1 = 8). Autrement dit, le FCN va traiter
l’intégralité de l’image une seule fois et produira une grille 8×8 dont chaque cel -
lule contient dix valeurs (cinq probabilités de classe, un score de présence d’objet
et quatre coordonnées de rectangle d’encadrement). Cela revient exactement à
prendre le CNN d’origine et à le déplacer sur l’image en utilisant huit pas par ligne
et huit par colonne.
Cartes de Cartes de
caractéristiques caractéristiques
1×1 8×8
Cartes de Cartes de
caractéristiques caractéristiques
7×7 14×14
Image Image
224×224 448×448
Figure 6.26 – Le même réseau entièrement convolutif traitant (à gauche) une petite image
et (à droite) une grande image
159. Cela suppose que le réseau utilise uniquement un remplissage "same". Un remplissage "valid"
réduirait évidemment la taille des cartes de caractéristiques. De plus, 448 se divise parfaitement par 2 à
plusieurs reprises, jusqu’à atteindre 7, sans aucune erreur d’arrondi. Si une couche utilise un pas différent
de 1 ou 2, des erreurs d’arrondi peuvent se produire et les cartes de caractéristiques peuvent nir par être
plus petites.
6.10 Détection d’objets 283
6.10.2 YOLO
YOLO (you only look once) est un algorithme de détection d’objet extrêmement
rapide et précis proposé par Joseph Redmon et al. dans un article160 publié en 2015.
Il est si rapide qu’il peut travailler en temps réel sur une vidéo, comme vous pouvez
le voir sur la démonstration mise en place par Redmon (https://homl.info/yolodemo).
L’architecture de YOLO est proche de celle que nous venons de présenter, mais avec
quelques différences importantes :
• Pour chaque cellule de la grille, YOLO ne prend en compte que les objets dont
le rectangle d’encadrement est centré dans cette cellule. Les coordonnées du
rectangle d’encadrement sont relatives à cette cellule, (0,0) correspondant au
coin supérieur gauche de la cellule et (1,1) correspond au coin inférieur droit.
Toutefois, le rectangle d’encadrement peut s’étendre bien au-delà de la cellule,
tant en hauteur qu’en largeur.
• YOLO produit deux rectangles d’encadrement pour chaque cellule de la grille
(au lieu d’un seul), ce qui permet au modèle de gérer les cas où deux objets sont
si proches l’un de l’autre que les centres de leurs rectangles d’encadrement se
trouvent dans la même cellule. Chaque rectangle d’encadrement possède son
propre score de présence d’objet.
• YOLO génère également vingt probabilités de classe par cellule, car il a été entraîné
sur le jeu de données PASCAL VOC, qui dénit vingt classes. Il fournit en sortie
une carte des probabilités de classe. Cette carte comporte une probabilité par classe
et par cellule de la grille, et non par rectangle d’encadrement. Cependant, il est
possible d’estimer les probabilités de classe pour chaque rectangle d’encadrement
durant le post-traitement, en mesurant la correspondance entre chaque rectangle
d’encadrement et chaque classe dans la carte des probabilités de classe. Prenons
l’exemple d’une image représentant une personne debout devant une auto. Il
y aura deux rectangles d’encadrement : un grand rectangle horizontal pour la
voiture, et un plus petit rectangle vertical pour la personne. Il se peut que les
centres de ces deux rectangles d’encadrement se trouvent dans la même cellule
de la grille. Comment pouvons-nous déterminer alors à quelle classe associer
chacun des rectangles d’encadrement ? Eh bien, la carte des probabilités de classe
contiendra une large région où la classe « auto » sera dominante, et à l’intérieur
il y aura une région plus petite où la classe « personne » sera dominante. Avec
un peu de chance, le rectangle d’encadrement de l’auto correspondra à peu
160. Joseph Redmon et al., « You Only Look Once: Unied, Real-Time Object Detection », Proceedings
of the IEEE Conference on Computer Vision and Pattern Recognition (2016), 779-788: https://homl.info/yolo.
284 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
161. Voir le §3.3.4 du chapitre3 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3eédi-
tion, 2023).
162. Ce phénomène est visible en partie supérieure gauche de la gure3.6 du chapitre3, op. cit.
6.10 Détection d’objets 285
163. Vous trouverez YOLOv3, YOLOv4 et leurs variantes de taille réduite dans le projet TensorFlow Mo-
dels sous https://homl.info/yolotf.
164. Wei Liu et al., « SSD: Single Shot Multibox Detector », Proceedings of the 14th European Conference on
Computer Vision, 1 (2016), 21-37 : https://homl.info/ssd.
165. Shaoqing Ren et al., « Faster R-CNN: Towards Real-Time Object Detection with Region Propo-
sal Networks », Proceedings of the 28th International Conference on Neural Information Processing Systems, 1
(2015), 91-99 : https://homl.info/fasterrcnn.
166. Mingxing Tan et al., « EfcientDet: Scalable and Efcient Object Detection », arXiv preprint
arXiv:1911.09070 (2019) : https://homl.info/efcientdet.
286 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
sur chaque image, mais ils doivent aussi être suivis dans le temps. Examinons donc
maintenant le suivi d’objets.
Jusqu’ici nous avons localisé des objets en les entourant de rectangles. Cela suft
souvent, mais parfois nous avons besoin de repérer des objets avec beaucoup plus de
précision, par exemple pour supprimer un arrière-plan derrière une personne pendant
une visioconférence. Voyons comment descendre au niveau du pixel.
167. Nicolai Wojke et al., « Simple Online and Realtime Tracking with a Deep Association Metric », arXiv
preprint arXiv:1703.07402 (2017): https://homl.info/deepsort.
6.12 Segmentation sémantique 287
Ciel
Bâtiments
Voitures
Personne
Vélos
tt oir
o Route
Tr
168. Ce type de couche est parfois appelé couche de déconvolution, mais elle n’effectue aucunement ce que
les mathématiciens nomment déconvolution. Ce terme doit donc être évité.
288 Chapitre 6. Vision par ordinateur et réseaux de neurones convolutifs
Lacouche de convolution transposée peut être initialisée an d’effectuer une opéra-
tion proche de l’interpolation linéaire, mais, puisqu’il s’agit d’une couche entraînable,
elle apprendra à mieux travailler pendant l’entraînement. Dans tf.keras, vous pouvez
utiliser la couche Conv2DTranspose.
Sortie 5×7
Pas = 2
Taille du noyau = 3
Entrée 2×3
Connexion de saut
Carte de
caractéristiques
Sous-échantillonnage Suréchantillonnage
Figure 6.29 – Des couches de saut récupèrent une certaine résolution spatiale
à partir de couches inférieures
6.13 EXERCICES
1. Dans le contexte de la classication d’images, quels sont les avantages
d’un réseau de neurones convolutif (ou CNN) par rapport à un réseau
de neurones profond (ou DNN) intégralement connecté?
2. Prenons un CNN constitué de trois couches de convolution,
chacune avec des noyaux 33, un pas de 2 et un remplissage "same".
La couche inférieure produit 100 cartes de caractéristiques, la
couche intermédiaire, 200, et la couche supérieure, 400. L’entrée est
constituée d’images RVB de 200×300 pixels:
a. Quel est le nombre total de paramètres du CNN ?
b. Si l’on utilise des nombres à virgule ottante sur 32bits, quelle
quantité de RAM minimale faut-il à ce réseau lorsqu’il effectue
une prédiction pour une seule instance ?
c. Qu’en est-il pour l’entraînement d’un mini-lot de cinquante
images ?
3. Si la carte graphique vient à manquer de mémoire pendant
l’entraînement d’un CNN, quelles sont les cinq actions que vous
pourriez effectuer pour tenter de résoudre le problème?
4. Pourquoi voudriez-vous ajouter une couche de pooling maximum
plutôt qu’une couche de convolution avec le même pas?
5. Quand devriez-vous ajouter une couche de normalisation de réponse
locale ?
6. Citez les principales innovations d’AlexNet par rapport à LeNet-5 ?
Quelles sont celles de GoogLeNet, de ResNet, de SENet, de Xception
et d’EfcientNet ?
6.13 Exercices 291
Nous prédisons le futur en permanence, que ce soit en terminant la phrase d’un ami
ou en anticipant l’odeur du café au petit-déjeuner. Dans ce chapitre, nous allons étu-
dier les réseaux de neurones récurrents (en anglais, recurrent neural networks, ou RNN),
une classe de réseaux qui permettent de prédire l’avenir (jusqu’à un certain point).
Ils sont capables d’analyser des séries chronologiques (encore appelées séries tempo-
relles), comme le nombre quotidien de visiteurs de votre site web, la température
heure par heure dans votre ville, votre consommation électrique domestique quo-
tidienne, les trajectoires des véhicules à proximité, etc. Une fois que votre RNN a
appris les motifs gurant dans vos données passées, il peut utiliser cette connaissance
pour prédire le futur, en supposant bien sûr que ce qui a été observé dans le passé se
reproduise à l’avenir.
Plus généralement, ils peuvent travailler sur des séquences de longueur quel-
conque, plutôt que sur des entrées de taille gée comme les réseaux examinés jusqu’à
présent. Par exemple, ils peuvent prendre en entrée des phrases, des documents ou
des échantillons audio, ce qui les rend très utiles pour le traitement automatique du
langage naturel (TALN), comme les systèmes de traduction automatique ou de saisie
vocale.
Dans ce chapitre, nous commencerons par examiner les concepts fondamentaux
des RNN et la manière de les entraîner en utilisant la rétropropagation dans le temps,
puis nous les utiliserons pour effectuer des prévisions sur des séries chronologiques.
Chemin faisant, nous verrons la très populaire famille des modèles ARMA, sou-
vent utilisés pour effectuer des prévisions à partir de séries chronologiques, et nous
294 Chapitre 7. Traitement des séquences avec des RNN et des CNN
les utiliserons comme points de comparaison avec nos propres RNN. Ensuite, nous
explorerons les deux principales difcultés auxquelles font face les RNN:
• l’instabilité des gradients (décrite au chapitre3), qui peut être réduite à l’aide
de diverses techniques, notamment l’abandon récurrent (en anglais, recurrent
dropout) et la normalisation de couche récurrente ;
• une mémoire à court terme (très) limitée, qui peut être étendue en utilisant des
cellules LSTM et GRU.
Les RNN ne sont pas les seuls types de réseaux de neurones capables de traiter
des données séquentielles. Pour les petites séquences, un réseau dense classique peut
faire l’affaire. Pour les séquences très longues, comme des enregistrements audio ou
du texte, les réseaux de neurones convolutifs fonctionnent aussi plutôt bien. Nous
examinerons ces deux approches, et nous terminerons ce chapitre par l’implémenta-
tion d’un WaveNet. Cette architecture de CNN est capable de traiter des séquences
constituées de dizaines de milliers d’étapes temporelles. Allons-y!
Σ 0 Σ Σ Σ Σ
Temps
ŷ
ŷ(0) ŷ(1) ŷ (2)
0
Σ Σ Σ Σ Σ
Figure 7.2 – Une couche de neurones récurrents (à gauche), dépliée dans le temps (à droite)
Chaque neurone récurrent possède deux jeux de poids : un premier pour les entrées,
x(t), et un second pour les sorties de l’étape temporelle précédente, yˆ(t–1). Appelons
ces vecteurs poids wx et wˆy, respectivement. Si l’on considère maintenant la couche
complète, nous pouvons regrouper les vecteurs poids de tous les neurones en deux
matrices poids Wx et Wˆy . Le vecteur de sortie de la couche récurrente complète peut
alors être calculé selon l’équation7.1 (b est le vecteur des termes constants et ϕ (.) est
la fonction d’activation, par exemple ReLU170).
Équation 7.1 – Sortie d’une couche récurrente pour une seule instance
(
yˆ(t ) = φ WxT x (t ) + WˆyTyˆ (t –1) + b )
Comme pour les réseaux de neurones non bouclés, nous pouvons calculer d’un
seul coup la sortie d’une couche pour un mini-lot entier en plaçant toutes les entrées
à l’étape temporelle t dans une matrice d’entrées X(t) (voir l’équation7.2).
ˆ
Wx
(
= φ X(t ) Y )
(t –1) W + b avec W =
Wˆy
170. De nombreux chercheurs préfèrent employer la tangente hyperbolique (tanh) dans les RNN plutôt que
la fonction ReLU, comme l’explique l’article de Vu Pham et al. publié en 2013 et intitulé « Dropout Impro-
ves Recurrent Neural Networks for Handwriting Recognition» (https://homl.info/91). Mais les RNN fondés
sur ReLU sont également employés, comme l’expliquent Quoc V. Le et al. dans leur article « A Simple Way
to Initialize Recurrent Networks of Rectied Linear Units» publié en 2015 (https://homl.info/92).
296 Chapitre 7. Traitement des séquences avec des RNN et des CNN
h
h (0) h (1)
Figure 7.3 – L’état caché d’une cellule et sa sortie peuvent être différents
171. Nal Kalchbrenner et Phil Blunsom, « Recurrent Continuous Translation Models », Proceedings of the
2013 Conference on Empirical Methods in Natural Language Processing (2013), 1700-1709: https://homl.info/
seq2seq.
298 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Sorties ignorées
ŷ (0) ŷ (1) ŷ (2) ŷ (3) ŷ (4) ŷ (0)
(0) ŷ (1) ŷ (2) ŷ (3)
Encodeur Décodeur
ŷ (0) ŷ (1) ŷ (2) ŷ (3) ŷ (0) ŷ (1) ŷ ‘(0) ŷ ‘ (1) ŷ ‘(2)
x x x x x (0) x (1) 00 00 0
rétropropagés qu’à travers Ŷ (2) , Ŷ (3) et Ŷ (4). Par ailleurs, puisque les mêmes paramètres
W et b sont utilisés à chaque étape temporelle, leurs gradients seront ajustés plusieurs
fois durant la rétropropagation. Une fois la passe arrière terminée et les gradients
calculés, la BPTT peut effectuer une étape de descente de gradient pour mettre à jour
les paramètres (de la même façon qu’une rétropropagation classique).
path = Path("datasets/ridership/CTA_-_Ridership_-_Daily_Boarding_Totals.csv")
df = pd.read_csv(path, parse_dates=["service_date"])
df.columns = ["date", "day_type", "bus", "rail", "total"] # noms plus courts
df = df.sort_values("date").set_index("date")
df = df.drop("total", axis=1) # pas besoin du total,
# même chose que bus + rail
df = df.drop_duplicates() # supprimer les mois dupliqués (10-2011 et 07-2014)
172. Les données à jour de la régie des transports de Chicago sont accessibles sur le portail de données de
Chicago (https://homl.info/ridership).
300 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Nous chargeons le chier CSV, raccourcissons les noms de colonnes, trions les
lignes par date, supprimons la colonne « total » qui est redondante ainsi que les lignes
dupliquées. Voyons maintenant à quoi ressemblent les premières lignes:
>>> df.head()
day_type bus rail
date
2001-01-01 U 297192 126455
2001-01-02 W 780827 501952
2001-01-03 W 824923 536432
2001-01-04 W 870021 550011
2001-01-05 W 890426 557917
Le 1erjanvier 2001, 297 192 personnes sont montées à bord d’un bus à Chicago et
126 455 sont montés à bord d’un train. La colonne day_type contient W (weekday)
pour les jours de semaine, A (Saturday) pour les samedis et U (Sunday) pour les
dimanches et fêtes.
Représentons maintenant graphiquement (gure7.6) les trajets en bus ou en train
durant quelques mois de 2019, pour voir à quoi cela ressemble:
import matplotlib.pyplot as plt
800 000
700 000
600 000 bus
500 000 rail
400 000
300 000
200 000
Mar Avr Mai
2019
date
Remarquez que Pandas inclut les deux extrémités de l’intervalle, c’est pourquoi
le graphique présente toutes les données du 1 ermars au 31mai. Il s’agit d’une série
chronologique, c’est-à-dire de données mesurées au cours du temps, habituellement
à intervalles réguliers. Plus précisément, étant donné qu’il y a plusieurs valeurs par
unité de temps, il s’agit d’une série chronologique multivariée. Si nous n’avion pris
en compte que la colonne bus, il s’agirait d’une série chronologique univariée, avec
une seule valeur par unité de temps. La prédiction des valeurs futures (c’est-à-dire
la prévision) est la tâche la plus courante lorsqu’on traite des séries chronologiques ;
c’est ce à quoi nous allons nous intéresser dans ce chapitre. Parmi les autres tâches,
on peut citer l’imputation (qui consiste à substituer des valeurs aux données man-
quantes), la classication, la détection d’anomalies, etc.
7.3 Prédire une série chronologique 301
Pour visualiser ces prévisions naïves, superposons ces deux séries chronologiques
(pour le bus et pour le rail), avec les mêmes séries chronologiques décalées d’une
semaine (vers la droite) en utilisant cette fois des lignes pointillées. Nous allons
aussi représenter graphiquement la différence entre les deux (à savoir, la valeur au
temps t moins la valeur au temps t–7): c’est ce qu’on appelle la différenciation (voir
gure7.7).
diff_7 = df[["bus", "rail"]].diff(7)["2019-03":"2019-05"]
800 000
600 000
400 000
200 000
200 000
–200 000
bus
–400 000 rail
Ce n’est pas si mal ! Remarquez à quel point la série chronologique décalée reste
proche de la série chronologique d’origine. Lorsqu’une série chronologique est
corrélée à une version décalée d’elle-même, on dit que la série chronologique est
auto-corrélée. Comme vous pouvez le voir, la plupart des différences sont relativement
petites, sauf à la n du mois de mai. Peut-être y a-t-il un congé à ce moment-là ?
Vérions la colonne day_type:
>>> list(df.loc["2019-05-25":"2019-05-27"]["day_type"])
['A', 'U', 'U']
Nos prévisions naïves obtiennent une MAE de 43 916 pour les trajets en autobus,
et de 42 143 pour les trajets ferroviaires. Il est difcile de décider directement si ces
résultats sont bons ou mauvais: rendons ces erreurs de prédiction comparables en les
divisant par les valeurs cibles:
>>> targets = df[["bus", "rail"]]["2019-03":"2019-05"]
>>> (diff_7 / targets).abs().mean()
bus 0.082938
rail 0.089948
dtype: float64
La valeur que nous venons de calculer est appelée erreur absolue moyenne en pour-
centage (mean absolute percentage error, ou MAPE): nous constatons que nos prévi-
sions naïves nous ont donné une MAPE d’environ 8,3 % pour les trajets en bus et
de 9,0 % pour les trajets ferroviaires. Il est intéressant de noter que la MAE pour les
prévisions ferroviaires paraît légèrement meilleure que la MAE pour les prévisions
de trajets en bus, alors que c’est le contraire pour la MAPE. La raison en est qu’il y a
beaucoup plus de déplacements en bus que de déplacements en train, par conséquent
les erreurs de prévision sont aussi supérieures, mais lorsque nous ramenons ces erreurs
à la même échelle, il apparaît que les prévisions pour les trajets en bus sont en réalité
un peu meilleures que celles pour les trajets ferroviaires.
La MAE, la MAPE et la MSE font partie des métriques les plus courantes que
vous pouvez utiliser pour évaluer vos prévisions. Comme toujours, le choix
de la bonne métrique dépend de la tâche. À titre d’exemple, si votre projet
est quadratiquement plus pénalisé par les erreurs importantes que par les
petites erreurs, alors la MSE peut être préférable, car elle pénalise considéra-
blement les grandes erreurs.
Nous allons examiner les données de 2001 à 2019. Pour réduire le risque d’espion-
nage des données, nous allons ignorer pour l’instant les données les plus récentes.
Représentons également une moyenne sur 12mois glissants pour chaque série, an
de visualiser les tendances à long terme (voir gure7.8):
period = slice("2001", "2019")
df_monthly = df.resample('M').mean() # calcul des moyennes mensuelles
rolling_average_12_months = df_monthly[period].rolling(window=12).mean()
1e6
1.0
bus
0.9 rail
0.8
0.7
0.6
0.5
2001 2003 2005 2007 2009 2011 2013 2015 2017 2019
date
100 000
bus
50 000 rail
0
–50 000
–100 000
2001 2003 2005 2007 2009 2011 2013 2015 2017 2019
date
173. George Box et Gwilym Jenkins, Time Series Analysis (Wiley, 1970)
306 Chapitre 7. Traitement des séquences avec des RNN et des CNN
order=(1, 0, 0),
seasonal_order=(0, 1, 1, 7))
model = model.fit() # nous réentraînons le modèle chaque jour !
y_pred = model.forecast()[0]
y_preds.append(y_pred)
y_preds = pd.Series(y_preds, index=time_period)
mae = (y_preds – rail_series[time_period]).abs().mean() # renvoie 32,040.7
Ah, c’est beaucoup mieux ! La MAE est d’environ 32 041, ce qui est nettement
plus faible que la MAE obtenue avec une prévision naïve (42 143). Par conséquent,
même si le modèle n’est pas parfait, il bat largement la prévision naïve, en moyenne.
Vous vous demandez peut-être maintenant comment choisir de bons hyperpara-
mètres pour le modèle SARIMA. Il existe plusieurs méthodes, mais la plus simple à
comprendre et à mettre en œuvre est l’approche privilégiant la force brute: effectuer
une recherche par quadrillage. Pour chacun des modèles que vous souhaitez évaluer
(c’est-à-dire pour chaque combinaison d’hyperparamètres), vous pouvez exécuter
l’exemple de code précédent en changeant uniquement les valeurs des hyperpara-
mètres. Les bonnes valeurs de p, q, P et Q sont d’ordinaire entre 0 et 2, et parfois
jusqu’à 5 ou 6, tandis que les bonnes valeurs de d et D sont généralement 0 ou 1, et
parfois 2. Pour ce qui concerne s, c’est juste la période de la saisonnalité principale:
dans notre cas, c’est 7 vu la forte saisonnalité hebdomadaire. Le modèle ayant la
plus faible MAE l’emporte. Bien sûr, vous pourrez remplacer la MAE par une autre
métrique si cette dernière correspond mieux à l’objectif de votre activité. Et c’est
tout !174
174. Il existe des démarches plus raisonnées pour la sélection de bons hyperparamètres: elles se basent sur
l’analyse de la fonction d’autocorrélation (autocorrelation function, ou ACF) et de la fonction d’autocorrélation
partielle (partial autocorrelation function, ou PACF), ou sur la minimisation des métriques AIC ou BIC (pré-
sentées dans l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod 3e édition, 2023, au chapitre9)
pour pénaliser les modèles utilisant trop d’hyperparamètres et réduire le risque de surajustement, mais une
recherche par quadrillage constitue un bon début. Pour en savoir plus sur l’approche ACF-PACF, vous
pouvez consulter l’article de Jason Brownlee sur https://homl.info/arimatuning.
308 Chapitre 7. Traitement des séquences avec des RNN et des CNN
my_series = [0, 1, 2, 3, 4, 5]
my_dataset = tf.keras.utils.timeseries_dataset_from_array(
my_series,
targets=my_series[3:], # les cibles sont décalées de 3 vers le futur
sequence_length=3,
batch_size=2
)
Chaque exemple du dataset est une fenêtre de longueur 3 avec sa cible corres-
pondante (à savoir, la valeur suivant immédiatement la fenêtre). Les fenêtres sont
[0, 1, 2], [1, 2, 3] et [2, 3, 4], et leurs cibles respectives sont 3, 4 et5. Étant donné qu’il
y a trois fenêtres au total et que ce n’est pas un multiple de la taille du lot, le dernier
lot ne comporte qu’une fenêtre au lieu de deux.
Une autre façon d’obtenir le même résultat consiste à utiliser la méthode window
de la classe Dataset de tf.data. C’est un peu plus complexe, mais cela vous permet
de tout contrôler, ce qui se révélera pratique dans la suite de ce chapitre: voyons donc
comment cela fonctionne. La méthode window() renvoie un dataset constitué de
datasets de fenêtres:
>>> for window_dataset in tf.data.Dataset.range(6).window(4, shift=1):
... for element in window_dataset:
... print(f"{element}", end=" ")
... print()
...
0 1 2 3
1 2 3 4
2 3 4 5
3 4 5
4 5
5
Dans cet exemple, le dataset comporte six fenêtres, chacune d’entre elles décalée
d’une étape par rapport à la précédente et les trois dernières fenêtres étant plus petites
7.3 Prédire une série chronologique 309
parce qu’elles ont atteint la n de la série. En général, vous préférerez vous débar-
rasser de ces fenêtres plus petites en spéciant drop_remainder=True lors de
l’appel de la méthode window().
La méthode window() renvoie un dataset imbriqué, analogue à une liste de listes.
C’est utile lorsque vous voulez transformer chaque fenêtre en appelant ses méthodes
de dataset (par exemple pour les mélanger ou les regrouper en lots). Cependant, nous
ne pouvons pas utiliser un dataset imbriqué directement pour l’entraînement, car
notre modèle s’attend à recevoir des tenseurs en entrée, et non des datasets.
Par conséquent, nous devons appeler la méthode flat_map() : celle-ci
convertit un dataset imbriqué en un dataset plat (qui contient des tenseurs, et non
des datasets). Supposons par exemple que {1, 2, 3} représente un dataset contenant la
séquence de tenseurs 1, 2 et 3. Si vous aplatissez le dataset imbriqué {{1, 2}, {3, 4, 5, 6}},
vous retrouvez le dataset plat {1, 2, 3, 4, 5, 6}.
De plus, la méthode flat_map() accepte comme argument une fonction qui
vous permet de transformer chaque dataset en dataset imbriqué avant de l’aplatir. Par
exemple, si vous passez la fonction lambda ds: ds.batch(2) à flat_map(),
alors elle transformera le dataset imbriqué {{1, 2}, {3, 4, 5, 6}} pour obtenir le dataset
plat {[1, 2], [3, 4], [5, 6]}: c’est un dataset contenant trois tenseurs, chacun de taille 2.
Une fois ceci compris, nous sommes prêts à aplatir notre dataset:
>>> dataset = tf.data.Dataset.range(6).window(4, shift=1, drop_remainder=True)
>>> dataset = dataset.flat_map(lambda window_dataset: window_dataset.batch(4))
>>> for window_tensor in dataset:
... print(f"{window_tensor}")
...
[0 1 2 3]
[1 2 3 4]
[2 3 4 5]
Étant donné que chaque dataset de fenêtres contient exactement quatre éléments,
l’appel de batch(4) sur une fenêtre produit un seul tenseur de taille4. Très bien !
Nous avons maintenant un dataset contenant des fenêtres consécutives représentées
sous forme de tenseurs. Créons une petite fonction utilitaire pour faciliter l’extrac-
tion de fenêtres à partir d’un dataset:
def to_windows(dataset, length):
dataset = dataset.window(length, shift=1, drop_remainder=True)
return dataset.flat_map(lambda window_ds: window_ds.batch(length))
La dernière étape consiste à partager chaque fenêtre entre les entrées et les cibles
en utilisant la méthode map(). Nous pouvons aussi regrouper les fenêtres résultantes
en lots de taille2:
>>> dataset = to_windows(tf.data.Dataset.range(6), 4) # 3 entrées + 1 cible
# = 4
>>> dataset = dataset.map(lambda window: (window[:-1], window[-1]))
>>> list(dataset.batch(2))
[(<tf.Tensor: shape=(2, 3), dtype=int64, numpy=
array([[0, 1, 2],
[1, 2, 3]])>,
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([3, 4])>),
310 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Comme vous pouvez le constater, nous avons maintenant la même sortie que
celles obtenues précédemment avec la fonction timeseries_dataset_from_
array() (avec un petit effort supplémentaire, mais qui se révélera bientôt payant).
Mais avant de commencer l’entraînement, nous devons partager nos données
entre une période d’entraînement, une période de validation et une période de test.
Nous allons nous concentrer sur les déplacements ferroviaires pour l’instant. Nous
allons aussi recalibrer les données en les divisant par un facteur de 1million, an
que les valeurs soient à peu près dans l’intervalle 0-1 ; ceux-ci s’accordent bien avec
l’initialisation des poids par défaut et avec le taux d’apprentissage:
rail_train = df["rail"]["2016-01":"2018-12"] / 1e6
rail_valid = df["rail"]["2019-01":"2019-05"] / 1e6
rail_test = df["rail"]["2019-06":] / 1e6
targets=rail_valid[seq_length:],
sequence_length=seq_length,
batch_size=32
)
Ce modèle atteint une MAE de validation d’environ 37 866. C’est mieux qu’une
prévision naïve, mais moins bien que le modèle SARIMA175 . Pouvons-nous faire
mieux avec un RNN ? Voyons cela !
Dans Keras, toutes les couches récurrentes attendent des entrées 3D ayant la
forme [taille du lot, étapes temporelles, dimension], où dimension est égale à 1 pour les
séries chronologiques univariées et à une valeur supérieure pour les séries chronolo-
giques multivariées.
Rappelez-vous que l’argument input_shape ignore la première dimension (à
savoir la taille du lot). Étant donné que les couches récurrentes peuvent accepter des
séquences d’entrée de longueur quelconque, nous pouvons xer la deuxième dimen-
sion à None, ce qui signie « n’importe quelle taille ». Enn, sachant qu’il s’agit
175. Remarquez que la période de validation commence le 1 erjanvier 2019, ce qui fait que la première
prédiction est pour le 26février 2019, 8semaines plus tard. Lorsque nous avons évalué les modèles les plus
simples, nous avons utilisé à la place des prévisions commençant le 1ermars, mais les résultats devraient
être assez proches.
312 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Pour corriger ces deux problèmes, nous allons créer un modèle avec une couche
récurrente plus large, comportant 30 neurones récurrents, et nous allons ajouter en
sortie une couche dense comportant un seul neurone de sortie et aucune fonction
d’activation. La couche récurrente sera capable de transférer beaucoup plus d’infor-
mations d’une étape temporelle vers la suivante, et la couche dense de sortie ramè-
nera le résultat nal à une valeur unique, sans imposer de contrainte sur l’intervalle
de variation:
univar_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, input_shape=[None, 1]),
tf.keras.layers.Dense(1) # aucune fonction d’activation par défaut
])
Avec Keras, l’implémentation d’un RNN profond est plutôt simple: il suft d’em-
piler des couches récurrentes. Dans l’exemple suivant, nous utilisons trois couches
SimpleRNN (mais nous aurions pu utiliser n’importe quel autre type de couche
récurrente, comme une couche LSTM ou une couche GRU ; nous le verrons plus loin).
Les deux premières couches sont de type séquence-vers-séquence, tandis que la der-
nière est de type séquence-vers-vecteur. Enn, la couche Dense produit la prévision
du modèle (considérez-la comme une couche vecteur-vers-vecteur). Par conséquent
ce modèle est analogue à celui présenté sur la gure7.10, à ceci près que les sorties
Ŷ(0) à Ŷ(t–1) sont ignorées et qu’il y a une couche dense au-dessus de Ŷ (t), qui fournit
en sortie de la véritable prévision:
deep_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(2032, return_sequences=True,
input_shape=[None, 1]),
tf.keras.layers.SimpleRNN(2032, return_sequences=True),
tf.keras.layers.SimpleRNN(32),
tf.keras.layers.Dense(1)
])
Si vous entraînez et évaluez ce modèle, vous constaterez que sa MAE est d’environ
31 211. C’est mieux que nos deux modèles basiques, mais moins bien que notre RNN
« superciel » ! On dirait que le RNN profond est un peu trop grand pour notre tâche.
des cinq colonnes en entrée). Malgré tout, il obtient une MAE de validation de
25 330 pour le rail et 26 369 pour le bus, ce qui est plutôt bon.
Dans ce code, nous prenons les trajets ferroviaires des 56 premiers jours de la
période de validation et nous convertissons les données en un tableau NumPy de
forme [1,56,1] (n’oubliez pas que les couches récurrentes attendent des entrées 3D).
Après quoi, nous utilisons le modèle de manière répétée pour prévoir la valeur sui-
vante et nous ajoutons chaque prévision à la série en entrée, le long de l’axe du temps
(axis=1). Les prévisions résultantes sont représentées sur la gure7.11.
800 000
7 00 000
800 000 Relevés
500 000 Prévisions
400 000 Aujourd’hui
300 000
200 000
04 11 18 25 04 11
Fév. Mars
2019
date
Si le modèle fait une erreur lors d’une étape temporelle, alors les prédic-
tions pour les étapes temporelles suivantes en sont aussi affectées : les
erreurs ont tendance à s’accumuler. C’est pourquoi il est préférable de
n’utiliser cette technique que pour un petit nombre d’étapes.
La deuxième solution consiste à entraîner un RNN pour qu’il prédise les 14 pro-
chaines valeurs en une fois. Nous pouvons toujours utiliser un modèle séquence-vers-
vecteur, mais il produira 14 valeurs à la place d’une seule. Nous devons commencer
par transformer les cibles en vecteurs contenant les 14 prochaines valeurs. Pour
ce faire, nous pouvons utiliser à nouveau timeseries_dataset_from_
array(), mais en lui demandant cette fois de créer des jeux de données sans cibles
(targets=None) et avec des séquences plus longues, de longueur seq_length
+14. Puis nous pouvons utiliser la méthode map() de ces datasets pour appliquer
une fonction personnalisée à chaque lot de séquences, pour les séparer entre entrées
et cibles. Dans cet exemple, nous utilisons la série chronologique multivariée comme
entrée (en utilisant les cinq colonnes), et nous prédisons les déplacements ferro-
viaires pour les 14 prochains jours176:
def split_inputs_and_targets(mulvar_series, ahead=14, target_col=1):
return mulvar_series[:, :-ahead], mulvar_series[:, -ahead:, target_col]
ahead_train_ds = tf.keras.utils.timeseries_dataset_from_array(
mulvar_train.to_numpy(),
targets=None,
sequence_length=seq_length + 14,
[...] # les 3 autres arguments sont les mêmes que précédemment
).map(split_inputs_and_targets)
ahead_valid_ds = tf.keras.utils.timeseries_dataset_from_array(
mulvar_valid.to_numpy(),
targets=None,
sequence_length=seq_length + 14,
batch_size=32
).map(split_inputs_and_targets)
Puis, il nous faut simplement une couche de sortie à 14 unités au lieu d’une:
ahead_model = tf.keras.Sequential([
tf.keras.layers.SimpleRNN(32, input_shape=[None, 5]),
tf.keras.layers.Dense(14)
])
Après avoir entraîné ce modèle, vous pouvez prédire très facilement les 14 pro-
chaines valeurs en une fois:
X = mulvar_valid.to_numpy()[np.newaxis, :seq_length] # forme [1, 56, 5]
Y_pred = ahead_model.predict(X) # forme [1, 14]
Cette approche donne de bons résultats. Ses prévisions pour le jour suivant sont
clairement meilleures que ses prévisions à 14 jours, mais il n’accumule pas les erreurs
176. N’hésitez pas à expérimenter avec ce modèle. Vous pouvez par exemple prédire à la fois les déplace-
ments en bus et par le rail pendant les 14jours suivants. Vous devrez modier les cibles pour inclure les
deux, et modier votre modèle pour qu'il produise en sortie 28prévisions au lieu de14.
318 Chapitre 7. Traitement des séquences avec des RNN et des CNN
comme l’approche précédente. Cependant nous pouvons faire mieux encore en utili-
sant un modèle séquence-vers-séquence (ou seq2seq).
[2, 3],
[3, 4],
[4, 5]])>),
(<tf.Tensor: shape=(4,), dtype=int64, numpy=array([1, 2, 3, 4])>,
<tf.Tensor: shape=(4, 2), dtype=int64, numpy=
array([[2, 3],
[3, 4],
[4, 5],
[5, 6]])>)]
Vous pourriez être surpris que les cibles contiennent des valeurs qui ap-
paraissent dans les entrées. N’est-ce pas tricher ? Non, absolument pas : à
chaque étape temporelle, un RNN connaît uniquement les étapes tempo-
relles passées et ne regarde donc pas en avant. Il s’agit d’un modèle causal.
Créons une autre petite fonction utilitaire pour préparer les jeux de données pour
notre modèle séquence-vers-séquence. Elle se chargera également du mélange (facul-
tatif) et de la préparation des lots:
def to_seq2seq_dataset(series, seq_length=56, ahead=14, target_col=1,
batch_size=32, shuffle=False, seed=None):
ds = to_windows(tf.data.Dataset.from_tensor_slices(series), ahead + 1)
ds = to_windows(ds, seq_length).map(lambda S: (S[:, 0], S[:, 1:, 1]))
if shuffle:
ds = ds.shuffle(8 * batch_size, seed=seed)
return ds.batch(batch_size)
Maintenant nous pouvons utiliser cette fonction pour créer les jeux de données:
seq2seq_train = to_seq2seq_dataset(mulvar_train, shuffle=True, seed=42)
seq2seq_valid = to_seq2seq_dataset(mulvar_valid)
C’est à peu près identique à notre modèle précédent: la seule différence est que
nous avons spécié return_sequences=True dans la couche SimpleRNN. De
cette façon, le modèle produira en sortie une séquence de vecteurs, chacun de taille
32, au lieu de produire un seul vecteur à la dernière étape temporelle. La couche
Dense est capable de gérer des séquences en entrée: elle sera appliquée à chaque
étape temporelle, acceptant en entrée un vecteur à 32 composantes et produisant
en sortie un vecteur à 14 composantes. Il existe en fait une autre manière d’obtenir
320 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Si vous évaluez les prévisions du modèle pour t+1, vous trouverez une MAE de
validation de 25 519. Pour t+2, elle est de 26 274 et la performance continue à dimi-
nuer graduellement à mesure que le modèle tente de prévoir plus loin dans le futur.
À t+14, la MAE est de 34 322.
Vous pouvez combiner ces deux approches pour effectuer des prévisions
plusieurs étapes temporelles à l’avance : si par exemple vous entraînez un
modèle qui effectue des prévisions 14 jours à l’avance, prenez alors sa sortie
et ajoutez-la aux entrées, puis exécutez à nouveau le modèle pour obtenir
les prévisions pour les 14 jours suivants, et répétez éventuellement le pro-
cessus.
Des RNN simples peuvent se révéler plutôt bons pour les prévisions de séries
chronologiques ou le traitement d’autres sortes de séries, mais leurs performances
sont moins probantes si les séries chronologiques ou les séquences sont très longues.
Voyons pourquoi et voyons ce que nous pouvons faire.
177. César Laurent et al., « Batch Normalized Recurrent Neural Networks », Proceedings of the IEEE Inter-
national Conference on Acoustics, Speech, and Signal Processing (2016), 2657-2661 : https://homl.info/rnnbn.
178. Jimmy Lei Ba et al., « Layer Normalization » (2016) : https://homl.info/layernorm.
322 Chapitre 7. Traitement des séquences avec des RNN et des CNN
en 2016. Elle ressemble énormément à la normalisation par lots, mais elle se fait non
plus suivant les lots mais suivant les caractéristiques. L’intérêt est que le calcul des
statistiques requises peut se faire à la volée, à chaque étape temporelle, indépendam-
ment pour chaque instance. En conséquence, elle se comporte de la même manière
pendant l’entraînement et les tests (contrairement à la normalisation par lots), et elle
n’a pas besoin des moyennes mobiles exponentielles pour estimer les statistiques de
caractéristiques sur toutes les instances du jeu d’entraînement. À l’instar de la nor-
malisation par lots, la normalisation par couche apprend un paramètre de réduction
et un paramètre de centrage pour chaque entrée. Dans un RNN, elle se place généra-
lement juste après la combinaison linéaire des entrées et des états cachés.
Utilisons Keras pour mettre en place une normalisation par couche à l’intérieur
d’une cellule de mémoire simple. Pour cela, nous devons dénir une cellule de
mémoire personnalisée. Elle est comparable à une couche normale, excepté que sa
méthode call() attend deux arguments : les entrées inputs de l’étape temporelle
courante et les états cachés states de l’étape temporelle précédente. Notez que
l’argument states est une liste contenant un ou plusieurs tenseurs. Dans le cas
d’une cellule de RNN simple, il contient un seul tenseur égal aux sorties de l’étape
temporelle précédente, mais d’autres cellules pourraient avoir des tenseurs à plusieurs
états (par exemple, un LSTMCell possède un état à long terme et un état à court
terme, comme nous le verrons plus loin).
Une cellule doit également avoir les attributs state_size et output_size.
Dans un RNN simple, tous deux sont simplement égaux au nombre d’unités. Le code
suivant implémente une cellule de mémoire personnalisée qui se comporte comme
un SimpleRNNCell, hormis la normalisation par couche appliquée à chaque étape
temporelle :
class LNSimpleRNNCell(tf.keras.layers.Layer):
def __init__(self, units, activation="tanh", **kwargs):
super().__init__(**kwargs)
self.state_size = units
self.output_size = units
self.simple_rnn_cell = tf.keras.layers.SimpleRNNCell(units,
activation=None)
self.layer_norm = tf.keras.layers.LayerNormalization()
self.activation = tf.keras.activations.get(activation)
Parcourons ce code:
• Notre classe LNSimpleRNNCell hérite naturellement de la classe tf.keras.
layers.Layer.
• Le constructeur reçoit en entrée le nombre d’unités et la fonction désirée
qu’il range dans les attributs state_size et output_size, puis crée une
SimpleRNNCell sans fonction d’activation (car nous voulons effectuer une
normalisation par couches après l’opération linéaire mais avant la fonction
7.4 Traiter les séquences longues 323
De manière comparable, vous pouvez créer une cellule personnalisée pour appli-
quer un abandon (alias dropout) entre chaque étape temporelle. Mais il existe une
solution plus simple: la plupart des couches récurrentes et des cellules fournies par
Keras disposent des hyperparamètres dropout et recurrent_dropout. Le pre-
mier dénit le taux d’abandon appliqué aux entrées, le second, celui destiné aux
états cachés (entre les étapes temporelles). Il est donc inutile de créer une cellule
personnalisée pour appliquer un abandon à chaque étape temporelle dans un RNN.
À l’aide de ces techniques, vous pouvez alléger le problème d’instabilité des gra-
dients et entraîner un RNN beaucoup plus efcacement. Voyons à présent comment
traiter le problème de mémoire à court terme.
Lorsqu’on effectue des prévisions sur une série chronologique, il est souvent
utile d’accompagner les prévisions de barres d’erreur. Une façon de le réaliser
consiste à utiliser l’abandon de Monte Carlo présenté au chapitre 3 : utilisez
recurrent_dropout durant l’entraînement, puis conservez l’abandon
actif dans la phase d’inférence en spécifiant training=True lors de l’ap-
pel du modèle. Répétez cette opération plusieurs fois pour obtenir plusieurs
prévisions légèrement différentes, puis calculez la moyenne et l’écart-type de
ces prévisions pour chaque période temporelle.
179. Il aurait été plus simple d’hériter de SimpleRNNCell de façon à ne pas avoir à créer de
SimpleRNNCell en interne ou de gérer les attributs state_size et output_size, mais le but ici
est de montrer comment créer une cellule personnalisée à partir de rien.
324 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Cellules LSTM
La cellule de longue mémoire à court terme (long short-term memory, ou LSTM) a été
proposée181 en 1997 par Sepp Hochreiter et Jürgen Schmidhuber. Elle a été progres-
sivement améliorée au l des ans par plusieurs chercheurs, comme Alex Graves182,
Haşim Sak183 et Wojciech Zaremba184 . Si l’on considère la cellule LSTM comme une
boîte noire, on peut s’en servir presque comme une cellule de base, mais avec de bien
meilleures performances. L’entraînement convergera plus rapidement et détectera les
structurations à plus long terme présentes dans les données. Dans Keras, il suft de
remplacer la couche SimpleRNN par une couche LSTM :
model = tf.keras.Sequential([
tf.keras.layers.LSTM(32, return_sequences=True, input_shape=[None, 5]),
tf.keras.layers.Dense(14)
])
180. Un personnage des lms d’animation Le monde de Nemo et Le monde de Dory qui souffre de pertes de
mémoire à court terme.
181. Sepp Hochreiter et Jürgen Schmidhuber, « Long Short-Term Memory », Neural Computation, 9, n°8
(1997), 1735-1780 : https://homl.info/93.
182. https://homl.info/graves
183. Haşim Sak et al., « Long Short-Term Memory Based Recurrent Neural Network Architectures for
Large Vocabulary Speech Recognition » (2014) : https://homl.info/94.
184. Wojciech Zaremba et al., « Recurrent Neural Network Regularization » (2014) : https://homl.info/95.
7.4 Traiter les séquences longues 325
ŷ (t)
Porte d’oubli
c (t–1) c(t)
Porte Porte
d’entrée de sortie
h(t)
f(t) g(t) i(t) o (t)
FC FC FC FC
Multiplication
par éléments
h (t–1) Addition
Cellule LSTM
Sigmoïde
x(t) tanh
Figure 7.12 – Cellule LSTM (FC, alias fully connected : couche intégralement connectée)
Ouvrons la boîte ! Voici l’idée centrale : le réseau peut apprendre ce qu’il faut
stocker dans l’état à long terme, ce qui doit être oublié et ce qu’il faut y lire. Le par-
cours de l’état à long terme c(t–1) au travers du réseau va de la gauche vers la droite. Il
passe tout d’abord par une porte d’oubli (forget gate), qui abandonne certaines informa-
tions, puis en ajoute de nouvelles via l’opération d’addition (les informations ajoutées
sont sélectionnées par une porte d’entrée [input gate]), et le résultat c(t) est envoyé
directement, sans autre transformation. Par conséquent, à chaque étape temporelle,
des informations sont retirées et d’autres sont ajoutées. Par ailleurs, après l’opération
d’addition, l’état à long terme est copié et soumis à la fonction tanh, dont le résultat
est ltré par la porte de sortie (output gate). On obtient alors l’état à court terme h(t),
qui est égal à la sortie de la cellule pour l’étape temporelle y(t). Voyons d’où pro-
viennent les nouvelles informations et comment fonctionnent les portes.
Premièrement, le vecteur d’entrée courant x(t) et l’état à court terme précédent
h (t–1) sont fournis à quatre couches intégralement connectées, ayant toutes un objectif
différent :
• La couche principale génère g(t) : elle joue le rôle habituel d’analyse des entrées
courantes x(t) et de l’état précédent (à court terme) h (t–1). Une cellule de base
comprend uniquement cette couche et sa sortie est transmise directement à y (t)
et à h(t) . En revanche, dans une cellule LSTM, la sortie de cette couche ne se
fait pas directement: ses parties les plus importantes sont stockées dans l’état à
long terme (le reste est abandonné).
• Les trois autres couches sont des contrôleurs de porte. Puisqu’elles utilisent la
fonction d’activation logistique, les sorties sont dans la plage 0 à 1. Celles-
ci étant passées à des opérations de multiplication par éléments, une valeur0
ferme la porte, tandis qu’une valeur 1 l’ouvre. Plus précisément :
– La porte d’oubli (contrôlée par f(t)) décide des parties de l’état à long terme
qui doivent être effacées.
326 Chapitre 7. Traitement des séquences avec des RNN et des CNN
– La porte d’entrée (contrôlée par i(t) ) choisit les parties de g(t) qui doivent être
ajoutées à l’état à long terme.
– La porte de sortie (contrôlée par o(t)) sélectionne les parties de l’état à long
terme qui doivent être lues et produites lors de cette étape temporelle, à la
fois dans h (t) et dans y (t).
En résumé, une cellule LSTM peut apprendre à reconnaître une entrée impor-
tante (le rôle de la porte d’entrée), la stocker dans l’état à long terme, la conserver
aussi longtemps que nécessaire (le rôle de la porte d’oubli) et l’extraire lorsqu’elle est
requise. Cela explique pourquoi elles réussissent très bien à identier des motifs à long
terme dans des séries chronologiques, des textes longs, des enregistrements audio, etc.
Les équations 7.4 récapitulent le calcul de l’état à long terme d’une cellule, de son
état à court terme et de sa sortie à chaque étape temporelle, pour une seule instance
(les équations pour un mini-lot complet sont très similaires).
Équations 7.4 – Calculs LSTM
Cellule GRU
La cellule d’unité récurrente à porte (gated recurrent unit, ou GRU), illustrée à la
gure7.13, a été proposée185 en 2014 par Kyunghyun Cho et al. Dans leur article, ils
décrivent également le réseau encodeur-décodeur mentionné précédemment.
185. Kyunghyun Cho et al., « Learning Phrase Representations Using RNN Encoder-Decoder for Statis-
tical Machine Translation», Proceedings of the 2014 Conference on Empirical Methods in Natural Language
Processing (2014), 1724-1734 : https://homl.info/97.
7.4 Traiter les séquences longues 327
ŷ(t)
h(t–1) h(t )
1–z (t)
g (t)
FC
r (t) z (t)
FC FC
Cellule GRU
x(t)
La cellule GRU est une version simpliée de la cellule LSTM. Puisque ses per-
formances semblent tout aussi bonnes186 , sa popularité ne fait que croître. Voici les
principales simplications :
• Les deux vecteurs d’état sont fusionnés en un seul vecteur h(t).
• Un seul contrôleur de porte z (t) s’occupe des portes d’oubli et d’entrée. S’il
produit un 1, la porte d’oubli est ouverte (=1) et la porte d’entrée est fermée
(1−1=0). S’il produit un 0, la logique inverse s’applique. Autrement dit, dès
qu’une information doit être stockée, son emplacement cible est tout d’abord
effacé. Il s’agit en réalité d’une variante répandue de la cellule LSTM en soi.
• La porte de sortie a disparu : le vecteur d’état complet est sorti à chaque étape
temporelle. Toutefois, un nouveau contrôleur de porte r(t) décide des parties de
l’état précédent qui seront présentées à la couche principale (g (t)).
Les équations 7.5 résument le calcul de l’état de la cellule à chaque étape tempo-
relle pour une seule instance.
186. Voir Klaus Greff et al. « LSTM: A Search Space Odyssey », IEEE Transactions on Neural Networks and
Learning Systems 28, n° 10 (2017), 2222-2232. Cet article montre que toutes les variantes de LSTM ont des
performances quasi équivalentes ; il est disponible à l’adresse https://homl.info/98.
328 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Utiliser des couches de convolution à une dimension pour traiter des séquences
Au chapitre6, nous avons vu qu’une couche de convolution à deux dimensions tra-
vaille en déplaçant plusieurs noyaux (ou ltres) assez petits sur une image, en produi-
sant plusieurs cartes de caractéristiques (une par noyau) à deux dimensions. De façon
comparable, une couche de convolution à une dimension fait glisser plusieurs noyaux
sur une série, en produisant une carte de caractéristiques à une dimension par noyau.
Chaque noyau va apprendre à détecter un seul très court motif séquentiel (pas plus
long que la taille du noyau). Avec dix noyaux, la couche de sortie sera constituée de
dix séquences 1D (toutes de la même longueur); vous pouvez également voir cette
sortie comme une seule séquence 10D.
Autrement dit, vous pouvez construire un réseau de neurones qui mélange
lescouches récurrentes et les couches de convolution à une dimension (ou même
les couches de pooling à une dimension). En utilisant une couche de convolution à
une dimension avec un pas de 1 et un remplissage "same", la série de sortie aura la
même longueur que la série d’entrée. En revanche, avec un remplissage "valid" ou
un pas supérieur à 1, la série de sortie sera plus courte que la série d’entrée; n’oubliez
pas d’ajuster les cibles en conséquence.
Par exemple, le modèle suivant est identique au précédent, excepté qu’il débute
par une couche de convolution 1D qui sous-échantillonne la séquence d’entrée avec
un facteur 2, en utilisant un pas de 2. Puisque la taille du noyau est plus importante
que le pas, toutes les entrées serviront au calcul de la sortie de la couche et le modèle
peut donc apprendre à conserver les informations utiles, ne retirant que les détails
non pertinents. En raccourcissant les séquences, la couche de convolution peut aider
les couches GRU à détecter des motifs plus longs, ce qui nous permet de doubler la
séquence d’entrée pour la porter à 112 jours. Notez que vous devez également couper
les trois premières étapes temporelles dans les cibles: en effet, la taille du noyau étant
égale à 4, la première entrée de la couche de convolution sera fondée sur les étapes
temporelles d’entrée 0 à 3, et les premières prévisions seront faites pour les étapes
temporelles 4 à 17 (au lieu de 10 à 14). De plus, nous devons sous-échantillonner les
cibles d’un facteur2, en raison du pas:
7.4 Traiter les séquences longues 329
conv_rnn_model = tf.keras.Sequential([
tf.keras.layers.Conv1D(filters=32, kernel_size=4, strides=2,
activation="relu", input_shape=[None, 5]),
tf.keras.layers.GRU(32, return_sequences=True),
tf.keras.layers.Dense(14)
])
Si vous entraînez et évaluez ce modèle, vous constaterez qu’il est légèrement meil-
leur que le modèle précédent. Il est même possible d’utiliser uniquement des couches
de convolution à une dimension et de retirer totalement les couches récurrentes!
WaveNet
Dans un article187 de 2016, Aaron van den Oord et d’autres chercheurs de DeepMind
ont présenté une architecture nommée WaveNet. Ils ont empilé des couches de
convolution à une dimension, en doublant le taux de dilatation (la distance de sépa-
ration entre les entrées d’un neurone) à chaque nouvelle couche : la première couche
de convolution reçoit uniquement deux étapes temporelles à la fois), tandis que la
suivante en voit quatre (son champ récepteur est long de quatre étapes temporelles),
celle d’après en voit huit, et ainsi de suite (voir la gure7.14). Ainsi, les couches
inférieures apprennent des motifs à court terme, tandis que les couches supérieures
apprennent des motifs à long terme. Grâce au taux de dilatation qui double, le réseau
peut traiter très efcacement des séries extrêmement longues.
dilatation 8
dilatation 4
dilatation 2
dilatation 1
Entrée
187. Aaron van den Oord et al., « WaveNet: A Generative Model for Raw Audio », (2016) : https://homl.
info/wavenet.
330 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Dans leur article, les auteurs ont empilé dix couches de convolution avec des taux
de dilatation égaux à 1, 2, 4, 8, …, 256, 512, puis un autre groupe de dix couches
identiques (toujours avec les mêmes taux de dilatation), et encore un autre groupe
identique. Pour justier cette architecture, ils ont souligné qu’une seule pile de dix
couches de convolution avec ces taux de dilatation fonctionnera comme une couche
de convolution super efcace avec un noyau de taille 1 024 (en étant plus rapide
et plus puissante, et en demandant beaucoup moins de paramètres). Avant chaque
couche, ils ont également appliqué un remplissage des séries d’entrée avec un nombre
de zéros égal au taux de dilatation, cela pour que les séries conservent la même lon-
gueur tout au long du réseau. Voici comment implémenter un WaveNet simple pour
traiter les mêmes séries que précédemment188 :
wavenet_model = tf.keras.Sequential()
wavenet_model.add(tf.keras.layers.Input(shape=[None, 5]))
for rate in (1, 2, 4, 8) * 2:
wavenet_model.add(tf.keras.layers.Conv1D(
filters=32, kernel_size=2, padding="causal", activation="relu",
dilation_rate=rate))
wavenet_model.add(tf.keras.layers.Conv1D(filters=14, kernel_size=1))
Ce modèle séquentiel commence par une couche d’entrée explicite (c’est plus
simple que d’essayer de xer input_shape uniquement sur la première couche),
puis ajoute une couche de convolution à une dimension avec un remplissage
"causal", qui est semblable au remplissage "same" à ceci près qu’on n’ajoute des
zéros qu’au début de la séquence au lieu d’en ajouter aux deux bouts. Nous sommes
ainsi certains que la couche de convolution ne regarde pas dans le futur lors de ses
prédictions. Nous ajoutons ensuite des paires de couches similaires en utilisant des
taux de dilatation croissants: 1, 2, 4, 8, et de nouveau 1, 2, 4, 8. Nous terminons par la
couche de sortie: une couche de convolution avec 14 ltres de taille1 et sans aucune
fonction d’activation. Comme nous l’avons vu précédemment, une telle couche de
convolution est équivalente à une couche Dense de 14 unités. Grâce au remplissage
causal, chaque couche de convolution produit une séquence de même longueur que
sa séquence d’entrée, et les cibles utilisées pendant l’entraînement peuvent être les
séquences complètes de 112 jours ; nous n’avons pas besoin de les couper ni de les
sous-échantillonner.
Les modèles dont nous avons parlé dans cette section donnent des résultats com-
parables pour les prévisions sur les déplacements journaliers, mais ils peuvent donner
des résultats très variables selon la tâche et la quantité de données disponibles. Dans
l’article sur WaveNet, les auteurs sont arrivés à des performances de pointe sur diffé-
rentes tâches audio (d’où le nom de l’architecture), y compris dans le domaine de la
dictée vocale, en produisant des voies incroyablement réalistes dans différentes lan-
gues. Ils ont également utilisé ce modèle pour générer de la musique, un échantillon
audio à la fois. Cet exploit est d’autant plus impressionnant si vous réalisez qu’une
188. Le WaveNet complet inclut quelques techniques supplémentaires, comme les connexions de saut que
l’on trouve dans un ResNet, et les unités d’activation à porte semblable à celles qui existent dans une cellule
GRU. Vous trouverez plus de détails dans le notebook (voir «15_processing_sequences_using_rnns_and_
cnns.ipynb» sur https://homl.info/colab3).
7.5 Exercices 331
seule seconde de son contient des dizaines de milliers d’étapes temporelles – même les
LSTM et les GRU ne sont pas capables de traiter de si longues séries.
Avec cela, vous pouvez maintenant vous attaquer à toutes sortes de séries chrono-
logiques. Au chapitre8, nous poursuivrons l’exploration des RNN, et nous verrons
comment ils peuvent résoudre différentes tâches de traitement automatique du lan-
gage naturel.
7.5 EXERCICES
1. Donnez quelques applications d’un RNN séquence-vers-séquence.
Que pouvez-vous proposer pour un RNN séquence-vers-vecteur ? Et
pour un RNN vecteur-vers-séquence ?
2. Combien de dimensions doivent avoir les entrées d’une couche de
RNN ? Que représente chaque dimension ? Qu’en est-il des sorties ?
3. Si vous souhaitez construire un RNN séquence-vers-séquence
profond, pour quelles couches du RNN devez-vous préciser
return_sequences=True ? Et dans le cas d’un RNN séquence-
vers-vecteur ?
4. Supposons que vous disposiez d’une série chronologique univariée
(c’est-à-dire à une seule variable) de relevés quotidiens et que vous
souhaitiez prévoir les sept jours suivants. Quelle architecture de
RNN devez-vous employer ?
5. Quelles sont les principales difcultés de l’entraînement des RNN ?
Comment pouvez-vous les résoudre ?
6. Esquissez l’architecture d’une cellule LSTM.
7. Pourquoi voudriez-vous utiliser des couches de convolution à une
dimension dans un RNN ?
8. Quelle architecture de réseau de neurones est adaptée à la
classication de vidéos ?
9. Entraînez un modèle de classication sur le jeu de données
SketchRNN (disponible dans TensorFlow Datasets).
10. Téléchargez le jeu de données Bach chorales (https://homl.info/bach)
et extrayez son contenu. Il est constitué de 382 chants choraux
332 Chapitre 7. Traitement des séquences avec des RNN et des CNN
Lorsque Alan Turing a imaginé son fameux test de Turing189 en 1950, il a proposé une
manière d’évaluer la capacité d’une machine à égaler l’intelligence humaine. Il aurait
pu tester diverses capacités, comme reconnaître des chats dans des images, jouer aux
échecs, composer de la musique ou sortir d’un labyrinthe, mais il a choisi une tâche
linguistique. Plus précisément, il a conçu un chatbot capable de tromper son inter-
locuteur en lui faisant croire qu’il discutait avec un être humain190.
Ce test présente des faiblesses: un jeu de règles codées peut tromper des personnes
naïves ou crédules (par exemple, la machine peut donner des réponses prédénies
en réponse à certains mots-clés, prétendre plaisanter ou être ivre pour expliquer des
réponses bizarres, ou écarter les questions difciles en répondant par d’autres ques-
tions) et de nombreux aspects de l’intelligence humaine sont complètement ignorés
(par exemple, la capacité à interpréter une communication non verbale, comme des
expressions faciales, ou à apprendre une tâche manuelle). Quoi qu’il en soit, le test
met en évidence le fait que la maîtrise du langage est sans doute la plus grande capa-
cité cognitive de l’Homo sapiens. Sommes-nous en mesure de construire une machine
capable de lire et d’écrire en langage naturel ? C’est le but ultime de la recherche
en traitement automatique du langage naturel, mais le sujet est un peu trop vaste,
c’est pourquoi les chercheurs se concentrent sur des tâches plus spéciques comme
189. Alan Turing, « Computing Machinery and Intelligence », Mind, 49 (1950), 433-460 : https://homl.
info/turingtest.
190. Bien entendu, le terme chatbot est arrivé beaucoup plus tard. Turing a appelé son test le jeu d’imitation :
la machine A et l’homme B discutent avec un interrogateur humain C au travers de messages textuels ;
l’interrogateur pose des questions an de déterminer qui est la machine (A ou B). La machine réussit le test
si elle parvient à tromper l’interrogateur, tandis que l’homme B doit essayer de l’aider.
334 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
par un modèle de char-RNN après qu’il a été entraîné sur l’ensemble de l’œuvre de
Shakespeare :
PANDARUS:
Alas, I think he shall be come approached and the day
When little srain would be attain’d into being never fed,
And who is but a chain and subjects of his death,
I should not sleep.
Ce n’est pas véritablement un chef-d’œuvre, mais il n’en est pas moins étonnant
de constater la capacité du modèle à apprendre des mots, une grammaire, une ponc-
tuation correcte et d’autres aspects linguistiques, simplement en apprenant à prédire
le caractère suivant dans une phrase. C’est notre premier exemple de modèle linguis-
tique : nous parlerons dans ce chapitre des modèles similaires –mais beaucoup plus
puissants– qui constituent désormais le cœur du traitement du langage naturel. Dans
la suite de cette section, nous allons construire un char-RNN pas à pas, en commen-
çant par la création du jeu de données.
All:
Speak, speak.
Ensuite, tout comme nous l’avons fait au chapitre7, nous pouvons transformer
cette très longue séquence en un jeu de données (ou dataset) de fenêtres que nous
pourrons utiliser pour entraîner un RNN séquence-vers-séquence. Les cibles seront
très semblables aux entrées, avec simplement une étape temporelle de décalage vers
le futur. Si par exemple une instance du dataset est une séquence d’identiants de
caractères représentant le texte « to be or not to b » (sans le « e » nal), la cible
correspondante sera une séquence d’identiants de caractères représentant le texte
« obe or not to be » (avec le « e » nal, mais sans le « t » du début). Écrivons une
petite fonction utilitaire pour convertir une longue séquence d’identiants de carac-
tères en un dataset de paires de fenêtres entrée/cible:
def to_dataset(sequence, length, shuffle=False, seed=None, batch_size=32):
ds = tf.data.Dataset.from_tensor_slices(sequence)
ds = ds.window(length + 1, shift=1, drop_remainder=True)
ds = ds.flat_map(lambda window_ds: window_ds.batch(length + 1))
if shuffle:
ds = ds.shuffle(buffer_size=100_000, seed=seed)
ds = ds.batch(batch_size)
return ds.map(lambda window: (window[:, :-1], window[:, 1:])).prefetch(1)
Cette fonction démarre de façon tout à fait analogue à la fonction utilitaire per-
sonnalisée que nous avons créée au chapitre7:
• elle reçoit une séquence en entrée (c’est-à-dire le texte encodé) et crée un jeu
de données contenant toutes les fenêtres de la longueur désirée ;
• elle augmente la longueur de 1, étant donné que nous avons besoin du caractère
suivant dans la cible ;
• puis elle mélange les fenêtres (optionnellement), les regroupe en lots, les
découpe en paires entrée/cible, et active la lecture anticipée.
La gure 8.1 résume les étapes de préparation du jeu de données: elle présente
des fenêtres de longueur 11, avec une taille de lot de 3. L’indice de début de chaque
fenêtre est indiqué à côté de celle-ci.
8.1 Générer un texte shakespearien à l’aide d’un RNN à caractères 337
fenêtres
Nous avons choisi une longueur de fenêtre de 100, mais vous pouvez
essayer d’ajuster cette valeur : il est plus facile et plus rapide d’entraîner un
RNN sur des séquences d’entrée plus courtes, mais ce RNN ne pourra
apprendre aucun motif de longueur supérieure à length, c’est pourquoi
il ne faut pas le choisir trop petit.
model_ckpt = tf.keras.callbacks.ModelCheckpoint(
"my_shakespeare_model", monitor="val_accuracy", save_best_only=True)
history = model.fit(train_set, validation_data=valid_set, epochs=10,
callbacks=[model_ckpt])
Examinons ce code :
• Nous utilisons comme première couche une couche Embedding pour encoder
les identiants de caractère (les plongements, ou embeddings en anglais, ont été
présentés au chapitre5). Le nombre d’entrées de la couche Embedding est
le nombre d’identiants de caractère distincts, et le nombre de composantes
des plongements produits est un hyperparamètre que vous pouvez ajuster (nous
lui donnons pour l’instant la valeur 16). Alors que les entrées de la couche
Embedding sont des tenseurs 2D de forme [taille de lot, longueur de fenêtre],
la sortie de la couche Embedding sera un tenseur 3D de forme [taille de lot,
longueur de fenêtre, dimension du plongement].
• Nous utilisons une couche Dense comme couche de sortie : elle doit avoir
39unités (n_tokens), car le texte est constitué de 39 caractères différents et
nous voulons sortir une probabilité pour chaque caractère possible (à chaque
étape temporelle). Puisque la somme des probabilités de sortie à chaque étape
temporelle doit être égale à 1, nous appliquons la fonction softmax aux sorties
de la couche Dense.
• Enn, nous compilons ce modèle, en utilisant la perte "sparse_
categorical_crossentropy" ainsi qu’un optimiseur Nadam,
et nous entraînons le modèle sur plusieurs époques191 en utilisant un
rappel ModelCheckpoint pour sauvegarder le meilleur modèle (en
termes d’exactitude de validation) au fur et à mesure de la progression de
l’entraînement.
Si vous exécutez ce code dans Colab avec un GPU activé, alors l’en-
traînement devrait prendre une à deux heures environ. Vous pou-
vez réduire le nombre d’époques si vous ne voulez pas attendre si
longtemps, mais bien sûr l’exactitude du modèle sera probablement
moindre. Si la session Colab se termine par un timeout, prenez soin
de vous reconnecter rapidement car sinon l’environnement d’exécution
de Colab sera supprimé.
Ce modèle ne prétraitant pas le texte, nous allons l’emballer dans un modèle nal
comportant une couche tf.keras.layers.TextVectorization en tant
que première couche, ainsi que la couche tf.keras.layers.Lambda pour sous-
traire 2 à chaque identiant de caractère étant donné que nous n’allons pas utiliser les
identiants réservés au remplissage et aux éléments inconnus:
shakespeare_model = tf.keras.Sequential([
text_vec_layer,
191. En raison du recouvrement des fenêtres d’entrée, le concept d’époque n’est pas clair dans ce cas :
durant chaque époque (telle que l’implémente Keras), le modèle va en réalité voir le même caractère à de
nombreuses reprises.
8.1 Générer un texte shakespearien à l’aide d’un RNN à caractères 339
Pour mieux contrôler la diversité du texte généré, nous pouvons diviser les logits
par une valeur appelée température, ajustable selon nos besoins. Une température
proche de zéro favorisera les caractères ayant une probabilité élevée, tandis qu’une
température très élevée donnera une probabilité égale à tous les caractères. On privi-
légie en général les températures basses s’il s’agit de générer un texte plutôt rigoureux
et précis, comme des équations mathématiques, et les températures plus élevées pour
produire un texte plus varié et créatif. La fonction utilitaire personnalisée next_
char() ci-après se fonde sur cette approche pour sélectionner le caractère qui sera
concaténé au texte d’entrée :
def next_char(text, temperature=1):
y_proba = shakespeare_model.predict([text])[0, -1:]
rescaled_logits = tf.math.log(y_proba) / temperature
char_id = tf.random.categorical(rescaled_logits, num_samples=1)[0, 0]
return text_vec_layer.get_vocabulary()[char_id + 2]
340 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Nous pouvons ensuite écrire une autre petite fonction utilitaire qui appellera
next_char() de façon répétée an d’obtenir le caractère suivant et l’ajouter au
texte donné :
def extend_text(text, n_chars=50, temperature=1):
for _ in range(n_chars):
text += next_char(text, temperature)
return text
second push:
gremio, lord all, a sistermen,
>>> print(extend_text("To be or not to be", temperature=100))
To be or not to bef ,mt'&o3fpadm!$
wh!nse?bws3est--vgerdjw?c-y-ewznq
précédent s’était arrêtée. Pour construire un RNN avec état, la première chose à faire
est donc d’utiliser des séquences d’entrée qui se suivent mais ne se chevauchent pas
(à la place des séquences mélangées et se chevauchant que nous avons utilisées pour
entraîner des RNN sans état). Lors de la création du tf.data.Dataset, nous
devons indiquer shift=length (à la place de shift=1) dans l’appel à la méthode
window(). Et, bien sûr, nous ne devons pas appeler la méthode shuffle().
Malheureusement, la création des lots est beaucoup plus compliquée lors de la pré-
paration d’un RNN avec état que d’un RNN sans état. Si nous appelions batch(32),
trente-deux fenêtres consécutives seraient alors placées dans le même lot, et le lot
suivant ne poursuivrait pas chacune de ces fenêtres là où elles se sont arrêtées. Le pre-
mier lot contiendrait les fenêtres 1 à 32, et le deuxième lot, les fenêtres 33 à 64. Par
conséquent, si nous prenons, par exemple, la première fenêtre de chaque lot (c’est-
à-dire les fenêtres 1 et 33), il est évident qu’elles ne sont pas consécutives. La solution
la plus simple à ce problème consiste à utiliser une taille de lot égale à 1. C’est la
stratégie utilisée par la fonction utilitaire to_dataset_for_stateful_rnn()
suivante pour préparer un jeu de données pour un RNN avec état :
def to_dataset_for_stateful_rnn(sequence, length):
ds = tf.data.Dataset.from_tensor_slices(sequence)
ds = ds.window(length + 1, shift=length, drop_remainder=True)
ds = ds.flat_map(lambda window: window.batch(length + 1)).batch(1)
return ds.map(lambda window: (window[:, :-1], window[:, 1:])).prefetch(1)
fenêtres
entrées cibles
lot 1
lot 2
Le partage en lots est plus compliqué, mais pas impossible. Par exemple, nous
pouvons découper le texte de Shakespeare en trente-deux textes de longueur égale,
créer un jeu de données de séquences d’entrée consécutives pour chacun d’eux, et
342 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
un excellent classicateur d’analyse d’opinion : bien que le modèle ait été entraîné sans
aucune étiquette, ce neurone d’opinion (en anglais sentiment neuron), comme ils l’ont
appelé, a obtenu les meilleures performances du moment sur des tests de référence en
matière d’analyse d’opinion. Ceci a suggéré et motivé un préentraînement non supervisé
pour le traitement du langage naturel.
Mais avant d’explorer le préentraînement non supervisé, intéressons-nous aux
modèles travaillant au niveau du mot et à la façon de les utiliser en mode super-
visé pour l’analyse d’opinion (en anglais, sentiment analysis). Nous en proterons pour
apprendre à traiter les séquences de longueur variable en utilisant les masques.
194. Rico Sennrich et al., « Neural Machine Translation of Rare Words with Subword Units », Proceedings
of the 54th Annual Meeting of the Association for Computational Linguistics 1 (2016), 1715-1725 : https://homl.
info/rarewords.
195. Le terme anglais « token » est parfois traduit par « jeton » en français, mais surtout dans le domaine des
télécommunications. Dans le domaine du Deep Learning, le terme anglais est largement employé dans les
documents français sur ce sujet, c’est pourquoi nous l’emploierons également pour plus de clarté.
8.2 Analyse d’opinion 345
des regroupements successifs des paires adjacentes les plus fréquentes jusqu’à ce que
le vocabulaire atteigne la taille désirée.
Un article196 publié en 2018 par Taku Kudo, chercheur de Google, a encore amé-
lioré ce découpage à un niveau inférieur au mot, en supprimant souvent le besoin
préalable d’un traitement spécique à la langue. Il proposait de plus une technique de
régularisation novatrice appelée régularisation en dessous du mot (en anglais, subword
regularization), qui améliore l’exactitude et la robustesse en introduisant une part de
hasard dans le découpage en tokens lors de l’entraînement: ainsi, « New England »
peut être découpé en « New » + « England », ou « New » + « Eng » + « land », ou sim-
plement « New England » (un seul token). Le projet SentencePiece de Google (https://
github.com/google/sentencepiece) fournit une implémentation open source de cette
technique. Elle est décrite dans un article197 rédigé par Taku Kudo et John Richardson.
La bibliothèque TensorFlow Text (https://homl.info/tftext), implémente aussi diffé-
rentes stratégies de conversion en tokens, notamment WordPiece198 (une variante de
l’encodage par paire d’octets), tandis que la bibliothèque Tokenizers de Hugging Face
(https://homl.info/tokenizers) implémente une grande variété d’algorithmes de décou-
page en tokens extrêmement rapides.Toutefois, pour ce qui concerne notre projet
IMDb en anglais, délimiter les tokens grâce aux caractères d’espacement devrait suf-
re. Commençons donc par créer une couche TextVectorization et l’adapter
à notre jeu d’entraînement. Nous limiterons le vocabulaire à 1000 tokens, qui seront
constituées des 998 mots les plus fréquents plus un token de remplissage et un token
pour les mots inconnus, car il est peu probable que les mots très rares soient impor-
tants pour cette tâche, et en limitant la taille du vocabulaire on réduit le nombre de
paramètres que le modèle a besoin d’apprendre :
vocab_size = 1000
text_vec_layer = tf.keras.layers.TextVectorization(max_tokens=vocab_size)
text_vec_layer.adapt(train_set.map(lambda reviews, labels: reviews))
196. Taku Kudo, « Subword Regularization: Improving Neural Network Translation Models with Multiple
Subword Candidates » (2018) : https://homl.info/subword.
197. Taku Kudo et John Richardson, « SentencePiece: A Simple and Language Independent Subword
Tokenizer and Detokenizer for Neural Text Processing» (2018) : https://homl.info/sentencepiece.
198. Yonghui Wu et al., « Google’s Neural Machine Translation System: Bridging the Gap Between Human
and Machine Translation », arXiv preprint arXiv:1609.08144 (2016) : https://homl.info/wordpiece.
346 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
8.2.1 Masquage
Avec Keras, il est très simple de demander au modèle d’ignorer les tokens de remplis-
sage: il suft d’ajouter mask_zero=True au moment de la création de la couche
Embedding. Cela signie que les caractères de remplissage (ceux dont l’identiant
vaut 0) seront ignorés par toutes les couches en aval. C’est tout! Si vous réentraînez
le modèle précédent durant quelques époques, vous vous apercevrez que l’exactitude
de validation dépasse rapidement 80 %.
La couche Embedding crée un tenseur de masque égal à tf.math.not_
equal(inputs, 0) (où K = keras.backend). Il s’agit d’un tenseur booléen
ayant la même forme que les entrées et valant False partout où les identiants de
tokens sont égaux à 0, sinon True. Ce tenseur de masque est ensuite propagé auto-
matiquement par le modèle à la couche suivante. Si la méthode call() de la couche
suivante possède un argument mask, alors elle reçoit automatiquement le masque.
Ceci permet à la couche d’ignorer les étapes temporelles appropriées. Chaque couche
peut faire un usage différent du masque, mais, en général, elles ignorent simplement
les étapes masquées (autrement dit, celles pour lesquelles le masque vaut False).
8.2 Analyse d’opinion 347
Par exemple, lorsqu’une couche récurrente rencontre une étape masquée, elle copie
simplement la sortie de l’étape précédente.
Ensuite, si l’attribut supports_masking de la couche a la valeur True, le
masque est automatiquement transmis à la couche suivante. Il continue à se pro-
pager de cette manière tant que la conguration des couches comporte supports_
masking=True. À titre d’exemple, l’attribut supports_masking d’une
couche récurrente a la valeur True si return_sequences=True, et False
si return_sequences=False, étant donné qu’il n’y a plus besoin de masque
dans ce cas. Par conséquent, si votre modèle comporte plusieurs couches récurrentes
avec return_sequences=True suivies d’une couche récurrente avec return_
sequences=False, alors le masque se propagera automatiquement jusqu’à la der-
nière couche: celle-ci utilisera le masque pour ignorer les étapes masquées, mais ne le
transmettra pas plus loin. De manière similaire, si vous spéciez mask_zero=True
lors de la création de la couche Embedding du modèle d’analyse d’opinion que nous
venons de créer, alors la couche GRU recevra et utilisera automatiquement le masque,
mais ne le propagera pas plus loin, étant donné que return_sequences n’a pas
la valeur True.
Les couches LSTM et GRU ont une implémentation optimisée pour les pro-
cesseurs graphiques, fondée sur la bibliothèque cuDNN de Nvidia. Mais
cette implémentation ne permet le masquage que si tous les tokens de
remplissage sont à la fin de la séquence. Ceci vous oblige également à
utiliser la valeur par défaut de plusieurs hyperparamètres : activation,
recurrent_activation, recurrent_dropout, unroll, use_
bias et reset_after. Si ce n’est pas le cas, on en revient pour ces
couches à l’implémentation GPU par défaut (beaucoup plus lente).
et, évidemment, faire en sorte que cette méthode utilise le masque. Par ailleurs, si
le masque doit être transmis aux couches suivantes, vous devez spécier self.
supports_masking = True dans le constructeur. Si ce masque doit être
modié avant d’être transmis, vous devez implémenter la méthode compute_
mask().
Si votre modèle ne commence pas par une couche Embedding, vous avez la
possibilité d’utiliser à la place la couche keras.layers.Masking. Elle xe par
défaut le masque à tf.math.reduce_any(tf.math.not_equal(X, 0),
axis=-1), signiant par là que les étapes dont la dernière dimension est remplie de
zéros seront masquées dans les couches suivantes.
Les couches de masquage et la propagation automatique du masque fonctionnent
bien pour les modèles simples. Ce ne sera pas toujours le cas pour des modèles plus
complexes, par exemple lorsque vous devez associer des couches Conv1D et des
couches récurrentes. Dans de telles congurations, vous devrez calculer explicite-
ment le masque et le passer aux couches appropriées, en utilisant l’API fonctionnelle
ou l’API de sous-classement. Par exemple, le modèle suivant est équivalent au pré-
cédent, excepté qu’il se fonde sur l’API fonctionnelle et gère le masquage manuelle-
ment. Il ajoute également un peu de régularisation par abandon, étant donné que le
modèle précédent surajustait légèrement:
inputs = tf.keras.layers.Input(shape=[], dtype=tf.string)
token_ids = text_vec_layer(inputs)
mask = tf.math.not_equal(token_ids, 0)
Z = tf.keras.layers.Embedding(vocab_size, embed_size)(token_ids)
Z = tf.keras.layers.GRU(128, dropout=0.2)(Z, mask=mask)
outputs = tf.keras.layers.Dense(1, activation="sigmoid")(Z)
model = tf.keras.Model(inputs=[inputs], outputs=[outputs])
199. Les tenseurs irréguliers ont été introduits au chapitre4 et sont présentés de manière plus détaillée à
l’annexeD.
8.2 Analyse d’opinion 349
Les couches récurrentes de Keras gérant en mode natif les tenseurs irréguliers, vous
n’avez rien d’autre à faire : utilisez simplement cette couche TextVectorization
dans votre modèle. Pas besoin de spécier mask_zero=True ni de gérer explici-
tement des masques: tout est implémenté pour vous. C’est pratique ! Toutefois, n
2023, la gestion par Keras des tenseurs irréguliers n’était pas encore parfaite. Ainsi,
lorsqu’on exécutait le code sur un GPU, il n’était pas possible d’utiliser des tenseurs
irréguliers en tant que cibles avec les fonctions de coût standard (mais le problème
aura peut-être été résolu lorsque vous lirez ces lignes).
Quelle que soit la méthode de masquage choisie, après avoir entraîné le modèle durant
quelques époques, celui-ci parvient à juger à peu près correctement si une critique est
positive ou non. À l’aide d’un rappel tf.keras.callbacks.TensorBoard(),
vous pouvez visualiser les plongements dans TensorBoard, au fur et à mesure de leur
apprentissage. Le regroupement progressif des mots comme «awesome » et « amazing »
d’un côté de l’espace des plongements et celui des mots comme «awful » et « terrible »
de l’autre côté est un spectacle fascinant. Certains mots ne sont pas aussi positifs que
vous pourriez le croire (tout au moins avec ce modèle). C’est par exemple le cas de l’ad-
jectif «good », probablement parce que de nombreuses critiques négatives contiennent
l’expression « not good ».
and wrong » (vrai et faux) même si sa signication est très différente dans chacun
des cas. Pour dépasser cette limitation, Matthew Peters a proposé dans une publica-
tion de 2018200 des plongements à partir de modèles linguistiques (en anglais, embeddings
from language models, ou ELMo) :il s’agit de plongements de mots contextualisés appris
à partir des états internes d’un modèle linguistique bidirectionnel profond. Au lieu
d’utiliser seulement des plongements préentraînés dans votre modèle, vous réutilisez
une partie d’un modèle linguistique préentraîné.
À peu près au même moment, un article de Jeremy Howard et Sebastian Ruder inti-
tulé «Universal Language Model Fine-Tuning for Text Classication » (ULMFiT)201 a
démontré l’efcacité d’un entraînement non supervisé pour le traitement du langage
naturel : les auteurs ont entraîné un modèle linguistique LSTM sur un corpus linguis-
tique de très grande taille en utilisant un apprentissage auto-supervisé (c’est-à-dire
en générant automatiquement les étiquettes à partir des données), puis l’ont ajusté
nement sur différentes tâches. Leur modèle a fait largement mieux que les meilleurs
modèles de l’époque, sur six tâches de classication de textes, en réduisant de 18 à 24 %
le taux d’erreur dans la plupart des cas. De plus, les auteurs ont montré qu’un modèle
préentraîné ajusté nement sur une centaine d’exemples étiquetés seulement pou-
vait être aussi performant qu’un modèle entraîné à partir de rien sur 10000exemples.
Avant ULMFit, l’utilisation de modèles préentraînés n’était la norme que dans le
domaine de la vision par ordinateur ; dans le contexte du traitement du langage
naturel, le préentraînement se limitait aux plongements de mots. Cette publication a
donc marqué le début d’une nouvelle époque dans le traitement du langage naturel :
aujourd’hui, réutiliser des modèles linguistiques préentraînés est devenu la norme.
Construisons par exemple un classicateur basé sur l’encodeur de phrase universel
(ou Universal Sentence Encoder), un modèle d’architecture présenté en 2018 par
une équipe de chercheurs de Google202. Ce modèle est basé sur l’architecture de
transformeur, que nous examinerons dans la suite de ce chapitre. Ce modèle est dis-
ponible sur la plateforme TensorFlow Hub, ce qui est pratique:
import os
import tensorflow_hub as hub
os.environ["TFHUB_CACHE_DIR"] = "my_tfhub_cache"
model = tf.keras.Sequential([
hub.KerasLayer("https://tfhub.dev/google/universal-sentence-encoder/4",
trainable=True, dtype=tf.string, input_shape=[]),
tf.keras.layers.Dense(64, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid")
])
200. Matthew Peters et al., « Deep Contextualized Word Representations », Proceedings of the 2018 Confe-
rence of the North American Chapter of the Association for Computational Linguistics: Human Language Techno -
logies1 (2018), 2227-2237 : https://homl.info/elmo.
201. Jeremy Howard et Sebastian Ruder, « Universal Language Model Fine-Tuning for Text Classi-
cation », Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics 1 (2018),
328-339 : https://homl.info/ulmt.
202. Daniel Cer et al., « Universal Sentence Encoder », arXiv preprint arXiv:1803.11175 (2018) : https://
homl.info/139.
8.3 Un réseau encodeur-décodeur pour la traduction automatique neuronale 351
model.compile(loss="binary_crossentropy", optimizer="nadam",
metrics=["accuracy"])
model.fit(train_set, validation_data=valid_set, epochs=10)
Ce modèle est plutôt volumineux (presque 1 Go) et peut être long à télé-
charger. Par défaut, les modules de TensorFlow Hub sont sauvegardés dans
un répertoire temporaire, ce qui fait qu’ils sont rechargés à chaque fois
que vous exécutez votre programme. Pour éviter cela, vous devez spéci-
fier dans la variable d’environnement TFHUB_CACHE_DIR un répertoire
dans lequel les modules seront sauvegardés, de sorte qu’ils ne seront télé-
chargés qu’une fois.
Notez que la dernière partie de l’URL du module à récupérer spécie que nous
voulons la version4 du modèle. La spécication de version garantit que si une nou-
velle version du modèle est publiée sur la plateforme TensorFlow, elle ne viendra pas
perturber notre modèle. Si vous saisissez simplement cette URL dans un navigateur
web, vous obtiendrez la documentation de ce module, ce qui est commode.
Notez aussi que nous spécions trainable=True lors de la création de hub.
KerasLayer. De cette façon, l’encodeur de phrase universel préentraîné est ajusté
nement durant l’entraînement: certains de ses poids sont ajustés grâce à la rétro-
propagation. Tous les modules de TensorFlow Hub ne sont pas ajustables nement,
vériez donc la documentation pour chacun des modules préentraînés qui vous inté-
ressent.
Après l’entraînement, ce modèle devrait atteindre une exactitude supérieure à
90 % sur le jeu de de validation. C’est en fait vraiment bon : si vous essayez d’effectuer
la tâche vous-même, vous ne ferez probablement pas beaucoup mieux étant donné
que la plupart des critiques comportent à la fois des commentaires positifs et négatifs.
Classer ces critiques ambiguës équivaut à jouer à pile ou face.
Jusqu’ici nous nous sommes intéressés à la génération de texte à l’aide d’un réseau
de neurones récurrent à base de caractères, ou char-RNN, puis à l’analyse d’opinion
à l’aide de réseaux de neurones récurrents à base de mots (en nous appuyant sur des
plongements entraînables) ainsi qu’à l’aide d’un puissant modèle préentraîné de
TensorFlow Hub. Dans la section qui suit, nous étudierons une autre tâche d’impor-
tance en traitement du langage naturel: la traduction automatique neuronale (neural
machine translation, ou NMT).
203. Ilya Sutskever et al., « Sequence to Sequence Learning with Neural Networks », arXiv preprint
(2014) : https://homl.info/103.
352 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Plongement Plongement
3 36 854 2 14 61 10 663
Voici en bref son architecture : des phrases en anglais sont fournies en entrée à
l’encodeur, et le décodeur renvoie en sortie des traductions en espagnol. Notez que
les traductions en espagnol sont aussi utilisées en tant qu’entrées du décodeur durant
l’entraînement, mais décalées d’une étape en arrière. En d’autres termes, durant l’en-
traînement le décodeur reçoit en entrée le mot qu’il devrait avoir produit en sortie à
l’étape précédente, sans tenir compte de ce qu’il a produit en réalité. Cette technique
du teacher forcing (littéralement, « gavage par le professeur ») permet d’accélérer signi-
cativement l’apprentissage et améliore les performances du modèle. Pour le tout
premier mot, le décodeur reçoit le token de début de séquence (start-of-sequence, ou
SOS), et on s’attend à ce que le décodeur termine la phrase par un token de n de
séquence (end-of-sequence, ou EOS).
Chaque mot est représenté initialement par son identiant, par exemple 854 pour
le mot « soccer ». Ensuite, une couche Embedding renvoie le plongement du mot.
Ces plongements de mots vont alors alimenter l’encodeur et le décodeur.
À chaque étape, le décodeur renvoie en sortie un score pour chaque mot du
vocabulaire de sortie (espagnol, ici), puis la fonction d’activation softmax trans-
forme ces scores en probabilités. Ainsi, à la première étape le mot « Me » peut
avoir une probabilité de 7 %, « Yo » peut avoir une probabilité de 1 %, etc. Le
mot renvoyé en sortie est celui ayant la plus forte probabilité. Ceci est tout à fait
analogue à une tâche de classication normale, et vous pouvez entraîner le modèle
8.3 Un réseau encodeur-décodeur pour la traduction automatique neuronale 353
<sos>
Figure 8.4 – Lors de l’inférence, le décodeur reçoit en entrée le mot qu’il a fourni en sortie
à l’étape précédente
Dans une publication de 2015 204 , Samy Bengio a proposé de modifier l’ali-
mentation du décodeur au cours de l’entraînement, de manière à passer
graduellement du token cible précédent au token de sortie précédent.
204. Samy Bengio et al., « Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks »,
arXiv preprint arXiv:1506.03099 (2015) : https://homl.info/scheduledsampling.
205. Ce jeu de données est composé de paires de phrases créées par les contributeurs du projet Tatoeba
(https://tatoeba.org). Les créateurs du site web https://manythings.org/anki ont sélectionné environ
120 000paires de phrases. Ce jeu de données est proposé sous licence Creative Commons Attribution 2.0
France. D’autres paires de langues sont également disponibles.
354 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
nous parcourons le texte pour récupérer toutes les paires de phrases et les mélan-
geons. Enn, nous les partageons en deux listes séparées, une par langue:
import numpy as np
(seulement pour le vocabulaire espagnol), puis les véritables mots, triés par fréquence
décroissante :
>>> text_vec_layer_en.get_vocabulary()[:10]
['', '[UNK]', 'the', 'i', 'to', 'you', 'tom', 'a', 'is', 'he']
>>> text_vec_layer_es.get_vocabulary()[:10]
['', '[UNK]', 'startofseq', 'endofseq', 'de', 'que', 'a', 'no', 'tom', 'la']
Pour que les choses restent simples, nous n’avons utilisé qu’une seule couche LSTM,
mais vous pourriez en empiler plusieurs. Nous avons aussi spécié return_state=
True pour obtenir une référence vers l’état nal de la couche. Étant donné que
nous utilisons une couche LSTM, il y a en fait deux états: l’état à court terme et
l’état à long terme. La couche renvoie ces deux états séparément, c’est pourquoi nous
avons dû écrire *encoder_state pour grouper les deux états dans une liste206.
Maintenant, nous pouvons utiliser ce (double) état comme état initial du décodeur :
decoder = tf.keras.layers.LSTM(512, return_sequences=True)
decoder_outputs = decoder(decoder_embeddings, initial_state=encoder_state)
Ensuite, nous pouvons transmettre les sorties du décodeur à une couche Dense
avec une fonction d’activation softmax pour obtenir les probabilités des mots à
chaque étape :
output_layer = tf.keras.layers.Dense(vocab_size, activation="softmax")
Y_proba = output_layer(decoder_outputs)
206. En Python, si vous exécutez a, *b = [1, 2, 3, 4], alors a vaut 1 et b vaut [2, 3, 4].
207. Sébastien Jean et al., « On Using Very Large Target Vocabulary for Neural Machine Translation »,
Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th Internatio-
nal Joint Conference on Natural Language Processing of the Asian Federation of Natural Language Processing 1
(2015), 1-10 : https://homl.info/104.
8.3 Un réseau encodeur-décodeur pour la traduction automatique neuronale 357
Hourra, ça marche ! À vrai dire, cela fonctionne pour des phrases très courtes. Si
vous jouez avec ce modèle pendant un certain temps, vous vous apercevrez qu’il n’est
pas encore bilingue, et qu’il a en particulier de réelles difcultés lorsque les phrases
sont plus longues. En voici un exemple :
>>> translate("I like soccer and also going to the beach")
'me gusta el fútbol y a veces mismo al bus'
La traduction dit : « J’aime le football et parfois même le bus ». Mais alors, com-
ment l’améliorer? Vous pourriez augmenter la taille du jeu d’entraînement et ajouter
davantage de couches LSTM tant dans l’encodeur que dans le décodeur, mais cela ne
réglerait pas totalement le problème. Voyons donc plutôt des techniques plus sophis-
tiquées, à commencer par les couches récurrentes bidirectionnelles.
358 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Il n’y a qu’un seul problème, cette couche va maintenant renvoyer quatre états
au lieu de deux : l’état nal à court terme et l’état nal à long terme de la couche
LSTM vers l’avant, ainsi que l’état nal à court terme et l’état nal à long terme de la
couche LSTM vers l’arrière. Nous ne pouvons pas utiliser ce quadruple état directe-
ment en tant qu’état initial de la couche LSTM du décodeur, étant donné que cette
couche s’attend à recevoir uniquement deux états (court terme et long terme). Nous
8.3 Un réseau encodeur-décodeur pour la traduction automatique neuronale 359
ne pouvons pas rendre le décodeur bidirectionnel, car il doit rester causal : sinon il
tricherait durant l’entraînement et ne fonctionnerait pas. Au lieu de cela, nous pou-
vons concaténer les deux états à court terme, et concaténer également les deux états
à long terme:
encoder_outputs, *encoder_state = encoder(encoder_embeddings)
encoder_state = [tf.concat(encoder_state[::2], axis=-1), # court terme (0 et 2)
tf.concat(encoder_state[1::2], axis=-1)] # long terme (1 et 3)
Voyons maintenant une autre technique assez en vogue qui peut grandement amé-
liorer les performances d’un modèle de traduction lors de l’inférence: la recherche en
faisceau.
Grâce à tout cela, vous pouvez obtenir d’assez bonnes traductions lorsque les
phrases sont relativement courtes. Malheureusement, ce modèle se révélera vraiment
mauvais pour la traduction de longues phrases. Une fois encore, le problème vient
de la mémoire à court terme limitée des RNN. La solution à ce problème vient des
mécanismes d’attention, une innovation qui a changé la donne.
La gure 8.7 présente notre modèle encodeur-décodeur après ajout d’un méca-
nisme d’attention. En partie gauche se trouvent l’encodeur et le décodeur. Au lieu
d’envoyer à chaque étape au décodeur uniquement l’état caché nal de l’encodeur
et le mot cible précédent (cette transmission est toujours présente mais elle n’est
pas représentée sur la gure), nous lui envoyons aussi à présent toutes les sorties de
l’encodeur. Le décodeur ne pouvant traiter toutes les entrées de l’encodeur simul-
tanément, celles-ci doivent être agrégées : à chaque étape temporelle, la cellule de
mémoire du décodeur calcule une somme pondérée de toutes les sorties de l’enco-
deur. Cela permet de déterminer les mots sur lesquels il va se concentrer lors de cette
étape.
209. Dzmitry Bahdanau et al., « Neural Machine Translation by Jointly Learning to Align and Translate »
(2014) : https://homl.info/attention.
362 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
� + � Softmax
Décodeur (3,0) (3,2)
�(3,1)
e(3,0) e (3,1) e(3,2)
ŷ (0) ŷ (1) ŷ (2)
Dense
h(2)
y (0)y(1) y(2)
x (0) x(1) x(2) Modèle d’alignement
Encodeur (ou couche d’attention)
Le poids α(t,i) est celui de la i ème sortie de l’encodeur pour l’étape temporelle t du
décodeur. Par exemple, si le poids α(3,2) est supérieur aux poids α(3,0) et α (3,1), alors
le décodeur prêtera une attention plus élevée à la sortie de l’encodeur pour le mot
numéro2 (« soccer ») qu’aux deux autres sorties, tout au moins lors de cette étape
temporelle. Les autres parties du décodeur fonctionnent comme précédemment.
Àchaque étape temporelle, la cellule de mémoire reçoit les entrées dont nous venons
de parler, plus l’état caché de l’étape temporelle précédente et (même si cela n’est pas
représenté sur la gure) le mot cible de l’étape temporelle précédente (ou, pour les
inférences, la sortie de l’étape temporelle précédente).
Mais d’où proviennent ces poids α(t,i) ? Ils sont générés par un petit réseau de neu-
rones appelé modèle d’alignement210 (ou couche d’attention), qui est entraîné conjoin-
tement aux autres parties du modèle encodeur-décodeur. Ce modèle d’alignement
est représenté en partie droite de la gure8.7. Il commence par une couche Dense
comportant un seul neurone qui traite chacune des sorties de l’encodeur, ainsi que
l’état caché précédent du décodeur (par exemple, h(2)). Cette couche produit un score
(ou énergie) pour chaque sortie de l’encodeur (par exemple, e(3,2) ) : ce score mesure
l’alignement de cette sortie avec l’état caché précédent du décodeur. Ainsi, sur la
gure8.7, le modèle a déjà produit en sortie « me gusta el » (signiant « j’aime le »),
210. Le terme « alignement » (traduction littérale de l’anglais « alignment ») n’en rend pas bien le sens
initial, qui est plutôt celui d’une mise en adéquation. Mais cette traduction littérale est largement utilisée.
8.4 Mécanismes d’attention 363
ce qui fait qu’il attend désormais unnom : le mot « soccer » est celui qui s’aligne le
mieux sur l’état actuel, donc il obtient un très bon score. Enn, tous les scores passent
par une couche softmax an d’obtenir un poids nal pour chaque sortie de l’encodeur
(par exemple, α (3,2) ).
Pour une étape temporelle donnée du décodeur, la somme de tous les poids est
égaleà1. Ce mécanisme d’attention spécique est appelé attention de Bahdanau (du
nom du premier auteur de l’article de 2014). Puisqu’il concatène la sortie de l’en-
codeur avec l’état caché précédent du décodeur, il est parfois appelé attention par
concaténation (ou attention additive).
211. Minh-Thang Luong et al., « Effective Approaches to Attention-Based Neural Machine Translation »,
Proceedings of the 2015 Conference on Empirical Methods in Natural Language Processing (2015), 1412-1421 :
https://homl.info/luongattention.
364 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
avec α ( , )
produit scalaire
et e (t ,i ) général
; concaténation
Ensuite, nous devons créer la couche d’attention et lui transmettre les états du
décodeur et les sorties de l’encodeur. Mais pour accéder aux états de l’encodeur à
chaque étape, il nous faudrait écrire dans une cellule mémoire spécique. Pour sim-
plier, utilisons les sorties du décodeur au lieu de ses états : en pratique, ceci fonc-
tionne bien également, et c’est beaucoup plus simple à coder. Nous nous contentons
de transmettre les sorties de la couche d’attention directement à la couche de sortie,
comme le suggère l’article sur l’attention de Luong:
attention_layer = tf.keras.layers.Attention()
attention_outputs = attention_layer([decoder_outputs, encoder_outputs])
output_layer = tf.keras.layers.Dense(vocab_size, activation="softmax")
Y_proba = output_layer(attention_outputs)
Et c’est tout ! Si vous entraînez ce modèle, vous verrez qu’il sait maintenant gérer
des phrases beaucoup plus longues. Par exemple :
>>> translate("I like soccer and also going to the beach")
'me gusta el fútbol y también ir a la playa'
212. Ashish Vaswani et al., « Attention Is All You Need », Proceedings of the 31st International Conference
on Neural Information Processing Systems (2017), 6000-6010 : https://homl.info/transformer.
213. Puisqu’un transformeur utilise des couches Dense distribuées temporellement, vous pouvez défendre
le fait que cette architecture utilise des couches convolutives à une dimension avec un noyau de taille1.
366 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
au problème d’instabilité des gradients que les modèles récurrents, il nécessite moins
d’étapes d’entraînement, il est plus facile à paralléliser entre plusieurs GPU et il repère
plus facilement les motifs très étendus que ces modèles récurrents. L’architecture de
transformeur originellement proposée en 2017 est représentée sur la gure8.8.
Probabilités
en sortie
Softmax
Linéaire
Encodage Encodage
positionnel positionnel
Plongement Plongement
des entrées des sorties
En bref, la partie gauche de la gure8.8 est l’encodeur, tandis que la partie droite
représente le décodeur. Chaque couche de plongement produit en sortie un tenseur
3D de forme [taille du lot, longueur de séquence, dimension de plongement]. Après quoi,
les tenseurs sont graduellement transformés lors de leur passage à travers le transfor-
meur, mais leur forme reste la même.
214. Il s’agit de la gure1 de l’article « Attention is all you need », reproduite avec l’aimable autorisation
des auteurs et adaptée pour la version française.
8.5 De l’attention suffit : l’architecture de transformeur 367
traduire une phrase en examinant les mots totalement séparément ? En fait, nous
ne le pouvons pas, et c’est donc là qu’apparaissent les nouveaux composants :
– La couche d’attention à plusieurs têtes (en anglais, multi-head attention) de
l’encodeur met à jour chaque représentation de mot en tenant compte
detous les autres mots de la même phrase. C’est ici que la représentation
assez vague du mot « like » devient une représentation plus riche et plus
exacte, en capturant son sens précis dans la phrase donnée. Nous en verrons
bientôt le fonctionnement.
– La couche d’attention à plusieurs têtes masquées (en anglais, masked multi-
head attention) du décodeur fait la même chose, mais lorsqu’elle traite un
mot, elle ne s’occupe pas des mots situés derrière lui : c’est une couche
causale. Lorsqu’elle traite par exemple le mot « gusta », elle n’examine que
les mots « SOS> me gusta » et ignore les mots « el fútbol » (car sinon, ce
serait tricher).
– C’est dans sa couche multi-head attention supérieure que le décodeur examine
les mots de la phrase anglaise. Dans ce cas, on parle d’attention croisée (en
anglais, cross-attention), et non d’auto-attention (en anglais, self-attention).
Ainsi, le décodeur va probablement faire attention au mot « soccer » lorsqu’il
traitera le mot « el » et transformer sa représentation en une représentation
du mot « fútbol ».
– Les encodages positionnels sont des vecteurs denses (très comparables aux
plongements de mots) qui représentent la position de chacun des mots
dans la phrase. Le nième encodage positionnel est ajouté au plongement de
mot du nième mot de la phrase. Ceci est nécessaire étant donné que toutes
les couches dans l’architecture du transformeur ignorent les positions des
mots : sans encodages positionnels, vous pourriez mélanger les séquences
d’entrée et il se contenterait de mélanger les séquences de sortie de la même
manière. Il est clair que l’ordre des mots a de l’importance, c’est pourquoi
nous devons transmettre d’une manière ou d’une autre au transformeur
l’information concernant les positions : ajouter des encodages positionnels
aux représentations des mots constitue une bonne manière de le faire.
Remarquez que cette implémentation suppose que les plongements sont repré-
sentés comme des tenseurs ordinaires, et non des tenseurs irréguliers (ou ragged
tensors) 215. L’encodeur et le décodeur partagent la même couche Embedding pour
les encodages positionnels, étant donné qu’ils ont la même taille de plongement
(c’est souvent lecas).
Au lieu d’utiliser des encodages positionnels entraînables, les auteurs de l’article
sur les transformeurs ont choisi d’utiliser des encodages positionnels xes, en se
basant sur les fonctions sinus et cosinus à différentes fréquences. La matrice d’enco-
dage positionnel P est dénie par l’équation8.2 et représentée (transposée) dans la
partie inférieure de la gure8.9; P p,i est la ième composante de l’encodage du mot situé
en pième position dans la phrase.
215. Il est toutefois possible de les remplacer par des tenseurs irréguliers si vous utilisez la version la plus
récente de TensorFlow.
370 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
1.0
p = 22
0.5
P(p, i) p = 60
0.0 p = 35
i = 100
–0.5 i = 101
–1.0
150
125
100
i
75
50
25
0
0 25 50 75 100 125 150 175 200
p
Cette solution peut donner d’aussi bons résultats que les encodages positionnels
entraînables, et elle peut s’étendre à des phrases de longueur arbitraire sans ajouter
aucun paramètre au modèle ; cependant, lorsqu’il y a une grande quantité de données
préentraînées, on préfère en général les encodages positionnels entraînables. Une
fois les encodages positionnels ajoutés aux plongements de mots, le reste du modèle
a accès à la position absolue de chaque mot dans la phrase, étant donné qu’il y a
un encodage positionnel unique pour chaque position (p. ex. l’encodage positionnel
pour le mot situé à la 22eposition dans la phrase est représenté par la ligne verticale
pointillée en haut à gauche de la gure8.9 et vous pouvez voir qu’il ne correspond à
aucune autre position). De plus, le choix de fonctions périodiques (sinus et cosinus)
permet au modèle d’apprendre également les positions relatives. Ainsi, des mots
situés à 38 mots d’intervalle (p.ex. aux positions p=22 et p=60) ont la même valeur
de plongement positionnel pour les composantes i=100 et i=101 du plongement.
Voilà pourquoi nous avons besoin du sinus et du cosinus pour chaque fréquence : si
nous avions utilisé uniquement le sinus (la courbe à i=100), le modèle n’aurait pas
su distinguer les emplacements p=25 et p=35 (signalés par une croix).
TensorFlow ne propose aucune couche PositionalEncoding, mais sa
création n’a rien de compliqué. Pour une question d’efcacité, nous précalculons
la matrice des encodages positionnels dans le constructeur. La méthode call()
tronque cette matrice des encodages à la taille maximum des séquences d’entrée et
l’ajoute aux entrées. Nous spécions aussi supports_masking=True pour pro-
pager le masquage automatique des entrées à la couche suivante :
class PositionalEncoding(tf.keras.layers.Layer):
def __init__(self, max_length, embed_size, dtype=tf.float32, **kwargs):
super().__init__(dtype=dtype, **kwargs)
8.5 De l’attention suffit : l’architecture de transformeur 371
Utilisons cette couche pour ajouter un encodage positionnel aux entrées de l’en-
codeur :
pos_embed_layer = PositionalEncoding(max_length, embed_size)
encoder_in = pos_embed_layer(encoder_embeddings)
decoder_in = pos_embed_layer(decoder_embeddings)
QK T
Attention Q, K, V = softmax V
d clés
Dans cette équation :
• Q est une matrice qui contient une ligne par requête. Sa forme est [nrequêtes,
d clés], où n requêtes est le nombre de requêtes, et dclés, le nombre de composantes de
chaque requête et chaque clé.
• K est une matrice qui contient une ligne par clé. Sa forme est [nclés, d clés], où nclés
est le nombre de clés et de valeurs.
• V est une matrice qui contient une ligne par valeur. Sa forme est [nclés, d valeurs],
où dvaleurs est le nombre de composantes de chaque valeur.
• La forme de QKT est [n requêtes, nclés ] : elle contient un score de similitude pour
chaque couple requête/clé. Pour éviter que cette matrice devienne énorme, les
séquences d’entrée ne doivent pas être trop longues (nous verrons un peu plus
loin comment s’affranchir de cette limitation). La sortie de la fonction softmax
a la même forme, mais toutes les lignes ont pour somme 1. La forme de la sortie
nale est [nrequêtes, d valeurs] : nous avons une ligne par requête, où chaque ligne
représente le résultat de la requête (une somme pondérée des valeurs).
372 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
1
• Le facteur réduit les scores de similitude an d’éviter la saturation de la
dclés
fonction softmax, ce qui pourrait conduire à des gradients minuscules.
• Il est possible de masquer certains couples clés/valeur en ajoutant une très grande
valeur négative au score de similitude correspondant, juste avant d’appliquer
la fonction softmax. Cela sera utile dans la couche d’attention à plusieurs têtes
masquées.
Si vous spéciez use_scale=True lors de la création d’une couche tf.
keras.layers.Attention, alors il y a création d’un paramètre supplémentaire
permettant à la couche d’apprendre à réduire correctement les scores de similitude.
L’attention à produit scalaire réduit utilisée dans le modèle de transformeur est à peu
près identique, à ceci près qu’elle réduit toujours les scores de similitude d’un même
1
facteur .
dclés
Remarquez que les entrées de la couche Attention sont semblables à Q, K et
V, sauf qu’elles possèdent une dimension de lot supplémentaire (la première dimen-
sion). En interne, la couche calcule tous les scores d’attention pour toutes les phrases
du lot en un seul appel à tf.matmul(queries, keys), ce qui la rend extrê-
mement efcace. Dans TensorFlow, si A et B sont des tenseurs ayant plus de deux
dimensions –par exemple, de forme [2, 3, 4, 5] et [2, 3, 5, 6], respectivement–, alors
tf.matmul(A, B) traitera ces tenseurs comme des tableaux 2×3 où chaque cel-
lule contient une matrice et multipliera les matrices correspondantes: la matrice de
la ligne i et de la colonne j de A sera multipliée par la matrice de la ligne i et de la
colonne j de B. Le produit d’une matrice 4×5 par une matrice 5×6 étant une matrice
4×6, tf.matmul(A, B) retournera alors un tableau de forme [2, 3, 4, 6].
Examinons à présent la couche d’attention à plusieurs têtes. Son architecture est
représentée à la gure8.10.
Vous le constatez, il s’agit simplement de plusieurs couches d’attention à produit
scalaire réduit, chacune précédée d’une transformation linéaire des valeurs, des clés
et des requêtes (autrement dit, une couche Dense distribuée temporellement sans
fonction d’activation). Toutes les sorties sont simplement concaténées et le résultat
passe par une dernière transformation linéaire (de nouveau distribuée temporelle-
ment).
Pour quelles raisons? Quelle est l’idée sous-jacente à cette architecture ? Prenons
le mot « like » dans la phrase « I like soccer ». L’encodeur a été sufsamment intel-
ligent pour encoder le fait qu’il s’agit d’un verbe. Mais la représentation du mot
comprend également son emplacement dans le texte, grâce aux encodages position-
nels, et elle inclut probablement d’autres informations utiles à sa traduction, comme
le fait qu’il est conjugué au présent. En résumé, la représentation d’un mot encode
de nombreuses caractéristiques différentes du mot. Si nous avions utilisé uniquement
une seule couche d’attention à produit scalaire réduit, nous ne pourrions obtenir
toutes ces caractéristiques qu’en une seule requête.
8.5 De l’attention suffit : l’architecture de transformeur 373
Linéaire
Concaténation
Attention
à produit scalaire réduit
216. Il s’agit de la partie droite de la gure2 de l’article, reproduite avec l’aimable autorisation des auteurs
et adaptée pour la version française.
374 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
skip = Z
attn_layer = tf.keras.layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_size, dropout=dropout_rate)
Z = attn_layer(Z, value=Z, attention_mask=encoder_pad_mask)
Z = tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z, skip]))
skip = Z
Z = tf.keras.layers.Dense(n_units, activation="relu")(Z)
Z = tf.keras.layers.Dense(embed_size)(Z)
Z = tf.keras.layers.Dropout(dropout_rate)(Z)
Z = tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z, skip]))
217. La situation aura vraisemblablement évolué quand vous lirez ces lignes ; consultez la demande Keras
n°16248 (https://github.com/keras-team/keras/issues/16248) pour en savoir plus. Lorsque cette demande sera
prise en compte, il ne sera plus nécessaire de spécier l'argument attention_mask, et par conséquent
plus nécessaire de créer encoder_pad_mask.
218. Pour l'instant, Z + skip ne gère pas le masquage automatique, c’est pourquoi il nous a fallu écrire
à la place tf.keras.layers.Add()([Z, skip]). Là encore, les choses auront peut -être changé
lorsque vous lirez ces lignes.
8.5 De l’attention suffit : l’architecture de transformeur 375
Le masque de remplissage est exactement le même que celui créé pour l’encodeur,
sauf qu’il est basé sur les entrées du décodeur plutôt que sur celles de l’encodeur.
Le masque causal est créé à l’aide de la fonction tf.linalg.band_part(), qui
reçoit un tenseur et renvoie une copie dans laquelle toutes les valeurs extérieures à la
bande diagonale spéciée sont mises à zéro. Avec ces arguments, nous obtenons une
matrice carrée de taille batch_max_len_dec (la taille maximale des séquences
d’entrée dans le lot), avec des « 1 » dans le triangle inférieur gauche et des « 0 » dans
le triangle supérieur droit. Si nous utilisons ce masque comme masque d’attention,
nous obtiendrons exactement ce que nous voulons: le premier token de requête ne
sera associé qu’au premier des tokens de valeur, le second seulement aux deux pre-
miers, le troisième seulement aux trois premiers, et ainsi de suite. En d’autres termes,
les tokens de requête ne peuvent être associés à aucune valeur dans le futur.
Construisons maintenant le décodeur :
encoder_outputs = Z # sauvegarde des sorties finales de l’encodeur
Z = decoder_in # le décodeur démarre avec ses propres entrées
for _ in range(N):
skip = Z
attn_layer = tf.keras.layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_size, dropout=dropout_rate)
Z = attn_layer(Z, value=Z, attention_mask=causal_mask & decoder_pad_mask)
Z = tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z, skip]))
skip = Z
attn_layer = tf.keras.layers.MultiHeadAttention(
num_heads=num_heads, key_dim=embed_size, dropout=dropout_rate)
Z = attn_layer(Z, value=encoder_outputs, attention_mask=encoder_pad_mask)
Z = tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z, skip]))
skip = Z
Z = tf.keras.layers.Dense(n_units, activation="relu")(Z)
Z = tf.keras.layers.Dense(embed_size)(Z)
Z = tf.keras.layers.LayerNormalization()(tf.keras.layers.Add()([Z, skip]))
Nous avons presque terminé. Il nous suft d’ajouter la couche de sortie nale, de
créer le modèle, de le compiler et de l’entraîner :
Y_proba = tf.keras.layers.Dense(vocab_size, activation="softmax")(Z)
model = tf.keras.Model(inputs=[encoder_inputs, decoder_inputs],
outputs=[Y_proba])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam",
metrics=["accuracy"])
model.fit((X_train, X_train_dec), Y_train, epochs=10,
validation_data=((X_valid, X_valid_dec), Y_valid))
219. Alec Radford et al., « Improving Language Understanding by Generative Pre-Training » (2018) :
https://homl.info/gpt.
220. Par exemple, la phrase « Jeanne s’est beaucoup amusée à la fête d’anniversaire de son amie » implique
« Jeanne a apprécié la fête » mais est contredite par « Tout le monde a détesté la fête » et n’a aucun rapport
avec « La Terre est plate ».
8.6 Une avalanche de transformeurs 377
des similitudes (par exemple, « Beau temps aujourd’hui » est très similaire à « Il y a
du soleil ») et des réponses aux questions (à partir de quelques paragraphes de texte
établissant un contexte, le modèle doit répondre à des questions à choix multiple).
Après quoi, Google a publié un article sur BERT221 qui démontre également l’ef-
cacité du préentraînement auto-supervisé sur un vaste corpus, en utilisant une archi-
tecture comparable à GPT mais avec uniquement des couches d’attention à plusieurs
têtes non masquées, comme dans l’encodeur du transformeur originel. Cela signie
que le modèle est naturellement bidirectionnel, d’où le B de BERT (bidirectional
encoder representations from transformers). Le plus important est que les auteurs ont
proposé deux tâches de préentraînement qui expliquent en grande partie la force du
modèle :
• Modèle linguistique masqué
Dans un modèle linguistique masqué (en anglais, masked language model, ou
MLM), chaque mot d’une phrase a une probabilité de 15 % d’être masqué et
le modèle est entraîné à prédire les mots masqués. Par exemple, si la phrase
d’origine est « Elle s’est amusée à la fête d’anniversaire », alors le modèle peut
recevoir « Elle <masque> amusée à la <masque> d’anniversaire » et doit prédire
les mots « s’est » et « fête » (les autres sorties seront ignorées). Pour être plus
précis, chaque mot sélectionné a 80 % de chances d’être masqué, 10 % de
chances d’être remplacé par un mot aléatoire (pour réduire la divergence entre
le préentraînement et le réglage n, car le modèle ne verra pas les tokens de
masque pendant le réglage n), et 10 % de chances d’être conservé inchangé
(pour attirer le modèle vers la réponse correcte).
• Prédiction de la phrase suivante
Un modèle de prédiction de la phrase suivante (en anglais, next sentence
prediction, ou NSP), est entraîné à prédire s’il existe ou non un lien logique ou
chronologique entre deux phrases. Par exemple, il doit prédire que les phrases
« Le chien dort » et « Il rone bruyamment » ont un lien logique, contrairement
aux phrases « Le chien dort » et « La Terre tourne autour du Soleil ». Des
études récentes ont montré que NSP n’avait pas autant d’importance qu’on
l’avait pensé initialement, ce qui a conduit à l’abandonner dans la plupart des
architectures ultérieures.
Le modèle est entraîné simultanément sur ces deux tâches (voir gure 8.11).
Pour la tâche NSP, les auteurs ont inséré un token de classe (<CLS>) au début
de chaque entrée, et le token de sortie correspondant représente la prédiction du
modèle: la phrase B suit la phrase A, ou non. Les deux phrases d’entrée sont concaté-
nées, séparées seulement par un token de séparation spécial (<SEP>), le résultat est
fourni en entrée au modèle. Pour aider le modèle à savoir à quelle phrase appartient
chaque token d’entrée, un plongement de ce segment est ajouté par-dessus le plon-
gement positionnel de chaque token: il y a seulement deux plongements de segment
221. Jacob Devlin et al., « BERT: Pre-training of Deep Bidirectional Transformers for Language Understan-
ding », Proceedings of the 2018 Conference of the North American Chapter of the Association for Computational
Linguistics: Human Language Technologies, 1 (2019) : https://homl.info/bert.
378 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
BERT BERT
E[CLS] E1 … EN E[SEP] E1‘ … EM ‘ E[CLS] E1 … EN E[SEP] E1 ‘ … EM‘
[CLS] Tok 1 … Tok N [SEP] Tok 1 … TokM [CLS] Tok 1 … Tok N [SEP] Tok 1 … TokM
Après cette phase d’entraînement non supervisé sur un très large corpus de textes,
le modèle est ajusté nement sur de nombreuses tâches différentes, en n’effectuant
que très peu de changements pour chaque tâche. À titre d’exemple, lorsqu’il s’agit
de classication de textes de type analyse d’opinion, tous les tokens de sortie sont
ignorés à l’exception du premier, qui correspond au token de classe, et une nouvelle
couche de sortie remplace la précédente qui n’était qu’une couche de classication
binaire pour NSP.
En février2019, seulement quelques mois après la présentation de BERT, Alec
Radford, Jeffrey Wu et d’autres chercheurs d’OpenAI ont présenté GPT-2 223, une
architecture très similaire à GPT, mais encore plus vaste (avec plus de 1,5milliard de
paramètres !). Ils ont montré que ce nouveau modèle GPT amélioré pouvait effectuer
différentes tâches sans apprentissage spécique préalable (ce qu’ils nomment zero-shot
learning, ou ZSL) et obtenir néanmoins d’excellentes performances sur ces tâches. Ce
fut le début d’une course vers des modèles de plus en plus grands : les transformeurs
à commutation ou switch transformers224, présentés par Google en janvier2021 uti-
lisaient mille milliards de paramètres, et très vite d’autres modèles beaucoup plus
grands ont vu le jour, comme le modèle Wu Dao 2.0 annoncé par l’Académie
222. Il s’agit de la première gure de la publication, reproduite avec l’aimable autorisation des auteurs et
adaptée pour la version française.
223. Alec Radford et al., « Language Models Are Unsupervised Multitask Learners » (2019) : https://homl.
info/gpt2.
224. William Fedus et al., « Switch Transformers: Scaling to Trillion Parameter Models with Simple and
Efcient Sparsity » (2021) : https://homl.info/switch.
8.6 Une avalanche de transformeurs 379
225. Victor Sanh et al., « DistilBERT, A Distilled Version of Bert: Smaller, Faster, Cheaper and Lighter »,
arXiv preprint arXiv:1910.01108 (2019) : https://homl.info/distilbert.
226. Mariya Yao a fait la synthèse d'un grand nombre de ces modèles dans l’article en ligne : https://homl.
info/yaopost.
227. Colin Raffel et al., « Exploring the Limits of Transfer Learning with a Unied Text-to-Text Transfor-
mer », arXiv preprint arXiv:1910.10683 (2019) : https://homl.info/t5.
380 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
228. Aakanksha Chowdhery et al., « PaLM: Scaling Language Modeling with Pathways », arXiv preprint
arXiv:2204.02311 (2022) : https://homl.info/palm.
229. Jason Wei et al., « Chain of Thought Prompting Elicits Reasoning in Large Language Models », arXiv
preprint arXiv:2201.11903 (2022) : https://homl.info/ctp.
8.7 Transformeurs d’images 381
Non seulement le modèle donne la bonne réponse bien plus fréquemment qu’en
utilisant une incitation simple, dans la mesure où il est encouragé à rééchir au pro-
blème posé, mais il fournit aussi toutes les étapes du raisonnement, ce qui peut être
utile pour mieux comprendre la logique sous-jacente à la réponse du modèle.
Les transformeurs ont pris le dessus dans le traitement du langage naturel, mais ils
ne se sont pas arrêtés là: ils ont rapidement étendu leur champ d’action à la vision
par ordinateur également.
Figure 8.12 – Attention visuelle : une image d’entrée (à gauche) et la zone de focalisation
du modèle avant de produire le mot « frisbee » (à droite) 231
230. Kelvin Xu et al., « Show, Attend and Tell: Neural Image Caption Generation with Visual Attention »,
Proceedings of the 32nd International Conference on Machine Learning (2015), 2048-2057 : https://homl.info/
visualattention.
231. Il s’agit d’un extrait de la gure3 de l’article, reproduit avec l’aimable autorisation des auteurs.
382 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Explicabilité
Les mécanismes d’attention ont pour avantage supplémentaire de permettre de
comprendre plus facilement les raisons qui ont conduit un modèle à produire cha-
cune de ses sorties. C’est ce que l’on nomme l’explicabilité. Cela peut se révéler très
utile lorsque le modèle fait une erreur.
Par exemple, si la légende produite pour l’image d’un chien marchant dans la neige
est « un loup marchant dans la neige », vous pouvez revenir en arrière et vérifier ce
sur quoi le modèle s’est focalisé lorsqu’il a généré le mot « loup ». Vous constaterez
peut-être qu’il prêtait attention non seulement au chien mais également à la neige,
essayant de trouver une explication possible : le modèle avait peut-être appris à dis-
tinguer les chiens des loups en vérifiant si l’environnement est enneigé. Pour corriger
ce problème, il suffit d’entraîner le modèle avec d’autres images de loups sans neige
et de chiens avec de la neige. Cet exemple est tiré de l’excellent article232 publié
en 2016 par Marco Tulio Ribeiro et al., dans lequel les auteurs utilisent une autre
approche pour l’explicabilité : apprendre un modèle interprétable localement autour
de la prédiction d’un classificateur.
Dans certaines applications, l’explicabilité n’est pas qu’un simple outil de débogage
d’un modèle. Il peut s’agir d’une exigence légale, comme ce serait le cas par exemple
pour un système qui déciderait de vous accorder ou non un prêt.
Lorsque les transformeurs sont apparus en 2017 et qu’on a commencé à les expéri-
menter au-delà du traitement du langage naturel, ils ont d’abord été utilisés conjoin-
tement aux réseaux de neurones convolutifs (CNN), sans les remplacer. À l’inverse,
les transformeurs ont été généralement utilisés pour remplacer les réseaux de neu-
rones récurrents (RNN), par exemple pour les modèles de description d’images. Les
transformeurs sont devenus légèrement plus orientés images grâce à un article233
publié en 2020 par des chercheurs de Facebook qui ont proposé une architecture
hybride dénommée CNN-transformer pour la détection d’objets. Là encore, le CNN
traite d’abord les images et produit en sortie des cartes de caractéristiques, puis ces
cartes de caractéristiques sont converties en séquences qui vont alimenter un trans-
formeur, qui produit en sortie des prédictions de rectangles d’encadrement. Mais là
encore, l’essentiel du travail visuel reste effectué par le CNN.
Puis en octobre2020, une équipe de chercheurs de Google a publié un article234
présentant un modèle d’analyse d’images basé entièrement sur des transformeurs,
appelé transformeur de vision (en anglais, vision transformer, ou ViT). L’idée est éto-
namment simple: il suft de découper l’image en petits carrés de 16×16 et de traiter
232. Marco Tulio Ribeiro et al., « “Why Should I Trust You?”: Explaining the Predictions of Any Classi-
er », Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and
Data Mining (2016), 1135-1144 : https://homl.info/explainclass.
233. Nicolas Carion et al., « End-to-End Object Detection with Transformers », arXiv preprint
arxiv:2005.12872 (2020) : https://homl.info/detr.
234. Alexey Dosovitskiy et al., « An Image Is Worth 16x16 Words: Transformers for Image Recognition at
Scale », arXiv preprint arxiv:2010.11929 (2020) : https://homl.info/vit.
8.7 Transformeurs d’images 383
Un biais inductif est une supposition implicite faite par le modèle, en raison
de son architecture. Ainsi, les modèles linéaires supposent implicitement
que les données sont... linéaires. Les CNN supposent implicitement que
les motifs appris en un endroit seront vraisemblablement utiles également
à d’autres endroits. Les RNN supposent implicitement que les entrées sont
ordonnées et que les tokens récents sont plus importants que ceux qui
sont plus anciens. Plus le modèle a de biais inductifs, en supposant que
ceux-ci soient corrects, moins il faut de données pour l’entraîner. Mais si les
suppositions implicites se révèlent inexactes, alors le modèle peut fournir de
mauvais résultats même s’il est entraîné sur un jeu de données de grande
taille.
Deux mois plus tard seulement, une équipe de chercheurs de Facebook a publié
un article235 introduisant des transformeurs d’images à gestion efcace des données,
nommés data-efcient image transformers (DeiT). Leur modèle obtenait des résultats
comparables sur ImageNet sans nécessiter de données additionnelles pour l’entraî-
nement. L’architecture du modèle est à peu près la même que celle du transformeur
de vision originel, mais les auteurs ont utilisé une technique de distillation pour
transférer les connaissances acquises par des modèles CNN dans l’état de l’art vers
leur modèle.
Puis en mars2021, DeepMind a publié un important article236 présentant l’archi-
tecture de percepteur (en anglais, perceiver). Il s’agit d’un transformeur multimodal, ce
qui signie que vous pouvez l’alimenter avec du texte, des images, de l’audio ou vir-
tuellement avec toute autre modalité de communication de données. Jusque-là, les
transformeurs étaient réduits à recevoir des séquences relativement courtes, du fait de
leurs performances et du goulot d’étranglement de la RAM dans les couches d’atten-
tion. Ceci excluait certaines modalités telles que l’audio ou la vidéo et cela forçait les
chercheurs à traiter des images comme des séquences de portions d’images plutôt que
235. Hugo Touvron et al., « Training Data-Efcient Image Transformers & Distillation Through Atten-
tion», arXiv preprint arxiv:2012.12877 (2020) : https://homl.info/deit.
236. Andrew Jaegle et al., « Perceiver: General Perception with Iterative Attention », arXiv preprint
arxiv:2103.03206 (2021) : https://homl.info/perceiver.
384 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Dans un article paru en 2021, des chercheurs de Google ont montré239 comment
complexier ou simplier les transformeurs de vision en fonction du volume de don-
nées. Ils ont réussi à créer un gigantesque modèle comportant 2milliards deparamètres
qui a atteint une exactitude top-1 supérieure à 90,4 % sur ImageNet. Àl’inverse, ils
ont aussi entraîné un modèle considérablement réduit crédité d’une exactitude top-1
supérieure à 84,8 % sur ImageNet, en utilisant uniquement 10 000 images : cela ne
fait que 10 images par classe !
Les avancées en matière de transformeurs d’images se sont poursuivies à un rythme
soutenu jusqu’à présent. Ainsi, en mars 2022, un article240 de Mitchell Wortsman
etal. a prouvé qu’il était possible de commencer par entraîner plusieurs transformeurs,
puis de prendre la moyenne de leurs poids pour créer un modèle nouveau et amélioré.
C’est semblable à un ensemble241, à ceci près qu’il n’y a qu’un modèle au nal, ce qui
signie qu’il n’y a pas de pénalité en ce qui concerne le temps d’inférence.
La dernière tendance en matière de transformeurs consiste à construire de grands
modèles multimodaux, souvent capables d’apprentissage sans exemples (zero-shot lear-
ning) ou avec très peu d’exemples seulement (few-shot learning). Ainsi, l’article sur
CLIP242 publié en 2021 par OpenAI a proposé un grand modèle de transformeur
préentraîné pour associer des descriptions à des images : cette tâche lui permet d’ap-
prendre d’excellentes représentations des images, après quoi le modèle peut être uti-
lisé directement pour des tâches telles que la classication d’images en utilisant de
simples descriptions textuelles telles que «photo d’un chat ». Peu après, OpenAI a
annoncé DALL-E243 , capable de générer d’incroyables images à partir de descriptions
textuelles, puis DALL-E2244, qui génère des images d’une qualité encore meilleure en
utilisant un modèle de diffusion (voir chapitre9).
En avril 2022, DeepMind a publié un article sur Flamingo 245 présentant une
famille de modèles préentraînés sur un vaste ensemble de tâches portant sur des don-
nées mélangeant différents types (ou modalités), comme du texte, des images et des
vidéos. Un seul modèle peut être utilisé pour des tâches très diverses, comme fournir
des réponses à des questions, ajouter une description à des images, etc. Peu après,
239. Xiaohua Zhai et al., « Scaling Vision Transformers », arXiv preprint arxiv:2106.04560v1 (2021) :
https://homl.info/scalingvits.
240. Mitchell Wortsman et al., « Model Soups: Averaging Weights of Multiple Fine-tuned Models Impro-
ves Accuracy Without Increasing Inference Time », arXiv preprint arxiv:2203.05482v1 (2022) : https://
homl.info/modelsoups.
241. Voir le chapitre 7 de l’ouvrage Machine Learning avec Scikit-Learn, A. Géron, Dunod (3 e édition,
2023).
242. Alec Radford et al., « Learning Transferable Visual Models From Natural Language Supervision »,
arXiv preprint arxiv:2103.00020 (2021) : https://homl.info/clip.
243. Aditya Ramesh et al., « Zero-Shot Text-to-Image Generation », arXiv preprint arxiv:2102.12092
(2021) : https://homl.info/dalle.
244. Aditya Ramesh et al., « Hierarchical Text-Conditional Image Generation with CLIP Latents », arXiv
preprint arxiv:2204.06125 (2022) : https://homl.info/dalle2.
245. Jean-Baptiste Alayrac et al., « Flamingo: a Visual Language Model for Few-Shot Learning », arXiv
preprint arxiv:2204.14198 (2022) : https://homl.info/amingo.
386 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
Comme vous pouvez le voir, les transformeurs sont partout ! Et la bonne nou-
velle, c’est que vous n’aurez en général pas besoin d’implémenter vous-même des
transformeurs car il existe d’excellents modèles préentraînés prêts à être téléchargés
à partir des plateformes de TensorFlow ou de Hugging Face. Puisque vous avez déjà
vu comment utiliser un modèle récupéré sur la plateforme TensorFlow, nous allons
conclure ce chapitre en examinant rapidement l’écosystème de Hugging Face.
246. Scott Reed et al., « A Generalist Agent », arXiv preprint arxiv:2205.06175 (2022) : https://homl.info/
gato.
8.8 Bibliothèque de transformeurs de Hugging Face 387
Le résultat est une liste Python contenant un dictionnaire par texte d’entrée :
>>> result
[{'label': 'POSITIVE', 'score': 0.9998071789741516}]
Dans cet exemple, le modèle a trouvé correctement que la phrase était positive,
avec un indice de conance d’environ 99,98 %. Bien sûr, vous pouvez aussi trans-
mettre tout un lot de phrases au modèle :
>>> classifier(["I am from India.", "I am from Iraq."])
[{'label': 'POSITIVE', 'score': 0.9896161556243896},
{'label': 'NEGATIVE', 'score': 0.9811071157455444}]
Biais et équité
Comme le résultat obtenu le suggère, ce classificateur particulier adore les Indiens,
mais a un a priori ou biais (en anglais, bias) sérieux à l’égard des Irakiens. Vous
pouvez essayer ce code avec votre propre pays ou ville. Ce genre de comportement
indésirable provient en général en grande partie des données d’entraînement. Dans
ce cas, il y avait un grand nombre de phrases négatives relatives aux guerres d’Irak
dans les données d’entraînement. Ce biais a ensuite été amplifié, étant donné que
le modèle a été forcé de choisir entre deux classes seulement : positive ou négative.
Si vous ajoutez une classe neutre lors de l’ajustement fin, alors l’a priori lié aux pays
disparaît pour l’essentiel. Mais les données d’entraînement ne constituent pas la seule
source de biais : l’architecture du modèle, le type de perte ou de régularisation utilisée
pour l’entraînement, l’optimiseur : tout ceci affecte ce que le modèle apprend finale-
ment. Même un modèle non biaisé pour l’essentiel peut être utilisé d’une manière
biaisée, tout comme les questions d’une enquête peuvent être biaisées.
Si comprendre ce qu’est un biais en IA et limiter ses effets négatifs reste un sujet sur
lequel les chercheurs travaillent encore activement, une chose est certaine: plutôt
que de mettre précipitamment un modèle en production, faites d’abord une pause
et réfléchissez. Demandez-vous comment le modèle pourrait faire du tort, même
indirectement. À titre d’exemple, si les prédictions d’un modèle sont utilisées pour
décider de l’attribution ou non d’un prêt à quelqu’un, le processus doit être équitable.
Assurez-vous donc que vous évaluez les performances du modèle non pas seule-
ment en moyenne sur l’ensemble du jeu de test, mais également sur différents sous-
ensembles : par exemple vous pourriez découvrir que bien que le modèle fonctionne
très bien en moyenne, les résultats sont catastrophiques pour certaines catégories
de personnes. Vous pourriez aussi exécuter des tests contrefactuels, pour vérifier par
exemple si les prédictions du modèle changent lorsque vous modifiez simplement le
sexe de la personne.
Si le modèle fonctionne bien en moyenne, il est tentant de le mettre en production
et de passer à autre chose, surtout s’il ne s’agit que d’un composant dans un système
plus vaste. Mais en général, si vous ne corrigez pas de tels défauts, personne d’autre
ne le fera et votre modèle peut finir par faire plus de mal que de bien. La solution
dépend du problème: il faudra peut-être rééquilibrer le jeu de données, ajuster fine-
ment sur un autre jeu de données, passer à un autre modèle préentraîné, modifier
un peu l’architecture du modèle ou ses hyperparamètres, etc.
388 Chapitre 8. Traitement automatique du langage naturel avec les RNN et les attentions
L’API pipeline est très simple et pratique, mais parfois vous avez besoin de
mieux contrôler les choses. Dans un tel cas, la bibliothèque transformers
fournit de nombreuses classes, parmi lesquelles toutes sortes de modèles, de con-
gurations, de rappels, de générateurs de tokens (ou tokenizers), etc. Chargeons par
exemple le même modèle DistilBERT ainsi que le générateur de tokens correspon-
dant, en utilisant les classes TFAutoModelForSequenceClassification
et AutoTokenizer :
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = TFAutoModelForSequenceClassification.from_pretrained(model_name)
Hugging Face propose également une bibliothèque de jeux de données (ou data-
sets) qui vous permet de télécharger facilement un jeu de données standard (comme
IMDb) ou un jeu adapté à vos besoins et de l’utiliser pour régler nement votre
modèle. C’est analogue aux jeux de données proposés par TensorFlow, mais avec en
prime des outils permettant de réaliser certaines tâches de prétraitement habituelles
comme le masquage. Vous trouverez la liste des jeux de données à l’adresse https://
huggingface.co/datasets.
Ceci devrait vous permettre de faire vos premiers pas dans l’écosystème de Hugging
Face. Pour approfondir, consultez https://huggingface.co/docs où vous trouverez entre
autres de nombreux tutoriels sous forme de notebooks, des vidéos, ainsi que l’API
complète. Je vous recommande aussi de consulter le livre publié par léquipe Hugging
Face chez O’Reilly: Natural Language Processing with Transformers: Building Language
Applications with Hugging Face, de Lewis Tunstall, Leandro von Werra et Thomas
Wolf.
Dans le prochain chapitre, nous verrons comment apprendre des représentations
profondes de manière non supervisée en utilisant des autoencodeurs et nous utili-
serons des réseaux antagonistes génératifs (GAN) pour produire, entre autres, des
images !
8.9 EXERCICES
1. Citez les avantages et les inconvénients de l’utilisation d’un RNN
avec état par rapport à celle d’un RNN sans état.
2. Pourquoi utiliserait-on des RNN de type encodeur-décodeur plutôt
que des RNN purement séquence-vers-séquence pour la traduction
automatique ?
3. Comment prendriez-vous en charge des séquences d’entrée de
longueur variable ? Qu’en est-il des séquences de sortie de longueur
variable ?
4. Qu’est-ce que la recherche en faisceau et pourquoi voudriez-vous
l’utiliser ? Donnez un outil qui permet de la mettre en œuvre.
5. Qu’est-ce qu’un mécanisme d’attention ? En quoi peut-il aider ?
6. Quelle est la couche la plus importante dans l’architecture d’un
transformeur ? Précisez son objectif.
8.9 Exercices 391
Les autoencodeurs sont des réseaux de neurones articiels capables d’apprendre des
représentations denses des données d’entrée, appelées représentations latentes ou
codages, sans aucune supervision (autrement dit, le jeu d’entraînement est dépourvu
d’étiquettes). Ces codages sont généralement de dimension plus faible que les don-
nées d’entrée, d’où l’utilité des autoencodeurs pour la réduction de dimension 247,
notamment dans les applications de traitement d’image. Les autoencodeurs sont éga-
lement des détecteurs de caractéristiques puissants et ils peuvent être utilisés pour
le préentraînement non supervisé de réseaux de neurones profonds (comme nous
l’avons indiqué au chapitre3). Enn, certains autoencodeurs sont des modèles généra-
tifs capables de produire aléatoirement de nouvelles données qui ressemblent énormé-
ment aux données d’entraînement. Par exemple, en entraînant un tel autoencodeur
sur des photos de visages, il sera capable de générer de nouveaux visages.
Les réseaux antagonistes génératifs (en anglais, generative adversial network, ou GAN)
sont aussi des réseaux de neurones capables de générer des données. Ils peuvent
générer des photos de visages si convaincantes qu’il est difcile de croire que les per-
sonnes représentées sont virtuelles. Vous pouvez en juger par vous-même en allant
sur le site https://thispersondoesnotexist.com, qui présente des visages produits par une
architecture GAN récente nommée StyleGAN (vous pouvez également visiter
https://thisrentaldoesnotexist.com pour visualiser quelques chambres Airbnb géné-
rées). Les GAN sont à présent largement utilisés pour augmenter la résolution d’une
image, coloriser (https://github.com/jantic/DeOldify), faire des retouches élaborées (par
exemple, remplacer les éléments indésirables dans une photo par des arrière-plans
247. Voir le chapitre 8 de l’ouvrage Machine Learning avec Scikit-Learn, A. Géron, Dunod (3e édition,
2023).
394 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
permettant de générer de fausses images, mais nous verrons que son entraînement
est souvent assez difcile. Nous expliquerons les principales difcultés de l’entraîne-
ment antagoniste, ainsi que quelques techniques majeures pour les contourner. Enn,
nous construirons et entraînerons un DDPM et l’utiliserons pour générer des images.
Commençons par les autoencodeurs!
248. William G. Chase et Herbert A. Simon, « Perception in Chess », Cognitive Psychology, 4, n°1 (1973),
55-81 : https://homl.info/111.
396 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Sorties x’ 1 x’2 x’ 3
(≈ entrées)
Décodeur
Représentation
latente
Encodeur
Entrées x1 x2 x3
on peut montrer qu’il réalise une analyse en composantes principales (en anglais, prin-
cipal component analysis, ou PCA) 249.
Le code suivant construit un autoencodeur linéaire simple pour effectuer une
PCA sur un jeu de données à trois dimensions, en le projetant sur deux dimensions:
import tensorflow as tf
encoder = tf.keras.Sequential([tf.keras.layers.Dense(2)])
decoder = tf.keras.Sequential([tf.keras.layers.Dense(3)])
autoencoder = tf.keras.Sequential([encoder, decoder])
optimizer = tf.keras.optimizers.SGD(learning_rate=0.5)
autoencoder.compile(loss="mse", optimizer=optimizer)
Ce code n’est pas très différent de celui des perceptrons multicouches que nous
avons construits dans les chapitres précédents. Quelques remarques cependant:
• Nous avons organisé l’autoencodeur en deux sous-composants: l’encodeur et le
décodeur. Tous deux sont des modèles Sequential normaux, chacun avec
une seule couche Dense. L’autoencodeur est un modèle Sequential qui
contient l’encodeur suivi du décodeur (rappelez-vous qu’un modèle peut être
utilisé en tant que couche dans un autre modèle).
• Le nombre de sorties de l’autoencodeur est égal au nombre d’entrées (c’est-à-
dire trois).
• Pour effectuer une PCA simple, nous n’utilisons pas de fonction d’activation
(autrement dit, tous les neurones sont linéaires) et la fonction de coût est la
MSE. Ceci parce que la PCA est linéaire. Nous verrons des autoencodeurs plus
complexes et non linéaires ultérieurement.
Entraînons à présent le modèle sur un jeu de données 3D généré simple 250 et
utilisons-le pour encoder ce jeu de données (c’est-à-dire en faire une projection 2D):
X_train = [...] # générons un jeu de données 3D
history = autoencoder.fit(X_train, X_train, epochs=500, verbose=False)
codings = encoder.predict(X_train)
Notez que le même jeu de données, X_train, est utilisé pour les entrées et pour
les cibles. La gure9.2 montre le jeu de données 3D original (à gauche) et la sortie
de la couche cachée de l’autoencodeur (c’est-à-dire la couche de codage, à droite).
Vous le constatez, l’autoencodeur a trouvé le meilleur plan à deux dimensions sur
lequel projeter les données, tout en conservant autant de variance que possible dans
les données (tout comme l’analyse en composantes principales).
249. Voir le chapitre 8 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3 e édition,
2023).
250. Il s’agit du même jeu de données que celui généré au chapitre8 de l’ouvrage Machine Learning avec
Scikit-Learn, A.Géron, Dunod (3e édition, 2023).
398 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
–0.5 Z2 0.00
–1.0 –0.25
1.0 –0.50
0.5
0.0 –0.75
–1.0
–0.5
0.0 –0.5 x2 –1.5 –1.0 –0.5 0.0 0.5 1.0 1.5
0.5 –1.0
x1 1.0 Z1
251. Voir le chapitre 3 de l’ouvrage Machine Learning avec Scikit-Learn, A. Géron, Dunod (3e édition,
2023).
9.3 Autoencodeurs empilés 399
Reconstructions
784 unités Couche de sortie
stacked_ae.compile(loss="mse", optimizer="nadam")
history = stacked_ae.fit(X_train, X_train, epochs=20,
validation_data=(X_valid, X_valid))
Examinons ce code:
• Comme précédemment, nous divisons le modèle de l’autoencodeur en deux
sous-modèles: l’encodeur et le décodeur.
• L’encodeur reçoit en entrée des images en niveaux de gris de 28×28 pixels,
les aplatit an de représenter chacune sous forme d’un vecteur de taille 784,
puis passe les vecteurs obtenus au travers de deux couches Dense de taille
décroissante (100 puis 30 unités), toutes deux utilisant la fonction d’activation
RELU. Pour chaque image d’entrée, l’encodeur produit un vecteur de taille30.
• Le décodeur prend les codages de taille 30 (générés par l’encodeur) et les passe
au travers de deux couches Dense de taille croissante (100 puis 784 unités). Il
remet les vecteurs naux sous forme de tableaux 28×28 an que ses sorties aient
la même forme que les entrées de l’encodeur.
• Lors de la compilation de l’encodeur empilé, nous utilisons la perte MSE et
l’optimisation Nadam.
400 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
• Enn, nous entraînons le modèle en utilisant X_train à la fois pour les entrées
et les cibles. De façon similaire, nous utilisons X_valid pour les entrées et les
cibles de validation.
plot_reconstructions(stacked_ae)
plt.show()
Figure 9.4 – Les images originales (en haut) et leur reconstruction (en bas)
Les reconstructions sont reconnaissables, mais la perte est un peu trop importante.
Nous pourrions entraîner le modèle plus longtemps, rendre l’encodeur et le décodeur
plus profonds, ou rendre les codages plus grands. Mais si le réseau devient trop puis-
sant, il réalisera des reconstructions parfaites sans découvrir de motifs utiles dans les
données. Pour le moment, conservons ce modèle.
visualisation, les résultats ne sont pas aussi bons que ceux obtenus avec d’autres algo-
rithmes de réduction de dimension252, mais les autoencodeurs ont le grand avantage
de pouvoir traiter des jeux de donnés volumineux, avec de nombreuses instances et
caractéristiques. Une stratégie consiste donc à utiliser un autoencodeur pour réduire
la dimension jusqu’à un niveau raisonnable, puis à employer un autre algorithme de
réduction de dimension pour la visualisation. Mettons en place cette stratégie pour
visualiser le jeu de données Fashion MNIST. Nous commençons par utiliser l’enco-
deur de notre autoencodeur empilé de façon à abaisser la dimension jusqu’à 30, puis
nous nous servons de l’implémentation Scikit-Learn de l’algorithme t-SNE pour la
réduire jusqu’à 2 en vue d’un afchage:
from sklearn.manifold import TSNE
X_valid_compressed = stacked_encoder.predict(X_valid)
tsne = TSNE(init="pca", learning_rate="auto", random_state=42)
X_valid_2D = tsne.fit_transform(X_valid_compressed)
La gure 9.5 montre le nuage de points obtenu, illustré par quelques images.
L’algorithme t-SNE a identié plusieurs groupes, qui correspondent raisonnablement
aux classes (chacune est représentée par une couleur différente 253).
252. Comme ceux décrits au chapitre 8 de l’ouvrage Machine Learning avec Scikit-Learn, A. Géron, Dunod
(3e édition, 2023).
253. Pour la version en couleurs de la gure, voir «17_autoencoders_gans_and_diffusion_models.ipyn»
sur https://homl.info/colab3.
402 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Sortie
Softmax
Couche cachée 3 Sortie
Couche Couche
cachée 2 cachée 2
Copie
des paramètres
Couche cachée 1 Couche cachée 1
Entrée Entrée
Phase 1 Phase 2
Entraîner l’autoencodeur Entraîner le classificateur
avec toutes les données avec les données étiquetées
Ce cas est en réalité assez fréquent, car la construction d’un vaste jeu de
données non étiquetées est souvent peu coûteuse (par exemple, un simple
script est en mesure de télécharger des millions d’images à partir d’Internet),
alors que leur étiquetage fiable ne peut être réalisé que par des humains (par
exemple, classer des images comme mignonnes ou non). Cette procédure
étant longue et coûteuse, il est assez fréquent de n’avoir que quelques cen-
taines d’instances étiquetées.
9.3 Autoencodeurs empilés 403
Cette couche personnalisée opère à la manière d’une couche Dense normale, mais
elle utilise les poids d’une autre couche Dense, transposés (spécier transpose_
b=True équivaut à transposer le second argument, mais l’approche choisie est plus
efcace car la transposition est effectuée à la volée dans l’opération matmul()).
Cependant, elle utilise son propre vecteur de termes constants. Nous pouvons main-
tenant construire un nouvel autoencodeur empilé, semblable au précédent, mais
dont les couches Dense du décodeur sont liées aux couches Dense de l’encodeur:
dense_1 = tf.keras.layers.Dense(100, activation="relu")
dense_2 = tf.keras.layers.Dense(30, activation="relu")
tied_encoder = tf.keras.Sequential([
tf.keras.layers.Flatten(),
404 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
dense_1,
dense_2
])
tied_decoder = tf.keras.Sequential([
DenseTranspose(dense_2, activation="relu"),
DenseTranspose(dense_1),
tf.keras.layers.Reshape([28, 28])
])
Cible = Entrées
Sortie Sortie
Couche Couche
cachée 1 cachée 2
Entrée Entrée
Phase 2
Phase 1 Entraînement du deuxième Phase 3
Entraînement du premier autoencodeur sur le jeu Empilement
autoencodeur d’entraînement encodé des autoencodeurs
par le premier encodeur
tf.keras.layers.MaxPool2D(pool_size=2), # sortie : 3 × 3 × 64
tf.keras.layers.Conv2D(30, 3, padding="same", activation="relu"),
tf.keras.layers.GlobalAvgPool2D() # sortie : 30
])
conv_decoder = tf.keras.Sequential([
tf.keras.layers.Dense(3 * 3 * 16),
tf.keras.layers.Reshape((3, 3, 16)),
tf.keras.layers.Conv2DTranspose(32, 3, strides=2, activation="relu"),
tf.keras.layers.Conv2DTranspose(16, 3, strides=2, padding="same",
activation="relu"),
tf.keras.layers.Conv2DTranspose(1, 3, strides=2, padding="same"),
tf.keras.layers.Reshape([28, 28])
])
conv_ae = tf.keras.Sequential([conv_encoder, conv_decoder])
Il est aussi possible de créer des autoencodeurs ayant d’autres types d’architectures,
comme des RNN (voir un exemple dans le notebook de ce chapitre257).
Prenons un peu de recul. Nous avons vu différentes sortes d’autoencodeurs (de
base, empilés et convolutifs) et comment les entraîner (en une fois ou couche par
couche). Nous avons également examiné deux applications: la visualisation de don-
nées et le préentraînement non supervisé.
Jusqu’à présent, pour forcer l’autoencodeur à apprendre des caractéristiques inté-
ressantes, nous avons limité la taille de la couche de codage, le rendant sous-complet.
Mais il est possible d’utiliser de nombreuses autres sortes de contraintes, y compris
autoriser la couche de codage à être aussi vaste que les entrées, voire plus vaste,
donnant un autoencodeur sur-complet. Dans les sections qui suivent, nous examine-
rons quelques autres sortes d’autoencodeurs: les autoencodeurs débruiteurs, les auto-
encodeurs creux et les autoencodeurs variationnels.
Le bruit peut être un bruit purement gaussien ajouté aux entrées ou un blocage
aléatoire des entrées, comme dans la technique d’abandon (ou dropout, voir cha-
pitre3). La gure9.8 illustre ces deux possibilités.
Sortie Sortie
Abandon
Entrée Entrée
Figure 9.8 – Autoencodeur débruiteur, avec ajout d’un bruit gaussien (à gauche)
ou abandon (à droite)
La gure9.9 montre quelques images avec du bruit (la moitié des pixels ont été
désactivés) et les images reconstruites par l’autoencodeur débruiteur avec abandon.
Vous remarquerez que l’autoencodeur a deviné des détails qui ne font pas partie de
l’entrée, comme le col de la chemise blanche (ligne du bas, quatrième image). Ainsi
que vous pouvez le voir, les autoencodeurs débruiteurs peuvent non seulement être
utilisés pour la visualisation de données et le préentraînement non supervisé, comme
les autres autoencodeurs décrits jusqu’à présent, mais ils peuvent également être
exploités assez simplement et efcacement pour retirer du bruit dans des images.
408 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Figure 9.9 – Des images comportant du bruit (en haut) et leur reconstruction (en bas)
Divergence de KL
0.8 MAE (ℓ1)
MSE (ℓ 2)
0.6
Coût
0.4 Dispersion
cible
0.2
0.0
0.0 0.2 0.4 0.6 0.8 1.0
Dispersion réelle
P( i)
D KL (P Q ) = ∑ i
P (i )log
Q( i)
410 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Dans notre cas, nous voulons mesurer la divergence entre la probabilité cible p
qu’un neurone de la couche de codage s’activera et la probabilité réelle q, estimée
en mesurant l’activation moyenne sur le lot d’entraînement. Nous pouvons donc
simplier la divergence de KL et obtenir l’équation9.2.
p 1– p
+ (1 – p) log
D KL ( p q) = p log
q 1– q
Après avoir calculé la perte de dispersion pour chaque neurone de la couche de
codage, il suft d’additionner ces pertes et d’ajouter le résultat à la fonction de coût.
Pour contrôler l’importance relative de la perte de dispersion et de la perte de recons-
truction, on multiplie la première par un hyperparamètre de poids de dispersion. Si ce
poids est trop élevé, le modèle restera proche de la dispersion cible, mais il risquera de
ne pas reconstruire correctement les entrées et donc d’être inutile. À l’inverse, s’il est
trop faible, le modèle ignorera en grande partie l’objectif de dispersion et n’apprendra
aucune caractéristique intéressante.
Nous disposons à présent des éléments nécessaires à l’implémentation d’un
autoencodeur épars fondé sur la divergence de KL. Commençons par créer un régu-
larisateur personnalisé de façon à appliquer une régularisation par divergence de KL:
kl_divergence = tf.keras.losses.kullback_leibler_divergence
class KLDivergenceRegularizer(tf.keras.regularizers.Regularizer):
def __init__(self, weight, target):
self.weight = weight
self.target = target
260. Diederik Kingma et Max Welling, « Auto-Encoding Variational Bayes » (2013) : https://homl.info/115.
261. Voir le chapitre 9 de l’ouvrage Machine Learning avec Scikit-Learn, A. Géron, Dunod (3 e édition,
2023).
412 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Sortie
Couche cachée 5
Couche cachée 4
Bruit
gaussien
Espace
de codage
Codages � Codages �
�
�
Couche cachée 2
Entrée
Vous le voyez sur la gure, même si les entrées peuvent avoir une distribution
très complexe, un autoencodeur variationnel a tendance à produire des codages qui
semblent avoir été échantillonnés à partir d’une simple distribution gaussienne 262:
au cours de l’entraînement, la fonction de coût (voir ci-après) pousse les codages à
migrer progressivement vers l’espace de codage (également appelé espace latent) pour
occuper une région semblable à un nuage de points gaussien. En conséquence, après
l’entraînement d’un autoencodeur variationnel, vous pouvez très facilement générer
une nouvelle instance: il suft de choisir au hasard un codage dans la distribution
gaussienne et de le décoder.
Passons à présent à la fonction de coût, qui comprend deux parties. La première
est la perte de reconstruction habituelle, qui pousse l’autoencodeur à reproduire ses
entrées (on peut choisir la MSE pour cela, comme précédemment). La seconde est
la perte latente, qui pousse l’autoencodeur à produire des codages semblant avoir été
échantillonnés à partir d’une simple distribution normale, pour laquelle on utilisera
la divergence de KL entre la distribution cible (la loi normale) et la distribution
réelle des codages. Le calcul est légèrement plus complexe qu’avec les autoenco-
deurs épars, notamment en raison du bruit gaussien, qui limite la quantité d’infor-
mations pouvant être transmises à la couche de codage: ceci incite l’autoencodeur à
262. Les autoencodeurs variationnels sont en réalité plus généraux ; les codages ne se limitent pas aux
distributions gaussiennes.
9.7 Autoencodeurs variationnels 413
Équation 9.4 – Perte latente d’un autoencodeur variationnel, réécrite en utilisant γ = log(σ2)
Cette couche Sampling reçoit deux entrées : mean (μ) et log_var (γ). Elle
utilise la fonction tf.random.normal() pour échantillonner un vecteur aléa-
toire (de la même forme que γ) à partir de la distribution normale de moyenne 0 et
d’écart-type 1. Elle multiplie ensuite ce vecteur par exp(γ/2) (qui est égal à σ, comme
vous pouvez le vérier), puis ajoute μ et renvoie le résultat. La couche échantillonne
donc un vecteur de codages à partir d’une distribution normale (c.-à-d. gaussienne)
de moyenne μ et d’écart-type σ.
Nous pouvons à présent créer l’encodeur, en utilisant l’API fonctionnelle, car le
modèle n’est pas intégralement séquentiel:
codings_size = 10
263. Tous les détails mathématiques se trouvent dans l’article d’origine sur les autoencodeurs variationnels
et dans le superbe tutoriel écrit en 2016 par Carl Doersch (https://homl.info/116).
414 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Z = tf.keras.layers.Dense(100, activation="relu")(Z)
codings_mean = tf.keras.layers.Dense(codings_size)(Z) # μ
codings_log_var = tf.keras.layers.Dense(codings_size)(Z) # γ
codings = Sampling()([codings_mean, codings_log_var])
variational_encoder = tf.keras.Model(
inputs=[inputs], outputs=[codings_mean, codings_log_var, codings])
Notez que les couches Dense qui produisent codings_mean (μ) et codings_
log_var (γ) ont les mêmes entrées (c’est-à-dire les sorties de la deuxième couche
Dense). Nous pouvons ensuite passer codings_mean et codings_log_var
à la couche Sampling. Enn, le modèle variational_encoder possède
trois sorties. Seules codings est recquise, mais nous ajoutons codings_mean et
codings_log_var pour le cas où nous aurions besoin d’inspecter leurs valeurs.
Construisons à présent le décodeur :
decoder_inputs = tf.keras.layers.Input(shape=[codings_size])
x = tf.keras.layers.Dense(100, activation="relu")(decoder_inputs)
x = tf.keras.layers.Dense(150, activation="relu")(x)
x = tf.keras.layers.Dense(28 * 28)(x)
outputs = tf.keras.layers.Reshape([28, 28])(x)
variational_decoder = tf.keras.Model(inputs=[decoder_inputs],
outputs=[outputs])
Dans ce cas, nous aurions pu utiliser l’API séquentielle à la place de l’API fonc-
tionnelle, car le décodeur n’est qu’une simple pile de couches, pratiquement iden-
tique à bon nombre d’autres décodeurs construits jusqu’à présent. Nous terminons
par la mise en place du modèle d’autoencodeur variationnel:
_, _, codings = variational_encoder(inputs)
reconstructions = variational_decoder(codings)
variational_ae = tf.keras.Model(inputs=[inputs], outputs=[reconstructions])
Nous ignorons les deux premières sorties de l’encodeur (nous souhaitons simple-
ment fournir les codages au décodeur). Enn, nous devons ajouter la perte latente et
la perte de reconstruction:
latent_loss = -0.5 * tf.reduce_sum(
1 + codings_log_var – tf.exp(codings_log_var) – tf.square(codings_mean),
axis=-1)
variational_ae.add_loss(tf.reduce_mean(latent_loss) / 784.)
Même si elles sont un peu trop oues, la plupart de ces images restent plutôt convain-
cantes. Pour les autres, le résultat n’est pas extraordinaire, mais ne soyez pas trop dur
avec l’autoencodeur, car il n’a eu que quelques minutes pour apprendre !
Grâce aux autoencodeurs variationnels, il est possible d’effectuer une interpolation
sémantique. Au lieu d’effectuer une interpolation entre deux images au niveau du pixel
(ce qui ressemblerait à une superposition des deux images), nous pouvons effectuer cette
interpolation au niveau des codages. Prenons par exemple quelques codages le long
d’une ligne arbitraire dans l’espace latent et décodons les points Nous obtenons une
séquence d’images qui vont graduellement des pantalons aux vestes (voir gure9.13):
codings = np.zeros([7, codings_size])
codings[:, 3] = np.linspace(-0.8, 0.8, 7) # l’axe 3 paraît le meilleur
# dans ce cas
images = variational_decoder(codings).numpy()
416 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Focalisons-nous à présent sur les GAN : ils sont plus difciles à entraîner, mais
quand vous arrivez à les faire fonctionner, ils produisent des images assez étonnantes.
264. Ian Goodfellow et al., « Generative Adversarial Nets », Proceedings of the 27th International Conference
on Neural Information Processing Systems, 2 (2014), 2672-2680 : https://homl.info/gan.
9.9 Réseaux antagonistes génératifs (GAN) 417
Faux/Réel
ou
Faux Réel
Bruit
Dense = tf.keras.layers.Dense
generator = tf.keras.Sequential([
Dense(100, activation="relu", kernel_initializer="he_normal"),
Dense(150, activation="relu", kernel_initializer="he_normal"),
Dense(28 * 28, activation="sigmoid"),
tf.keras.layers.Reshape([28, 28])
])
discriminator = tf.keras.Sequential([
tf.keras.layers.Flatten(),
Dense(150, activation="relu", kernel_initializer="he_normal"),
Dense(100, activation="relu", kernel_initializer="he_normal"),
Dense(1, activation="sigmoid")
])
gan = tf.keras.Sequential([generator, discriminator])
Nous sommes alors prêts à écrire la boucle d’entraînement, que nous plaçons dans
une fonction train_gan() :
def train_gan(gan, dataset, batch_size, codings_size, n_epochs):
generator, discriminator = gan.layers
for epoch in range(n_epochs):
for X_batch in dataset:
# phase 1 – entraînement du discriminateur
noise = tf.random.normal(shape=[batch_size, codings_size])
generated_images = generator(noise)
X_fake_and_real = tf.concat([generated_images, X_batch], axis=0)
y1 = tf.constant([[0.]] * batch_size + [[1.]] * batch_size)
discriminator.train_on_batch(X_fake_and_real, y1)
# phase 2 – entraînement du générateur
noise = tf.random.normal(shape=[batch_size, codings_size])
y2 = tf.constant([[1.]] * batch_size)
gan.train_on_batch(noise, y2)
Vous pouvez retrouver les deux phases de chaque itération comme présenté pré-
cédemment:
• Dans la phase 1, nous fournissons au générateur un bruit gaussien an qu’il
produise des images factices et nous complétons ce lot d’images par autant
d’images réelles. Les cibles y1 sont xées à 0 pour les fausses images et à 1
pour les images réelles. Ensuite, nous entraînons le discriminateur sur ce lot.
N’oubliez pas que le discriminateur est entraînable dans cette phase, mais que
nous ne touchons pas au générateur.
• Dans la phase 2, nous alimentons le GAN avec du bruit gaussien. Son
générateur commence à produire des images factices, puis le discriminateur
tente de deviner si ces images sont fausses ou réelles. Durant cette phase, nous
essayons d’améliorer le générateur, ce qui signie que nous voulons que le
discriminateur échoue: c’est pourquoi les cibles y2 sont toutes initialisées à 1,
alors même que les images sont fausses. Durant cette phase, le discriminateur
n’est pas entraînable, par conséquent la seule partie du modèle gan qui va
s’améliorer est le générateur.
Et voilà ! Après l’entraînement, vous pouvez échantillonner au hasard certains
codages de la distribution gaussienne, et les fournir en entrée au générateur an de
produire de nouvelles images:
codings = tf.random.normal(shape=[batch_size, codings_size])
generated_images = generator.predict(codings)
Si vous afchez les images générées (voir la gure9.15), vous verrez qu’au bout de
la première époque elles commencent déjà à ressembler à des images Fashion MNIST
(mais avec beaucoup de bruit).
420 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Figure 9.15 – Images générées par le GAN après une époque d’entraînement
générateur parfait. Malheureusement, ce n’est pas si simple. Rien ne garantit que cet
équilibre sera un jour atteint.
La plus grande difculté se nomme effondrement des modes (en anglais, mode col-
lapse): il s’agit du moment où les sorties du générateur deviennent progressivement
de moins en moins variées, signe que celui-ci ignore les autres modes de la distribu-
tion. Comment cela peut-il se produire? Supposons que le générateur parvienne de
mieux en mieux à produire des chaussures convaincantes, plus que toute autre classe.
Il trompera un peu mieux le discriminateur avec les chaussures, ce qui l’encouragera
à générer encore plus d’images de chaussures. Progressivement, il oubliera comment
produire d’autres images. Dans le même temps, les seules images factices que verra le
discriminateur représenteront des chaussures, et il oubliera progressivement comment
identier les fausses images d’autres classes. À terme, lorsque le discriminateur réus-
sira à différencier les fausses chaussures des vraies, le générateur sera obligé de passer
à une autre classe. Il pourra alors devenir performant sur les chemises, par exemple,
oubliant tout des chaussures, et le discriminateur fera de même. Le GAN pourrait
ainsi passer par plusieurs classes, sans jamais devenir bon dans aucune d’entre elles.
Par ailleurs, puisque le générateur et le discriminateur luttent constamment
l’un contre l’autre, leurs paramètres peuvent nir par osciller et devenir instables.
L’entraînement peut débuter correctement, puis diverger soudainement sans cause
apparente, en raison de ces instabilités. Et, puisque de nombreux facteurs affectent
ces mouvements complexes, les GAN sont très sensibles aux valeurs des divers para-
mètres. Vous devrez peut-être consacrer beaucoup d’efforts à les ajuster. En pratique,
c’est la raison pour laquelle j’ai utilisé RMSProp plutôt que Nadam lorsque j’ai compilé
les modèles : en utilisant Nadam, j’ai abouti à un effondrement des modes gravissime.
Ces problèmes ont largement occupé les chercheurs depuis 2014. De nombreux
articles ont été écrits sur ce sujet, certains proposant de nouvelles fonctions de coût265
(même si un article266 publié en 2018 par des chercheurs de Google remet en question
leur efcacité) ou des techniques permettant de stabiliser l’entraînement ou d’éviter le
problème d’effondrement des modes. Par exemple, une technique répandue nommée
réitération d’expériences ou rejeu d’expériences (en anglais, experience replay) consiste à
stocker dans un tampon les images produites par le générateur à chaque itération (les
images plus anciennes sont retirées au fur et à mesure) et à entraîner le discriminateur
en employant des images réelles et des images factices extraites de ce tampon (plutôt
que seulement des images factices produites par le générateur actuel). Cela réduit les
risques que le discriminateur surajuste les dernières sorties du générateur.
Une autre technique fréquente se nomme discrimination par mini-lots (mini-batch
discrimination). Elle mesure la similitude des images sur le lot et fournit cette infor-
mation au discriminateur, qui peut alors facilement rejeter tout un lot d’images fac-
tices qui manquent de diversité. Cela encourage le générateur à produire une grande
265. Le projet GitHub mené par Hwalsuk Lee (https://homl.info/ganloss) propose une bonne comparaison
des principales pertes des GAN.
266. Mario Lucic et al., « Are GANs Created Equal? A Large-Scale Study », Proceedings of the 32nd Inter-
national Conference on Neural Information Processing Systems (2018), 698-707 : https://homl.info/gansequal.
422 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
variété d’images, réduisant ainsi le risque d’effondrement des modes. D’autres articles
proposent simplement des architectures spéciques qui montrent de bonnes perfor-
mances.
En résumé, ce domaine de recherche est encore très actif et la dynamique des
GAN n’est pas encore parfaitement comprise. Néanmoins, les avancées existent et
certains résultats sont véritablement époustouants ! Examinons à présent certaines
des architectures les plus abouties, en commençant par les GAN convolutifs pro-
fonds, qui représentaient encore l’état de l’art il y a quelques années. Ensuite, nous
étudierons deux architectures plus récentes (et plus complexes).
generator = tf.keras.Sequential([
tf.keras.layers.Dense(7 * 7 * 128),
tf.keras.layers.Reshape([7, 7, 128]),
267. Alec Radford et al., « Unsupervised Representation Learning with Deep Convolutional Generative
Adversarial Networks » (2015): https://homl.info/dcgan.
9.9 Réseaux antagonistes génératifs (GAN) 423
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Conv2DTranspose(64, kernel_size=5, strides=2,
padding="same", activation="relu"),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Conv2DTranspose(1, kernel_size=5, strides=2,
padding="same", activation="tanh"),
])
discriminator = tf.keras.Sequential([
tf.keras.layers.Conv2D(64, kernel_size=5, strides=2, padding="same",
activation=tf.keras.layers.LeakyReLU(0.2)),
tf.keras.layers.Dropout(0.4),
tf.keras.layers.Conv2D(128, kernel_size=5, strides=2, padding="same",
activation=tf.keras.layers.LeakyReLU(0.2)),
tf.keras.layers.Dropout(0.4),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(1, activation="sigmoid")
])
gan = tf.keras.Sequential([generator, discriminator])
Le générateur reçoit des codages de taille 100, les projette en 6 272 dimensions
(=7 ×7 ×128), et reforme le résultat pour obtenir un tenseur 7×7×128. Celui-ci
subit une normalisation par lots, puis est transmis à une couche de convolution
transposée avec un pas de 2. Elle le suréchantillonne de 7×7 à 14×14 et réduit sa
profondeur de 128 à 64. Le résultat subit de nouveau une normalisation par lots, puis
est transmis à une autre couche de convolution transposée avec un pas de 2. Elle le
suréchantillonne de 14×14 à 28×28 et réduit sa profondeur de 64 à 1. Cette couche
utilise la fonction d’activation tanh, donc les sorties sont dans la plage –1 à 1. C’est
pourquoi, avant d’entraîner le GAN, nous devons transformer le jeu d’entraînement
de sorte qu’il se trouve dans cette plage. Nous devons également en changer la forme
pour ajouter la dimension du canal :
X_train_dcgan = X_train.reshape(-1, 28, 28, 1) * 2. - 1. # changer forme
# et valeurs
268. Reproduite avec l’aimable autorisation des auteurs et adaptée pour la version française.
9.9 Réseaux antagonistes génératifs (GAN) 425
Si vous effectuez le calcul hommes avec lunettes, moins hommes sans lunettes,
plus femmes sans lunettes – où chaque terme correspond à l’un des codages moyens
– et si vous générez l’image qui correspond à ce codage, vous obtenez celle placée au
centre de la grille 3×3 de visages sur la droite : une femme avec des lunettes ! Les huit
autres images ont été générées à partir du même vecteur, auquel un léger bruit a été
ajouté. Cela permet d’illustrer les capacités d’interpolation sémantique des DCGAN.
Avec l’arithmétique de visages, nous entrons dans le monde de la science-ction !
Toutefois, les DCGAN sont loin d’être parfaits. Par exemple, si vous essayez de
générer de très grandes images avec des DCGAN, vous obtenez souvent des caracté-
ristiques locales convaincantes mais des incohérences globales (comme une chemise
avec une manche plus longue que l’autre, des boucles d’oreille différentes ou des yeux
ne regardant pas dans la même direction). Comment corriger cela?
269. Mehdi Mirza et Simon Osindero, « Conditional Generative Adversarial Nets » (2014) : https://homl.
info/cgan.
270. Tero Karras et al., « Progressive Growing of GANs for Improved Quality, Stability, and Variation »,
Proceedings of the International Conference on Learning Representations (2018) : https://homl.info/progan.
426 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Convolution 2
Suréchantillonnage
Convolution 1 Convolution 1
Dense Dense
Bruit Bruit
Figure 9.18 – Croissance progressive d’un GAN : le générateur du GAN produit des images
4x4 en couleurs (à gauche) et il est étendu pour produire des images 8×8 (à droite)
271. La plage dynamique d’une variable est le rapport entre la valeur la plus élevée et la valeur la plus faible
qu’elle peut avoir.
428 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
9.9.4 StyleGAN
L’état de l’art de la génération d’images en haute résolution a de nouveau fait un
bond en avant grâce à la même équipe de chercheurs chez Nvidia. Dans un article272
publié en 2018, les auteurs ont présenté l’architecture StyleGAN. Ils ont utilisé des
techniques de transfert de style dans le générateur pour s’assurer que les images géné-
rées présentent la même structure locale que les images d’entraînement, à chaque
échelle, améliorant énormément la qualité des images produites. Le discriminateur et
la fonction de perte n’ont pas été modiés, seul le générateur l’a été. Un générateur
StyleGAN est constitué de deux réseaux (voir la gure 9.19):
• Réseau de correspondance
Un perceptron à huit couches transforme les représentations latentes z
(c’est-à-dire les codages) en un vecteur w. Ce vecteur est transmis ensuite à
plusieurs transformations afnes (c’est-à-dire des couches Dense sans fonction
d’activation, représentées par les carrés «A » dans la gure9.19). Nous obtenons
alors plusieurs vecteurs, qui contrôlent le style de l’image générée à différents
niveaux, allant de la texture ne (par exemple, la couleur des cheveux) à des
caractéristiques de haut niveau (par exemple, adulte ou enfant). En résumé, le
réseau de correspondance associe les codages à plusieurs vecteurs de styles.
Tenseur constant
Normalisation 4×4×512
Convolution 3×3
style
Suréchantillonnage
Convolution 3×3
style
Convolution 3×3
style
272. Tero Karras et al., « A Style-Based Generator Architecture for Generative Adversarial Networks »
(2018) : https://homl.info/stylegan.
273. Reproduite avec l’aimable autorisation des auteurs et adaptée pour la version française.
9.9 Réseaux antagonistes génératifs (GAN) 429
• Réseau de synthèse
Il est responsable de la génération des images. Il dispose d’une entrée
apprise constante (plus précisément, cette entrée sera constante après
l’entraînement, mais, pendant l’entraînement, elle est peu à peu ajustée par
la rétropropagation). Comme précédemment, il traite cette entrée à l’aide de
plusieurs couches de convolution et de suréchantillonnage, mais avec deux
ajustements supplémentaires. Premièrement, du bruit est ajouté à l’entrée et à
toutes les sorties des couches de convolution (avant la fonction d’activation).
Deuxièmement, chaque couche de bruit est suivie d’une couche de normalisation
d’instance adaptative (adaptive instance normalization, ou AdaIn). Elle normalise
indépendamment chaque carte de caractéristiques (en soustrayant la moyenne
de la carte de caractéristiques et en divisant par son écart-type), puis utilise
le vecteur de style pour déterminer l’échelle et le décalage de chaque carte
de caractéristiques (le vecteur de style contient un facteur multiplicatif et un
terme constant pour chaque carte de caractéristiques).
L’idée d’ajouter du bruit indépendamment des codages est très importante.
Certaines parties d’une image sont assez aléatoires, comme l’emplacement exact de
chaque tache de rousseur ou poil. Dans les GAN précédents, ce côté aléatoire devait
provenir soit des codages, soit d’un bruit pseudo-aléatoire produit par le générateur
lui-même. S’il était issu des codages, le générateur devait dédier une partie signica-
tive de la puissance de représentation des codages au stockage du bruit; un vrai gas-
pillage. De plus, le bruit devait passer par tout le réseau pour atteindre les couches
nales du générateur ; une contrainte plutôt inutile qui ralentissait probablement
l’entraînement. Enn, certains artefacts visuels pouvaient apparaître car le même
bruit était utilisé à différents niveaux. Si, à la place, le générateur tente de produire
son propre bruit pseudo-aléatoire, celui-ci risque de ne pas être très convaincant,
conduisant à encore plus d’artefacts visuels. Sans oublier qu’une partie des poids
du générateur doit être réservée à la production d’un bruit pseudo-aléatoire, ce qui
ressemble de nouveau à du gaspillage. En ajoutant des entrées de bruit supplémen-
taires, tous ces problèmes sont évités; le GAN est capable d’utiliser le bruit fourni
pour ajouter la bonne quantité de hasard à chaque partie de l’image.
Le bruit ajouté est différent pour chaque niveau. Chaque entrée de bruit est
constituée d’une seule carte de caractéristiques remplie d’un bruit gaussien, qui est
diffusé à toutes les cartes de caractéristiques (du niveau donné) et recalibré à l’aide
des facteurs de redimensionnement par caractéristique appris (les carrés « B » dans la
gure9.20) avant d’être ajouté.
Pour nir, un StyleGAN utilise une technique de régularisation par mélange (mixing
regularization), ou mélange de style (style mixing), dans laquelle un pourcentage des
images générées est produit en utilisant deux codages différents. Plus précisément, les
codages c1 et c2 passent par le réseau de correspondance, ce qui donne deux vecteurs
de styles w1 et w 2. Ensuite, le réseau de synthèse génère une image à partir des styles
w 1 pour les premiers niveaux, et les styles w2 pour les niveaux restants. Le niveau de
transition est choisi aléatoirement. Cela évite que le réseau ne suppose une corréla-
tion entre des styles de niveaux adjacents, ce qui encourage la localité dans le GAN,
430 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
à savoir que chaque vecteur de styles affecte uniquement un nombre limité de carac-
téristiques dans l’image générée.
Il existe une telle variété de GAN qu’il faudrait un livre entier pour les décrire
tous. Nous espérons que cette introduction vous a apporté les idées principales
et, plus important, le souhait d’en savoir plus. Ensuite, implémentez votre propre
GAN et ne soyez pas découragé si son apprentissage est au départ problématique.
Malheureusement, ce comportement est normal et il faut un peu de patience avant
d’obtenir des résultats, mais ils en valent la peine. Si vous rencontrez un problème
avec un détail d’implémentation, vous trouverez un grand nombre d’exemples d’im-
plémentations avec Keras ou TensorFlow. Mais, si vous souhaitez uniquement obtenir
des résultats impressionnants très rapidement, vous pouvez vous contenter d’utiliser
un modèle préentraîné (il existe des modèles StyleGAN préentraînés pour Keras).
Maintenant que nous avons examiné les autoencodeurs et les GAN, voyons un
dernier type d’architecture: les modèles de diffusion.
274. Jascha Sohl-Dickstein et al., « Deep Unsupervised Learning using Nonequilibrium Thermodyna-
mics», arXiv preprint arXiv:1503.03585 (2015): https://homl.info/diffusion.
275. Jonathan Ho et al., « Denoising Diffusion Probabilistic Models » (2020): https://homl.info/ddpm.
276. Alex Nichol and Prafulla Dhariwal, « Improved Denoising Diffusion Probabilistic Models » (2021):
https://homl.info/ddpm2.
9.10 Modèles de diffusion 431
des DDPM, comme vous le verrez, c’est qu’ils mettent très longtemps à générer des
images, par rapport aux GAN ou aux VAE.
Comment fonctionne donc exactement un DDPM? Supposons que vous partiez
de l’image d’un chat (comme celle de la gure9.20) notée x0, et qu’à chaque étape
temporelle t vous ajoutiez un petit peu de bruit gaussien à l’image, de moyenne 0 et
de variance β .t Ce bruit est indépendant pour chaque pixel: on dit qu’il est isotrope.
Vous obtenez d’abord l’image x 1,puis x 2, et ainsi de suite, jusqu’à ce que le chat soit
complètement caché par le bruit, impossible à voir. La dernière étape temporelle est
notée T. Dans l’article DDPM d’origine, les auteurs ont utilisé T = 1 000 et ont choisi
la variance βt de telle sorte que le signal du chat s’affaiblisse linéairement entre les
étapes temporelles 0 et T. Dans l’article sur le DDPM amélioré, T a été porté à 4 000
et l’évolution de la variance a été légèrement modiée de manière à évoluer plus len-
tement au début et à la n. En bref, nous noyons graduellement le chat dans le bruit:
c’est ce qu’on appelle le processus avant (en anglais, forward process).
q(X t Xt–1)
X0 Xt–1 Xt XT
X0 Xt–1 Xt XT
pq(Xt–1 Xt )
Au fur et à mesure que nous ajoutons du bruit gaussien au cours du processus avant,
la distribution des valeurs des pixels devient de plus en plus gaussienne. Un détail
important que je n’ai pas encore mentionné est que les valeurs des pixels sont réduites
légèrement à chaque étape, d’un facteur 1 – β t. Ceci garantit que la moyenne des
valeurs des pixels se rapprochera graduellement de 0, étant donné que ce facteur de
réduction est un peu plus petit que 1 (imaginez ce qui se passe si vous multipliez à de
nombreuses reprises un nombre par 0,99). Ceci garantit également que la variance va
converger graduellement vers 1. En effet, l’écart-type des valeurs des pixels se trouve
aussi multiplié par 1 – βt , et donc la variance est multipliée par 1 − βt (soit le carré
du facteur de réduction). Mais la variance ne va pas diminuer jusqu’à 0, étant donné
que nous ajoutons un bruit gaussien de variance βt à chaque étape. Et étant donné
que les variances s’ajoutent lorsqu’on additionne des distributions gaussiennes, la
variance ne peut que converger vers 1 − βt + βt =1.
Le processus de diffusion avant est résumé dans l’équation9.5. Celle-ci ne vous
apprendra rien de neuf sur le processus avant, mais il est utile de comprendre ce
432 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
type de notation mathématique, qui est souvent utilisée dans les articles de Machine
Learning. Cette équation dénit la distribution de probabilité q de xt , connaissant
xt−1, comme une distribution gaussienne (ou distribution normale, d’où le �) de
moyenne xt–1 multipliée par le facteur de réduction et de matrice de covariance égale
à β tI où I est la matrice identité, ce qui signie que le bruit est isotrope de variance βt.
1.0
βt
0.8 –
αt
0.6
0.4
0.2
0.0
0 500 1 000 1 500 2 000 2 500 3 000 3 500 4 000
t
T = 4000
alpha, alpha_cumprod, beta = variance_schedule(T)
return {
"X_noisy": alpha_cm ** 0.5 * X + (1 – alpha_cm) ** 0.5 * noise,
"time": t,
}, noise
Analysons ce code:
• Par souci de simplicité, nous utiliserons Fashion MNIST, c’est pourquoi
la fonction doit d’abord ajouter un axe de canal. Il sera également utile de
transformer les valeurs des pixels pour qu’elles soient toutes comprises entre –1
et 1, an de se rapprocher de la distribution gaussienne nale de moyenne 0 et
de variance1.
• Ensuite, la fonction crée t, un vecteur contenant une étape temporelle choisie
au hasard entre 1 et T pour chaque image du lot.
• Puis elle utilise tf.gather() pour obtenir la valeur de alpha_cumprod
pour chacune des étapes temporelles contenues dans le vecteur t. Ceci nous
donne le vecteur alpha_cm contenant une valeur de αt pour chaque image.
• La ligne suivante change la forme de alpha_cm de [taille du lot] en [taille du lot,
1, 1, 1]. C’est ce qui permettra de diffuser alpha_cm sur l’ensemble du lot X.
• Puis nous générons du bruit gaussien de moyenne 0 et de variance1.
• Enn, nous utilisons l’équation9.6 pour appliquer le processus de diffusion aux
images. Notez que x ** 0.5 est égal à la racine carrée de x. La fonction renvoie
un n-uplet contenant les entrées et les cibles. Les entrées sont représentées
sous forme d’un dictionnaire Python contenant les images avec bruit et les
étapes temporelles utilisées pour les générer. Les cibles correspondent au bruit
gaussien utilisé pour générer chaque image.
Préparé ainsi, le modèle va prédire le bruit qui doit être soustrait de l’image
d’entrée pour obtenir l’image d’origine. Pourquoi ne pas prédire l’image
d’origine directement ? En réalité, les auteurs l’ont essayé : la réponse est
que, tout simplement, ça ne marche pas aussi bien.
def build_diffusion_model():
X_noisy = tf.keras.layers.Input(shape=[28, 28, 1], name="X_noisy")
time_input = tf.keras.layers.Input(shape=[], dtype=tf.int32, name="time")
[...] # construire le modèle recevant les images avec bruit et les étapes
outputs = [...] # prédire le bruit à retrancher (même forme que
# les entrées)
return tf.keras.Model(inputs=[X_noisy, time_input], outputs=[outputs])
Les chercheurs ayant présenté DDPM ont utilisé une architecture U-Net modi-
ée277 qui ressemble en bien des points à l’architecture de réseau entièrement convo-
lutif (FCN) que nous avons vue au chapitre6 pour la segmentation sémantique. C’est
un réseau de neurones convolutif qui sous-échantillonne progressivement les images
d’entrée, puis les suréchantillonne à nouveau progressivement, avec des connexions
de saut reliant chacune niveau de la partie sous-échantillonnage au niveau correspon-
dant de la partie suréchantillonnage. Pour prendre en compte les étapes temporelles,
ils les ont encodées grâce à la même technique que les encodages positionnels dans
l’architecture de transformeurs (voir chapitre8). À chaque niveau de l’architecture
U-Net, ils ont fait transiter ces encodages temporels par des couches Dense puis les
ont transmis au réseau U-Net. Enn, ils ont également utilisé à différents niveaux des
couches d’attention à plusieurs têtes. Le notebook de ce chapitre278 en propose une
implémentation basique, mais vous pouvez aussi consulter https://homl.info/ddpmcode
pour l’implémentation ofcielle. Celle-ci est basée sur une version TensorFlow 1.x
désormais obsolète, mais c’est plutôt lisible.
Nous pouvons maintenant entraîner le modèle normalement. Les auteurs ont
remarqué qu’ils obtenaient de meilleurs résultats avec la perte MAE qu’avec la MSE.
Vous pouvez aussi utiliser la perte de Huber:
model = build_diffusion_model()
model.compile(loss=tf.keras.losses.Huber(), optimizer="nadam")
history = model.fit(train_set, validation_data=valid_set, epochs=100)
Une fois le modèle entraîné, vous pouvez l’utiliser pour générer de nouvelles
images. Malheureusement, il n’existe pas de raccourci dans le processus de diffusion
inverse, et il vous faut donc échantillonner xT aléatoirement à partir d’une distribu-
tion gaussienne de moyenne 0 et de variance 1, puis le transmettre au modèle pour
prédire le bruit, puis retrancher ce dernier de l’image en utilisant l’équation9.8, an
d’obtenir x T–1 . Répétez alors le processus 3 999 fois supplémentaires jusqu’à ce que
vous obteniez x 0. Si tout s’est bien passé, le résultat obtenu devrait ressemblait à une
image Fashion MNIST normale !
1 βt
xt–1 = xt – ε θ (x t, t) + βtz
αt 1 – αt
277. Olaf Ronneberger et al., « U-Net: Convolutional Networks for Biomedical Image Segmentation »,
arXiv preprint arXiv:1505.04597 (2015): https://homl.info/unet.
278. Voir « 17_autoencoders_gans_and_diffusion_models.ipynb » sur https://homl.info/colab3.
436 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
Dans cette équation, εθ (xt, t) représente le bruit prédit par le modèle étant donné
l’image d’entrée x et l’étape temporelle t. Les paramètres du modèle sont représentés
par θ, et z est un bruit gaussien de moyenne 0 et de variance 1. Ceci rend le processus
inverse stochastique : si vous l’exécutez à plusieurs reprises, vous obtiendrez diffé-
rentes images.
Écrivons une fonction qui implémente ce processus inverse, puis appelons-la pour
générer quelques images:
def generate(model, batch_size=32):
X = tf.random.normal([batch_size, 28, 28, 1])
for t in range(T, 0, -1):
noise = (tf.random.normal if t > 1 else tf.zeros)(tf.shape(X))
X_noise = model({"X_noisy": X, "time": tf.constant([t] * batch_size)})
X = (
1 / alpha[t] ** 0.5
* (X – beta[t] / (1 – alpha_cumprod[t]) ** 0.5 * X_noise)
+ (1 – alpha[t]) ** 0.5 * noise
)
return X
Les modèles de diffusion ont fait des progrès considérables récemment. En par-
ticulier, dans un article publié en décembre 2021279, Robin Rombach, Andreas
279. Robin Rombach, Andreas Blattmann, et al., « High-Resolution Image Synthesis with Latent Diffu-
sion Models », arXiv preprint arXiv:2112.10752 (2021): https://homl.info/latentdiff.
9.11 Exercices 437
Blattmann et al. ont présenté les modèles de diffusion latente, où le processus de dif-
fusion prend place dans l’espace latent, plutôt que dans l’espace des pixels. Pour ce
faire, ils utilisent un autoencodeur puissant pour compresser chaque image d’entraî-
nement dans un espace latent beaucoup plus petit dans lequel se passe le processus
de diffusion, puis ils utilisent l’autoencodeur pour décompresser la représentation
latente nale, an de générer l’image de sortie. Ceci accélère considérablement la
génération d’images et réduit grandement le temps d’entraînement et le coût. Chose
importante, la qualité des images générées est exceptionnelle.
De plus, les chercheurs ont aussi adapté différentes techniques de conditionne-
ment pour guider le processus de diffusion en utilisant des messages de texte, des
images, ou toute autre forme d’entrée. Il devient possible grâce à cela de produire
rapidement une superbe image haute résolution d’une salamandre en train de lire
un livre, ou de ce qui peut vous passer par la tête. Vous pouvez aussi contraindre le
processus de génération d’images à utiliser une image en entrée. Ceci ouvre la porte
à de nombreuses applications, comme de compléter une image d’entrée au-delà de
ses bords externes (en anglais, outpainting), ou au contraire d’en combler les manques
(inpainting).
Enn, un puissant modèle préentraîné de diffusion latente, nommé Stable Diffusion,
a été proposé en open source en août 2022: c’est le fruit d’une collaboration entre
l’université Louis-et-Maximilien de Münich et quelques entreprises parmi lesquelles
StabilityAI et Runway, aidées par EleutherAI et LAION. En septembre 2022, ce
modèle a été porté vers TensorFlow et inclus dans KerasCV (https://keras.io/keras_cv),
une bibliothèque consacrée à la vision par ordinateur construite par l’équipe Keras.
Désormais n’importe qui peut générer des images impressionnantes en quelques
secondes, gratuitement, même sur un ordinateur portable ordinaire (voir le dernier
exercice de ce chapitre). Les possibilités sont innombrables!
Dans le chapitre suivant, nous aborderons un domaine du Deep Learning totale-
ment différent: l’apprentissage par renforcement profond.
9.11 EXERCICES
1. Quelles sont les principales tâches dans lesquelles les autoencodeurs
sont employés ?
2. Supposons que vous souhaitiez entraîner un classicateur et que vous
disposiez d’un grand nombre de données d’entraînement non étiquetées,
mais seulement de quelques milliers d’instances étiquetées. En quoi les
autoencodeurs peuvent-ils vous aider ? Comment procéderiez-vous ?
3. Si un autoencodeur reconstruit parfaitement les entrées, est-il
nécessairement un bon autoencodeur ? Comment pouvez-vous
évaluer les performances d’un autoencodeur ?
4. Que sont les autoencodeurs sous-complets et sur-complets ? Quel est
le principal risque d’un autoencodeur excessivement sous-complet ?
Et celui d’un autoencodeur sur-complet ?
438 Chapitre 9. Autoencodeurs, GAN et modèles de diffusion
280. Pour de plus amples informations, consultez l’ouvrage Reinforcement Learning: An Introduction (MIT
Press), de Richard Sutton et Andrew Barto (https://homl.info/126).
281. Volodymyr Mnih et al., « Playing Atari with Deep Reinforcement Learning » (2013) : https://homl.
info/dqn.
282. Volodymyr Mnih et al., « Human-Level Control Through Deep Reinforcement Learning », Nature,
518 (2015), 529-533 : https://homl.info/dqn2.
283. Des vidéos montrant le système de DeepMind qui apprend à jouer à Space Invaders, Breakout et
d’autres sont disponibles à l’adresse https://homl.info/dqn3.
440 Chapitre 10. Apprentissage par renforcement
Figure 10.1 – Exemples d’apprentissage par renforcement : (a) robot, (b) Ms. Pac-Man,
(c) joueur de Go, (d) thermostat, (e) courtier automatique284
Parfois, les récompenses positives n’existeront pas. Par exemple, l’agent peut se
déplacer dans un labyrinthe, en recevant une récompense négative à chaque pas. Il
vaut donc mieux pour lui trouver la sortie aussi rapidement que possible ! Il existe de
nombreux autres exemples de tâches pour lesquelles l’apprentissage par renforcement
convient parfaitement, comme les voitures autonomes, les systèmes de recommanda-
tion, le placement de publicités dans les pages web ou encore le contrôle de la zone d’une
image sur laquelle un système de classication d’images doit focaliser son attention.
284. L’image (a) provient de la NASA (domaine public). (b) est une capture d’écran du jeu Ms. Pac-Man
d’Atari (l’auteur pense que son utilisation dans ce chapitre est acceptable). (c) et (d) proviennent de
Wikipédia. (c) a été créée par l’utilisateur Stevertigo et a été publiée sous licence Creative Commons
BY-SA 2.0 (https://creativecommons.org/licenses/by-sa/2.0/). (e) a été reproduite à partir de Pixabay, publiée
sous licence Creative Commons CC0 (https://creativecommons.org/publicdomain/zero/1.0/).
442 Chapitre 10. Apprentissage par renforcement
Agent Environnement
Actions
Récompenses
+ observations
Politique
a b
c
c d d
Comment pouvons-nous entraîner un tel robot ? Nous n’avons que deux para-
mètres de politique à ajuster : la probabilité p et la plage de l’angle r. L’algorithme
d’apprentissage pourrait consister à essayer de nombreuses valeurs différentes pour
ces paramètres et à retenir la combinaison qui afche les meilleures performances
(voir la gure10.3). Voilà un exemple de recherche de politique, dans ce cas fondée sur
une approche par force brute. Cependant, lorsque l’espace des politiques est trop vaste
(ce qui est fréquent), rechercher un bon jeu de paramètres revient à rechercher une
aiguille dans une gigantesque botte de foin.
10.3 Introduction à Gymnasium 443
Une autre manière d’explorer l’espace des politiques consiste à utiliser des algo-
rithmes génétiques. Par exemple, on peut créer aléatoirement une première généra-
tion de 100politiques et les essayer, puis «tuer » les 80 plus mauvaises285 et laisser
les 20survivantes produire chacune quatre descendants. Un descendant n’est rien
d’autre qu’une copie de son parent286 avec une variation aléatoire. Les politiques
survivantes et leurs descendants forment la deuxième génération. On peut ainsi
poursuivre cette itération sur les générations jusqu’à ce que l’évolution produise une
politique appropriée287 .
Une autre approche se fonde sur des techniques d’optimisation. Il s’agit d’éva-
luer les gradients des récompenses par rapport aux paramètres de la politique, puis
d’ajuster ces paramètres en suivant les gradients vers des récompenses plus élevées288.
Cette approche est appelée gradients de politique et nous y reviendrons plus loin dans
ce chapitre. Par exemple, dans le cas du robot aspirateur, on peut augmenter légè-
rement p et évaluer si cela augmente la quantité de poussière ramassée par le robot
en 30minutes. Dans l’afrmative, on augmente encore un peu p, sinon on le réduit.
Nous implémenterons avec TensorFlow un algorithme de gradients de politique bien
connu, mais avant cela nous devons commencer par créer un environnement dans
lequel opérera l’agent. C’est le moment de présenter la bibliothèque Gymnasium.
285. Il est souvent préférable de laisser une petite chance de survie aux moins bons éléments an de
conserver une certaine diversité dans le « patrimoine génétique ».
286. S’il n’y a qu’un seul parent, il s’agit d’une reproduction asexuée. Avec deux parents ou plus, il s’agit
d’une reproduction sexuée. Le génome d’un descendant (dans ce cas un jeu de paramètres de politique) est
constitué de portions aléatoires des génomes de ses parents.
287. L’algorithme NeuroEvolution of Augmenting Topologies (NEAT) (https://homl.info/neat) est un exemple
intéressant d’algorithme génétique utilisé pour l’apprentissage par renforcement.
288. Il s’agit d’une montée de gradient. Cela équivaut à une descente de gradient, mais dans le sens opposé :
maximisation à la place de minimisation.
444 Chapitre 10. Apprentissage par renforcement
289. OpenAI est une entreprise de recherche en intelligence articielle à but non lucratif cofondée par
Elon Musk. Son objectif annoncé est de promouvoir et de développer des intelligences articielles convi-
viales au prot de l’humanité (et non pour la détruire).
10.3 Introduction à Gymnasium 445
Angle
Vitesse angulaire
Vitesse 0
Position
Vous pouvez alors utiliser la fonction imshow() de Matplotlib pour afcher cette
image, comme d’habitude.
446 Chapitre 10. Apprentissage par renforcement
Discrete(2) signie que les actions possibles sont les entiers 0 et 1, qui repré-
sentent une accélération vers la gauche (0) ou vers la droite (1). D’autres environ-
nements peuvent proposer plus d’actions discrètes ou d’autres types d’actions (par
exemple, continues). Puisque le bâton penche vers la droite (obs[2] > 0), accé-
lérons le chariot vers la droite:
>>> action = 1 # accélérer vers la droite
>>> obs, reward, done, truncated, info = env.step(action)
>>> obs
array([ 0.02727336, 0.18847767, 0.03625453, -0.26141977], dtype=float32)
>>> reward
1.0
>>> done
False
>>> truncated
False
>>> info
{}
Implémentons une politique simple qui déclenche une accélération vers la gauche
lorsque le bâton penche vers la gauche, et une accélération vers la droite lorsque
le bâton penche vers la droite. Ensuite, exécutons-la pour voir quelle récompense
moyenne elle permet d’obtenir après 500 épisodes :
def basic_policy(obs):
angle = obs[2]
return 0 if angle < 0 else 1
totals = []
for episode in range(500):
episode_rewards = 0
obs, info = env.reset(seed=episode)
for step in range(200):
action = basic_policy(obs)
obs, reward, done, truncated, info = env.step(action)
episode_rewards += reward
if done or truncated:
break
totals.append(episode_rewards)
Même au bout de 500 essais, cette stratégie n’a pas réussi à garder le bâton vertical
pendant plus de 63 étapes consécutives. Peu satisfaisant. Si vous observez la simu-
lation dans le notebook de ce chapitre 290, vous constaterez que le chariot oscille à
gauche et à droite de plus en plus fortement jusqu’à ce que le bâton soit trop incliné.
Voyons si un réseau de neurones ne pourrait pas aboutir à une meilleure politique.
de nouvelles actions et exploiter les actions réputées bien fonctionner. Voici une
analogie: supposons que vous alliez dans un restaurant pour la première fois et que,
puisque tous les plats semblent aussi appétissants l’un que l’autre, vous en choisissiez
un au hasard. S’il est effectivement bon, vous augmentez sa probabilité de le com-
mander la prochaine fois, mais vous ne devez pas la passer à 100% car cela vous
empêcherait d’essayer d’autres plats, qui pourraient être meilleurs que votre choix
initial. Ce dilemme exploration/exploitation est au cœur de l’apprentissage par ren-
forcement.
Action
Échantillonnage multinomial
Couche cachée
x1 x2 x3 x4 Observations
model = tf.keras.Sequential([
tf.keras.layers.Dense(5, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid"),
])
10.5 Évaluer des actions : le problème d’affectation de crédit 449
Bien entendu, une bonne action peut être suivie de plusieurs mauvaises actions
qui provoquent la chute rapide du bâton. Dans ce cas, la bonne action reçoit un
rendement faible, comme un bon acteur peut parfois jouer dans un très mauvais
lm. Cependant, si l’on joue un nombre sufsant de parties, les bonnes actions
obtiendront en moyenne un meilleur rendement que les mauvaises. Nous souhai-
tons estimer la qualité moyenne d’une action en comparaison des autres actions
possibles. Il s’agit du bénéce de l’action (action advantage). Pour cela, nous devons
exécuter de nombreux épisodes et normaliser tous les rendements des actions (en
soustrayant la moyenne et en divisant par l’écart-type). Suite à cela, on peut rai-
sonnablement supposer que les actions ayant un bénéce négatif étaient mauvaises,
tandis que celles ayant un bénéce positif étaient bonnes. Puisque nous avons à pré-
sent une solution pour évaluer chaque action, nous sommes prêts à entraîner notre
premier agent en utilisant des gradients de politique.
291. Ronald J. Williams, « Simple Statistical Gradient-Following Algorithms for Connectionist Reinfor-
cement Leaning », Machine Learning, 8 (1992), 229-256 : https://homl.info/132.
10.6 Gradients de politique 451
all_rewards.append(current_rewards)
all_grads.append(current_grads)
Ce code renvoie une liste de listes de récompenses (une liste de récompenses par
épisode, contenant une récompense par étape), ainsi qu’une liste de listes de gra-
dients (une liste de gradients par épisode, chacun contenant un n-uplet de gradients
par étape, chacun d’eux contenant un tenseur de gradient par variable entraînable).
L’algorithme utilisera la fonction play_multiple_episodes() pour jouer
plusieurs parties (par exemple, dix), puis reviendra en arrière pour examiner toutes
les récompenses, leur appliquer un rabais et les normaliser. Pour cela, nous avons
besoin de deux autres fonctions. La première calcule la somme des récompenses
futures avec rabais à chaque étape. La seconde normalise toutes ces récompenses
avec rabais (rendements) sur plusieurs épisodes, en soustrayant la moyenne et en
divisant par l’écart-type.
def discount_rewards(rewards, discount_factor):
discounted = np.array(rewards)
for step in range(len(rewards) - 2, -1, -1):
discounted[step] += discounted[step + 1] * discount_factor
return discounted
flat_rewards = np.concatenate(all_discounted_rewards)
reward_mean = flat_rewards.mean()
reward_std = flat_rewards.std()
return [(discounted_rewards - reward_mean) / reward_std
for discounted_rewards in all_discounted_rewards]
Examinons ce code :
• À chaque itération d’entraînement, la boucle appelle la fonction play_
multiple_episodes(), qui joue dix parties (ou épisodes) et retourne
toutes les récompenses et tous les gradients de chaque étape de chaque épisode.
• Ensuite, nous appelons discount_and_normalize_rewards() pour
calculer le bénéce normalisé de chaque action (que, dans le code, nous
appelons final_reward). Cela nous indique après coup la qualité réelle de
chaque action.
• Puis nous parcourons chaque variable d’entraînement et, pour chacune, nous
calculons la moyenne pondérée des gradients sur tous les épisodes et toutes les
étapes, pondérée par final_reward.
• Enn, nous appliquons ces gradients moyens en utilisant l’optimiseur. Les
variables entraînables du modèle seront ajustées et, espérons-le, la politique
sera un peu meilleure.
Et voilà! Ce code entraînera la politique par réseau de neurones et apprendra à
équilibrer le bâton placé sur le chariot. La récompense moyenne par épisode sera très
proche de 200 (le maximum par défaut avec cet environnement). C’est gagné !
L’algorithme de gradients de politique que nous venons d’entraîner a résolu la tâche
CartPole, mais il ne s’adapterait pas bien à des tâches plus grandes et plus complexes.
À vrai dire, il est hautement inefcace dans l’exploitation des exemples (en anglais,
sample inefcient), ce qui signie qu’il doit explorer le jeu pendant très longtemps
avant de pouvoir véritablement progresser. Ceci est dû au fait qu’il doit exécuter
de nombreuses parties pour estimer l’avantage de chaque action. Cependant, ceci
constitue la base d’algorithmes plus puissants, tels que les algorithmes acteur-critique
(dont nous parlerons brièvement la n de ce chapitre).
Nous allons à présent examiner une autre famille d’algorithmes très en vogue.
Alors que les algorithmes de gradients de politique essaient d’optimiser directement
la politique de façon à augmenter les récompenses, ces nouveaux algorithmes opèrent
de façon plus indirecte. L’agent apprend à estimer le rendement attendu pour chaque
état, ou pour chaque action dans chaque état, puis décide d’agir en fonction de ces
connaissances. Pour comprendre ces algorithmes, nous devons commencer par étu-
dier les processus de décision markoviens.
10.7 Processus de décision markoviens 455
0.9
0.2
0.7 S0 S1 S2
1.0
Supposons que le processus démarre dans l’état s 0 et que ses chances de rester
dans cet état à l’étape suivante soient de 70%. À terme, il nira bien par quitter
cet état pour ne jamais y revenir, car aucun autre état ne pointe vers s0 . S’il passe
dans l’états 1, il ira probablement ensuite dans l’état s2 (probabilité de 90%), pour
revenir immédiatement dans l’état s1 (probabilité de 100%). Il peut osciller un cer-
tain nombre de fois entre ces deux états mais nira par aller dans l’état s3, pour y rester
indéniment car il n’y a pas de chemin pour en sortir : il s’agit d’un état terminal. Les
chaînes de Markov peuvent avoir des dynamiques très différentes et sont très utilisées
en thermodynamique, en chimie, en statistiques et bien d’autres domaines.
Les processus de décision markoviens (Markov decision processes, ou MDP) ont été
décrits292 pour la première fois dans les années 1950 par Richard Bellman. Ils res-
semblent aux chaînes de Markov mais, à chaque étape, un agent peut choisir parmi
plusieurs actions possibles et les probabilités des transitions dépendent de l’action
choisie. Par ailleurs, certaines transitions entre états renvoient une récompense
(positive ou négative) et l’objectif de l’agent est de trouver une politique qui maxi-
mise les récompenses au l du temps.
Par exemple, le MDP représenté à la gure10.8 possède trois états (représentés par
des cercles) et jusqu’à trois actions discrètes possibles à chaque étape (représentées
par des losanges). S’il démarre dans l’état s0 , l’agent peut choisir entre les actions a0 ,
a1 et a2 . S’il opte pour l’action a1, il reste dans l’état s 0 avec certitude et sans aucune
292. Richard Bellman, « A Markovian Decision Process », Journal of Mathematics and Mechanics, 6, n°5
(1957), 679-684 : https://homl.info/133.
456 Chapitre 10. Apprentissage par renforcement
récompense. Il peut ensuite décider d’y rester à jamais s’il le souhaite. En revanche,
s’il choisit l’action a0 , il a une probabilité de 70% d’obtenir une récompense égale à
+10 et de rester dans l’état s0. Il peut ensuite essayer de nouveau pour obtenir autant
de récompenses que possible. Mais, à un moment donné, il nira bien par aller dans
l’état s1 , où il n’a que deux actions possibles : a0 et a2 . Il peut décider de ne pas bouger
en choisissant en permanence l’action a0 ou de passer dans l’état s2 en recevant une
récompense négative égale à –50. Dans l’état s2 , il n’a pas d’autre choix que d’effec-
tuer l’action a1, qui le ramènera très certainement dans l’état s0 , gagnant au passage
une récompense de +40.
+40
0.8
1.0 a1 a1 0.1
0.1
–50
S0 a2 0.2 S1 a2 S2
0.8 1.0
+10
a0 0.3 a0 1.0
0.7
pour tout s
Dans cette équation:
• T(s, a, s’) est la probabilité de transition de l’état s vers l’état s’ si l’agent choisit
l’action a. Par exemple, à la gure10.8, T(s 2, a1, s 0 ) =0,8.
10.7 Processus de décision markoviens 457
• R(s, a, s’) est la récompense obtenue par l’agent lorsqu’il passe de l’état s à l’état
s’ si l’agent choisit l’action a. Par exemple, à la gure10.8, R(s 2, a1 , s0) =+40.
• γ est le facteur de rabais.
Cette équation conduit directement à un algorithme qui permet d’estimer précisé-
ment la valeur d’état optimale de chaque état possible. On commence par initialiser
toutes les estimations des valeurs d’états à zéro et on les actualise progressivement en
utilisant un algorithme d’itération sur la valeur (voir l’équation10.2). Si l’on prend
le temps nécessaire, ces estimations vont obligatoirement converger vers les valeurs
d’état optimales, qui correspondent à la politique optimale.
pour tout s
Dans cette équation, Vk(s) est la valeur estimée de l’état s au cours de la kième itér-
ation de l’algorithme.
Connaître les valeurs d’état optimales est utile, en particulier pour évaluer une poli-
tique, mais cela ne dit pas explicitement à l’agent ce qu’il doit faire. Heureusement,
Bellman a trouvé un algorithme comparable pour estimer les valeurs état-action opti-
males, généralement appelées valeurs qualité ou valeurs Q. La valeur Q optimale du
couple état-action (s, a), notée Q* (s, a), est la somme des récompenses futures avec
rabais que l’agent peut espérer en moyenne après avoir atteint l’état s et choisi l’ac-
tion a, mais avant qu’il ne voie le résultat de cette action, en supposant qu’il agisse de
façon optimale après cette action.
Voici comment il fonctionne. Une fois encore, on commence par initialiser toutes
les estimations des valeurs Q à zéro, puis on les actualise à l’aide de l’algorithme
d’itération sur la valeur Q (Q-value iteration) (voir l’équation10.3).
Après avoir obtenu les valeurs Q optimales, il n’est pas difcile de dénir la politique
optimale, notée π*(s) : lorsque l’agent se trouve dans l’état s, il doit choisir l’action
* *
qui possède la valeur Q la plus élevée pour cet état, c’est-à-dire π ( s) = max Q ( s, a) .
a
Appliquons cet algorithme au MDP représenté à la gure10.8. Commençons par
dénir celui-ci :
transition_probabilities = [ # forme=[s, a, s']
[[0.7, 0.3, 0.0], [1.0, 0.0, 0.0], [0.8, 0.2, 0.0]],
[[0.0, 1.0, 0.0], None, [0.0, 0.0, 1.0]],
458 Chapitre 10. Apprentissage par renforcement
Par exemple, lorsque l’agent se trouve dans l’état s0 et choisit l’action a1 , la somme
attendue des récompenses futures avec rabais est environ égale à 17,0.
Pour chaque état, nous pouvons trouver l’action qui possède la plus haute valeur Q :
>>> Q_values.argmax(axis=1) # action optimale pour chaque état
array([0, 0, 1])
est sensé, car plus on accorde de valeur aux récompenses futures, plus on est prêt à
endurer les souffrances présentes pour obtenir la récompense ultérieure.
Vk +1 (s ) ← (1 – α )Vk ( s) + α (r + γ ⋅ Vk ( s′))
ou, de façon équivalente :
Vk +1 (s ) ← Vk (s ) + α ⋅ δk ( s, r, s′ )
avec
δ k ( s, r, s′) = r + γ ⋅ Vk ( s ′) – Vk (s )
Dans cette équation :
• α est le taux d’apprentissage (par exemple, 0,01).
• r + γ · Vk(s’) est appelé cible de différence temporelle, ou cible TD.
• δ k(s, r, s’) est appelé erreur de différence temporelle, ou erreur TD.
Une façon plus concise d’écrire la première forme de cette équation se fonde sur
la notation a b , qui signie ak+1 ← (1 – α) ⋅ ak + α ⋅ bk . La première ligne de l’équa-
tion10.4 peut donc être récrite ainsi : V ( s)← r +γ ⋅ V ( s′ ) .
α
10.9 APPRENTISSAGE Q
De manière comparable, l’algorithme d’apprentissage Q (Q-Learning) correspond à
l’algorithme d’itération sur la valeur Q adapté au cas où les probabilités des transitions
et les récompenses sont initialement inconnues (voir l’équation10.5). Ilregarde un
agent jouer (par exemple, aléatoirement) et améliore progressivement ses estima-
tions des valeurs Q. Lorsqu’il dispose d’estimations de valeur Q précises (ou sufsam-
ment proches), la politique optimale est de choisir l’action qui possède la valeur Q la
plus élevée (autrement dit, la politique gloutonne).
Q( s , a) ← r + γ ⋅ max Q k ( s′ , a′)
α a′
Pour chaque couple état-action (s, a), cet algorithme conserve une moyenne
mobile de la récompense r reçue par l’agent lorsqu’il quitte l’état s avec l’action a
plus la somme des récompenses futures avec rabais qu’il espère obtenir. Pour estimer
cette somme, nous prenons le maximum des estimations de la valeur Q pour l’état
s’ suivant, car nous supposons que la politique cible travaillera à terme de manière
optimale.
Implémentons cet algorithme d’apprentissage Q. Tout d’abord, nous devons faire
en sorte qu’un agent explore l’environnement. Pour cela, nous avons besoin d’une
fonction (step) qui permette à l’agent d’exécuter une action et d’obtenir l’état et la
récompense résultants :
def step(state, action):
probas = transition_probabilities[state][action]
next_state = np.random.choice([0, 1, 2], p=probas)
reward = rewards[state][action][next_state]
return next_state, reward
Ensuite, après avoir initialisé les valeurs Q comme précédemment, nous sommes
prêts à exécuter l’algorithme d’apprentissage Q avec une décroissance du taux d’ap-
prentissage (en utilisant la décroissance hyperbolique décrite au chapitre3):
alpha0 = 0.05 # taux d’apprentissage initial
decay = 0.005 # décroissance du taux d’apprentissage
gamma = 0.90 # facteur de rabais
10.9 Apprentissage Q 461
Cet algorithme convergera vers les valeurs Q optimales, mais il faudra de nom-
breuses itérations et, potentiellement, un assez grand nombre d’ajustements des
hyperparamètres. Vous pouvez le voir à la gure 10.9, l’algorithme d’itération sur
la valeur Q (à gauche) converge très rapidement, en moins de 20 itérations, tandis
que la convergence de l’algorithme d’apprentissage Q (à droite) demande environ
8 000itérations. Il est clair que ne pas connaître les probabilités de transition ou les
récompenses complique énormément la recherche de la politique optimale !
20
Q– values (s0, a0)
15
10
0
0 10 20 30 40 50 0 2000 4000 6000 8000 10000
Itérations Itérations
y
Avec cette valeur Q cible, nous pouvons exécuter une étape d’entraînement à
l’aide de tout algorithme de descente de gradient. Plus précisément, nous essayons
en général de minimiser l’erreur quadratique entre la valeur Q estimée Q θ(s, a) et la
valeur Q cible y(s, a) (ou la perte de Hubber pour réduire la sensibilité de l’algorithme
aux grandes erreurs). Voilà tout pour l’apprentissage Q profond de base! Voyons com-
ment l’implémenter pour résoudre l’environnement CartPole.
model = tf.keras.Sequential([
tf.keras.layers.Dense(32, activation="elu", input_shape=input_shape),
464 Chapitre 10. Apprentissage par renforcement
tf.keras.layers.Dense(32, activation="elu"),
tf.keras.layers.Dense(n_outputs)
])
Avec ce réseau Q profond, l’action choisie est celle dont la valeur Q prédite est
la plus grande. Pour nous assurer que l’agent explore l’environnement, nous utilisons
une politique ε-gourmande (autrement dit, nous choisissons une action aléatoire
avec une probabilitéε) :
def epsilon_greedy_policy(state, epsilon=0):
if np.random.rand() < epsilon:
return np.random.randint(n_outputs) # action au hasard
else:
Q_values = model.predict(state[np.newaxis], verbose=0)[0]
return Q_values.argmax() # action optimale selon le DQN
replay_buffer = deque(maxlen=2000)
Un deque est une file d’attente dans laquelle on peut facilement ajouter
ou supprimer des éléments aux deux extrémités. Ajouter et supprimer des
éléments aux deux extrémités est très rapide, mais l’accès aléatoire peut
être lent lorsque la file d’attente devient longue. Si vous avez besoin d’un
très grand tampon de rejeu, utilisez plutôt un tampon circulaire (voir une
implémentation dans le notebook de ce chapitre 293) ou consultez la biblio-
thèque Reverb de DeepMin (https://homl.info/reverb).
Créons également une fonction qui réalise une seule étape en utilisant la politique
ε-gloutonne, puis stocke l’expérience résultante dans la mémoire de rejeu :
def play_one_step(env, state, epsilon):
action = epsilon_greedy_policy(state, epsilon)
next_state, reward, done, truncated, info = env.step(action)
replay_buffer.append((state, action, reward, next_state, done, truncated))
return next_state, reward, done, truncated, info
Pour nir, écrivons une dernière fonction qui échantillonne un lot d’expériences
à partir de la mémoire de rejeu et entraîne le DQN en réalisant une seule étape de
descente de gradient sur ce lot :
batch_size = 32
discount_factor = 0.95
optimizer = tf.keras.optimizers.Nadam(learning_rate=1e-2)
loss_fn = tf.keras.losses.mean_squared_error
def training_step(batch_size):
experiences = sample_experiences(batch_size)
states, actions, rewards, next_states, dones, truncateds = experiences
next_Q_values = model.predict(next_states, verbose=0)
max_next_Q_values = next_Q_values.max(axis=1)
runs = 1.0 – (dones | truncateds) # l’épisode n’est ni terminé
# ni interrompu
target_Q_values = rewards + runs * discount_factor * max_next_Q_values
target_Q_values = target_Q_values.reshape(-1, 1)
mask = tf.one_hot(actions, n_outputs)
with tf.GradientTape() as tape:
all_Q_values = model(states)
Q_values = tf.reduce_sum(all_Q_values * mask, axis=1, keepdims=True)
loss = tf.reduce_mean(loss_fn(target_Q_values, Q_values))
200
175
Somme des récompenses
150
125
100
75
50
25
0
0 100 200 300 400 500 600
Épisode
294. Un excellent billet publié en 2018 par Alex Irpan expose parfaitement les plus grandes difcultés et
limites de l’apprentissage par renforcement : https://homl.info/rlhard.
468 Chapitre 10. Apprentissage par renforcement
Nous n’avons pas afché la perte, car il s’agit d’une piètre mesure de la performance
du modèle. La perte peut baisser alors que l’agent est mauvais. Par exemple, si l’agent
reste bloqué dans une petite région de l’environnement et si le DQN commence à
surajuster cette région. À l’inverse, la perte peut augmenter alors que l’agent travaille
mieux. Par exemple, si le DQN avait sous-estimé les valeurs Q et s’il commence à
améliorer ses prédictions, l’agent afchera de meilleures performances, obtiendra plus
de récompense, mais la perte peut augmenter car le DQN xe également les cibles,
qui seront aussi plus grandes. C’est pourquoi il est préférable de représenter graphi-
quement les récompenses.
L’algorithme d’apprentissage Q profond de base que nous avons utilisé est trop
instable pour apprendre à jouer aux jeux d’Atari. Comment ont donc procédé les
chercheurs de DeepMind? Ils ont simplement perfectionné l’algorithme.
Puisque le modèle cible est actualisé moins souvent que le modèle en ligne, les
cibles de la valeur Q sont plus stables, la boucle de rétroaction mentionnée est atté-
nuée et ses effets sont moins sévères. Cette approche a été l’une des principales
contributions des chercheurs de DeepMind dans leur article de 2013: elle a permis
aux agents d’apprendre à jouer aux jeux Atari à partir des pixels bruts. Pour stabiliser
10.11 Variantes de l’apprentissage Q profond 469
l’entraînement, ils ont utilisé un taux d’apprentissage minuscule égal à 0,00025, n’ont
actualisé le modèle cible que toutes les 10 000 étapes (à la place de 50 dans l’exemple
de code précédent), et ont employé une très grande mémoire de rejeu d’unmillion
d’expériences. Ils ont fait baisser epsilon très lentement, de 1 à 0,1 en unmillion
d’étapes, et ils ont laissé l’algorithme s’exécuter pendant 50 millions d’étapes. De
plus, leur DQN était un réseau convolutif profond.
Maintenant, examinons une autre variante de DQN qui a réussi une fois de plus à
surpasser ce qui se faisait de mieux jusque-là.
Quelques mois plus tard, une autre amélioration de l’algorithme DQN était pro-
posée. Nous allons l’examiner.
295. Hado van Hasselt et al., « Deep Reinforcement Learning with Double Q-Learning », Proceedings of the
30th AAAI Conference on Articial Intelligence (2015), 2094-2100 : https://homl.info/doubledqn.
470 Chapitre 10. Apprentissage par renforcement
Pour comprendre son fonctionnement, nous devons tout d’abord remarquer que
la valeur Q d’un couple état-action (s, a) peut être exprimée sous la forme Q(s, a)
= V(s)+ A(s, a), où V(s) est la valeur de l’état s et A(s, a) correspond à l’avantage
de prendre l’action a dans l’état s, en comparaison de toutes les autres actions pos-
sibles dans cet état. Par ailleurs, la valeur d’un état est égale à la valeur Q de la
meilleure action a* pour cet état (puisque nous supposons que la politique optimale
sélectionnera la meilleure action). Par conséquent, V(s) = Q(s, a*), ce qui implique
que A(s,a*) = 0. Dans un duel de DQN, le modèle estime à la fois la valeur de l’état
et l’avantage de chaque action possible. Puisque la meilleure action doit avoir un
avantage égal à zéro, le modèle soustrait l’avantage maximal prédit de tous les avan-
tages prédits.
Voici un modèle de duel de DQN simple, implémenté avec l’API fonctionnelle:
input_states = tf.keras.layers.Input(shape=[4])
hidden1 = tf.keras.layers.Dense(32, activation="elu")(input_states)
hidden2 = tf.keras.layers.Dense(32, activation="elu")(hidden1)
state_values = tf.keras.layers.Dense(1)(hidden2)
raw_advantages = tf.keras.layers.Dense(n_outputs)(hidden2)
advantages = raw_advantages – tf.reduce_max(raw_advantages, axis=1,
keepdims=True)
Q_values = state_values + advantages
model = tf.keras.Model(inputs=[input_states], outputs=[Q_values])
299. Matteo Hessel et al., « Rainbow: Combining Improvements in Deep Reinforcement Learning »
(2017), 3215-3222 : https://homl.info/rainbow.
300. David Silver et al., « Mastering the Game of Go with Deep Neural Networks and Tree Search »,
Nature 529 (2016), 484-489 : https://homl.info/alphago.
472 Chapitre 10. Apprentissage par renforcement
301. David Silver et al., « Mastering the Game of Go Without Human Knowledge », Nature 550 (2017),
354-359 : https://homl.info/alphagozero.
302. David Silver et al., « Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning
Algorithm », arXiv preprint arXiv:1712.01815 : https://homl.info/alphazero.
303. Julian Schrittwieser et al., « Mastering Atari, Go, Chess and Shogi by Planning with a Learned
Model », arXiv preprint arXiv:1911.08265 (2019) : https://homl.info/muzero.
304. Volodymyr Mnih et al., « Asynchonous Methods for Deep Reinforcement Learning », Proceedings of
the 33rd International Conference on Machine Learning (2016), 1928 -1937 : https://homl.info/a3c.
10.12 Quelques algorithmes RL intéressants 473
éléments appris par les autres agents. Par ailleurs, au lieu d’estimer les valeursQ,
le DQN estime l’avantage de chaque action, ce qui stabilise l’entraînement.
• Acteur-critique à avantages 305 (A2C, advantage actor-critic)
Cette variante de l’algorithme A3C retire l’asynchronisme. Puisque toutes les
actualisations du modèle sont synchrones, les actualisations des gradients sont
effectuées sur des lots plus grands. Cela permet au modèle de mieux proter de
la puissance du GPU.
• Acteur-critique soft306 (SAC, soft actor-critic)
Il s’agit d’une variante de l’acteur-critique proposée en 2018 par Tuomas
Haarnoja et d’autres chercheurs de l’université de Californie àBerkeley. Elle
apprend non seulement les récompenses, mais maximiseégalement l’entropie
de ses actions. Autrement dit, elle tente d’être aussiimprévisible que possible
tout en obtenant autant de récompenses que possible.Cela encourage l’agent
à explorer l’environnement, accélérant ainsi l’entraînement, et réduit la
probabilité qu’il exécute de façon répétée la même action lorsque le DQN
produit des estimations imparfaites. Cet algorithme a démontré une efcacité
étonnante (contrairement à tous les algorithmes antérieurs à SAC, qui
apprennent très lentement).
• Optimisation de politique proximale307 (PPO, proximal policy optimization)
Cet algorithme proposé par John Schulman et d’autres chercheurs d’OpenAI
se fonde sur A2C mais il rogne la fonction de perte de façon à éviter
les actualisations excessivement importantes des poids (souvent sources
d’instabilité de l’entraînement). PPO est une simplication de l’algorithme
d’optimisation de la politique de région de conance 308 (TRPO, trust region policy
optimization), également proposé par des chercheurs d’OpenAI. Cette dernière
a fait sensation en avril2019 avec son intelligence articielle, appelée OpenAI
Five et fondée sur l’algorithme PPO, qui a battu les champions du monde du
jeu multijoueur Dota2.
• Exploration fondée sur la curiosité309 (curiosity-based exploration)
La rareté des récompenses est un problème récurrent de l’apprentissage par
renforcement. L’apprentissage s’en trouve très lent et inefcace. Deepak Pathak
et d’autres chercheurs de l’université de Californie à Berkeley ont proposé
une manière très intéressante d’aborder ce problème : pourquoi ne pas ignorer
les récompenses et rendre l’agent extrêmement curieux pour qu’il explore
l’environnement ? Les récompenses ne viennent plus de l’environnement mais
305. https://homl.info/a2c
306. Tuomas Haarnoja et al., « Soft Actor-Critic: Off-Policy Maximum Entropy Deep Reinforcement
Learning with a Stochastic Actor», Proceedings of the 35th International Conference on Machine Learning
(2018), 1856-1865 : https://homl.info/sac.
307. John Schulman et al., « Proximal Policy Optimization Algorithms » (2017) : https://homl.info/ppo.
308. John Schulman et al., « Trust Region Policy Optimization », Proceedings of the 32nd International
Conference on Machine Learning (2015), 1889-1897 : https://homl.info/trpo.
309. Deepak Pathak et al., « Curiosity-Driven Exploration by Self-Supervised Prediction », Proceedings of
the 34th International Conference on Machine Learning (2017), 2778-2787 : https://homl.info/curiosity.
474 Chapitre 10. Apprentissage par renforcement
310. Rui Wang et al., « Paired Open-Ended Trailblazer (POET): Endlessly Generating Increasingly
Complex and Diverse Learning Environments and Their Solutions », arXiv preprint arXiv:1901.01753
(2019) : https://homl.info/poet.
311. Rui Wang et al., « Enhanced POET: Open-Ended Reinforcement Learning Through Unbounded
Invention of Learning Challenges and Their Solutions », arXiv preprint arXiv:2003.08536 (2020) : https://
homl.info/epoet.
312. Open-Ended Learning Team et al., « Open-Ended Learning Leads to Generally Capable Agents »,
arXiv preprint arXiv:2107.12808 (2021) : https://homl.info/oel2021.
10.13 Exercices 475
10.13 EXERCICES
1. Comment déniriez-vous l’apprentissage par renforcement ? En quoi
est-il différent d’un entraînement supervisé ou non supervisé classique ?
2. Imaginez trois applications possibles de l’apprentissage par
renforcement que nous n’avons pas mentionnées dans ce chapitre.
Pour chacune d’elles, décrivez l’environnement approprié, l’agent,
les actions possibles et les récompenses.
3. Qu’est-ce que le facteur de rabais ? La politique optimale change-
t-elle si le facteur de rabais est modié ?
4. Comment pouvez-vous mesurer les performances d’un agent dans
l’apprentissage par renforcement ?
5. Qu’est-ce que le problème d’affectation du crédit ? Quand survient-
il? Comment peut-il être réduit ?
6. Quel est l’intérêt d’utiliser une mémoire de rejeu ?
7. Qu’est-ce qu’un algorithme d’apprentissage par renforcement hors
politique ?
8. Utilisez les gradients de politique pour résoudre l’environnement
LunarLander-v2 de Gymnasium.
9. Utilisez un duel de DQN doubles pour entraîner un agent capable
d’atteindre le niveau surhumain dans le fameux jeu Breakout d’Atari
("ALE/Breakout-v5"). Les observations sont des images.
Pour simplier la tâche, convertissez-les en niveaux de gris (en
effectuant une moyenne sur l’axe des canaux), puis rognez-les et
sous-échantillonnez-les de sorte qu’elles soient juste assez grandes
pour jouer, mais pas plus. Une image prise isolément ne vous dit pas
dans quel sens vont la balle et les raquettes, c’est pourquoi vous devez
regrouper deux ou trois images consécutives pour constituer chaque
état. Enn, le DQN doit être composé principalement de couches de
convolution.
476 Chapitre 10. Apprentissage par renforcement
10. Si vous avez une centaine d’euros à y consacrer, vous pouvez acheter
un Raspberry Pi 3 et quelques composants robotiques bon marché,
installer TensorFlow sur le Pi et partir à l’aventure ! Consultez, par
exemple, le billet amusant publié par Lukas Biewald (https://homl.
info/2) ou jetez un œil à GoPiGo ou à BrickPi. Commencez avec
des objectifs simples, comme faire tourner le robot an de trouver
l’angle le plus lumineux (s’il est équipé d’un capteur lumineux) ou
l’objet le plus proche (s’il est équipé d’un capteur à ultrasons) et de
le déplacer dans cette direction. Exploitez ensuite le Deep Learning.
Par exemple, si le robot dispose d’une caméra, vous pouvez essayer
d’implémenter un algorithme de détection d’objets an qu’il repère
les personnes et se dirige vers elles. Vous pouvez également essayer
de mettre en place un apprentissage par renforcement de façon
que l’agent apprenne lui-même comment utiliser ses moteurs pour
atteindre cet objectif. Amusez-vous !
Les solutions de ces exercices sont données à l’annexeA.
11
Entraînement
et déploiement
à grande échelle
de modèles TensorFlow
Vous disposez d’un beau modèle qui réalise d’époustouantes prédictions. Très bien,
mais que pouvez-vous en faire ? Le mettre en production, évidemment ! Par exemple,
vous pourriez tout simplement exécuter le modèle sur un lot de données, en écrivant
éventuellement un script qui lance la procédure chaque nuit. Cependant, la mise en
production est souvent beaucoup plus complexe. Il est possible que différentes parties
de votre infrastructure aient besoin d’appliquer le modèle sur des données en temps
réel, auquel cas vous voudrez probablement l’intégrer dans un service web. De cette
manière, n’importe quelle partie de l’infrastructure peut interroger le modèle à tout
moment en utilisant une simple API REST (ou tout autre protocole)313.
Mais, au bout d’un certain temps, il faudra certainement réentraîner le modèle
sur des données récentes et mettre cette version actualisée en production. Vous
devez donc assurer la gestion des versions du modèle, en proposant une transition
en douceur d’une version à la suivante, avec la possibilité éventuelle de revenir au
modèle précédent en cas de problème, voire exécuter plusieurs modèles différents en
parallèle pour effectuer des tests A/B 314. En cas de succès de votre produit, votre ser-
vice va commencer à recevoir un grand nombre de requêtes par seconde (QPS, queries
per second) et devra changer d’échelle pour supporter la charge. Nous le verrons dans
313. Voir le chapitre2 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3 eédition,
2023).
314. Un test A/B consiste à proposer deux versions différentes d’un produit à différents groupes d’utilisa-
teurs an de déterminer celle qui fonctionne le mieux et d’obtenir d’autres informations.
478 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
ce chapitre, une bonne solution pour augmenter l’échelle de votre service consiste
à utiliser TF Serving, soit sur votre propre infrastructure matérielle, soit au travers
d’un service de cloud comme Google Vertex AI315. TF Serving se chargera de servir
votre modèle de façon efcace, de traiter les changements de version en douceur, et
de bien d’autres aspects. Si vous optez pour une plateforme de cloud, vous béné-
cierez également de nombreuses autres fonctionnalités, comme des outils de super-
vision puissants.
Par ailleurs, si la quantité de données d’entraînement est importante et si les
modèles demandent des calculs intensifs, le temps d’entraînement risque d’être extrê-
mement long. Dans le cas où votre produit doit s’adapter rapidement à des change-
ments, une durée d’entraînement trop longue risque d’être rédhibitoire (imaginez, par
exemple, un système de recommandations d’informations qui promeut des nouvelles
de la semaine précédente). Peut-être plus important encore, un entraînement trop
long pourrait vous empêcher d’expérimenter de nouvelles idées. Dans le domaine du
Machine Learning, comme dans bien d’autres, il est difcile de savoir à l’avance les
idées qui fonctionneront. Vous devez donc en essayer autant que possible, aussi rapi-
dement que possible. Une manière d’accélérer l’entraînement consiste à employer
des accélérateurs matériels, comme des GPU ou des TPU. Pour aller encore plus vite,
vous pouvez entraîner un modèle sur plusieurs machines, chacune équipée de multi-
ples accélérateurs matériels. L’API de stratégies de distribution de TensorFlow, simple
et néanmoins puissante, facilite une telle mise en place.
Dans ce chapitre, nous verrons comment déployer des modèles, tout d’abord avec
TF Serving, puis avec Vertex AI. Nous expliquerons brièvement comment déployer
des modèles sur des applications mobiles, des dispositifs embarqués et des applica-
tions web. Puis nous montrerons comment accélérer les calculs en utilisant des GPU
et comment entraîner des modèles sur plusieurs processeurs et serveurs à l’aide de
l’API de stratégies de distribution. Enn, nous verrons comment utiliser Vertex AI
pour entraîner des modèles et régler nement leurs hyperparamètres à moindre coût.
Cela fait beaucoup de sujets à traiter, commençons tout de suite!
315. Google AI Platform (connue précédemment sous l’appellation Google ML Engine) et Google
AutoML ont fusionné en 2021 pour former Google Vertex AI.
316. Une API REST (ou RESTful) utilise des verbes HTTP, comme GET, POST, PUT et DELETE, et des
entrées et des sorties au format JSON. Le protocole gRPC est plus complexe, mais aussi plus efcace. Les
échanges de données se font par l’intermédiaire de tampons de protocole (voir le chapitre5).
11.1 Servir un modèle TensorFlow 479
TF Serving
Entrées
Modèle A v2
Applications
Prédictions Modèle B v1
Déploiement
automatique
Supposons que vous ayez entraîné un modèle MNIST avec Keras et que vous
souhaitiez le déployer avec TF Serving. La première chose à faire est d’exporter ce
modèle au format SavedModel présenté au chapitre 2.
model_name = "my_mnist_model"
480 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
model_version = "0001"
model_path = Path(model_name) / model_version
model.save(model_path, save_format="tf")
317. Si vous ne connaissez pas Docker, sachez que cet outil vous permet de télécharger facilement un en-
semble d’applications préparées sous forme d’une image Docker (avec toutes les dépendances et, en général,
dans une conguration par défaut adéquate) et de les exécuter sur votre système à l’aide d’un moteur Docker.
Lorsque vous exécutez une image, le moteur crée un conteneur Docker qui assure une parfaite isolation entre
les applications et votre propre système (vous pouvez lui accorder un accès limité si vous le souhaitez). Le
conteneur est comparable à une machine virtuelle, mais il est beaucoup plus rapide et plus léger, car il se
fonde directement sur le noyau de l’hôte. L’image n’a donc pas besoin d’inclure ni d’exécuter son propre
noyau.
482 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
os.environ["MODEL_DIR"] = str(model_path.parent.absolute())
318. Il existe également des images compatibles GPU, ainsi que d’autres options d’installation. Pour en
savoir plus, reportez-vous aux instructions d’installation ofcielles (https://homl.info/tfserving).
11.1 Servir un modèle TensorFlow 483
• -v "/chemin/absolu/vers/my_mnist_model:/models/mnist_
model"
Permet au conteneur d’accéder au modèle de chemin d’accès absolu spécifié
via le chemin /models/mnist_model. Sur les systèmes Windows, vous
devrez remplacer / par \ dans le chemin d’accès du système de fichiers
(mais pas dans le chemin du conteneur, car Docker s’exécute sous Linux).
• -p 8500:8500
Fait en sorte que le moteur Docker redirige le port TCP 8500 de l’hôte vers
le port TCP 8500 du conteneur. Par défaut, TF Serving utilise ce port pour
l’API gRPC.
• -p 8501:8501
Redirige le port TCP 8501 de l’hôte vers le port TCP 8501 du conteneur. Par
défaut, l’image de Docker utilise ce port pour l’API REST.
• -e MODEL_NAME=my_mnist_model
Fixe la valeur de la variable d’environnement MODEL_NAME du conteneur
afin que TF Serving sache quel modèle servir. Par défaut, il recherchera des
modèles dans le répertoire /models et servira automatiquement la dernière
version trouvée.
• tensorflow/serving
Le nom de l’image à exécuter.
Maintenant que le serveur est lancé, envoyons-lui des requêtes, tout d’abord par le
biais de l’API REST, puis de l’API gRPC.
Notez que le format JSON est exclusivement textuel. La chaîne de requête res-
semble à ceci :
>>> request_json
'{"signature_name": "serving_default", "instances": [[[0, 0, 0, 0, ... ]]]}'
484 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
Transmettons maintenant ceci à TF Serving sous forme d’une requête HTTP
POST. La bibliothèque requests facilite cette opération (elle ne fait pas partie de
la bibliothèque standard de Python, mais elle est préinstallée sur Colab) :
import requests
server_url = "http://localhost:8501/v1/models/my_mnist_model:predict"
response = requests.post(server_url, data=request_json)
response.raise_for_status() # lever une exception en cas d’erreur
response = response.json()
Si tout se passe bien, la réponse devrait être un dictionnaire contenant une seule
clé "predictions". La valeur correspondante est la liste des prédictions. Il s’agit
d’une liste Python, que nous convertissons en tableau NumPy et dont nous arrondis-
sons les valeurs réelles à la deuxième décimale:
>>> import numpy as np
>>> y_proba = np.array(response["predictions"])
>>> y_proba.round(2)
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 1. , 0. , 0. ],
[0. , 0. , 0.99, 0.01, 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0.97, 0.01, 0. , 0. , 0. , 0. , 0.01, 0. , 0. ]])
Super, nous avons les prédictions ! Le modèle est presque sûr à 100% que la pre-
mière image est un 7, sûr à 99% que la deuxième est un 2 et sûr à 96% que la troi-
sième est un1. C’est exact.
L’API REST est simple et fonctionne parfaitement lorsque les données d’en-
trée et de sortie ne sont pas trop volumineuses. Par ailleurs, quasiment n’importe
quelle application cliente peut effectuer des requêtes REST sans compléments logi-
ciels, alors que la disponibilité d’autres protocoles est moindre. Toutefois, elle se
fonde sur JSON, un format textuel plutôt verbeux. Par exemple, nous avons dû
convertir le tableau NumPy en une liste Python et chaque nombre à virgule ot-
tante a été représenté sous forme d’une chaîne de caractères. Cette approche est
très inefcace, à la fois en temps de sérialisation/désérialisation (pour convertir
tous les réels en chaînes de caractères, et inversement) et en occupation mémoire,
car de nombreuses valeurs ont été représentées avec plus de quinze caractères, ce
qui correspond à plus de 120bits pour des nombres à virgule ottante sur 32bits !
Cela conduira à un temps d’attente notable et à une occupation importante de la
bande passante pour le transfert de tableaux NumPy de grande taille319. Voyons
donc comment utiliser à la place gRPC.
319. Pour être honnête, ce problème peut être atténué en sérialisant les données et en les encodant au
format Base64 préalablement à la création de la requête REST. Par ailleurs, les requêtes REST peuvent être
compressées avec gzip, ce qui réduit énormément la taille des informations transmises.
11.1 Servir un modèle TensorFlow 485
request = PredictRequest()
request.model_spec.name = model_name
request.model_spec.signature_name = "serving_default"
input_name = model.input_names[0] # == "flatten_input"
request.inputs[input_name].CopyFrom(tf.make_tensor_proto(X_new))
channel = grpc.insecure_channel('localhost:8500')
predict_service = prediction_service_pb2_grpc.PredictionServiceStub(channel)
response = predict_service.Predict(request, timeout=10.0)
Ce code n’a rien de compliqué. Après les importations, nous créons un canal de
communication gRPC avec localhost sur le port TCP 8500, puis un service gRPC
sur ce canal. Nous l’utilisons pour envoyer une requête, avec un délai d’expiration
de 10secondes (l’appel étant synchrone, il restera bloqué jusqu’à la réception de la
réponse ou l’expiration du délai). Dans cet exemple, le canal n’est pas sécurisé (aucun
chiffrement, aucune authentication), mais gRPC et TF Serving permettent égale-
ment de gérer des canaux de communication sécurisés via SSL/TLS.
Convertissons ensuite le protobuf PredictResponse en un tenseur:
output_name = model.output_names[0] # == "dense_1"
outputs_proto = response.outputs[output_name]
y_proba = tf.make_ndarray(outputs_proto)
model_version = "0002"
model_path = Path(model_name) / model_version
model.save(model_path, save_format="tf")
Cette approche permet une transition en douceur, mais elle risque d’exiger une très
grande quantité de mémoire (en particulier la RAM du GPU, qui est généralement
la plus limitée). Dans ce cas, vous pouvez congurer TF Serving pour qu’il traite les
requêtes en attente avec la version précédente du modèle et décharge celle-ci avant
de charger et d’utiliser la nouvelle. Cette conguration évitera la présence simultanée
des deux versions du modèle en mémoire, mais le service sera indisponible pendant
une courte période.
Vous le constatez, TF Serving simplie énormément le déploiement de nouveaux
modèles. De plus, si vous découvrez que la version 2 ne fonctionne pas comme
attendu, le retour à la version1 consiste simplement à supprimer le répertoire my_
mnist_model/0002.
11.1 Servir un modèle TensorFlow 487
Si vous pensez que le nombre de requêtes par seconde sera très élevé, vous pouvez
déployer TF Serving sur plusieurs serveurs et répartir les requêtes de façon équilibrée
(voir la gure11.2). Il faudra pour cela déployer et gérer de nombreux conteneurs
TF Serving sur ces serveurs. Pour vous y aider, tournez-vous vers un outil comme
Kubernetes (https://kubernetes.io). Il s’agit d’un système open source qui simplie
l’orchestration de conteneurs sur de nombreux serveurs. Si vous ne souhaitez pas
acheter, maintenir et mettre à niveau toute l’infrastructure matérielle, vous pouvez
opter pour des machines virtuelles sur une plateforme de cloud, comme Amazon
AWS, Microsoft Azure, Google Cloud Platform, IBM Cloud, Alibaba Cloud, Oracle
Cloud ou toute autre offre de plateforme en tant que service (PaaS, Platform-as-a-
Service). La gestion de toutes les machines virtuelles, l’orchestration des conteneurs
(même avec l’aide de Kubernetes), la conguration, le réglage et la supervision de TF
Serving, tout cela peut être un travail à plein temps.
Heureusement, certains fournisseurs de services proposent de s’en occuper à votre
place. Dans ce chapitre, nous allons utiliser VertexAI car elle est, aujourd’hui, la seule
plateforme à disposer de TPU (tensor processing units, voir chapitre4), elle est compa-
tible avec TensorFlow2, Scikit-Learn et XGBoost, et elle propose une suite de services
AI intéressants. Mais il existe dans ce secteur plusieurs autres fournisseurs capables
également de servir des modèles TensorFlow, comme Amazon AWS SageMaker et
Microsoft AI Platform, alors n’oubliez pas de regarder de ce côté également.
TF Serving
Répartiteur
TF Serving
de charge
Applications
TF Serving
2. S’il s’agit de votre première utilisation de GCP, vous devrez lire et accepter les
conditions d’utilisation. Il est proposé aux nouveaux utilisateurs un crédit de
300$ valable pendant 90 jours (conditions en septembre2023). Vous n’aurez
besoin que d’une petite partie de cette somme pour payer les services que vous
utiliserez dans ce chapitre. Après avoir accepté un essai gratuit, vous devrez
11.1 Servir un modèle TensorFlow 489
créer un prol de paiement et saisir le numéro de votre carte bancaire. Elle est
utilisée pour des vérications (probablement pour éviter plusieurs inscriptions
à l’essai gratuit), mais vous ne serez pas facturé pour les 300 premiers dollars et,
par la suite, vous ne serez facturé que si vous avez choisi de passer à un compte
payant.
3. Si vous avez déjà employé GCP et si votre période d’essai gratuit a expiré, les
services utilisés dans ce chapitre vous coûteront un peu d’argent. Le montant
sera faible, notamment si vous pensez à désactiver les services lorsque vous n’en
avez plus besoin. Avant d’exécuter un service, assurez-vous de bien comprendre
les conditions tarifaires, puis acceptez-les. Je décline toute responsabilité si
des services nissent par vous coûter plus qu’attendu ! Vériez également que
votre compte de facturation est actif. Pour cela, ouvrez le menu de navigation
≡ et cliquez sur Facturation. Assurez-vous d’avoir conguré une méthode de
paiement et que le compte de facturation est actif.
4. Dans GCP, toutes les ressources sont associées à un projet. Cela comprend toutes
les machines virtuelles que vous utilisez, les chiers que vous stockez et les tâches
d’entraînement que vous exécutez. Lorsque vous créez un compte, GCP crée
automatiquement un projet intitulé « My First Project ». Vous pouvez changer
son nom d’afchage en allant dans les paramètres du projet. Pour cela, dans le
menu de navigation ≡, choisissez « IAM et administration » → Paramètres,
modiez le nom du projet et cliquez sur Enregistrer. Notez que le projet possède
également un identiant et un numéro unique. L’identiant peut être choisi
au moment de la création du projet, mais il ne peut plus être changé ensuite.
Le numéro du projet est généré automatiquement et ne peut pas être modié.
Pour créer un nouveau projet, cliquez sur « Sélectionner un projet », puis sur
« Nouveau projet ». Saisissez le nom du projet, modiez éventuellement son
ID en cliquant sur Modier, et cliquez sur Créer. Vériez que la facturation est
active sur ce nouveau projet de sorte que le coût du service puisse vous être
facturé (sur votre avoir, le cas échéant).
Lorsque vous savez que vous n’aurez besoin de services que pour quelques
heures, définissez toujours une alarme pour vous rappeler de les désactiver.
Si vous l’oubliez, ils risquent de s’exécuter pendant des jours ou des mois,
conduisant à des coûts potentiellement élevés.
5. Maintenant que vous disposez d’un compte GCP et d’un projet, avec une
facturation activée, vous devez activer les API dont vous avez besoin. Dans
le menu de navigation ≡, sélectionnez « API et services », et assurez-vous
que l’API Cloud Storage est activée. Si nécessaire, cliquez sur « + Activer les
APIet les services », trouvez Stockage Cloud et activez-le. Activez également
l’API Vertex AI.
Vous pourriez continuer en travaillant exclusivement à partir de la console GCP,
mais je vous recommande d’utiliser plutôt Python : vous pourrez ainsi écrire des
scripts pour automatiser tout ce que vous voulez faire avec GCP, et c’est souvent plus
pratique que de cliquer à travers des menus et des formulaires, tout particulièrement
pour les tâches usuelles.
490 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
Si vous préférez installer l’interface en ligne de commande sur votre machine (https://
homl.info/gcloud), vous devrez ensuite l’initialiser en exécutant gcloud init : suivez
les instructions pour vous connecter à GCP et donner accès à vos ressources GCP, puis
sélectionnez le projet par défaut que vous voulez utiliser (si vous en avez plusieurs) et la
région par défaut dans laquelle vous voulez exécuter vos programmes.
Avant d’utiliser un service GCP quel qu’il soit, la première chose à faire est de
s’authentier. Le plus simple, lorsqu’on utilise Colab, est d’exécuter le code suivant :
from google.colab import auth
auth.authenticate_user()
dans ce cas l’arrêter et mettre à jour sa configuration. Dans tous les cas, une fois
qu’un compte de service est attaché à une instance de machine virtuelle ou à toute
autre ressource GCP exécutant votre code, les bibliothèques clientes de GCP (dont
nous parlerons bientôt) utiliseront automatiquement le compte de service choisi pour
effectuer leur authentification, sans aucune étape supplémentaire.
Si votre application est hébergée via Kubernetes, utilisez le service Google Workload
Identity pour associer le bon compte de service à chacun des comptes de service
Kubernetes. Si votre application n’est pas hébergée sur GCP (ce qui est le cas, entre
autres, si vous vous contentez d’exécuter un notebook Jupyter sur votre propre
machine), alors vous pouvez :
• soit utiliser le service Workload Identity Federation (encore appelé Fédération
d’identité de charge de travail), solution la plus sûre mais la plus difficile aussi,
• soit générer une clé d’accès pour votre compte de service, et la sauvegarder
dans un fichier JSON dont vous conserverez le chemin d’accès dans la variable
d’environnement GOOGLE_APPLICATION_CREDENTIALS afin que votre
application cliente puisse y accéder.
Vous pouvez gérer les clés d’accès en cliquant sur le compte de service que vous
venez de créer, puis en ouvrant l’onglet CLÉS. Veillez à la confidentialité du fichier
contenant la clé : c’est en quelque sorte un mot de passe vers le compte de service.
Pour plus d’informations sur la configuration de l’authentification et de l’autorisation
permettant à votre application d’accéder aux services GCP, reportez-vous à la docu-
mentation (https://homl.info/gcpauth).
Pour pouvoir enregistrer des modèles, il faut d’abord créer un bucket Google Cloud
Storage: il s’agit d’un simple conteneur pour vos données. Nous utiliserons pour cela
la bibliothèque google-cloud-storage, qui est préinstallée dans Colab. Nous
créons tout d’abord un objet Client, qui servira d’interface avec GCS, puis nous
l’utilisons pour créer le bucket:
from google.cloud import storage
storage_client = storage.Client(project=project_id)
bucket = storage_client.create_bucket(bucket_name, location=location)
Puisque GCS utilise un seul espace de noms mondial pour les buckets, les noms
trop simples, surtout en anglais, risquent d’être indisponibles. Assurez-vous que le
nom du bucket est conforme aux conventions de nommage du DNS, car il pourra
11.1 Servir un modèle TensorFlow 493
être utilisé dans des enregistrements DNS. Par ailleurs, les noms des buckets sont
publics. Évitez donc d’y inclure des informations privées. Une pratique courante
consiste à utiliser votre nom de domaine ou votre nom d’entreprise comme préxe
an de garantir une certaine unicité, ou d’inclure simplement un nombre aléatoire
dans le nom.
Vous pouvez changer l’emplacement géographique si vous le voulez, mais
assurez-vous d’en choisir un qui gère les GPU. Votre choix sera peut-être éga-
lement dicté par d’autres considérations, comme les prix qui varient grandement
d’une région à l’autre, le fait que certaines régions produisent beaucoup plus de CO2
ou ne proposent pas tous les services, ou qu’un emplacement géographique mono-
région permet d’obtenir de meilleures performances. Pour en savoir plus, consultez
la liste des régions de Google Cloud (https://homl.info/regions) et la documentation
de VertexAI sur les emplacements géographiques (https://homl.info/locations). En cas
d’hésitation, le mieux est probablement de choisir votre propre région.
Chargeons ensuite le répertoire my_mnist_model dans le nouveau bucket. Dans
GCS, les chiers sont appelés blobs (ou objets) et en pratique ils sont tous placés dans
le bucket sans aucune structuration arborescente. Les noms des blobs peuvent être
des chaînes Unicode arbitraires et peuvent même contenir des barres obliques (/).
La console GCP ainsi que d’autres outils utilisent ces barres obliques pour donner
l’illusion qu’il y a une structure arborescente. Par conséquent, lorsque nous chargeons
le répertoire my_mnist_model vers le serveur, nous ne nous soucions que des chiers,
et non des répertoires:
def upload_directory(bucket, dirpath):
dirpath = Path(dirpath)
for filepath in dirpath.glob("**/*"):
if filepath.is_file():
blob = bucket.blob(filepath.relative_to(dirpath.parent).as_posix())
blob.upload_from_filename(filepath)
upload_directory(bucket, "my_mnist_model")
Cette fonction convient bien pour l’instant, mais elle prendrait trop de temps s’il y
avait beaucoup de chiers à charger sur le serveur. Il n’est pas trop difcile d’accélérer
considérablement le processus en gérant plusieurs ls d’exécution (ce qu’on appelle
le multithreading –voir le notebook de ce chapitre320 pour une implémentation). Avec
l’interface en ligne de commande de Google Cloud, vous pouvez aussi exécuter la
commande suivante :
!gsutil -m cp -r my_mnist_model gs://{nom_de_bucket}/
créer un nouveau modèle VertexAI : nous devons donner un nom à afcher, le chemin
d’accès GCS de notre modèle (dans ce cas la version 0001) et l’URL du conteneur
Docker dans lequel nous voulons que VertexAI exécute ce modèle. Si vous accédez
à cette URL et remontez d’un niveau, vous trouverez d’autres conteneurs que vous
pouvez utiliser. Celui-ci est compatible avec TensorFlow2.8 et un GPU :
from google.cloud import aiplatform
server_image = "gcr.io/cloud-aiplatform/prediction/tf2-gpu.2-8:latest"
aiplatform.init(project=project_id, location=location)
mnist_model = aiplatform.Model.upload(
display_name="mnist",
artifact_uri=f"gs://{bucket_name}/my_mnist_model/0001",
serving_container_image_uri=server_image,
)
endpoint.deploy(
mnist_model,
min_replica_count=1,
max_replica_count=5,
machine_type="n1-standard-4",
accelerator_type="NVIDIA_TESLA_K80",
accelerator_count=1
)
Ce code mettra peut-être quelques minutes à s’exécuter, car VertexAI doit pré-
parer une machine virtuelle. Dans cet exemple, nous utilisons une machine plutôt
basique de type n1-standard-4 (voir la description des autres types sur https://
homl.info/machinetypes). Nous utilisons aussi un GPU de base de type NVIDIA_
TESLA_K80 (voir la description des autres types d’accélérateurs sur https://homl.
info/accelerators). Si vous avez sélectionné une autre région que "us-central1",
il vous faudra peut-être choisir un type de machine ou un type d’accélérateur pris en
charge dans cette région (toutes les régions n’ont pas de GPU NVIDIA Tesla K80
par exemple).
Google Cloud Platform applique différents quotas aux GPU, tant au niveau
non mondial que par région : vous ne pouvez pas créer des milliers de GPU
sans avoir obtenu au préalable l’autorisation de Google. Pour vérifier vos
quotas, ouvrez « IAM et administration → Quotas » dans la console GCP.
Si certains quotas sont trop faibles (c’est-à-dire s’il vous faut davantage de
GPU dans une région donnée), vous pouvez demander que le quota soit
augmenté ; cela prend en général à peu près 48 heures.
11.1 Servir un modèle TensorFlow 495
Nous devons d’abord convertir les images à classer en une liste Python, comme nous
l’avons fait précédemment lorsque nous avons transmis des requêtes à TFServing en
utilisant l’API REST. L’objet response contient les prédictions, représentées par
une liste Python de listes de nombres en virgule ottante. Arrondissons-les à deux
décimales et convertissons-les en un tableau NumPy :
>>> import numpy as np
>>> np.round(response.predictions, 2)
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 1. , 0. , 0. ],
[0. , 0. , 0.99, 0.01, 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0.97, 0.01, 0. , 0. , 0. , 0. , 0.01, 0. , 0. ]])
Voyons maintenant comment exécuter une tâche sur VertexAI pour effectuer des
prédictions sur un lot de données potentiellement de très grande taille.
ce chier à VertexAI. Créons donc un chier JSON Lines dans un nouveau réper-
toire, puis chargeons ce répertoire vers GCS :
batch_path = Path("my_mnist_batch")
batch_path.mkdir(exist_ok=True)
with open(batch_path / "my_mnist_batch.jsonl", "w") as jsonl_file:
for image in X_test[:100].tolist():
jsonl_file.write(json.dumps(image))
jsonl_file.write("\n")
upload_directory(bucket, batch_path)
Pour les lots de données très importants, vous pouvez partager les entrées
entre plusieurs fichiers JSON Lines dont vous donnerez la liste dans le
paramètre d’appel gcs_source.
Vous savez maintenant comment déployer un modèle sur Vertex AI, comment
créer un service de prédiction et comment exécuter des tâches de prédiction grou-
pées. Voyons à présent comment déployer votre modèle sur une application mobile
ou un système embarqué comme un système de contrôle de chauffage, un capteur
d’activité ou un véhicule autonome.
321. Jetez également un œil à Graph Transform Tool (https://homl.info/tfgtt) de TensorFlow pour modier
et optimiser des graphes de calcul.
11.2 Déployer un modèle sur un équipement mobile ou embarqué 499
Poids
(nombres à virgule
flottante)
Poids
quantifiés
(octets)
Voyons maintenant comment utiliser votre modèle sur un site web, en l’exécutant
directement dans le navigateur de l’utilisateur.
mobilenet.load().then(model => {
model.classify(image).then(predictions => {
for (var i = 0; i < predictions.length; i++) {
let className = predictions[i].className
let proba = (predictions[i].probability * 100).toFixed(1)
console.log(className + " : " + proba + "%");
}
});
});
Il est même possible de transformer ce site web en une application web progressive
(progressive web app, ou PWA): il s’agit d’un site web respectant un certain nombre de
502 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
critères322 de façon à pouvoir être visualisé dans n’importe quel navigateur et même être
installé comme application indépendante sur un appareil mobile. Essayez par exemple
de consulter https://homl.info/tfjswpa sur votre smartphone : la plupart des navigateurs
modernes vous demanderont si vous souhaitez ajouter TFJS Demo à votre écran d’ac-
cueil. Si vous acceptez, vous verrez apparaître une nouvelle icône dans votre liste
d’applications. En cliquant sur celle-ci, vous déclencherez le chargement du site web
TFJS Demo dans sa propre fenêtre, tout comme une application mobile ordinaire. Un
PWA peut même être conguré pour travailler hors connexion, en utilisant un service
worker: il s’agit d’un module JavaScript qui utilise un l d’exécution (ou thread) séparé
dans le navigateur et intercepte les requêtes au réseau, permettant de mettre en cache
les ressources de sorte que le PWA puisse s’exécuter plus rapidement, voire entière-
ment en mode déconnecté. Il peut aussi délivrer des notications, exécuter des tâches
en arrière-plan, etc. Les PWA vous permettent de gérer une même base logicielle
pour le web et pour vos appareils mobiles. Il devient plus facile de garantir que tous
les utilisateurs exécuteront la même version de votre application. Vous pouvez expéri-
menter avec le code PWA de TFJS Demo sur Glitch.com (https://homl.info/wpacode).
TFJS vous permet aussi d’entraîner un modèle directement dans votre navigateur,
et cela plutôt rapidement. Si votre ordinateur dispose d’une carte GPU, alors TFJS
peut en général l’utiliser, même si ce n’est pas une carte Nvidia. De fait, TFJS utilisera
WebGL s’il est disponible, et comme les navigateurs web modernes sont compatibles
en général avec un grand nombre de cartes GPU, TFJS gère en pratique davantage
de cartes GPU que TensorFlow (qui, par lui-même, ne gère que les cartes Nvidia).
Entraîner un modèle dans le navigateur d’un utilisateur peut être particulièrement
utile pour garantir la condentialité des données de cet utilisateur. Un modèle peut
être entraîné centralement, puis ajusté localement dans un navigateur, à partir des
données de l’utilisateur. Si ce sujet vous intéresse, jetez un œil à l’apprentissage fédéré
(https://tensorow.org/federated).
De nouveau, pour traiter comme il se doit ce sujet, il faudrait un livre complet. Si vous
souhaitez en savoir plus sur TensorFlow.js, consultez l’ouvrage Practical Deep Learning for
Cloud, Mobile, and Edge de Anirudh Koul, Siddha Ganju et Meher Kasam (https://homl.
info/tfjsbook) ou Learning TensorFlow.js de Gant Laborde, aux éditions O’Reilly.
Maintenant que nous avons vu comment déployer des modèles TensorFlow avec
TFServing, ou sur le cloud avec VertexAI, ou sur des appareils mobiles ou des sys-
tèmes embarqués avec TFLite, ou sur un navigateur web avec TFJS, voyons à présent
comment utiliser des GPU pour accélérer les calculs.
322. Un PWA doit par exemple inclure des icônes de différentes tailles pour les différents types d’appareils
mobiles, il doit utiliser le protocole HTTPS, il doit inclure un chier manifeste contenant des métadonnées
telles que le nom de l'application et la couleur de fond.
11.4 Utiliser des GPU pour accélérer les calculs 503
CPU
GPU 0 GPU 1
Figure 11.6 – Exécution d’un graphe TensorFlow en parallèle sur plusieurs processeurs
données dans le cloud. Ou peut-être voulez-vous une carte graphique pour vos jeux,
tout en souhaitant l’utiliser également pour vos applications de Deep Learning. Si
vous décidez d’acheter une carte graphique, prenez le temps d’effectuer le bon choix.
Vous devrez prendre en compte la quantité de RAM nécessaire pour vos tâches (au
moins 10Go pour le traitement d’images ou du langage naturel), la bande passante
(c’est-à-dire la vitesse à laquelle vous pouvez transférer les données vers et depuis
votre GPU), le nombre de cœurs, le système de refroidissement, etc. Tim Dettmers
a rédigé un billet très intéressant qui vous aidera à choisir une carte (https://homl.
info/66) : je vous encourage à le lire attentivement. Au départ, TensorFlow ne pre-
nait en charge –hormis les TPU de Google qui sont évidemment reconnus– que les
cartes Nvidia dotées de la technologie CUDA Compute Capability, en version 3.5
ou ultérieure (https://homl.info/cudagpus), mais la compatibilité avec d’autres fabri-
cants a été améliorée depuis, alors vériez de temps à autre dans la documentation
de TensorFlow (https://tensorow.org/install) quels sont les matériels pris en charge.
Si vous optez pour une carte graphique Nvidia, vous devrez installer les pilotes
Nvidia adéquats, ainsi que plusieurs bibliothèques Nvidia323, en particulier :
• la bibliothèque CUDA (compute unied device architecture) Toolkit, qui permet
aux développeurs d’exploiter les GPU compatibles CUDA dans toutes sortes
de calculs et pas uniquement pour l’accélération graphique ;
• la bibliothèque cuDNN (CUDA deep neural network), qui comprend des
primitives accélérées par le GPU pour les DNN. Elle fournit des implémentations
optimisées des calculs propres aux réseaux de neurones profonds, comme les
couches d’activation, la normalisation, les convolutions directes et transposées,
et le pooling (voir chapitre 6). La bibliothèque cuDNN fait partie du kit de
développement logiciel Deep Learning de Nvidia (SDK). Notez qu’il vous
faudra créer un compte développeur Nvidia pour le charger.
TensorFlow utilise CUDA et cuDNN pour contrôler les cartes graphiques et accé-
lérer les calculs (voir la gure11.7).
TensorFlow
cuDNN
CUDA
Figure 11.7 – TensorFlow utilise CUDA et cuDNN pour contrôler les GPU
et accélérer les DNN
323. Vériez la documentation TensorFlow où vous trouverez des instructions d'installation détaillées et à
jour, car les modications sont fréquentes.
11.4 Utiliser des GPU pour accélérer les calculs 505
Après avoir installé la ou les cartes graphiques, ainsi que les pilotes et les biblio-
thèques nécessaires, vous pouvez utiliser la commande nvidia-smi pour vérier
la bonne installation de CUDA. Elle afche la liste des cartes graphiques disponibles
et les processus qui s’exécutent sur chacune d’elles. Dans l’exemple ci-après, il s’agit
d’une carte GPU NVidia Tesla T4 dotée d’environ 15 Go de RAM, sur laquelle
aucun processus n’est en cours d’exécution :
$ nvidia-smi
Sun Apr 10 04:52:10 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03 Driver Version: 460.32.03 CUDA Version: 11.2 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 34C P8 9W / 70W | 3MiB / 15109MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
Pour vérier que TensorFlow voit bien votre GPU, exécutez les commandes sui-
vantes et vériez que le résultat n’est pas vide:
>>> physical_gpus = tf.config.list_physical_devices("GPU")
>>> physical_gpus
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
Programme 1 Programme 2
En supposant que vous disposiez de quatre GPU, chacun équipé d’au moins 4Gio
de mémoire, deux programmes comme celui-ci peuvent à présent s’exécuter en paral-
lèle, chacun utilisant les quatre cartes graphiques (voir la gure11.9). Si vous exé-
cutez la commande nvidia-smi pendant que les deux programmes s’exécutent,
vous devriez constater que chacun possède 2Gio de RAM sur chaque carte.
11.4 Utiliser des GPU pour accélérer les calculs 507
Programme 1 Programme 2
324. Nous l’avons vu au chapitre4, un noyau est l’implémentation d’une opération pour un type de don-
nées et un type de processeur spéciques. Par exemple, il existe un noyau GPU pour l’opération float32
tf.matmul(), mais pas pour int32 tf.matmul() (uniquement un noyau CPU).
325. Vous pouvez également appeler tf.debugging.set_log_device_placement(True)
pour consigner tous les placements sur les processeurs.
11.4 Utiliser des GPU pour accélérer les calculs 509
...
>>> c.device
'/job:localhost/replica:0/task:0/device:CPU:0'
Si vous tentez de placer explicitement une opération ou une variable sur un pro-
cesseur qui n’existe pas ou pour lequel il n’existe aucun noyau, alors TensorFlow se
rabattra silencieusement sur le processeur qu’il aurait choisi par défaut. Ceci est utile
lorsque vous voulez pouvoir exécuter le même code sur différentes machines n’ayant
pas le même nombre de GPU. Cependant, vous pouvez exécuter tf.config.set_
soft_device_placement(False) si vous préférez obtenir une exception.
Voyons à présent comment TensorFlow exécute toutes ces opérations sur plusieurs
processeurs.
326. Cette option peut se révéler utile pour garantir une parfaite reproductibilité, comme il est expliqué
dans la vidéo disponible à l’adresse https://homl.info/repro; elle est basée sur TF1.
327. Au moment de l’écriture de ces lignes, la lecture anticipée des données se fait uniquement vers la
mémoire du CPU, mais vous pouvez utiliser tf.data.experimental.prefetch_to_device()
pour que la lecture anticipée des données soit effectuée par le processeur de votre choix. Ainsi, le GPU ne
perdra pas de temps à attendre le transfert des données.
328. Lorsque les deux réseaux de neurones convolutifs (ou CNN) sont identiques, ceci s’appelle un réseau
de neurones siamois.
512 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
les processeurs, et le parallélisme des données, dans lequel le modèle est répliqué sur
chaque processeur et chaque réplique est entraînée sur un sous-ensemble des don-
nées. Examinons de plus près ces deux options, avant d’entraîner un modèle sur plu-
sieursGPU.
Cellule 4 5
Cellule 3 4 5
Cellule 2 3 4 5
Cellule 1 2 3 4 5
Mini-lots
329. Si vous souhaitez aller plus loin en matière de parallélisme du modèle, intéressez-vous à Mesh Tensor-
Flow (https://github.com/tensorow/mesh).
11.5 Entraîner des modèles sur plusieurs processeurs 515
Paramètres
Mini-lots
Alors que la stratégie avec mise en miroir impose une mise à jour synchronisée
des paramètres sur tous les GPU, cette approche centralisée autorise des mises à jour
synchrones ou asynchrones. Voyons les avantages et les inconvénients de ces deux
options.
330. Le terme peut introduire une légère confusion, car il semble indiquer que certains travailleurs (GPU)
sont particuliers, ne faisant rien. En réalité, ils sont tous équivalents et s’efforcent de faire partie des plus
rapides à chaque étape d’entraînement. Les perdants ne sont pas nécessairement les mêmes à chaque étape
(sauf si certains processeurs sont réellement plus lents que d’autres). Toutefois, cela signie qu’en cas de
dysfonctionnement d’un ou deux processeurs, l’entraînement se poursuivra sans problème.
11.5 Entraîner des modèles sur plusieurs processeurs 517
� Coût
2
Les gradients sont … mais ils sont
calculés ici… appliqués là
Aïe, on remonte
�� la pente !
��
Gradients
Mises à jour par périmés
les autres répliques
�
1
Il existe quelques solutions pour diminuer les effets des gradients périmés :
• abaisser le taux d’apprentissage ;
• ignorer les gradients périmés ou les réduire ;
• ajuster la taille du mini-lot ;
• démarrer les quelques premières époques en utilisant une seule réplique
(phase d’échauffement). Les gradients périmés ont tendance à avoir un effet
plus important au début de l’entraînement, lorsqu’ils sont grands et que les
paramètres ne sont pas encore entrés dans une vallée de la fonction de coût,
ce qui peut conduire différentes répliques à pousser les paramètres dans des
directions assez différentes.
Un article331 publié par l’équipe Google Brain en avril 2016 évalue différentes
approches. Il conclut que le parallélisme des données avec mises à jour synchrones
et quelques répliques de rechange est le plus efcace, en raison d’une convergence
plus rapide et de la production d’un meilleur modèle. Cependant, cela reste un sujet
de recherche très actif et il n’est pas encore possible d’écarter les mises à jour asyn-
chrones.
331. Jianmin Chen et al., « Revisiting Distributed Synchronous SGD » (2016) : https://homl.info/68.
518 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
avec la RAM du GPU (éventuellement au travers du réseau, s’il s’agit d’un environ-
nement distribué) annulera l’accélération obtenue par la répartition de la charge
de calcul. À partir de là, l’ajout d’autres GPU ne fera qu’augmenter la saturation et
ralentir l’entraînement.
La saturation est encore plus importante avec les grands modèles denses, car ils
impliquent la transmission d’un grand nombre de paramètres et de gradients. Elle
est moindre avec les petits modèles (mais le gain obtenu grâce à la parallélisation
est faible) et avec les grands modèles creux, car la plupart des gradients sont géné-
ralement à zéro et peuvent être transmis de façon efcace. Jeff Dean, initiateur et
responsable du projet Google Brain, a fait état (https://homl.info/69) d’accélérations
d’un facteur 25 à 40 lors de la distribution des calculs sur 50 GPU pour des modèles
denses, et d’un facteur 300 pour des modèles plus creux entraînés sur 500 GPU. Cela
montre bien que les modèles creux sont mieux adaptés à un parallélisme plus étendu.
Voici quelques exemples concrets:
• traduction automatique neuronale : accélération d’un facteur 6 sur 8 GPU ;
• Inception/ImageNet : accélération d’un facteur 32 sur 50 GPU ;
• RankBrain : accélération d’un facteur 300 sur 500 GPU.
Beaucoup de travaux de recherche visent actuellement à tenter de résoudre le
problème de saturation de la bande passante, avec comme objectif d’obtenir une
croissance linéaire des capacités d’entraînement selon le nombre de GPU dispo-
nibles. Ainsi, dans un article publié en 2018332, des chercheurs des universités de
Carnegie-Mellon et Stanford, ainsi que de Microsoft Research, ont proposé un sys-
tème nommé PipeDream permettant de réduire de plus de 90 % les communications
réseau, rendant ainsi possible l’entraînement de très grands modèles sur de nom-
breuses machines. Ils y sont parvenus en utilisant une nouvelle technique appelée
parallélisme en pipeline (pipeline parallelism), qui combine le parallélisme du modèle et
le parallélisme des données : le modèle est découpé en parties consécutives appelées
stades (en anglais, stages), chacun de ces stades étant entraîné sur une machine dif-
férente.
Ceci donne un pipeline asynchrone dans lequel toutes les machines travaillent
en parallèle avec très peu de temps d’inactivité (ou idle time). Durant l’entraînement,
chaque stade alterne un cycle de propagation avant et un cycle de rétropropaga-
tion (voir gure11.17) : il extrait un mini-lot de sa le d’attente d’entrée, le traite
et transmet la sortie à la le d’attente d’entrée du stade suivant, puis il extrait un
mini-lot de gradients de sa le d’attente de gradients, effectue la rétropropagation de
ces gradients et met à jour ses propres paramètres de modèle et ajoute les gradients
rétropropagés à la le d’attente de gradients du stade précédent. Il répète perpétuelle-
ment le même processus. Chaque stade peut aussi utiliser le parallélisme des données
ordinaire (c’est-à-dire utiliser une stratégie de mise en miroir) indépendamment des
autres stades.
332. Aaron Harlap et al., « PipeDream: Fast and Efcient Pipeline Parallel DNN Training », arXiv preprint
arXiv:1806.03377 (2018): https://homl.info/pipedream.
11.5 Entraîner des modèles sur plusieurs processeurs 519
Cependant, tel que présenté ici, PipeDream ne fonctionnerait pas tellement bien.
Pour comprendre pourquoi, examinons le mini-lot numéro 5 sur la gure 11.17 :
lorsqu’il est passé par le stade 1 durant la passe en avant, les gradients du mini-lot
numéro4 n’avaient pas encore été rétropropagés jusqu’à ce stade, mais au moment
où les gradients du lot 5 sont rétropropagés jusqu’au stade 20, les gradients du lot 4
auront été utilisés pour mettre à jour les paramètres du modèle, c’est pourquoi les gra-
dients du lot 5 seront quelque peu périmés. Comme nous l’avons vu, ceci peut nuire
à la vitesse d’entraînement et à la précision et même faire diverger l’algorithme : plus
il y a de stades, plus le problème s’aggrave. Les auteurs de l’article ont cependant pro-
posé des méthodes pour atténuer ce problème: entre autres, chaque stade sauvegarde
les poids durant la propagation avant et les restaure lors de la propagation arrière, an
que les mêmes poids soient utilisés lors de la passe avant et de la passe arrière. C’est
ce qu’on appelle la mise en réserve des poids (weight stashing). Grâce à cela, PipeLine
fournit d’excellents résultats en termes de capacité d’extension (scalability), bien
au-delà du simple parallélisme des données.
La dernière avancée notable dans ce domaine a été présentée dans un article
publié en 2022 333 par des chercheurs de Google. Ils ont développé un système nommé
Pathways utilisant un parallélisme du modèle automatique, un ordonnancement en
gang asynchrone et d’autres techniques an d’atteindre une utilisation du maté-
riel voisine de 100 % sur des milliers de TPU ! L’ordonnancement (ou scheduling)
consiste à décider où et quand chaque tâche s’exécutera, tandis que l’ordonnancement
en gang (gang scheduling) consiste à exécuter des tâches apparentées en même temps,
en parallèle et à proximité les unes des autres pour réduire le temps durant lequel ces
tâches doivent attendre les sorties des autres. Comme nous l’avons vu au chapitre8,
ce système a été utilisé pour entraîner un gigantesque modèle linguistique sur plus de
6 000 TPU, avec un taux d’utilisation du matériel voisin de 100 % : c’est une véri-
table prouesse technique.
À l’heure actuelle, Pathways n’est pas encore dans le domaine public, mais il
est probable que bientôt vous pourrez entraîner des modèles très volumineux sur
VertexAI en utilsant Pathways ou un système similaire. En attendant, pour réduire
le problème de saturation, il est préférable d’utiliser quelques GPU puissants plutôt
qu’un grand nombre de GPU aux performances modestes, et si vous devez entraîner
votre modèle sur plusieurs serveurs, il vaut mieux regrouper les GPU sur quelques
serveurs parfaitement interconnectés. Vous pouvez également essayer d’abaisser la
333. Paul Barham et al., « Pathways: Asynchronous Distributed Dataow for ML », arXiv preprint
arXiv:2203.12533 (2022) : https://homl.info/pathways.
520 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
with strategy.scope():
model = tf.keras.Sequential([...]) # créer un modèle Keras normalement
model.compile([...]) # compiler le modèle normalement
parallèle (de nouveau, la taille du lot doit être divisible par le nombre de répliques).
Si vous appelez la méthode save() du modèle, il sera enregistré non pas comme
un modèle en miroir avec plusieurs répliques, mais comme un modèle normal. Par
conséquent, lorsque vous le chargez, il s’exécutera en tant que modèle normal, sur
un seul processeur (par défaut le GPU0, ou, en l’absence de GPU, sur le CPU). Si
vous souhaitez le charger et l’exécuter sur tous les processeurs disponibles, vous devez
appeler tf.keras.models.load_model() à l’intérieur d’un contexte de dis-
tribution :
with strategy.scope():
model = tf.keras.models.load_model("my_mirrored_model")
Pour n’utiliser qu’un sous-ensemble de tous les GPU disponibles, passez-en la liste
au constructeur de MirroredStrategy :
strategy = tf.distribute.MirroredStrategy(devices=["/gpu:0", "/gpu:1"])
334. Pour de plus amples informations sur les algorithmes AllReduce, consultez le billet rédigé par Yuichiro
Ueno (https://homl.info/uenopost) et la page sur l’évolutivité des ressources d’entraînement avec NCCL
(https://homl.info/ncclalgo).
522 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
adresse IP, un port et un type (également appelé son rôle ou job). Le type peut être
"worker", "chief", "ps" (pour serveur de paramètres) ou "evaluator" :
• Chaque travailleur (worker) effectue des calculs, en général sur une machine
dotée d’un ou plusieurs GPU.
• Le chef (chief) effectue lui aussi des calculs (il s’agit d’un travailleur)
mais réalise des opérations supplémentaires, comme remplir des journaux
TensorBoard ou effectuer des sauvegardes ponctuelles. Il n’y a qu’un seul chef
dans une partition. Si aucun chef n’est désigné, le premier travailleur le devient.
• Un serveur de paramètres (parameter server, ps) conserve uniquement des valeurs
de variables et se trouve en général sur une machine équipée seulement d’un
CPU. Ce type de tâche n’est utilisé qu’avec ParameterServerStrategy.
• Un évaluateur (evaluator) prend évidemment en charge l’évaluation. Ce
type n’est pas utilisé très souvent, et lorsqu’il l’est, il n’y a en général qu’un seul
évaluateur.
Pour démarrer une partition TensorFlow, vous devez commencer par la dénir,
c’est-à-dire préciser l’adresse IP, le port TCP et le type de chaque tâche. Par exemple,
la spécication de partition suivante dénit une partition possédant trois tâches (deux
travailleurs et un serveur de paramètres; voir gure11.18). La spécication est un
dictionnaire avec une clé par rôle, et les valeurs sont des listes d’adresses de tâches
(adresse IP:port) :
cluster_spec = {
"worker": [
"machine-a.example.com:2222", # /rôle:travailleur/tâche:0
"machine-b.example.com:2222" # /rôle:travailleur/tâche:1
],
"ps": ["machine-a.example.com:2221"] # /rôle:ps/tâche:0
}
De façon générale, il y a une tâche par machine, mais, comme le montre cet
exemple, vous pouvez congurer plusieurs tâches sur la même machine (si elles par-
tagent les mêmes GPU, assurez-vous que la mémoire est correctement répartie).
Lorsque vous démarrez une tâche, vous devez lui fournir la spécication de par-
tition et lui indiquer son type et son indice (par exemple, travailleur0). Pour tout
préciser en même temps (spécication de partition et type et indice de la tâche
courante), l’approche la plus simple consiste à dénir la variable d’environnement
TF_CONFIG avant de démarrer TensorFlow. Sa valeur doit être un dictionnaire
JSON contenant une spécication de partition (sous la clé "cluster") et le type
et l’indice de la tâche courante (sous la clé "task"). Par exemple, la variable d’en-
vironnement TF_CONFIG suivante utilise la partition que nous venons de dénir et
indique que la tâche à démarrer est le travailleur0:
os.environ["TF_CONFIG"] = json.dumps({
"cluster": cluster_spec,
"task": {"type": "worker", "index": 0}
})
with strategy.scope():
model = tf.keras.Sequential([...]) # construire le modèle Keras
524 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
Si vous préférez mettre en place un parallélisme des données asynchrone avec des
serveurs de paramètres, changez la stratégie en ParameterServerStrategy,
ajoutez un ou plusieurs serveurs de paramètres et congurez TF_CONFIG de façon
appropriée pour chaque tâche. Même si les travailleurs vont opérer de façon asyn-
chrone, notez que les répliques sur chaque travailleur opéreront de façon synchrone.
Enn, si vous avez accès à des TPU sur Google Cloud (https://cloud.google.com/
tpu) – par exemple si vous utilisez Colab et si vous avez choisi TPU comme type
d’accélérateur–, vous pouvez créer une TPUStrategy comme ceci :
resolver = tf.distribute.cluster_resolver.TPUClusterResolver()
tf.tpu.experimental.initialize_tpu_system(resolver)
strategy = tf.distribute.experimental.TPUStrategy(resolver)
Ceci doit être exécuté juste après l’importation de TensorFlow. Vous pouvez
ensuite utiliser cette stratégie normalement.
11.5 Entraîner des modèles sur plusieurs processeurs 525
Si vous êtes un chercheur, vous pourriez être éligible à une utilisation gra-
tuite des TPU. Pour de plus amples informations, rendez-vous sur https://
tensorflow.org/tfrc.
Vous pouvez à présent entraîner des modèles sur plusieurs GPU et plusieurs ser-
veurs. Vous pouvez vous féliciter! Mais si vous souhaitez entraîner un très grand
modèle, vous aurez besoin de nombreux GPU, sur de nombreux serveurs. Il vous
faudra alors acheter un grand nombre de matériels ou gérer un grand nombre de
machines virtuelles dans le cloud. Le plus souvent, il sera moins pénible et moins
onéreux d’utiliser un service de cloud qui provisionne et gère toute cette infrastruc-
ture pour vous, uniquement lorsque vous en avez besoin. Voyons comment procéder
en utilisant VertexAI.
if resolver.task_type == "chief":
model_dir = os.getenv("AIP_MODEL_DIR") # chemins fournis par Vertex AI
tensorboard_log_dir = os.getenv("AIP_TENSORBOARD_LOG_DIR")
checkpoint_dir = os.getenv("AIP_CHECKPOINT_DIR")
else:
tmp_dir = Path(tempfile.mkdtemp()) # autres travailleurs :
# répertoires temporaires
model_dir = tmp_dir / "model"
tensorboard_log_dir = tmp_dir / "logs"
checkpoint_dir = tmp_dir / "ckpt"
callbacks = [tf.keras.callbacks.TensorBoard(tensorboard_log_dir),
tf.keras.callbacks.ModelCheckpoint(checkpoint_dir)]
526 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
Si vous placez les données d’entraînement sur GCS, vous pouvez y ac-
céder en créant un tf.data.TextLineDataset ou un tf.data.
TFRecordDataset. Il suffit d’utiliser des chemins GCS comme noms de
fichiers (par exemple, gs://my_bucket/data/001.csv). Ces jeux de données uti-
lisent tf.io.gfile pour l’accès aux fichiers, qu’ils soient locaux ou sur GCS.
À partir du script suivant, vous pouvez maintenant créer une tâche d’entraîne-
ment personnalisée sur VertexAI. Vous devrez spécier le nom de la tâche, le chemin
d’accès de votre script d’entraînement, l’image Docker à utiliser pour l’entraînement,
celle à utiliser pour les prédictions (après l’entraînement), ainsi que toutes les biblio-
thèques Python supplémentaires dont vous pourriez avoir besoin, et enn le bucket
qui constituera le répertoire de travail dans lequel VertexAI placera le script d’entraî-
nement. Par défaut, c’est aussi là que le script d’entraînement sauvegardera le modèle
entraîné, ainsi que les journaux TensorBoard et les points de reprise du modèle (s’il y
en a). Créons donc la tâche:
custom_training_job = aiplatform.CustomTrainingJob(
display_name="my_custom_training_job",
script_path="my_vertex_ai_training_task.py",
container_uri="gcr.io/cloud-aiplatform/training/tf-gpu.2-4:latest",
model_serving_container_image_uri=server_image,
requirements=["gcsfs==2022.3.0"], # pas nécessaire, c’est juste un exemple
staging_bucket=f"gs://{bucket_name}/staging"
)
Et voilà ! VertexAI fournira les nœuds de calcul que vous avez demandés (dans
la limite de vos quotas) et exécutera votre script d’entraînement sur ceux-ci. À la n
de l’exécution, la méthode run() renverra un modèle entraîné que vous pourrez
utiliser exactement comme celui que vous avez créé précédemment : vous pourrez
le déployer sur un nœud de terminaison ou l’utiliser pour effectuer des prédictions
groupées. Si quelque chose se passe mal durant l’entraînement, vous pouvez consulter
les journaux d’exécution sur la console : dans le menu de navigation ≡, sélectionnez
« Vertex AI » → Entraînement, cliquez sur votre tâche d’entraînement, puis sur
« Voir les journaux ». Sinon, vous pouvez cliquer sur l’onglet « Tâches personnali-
sées» et copier l’identicateur de la tâche (p. ex. 1234), puis sélectionner Journaux
dans le menu de navigation ≡ et consulter resource.labels.job_id=1234.
11.5 Entraîner des modèles sur plusieurs processeurs 527
parser = argparse.ArgumentParser()
parser.add_argument("--n_hidden", type=int, default=2)
parser.add_argument("--n_neurons", type=int, default=256)
parser.add_argument("--learning_rate", type=float, default=1e-2)
parser.add_argument("--optimizer", default="adam")
args = parser.parse_args()
def build_model(args):
with tf.distribute.MirroredStrategy().scope():
model = tf.keras.Sequential()
model.add(tf.keras.layers.Flatten(input_shape=[28, 28],
dtype=tf.uint8))
528 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
for _ in range(args.n_hidden):
model.add(tf.keras.layers.Dense(args.n_neurons, activation="relu"))
model.add(tf.keras.layers.Dense(10, activation="softmax"))
opt = tf.keras.optimizers.get(args.optimizer)
opt.learning_rate = args.learning_rate
model.compile(loss="sparse_categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
return model
hypertune = hypertune.HyperTune()
hypertune.report_hyperparameter_tuning_metric(
hyperparameter_metric_tag="accuracy", # nom de la métrique renvoyée
metric_value=max(history.history["val_accuracy"]), # valeur de la métrique
global_step=model.optimizer.iterations.numpy(),
)
Maintenant que votre script d’entraînement est prêt, vous devez dénir le type de
machine sur lequel vous souhaitez l’exécuter. Pour cela, vous devez dénir une tâche
personnalisée que VertexAI utilisera comme gabarit (ou template) pour chaque essai:
trial_job = aiplatform.CustomJob.from_local_script(
display_name="my_search_trial_job",
script_path="my_vertex_ai_trial.py", # chemin d’accès du script
# d’entraînement
container_uri="gcr.io/cloud-aiplatform/training/tf-gpu.2-4:latest",
staging_bucket=f"gs://{bucket_name}/staging",
accelerator_type="NVIDIA_TESLA_K80",
accelerator_count=2, # ici, chaque essai se fera sur 2 GPU
)
Enn vous voilà prêt à lancer et exécuter la tâche de réglage des hyperparamètres :
from google.cloud.aiplatform import hyperparameter_tuning as hpt
hp_job = aiplatform.HyperparameterTuningJob(
display_name="my_hp_search_job",
custom_job=trial_job,
11.5 Entraîner des modèles sur plusieurs processeurs 529
metric_spec={"accuracy": "maximize"},
parameter_spec={
"learning_rate": hpt.DoubleParameterSpec(min=1e-3, max=10,
scale="log"),
"n_neurons": hpt.IntegerParameterSpec(min=1, max=300, scale="linear"),
"n_hidden": hpt.IntegerParameterSpec(min=1, max=10, scale="linear"),
"optimizer": hpt.CategoricalParameterSpec(["sgd", "adam"]),
},
max_trial_count=100,
parallel_trial_count=20,
)
hp_job.run()
trials = hp_job.trials
trial_accuracies = [get_final_metric(trial, "accuracy") for trial in trials]
best_trial = trials[np.argmax(trial_accuracies)]
Regardons quelle est l’exactitude obtenue pour cet essai, et avec quelles valeurs
des hyperparamètres :
>>> max(trial_accuracies)
0.977400004863739
>>> best_trial.id
'98'
>>> best_trial.parameters
[parameter_id: "learning_rate" value { number_value: 0.001 },
parameter_id: "n_hidden" value { number_value: 8.0 },
parameter_id: "n_neurons" value { number_value: 216.0 },
parameter_id: "optimizer" value { string_value: "adam" }
]
530 Chapitre 11. Entraînement et déploiement à grande échelle de modèles TensorFlow
Vous disposez à présent de tous les outils et de toutes les connaissances néces-
saires pour créer des architectures de réseaux de neurones performantes, les entraîner
à grande échelle en utilisant diverses stratégies de distribution, sur votre propre
infrastructure ou dans le cloud, puis les déployer où vous le souhaitez. En d’autres
termes, vous disposez désormais de super-pouvoirs: utilisez-les au mieux !
11.6 EXERCICES
1. Que contient un SavedModel ? Comment pouvez-vous inspecter son
contenu ?
2. Quand devez-vous utiliser TF Serving ? Quelles sont ses principales
caractéristiques ? Donnez quelques outils que vous pouvez utiliser
pour le mettre en service.
3. Comment déployez-vous un modèle sur plusieurs instances de TF
Serving ?
4. Quand devez-vous utiliser l’API gRPC à la place de l’API REST pour
interroger un modèle servi par TF Serving ?
5. De quelles manières TFLite réduit-il la taille d’un modèle an qu’il
puisse s’exécuter sur un périphérique mobile ou embarqué ?
6. Qu’est-ce qu’un entraînement conscient de la quantication et
pourquoi en auriez-vous besoin ?
7. Expliquez ce que sont le parallélisme du modèle et le parallélisme des
données. Pourquoi conseille-t-on généralement ce dernier ?
8. Lors de l’entraînement d’un modèle sur plusieurs serveurs, quelles
stratégies de distribution pouvez-vous appliquer ? Comment
choisissez-vous celle qui convient ?
9. Entraînez un modèle (celui que vous voulez) et déployez-le sur TF
Serving ou Google Vertex AI. Écrivez le code client qui l’interrogera
au travers de l’API REST ou de l’API gRPC. Actualisez le modèle
et déployez la nouvelle version. Votre code client interrogera alors
cette nouvelle version. Revenez à la première.
10. Entraînez un modèle de votre choix sur plusieurs GPU sur la même
machine en utilisant MirroredStrategy (si vous n’avez pas accès
àdes GPU, vous pouvez employer Google Colab dans un environne-
ment d’exécution avec GPU et créer deux GPU logiques). Entraînez
de nouveau le modèle en utilisant CentralStorageStrategy
et comparez les temps d’entraînement.
11. Ajustez un modèle de votre choix sur Vertex AI, en utilisant le service
de réglage d’hyperparamètres de Keras Tuner ou celui de Vertex AI.
Les solutions de ces exercices sont données à l’annexeA.
Le mot de la n
337. Les corrigés se trouvent en n des notebooks des différents chapitres, sachant que le chapitre1 du
présent livre correspond au chapitre4 sur Github, puis le chapitre2 correspond au chapitre10 sur Github,
le chapitre3 au 11, etc. jusqu’au 11, correspondant au19.
536 Annexe A. Solutions des exercices
contraints de rester petits, donc les variables dont les valeurs sont plus
petites que les autres auront tendance à être ignorées.
3. Une descente de gradient ne peut pas rester bloquée sur un minimum
local lors de l’entraînement d’un modèle de régression logistique, car
la fonction de coût est convexe 338.
4. Si la fonction à optimiser est convexe (comme par exemple pour une
régression linéaire ou une régression logistique) et si le taux d’apprentissage
n’est pas trop élevé, alors tous les algorithmes de descente de gradient
s’approcheront du minimum global et produiront au nal des modèles
assez similaires. Cependant, si vous ne réduisez pas graduellement le taux
d’apprentissage, les descentes de gradient stochastique et par mini-lots ne
convergeront jamais véritablement: au lieu de cela, elles continueront
à errer autour du minimum global. Cela signie que, même si vous les
laissez s’exécuter pendant très longtemps, ces algorithmes de descente de
gradient produiront des modèles légèrement différents.
5. Si l’erreur de validation augmente régulièrement après chaque
époque, alors il se peut que le taux d’apprentissage soit trop élevé
et que l’algorithme diverge. Si l’erreur d’entraînement augmente
également, alors c’est clairement là qu’est le problème et vous devez
réduire le taux d’apprentissage. Par contre, si l’erreur d’entraînement
n’augmente pas, votre modèle surajuste le jeu d’entraînement et vous
devez arrêter l’entraînement.
6. Du fait de leur nature aléatoire, rien ne garantit qu’une descente de
gradient stochastique ou qu’une descente de gradient par mini-lots
fera des progrès à chaque itération de l’entraînement. Par conséquent,
si vous arrêtez immédiatement l’entraînement dès que l’erreur de
validation augmente, vous risquez de vous arrêter bien trop tôt, alors
que le minimum n’est pas atteint. Une meilleure solution consiste à
sauvegarder le modèle à intervalles réguliers, puis, lorsqu’il ne s’est
plus amélioré pendant assez longtemps (ce qui signie probablement
qu’il ne fera jamais mieux), vous pouvez revenir au meilleur modèle
sauvegardé.
7. La descente de gradient stochastique est celle dont l’itération
d’entraînement est la plus rapide étant donné qu’elle ne prend en
compte qu’une seule observation d’entraînement à la fois, et par
conséquent c’est celle qui arrive le plus rapidement à proximité
du minimum global (ou la descente de gradient par mini-
lots, lorsque la taille du lot est très petite). Cependant, seule la
descente de gradient ordinaire convergera effectivement, si le
temps d’entraînement est sufsant. Comme expliqué ci-dessus, les
descentes de gradient stochastique et par mini-lots continueront à
errer autour du minimum, à moins de réduire graduellement le taux
d’apprentissage.
338. Le segment de droite reliant deux points quelconques de la courbe ne traverse jamais la courbe.
Chapitre 1 : Les fondamentaux du Machine Learning 537
339. De plus, la résolution de l’équation normale nécessite l’inversion d’une matrice, mais cette matrice
n’est pas toujours inversible. Par contraste, la matrice de la régression ridge est toujours inversible.
538 Annexe A. Solutions des exercices
E=C D=A B
C=A B ET NON
D= A B OU OU exclusif
pour analyser les protobufs sérialisés (un exemple est donné dans la
section «Custom protobuf» du notebook 340). La procédure est plus
complexe et impose le déploiement du descripteur avec le modèle,
mais elle est réalisable.
6. Lorsque des chiers TFRecord sont utilisés, leur compression sera
généralement activée s’ils doivent être téléchargés par le script
d’entraînement. En effet, elle permet de réduire leur taille et donc
le temps de téléchargement. En revanche, si les chiers se trouvent
sur la même machine que le script d’entraînement, il est préférable
de la désactiver an de ne pas gaspiller du temps processeur dans la
décompression.
7. Voici les avantages et les inconvénients de chaque option de
prétraitement :
– Si les données sont prétraitées lors de la création des chiers de
données, le script d’entraînement s’exécutera plus rapidement,
car il n’aura pas à effectuer ces étapes à la volée. Dans certains
cas, la taille des données prétraitées sera également plus petite
que celle des données d’origine. Vous économiserez alors de la
place et accélérerez les téléchargements. Il peut être également
utile de matérialiser les données prétraitées, par exemple pour les
inspecter ou les archiver, mais cette approche présente quelques
inconvénients. Tout d’abord, il peut être difcile de mener des
expériences fondées sur différentes logiques de prétraitement
si un jeu de données prétraitées doit être généré pour chaque
cas. Ensuite, si vous souhaitez effectuer une augmentation des
données, vous devrez matérialiser de nombreuses variantes du jeu
de données, ce qui occupera un espace important sur le disque et
prendra beaucoup de temps. Enn, le modèle entraîné attendra
des données prétraitées et vous devrez donc ajouter le code de
prétraitement dans l’application avant qu’elle n’appelle le modèle.
– Si les données sont prétraitées dans un pipeline tf.data, il
est beaucoup plus facile d’ajuster la logique de prétraitement
et d’appliquer une augmentation des données. Par ailleurs,
tf.data simplie la construction de pipelines de prétraitement
extrêmement efcaces (par exemple, avec le multithread et la
lecture anticipée). Toutefois, un tel prétraitement des données
ralentira l’entraînement. De plus, chaque instance d’entraînement
sera prétraitée une fois par époque et non pas une seule fois lorsque
le prétraitement se fait au moment de la création des chiers de
données. Enn, le modèle entraîné attendra toujours des données
prétraitées. Mais si vous utilisez des couches de prétraitement
dans votre pipeline tf.data, alors vous pourrez simplement
réutiliser ces couches dans votre modèle nal (en les ajoutant
les noms de leurs entrées et sorties, les types et les formes). Chaque
métagraphe est identié par un ensemble de balises. Pour inspecter
un SavedModel, vous pouvez utiliser l’outil en ligne de commande
saved_model_cli ou l’examiner dans Python après l’avoir
chargé avec tf.saved_model.load().
2. TF Serving permet de déployer plusieurs modèles TensorFlow (ou
plusieurs versions du même modèle) et de les rendre facilement
accessibles à toutes vos applications au travers d’une API REST
ou gRPC. Si vous utilisiez les modèles directement dans vos
applications, le déploiement d’une nouvelle version d’un modèle sur
toutes les applications serait plus complexe. Développer votre propre
microservice intégrant un modèle TF demanderait beaucoup de
travail pour obtenir une petite partie des nombreuses fonctionnalités
de TF Serving. TF Serving peut surveiller un répertoire et déployer
automatiquement les modèles qui y sont placés, sans avoir à modier
ni même redémarrer les applications pour qu’elles bénécient de
nouvelles versions des modèles ; il est rapide, bien testé et s’adapte
parfaitement aux besoins. Il prend en charge les tests A/B de modèles
expérimentaux et le déploiement de nouvelles versions d’un modèle à
un groupe d’utilisateurs (dans ce cas, le modèle est appelé un canari).
Il est également capable de regrouper des requêtes individuelles en
lots pour les exécuter conjointement sur le GPU. Pour déployer TF
Serving, vous pouvez partir des chiers sources, mais il est plus simple
d’employer une image Docker. Pour déployer un groupe d’images
Docker de TF Serving, vous pouvez utiliser un outil d’orchestration,
comme Kubernetes, ou vous tourner vers une solution hébergée,
comme Vertex AI.
3. Pour déployer un modèle sur plusieurs instances de TF Serving, vous
devez simplement congurer ces instances pour qu’elles surveillent le
même répertoire de modèles, puis exporter votre nouveau modèle au
format SavedModel dans un sous-répertoire.
4. L’API gRPC est plus efcace que l’API REST. Toutefois, ses
bibliothèques clientes sont moins répandues et, en activant la
compression dans l’API REST, vous pouvez arriver à des performances
comparables. Par conséquent, l’API gRPC est la plus utile lorsque
vous avez besoin des plus hautes performances possible et lorsque les
clients ne sont pas limités à l’API REST.
5. Pour réduire la taille d’un modèle de sorte qu’il puisse s’exécuter sur
un périphérique mobile ou embarqué, TFLite met en œuvre plusieurs
techniques :
– Son convertisseur est capable d’optimiser un SavedModel : il
rétrécit le modèle et réduit son temps de réponse. Pour cela, il
élague toutes les opérations non indispensables aux prédictions
(comme les opérations d’entraînement) et optimise et fusionne
des opérations lorsque c’est possible.
Chapitre 11 : Entraînement et déploiement à grande échelle de modèles TensorFlow 563
est la plus simple car tous les serveurs et processeurs sont traités de
la même manière. De plus, ses performances sont plutôt bonnes.
De façon générale, vous devriez employer cette stratégie. Sa
principale limite est que le modèle doit tenir dans la mémoire de
chaque réplique.
– ParameterServerStrategy réalise un parallélisme des
données en mode asynchrone. Le modèle est répliqué sur tous les
processeurs sur tous les travailleurs, et les paramètres sont éclatés
sur tous les serveurs de paramètres. Chaque travailleur dispose de sa
propre boucle d’entraînement, qui s’exécute de façon asynchrone
par rapport aux autres travailleurs. Lors de chaque itération
d’entraînement, un travailleur reçoit son propre lot de données et
récupère la dernière version des paramètres du modèle à partir des
serveurs de paramètres. Il calcule ensuite les gradients de la perte
par rapport à chacun de ces paramètres et les envoie aux serveurs
de paramètres. Ceux-ci réalisent une étape de descente de gradient
à partir des gradients reçus. Cette stratégie est généralement plus
lente que la précédente et un peu plus difcile à déployer, car
elle exige une gestion des serveurs de paramètres. Toutefois, elle
se révélera utile pour l’entraînement de modèles extrêmement
volumineux qui ne tiennent pas dans la mémoire du GPU.
Les solutions des exercices 9 à 11 gurent à la n du notebook Jupyter 19_training_
and_deploying_at_scale.ipynb disponible sous https://homl.info/colab3.
Annexe B
Différentiation
automatique
DIFFÉRENTIATION MANUELLE
La première option consiste à prendre un crayon et une feuille de papier, et à exploiter
nos connaissances en algèbre pour dériver l’équation appropriée. Pour la fonction
f(x,y) dénie précédemment, cela n’a rien de très complexe. Nous utilisons simple-
ment cinq règles:
• La dérivée d’une constante est 0.
• La dérivée de λx est λ (où λ est une constante).
• La dérivée de x λ est λxλ –1 ; la dérivée de x2 est donc 2x.
• La dérivée d’une somme de fonctions est la somme des dérivées de ces fonctions.
• La dérivée de λ fois une fonction est λ fois la dérivée de la fonction.
En appliquant ces règles, nous obtenons les dérivées suivantes.
566 Annexe B. Différentiation automatique
∂ f ∂( x y) ∂ y ∂2 ∂( x2 )
2
= + + =y + 0 + 0 = 2xy
∂x ∂x ∂ x ∂x ∂x
∂ f ∂ (x y ) ∂ y ∂2
2
= + + = x2 + 1 + 0 = x2 + 1
∂y ∂y ∂ y ∂y
Avec des fonctions plus complexes, cette méthode peut devenir très fastidieuse et
le risque d’erreurs n’est pas négligeable. Heureusement, il existe d’autres approches,
notamment l’approximation par différences nies.
h ( x) – h (x 0 )
h ′(x0 ) = lim
x →x 0 x – x0
h( x0 + ε ) – h( x0 )
= lim
ε →0 ε
Par conséquent, si nous voulons calculer la dérivée partielle de f(x, y) par rapport à
x, pour x=3 et y=4, nous pouvons simplement calculer f(3+ε, 4) – f(3, 4) et diviser
le résultat par ε, en prenant ε très petit. Ce type d’approximation numérique de la
dérivée est appelé approximation par différences nies et cette équation spécique est le
taux d’accroissement. C’est exactement ce que fait le code suivant :
def f(x, y):
return x**2*y + y + 2
Malheureusement, le résultat n’est pas précis, et c’est encore pire pour les fonc-
tions plus complexes. Les valeurs exactes sont, respectivement, 24 et 10, alors que
nous obtenons à la place:
>>> df_dx
24.000039999805264
>>> df_dy
10.000000000331966
Différentiation automatique en mode direct 567
Pour calculer les deux dérivées partielles, nous appelons f() au moins trois fois
(elle a été appelée à quatre reprises dans le code précédent, mais il peut être optimisé).
Si nous avions 1 000 paramètres, nous devrions appeler f() au moins 1 001 fois.
Dans le cas des grands réseaux de neurones, l’approximation par différences nies
manque donc totalement d’efcacité.
Toutefois, cette différentiation est tellement simple à mettre en œuvre qu’elle
constitue un bon outil de vérication de l’implémentation des autres méthodes. Par
exemple, si elle contredit la dérivée manuelle d’une fonction, il est probable que
cette dernière comporte une erreur.
Pour le moment, nous avons vu deux façons de calculer des gradients: la différentia-
tion manuelle et l’approximation par différences nies. Malheureusement, aucune des
deux ne convient à l’entraînement d’un réseau de neurones à grande échelle. Tournons-
nous vers la différentiation automatique, en commençant par le mode direct.
L’algorithme parcourt le graphe de calcul, depuis les entrées vers les sorties. Il
commence par déterminer la dérivée partielle des nœuds feuilles. Le nœud de
constante (5) retourne la constante 0, car la dérivée d’une constante est toujours 0.
Le nœud de variable x retourne la constante 1 car ∂x/∂x = 1, et celui de y retourne la
constante 0 car ∂y/∂x = 0 (si nous recherchions la dérivée partielle par rapport à y,
ce serait l’inverse).
568 Annexe B. Différentiation automatique
λ ( a + bε ) = λ a + λ bε
( a + bε ) + ( c + dε ) = ( a + c) + ( b + d) ε
( a + bε ) × ( c + dε ) = ac + ( ad + bc) ε + ( bd) ε 2 = ac + ( ad + bc) ε
Différentiation automatique en mode inverse 569
f
x
a + bε avec ε 2 = 0
stocké sous forme d’un couple de nombres à virgule
Nombre dual flottante (a, b), comme (42.0, 24.0) et non pas 42.000024
Figure B.2 – Différentiation automatique en mode direct avec des nombres duaux
∂f
Pour calculer (3,4) , nous devons à nouveau parcourir le graphe, mais cette
∂x
fois-ci avec x=3 et y=4+ε .
La différentiation automatique en mode direct est donc bien plus précise que l’ap-
proximation par différences nies, mais elle souffre du même inconvénient majeur,
tout au moins en cas de nombreuses entrées et de peu de sorties (ce que l’on trouve
avec les réseaux de neurones): elle demande 1 000 passes dans le graphe pour calculer
les dérivées partielles sur 1 000 paramètres. C’est dans cette situation que la différen-
tiation automatique en mode inverse brille : elle est capable de les calculer toutes en
seulement deux passes dans le graphe.
automatique vient de cette seconde passe sur le graphe, dans laquelle les gradients
vont dans le sens inverse. La gureB.3 illustre cette seconde phase. Au cours de la
première, toutes les valeurs des nœuds ont été calculées, en partant de x=3 et y=4.
Ces valeurs sont données en partie inférieure droite de chaque nœud (par exemple,
x×x = 9). Pour faciliter la lecture, les nœuds sont libellés n1 à n7. Le nœud de sortie
est n7 : f(3, 4) = n7 =42.
∂f ∂f ∂n
= × i
∂ x ∂n i ∂x
∂f
Puisque n7 est le nœud de sortie, f = n 7 et =1.
∂n 7
Continuons à descendre dans le graphe jusqu’à n5 : quelle est la variation de f
∂f ∂f ∂n
lorsque n5 varie ? La réponse est = × 7 . Puisque nous savons déjà que
∂ n5 ∂n7 ∂n5
∂f ∂n 7
= 1, nous avons uniquement besoin de . Puisque n 7 effectue simplement la
∂ n7 ∂ n5
∂n 7 ∂f
somme n5+n6, nous trouvons que = 1 , et donc = 1 × 1 = 1.
∂ n5 ∂ n5
Différentiation automatique en mode inverse 571
∂n5 ∂f
minons que = n 2, et donc = 1 × n = 4.
2
∂ n4 ∂n4
Le processus continue jusqu’à atteindre la base du graphe. À ce stade, nous aurons
calculé toutes les dérivées partielles de f(x,y) au point x = 3 et y = 4. Dans cet exemple,
∂f ∂f
nous trouvons = 2 4 et = 10 . Il semble que nous ayons bon !
∂x ∂y
La différentiation automatique en mode inverse est une technique très puissante
et précise, en particulier lorsque nous avons de nombreuses entrées et peu de sorties.
En effet, elle ne demande qu’une seule passe en avant et une passe en arrière par
sortie pour calculer toutes les dérivées partielles de toutes les sorties par rapport à
toutes les entrées. Lors de l’entraînement des réseaux de neurones, nous souhaitons
généralement minimiser la perte. Nous avons par conséquent une seule sortie (la
perte), et deux passes sur le graphe sufsent pour calculer les gradients. La différen-
tiation automatique accepte également les fonctions qui ne sont pas différentiables
en tout point, à condition de lui demander de calculer les dérivées partielles en des
points où elles sont bien différentiables.
À la gureB.3, les résultats numériques sont calculés à la volée, à chaque nœud.
TensorFlow procède différemment, en créant à la place un nouveau graphe de calcul.
Autrement dit, il met en œuvre une différentiation automatique en mode inverse
symbolique. De cette manière, le graphe qui sert au calcul des gradients de la perte par
rapport à tous les paramètres du réseau de neurones n’est généré qu’une seule fois et
peut être exécuté ensuite à de nombreuses reprises, dès que l’optimiseur a besoin de
calculer les gradients. De plus, cela permet de calculer des dérivés d’ordre supérieur,
si nécessaire.
RÉSEAUX DE HOPFIELD
Les réseaux de Hopeld ont été présentés pour la première fois par W. A. Little en 1974,
puis rendus populaires par J. Hopeld en 1982. Il s’agit de réseaux à mémoire associa-
tive : on commence par leur apprendre certains motifs, puis, lorsqu’ils rencontrent
un nouveau motif, ils produisent (avec un peu de chance) le motif appris le plus
proche. Ils se sont révélés utiles notamment dans la reconnaissance des caractères,
avant d’être dépassés par d’autres approches. On entraîne tout d’abord le réseau en
lui fournissant des exemples d’images de caractères (chaque pixel binaire correspond
à un neurone) et, lorsqu’on lui montre une nouvelle image de caractère, il produit en
sortie, après quelques itérations, le caractère appris le plus proche.
Ce sont des graphes intégralement connectés (voir la gure C.1), car chaque
neurone est connecté à chaque autre neurone. Puisque les images de la gure font
6×6pixels, le réseau de neurones représenté à gauche devrait contenir 36 neurones
(et 630 connexions), mais, pour une meilleure lisibilité, nous avons dessiné un réseau
bien plus petit.
574 Annexe C. Autres architectures de réseaux de neurones artificiels répandues
Motif
stable
L’algorithme d’entraînement se fonde sur la loi de Hebb. Pour chaque image d’en-
traînement, le poids entre deux neurones est augmenté si les pixels correspondants sont
tous deux allumés ou éteints, mais diminué si l’un des pixels est allumé et l’autre, éteint.
Pour montrer une nouvelle image au réseau, vous activez simplement les neurones
qui correspondent aux pixels allumés. Le réseau calcule ensuite la sortie de chaque
neurone, et l’on obtient ainsi une nouvelle image. Vous pouvez prendre cette nou-
velle image et répéter l’intégralité du processus. Au bout d’un certain temps, le réseau
parvient à un état stable. En général, cela correspond à l’image d’entraînement qui
ressemble le plus à l’image d’entrée.
Une fonction d’énergie est associée à un réseau de Hopeld. À chaque itération,
l’énergie diminue, ce qui garantit à terme une stabilisation du réseau dans un état
de faible énergie. L’algorithme d’entraînement ajuste les poids de sorte que le niveau
d’énergie des motifs d’entraînement diminue. De cette manière, le réseau nira
normalement par se stabiliser dans l’une de ces congurations de faible énergie.
Malheureusement, il est possible que des motifs qui ne faisaient pas partie du jeu
d’entraînement nissent également par avoir une énergie faible. Le réseau se stabilise
donc parfois dans une conguration qui n’a pas été apprise. Il s’agit alors de faux motifs.
Les réseaux de Hopeld ont également pour autre inconvénient majeur une dif-
culté à grandir. Leur capacité de stockage est approximativement égale à 14% du
nombre de neurones. Par exemple, pour classer des images de 28 × 28 pixels, il faut
un réseau de Hopeld avec 784 neurones intégralement connectés, et donc 306 936
poids. Un tel réseau ne pourrait apprendre qu’environ 110 caractères différents (14%
de 784). Cela fait beaucoup de paramètres pour une si petite capacité de stockage.
MACHINES DE BOLTZMANN
Les machines de Boltzmann ont été inventées en 1985 par Geoffrey Hinton et Terrence
Sejnowski. À l’instar des réseaux de Hopeld, ce sont des réseaux intégralement
connectés, mais elles se fondent sur des neurones stochastiques : au lieu que la valeur
de sortie soit décidée par une fonction échelon déterministe, ces neurones produisent
Machines de Boltzmann 575
un1 avec une certaine probabilité, sinon un 0. La fonction de probabilité employée par
ces réseaux repose sur la distribution de Boltzmann (utilisée en mécanique statistique),
d’où leur nom. L’équationC.1 donne la probabilité qu’un neurone particulier produise
un 1.
N
∑ w i, j s j + b i
p (s(éta
i
pe suivante)
= 1) = σ j=1
T
• sj est l’état du j ème neurone (0 ou 1).
• wi,j est le poids de la connexion entre les ième et j ème neurones. Notez que wi,i est
xé à 0.
• bi est le terme constant du ième neurone. On peut implémenter ce terme en
ajoutant un neurone de terme constant au réseau.
• N est le nombre de neurones dans le réseau.
• T est un nombre qui donne la température du réseau ; plus la température est
élevée, plus la sortie est aléatoire (plus la probabilité approche de 50%).
• σ est la fonction logistique.
Dans les machines de Boltzmann, les neurones sont séparés en deux groupes: les
unités visibles et les unités cachées (voir la gureC.2). Tous les neurones opèrent de la
même manière stochastique, mais ce sont les unités visibles qui reçoivent les entrées
et qui fournissent les sorties.
Visibles Cachés
Entrée
Sortie
conguration d’origine est « oubliée », il est dit en équilibre thermique (même si sa con-
guration change en permanence). En xant correctement les paramètres du réseau, en
le laissant atteindre un équilibre thermique et en observant son état, il est possible de
simuler une grande diversité de lois de probabilité. Il s’agit d’un modèle génératif.
Entraîner une machine de Boltzmann signie trouver les paramètres qui permet-
tront au réseau d’approcher la loi de probabilité du jeu d’entraînement. Par exemple,
s’il y a trois neurones visibles et si le jeu d’entraînement comprend 75% de triplets
(0, 1, 1), 10% de triplets (0, 0, 1) et 15% de triplets (1, 1, 1), alors, au terme de
l’entraînement d’une machine de Boltzmann, vous pouvez l’utiliser pour générer des
triplets binaires aléatoires avec la même loi de probabilité. Par exemple, environ
75% des triplets générés par cette machine de Boltzmann seront (0, 1, 1).
Un tel modèle génératif peut être exploité de différentes manières. Par exemple, s’il
est entraîné sur des images et si l’on fournit au réseau une image partielle ou brouillée,
il la «réparera » automatiquement de manière raisonnable. Vous pouvez également
utiliser un modèle génératif pour la classication : il suft pour cela d’ajouter quelques
neurones visibles pour encoder la classe de l’image d’entraînement (par exemple,
ajouter dix neurones visibles et pendant l’entraînement activer uniquement le cin-
quième lorsque l’image d’entraînement représente un 5). Ensuite, lorsqu’une nou-
velle image sera fournie, le réseau activera automatiquement les neurones visibles
appropriés, indiquant ainsi la classe de l’image (par exemple, il allumera le cinquième
neurone visible si l’image représente un 5).
Malheureusement, il n’existe aucune technique vraiment efcace pour entraîner
des machines de Boltzmann. En revanche, des algorithmes assez puissants ont été
développés pour entraîner des machines de Boltzmann restreintes.
Couche
cachée
Couche
visible
Entrée Sortie
wi ,j ← wi ,j + η (xhT – x′h′ T)
L’avantage de cet algorithme est qu’il est inutile d’attendre que le réseau atteigne
un équilibre thermique. Il fait un aller-retour-aller, et c’est tout. Il est incomparable-
ment plus efcace que les algorithmes précédents et cette efcacité a été essentielle
aux premiers succès du Deep Learning fondé sur l’empilement de multiples RBM.
RBM3
RBM 2
Entrée Sortie
Étiquettes
RBM 1
Entrée Sortie
Caractéristiques
Tout d’abord, RBM 1 est entraîné sans supervision et apprend les caractéris-
tiques de bas niveau présentes dans les données d’entraînement. Ensuite, RBM2 est
entraîné à partir des unités cachées de RBM1, qui lui servent d’entrées, de nouveau
sans supervision. Il apprend des caractéristiques de plus haut niveau (notons que les
unités cachées de RBM2 comprennent uniquement les trois unités de droite, sans
les unités d’étiquettes). Voilà l’idée générale; plusieurs autres RBM peuvent être
empilés ainsi. Jusque-là, l’entraînement a été intégralement non supervisé. Enn,
RBM3 est entraîné en utilisant en entrée les unités cachées de RBM2 et des unités
visibles supplémentaires pour représenter les étiquettes cibles (par exemple, un vec-
teur one-hot donnant la classe de l’instance). Il apprend à associer des caractéris-
tiques de haut niveau à des étiquettes d’entraînement. Cette étape est supervisée.
Au terme de l’entraînement, si vous fournissez une nouvelle instance à RBM1,
le signal se propage jusqu’à RBM2, puis jusqu’au sommet de RBM3, pour revenir
ensuite vers les unités d’étiquettes. Si le réseau a bien appris, alors l’étiquette appro-
priée sera révélée. Voilà comment un DBN peut servir à la classication.
Cette approche semi-supervisée présente un grand avantage: il n’est pas néces-
saire de disposer d’une grande quantité de données d’entraînement étiquetées. Si les
RBM non supervisés réalisent un bon travail, une petite quantité d’instances d’en-
traînement étiquetées par classe sufra. C’est comme un jeune enfant qui apprend à
reconnaître des objets sans supervision. Lorsque vous lui montrez une chaise et dites
«chaise », il peut associer le mot « chaise » à la classe des objets qu’il a déjà appris à
reconnaître par lui-même. Vous n’avez pas besoin de désigner chaque chaise indivi-
duelle en annonçant « chaise » ; seuls quelques exemples sufront (ils doivent tout
Cartes autoadaptatives 579
de même être sufsamment nombreux pour que l’enfant sache que vous faites réfé-
rence à la chaise et non au chat assis dessus, ou à la couleur de la chaise ou encore à
son dossier ou ses pieds).
Étonnamment, les DBN peuvent également travailler en sens inverse. Si vous
activez l’une des unités d’étiquettes, le signal se propage jusqu’aux unités cachées de
RBM3, puis descend vers RBM2 et RBM1. Une nouvelle instance est alors générée
par les unités visibles de RBM1. Elle ressemble en général à une instance normale
de la classe dont vous avez activé l’unité d’étiquette. Cette capacité de génération
des DBN est très puissante. Elle a été employée, par exemple, pour générer auto-
matiquement des légendes d’images, et inversement. Tout d’abord, un DBN est
entraîné (sans supervision) pour apprendre des caractéristiques dans des images, et
un autre DBN est entraîné (de nouveau sans supervision) pour apprendre des carac-
téristiques dans des jeux de légendes (par exemple, «voiture » vient souvent avec
« automobile »). Ensuite, un RBM est empilé au-dessus des deux DBN et entraîné
avec un jeu d’images et leur légende. Il apprend à relier des caractéristiques de haut
niveau dans des images avec des caractéristiques de haut niveau dans des légendes.
Si vous fournissez ensuite la photo d’une voiture au DBN d’images, le signal se pro-
page dans le réseau jusqu’au RBM supérieur, et redescend vers le DBN de légendes,
générant une légende. En raison de la nature stochastique des RBM et des DBN,
la légende change de façon aléatoire, mais elle correspond généralement bien à
l’image. En générant quelques centaines de légendes, il est fort probable que celles
qui reviennent le plus souvent constituent une bonne description de l’image 343.
CARTES AUTOADAPTATIVES
Les cartes autoadaptatives (SOM, self-organizing map) sont assez différentes de tous les
autres types de réseaux de neurones dont nous avons parlé jusqu’à présent. Elles sont
utilisées pour générer une représentation en petites dimensions d’un jeu de données
en grandes dimensions, le plus souvent pour la visualisation, le partitionnement ou la
classication. Les neurones sont répartis sur une carte (généralement en 2D pour la
visualisation, mais le nombre de dimensions peut être quelconque), comme l’illustre
la gureC.5, et chaque neurone possède une connexion pondérée avec chaque entrée
(la gure montre uniquement deux entrées, mais elles sont généralement beaucoup
plus nombreuses, puisque l’objectif des SOM est de réduire la dimension).
343. Pour plus de détails et une démonstration, voir la vidéo publiée par Geoffrey Hinton à l’adresse
https://homl.info/137
580 Annexe C. Autres architectures de réseaux de neurones artificiels répandues
Entrées
Après l’entraînement du réseau, vous pouvez lui fournir une nouvelle instance,
qui n’activera qu’un seul neurone (donc un point sur la carte) : celui dont le vecteur
de poids est le plus proche du vecteur d’entrée. En général, les instances qui sont
proches dans l’espace d’entrée d’origine vont activer des neurones qui sont proches
sur la carte. C’est pourquoi les SOM conviennent bien à la visualisation (vous pouvez
notamment identier facilement des groupes d’instances similaires sur la carte),
mais également à des applications comme la reconnaissance vocale. Par exemple, si
chaque instance représente l’enregistrement audio d’une personne qui prononce une
voyelle, alors différentes prononciations de la voyelle « a » activeront des neurones
dans la même zone de la carte, tandis que les instances de la voyelle « e » activeront
des neurones dans une autre zone. Les sons intermédiaires activeront généralement
des neurones intermédiaires sur la carte.
344. Voir le chapitre8 de l’ouvrage Machine Learning avec Scikit-Learn, A.Géron, Dunod (3eédition, 2023).
Cartes autoadaptatives 581
Le neurone qui mesure la plus petite distance l’emporte et ajuste son vecteur de poids
pour se rapprocher plus encore du vecteur d’entrée. Il devient alors le favori lors des
compétitions suivantes sur d’autres entrées similaires à celle-ci. Il implique également
les neurones voisins, qui actualisent leur vecteur de poids pour le rapprocher légère-
ment du vecteur d’entrée (mais pas autant que le neurone victorieux). L’algorithme
choisit ensuite une autre instance d’entraînement et répète le processus, encore et
encore. Les neurones voisins ont ainsi tendance à se spécialiser progressivement dans
des entrées comparables345.
345. Imaginons une classe de jeunes enfants ayant des compétences à peu près équivalentes. L’un d’eux se
révèle légèrement meilleur que les autres au basket. Cela l’encourage à pratiquer encore plus, notamment
avec ses amis. Après un certain temps, ce groupe de copains devient si bon au basket que les autres enfants
ne peuvent pas rivaliser. Mais ce n’est pas un problème, car ces autres jeunes se seront spécialisés dans
d’autres domaines. À terme, la classe est constituée de petits groupes spécialisés.
Annexe D
Structures de données
spéciales
Dans cette annexe, nous présentons très rapidement les structures de données recon-
nues par TensorFlow, autres que les tenseurs à virgule ottante ou entiers classiques.
Cela comprend les chaînes de caractères, les tenseurs irréguliers, les tenseurs creux,
les tableaux de tenseurs, les ensembles et les les d’attente.
CHAÎNES DE CARACTÈRES
Ces tenseurs contiennent des chaînes de caractères d’octets et se révéleront parti-
culièrement utiles pour le traitement automatique du langage naturel (voir le cha-
pitre8) :
>>> tf.constant(b"hello world")
<tf.Tensor: shape=(), dtype=string, numpy=b'hello world'>
Il est également possible de créer des tenseurs qui représentent des chaînes de
caractères Unicode, simplement en créant un tableau d’entiers 32bits représentant
chacun un seul point de code Unicode346 :
>>> u = tf.constant([ord(c) for c in "café"])
>>> u
<tf.Tensor: shape=(4,), [...], numpy=array([ 99, 97, 102, 233], dtype=int32)>
346. Si les points de code Unicode ne vous sont pas familiers, consultez la page https://homl.info/unicode.
584 Annexe D. Structures de données spéciales
Vous pouvez également manipuler des tenseurs qui contiennent plusieurs chaînes
de caractères :
>>> p = tf.constant(["Café", "Coffee", "caffè", "咖啡"])
>>> tf.strings.length(p, unit="UTF8_CHAR")
<tf.Tensor: shape=(4,), dtype=int32, numpy=array([4, 6, 5, 2], dtype=int32)>
>>> r = tf.strings.unicode_decode(p, "UTF8")
>>> r
<tf.RaggedTensor [[67, 97, 102, 233], [67, 111, 102, 102, 101, 101],
[99, 97, 102, 102, 232], [21654, 21857]]>
TENSEURS IRRÉGULIERS
Un tenseur irrégulier (ragged tensor) est un tenseur de type particulier qui représente
une liste de tableaux de tailles différentes. Plus généralement, il s’agit d’un tenseur avec
une ou plusieurs dimensions irrégulières, autrement dit des dimensions dont les parties
peuvent avoir différentes longueurs. Dans le tenseur irrégulier r, la deuxième dimen-
sion est une dimension irrégulière. Dans tous les tenseurs irréguliers, la première dimen-
sion est toujours une dimension régulière (également appelée dimension uniforme).
Tous les éléments du tenseur irrégulier r sont des tenseurs normaux. Examinons
par exemple le deuxième élément:
>>> r[1]
<tf.Tensor: [...], numpy=array([ 67, 111, 102, 102, 101, 101], dtype=int32)>
Le résultat n’est pas trop surprenant. Les tenseurs de r2 ont été ajoutés après les
tenseurs de r sur l’axe 0. Que se passe-t-il si nous concaténons r et un autre tenseur
irrégulier le long de l’axe1 ?
>>> r3 = tf.ragged.constant([[68, 69, 70], [71], [], [72, 73]])
>>> print(tf.concat([r, r3], axis=1))
<tf.RaggedTensor [[67, 97, 102, 233, 68, 69, 70], [67, 111, 102, 102, 101, 101,
71], [99, 97, 102, 102, 232], [21654, 21857, 72, 73]]>
Cette fois-ci, vous remarquerez que le ième tenseur de r et le ième tenseur de r3 ont
été concaténés. Le résultat est plus inhabituel, car tous ces tenseurs peuvent avoir des
longueurs différentes.
Si vous invoquez la méthode to_tensor(), le tenseur irrégulier est converti
en un tenseur normal par remplissage des tenseurs plus courts avec des zéros an
d’obtenir des longueurs identiques (la valeur par défaut est modiable via l’argument
default_value) :
>>> r.to_tensor()
<tf.Tensor: id=1056, shape=(4, 6), dtype=int32, numpy=
array([[ 67, 97, 102, 233, 0, 0],
[ 67, 111, 102, 102, 101, 101],
[ 99, 97, 102, 102, 232, 0],
[21654, 21857, 0, 0, 0, 0]], dtype=int32)>
TENSEURS CREUX
TensorFlow sait aussi représenter efcacement les tenseurs creux (sparse tensors),
c’est-à-dire les tenseurs contenant principalement des zéros. Créez simplement un
tf.SparseTensor, en précisant les indices et les valeurs des éléments différents
de zéro, ainsi que la forme du tenseur. Les indices doivent être fournis conformément
à l’ordre normal de lecture (de gauche à droite, et de haut en bas). Si vous avez un
doute, utilisez tf.sparse.reorder(). Vous pouvez convertir un tenseur creux
en un tenseur dense (normal) avec tf.sparse.to_dense() :
>>> s = tf.SparseTensor(indices=[[0, 1], [1, 0], [2, 3]],
... values=[1., 2., 3.],
... dense_shape=[3, 4])
...
>>> tf.sparse.to_dense(s)
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[0., 1., 0., 0.],
[2., 0., 0., 0.],
[0., 0., 0., 3.]], dtype=float32)>
586 Annexe D. Structures de données spéciales
Notez que les possibilités de manipulation des tenseurs creux sont inférieures à
celles des tenseurs denses. Par exemple, si vous pouvez multiplier un tenseur creux
par une valeur scalaire, produisant un nouveau tenseur creux, il est en revanche
impossible d’ajouter une valeur scalaire à un tenseur creux, car cela ne donnerait pas
un tenseur creux :
>>> s * 42.0
<tensorflow.python.framework.sparse_tensor.SparseTensor at 0x7f84a6749f10>
>>> s + 42.0
[...] TypeError: unsupported operand type(s) for +: 'SparseTensor' and 'float'
TABLEAUX DE TENSEURS
Un tf.TensorArray représente une liste de tenseurs. Cette structure de don-
nées se révélera pratique dans les modèles dynamiques qui incluent des boucles, car
elle permettra d’accumuler des résultats et d’effectuer ultérieurement des statistiques.
Vous pouvez lire ou écrire des tenseurs dans n’importe quel emplacement du tableau:
array = tf.TensorArray(dtype=tf.float32, size=3)
array = array.write(0, tf.constant([1., 2.]))
array = array.write(1, tf.constant([3., 10.]))
array = array.write(2, tf.constant([5., 7.]))
tensor1 = array.read(1) # => renvoie (et remet à zéro !) tf.constant([3., 10.])
Par défaut, la lecture d’un élément le remplace par un tenseur de même forme
rempli de zéros. Si vous ne voulez pas cela, vous pouvez donner à clear_after_
read la valeur False.
Lorsque vous écrivez dans le tableau, vous devez réaffecter la sortie au
tableau, comme nous l’avons fait dans l’exemple de code. Dans le cas
contraire, votre code fonctionnera parfaitement en mode pressé (eager
mode) mais échouera en mode graphe (ces modes ont été décrits au
chapitre 4).
Par défaut, un TensorArray a une taille xe, dénie par le paramètre size au
moment de la création. Vous pouvez également ne pas la préciser et, à la place, indi-
quer size=0 et dynamic_size=True an que le tableau puisse grandir auto-
matiquement lorsque nécessaire, mais les performances en pâtiront. Par conséquent,
si vous connaissez la taille à l’avance, il vaut mieux utiliser un tableau de taille xe.
Vous devez également préciser dtype, et tous les éléments doivent avoir la même
forme que le premier ajouté au tableau.
Tous les éléments peuvent être empilés dans un tenseur normal en invoquant la
méthode stack() :
>>> array.stack()
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 2.],
[0., 0.],
[5., 7.]], dtype=float32)>
Ensembles 587
ENSEMBLES
TensorFlow prend en charge les ensembles d’entiers ou de chaînes de caractères
(mais pas les ensembles de réels). Il les représente à l’aide de tenseurs normaux. Par
exemple, l’ensemble {1, 5, 9} est représenté par le tenseur [[1, 5, 9]].
Notez que celui-ci doit avoir au moins deux dimensions et que l’ensemble doit se
trouver dans la dernière. Par exemple, [[1, 5, 9], [2, 5, 11]] est un ten-
seur qui contient deux ensembles indépendants, {1, 5, 9} et {2, 5, 11}. Si
certains ensembles sont plus courts que d’autres, vous devez les remplir à l’aide d’une
valeur de remplissage (par défaut 0, mais vous pouvez la modier).
Le package tf.sets comprend plusieurs fonctions de manipulation des
ensembles. Par exemple, créons deux ensembles et déterminons leur union (le
résultat étant un tenseur creux, nous appelons to_dense() pour l’afcher) :
>>> a = tf.constant([[1, 5, 9]])
>>> b = tf.constant([[5, 6, 9, 11]])
>>> u = tf.sets.union(a, b)
>>> u
<tensorflow.python.framework.sparse_tensor.SparseTensor at 0x132b60d30>
>>> tf.sparse.to_dense(u)
<tf.Tensor: [...] numpy=array([[ 1, 5, 6, 9, 11]], dtype=int32)>
Si vous préférez utiliser une valeur de remplissage différente, telle que –1, alors
vous devez spécier default_value=-1 (ou la valeur de votre choix) lors de
l’appel à to_dense().
La valeur par défaut de default_value est 0. Par conséquent, lorsque
vous manipulez des ensembles de chaînes de caractères, vous devez
nécessairement changer default_value (par exemple, en la fixant à
la chaîne vide).
Parmi les autres fonctions disponibles dans tf.sets, nous trouvons par exemple
difference(), intersection() et size(), dont le rôle est évident. Pour
vérier si un ensemble contient ou non certaines valeurs, vous pouvez calculer l’in-
tersection de cet ensemble et de ces valeurs. Pour ajouter des valeurs à un ensemble,
calculez l’union de l’ensemble et des valeurs.
588 Annexe D. Structures de données spéciales
FILES D’ATTENTE
Une le d’attente est une structure de données dans laquelle vous pouvez ajouter
des enregistrements de données, pour les en extraire plus tard. TensorFlow implé-
mente plusieurs types de les d’attente dans le package tf.queue. Elles étaient
très importantes pour la mise en œuvre efcace du chargement des données et des
pipelines de prétraitement, mais l’API tf.data les a pratiquement rendues obsolètes
(excepté peut-être dans quelques rares cas) car elle est beaucoup plus simple d’emploi
et fournit tous les outils nécessaires à la construction de pipelines efcaces. Toutefois,
par souci d’exhaustivité, examinons-les rapidement.
La le d’attente la plus simple est de type premier entré/premier sorti (FIFO,
rstin, rstout). Pour la construire, vous devez préciser le nombre d’enregistrements
qu’elle contiendra. Par ailleurs, chaque enregistrement étant un n-uplet de tenseurs,
vous devez préciser le type de chaque tenseur et, éventuellement, leur forme. Par
exemple, le code suivant crée une le d’attente FIFO avec un maximum de trois enre-
gistrements, chacun contenant un n-uplet constitué d’un entier sur 32bits et d’une
chaîne de caractères. Il y ajoute ensuite deux enregistrements, afche sa taille (2 à ce
stade) et extrait un enregistrement:
>>> q = tf.queue.FIFOQueue(3, [tf.int32, tf.string], shapes=[(), ()])
>>> q.enqueue([10, b"windy"])
>>> q.enqueue([15, b"sunny"])
>>> q.size()
<tf.Tensor: shape=(), dtype=int32, numpy=2>
>>> q.dequeue()
[<tf.Tensor: shape=(), dtype=int32, numpy=10>,
<tf.Tensor: shape=(), dtype=string, numpy=b'windy'>]
Dans cette annexe, nous étudions les graphes générés par les fonctions TF (voir le
chapitre4).
Chaque fois que vous appelez une fonction TF avec une nouvelle combinaison
de types ou de formes d’entrées, elle génère une nouvelle fonction concrète ayant son
propre graphe adapté à cette combinaison spécique. Une telle combinaison de types
et de formes d’arguments est appelée signature d’entrée. Lorsque vous appelez la fonc-
tion TF avec une signature d’entrée qu’elle a déjà rencontrée, elle réutilise la fonc-
tion concrète générée précédemment.
Par exemple, pour l’appel tf_cube(tf.constant(3.0)), la fonction TF réu-
tilise la fonction concrète déjà employée pour tf_cube(tf.constant(2.0))
(pour des tenseurs scalaires de type oat32). En revanche, elle générera une nouvelle
fonction concrète pour les appels tf_cube(tf.constant([2.0])) ou tf_
cube(tf.constant([3.0])) (pour des tenseurs de type oat32 et de forme
[1]), et encore une autre pour l’appel tf_cube(tf.constant([[1.0, 2.0],
[3.0, 4.0]])) (pour des tenseurs de type oat32 et de forme [2, 2]).
Vous pouvez obtenir la fonction concrète qui correspond à une combinaison par-
ticulière d’entrées en invoquant la méthode get_concrete_function() de la
fonction TF. Elle s’utilise ensuite comme une fonction normale, mais ne prenant en
592 Annexe E. Graphes TensorFlow
charge qu’une seule signature d’entrée (dans l’exemple suivant, des tenseurs scalaires
de type oat32) :
>>> concrete_function = tf_cube.get_concrete_function(tf.constant(2.0))
>>> concrete_function
<ConcreteFunction tf_cube(x) at 0x7F84411F4250>
>>> concrete_function(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>
Sortie Sortie
Entrée
Fonction TF
Dans ces graphes, les tenseurs sont des tenseurs symboliques. Autrement dit, ils
n’ont pas de valeur réelle, juste un type de données, une forme et un nom. Ils repré-
sentent les futurs tenseurs qui traverseront le graphe dès qu’une valeur réelle sera
donnée au paramètre x et que le graphe sera exécuté. Grâce aux tenseurs symbo-
liques, il est possible de préciser à l’avance la connexion des opérations et TensorFlow
Explorer les définitions et les graphes de fonctions 593
peut inférer récursivement les types de données et les formes de tous les tenseurs à
partir des types de données et des formes de leurs entrées.
À présent, entrons un peu plus dans les détails et voyons comment accéder aux
dénitions de fonctions et aux graphes de fonctions, et comment explorer les opéra-
tions et les tenseurs d’un graphe.
Dans cet exemple, la première opération représente l’argument d’entrée x (il s’agit
d’un champ substituable, ou placeholder). La deuxième « opération », représente la
constante 3. La troisième opération correspond à l’élévation à la puissance (**). La
dernière représente la sortie de cette fonction (il s’agit d’une opération identité qui
se contente de copier la sortie de l’opération puissance347).
Chaque opération possède une liste de tenseurs d’entrée et de sortie auxquels vous
pouvez facilement accéder via les attributs inputs et outputs. Par exemple, obte-
nons la liste des entrées et les sorties de l’opération d’élévation à la puissance:
>>> pow_op = ops[2]
>>> list(pow_op.inputs)
[<tf.Tensor 'x:0' shape=() dtype=float32>,
<tf.Tensor 'pow/y:0' shape=() dtype=float32>]
>>> pow_op.outputs
[<tf.Tensor 'pow:0' shape=() dtype=float32>]
347. Vous pouvez l’ignorer car elle n’existe que pour des raisons techniques. Elle évite que les fonctions TF
ne divulguent des structures internes.
594 Annexe E. Graphes TensorFlow
Identité Opération
Tenseur
Puissance
Notez que chaque opération est nommée. Par défaut, il s’agit du nom de l’opéra-
tion (par exemple, "pow"), mais vous pouvez le dénir manuellement lors de l’appel
à l’opération (par exemple, tf.pow(x, 3, name="autre_nom")). Si le nom
existe déjà, TensorFlow ajoute automatiquement un sufxe unique (par exemple,
"pow_1", "pow_2", etc.). Chaque tenseur a également un nom unique, toujours
celui de l’opération qui produit ce tenseur plus :0 s’il s’agit de la première sortie de
l’opération, :1 s’il s’agit de la deuxième sortie, et ainsi de suite. Vous pouvez obtenir
une opération ou un tenseur à partir de leur nom grâce aux deux méthodes get_
operation_by_name() et get_tensor_by_name() du graphe:
>>> concrete_function.graph.get_operation_by_name('x')
<tf.Operation 'x' type=Placeholder>
>>> concrete_function.graph.get_tensor_by_name('Identity:0')
<tf.Tensor 'Identity:0' shape=() dtype=float32>
Puis, appelons-la :
>>> result = tf_cube(tf.constant(2.0))
x = Tensor("x:0", shape=(), dtype=float32)
>>> result
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>
Le résultat semble correct, mais examinez ce qui a été afché : x est un tenseur
symbolique ! Il possède une forme et un type de données, mais aucune valeur. Il a
également un nom ("x:0"). Cela vient du fait que la fonction print() n’étant
pas une opération TensorFlow, elle s’exécute uniquement lors du traçage de la fonc-
tion Python, ce qui se produit en mode graphe, avec des arguments remplacés par des
tenseurs symboliques (de même type et forme, mais sans valeur). Puisque la fonction
print() n’a pas été capturée dans le graphe, les appels suivants à tf_cube()
avec des tenseurs scalaires de type oat32 n’afchent rien:
>>> result = tf_cube(tf.constant(3.0))
>>> result = tf_cube(tf.constant(4.0))
fonction concrète différente pour chaque taille de lot, ni compter sur lui pour déter-
miner quand utiliser None. Dans ce cas, vous pouvez préciser la signature d’entrée
de la manière suivante :
@tf.function(input_signature=[tf.TensorSpec([None, 28, 28], tf.float32)])
def shrink(images):
return images[:, ::2, ::2] # enlever la moitié des lignes et colonnes
Cette fonction TF acceptera tout tenseur de type oat32 et de forme [*, 28, 28], et
réutilisera à chaque fois la même fonction concrète :
img_batch_1 = tf.random.uniform(shape=[100, 28, 28])
img_batch_2 = tf.random.uniform(shape=[50, 28, 28])
preprocessed_images = shrink(img_batch_1) # OK. Traçage de la fonction
preprocessed_images = shrink(img_batch_2) # OK. Même fonction concrète
En revanche, si vous tentez d’appeler cette fonction TF avec une valeur Python ou
un tenseur ayant un type de donnée ou une forme non pris en charge, une exception
est levée :
img_batch_3 = tf.random.uniform(shape=[2, 2, 2])
preprocessed_images = shrink(img_batch_3) # ValueError! Entrées incompatibles
Elle fonctionne parfaitement mais, lorsque nous examinons son graphe, nous
constatons l’absence totale de boucle. Elle contient uniquement dix opérations
d’addition !
>>> add_10(tf.constant(0))
<tf.Tensor: shape=(), dtype=int32, numpy=15>
>>> add_10.get_concrete_function(tf.constant(0)).graph.get_operations()
[<tf.Operation 'x' type=Placeholder>, [...],
<tf.Operation 'add' type=AddV2>, [...],
<tf.Operation 'add_1' type=AddV2>, [...],
<tf.Operation 'add_2' type=AddV2>, [...],
[...]
<tf.Operation 'add_9' type=AddV2>, [...],
<tf.Operation 'Identity' type=Identity>]
En réalité, ce résultat fait sens. Lorsque la fonction a été tracée, la boucle s’est
exécutée à dix reprises et l’opération x += 1 a été effectuée dix fois. Puisque que
la fonction était en mode graphe, elle a enregistré cette opération dix fois dans le
graphe. Vous pouvez considérer cette boucle for comme une boucle « statique »
déroulée lorsque le graphe est créé.
Gérer des variables et d’autres ressources dans des fonctions TF 597
@tf.function
def increment(counter, c=1):
return counter.assign_add(c)
@tf.function
def increment(c=1):
return counter.assign_add(c)
@tf.function
def increment(self, c=1):
return self.counter.assign_add(c)
Avec les variables TF, n’utilisez pas =, +=, -=, ni tout autre opérateur
d’affectation Python. À la place, vous devez invoquer les méthodes
assign(), assign_add() et assign_sub(). Toute tentative
d’utilisation d’un opérateur d’affectation Python conduit à la levée d’une
exception au moment de l’appel de la méthode.
Un bon exemple de cette approche orientée objet est, évidemment, Keras. Voyons
comment utiliser des fonctions TF avec Keras.
Si votre couche ou modèle personnalisé doit toujours être dynamique, vous pouvez
à la place invoquer le constructeur de la classe de base avec dynamic=True :
class MyDense(tf.keras.layers.Layer):
def __init__(self, units, **kwargs):
super().__init__(dynamic=True, **kwargs)
[...]
A AlphaGo 471
analyse d’opinion 343
A2C (advantage actor-critic) 473
analyse en composantes principales 397
A3C (asynchronous A2C) 472
anchor prior Voir préalable d’ancrage
abandon 148, 323, 407
API de lecture en continu 199
abandon alpha 151
API de sous-classement 89, 178
abandon de Monte Carlo 323
API fonctionnelle 84, 348
accuracy Voir exactitude
API séquentielle 73
acteur-critique 472
API tf.data 197-198
acteur-critique à avantages 473
application web progressive 501
acteur-critique asynchrone à avantages 472
apprentissage antagoniste 290
acteur-critique soft 473
apprentissage auto-supervisé 350, 376
AdaGrad 136
apprentissage de représentations 223
AdaIn Voir normalisation d’instance adap-
apprentissage hebbien 60
tative
apprentissage non supervisé 8
Adam 138
apprentissage ouvert 474
AdaMax 139
apprentissage par différence temporelle 459
AdamW 140
apprentissage par renforcement 439
affûtage 384
apprentissage Q 460
ajout d’une marge de zéros 238
par approximation 462
AlexNet 255 profond 463
algèbre linéaire accélérée 190 apprentissage résiduel 262
algorithme AllReduce 515 apprentissage sans exemples 385
algorithme génétique 443 apprentissage supervisé 7
algorithme hors politique 461 ARIMA Voir modèle autorégressif
AllReduce 515 etmoyenne mobile intégré
602 Deep Learning avec Keras et TensorFlow
ℓ 1 35, 37 poids 9
ℓ 2 34-35, 37 policy gradients Voir gradients de politique
ℓ k 35 politique 441, 447
notebook Jupyter 3 politique d’exploration 459
noyau de convolution Voir ltre pondération 37
noyau de pooling 247 pooling 247
NSP Voir prédiction de la phrase suivante porte d’entrée 325, 327
porte d’oubli 325, 327
O porte de sortie 325
observation d’entraînement 7 posterior Voir distribution a posteriori
OEL (open-ended learning) 474 PPO (proximal policy optimization) 473
off-policy algorithm 461 préalable d’ancrage 284
OOV bin Voir casier hors vocabulaire prédiction de la phrase suivante 377
optimisation avec inertie 134 préentraînement à partir d’une tâche secon-
optimisation avec inertie de Nesterov 135 daire 133
optimisation de politique proximale 473 préentraînement auto-supervisé 377
optimiseur 105, 134, 142 préentraînement glouton par couche 132
ordonnancement en gang 519 préentraînement non supervisé 131, 402
PReLU (parametric Leaky ReLU) 116
P prétraitement d’images 230
prétraitement de texte 227
PaLM Voir Pathways prétraitement des données 204
parallélisme des données 514 prior Voir distribution a priori
parallélisme du modèle 512 problème d’affectation de crédit 449
parallélisme en pipeline 518 problème de mémoire à court terme 324
partitionnement 8 processus de décision markovien 455, 459
pas 239 programme unique, données multiples 514
passe en arrière 64 protobuf 211
passe en avant 64 dans TensorFlow 212
Pathways 380, 519 Protocol Buffers 210, 594
PCA Voir analyse en composantes principales pseudo-inverse 15
percepteur 383 PWA Voir application web progressive
perceptron 57
perceptron multicouche 62
de classication 68 Q
de régression 66, 82 Q-Learning 460
entraînement 63 quantication post-entraînement 499
performance 7
personnalisation 168
R
perte de dispersion 409
perte de Huber 311 ragged tensor Voir tenseur irrégulier
perte de reconstruction 396 rappel 91, 209
perte logistique 44 rappel personnalisé 342
phase d’entraînement 9 ratio de mélange elastic net 40
phase d’inférence 10 RBM (restricted boltzmann machines) Voir
plongement 223-224, 338, 346 machine de Boltzmann restreinte
à partir de modèle linguistique 350 recherche d’architecture neuronale 269
plongement préentraîné 349 recherche en faisceau 359
608 Deep Learning avec Keras et TensorFlow