in Intelligence artificielle data science Machine Learning deep learning python computer vision vision par ordinateur ~ read.
Construire de puissants modèles de classification d'images en utilisant très peu de données

Construire de puissants modèles de classification d'images en utilisant très peu de données

A l'instar de mon dernier article créer un réseau de neurones en javascript en 30 lignes de code, je vous propose une nouvelle traduction.

Cette fois il s'agit d'un article écrit par François Chollet, auteur de Keras et fondateur de Wysp : Building powerful image classification models using very little data sur le blog de keras.io.

Ce petit exercice de traduction m'aide à progresser dans cette exploration du Deep Learning qui me passionne beaucoup ces derniers mois.
Cela ne fait absolument pas de moi un expert sur ce sujet, mais j'avance doucement dans cet univers fascinant.
Si vous voyez des points qu'il serait utile de voir développés, précisés ou ajustés - peut-être je n'utilise pas toujours le vocabulaire adéquat en français - n'hésitez pas à m'en faire part sur Twitter.

Mille merci à François Chollet, d'abord pour son article, mais également pour m'avoir autorisé à publier cette traduction.

Passé ce petit moment d'introduction, il est temps de rentrer dans les neurones.


Dans ce tutoriel, nous allons présenter quelques méthodes simples mais efficaces que vous pouvez utiliser pour construire une puissante application de classification d'images, en utilisant très peu d'exemples d'entraînement - seulement quelques centaines ou milliers d'images pour chaque classe à reconnaître.

Nous allons étudier les options suivantes :

  • entraîner un petit réseau de neurones from scratch (pourra servir de modèle de base)
  • utiliser les caractéristiques du goulot d'étranglement d'un réseau pré-entraîné
  • affiner les couches supérieures d'un réseau pré-entraîné

Cela nous amènera à couvrir les fonctionnalités de Keras suivantes :

  • fit_generator pour entraîner Keras à produire un modèle en utilisant des générateurs de données en Python
  • ImageDataGenerator pour augmenter la quantité de donnée en temps réel
  • le gel de couche et le réglage fin de modèle
  • ... et bien d'autres encore.

Note : le code des exemples a été mis à jour le 14 mars 2017 pour rester compatible de l'API 2.0 de Keras. Il vous faut une version de Keras 2.0.0 ou plus récente pour l'exécuter.

Notre configuration : seulement 2000 exemples d'entraînement (1000 par classe)

Nous allons débuter avec la configuration suivante :

  • une machine avec Keras, SciPy et PIL installés. Si vous avez un GPU NVIDIA compatible (et cuDNN installé), c'est très bien, toutefois comme nous travaillons avec peu d'images ce n'est pas absolument nécessaire.
  • un répertoire comprenant les données d'entraînement, un autre celles de validation, chacun comprenant lui-même des sous-répertoires par classe d'image lesquels contiennent les images .png ou .jpg.
data/  
    train/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001.jpg
            cat002.jpg
            ...
    validation/
        dogs/
            dog001.jpg
            dog002.jpg
            ...
        cats/
            cat001.jpg
            cat002.jpg
            ...

Pour récupérer quelques centaines ou milliers d'images d'entraînement dans les classes qui vous intéressent, un bon moyen de procéder et de vous appuyer sur l'API Flickr. Celle-ci vous permet de télécharger des images correspondant à des tags donnés, et diffusées sous des licences en permettant la réutilisation.

Dans nos exemples nous allons utiliser deux sets d'images. Nous les avons récupérer sur Kaggle : 1000 chats et 1000 chiens (bien que le dataset originel comprenne 12,500 chats et 12,500 chiens, nous avons simplement pris les 1000 premières images sur chaque classe). Nous utilisons également 400 échantillons aditionnels pour chaque classe comme données de validation, pour évaluer nos modèles.

Cela fait vraiment peu de données exemples desquelles il faut apprendre, pour un problème de classification loin d'être si simple. C'est donc un beau défi de machine learning, mais un défi réalisable : dans beaucoup de cas d'usages réels, même sur des collections de données de petite échelle, cela peut être extrêmement coûteux voire parfois presque impossible (ex en imagerie médicale). Etre capable de tirer un maximum d'un très petit jeu de données est une compétence clé chez un bon data scientist.

