TP 3 - Transfer Learning et Fine-Tuning

Version Google Collab du TP (GPU) : https://colab.research.google.com/drive/1pUG5-qVdKBvL3e14cws-9TuhTOZR2FWc?usp=sharing

Version classique du TP (CPU)

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 à la base PASCAL VOC2007 http://host.robots.ox.ac.uk/pascal/VOC/voc2007/, qui est une base contenant:

  • 20 classes (parmi les macro-catégories Person, Animal, Vehicle, Indoor), mais avec des étiquettes « multi-labels ». Par exemple, une image peut contenir à la fois une personne et un cheval.
  • L’ensemble d’apprentissage (train+val) contient environ 5000 images, l’ensemble de test contient également environ 5000 images.
  • Les images sont de tailles variables mais autour de 500x300.

Avec des volumes de données tels que ceux de PASCAL VOC, il est impossible d’entraîner des réseaux de neurones avec autant de paramètres que ceux utilisés pour ImageNet sans être confrontés au problème du sur-apprentissage. Nous allons étudier des solutions d’apprentissage par transfert pour surmonter ce problème.

Il faut au préalabale avoir téléchargé les données :

Et décompresser le tout dans un unique dossier.

Exercice 1 : Modèle ResNet-50 avec Keras

Nous allons récupérer une architecture de réseau convolutif donnant des très bonnes performances sur ImageNet. On va ici s’intéresser aux réseaux ResNet [HZRS16], qui ont remporté le challenge ILSVRC en 2015. On utilisera un réseau ResNet-50 dont l’architecture détaillée peut être trouvé ici : https://github.com/fchollet/deep-learning-models/blob/master/resnet50.py.

Avec Keras, ce réseau est accessible avec le code suivant :

from keras.applications.resnet50 import ResNet50
model = ResNet50(include_top=True, weights='imagenet')

Où les poids issus de l’entraînement sur ImageNet sont directement récupérés (weights='imagenet').

Exercice 2 : 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 comme extracteur de descripteurs. Nous allons appliquer le réseau ResNet50 et extraire la couche d’activations du réseau avant les 1000 classes d’ImageNet, couche de taille 2048. Ainsi, l’application du réseau sur chaque image de la base produit un vecteur de taille 2048, appelé « Deep Feature ». On peut pour cela utiliser le code suivant :

model.layers.pop()

L’utilisation de la fonction pop() permet de supprimer la dernière couche des (1000) classes d’ImageNet. La classe ResNet50 du module keras.applications.resnet50 charge un modèle de l’API fonctionnelle de Keras : https://keras.io/getting-started/functional-api-guide/. Pour que le dépilement de la dernière couche ait un effet sur le modèle, il faut le préciser explicitement :

model = Model(input=model.input,output=model.layers[-1].output)

On pourra ensuite vérifier l’architecture du modèle, et le compiler (pour l’extraction futures des Deep Features):

model.summary()
model.compile(loss='binary_crossentropy', optimizer=SGD(lr, momentum=0.9), metrics=['binary_accuracy'])

Chargement des données de la base. Stocker en mémoire l’ensemble des données devient impossible pour des bases de données massives. On arrive à la limite sur VOC 2007 ou le tenseur d’entrée prend plusieurs Go de mémoire. Au lieu de charger l’intégralité des données on va s’appuyer sur une fonction génératrice, i.e. capable de générer à la volée un batch d’exemples sur lequel calculer une étape forward pour l’extraction des « Deep Features ».

Sur la base VOC 2007, on utilisera le générateur PascalVOCDataGenerator fourni : http://cedric.cnam.fr/~thomen/cours/US330X/data_gen.py. On instanciera le générateur sur la base d’apprentissage de la manière suivante :

from data_gen import PascalVOCDataGenerator
data_dir = '/data/VOCdevkit/VOC2007/' # A changer avec votre chemin
data_generator_train = PascalVOCDataGenerator('trainval', data_dir)

Le générateur contient un dictionnaire dont les clés correspondent aux identifiants des images de la base (e.g. 000012), et les clés sont le vecteur codé au format « one-hot » (par exemple [0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0] va indiquer que l’image contient les classes bicycle et person). Un générateur va être crée par l’appel de la méthode flow :

generator = data_generator_train.flow(batch_size=batch_size)

L’appel de next() sur le générateur va permettre d’exécuter le code à l’intérieur de la bouche while de flow, i.e. :

  • Charger en mémoire le batch d’images et de labels suivant
  • Retailler chaque image en une taille précisée (224x224)
  • Appliquer un pré-traitement à l’image (soustraction de l’image moyenne d’ImageNet)

Ainsi, on va pouvoir utiliser ce générateur afin d’extraire séquentiellement les données des PASCAL VOC 2007, et d’extraire les « Deep Features » :

