Travaux pratiques - Apprentissage auto-supervisé: RotNet

L’objectif de cette séance de travaux pratiques est de découvrir l’utilisation des tâches prétextes pour l’apprentissage auto-supervisé. Plus spécifiquement, nous allons entraîner un modèle sans annotation humaine, sur une tâche d’identification de l’angle de rotation. Nous appliquerons ensuite des fine-tuning pour spécialiser ce modèle avec peu de données annotées.

Apprentissage auto-supervisé et tâche prétexte

L’apprentissage auto-supervisé ou self-supervised learning (SSL) est une question importante en apprentissage automatique. Le SSL regroupe des approches prometteuses pour exploiter de grandes quantités d’information non annotée. La difficulté relative au manque d’annotation est typiquement un frein important pour les méthodes d’apprentissage supervisées que nous avons pu voir dans ce cours. Le SSL vise à encoder l’information de grands jeux de données dans l’espace latent d’un modèle de sorte à apprendre des représentations qui seront facilement adaptable par apprentissage par transfert à de nouvelles tâches.

Une famille de méthodes auto-supervisées consiste à résoudre des tâches prétextes permettant de capter les statistiques des données d’entrées. Ces tâches prétextes varient selon les modalités considérées, néanmoins elles consistent toujours à modéliser les données en conservant les informations que l’on pense être utiles pour les tâches futures. Ainsi, des tâches prétextes classiques sont la restauration de données corrompues ou cachées, la remise en ordre de séquences mélangées ou l’identification de transformations appliquées aux données. D’autres approches, que nous n’aborderons pas, utilisent des modèles génératifs afin d’apprendre la distribution des données peuvent ensuite servir de régularisateurs pour pallier le manque d’annotations.

Définition de la tâche prétexte

Dans cette séance TP, nous nous intéresserons à une tâche prétexte consistant à identifier l’angle de la rotation appliquées à une image. Cette tâche prétexte provient de l’article Unsupervised Representation Learning by Predicting Image Rotations https://arxiv.org/abs/1803.07728 (Gidaris et al., 2018). Le principe consiste à pré-entraîner un CNN sur une tâche de classification auxiliaire. Dans notre cas, les entrées sont des images auxquelles ont été appliquées des rotations d’un angle \(\theta \in \Theta\)\(\Theta\) est l’ensemble des rotations autorisée (typiquement, les multiples de 90°). Pour une image \(\mathbf x_i\), nous allons tirer au hasard \(\theta_i\) parmi \(\Theta\) de façon uniforme. Le modèle devra ensuite prédire quel angle \(\theta_i\) a été appliqué sur l’image. Ainsi, le modèle apprendra quelles sont les orientations « naturelles » des exemples, ce qui forcera les représentations apprises à encoder la géométrie des images.

Pour ce TP, nous utiliserons le jeu de données Fashion MNIST. Il est constitué de 70 000 images de dimensions \(28 \times 28\) pixels, représentant différents types d’articles de mode, tels que des hauts, des bas, des bottes, etc. 50 000 seront utilisées pour le pré-entraînement sur la tâche prétexte (40 000 en entraînement et 10 000 en validation). Parmi les images restantes, 10 000 seront utilisées pour entraîner le modèle sur une tâche finale de classification. Les 10 000 dernières images feront office d’ensemble de test. Nous pourrons alors comparer les performances du modèle entraîné sur 10 000 images de FashionMNIST avec et sans le pré-entraînement.

Chargement des données

# Import des bibliothèques utiles

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf
from tensorflow import keras

Commençons par charge le jeu de données. Visualisez quelques images.

data = keras.datasets.fashion_mnist

# Chargement des données depuis Keras
(X_train, y_train), (X_test, y_test) = data.load_data()

Nous allons ensuite normaliser les données (initialement les pixels ont des valeurs entières entre 0 et 255) puis le diviser en jeu d’entraînement, jeu de validation et jeu de test.

# Normalisation entre 0 et 1
X_train = X_train / 255
X_test = X_test / 255


# Séparation du jeu de test en jeu de validation et jeu de test final
X_val = X_test[:5000]
y_val = y_test[:5000]

X_test = X_test[5000:]
y_test = y_test[5000:]

# Création du jeu d'apprentissage pour la phase de pré-entraînement
X_unlabeled = X_train[10000:]

# Création du jeu d'apprentissage pour la phase de classification
n_labeled = 5000 # cette variable peut être changée pour mesurer l'impact du nombre d'exemples d'apprentissages
X_labeled = X_train[:n_labeled]
y_labeled = y_train[:n_labeled]

# Mise en forme des entrées au format tenseur
X_labeled = X_labeled.reshape(-1, 28, 28, 1)
X_val = X_val.reshape(-1, 28, 28, 1)
X_test = X_test.reshape(-1, 28, 28, 1)

Création des pseudo-étiquettes

Maintenant que nous avons créé un premier jeu de données non étiquetées, l’étape suivante consiste à créer des pseudo-étiquettes pour cet ensemble d’observations. Comme la tâche prétexte est la classification d’angles de rotation, nous allons faire pivoter l’ensemble des données de \(\theta \in \Theta = \{ 0°, 90°, 180°, 270°\}\) et leur attribuer les pseudo-étiquettes correspondantes.

Question

Compléter le code suivant pour la création du jeu d’apprentissage. Nous allons appliquer sur toutes les images une rotation de 90 degrés, 180 degrés et 270 degrés. La fonction np.rot90() peut vous servir (voir la documentation).

# X_train 0 contiendra les images sans rotation (0 degrés)
X_train_0 = X_unlabeled.copy()

