Introduction à l'apprentissage automatique - TP6 exercice 2

Classification d'images au XXIème siècle: CNN et transfer learning


Remarque préliminaire: il s'agit plus d'un tutoriel que d'un exercice. Vous n'avez pas de code à écrire, mais passez y suffisamment de temps et assurez-vous de bien comprendre.


Dans cet exercice, nous mettons en oeuvre des réseaux de neurones convolutifs. Nous allons utiliser la bibliothèque Tensorflow par l'intermédiaire de l'API Keras qui simplifie la manipulation des réseaux. Notez les nombreuses ressources pédagogiques sur la page web de Tensorflow, qui pourront vous être utiles en stage, pour un projet, etc.

Commencez par prendre connaissance des pages suivantes:


Installez Tensorflow et Keras si ce n'est déjà fait:


Notre exercice est partiellement une adaptation simplifiée de cette page du blog de François Chollet, papa de Keras. Lisez cette page à la fin du TP, notamment ce qui concerne la génération d'images synthétiques pour augmenter la base d'apprentissage, que nous n'abordons pas dans cet exercice par souci de simplification. Par ailleurs, le problème chiens et chats du blog ne concerne que deux classes alors que le nôtre est multiclasses (6 classes à identifier):

0. Préparatifs

On commence par charger quelques bibliothèques et définir la fonction display_test (comme dans l'exercice précédent).

On charge à présent les données. Cette fois, nos classifieurs vont admettre directement les images en entrée, et pas des descripteurs comme les histogrammes de l'exercice 1. Comme les réseaux de neurones utilisés exigent des images de taille identique en entrée, il va falloir redimensionner les images de la base de données avant de les stocker dans les tableaux X_train et X_test. On choisit une taille de redimensionnement de $150\times 100$ pixels (150 colonnes et 100 lignes), ce qui a l'avantage de ne pas trop déformer la majorité des images (l'"aspect ratio" est globalement conservé).


1. Classification par un réseau convolutif

Nous allons tester le premier réseau étudié dans le blog de F. Chollet, inspiré des réseaux convolutifs comme LeNet5 proposés par Yann Le Cun dans les années 1990.

Le réseau est formé des couches suivantes.

Partie "définition de descripteurs" ( features ):

Partie "classification", qui ressemble au perceptron multi-couches de l'exercice précédent:

Ce modèle nécessite l'apprentissage de 725 414 paramètres... Par exemple, la seconde couche de convolution nécessite l'estimation de 9248 paramètres. En effet, elle effectue 32 convolutions de taille $3 \times 3 \times \text{(épaisseur de la sortie de la couche précédente)}$, soit: $(9\times 32+1)\times 32 = 9248$ paramètres (la couche précédente effectue 32 convolutions, et +1 car il faut ajouter le terme de biais).

Remarquez que les couches convolutives "rognent" les bords (à cause du traitement des effets de bord lorsque le noyau de convolution "dépasse" des bords du domaine où il est appliqué). Cela explique pourquoi la première couche part d'images de taille $100\times 150$ et aboutit à un résultat de taille $98\times 148$.


L'apprentissage proprement dit se lance dans la cellule suivante. Notez la taille des batches utilisés ainsi que le nombre d'epochs. Les données de validation ne sont pas utilisées pour l'apprentissage, seulement à titre indicatif pour surveiller un surapprentissage potentiel, comme expliqué en cours.

Constatez que ce réseau convolutif présente de meilleures performance que le meilleur modèle de l'exercice 1 (si ce n'est pas le cas, vous n'avez pas eu de chance: relancez l'apprentissage...)

Vérifiez le rôle du dropout: commentez la ligne model.add(Dropout(0.5)) dans ce qui précède, et relancez la construction puis l'apprentissage du réseau. Vous allez voir le réseau apprendre "par coeur" la base d'apprentissage (accuracy=1) mais une performance moindre sur la base de test.

Remarque: il ne faut pas perdre de vue que notre base de données est assez petite: deux erreurs sur la base test font baisser le score de classification ( accuracy ) de 1%.


La cellule suivante permet de calculer la sortie du réseau (predict). Il s'agit des probabilités a posteriori de chaque classe, on assigne donc chaque observation à la classe de probabilité maximale, selon le principe MAP. Seuls quelques papillons semblent encore poser problème...


2. Génération aléatoire d'exemples adversariaux


La cellule suivante prend une image-test, et change aléatoirement la valeur (R,V,B) de pixels par un triplet dont les composantes varient entre $0$ et $M$, jusqu'à ce que l'image ne soit plus classée dans la bonne catégorie...

Quelques remarques...

L'objectif de cette expérience très simple est d'illustrer que la perception humaine n'a rien à voir avec la classification algorithmique.

Relancez plusieurs fois la cellule: vous voyez que la classe affectée à notre exemple adversarial change, et que quelques pixels suffisent à tromper le modèle. A quelques pixels près, un motard est reconnu comme un papillon...

En fait on triche un peu ici: on autorise des modifications des valeurs des pixels entre 0 et M=10, alors que les composantes RVB des "vraies" images varient entre 0 et 1.

Relancez la cellule avec M=3, il faut alors changer une petite centaine de pixels pour tromper le modèle.

Dans les exemples présentés en cours (issus d'articles de recherche récents), les perturbations adversariales ne sont pas générées aléatoirement, mais en fonction du modèle de classification, et de manière à atteindre une classe prédeterminée.


3. Apprentissage par transfert


L'apprentissage est limité par la taille de la base d'apprentissage: nous n'avons dans chaque catégorie moins de 200 images de taille $150\times100\times3$, pour apprendre plus de 700000 paramètres.

Une manière de surmonter cette difficulté est de réutiliser un réseau complexe dont les paramètres auront été appris sur une très grande base de données, par des chercheurs ou entreprises disposant de grandes ressources de calcul. On parle d'apprentissage par transfert.

Nous allons adapter le réseau VGG16, décrit sur cette page. VGG16 a été construit pour un problème de classification à 1000 classes, et ses paramètres ont été appris sur 14 millions d'images.

L'idée est d'utiliser uniquement la partie de VGG16 construisant des descripteurs (on ne modifie pas les paramètres de cette partie), puis d'utiliser ces descripteurs en entrée d'un classifieur permettant de discriminer 6 classes. Seuls les paramètres de la partie "classifieur" seront appris sur nos données. On peut aussi dire qu'on remplace la partie "features" du modèle CNN précédent par la partie "features" de VGG16 dont on ne modifie pas les poids. Remarquons au passage que nous pourrions utiliser un autre classifieur qu'un réseau de neurones (les curieux pourront aller voir la dernière section du carnet).

Bien entendu, tout cela a une chance raisonnable de fonctionner si les descripteurs fournis par VGG16 sont bien adaptés à nos images, donc si les images d'apprentissage de VGG16 ressemblent aux nôtres. Les classes que nous cherchons à identifier ne sont pas présentes dans VGG16 (je n'ai pas vérifié mais ce ne sont pas les mêmes images): ce n'est pas un problème puisque nous entraînerons la partie "classifieur" sur nos données.

Voir plus bas en cas de capacités de calcul, de bande passante, ou d'espace disque limitées.

On remarque que si on considère uniquement la partie convolutive du réseau, la dimension des images en entrée peut être arbitraire (d'où les None dans le tableau ci-dessus). En effet, les paramètres sont ici les coefficients des noyaux de convolution, qui peuvent s'appliquer sur les images indépendamment de leur taille. La seule contrainte est que les images en entrée doivent avoir trois canaux (les noyaux de convolutions de la première couche sont de taille $3 \times 3 \times 3$). Comme nos images en entrée ont toute la même définition, les features en sortie auront aussi tous la même dimension.

On peut donc calculer les caractéristiques ( features ) de nos images train et test à l'aide de modelVGG:

La base d'apprentissage est formée de 825 observations, celle de test de 207 observations. La sortie de la partie "convolutive" de VGG16 sort des blocs de 3x4x512 valeurs (512 convolutions dans la dernière couche convolutive, qui sortent chacunes $3\times 4$ valeurs).

Au cas où votre connexion (ou votre espace disque) ne vous permet pas de charger VGG16, ou si votre processeur n'est pas assez rapide, vous pouvez charger les features calculés pour vous, à ces liens ou sur Arche:

à lire (dans une cellule que vous créez) par:

features_train = np.load('features_train.npy')

features_test = np.load('features_test.npy')

Ensuite, on définit le réseau classifieur qui admettra en entrée les caractéristiques précédentes (de dimension 3x4x512=6144 ici) et calculera les probabilités a posteriori. On définit un réseau constitué d'une couche d'entrée où on "applatit" les features (ce sont donc des vecteurs de dimension 6144), d'une simple couche cachée de 256 neurones (notez le dropout), et de 6 neurones de sortie (autant que de catégories). N'hésitez pas à expérimenter avec d'autre valeurs que 256. C'est à cet endroit que l'on pourrait utiliser un autre classifieur qu'un réseau de neurones, comme une SVM: faites moi savoir si vous avez le temps d'essayer.

On entraîne à présent notre classifieur sur les features de VGG16. Notez que l'apprentissage est très rapide.

On visualise le résultat final:

On n'observe généralement pas d'erreur sur la base de test.


La génération aléatoire d'exemples adversariaux fonctionne toujours, comme on peut le constater à l'aide de la cellule suivante. Néanmoins, il faut généralement changer les valeurs d'un plus grand nombre de pixels que dans l'exemple précédent: les features générés par VGG16 s'avèrent plus robustes que ceux générés par notre réseau convolutif à quelques couches.


4. Et si on utilisait un autre classifieur qu'un MLP sur les descripteurs calculés par VGG16? (facultatif)


Dans notre expérience d'apprentissage par transfert, on utilise des descripteurs calculés par la partie "convolutive" de VGG16, puis on entraîne un perceptron multicouche (MLP) classifieur de manière à discriminer nos six classes. Comme nous l'avons suggéré plus haut, on pourrait envisager d'utiliser autre chose qu'un MLP. Nous allons tester ce que donnerait la SVM RBF.


En sortie de VGG16, les descripteurs sont des tableaux multidimensionnels de taille $3\times 4\times 512$, que l'on commence par transformer en des vecteurs de dimension 6144 dans la cellule suivante:

Ensuite on cherche la valeur optimale pour l'hyperparamètre $C$:

Enfin, on procède à l'apprentissage et on visualise le résultat:

On obtient également 100% de succès!

L'enseignement principal est que les performances de l'apprentissage profond (en particulier des CNN) vient du calcul "automatique" de descripteurs bien adaptés. En effet, on voit dans cette expérience particulière que si on remplace la partie classifieur par une SVM on obtient également d'excellents résultats.

Lorsqu'on entraîne "de zéro", il est toutefois plus pratique (et généralement plus efficace) de tout faire avec un réseau de neurones pour entraîner le modèle dans un cadre unifié basé sur la rétropropagation.