Images exemples des chiens et chats

A quel point ce problème est-il difficile ? Lorsque Kaggle a débuté la compétition chats vs chiens (avec 25 000 images d'entraînement au total), il y a de cela à peine deux ans (~2014), celle-ci était présentée comme suit :
"Dans un sondage informel réalisé il y a de cela quelques années, les experts en vision par odinateur postulaient qu'un outils de classification avec un taux de précision supérieur à 60% serait difficile à réaliser sans avancée majeure dans l'état de l'art. Pour référence, un outils de classement à 60% efficace améliore la probabilité de prédiction sur un HIP 12-images de 1/4096 à 1/459. La littérature actuelle suggère pourtant qu'un score supérieur à 80% dans cette tâche est à la portée des machines [ref]."

Dans cette compétition de 2014, les têtes d'affiche ont produit des scores supérieurs à 98% en utilisant des techniques modernes de deep learning. Dans notre cas, étant donné que nous allons nous restreindre à seulement 8% du dataset, le problème va être plus difficile encore.

De la pertinence du deep learning sur des problèmes à petits volumes de données

J'entends souvent dire que "le deep learning n'est adapté que dans les cas où vous disposez de grands volumes de données". Bien que ce ne soit pas complètement faux, c'est quand même bien trompeur.
Assurément, le deep learning requiert d'apprendre automatiquement des caractéristiques provenant des données, ce qui n'est généralement possible que lorsque beaucoup de données d'entraînement sont disponibles --et c'est tout particulièrement vrai dans des problèmes dans lesquels les échantillons en entrée présentent de très grandes dimensions comme c'est le cas pour des images. Cependant, les réseaux de neurones convolutionnels - un pilier parmi les algorithmes en deep learning - sont par conception l'un des meilleurs modèles pour les problèmes "perceptifs" (comme la classification d'images), même lorsqu'on dispose de peu de données d'apprentissage. Entraîner un convnet from scratch sur un petit dataset d'images donnera toujours des résultats raisonnables, sans que cela nécessite une personnalisation avancée de ses réglages. Les convnets sont juste bons par défaut. Ils constituent l'outils idéal pour faire le boulot attendu.

Mais ce qu'il y a de plus à savoir, c'est que les modèles de deep learning sont par nature hautement réutilisables : vous pouvez, disons, reprendre une application de classification d'images ou un modèle de conversion de voix en texte pré-entraînés sur de grands volumes de données, et les réutiliser ensuite sur un problème significativement différent moyennant de mineures adaptations, comme nous allons le voir dans cet article. C'est tout particulièrement le cas en vision par ordinateur, de nombreux modèles pré-entraînés (généralement sur le dataset Imagenet) sont disponibles en téléchargement et peuvent être utilisés pour bâtir de nouveaux modèles de vision très efficaces adaptés avec de petits jeux de données.

Pré-traitements et augmentation des données

De sorte à tirer le meilleur de notre modeste jeu de données pour l'entraînement, nous allons "l'augmenter" au travers d'un certain nombre de transformations aléatoires, ceci afin que notre modèle ne voit jamais deux fois exactement la même image. Cela nous évitera de faire du surapprentissage et cela aidera le modèle à mieux généraliser.

Dans Keras cela peut être réalisé via la classe keras.preprocessing.image.ImageDataGenerator. Cette classe vous permet de :

  • définir des transformations aléatoires et des opérations de normalisation réalisées sur les données image pendant l'entraînement
  • instancier des générateurs de batchs d'images augmentées (et leurs labels) via .flow(data, labels) ou .flow_from_directory(directory). Ces générateurs peuvent être utilisés avec des méthodes de modèles Keras acceptant des générateurs en données d'entrées comme inputs, fit_generator, evaluate_generator et predict_generator.

Jettons un oeil à l'exemple suivant :

from keras.preprocessing.image import ImageDataGenerator

datagen = ImageDataGenerator(  
        rotation_range=40,
        width_shift_range=0.2,
        height_shift_range=0.2,
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True,
        fill_mode='nearest')

Dans celui-ci nous n'utilisons que quelques unes des options disponibles (pour découvrir toutes celles possibles, consultez la documentation). Regardons un peu ce que nous avons écris :

  • rotation_range est une valeur en degrés (0-180), une plage dans laquelle opérer une rotation aléatoire des images
  • width_shift et height_shift sont des plages de modifications (représentant une fraction de la largeur ou la hauteur totale) dans lesquelles faire translater les images verticalement ou horizontalement
  • rescale est une valeur par laquelle nous allons multiplier les données avant de réaliser tout autre traitement. Nos images d'origine sont constituées de coefficients RGB entre 0 et 255, mais de telles valeurs seraient trop élevées pour être traitées dans nos modèles (considérant un temps d'apprentissage typique), nous ciblons donc en lieu et place des valeurs comprises entre 0 et 1 en appliquant un changement d'échelle suivant le facteur 1/255.
  • shear_range applique aléatoirement des transformations de cisaillement
  • zoom_range zoome de façon aléatoire dans les images
  • horizontal_flip retourne horizontalement de façon aléatoire la moitié des images --pertinent lorsqu'il n'y a pas de présomption d'asymétrie horizontale dans le sujet à reconnaître (par exemple dans des images de purs paysages).
  • fill_mode est la stratégie de remplissage utilisée pour les pixels nouvelement créés, lesquels peuvent apparaître après une rotation ou un glissement en largeur/hauteur.

Il est temps de générer quelques images en utilisant cet outils et de les sauvegarder dans un répertoire temporaire, histoire de visualiser un peu ce que cette stratégie d'augmentation produit --nous désactivons le rescaling dans ce cas afin que l'on puisse encore y voir quelque chose :

from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img

datagen = ImageDataGenerator(  
        rotation_range=40,
        width_shift_range=0.2,
        height_shift_range=0.2,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True,
        fill_mode='nearest')

img = load_img('data/train/cats/cat.0.jpg')  # this is a PIL image  
x = img_to_array(img)  # this is a Numpy array with shape (3, 150, 150)  
x = x.reshape((1,) + x.shape)  # this is a Numpy array with shape (1, 3, 150, 150)

# the .flow() command below generates batches of randomly transformed images
# and saves the results to the `preview/` directory
i = 0  
for batch in datagen.flow(x, batch_size=1,  
                          save_to_dir='preview', save_prefix='cat', save_format='jpeg'):
    i += 1
    if i > 20:
        break  # otherwise the generator would loop indefinitely

Et voici ce que nous obtenons --c'est à cela que ressemble le résultat de notre stratégie d'augmentation.
Images des deux chats avec les transformations

Entraîner un petit convnet from scratch : un score de 80% en 40 lignes de code

Le bon outils pour un travail de classement d'images c'est un convnet. Voyons donc pour en entraîner un avec nos données, comme base de départ. Comme nous n'avons que quelques exemples, notre première préoccupation doit être le surapprentissage. Surapprendre c'est ce qui arrive lorsqu'un modèle exposé à trop peu d'exemples intègre des schémas d'apprentissage dont la généralisation ne s'applique pas vraiment à de nouvelles données, comprendre par là que le modèle commence à utiliser des caractéristiques sans rapport avec le sujet pour réaliser de nouvelles prédictions. Prenons un exemple. Si vous-même comme humain jouiez ce rôle, en voyant seulement trois images de bûcherons, et trois images de marins, et parmis toutes ces images seulement un bûcheron porte une cape, vous pouvez commencer à penser que porter une cape est un signe de reconnaissance d'un bûcheron contrairement à un marin. Vous feriez alors un bien piètre agent de classement bûcheron/marin.

L'augmentation de données est une manière de combattre le surapprentissage, mais ça n'est toutefois pas suffisant étant donné que nos échantillons nouveaux restent très fortement liés aux images d'origine. Votre principale préoccupation pour combattre le surapprentissage doit être la capacité entropique de votre modèle --c'est la quantité d'information que votre modèle peut ingérer. Un modèle qui peut mémoriser une très grande quantité d'information présente un plus grand potentiel de précision en s'appuyant sur de nombreuses caractéristiques, mais il présente aussi le risque de commencer à intégrer des caractéristiques hors sujet. Dans le même temps, un modèle plus limité ne pouvant mémoriser que quelques caractéristiques devra se concentrer sur les plus significatives trouvées dans les données, et celles-ci ont plus de chance d'être appropriées et conduire à de meilleures généralisations.

Il existe différentes manières de moduler la capacité entropique. La principale est le choix du nombre de paramètres dans votre modèle, c'est-à-dire le nombre de couches et la taille de chaque couche. Une autre façon de faire c'est d'avoir recourt à la régularisation des poids, telles que les régularisations L1 ou L2, elles consistent à forcer les poids du modèle à prendre des valeurs plus petites.

Dans notre cas nous allons utiliser un très petit convnet avec quelques couches et quelques filtres par couche, en plus de l'augmentation et l'abandon de données. L'abandon de données aide également à réduire le surapprentissage, en empêchant une couche de voir deux fois exactement le même pattern, ceci en agissant d'une manière similaire à l'augmentation des données (vous pourriez dire qu'ensemble l'abandon et l'augmentation des données tend à disrupter les corrélations aléatoires présentes dans vos données).