batch_size = 32
generator = data_generator_train.flow(batch_size=batch_size)
# Initilisation des matrices contenant les Deep Features et les labels
X_train = # TODO...
Y_train = # TODO...


# Calcul du nombre e batchs
nb_batches = int(len(data_generator_train.images_ids_in_subset) / batch_size) + 1

for i in range(nb_batches):
    # Pour chaque batch, on extrait les images d'entrée X et les labels y
    X, y = next(generator)
    # On récupère les Deep Feature
    # TODO...

Le temps d’extraction en CPU pour être long (plus d’une minute par batch de 32 images). L’utilisation du GPU accélère considérablement le calcul. On calculera de manière identique la matrice des Deep Features sur la base de test. Finalement, on sauvegardera les Deep Features et labels de la manière suivante :

outfile = 'DF_ResNet50_VOC2007'
np.savez(outfile, X_train=X_train, Y_train=Y_train,X_test=X_test, Y_test=Y_test)

Exercice 3 : Transfert sur VOC 2007

On commencera par charger les données calculées à l’exercice précédent (téléchargeables directement ici : http://cedric.cnam.fr/~thomen/cours/US330X/DF_ResNet50_VOC2007.npz) :

outfile = 'DF_ResNet50_VOC2007.npz'
npzfile = np.load(outfile)
X_train = npzfile['X_train']
Y_train = npzfile['Y_train']
X_test = npzfile['X_test']
Y_test = npzfile['Y_test']
print "X_train=",X_train.shape, "Y_train=",Y_train.shape, " X_test=",X_test.shape, "Y_train=",Y_test.shape

On va maintenant considérer les Deep Features comme les données d’entrée et définir un réseau de neurones complètement connectés sans couche cachée pour prédire les labels de sortie :

  • Créer un modèle d’une seule couche permettant la classification des features
  • On prendra une fonction d’activation de type sigmoïde
  • Le compiler (learning rate = 0.1, batch size 32)
  • L’entrainer

On va pouvoir évaluer le modèle classiquement :

scores = model.evaluate(X_test, Y_test, verbose=0)
print("%s TEST: %.2f%%" % (model.metrics_names[0], scores[0]*100))
print("%s TEST: %.2f%%" % (model.metrics_names[1], scores[1]*100))

Évaluation finale de performances. La métrique utilisée pour évaluer les performances sur PASCAL VOC est la Précision Moyenne (Average Precision). On pourra la calculer ainsi :

from sklearn.metrics import average_precision_score

y_pred_test = model.predict(X_test)
y_pred_train = model.predict(X_train)
AP_train = np.zeros(20)
AP_test = np.zeros(20)
for c in range(20):
    #TODO...

print "MAP TRAIN =", AP_train.mean()*100
print "MAP TEST =", AP_test.mean()*100

Question :

Quel MAP obtenez-vous sur la base de test ?

Exercice 4 : Fine-tuning sur VOC 2007

Enfin, on va tester un apprentissage où les paramètres du réseaux seront initialisées sur la base ImageNet, mais fine-tunées sur VOC2007 pour spécialiser les représentations internes à la bas cible et améliorer les performances. N.B. : il faut impérativement utiliser une carte GPU dans cette partie.

On commencera pour cela à charger le réseau et ajouter la couche de classification dédiée à VOC2007 :

# Load ResNet50 architecture & its weights
model = ResNet50(include_top=True, weights='imagenet')
model.layers.pop()
# Modify top layers
# TODO...

On spécifiera ensuite qu’on souhaite apprendre les paramètres de l’ensemble du réseau :

for i in range(nlayers):
  model.layers[i].trainable = True

Question :

Si on avait indiqué model.layers[i].trainable = False pour toutes les couches sauf la dernière, dans quel mode serions nous ?

On va ensuite compiler le modèle :

lr = 0.1
model.compile(loss='binary_crossentropy', optimizer=SGD(lr=lr), metrics=['binary_accuracy'])

Et simplement utiliser la méthode fit_generator pour entraîner le modèle

batch_size=32
nb_epochs=10
data_generator_train = PascalVOCDataGenerator('trainval', data_dir)
steps_per_epoch_train = int(len(data_generator_train.id_to_label) / batch_size) + 1
model.fit_generator(data_generator_train.flow(batch_size=batch_size),
                    steps_per_epoch=steps_per_epoch_train,
                    epochs=nb_epochs,
                    verbose=1)
  • fit_generator va prendre a paramètre la fonction flow qui va renvoyer un batch d’exemple et de labels. La méthode forward va comparer la sortie prédite par le modèle aux labels données par la supervision puis l’étape backward va mettre à jour l’ensemble des paramètres du modèle.

Question :

Evaluer le modèle sur la base de test

Quel MAP obtenez-vous sur la base de test dans ce régime de fine-tuning ? Conclure.

[HZRS16]Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. Deep residual learning for image recognition. In CVPR. 2016.