Travaux pratiques - Transfer learning et fine-tuning¶
Fonction d’activation ReLU¶
Pour approfondir la séance précédente, si vous avez le temps.
Dans un premier temps, nous pouvons comparer la fonction d’activation sigmoide utilisée dans les TP précédents et la fonction d’activation ReLU (Rectifed Linear Unit).
Question
Reprendre le perceptron à une couche cachée du TP précédent en utilisant une non-linéarité ReLU. Observer le nombre d’itérations nécessaires pour atteindre la convergence du modèle.
Reprendre le réseau convolutif du TP précédent en utilisant des non-linéarités ReLU pour les couches convolutives. Observer le nombre d’itérations nécessaires pour atteindre la convergence du modèle.
Conclure sur l’intérêt de la fonction d’activation ReLU.
Transfer Learning et fine-tuning sur EuroSAT¶
Pour aller plus loin, nous allons nous intéresser aux propriétés de « transfert » des réseaux convolutifs profonds pré-entraînés sur des bases large échelle comme ImageNet. Nous allons nous intéresser au jeu de données EuroSAT, qui est une base d’images contenant :
10 classes (bâtiments industriels, bâtiments résidentiels, cultures saisonnières, cultures permanentes, rivières, lacs & mers, végétation herbacée, routes/autoroutes, pâturages et forêts).
L’ensemble d’apprentissage contient environ 27000 images. Il n’y a pas d’ensemble de test prédéfini, nous allons donc la diviser en deux.
Les images sont de taille \(64\times64\). Les images que nous allons utiliser sont en couleur (RGB) mais il existe une version multispectrale utilisant 13 longueurs d’onde différentes.
Ce jeu de données est géré nativement par TensorFlow via son module
tensorflow-datasets
(tfds).
%pip install tensorflow-datasets
Nous pouvons commencer par visualiser quelques images de ce jeu de données. Si c’est la première fois que vous exécutez cette cellule de code, le téléchargement du jeu de données EuroSAT peut prendre quelques minutes (≈80Mo).
import matplotlib.pyplot as plt
import tensorflow_datasets as tfds
eurosat = tfds.load('eurosat', split='train').take(10)
fig = plt.figure(figsize=(18, 6))
for idx, data in enumerate(eurosat):
fig.add_subplot(1, 10, idx+1)
plt.imshow(data['image'])
plt.axis("off")
plt.title(data['filename'].numpy().decode("utf-8").split("_")[0])
plt.show()
Avec des « petits » volumes de données (de l’ordre de quelques milliers d’images), il est impossible d’entraîner des réseaux de neurones avec autant de paramètres que ceux utilisés pour ImageNet sans devoir se confronter au problème du sur-apprentissage. Toutefois, une solution généralement assez efficace consiste à appliquer de l’apprentissage par transfert. C’est une approche commune et peu coûteuse, qui permet d’exploiter des modèles « sur étagère » sans les ré-entraîner en les utilisant comme extracteur de caractéristiques.
Chargement du modèle ResNet-50 avec Keras
¶
Pour cette séance, nous allons nous appuyer sur une architecture de réseau convolutif qui obtient des performances plus que satisfaisantes sur ImageNet : les réseaux de la famille des ResNet. La compétition ILSVRC en 2015 (classification d’images naturelles sur les 1000 classes de ImageNet) a été remportée par un modèle type ResNet.
Nous allons repartir d’un modèle ResNet-50 dont l’architecture détaillée
peut être trouvé dans le code source de
Keras.
Comme il s’agit d’un modèle assez répandu, il est directement implémenté
dans Keras et il n’est pas nécessaire de le redéfinir. Ainsi, en passant
par le sous-module keras.applications
:
from tensorflow.keras.applications.resnet50 import ResNet50
model = ResNet50(include_top=True, weights='imagenet')
Question
À partir de la documentation de
Tensorflow/Keras,
déterminer l’utilité des paramètres include_top
et weights
.
model.summary()
Extraction de deep features¶
Une première solution pour surmonter le manque d’exemples d’apprentissage est de se servir des réseaux pré-entraînés sur ImageNet pour extraire des descripteurs d’image. En effet, la représentation apprise par les dernières couches du réseau contiennent normalement l’information utile à leur classification. Cette connaissance devrait pouvoir se transférer sur un autre jeu de données.
Nous allons ainsi appliquer le réseau ResNet50 et extraire, pour chaque image de notre nouveau jeu de donnée, les activations de l’avant-dernière couche, c’est-à-dire celle se trouvant juste avant la couche linéaire qui réalise la classification dans les 1000 classes d’ImageNet.
Question
D’après l’architecture du modèle affichée par
model.summary()
, quelle sera la taille du vecteur de descripteurs ?
L’application du réseau sur chaque image de la base produira donc un
vecteur de taille \(d\), que l’on appelle aussi deep feature («
caractéristique profonde »). Le modèle que nous allons utiliser est donc
notre ResNet50 mais pour lequel nous supprimons la dernière couche. En
utilisant l’interface fonctionnelle de
Keras, cela
correspond à créer un nouveau modèle dont la sortie est la sortie de
l’avant-dernière couche du ResNet50, c’est-à-dire la sortie de
model.layers[-2]
(puisque model.layers[-1]
est la dernière
couche).
Autrement dit, notre nouveau modèle s’obtient de la façon suivante :
from keras.models import Model
model = Model(inputs=model.input, outputs=model.layers[-2].output)
Question
Afficher l’architecture de ce modèle et vérifier la dimension du descripteur en sortie. Compiler ce modèle ensuite ce modèle. On peut utiliser n’importe quelle fonction de coût et optimiseur car ils ne seront pas utilisés (on n’utilisera le modèle qu’en inférence pour cette partie).
Ensuite, nous pouvons créer les deux jeux de données d’entraînement et d’évaluation de EuroSAT. Comme il n’y a pas de split fourni avec le jeu de données, nous allons en créer un. Dans notre cas, nous allons utiliser la première moitié comme entraînement et la seconde moitié comme test.
Comme précédemment, nous utilisons tfds
(tensorflow_datasets
)
pour définir un jeu de données au format TensorFlow, qui renvoie les
couples (image, étiquette) sous forme de tenseurs :
batch_size = 32
train_dataset = tfds.load('eurosat', split='train[:50%]',
batch_size=batch_size,
shuffle_files=True).take(20)
test_dataset = tfds.load('eurosat', split='train[50%:]',
batch_size=batch_size,
shuffle_files=True).take(20)
n_train_data = len(train_dataset) * batch_size
n_test_data = len(test_dataset) * batch_size
Remarquons que nous ne conservons que les 20 premiers batches de chaque jeu de données (en apprentissage et en évaluation), ce qui correspond donc à \(20 \times 32 = 640\) examples chacun. Le jeu de données EuroSAT contient bien plus d’images mais cet échantillon sera suffisant pour notre séance de TP. Par ailleurs, cela va nous permettre de mesurer l’intérêt du transfer learning lorsque l’on n’a accès qu’à peu de données.
Question
Compléter le code ci-dessous et itérer sur l’objet
train_dataset
pour extraire séquentiellement les deep features sur
le jeu de données. En itérant, le jeu de données renvoie un dictionnaire
d
contenant deux entrées : d[image]
qui contient le batch
d’images et d[label]
qui contient le batch des étiquettes de
classe.
Comme les images du jeu de données n’ont pas la même taille que les
images utilisées pour ImageNet, on prendra soin de redimensionner les
images à l’aide de la fonction tf.image.resize
de TensorFlow (cf. la
documentation).
import numpy as np
from tqdm.auto import tqdm
d_size = 2048
X_train = np.zeros((n_train_data, d_size), dtype="float32")
y_train = np.zeros(n_train_data, dtype="uint8")
for idx, batch in enumerate(tqdm(train_dataset)):
# TODO: compléter ce code
...
Question
Faire cette même opération sur test_dataset
.
Le temps d’extraction des caractéristiques peut être relativement long sur CPU (plusieurs minutes pour traiter l’intégralité du jeu de données par batch de 32 images). L’utilisation d’un GPU accélère considérablement le calcul. Pour éviter de recalculer les deep features, nous allons les sauvegarder à l’aide de NumPy :
output_file = 'deepfeatures_resnet50_eurosat'
np.savez(output_file, X_train=X_train, y_train=y_train,
X_test=X_test, y_test=y_test)
Transfert de ImageNet vers EuroSAT¶
Si vous n’avez pas réussi la partie précédente, ou si jamais le temps de calcul était trop long car vous n’avez pas d’accès à un GPU, vous pouvez télécharger les deep features précalculées à l’adresse suivante : http://cedric.cnam.fr/vertigo/Cours/RCP209/docs/deepfeatures_resnet50_eurosat.npz
Puis vous pouvez les charger en mémoire :
!wget -nc http://cedric.cnam.fr/vertigo/Cours/RCP209/docs/deepfeatures_resnet50_eurosat.npz
features = np.load("deepfeatures_resnet50_eurosat.npz")
X_train = features['X_train']
y_train = features['y_train']
X_test = features['X_test']
y_test = features['y_test']
Dans tous les cas, nous allons définir notre nouveau jeu de données qui prend comme observations les deep features calculées précédemment, et comme étiquettes celles du jeu de données EuroSAT.
Dans cette partie, nous allons considérer les deep features comme les données d’entrée.
Question
En utilisant scikit-learn
, réaliser une visualisation t-SNE des
deep features ainsi obtenues. On réalisera une projection dans
un espace de dimension 2, puis on affichera les features projetées
en les colorant en fonction de leur classe.
Qu’observe-t-on? Que peut-on dire des représentations intermédiaires des images ainsi obtenues, par rapport aux images de départ?
Question
À l’aide de scikit-learn
toujours, entraîner une SVM linéaire
simple qui va prédire les étiquettes de classes à partir des deep
features. On apprendra la SVM sur le jeu de test (X_train, y_train)
.
Évaluer le taux de bonne classification (accuracy) de ce classifieur
SVM sur le jeu de test (X_test, y_test)
à l’aide de la méthode
.score()
.
from sklearn.svm import SVC
# TODO: compléter
...
Question
pour approfondir, si vous avez le temps
Complémenter le code ci-dessous et implémenter un réseau simple (percepton à une seule couche) avec Keras. Entraîner ce modèle et évaluer ses performances en classification sur EuroSAT à partir des descripteurs extraits ci-dessus. Que constatez-vous ?
from keras.utils import to_categorical
y_train_one_hot = to_categorical(y_train)
y_test_one_hot = to_categorical(y_test)
# TODO: compléter en entraînant un modèle de perceptron à une couche
Fine-tuning¶
Pour terminer ce TP, nous allons non plus simplement utiliser le modèle ResNet-50 préentraîné comme extracteur de caractéristiques « sur étagère », mais nous allons le spécialiser sur le jeu de données EuroSAT. Plus précisément, nous allons entraîner un ResNet-50 dont les paramètres seront initialisées à partir des poids appris sur la base ImageNet, puis nous allons entraîner pendant quelques itérations ce modèle sur EuroSAT pour spécialiser ses représentations internes sur la nouvelle base de données. Cela devrait notamment améliorer encore plus ses performances.
Note
L’exécution des apprentissages sur une carte graphique (GPU) est fortement recommandée pour cette partie.
Commençons par charger le modèle depuis Keras en l’initialisant à partir des poids appris sur ImageNet :
from tensorflow.keras.applications.resnet50 import ResNet50
# Chargement du modèle ResNet50 préentraîné sur ImageNet
model = ResNet50(include_top=True, weights='imagenet')
Question
Retirer la dernière couche du modèle (la couche entièrement connectée qui renvoie les prédictions finales) et la remplacer par une nouvelle couche de taille adaptée. On remarquera que le modèle pré-entraîné renvoie 1000 probabilités (une pour chaque classe de ImageNet), tandis que le modèle que l’on souhaite fine-tune doit avoir 10 classes en sortie (10 classe dans EuroSAT).
Nous allons désormais spécifier que toutes les couches du réseau peuvent être apprises, et pas seulement la dernière que l’on vient d’ajouter :
for layer in model.layers:
layer.trainable = True
Question
Si nous avions spécifié model.layers[i].trainable = False
pour
toutes les couches sauf la dernière, dans quel mode d’apprentissage
serions-nous ?
Nous pouvons finalement compiler le modèle comme d’habitude :
from tensorflow.keras.optimizers import SGD
model.compile(loss='categorical_crossentropy', optimizer=SGD(learning_rate=0.01, momentum=0.9), metrics=['accuracy'])
Comme tout le jeu de données EuroSAT risque de ne pas rentrer en
mémoire, il est préférable de charger les données à la volée en
utilisant un générateur. La classe tensorflow.Dataset
gère
automatiquement cet aspect pour nous (ainsi que le multiprocessing
pour charger les données en parallèle des calculs, de façon asynchrone).
train_dataset = tfds.load('eurosat', split='train[:50%]',
batch_size=batch_size,
shuffle_files=True,
as_supervised=True).take(20)
test_dataset = tfds.load('eurosat', split='train[50%:]',
batch_size=batch_size,
shuffle_files=True,
as_supervised=True).take(20)
train_ds = train_dataset.map(lambda image, label: (tf.image.resize(image, (224, 224)),
tf.one_hot(label, 10)))
Question
Que fait la méthode map
dans le code ci-dessus ?
Finalement, on peut démarrer le fine-tuning en lançant l’apprentissage
du modèle à l’aide de la méthode fit()
.
model.fit(train_ds, epochs=10)
Question
Évaluer les performances finales du modèle sur le jeu de test de EuroSAT. Commenter par rapport à l’approche précédente de transfer learning.