L'extrait de code ci-dessous est notre premier modèle, une simple pile de 3 couches de convolution avec une activation ReLU et suivie
par des couches max-pooling. C'est vraiment très proche des architectures que préconisait Yann LeCun dans les années 90s pour le classement d'images (à l'exception près du ReLU).

Le code complet pour cette expérience se trouve ici.

from keras.models import Sequential  
from keras.layers import Conv2D, MaxPooling2D  
from keras.layers import Activation, Dropout, Flatten, Dense

model = Sequential()  
model.add(Conv2D(32, (3, 3), input_shape=(3, 150, 150)))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(32, (3, 3)))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, (3, 3)))  
model.add(Activation('relu'))  
model.add(MaxPooling2D(pool_size=(2, 2)))

# the model so far outputs 3D feature maps (height, width, features)

Par dessus celui-ci nous ajoutons deux couches entièrement connectées. Et nous terminons ce modèle avec un simple neurone et une sigmoïde d'activation, laquelle est parfaite pour un classement binaire. Pour l'accompagner nous allons aussi utiliser le binary_crossentropy loss pour entraîner notre modèle.

model.add(Flatten())  # this converts our 3D feature maps to 1D feature vectors  
model.add(Dense(64))  
model.add(Activation('relu'))  
model.add(Dropout(0.5))  
model.add(Dense(1))  
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',  
              optimizer='rmsprop',
              metrics=['accuracy'])