# X_train_90 contiendra les images avec une rotation de 90 degrés
X_train_90 = ... # à compléter

# X_train_180 contiendra les images avec une rotation de 180 degrés
X_train_180 = ... # à compléter

# X_train_270 contiendra les images avec une rotation de 270 degrés
X_train_270 = ... # à compléter

# Assigner les pseudo-étiquettes (0 pour "pas de rotation", 1 pour 90 degrés,
# 2 pour 180 degrés et 3 pour 270 degrés)
y_train_0 = np.full((len(X_train_0)), 0)
y_train_90 = np.full((len(X_train_90)), 1)
y_train_180 = np.full((len(X_train_180)), 2)
y_train_270 = np.full((len(X_train_270)), 3)

# Concaténation de tous les tenseurs en un seul jeu de données
X_train_unlabeled_full = np.concatenate((X_train_0, X_train_90, X_train_180, X_train_270), axis=0)
y_train_unlabeled_full = np.concatenate((y_train_0, y_train_90, y_train_180, y_train_270), axis=0)

# Mélange aléatoirement les observations et les étiquettes de la même façon
p = np.random.permutation(len(X_train_unlabeled_full))
n = 20000 # limite le nombre d'exemples utilisés
X_train_unlabeled_full = X_train_unlabeled_full[p[:n]]
y_train_unlabeled_full = y_train_unlabeled_full[p[:n]]

L’ensemble des données d’entraînement est divisé en un ensemble de validation (X_rot_val, y_rot_val) et un ensemble d’entraînement (X_rot_train, y_rot_train) pour la tâche prétexte. Étant donné que nous entraînons le modèle sur la tâche prétexte sur laquelle le modèle ne sera pas évalué, nous n’utilisons pas d’ensemble de test.

# Création d'un ensemble d'apprentissage et de validation pour la tâche prétexte
nn = n // 2
X_rot_val, X_rot_train = X_train_unlabeled_full[:nn], X_train_unlabeled_full[nn:]
y_rot_val, y_rot_train = y_train_unlabeled_full[:nn], y_train_unlabeled_full[nn:]

# Redimensionne les observations
X_rot_val = X_rot_val.reshape(-1, 28, 28, 1)
X_rot_train = X_rot_train.reshape(-1, 28, 28, 1)

Entraînement auto-supervisé sur tâche prétexte

Maintenant que le jeu de données et les pseudo-étiquettes sont prêtes, nous pouvons créer le CNN et l’entraîner sur la tâche prétexte.

# Définition d'un réseau convolutif simple
model = keras.models.Sequential([
        keras.layers.Conv2D(64, 7, activation="relu", padding="same",
                            input_shape=[28, 28, 1]),
        keras.layers.MaxPooling2D(2),
        keras.layers.Conv2D(128, 3, activation="relu", padding="same"),
        keras.layers.Conv2D(128, 3, activation="relu", padding="same"),
        keras.layers.MaxPooling2D(2),
        keras.layers.Conv2D(256, 3, activation="relu", padding="same"),
        keras.layers.Conv2D(256, 3, activation="relu", padding="same"),
        keras.layers.MaxPooling2D(2),
        keras.layers.Flatten(),
        keras.layers.Dense(128, activation="relu",name='dense1'),
        keras.layers.Dropout(0.5),
        keras.layers.Dense(64, activation="relu",name='dense2'),
        keras.layers.Dropout(0.5),
        keras.layers.Dense(4, activation="softmax",name="dense3")
])

Question

Compiler et entraîner le modèle défini précédement sur la tâche prétexte.

Pour la compilation du modèle nous utilisons la loss sparse_categorical_cross_entropy. Les loss categorical_cross_entropy et sparse_categorical_cross_entropy correspondent à la même fonction et la seule différence réside dans le format dans lequel sont codés les labels. Si les labels sont encodés en “one-hot”, il convient d’utiliser la categorical_crossentropy. Mais si les labels sont des entiers, convient d’utiliser la loss sparse_categorical_crossentropy.

# TODO: compilation du modèle

# TODO: entraînement du modèle sur la tâche prétexte

Transfert sur une tâche de classification

À présent, nous souhaitons tirer parti des représentations apprises par notre modèle sur la tâche prétexte pour aborder une tâche de classification. Pour cela, nous allons garder les filtres appris et ré-entraîner seulement la tête de classification (fine-tuning).

Question

Retirer la dernière couche du modèle pour la remplasser par une projection linéaire adaptée au nombre de classes (ici 10).

Question

Geler les paramètres associées aux couches convolutives.

Question

Compiler le modèle en utilisant comme fonction de coût la sparse_categorical_crossentropy.

Question

Entraîner le modèle sur la tâche finale en utilisant un petit ensemble de données étiquetées (X_labeled, y_labeled) pendant un total de 10 epochs. À chaque epoch, évaluer les performances du modèle sur le jeu de validation (X_val, y_val).

Nous pouvons finalement évaluer le modèle appris en classification sur le jeu de test :

model.evaluate(X_test, y_test)

Vous devriez obtenir un score d”accuracy proche de 84% (ou 73% si vous ne prenez qu’un ensemble réduit à 20000 éléments en pré-apprentissage).

Question

Comparer les performances obtenues avec celles issues d’un modèle entrainé de manière complètement supervisée sur le jeu de données réduit X_labeled.

Conclure sur l’intérêt du pré-entraînement, en observant tout particulièrement les performances de validation après les premières epochs.

Question

(pour aller plus loin)

Refaire cette analyse en diminuant la taille du jeu d’apprentissage pour la classification supervisée. Que peut-on en dire ?