Le moment est venu de préparer nos données. Nous allons utiliser .flow_from_directory() pour générer les batchs de données images (et leurs labels) directement à partir de nos jpgs dans leurs répertoires respectifs.

batch_size = 16

# this is the augmentation configuration we will use for training
train_datagen = ImageDataGenerator(  
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1./255)

# this is a generator that will read pictures found in
# subfolers of 'data/train', and indefinitely generate
# batches of augmented image data
train_generator = train_datagen.flow_from_directory(  
        'data/train',  # this is the target directory
        target_size=(150, 150),  # all images will be resized to 150x150
        batch_size=batch_size,
        class_mode='binary')  # since we use binary_crossentropy loss, we need binary labels

# this is a similar generator, for validation data
validation_generator = test_datagen.flow_from_directory(  
        'data/validation',
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode='binary')

Nous pouvons utiliser ces générateurs pour entraîner notre modèle. Chaque période de calcul (epoch) prend 20 à 30s sur GPU et 300 à 400s sur CPU. Il est donc définitivement valable de lancer le calcul d'un tel modèle sur CPU, à condition de ne pas être pressé.

model.fit_generator(  
        train_generator,
        steps_per_epoch=2000 // batch_size,
        epochs=50,
        validation_data=validation_generator,
        validation_steps=800 // batch_size)
model.save_weights('first_try.h5')  # always save your weights after training or during training  

Cette approche nous donne une précision de validation entre 0,79 et 0,81 après 50 périodes de calcul (ce nombre a été pris arbitrairement --parce que le modèle est petit et utilise un abandon agressif, il ne semble pas faire de surapprentissage jusque là). Donc, à l'époque où cette compétition Kaggle a été lancée, nous serions déjà au niveau de "l'état de l'art" --en utilisant seulement 8% des données, sans effort pour optimiser notre architecture et sans hyperparamètrages. En fait, dans cette compétition Kaggle, ce modèle nous aurait positionné dans le top 100 (sur les 215 compétiteurs). Je pense donc qu'au moins 115 des participants n'utilisaient pas le deep learning ;)

Au passage notez que la variance de la précision de validation est plutôt élevée, à la fois parce que cette précision est elle-même un métrique à haut niveau de variance, mais aussi parce que nous utilisons seulement 800 échantillons de validation. Une bonne stratégie de validation dans un tel cas serait de faire une cross-validation k-fold, mais cela nécessiterait d'entraîner des modèles k pour chaque tour d'évaluation.

Utiliser les caractéristques du goulot d'étranglement d'un réseau pré-entraîné : 90% de précision en une minute

Une approche plus raffinée consiste à s'appuyer sur un réseau pré-entraîné à partir d'un grand dataset. Un tel réseau aura ainsi déjà appris bon nombre de caractéristiques utiles pour la plupart des problèmes de vision par ordinateur. En s'appuyant ainsi sur ces caractéristiques, cela nous permet d'atteindre un bien meilleur niveau de précision que par n'importe quelle autre méthode reposant uniquement sur les données disponiles.

Nous allons utiliser l'architecture VGG16, pré-entraînée sur le dataset Imagenet --un modèle déjà évoqué sur le blog de Keras.io. Parce que le dataset ImageNet contient quelques classes de "chats" (chat persan, chat siamois...) ainsi que quelques classes de "chiens" sur un total d'un millier de classes, ce modèle aura déjà appris quelles caractéristiques sont utiles dans un problème de classement d'images. En fait, il est possible que de simplement enregistrer les prédictions softmax du modèle sur nos données en lieu et place des caractéristiques au goulot d'étranglement soit suffisant pour très bien résoudre notre classement chiens vs chats. Cependant, la méthode que nous présentons içi se généralise très bien à une grande variété de problèmes, y compris pour des problèmes reposant sur des classes absentes d'ImageNet.

Voici à quoi ressemble l'architecture VGG16 :
Image architecture VGG16

Notre stratégie est la suivante : nous allons instancier seulement la partie convolutionnelle du modèle, tout ce que cela comprend jusqu'aux couches entièrement connectées. Nous allons alors faire fonctionner une fois ce modèle sur nos données de training et de validation, en enregistrant la sortie (les "caractéristiques de goulot d'étranglement" du modèle VGG16 : la dernière activité d'activation précédent les couches entièrement connectées) dans deux tableaux numpy. Ensuite nous allons entraîner un petit modèle entièrement connecté par dessus les caractéristiques enregistrées.

La raison pour laquelle nous stockons les caractéristiques en dehors plutôt que d'ajouter notre modèle avec toutes ses connexions par dessus une base convolutionnelle gelée et faire fonctionner l'ensemble, c'est pour l'efficacité des calculs. Faire fonctionner un VGG16 est coûteux en ressource, tout particulièrement si vous travaillez sur CPU, et nous voulons le faire une fois seulement. Notez au passage que cela nous économise le recours à l'augmentation des données.

Vous pouvez trouver la totalité du code de cette expérience ici. Vous pouvez récupérer le fichier des poids sur GitHub. Nous n'allons pas revoir comment le modèle est construit et chargé --c'est déjà expliqué dans de multiples exemples Keras. Mais jetons plutôt un oeil sur commment nous enregistrons les caractéristiques au goulot d'étranglement en utilisant les générateurs de données images :

batch_size = 16

generator = datagen.flow_from_directory(  
        'data/train',
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None,  # this means our generator will only yield batches of data, no labels
        shuffle=False)  # our data will be in order, so all first 1000 images will be cats, then 1000 dogs
# the predict_generator method returns the output of a model, given
# a generator that yields batches of numpy data
bottleneck_features_train = model.predict_generator(generator, 2000)  
# save the output as a Numpy array
np.save(open('bottleneck_features_train.npy', 'w'), bottleneck_features_train)

generator = datagen.flow_from_directory(  
        'data/validation',
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode=None,
        shuffle=False)
bottleneck_features_validation = model.predict_generator(generator, 800)  
np.save(open('bottleneck_features_validation.npy', 'w'), bottleneck_features_validation)  

Nous pouvons alors chargé nos données sauvergardées et entraîner un petit modèle entièrement connecté :

train_data = np.load(open('bottleneck_features_train.npy'))  
# the features were saved in order, so recreating the labels is easy
train_labels = np.array([0] * 1000 + [1] * 1000)

validation_data = np.load(open('bottleneck_features_validation.npy'))  
validation_labels = np.array([0] * 400 + [1] * 400)

model = Sequential()  
model.add(Flatten(input_shape=train_data.shape[1:]))  
model.add(Dense(256, activation='relu'))  
model.add(Dropout(0.5))  
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',  
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(train_data, train_labels,  
          epochs=50,
          batch_size=batch_size,
          validation_data=(validation_data, validation_labels))
model.save_weights('bottleneck_fc_model.h5')  

Grace à sa petite taille, ce modèle est entraîné très rapidement même sur CPU (1s par période de calcul) :

Train on 2000 samples, validate on 800 samples  
Epoch 1/50  
2000/2000 [==============================] - 1s - loss: 0.8932 - acc: 0.7345 - val_loss: 0.2664 - val_acc: 0.8862  
Epoch 2/50  
2000/2000 [==============================] - 1s - loss: 0.3556 - acc: 0.8460 - val_loss: 0.4704 - val_acc: 0.7725  
...
Epoch 47/50  
2000/2000 [==============================] - 1s - loss: 0.0063 - acc: 0.9990 - val_loss: 0.8230 - val_acc: 0.9125  
Epoch 48/50  
2000/2000 [==============================] - 1s - loss: 0.0144 - acc: 0.9960 - val_loss: 0.8204 - val_acc: 0.9075  
Epoch 49/50  
2000/2000 [==============================] - 1s - loss: 0.0102 - acc: 0.9960 - val_loss: 0.8334 - val_acc: 0.9038  
Epoch 50/50  
2000/2000 [==============================] - 1s - loss: 0.0040 - acc: 0.9985 - val_loss: 0.8556 - val_acc: 0.9075  

Nous atteignons une précision de validation de 0,90 - 0,91 : pas si mal. C'est quand même partiellement du au fait que le modèle de base était entraîné avec un dataset comprenant déjà des chiens et des chats (parmi les milliers d'autres classes).

Affiner les couches supérieures d'un réseau pré-entraîné

Pour améliorer encore nos résultats précédents, nous pouvons essayer "d'affiner" le dernier bloc de convolution sur le modèle VGG16 juste avant le classificateur de haut niveau. Cet affinage consiste à partir d'un réseau entraîné, et de le ré-entraîner sur un nouveau dataset avec de très petites mises à jour sur les poids. Dans notre cas, ce peut être réalisé en trois étapes :

  • on instancie la base de convolution issue du VGG16 et on charge ses poids
  • on ajoute par dessus notre modèle complètement connecté défini précédemment, et on charge ses poids
  • on gèle les couches du modèle VGG16 jusqu'au dernier bloc de convolution

Diagramme du modèle VGG modifié

Notez que :

  • pour réaliser cet affinage, toutes les couches doivent être initiées avec des poids proprement entraînés : en outre vous ne devriez pas claquer un réseau complètement connecté initié aléatoirement par dessus une base convolutionnelle pré-entraînée. Simplement parce que les gradients importants de mises à jour déclenchés par les poids initiés aléatoirement vont démolir les poids issus de l'entraînement dans la base convolutionnelle. Dans notre cas c'est la raison pour laquelle nous entraînons d'abord le classificateur de niveau supérieur, et ensuite seulement nous voyons pour affiner les poids convolutionnels qui le précèdent.
  • nous avons choisi de n'affiner que le dernier bloc convolutionnel plutôt que le réseau en entier de sorte à éviter du surapprentissage, étant donné que le réseau entier présente une très grande capacité entropique et donc une grande tendance à surapprendre. Les caractéristiques apprises par les blocs convolutionnels de bas niveau sont plus générales, moins abstraites que celles que l'on trouve plus haut, par conséquent il est préférable de garder les quelques premiers blocs figés (caractéristiques plus générales) et de seulement ajuster le dernier (caractéristques plus spécialisées).
  • cet ajustement fin doit être réalisé avec une vitesse d'apprentissage très lente, et typiquement avec l'optimiseur SGD plutôt qu'avec un optimiseur du rythme d'apprentissage adaptatif tel que RMSProp. Cela permet de s'assurer que l'amplitude des changements reste très petite, pour ne pas démolir les caractéristiques précédemment apprises.

Vous pouvez trouver tout le code de cette expérience ici.

Après avoir instancié la base VGG et chargé ses poids, nous ajoutons notre classificateur complètement connecté précédemment entraîné par dessus :

# build a classifier model to put on top of the convolutional model
top_model = Sequential()  
top_model.add(Flatten(input_shape=model.output_shape[1:]))  
top_model.add(Dense(256, activation='relu'))  
top_model.add(Dropout(0.5))  
top_model.add(Dense(1, activation='sigmoid'))

# note that it is necessary to start with a fully-trained
# classifier, including the top classifier,
# in order to successfully do fine-tuning
top_model.load_weights(top_model_weights_path)

# add the model on top of the convolutional base
model.add(top_model)  

Nous figeons alors toutes les couches convolutionnelles jusqu'avant la dernière :

# set the first 25 layers (up to the last conv block)
# to non-trainable (weights will not be updated)
for layer in model.layers[:25]:  
    layer.trainable = False

# compile the model with a SGD/momentum optimizer
# and a very slow learning rate.
model.compile(loss='binary_crossentropy',  
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

Pour finir, nous lançons l'entraînement du tout, avec une vitesse d'apprentissage très lente :

batch_size = 16

# prepare data augmentation configuration
train_datagen = ImageDataGenerator(  
        rescale=1./255,
        shear_range=0.2,
        zoom_range=0.2,
        horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(  
        train_data_dir,
        target_size=(img_height, img_width),
        batch_size=batch_size,
        class_mode='binary')

validation_generator = test_datagen.flow_from_directory(  
        validation_data_dir,
        target_size=(img_height, img_width),
        batch_size=batch_size,
        class_mode='binary')

# fine-tune the model
model.fit_generator(  
        train_generator,
        steps_per_epoch=nb_train_samples // batch_size,
        epochs=epochs,
        validation_data=validation_generator,
        validation_steps=nb_validation_samples // batch_size)

Cette approche nous amène à une précision de validation de 0,94 après 50 périodes de calcul. C'est un succès total !

Quelques orientations pour passer la barre des 0,95 :

  • user d'une augmentation de données plus agressive
  • un abandon plus agressif encore
  • utiliser une régularisation L1 et L2 (également connue comme "affaiblissement de poids")
  • affiner un bloc convolutionnel supplémentaire (en parallèle d'une plus grande régularisation)

C'est la fin de cet article ! Pour récapituler, ci-dessous les liens vers les extraits de code pour les trois expérimentations :

Si vous souhaitez faire des commentaires sur cet article ou si vous avez des suggestions pour un nouveau sujet à traiter, vous pouvez contacter l'auteur original de cet article sur Twitter.

comments powered by Disqus