Classifier des images de chats et de chiens avec une précision de 92%, sans transfer learning
Dans cet article, nous allons construire un réseau de neurones convolutionnel pour classer des photos de chats et de chiens, avec une précision de 92%
Nous n'utiliserons pas le transfer learning cette fois-ci (donc on ne triche pas!), et j'expliquerai en détail le chemin que j'ai suivi pour arriver à la solution de cet exercice classique.
Vous allez apprendre comment :
Et en bonus, vous essaierez le réseau pré-entraîné ResNet50, juste pour voir ce que ça donne.
Dans ce post, contrairement à la plupart de ceux que l'on peut trouver sur ce blog, je ne fournis pas de recette pour exécuter ce notebook sur Google Colab. J'ai essayé, mais il semble que:
En conséquence, vous allez devoir tourner sur votre propre machine.
D'abord, installez TensorFlow pour votre PC Linux ou Windows . En suivant ces recettes, vous installerez aussi Anaconda, avec le package keras.
Ensuite, installez les packages dont nous aurons besoin avec anaconda:
conda install numpy matplotlib
Clonez mon repo github localement, et démarrez le serveur jupyter notebook:
git clone https://github.com/cbernet/maldives.git
cd maldives/dogs_vs_cats
jupyter notebook
Enfin, ouvrez le notebook
dogs_vs_cats_local_fr.ipynb
.
Je n'ai pas testé cette recette. Si elle ne marche pas, dîtes-le moi dans les commentaires, et je vous aiderai immédiatement.
Ce dataset a originellement été introduit pour une compétition Kaggle en 2013. Pour y accéder, vous devrez vous créer un compte Kaggle , et vous logguer sur ce compte. Pas de pression, on n'est pas là pour la compétition, mais pour apprendre!
Le dataset est disponible ici . Vous pouvez utiliser l'utilitaire Kaggle pour le récupérer, ou simplement télécharger le fichier train.zip (540 Mo). N'oubliez pas de vous logguer d'abord.
Les instructions ci-dessous pour préparer le dataset sont pour Linux ou macOS. Si vous travaillez sous Windows, je suis sûr que vous pourrez trouver un moyen de faire de même (par exemple, vous pouvez utiliser 7-zip pour décompresser l'archive, et l'explorateur Windows pour créer des répertoires et bouger les fichiers).
Une fois le téléchargement terminé, décompressez l'archive :
unzip train.zip
Listez le contenu du répertoire
train
:
ls train
Vous allez y trouver un grand nombre d'images de chiens et de chats.
Dans les sections suivantes, nous utiliserons Keras pour lire les images depuis le disque, avec la méthode flow_from_directory ) de la classe ImageDataGenerator .
Pour cela, il faut que les images des deux catégories soient dans des répertoires différents. Nous allons donc mettre toutes les images de chiens dans
dogs
, et toutes les images de chats dans
cats
:
mkdir cats
mkdir dogs
find train -name 'dog.*' -exec mv {} dogs/ \;
find train -name 'cat.*' -exec mv {} cats/ \;
Vous vous demandez peut-être pourquoi j'ai utilisé
find
au lieu de
mv
pour déplacer ces fichiers. C'est dû au fait qu'avec
mv
, le shell doit passer un grand nombre d'arguments à la commande (tous les noms de fichier), et qu'il y a une limitation sur ce nombre sous macOS (avec Linux, tout va bien). Avec
find
, nous pouvons contourner cette limitation.
Maintenant, entrez dans la cellule ci-dessous le chemin vers le répertoire du dataset, celui qui contient les sous-répertoires
dogs
et
cats
. Puis exécutez cette cellule.
# définition du répertoire du dataset,
# et déplacement dans ce répertoire
datasetdir = '/data2/cbernet/maldives/dogs_vs_cats'
import os
os.chdir(datasetdir)
# import des packages nécessaires
import matplotlib.pyplot as plt
import matplotlib.image as img
from tensorflow import keras
# raccourci vers la classe ImageDataGenerator
ImageDataGenerator = keras.preprocessing.image.ImageDataGenerator
Commençons par afficher la premier image de chaque catégorie :
plt.subplot(1,2,1)
plt.imshow(img.imread('cats/cat.0.jpg'))
plt.subplot(1,2,2)
plt.imshow(img.imread('dogs/dog.0.jpg'))
Ils sont bien mignons, mais allons plus loin et voyons quelle est la taille de nos images :
images = []
for i in range(10):
im = img.imread('cats/cat.{}.jpg'.format(i))
images.append(im)
print('image shape', im.shape, 'maximum color level', im.max())
Dans la forme (shape) de l'image, les deux premières colonnes correspondent à la hauteur et la largeur de l'image en nombre de pixels, et la troisième aux trois canaux de couleur. Donc chaque pixel contient trois valeurs, pour rouge, vert, et bleu (RGB). Nous avons aussi imprimé le niveau de couleur maximum pour l'ensemble des troix canaux, et nous pouvons conclure que les niveaux RGB sont codés sur l'intervalle 0-255.
S'il y a une chose dont il faudrait se rappeler à la fin de ce tuto, la voici:
Ne faites jamais confiance à vos données
Les données sont toujours sales et bruitées.
Pour contrôler ce dataset, j'ai utilisé un outil permettant d'afficher un grand nombre d'images rapidement. En fait, je me suis contenté de l'application Aperçu de mac pour regarder tous les icônes d'aperçu dans les deux répertoires du dataset. Le cerveau peut rapidement identifier des problèmes évidents, même si l'on regarde globalement un grand nombre d'images à la fois. Ainsi, ce travail ne m'a pas pris plus de 20 minutes. Bien sûr, j'ai certainement manqué un certain nombre de problèmes moins évidents.
Quoiqu'il en soit, voici ce que j'ai trouvé.
D'abord, voici les indices des mauvaises images pour chaque catégorie :
bad_dog_ids = [5604, 6413, 8736, 8898, 9188, 9517, 10161,
10190, 10237, 10401, 10797, 11186]
bad_cat_ids = [2939, 3216, 4688, 4833, 5418, 6215, 7377,
8456, 8470, 11565, 12272]
Nous pouvons alors récupérer les images avec ces indices depuis les répertoires
cats
et
dogs
:
def load_images(ids, categ):
'''retourne les images correspondant à une liste d'indices,
pour une catégorie donnée (cat ou dog)
'''
images = []
dirname = categ+'s' # dog -> dogs
for theid in ids:
fname = '{dirname}/{categ}.{theid}.jpg'.format(
dirname=dirname,
categ=categ,
theid=theid
)
im = img.imread(fname)
images.append(im)
return images
bad_dogs = load_images(bad_dog_ids, 'dog')
bad_cats = load_images(bad_cat_ids, 'cat')
def plot_images(images, ids):
ncols, nrows = 4, 3
fig = plt.figure( figsize=(ncols*3, nrows*3), dpi=90)
for i, (img, theid) in enumerate(zip(images,ids)):
plt.subplot(nrows, ncols, i+1)
plt.imshow(img)
plt.title(str(theid))
plt.axis('off')
plot_images(bad_dogs, bad_dog_ids)
Certaines de ces images sont complètement inutiles, comme 5604 et 8736. Pour 10401 et 10797, nous voyons en fait un chat, alors que ces images sont supposées être des images de chiens! Garder les dessins de chien peut se discuter, mais mon impression est qu'il vaut mieux s'en débarasser. De même, nous pourrions garder 6413, mais je pense que le network se focalisera plus sur le dessin encadrant la photo que sur celle-ci.
Maintenant, regardons les mauvais chats :
plot_images(bad_cats, bad_cat_ids)
Encore une fois, je ne suis pas trop pour garder les dessins de chats pour l'entraînement du réseau. Mais qui sait, cela pourrait ne pas avoir d'importance... il faudrait tester ça. Dans l'image 4688, nous avons une image de chien et une image de chat. Elle n'est donc pas discriminante et doit être rejetée. Dans l'image 6215, nous voyons juste de la fourrure, qui pourrait appartenir à un chat ou à un chien, même si on dirait bien du poil de chat. Et pourquoi ce type dans l'image 7377?
Il faut noter que même si nous rejetons les dessins de chat pour l'entraînement, le réseau pourrait quand même réussir à les identifier correctement. Nous en parlerons à la fin du tuto.
Maintenant, implémentons une petite fonction pour nettoyer le dataset:
import glob
import re
import shutil
# ce pattern correspond à n'importe quelle chaîne de
# caractères contenant ".<chiffres>.",
# comme dog.666.jpg
pattern = re.compile(r'.*\.(\d+)\..*')
def trash_path(dirname):
'''retourne le chemin vers le répertoire poubelle
(Trash/cats/ ou Trash/dogs/),
ou les images de mauvais chiens et chats seront déplacées.
Notez que ce répertoire ne doit pas être dans cats/ ou dogs/,
ou Keras sera quand même capable de les trouver.
'''
return os.path.join('../Trash', dirname)
def cleanup(ids, dirname):
'''déplace dans la poubelle les images de dirname contenant ces indices
'''
os.chdir(datasetdir)
# garde la trace du répertoire courant
oldpwd = os.getcwd()
# on va soit dans cats/ soit dans dogs/
os.chdir(dirname)
# on crée le répertoire poubelle.
# s'il existe, on le supprime et on le recrée.
trash = trash_path(dirname)
if os.path.isdir(trash):
shutil.rmtree(trash)
os.makedirs(trash, exist_ok=True)
# boucle sur toutes les images de chiens ou de chats
fnames = os.listdir()
for fname in fnames:
m = pattern.match(fname)
if m:
# extraction de l'indice
the_id = int(m.group(1))
if the_id in ids:
# cet indice correspond effectivement à une image
# qu'il faut virer
print('moving to {}: {}'.format(trash, fname))
shutil.move(fname, trash)
# on retourne au répertoire du dataset
os.chdir(oldpwd)
def restore(dirname):
'''Restaure les fichiers de la poubelle.
J'aurai besoin de cette fonction pour ramener ce tutorial à son
état initial pour vous. Et vous pourriez en avoir besoin si vous voulez
tester le réseau sans avoir effectué le toilettage auparavant.
'''
os.chdir(datasetdir)
oldpwd = os.getcwd()
os.chdir(dirname)
trash = trash_path(dirname)
print(trash)
for fname in os.listdir(trash):
fname = os.path.join(trash,fname)
print('restoring', fname)
print(os.getcwd())
shutil.move(fname, os.getcwd())
os.chdir(oldpwd)
cleanup(bad_cat_ids,'cats')
cleanup(bad_dog_ids, 'dogs')
Si vous voulez restaurer votre dataset, décommentez les lignes suivantes et exécutez la cellule :
# restore('dogs')
# restore('cats')
Pour entraîner un réseau de neurones, on lui présente des paquets ( batchs ) d'images, où chaque image est dotée d'une étiquette identifiant la véritable nature de l'image (soit chien soit chat dans notre cas). Un batch peut contenir entre une dizaine et plusieurs centaines d'images. Pour une introduction aux réseaux de neurones et à l'apprentissage supervisé pour le classement, vous pouvez regarder mon article sur la Reconnaissance de Chiffres Manuscrits avec scikit-learn .
À chaque image du batch, la prédiction du réseau est comparée à l'étiquette, et la distance entre la prédiction du réseau et la vérité est évaluée pour l'ensemble du batch. Ensuite, les paramètres du réseau sont modifiés de façon à minimiser cette distance, de façon à améliorer la capacité de prédiction du réseau. Ensuite, l'entraînement continue, batch après batch.
Il nous faut donc un moyen de transformer nos images, pour l'instant des fichiers sur le disque, en batchs de tableaux de données en mémoire, qui pourront être fournies au réseau durant l'entraînement.
La classe ImageDataGenerator est justement faite pour ça. Importons cette classe et créons une instance (un objet) de cette classe.
gen = ImageDataGenerator()
Maintenant, nous allons utiliser la méthode
flow_from_directory
de l'objet
gen
pour produire des batchs.
Cette méthode retourne un itérateur qui fournit un batch lorsqu'on le parcourt. Pour savoir comment les données du batch sont organisées, nous pouvons simplement créer l'itérateur, et récupérer un premier batch pour l'examiner :
iterator = gen.flow_from_directory(
os.getcwd(),
target_size=(256,256),
classes=('dogs','cats')
)
# tous les itérateurs python ont une fonction next()
batch = iterator.next()
len(batch)
Le batch a deux éléments. Quel est leur type?
print(type(batch[0]))
print(type(batch[1]))
Deux tableaux numpy! parfait. On peut imprimer leur shape, le type des données stockées, et la valeur maximum de ces données :
print(batch[0].shape)
print(batch[0].dtype)
print(batch[0].max())
print(batch[1].shape)
print(batch[1].dtype)
On voit que le premier élément est un tableau de 32 images avec 256x256 pixels et 3 couleurs, encodées comme floats entre 0 et 255. Ainsi, l'ImageDataGenerator a bien forcé les images en 256x256 pixels, mais n'a pas normalisé les niveaux de couleur entre 0 et 1. Nous devrons faire cela plus tard.
Le deuxième élément contient les 32 étiquettes correspondantes.
Avant de regarder les étiquettes en détail, nous pouvons afficher la première image :
import numpy as np
# il faut caster l'image vers un tableau d'entier
# avant de la tracer car imshow prend
# soit un tableau d'entiers
# soit un tableau de réels entre 0. et 1.
plt.imshow(batch[0][0].astype(np.int))
Et voici l'étiquette correspondante :
batch[1][0]
Nous voyons que l' ImageDataGenerator produit automatiquement l'étiquette de chaque image suivant le répertoire ou il l'a trouvée. La technique d'encodage one-hot est utilisée pour les étiquettes, et c'est exactement ce dont nous avons besoin pour cette tâche de classement. Pour en savoir plus sur cette technique, vous pouvez vous référer à mon article Premier Réseau de Neurones avec Keras .
On peut aussi deviner que l'étiquette
[0., 1.]
correspond à un vrai chat, et
[1., 0.]
à un vrai chien. La prédiction du réseau pour une image donnée sera quelque part entre les deux, par exemple
[0.6, 0.4]
pour un yorkshire.
Comme c'est peut-être la première fois que vous utilisez l'ImageDataGenerator, vous voulez sans doute vérifier que cet outil fonctionne correctement. Pour cela, nous allons développer une petite fonction dans la section suivante pour valider le dataset.
Pour vérifier que les étiquettes sont correctes, nous allons voir si, pour quelques batchs, elles sont correctement attribuées. Nous avons donc besoin d'une fonction permettant d'afficher un certain nombre d'images ainsi que leurs étiquettes. La voici :
def plot_images(batch):
imgs = batch[0]
labels = batch[1]
ncols, nrows = 4,8
fig = plt.figure( figsize=(ncols*3, nrows*3), dpi=90)
for i, (img,label) in enumerate(zip(imgs,labels)):
plt.subplot(nrows, ncols, i+1)
plt.imshow(img.astype(np.int))
assert(label[0]+label[1]==1.)
categ = 'dog' if label[0]>0.5 else 'cat'
plt.title( '{} {}'.format(str(label), categ))
plt.axis('off')
plot_images(iterator.next())
Vous pouvez ré-exécuter la cellule précédente pour contrôler autant d'images que vous le souhaitez.
Nous allons entraîner le réseau de neurones sur un sous-ensemble des photos de chiens et de chats appelé l' échantillon d'entraînement .
Si un réseau est suffisamment complexe (s'il a suffisamment de paramètres), il peut être surentraîné . Cela veut dire qu'il commence à reconnaître les aspects spécifiques des images de l'échantillon d'entraînement. En d'autres termes, le réseau perd sa généralité et sa capacité à classifier une image de chien ou de chat encore inconnue.
Pour contrôler le surentraînement, nous allons évaluer les performances du réseau sur un échantillon de validation , disjoint de l'échantillon d'entraînement.
Créons d'abord un nouvel
ImageDataGenerator
. Par rapport au précédent, nous demandons que
imgdatagen = ImageDataGenerator(
rescale = 1/255.,
validation_split = 0.2,
)
Ensuite, nous définissons nos itérateurs pour les échantillons d'entraînement et de validation. Nous utilisons des batchs de 30 images car, typiquement, les réseaux ont du mal à apprendre si les batchs sont trop gros ou trop petits. Vous pourriez essayer avec une taille de batch différente après avoir terminé ce tuto.
On force les images à 256x256 pixels. En fait, nous devons juste nous assurer que toutes les images ont le même format, car le réseau de neurones convolutionnel que nous allons utiliser a un nombre fixé d'entrées. J'ai choisi une forme carrée pour éviter de trop grandes distorsions, que ce soit pour les images de format portrait ou paysage. Mais si la majorité des images sont en format portrait, il pourrait être intéressant de forcer un format portrait. Je n'ai pas essayé.
batch_size = 30
height, width = (256,256)
train_dataset = imgdatagen.flow_from_directory(
os.getcwd(),
target_size = (height, width),
classes = ('dogs','cats'),
batch_size = batch_size,
subset = 'training'
)
val_dataset = imgdatagen.flow_from_directory(
os.getcwd(),
target_size = (height, width),
classes = ('dogs','cats'),
batch_size = batch_size,
subset = 'validation'
)
Pour la classement d'images, la première architecture à essayer est le réseau de neurones convolutionnel profond. Pour une introduction à ce type de réseaux, vous pouvez vous référer à mon article Tuning a Deep Convolutional Network for Image Recognition, with keras and TensorFlow . Le modèle ci-dessous est très similaire à celui que nous avons utilisé dans cet article.
model = keras.models.Sequential()
initializers = {
}
model.add(
keras.layers.Conv2D(
24, 5, input_shape=(256,256,3),
activation='relu',
)
)
model.add( keras.layers.MaxPooling2D(2) )
model.add(
keras.layers.Conv2D(
48, 5, activation='relu',
)
)
model.add( keras.layers.MaxPooling2D(2) )
model.add(
keras.layers.Conv2D(
96, 5, activation='relu',
)
)
model.add( keras.layers.Flatten() )
model.add( keras.layers.Dropout(0.9) )
model.add( keras.layers.Dense(
2, activation='softmax',
)
)
model.summary()
Voici les différences principales par rapport au modèle utilisé pour la reconnaissance de chiffres manuscrits:
input_shape
de la première couche doit être adaptée au format des images fournies par le générateur.
Les deux premiers points sont techniques. Nous sommes forcés de faire ça pour que le réseau puisse tourner.
Les deux derniers points ne sont pas du tout évidents. Ces choix proviennent d'une longue optimisation. J'ai commencé avec deux couches et moins de caractéristiques, mais la précision calculée sur l'échantillon d'entraînement plafonnait, ce qui est un signe de sous-entraînement . Cela veut dire que le réseau n'a pas assez de paramètres pour décrire la variété du problème.
J'ai donc augmenté la complexité en ajoutant une couche, et augmenté le nombre de caractéristiques à extraire à chaque couche jusqu'à obtenir à l'entraînement une précision proche de 100%.
À ce moment-là, le réseau était surentraîné : la précision obtenue avec l'échantillon de validation était bien inférieure à celle obtenue avec l'échantillon d'entraînement. J'ai alors augmenté le taux de dropout par paliers de 0.4 à 0.9 pour réduire le surentraînement. Et j'ai fini par atteindre cette valeur très élevée. Elle veut dire que, avant la dernière couche dense, la couche de dropout élimine 90% des variables provenant de l'amont du réseau de manière aléatoire. C'est beaucoup!
Pour entraîner un réseau de neurones, il faut utiliser un optimiseur. Cet outil décide après batch du changement à appliquer aux paramètres du réseau pour minimiser la distance entre ses prédictions et la vérité. Parmi les optimiseurs implémentés dans Keras , on choisit généralement Adam ou RMSProp, souvent par habitude.
Mais dans le présent, ces optimiseurs ne fonctionnent pas bien. Le réseau démarre très souvent dans une configuration très éloignée du jeu de paramètres optimal, et il est tout simplement incapable d'apprendre. La fonction de coût démarre autour de 8, et ne diminue pas. En conséquence, la précision reste à 50%, ce qui équivaut à faire un choix à l'aveugle entre chien et chat.
Je suis donc revenu au Stochastic Gradient Descent (SGD). Cet optimiseur basique fonctionne, et le réseau apprend. Cependant, l'entraînement prend énormément de temps. Et c'est d'ailleurs la raison pour laquelle les optimiseurs rapides comme Adam et RMSProp ont été inventés.
Dans toutes ces études, j'ai tenté de varier fortement le taux d'apprentissage (learning rate), sans succès.
J'ai ensuite lu l'article Adam, A Method for Stochastic Optimization , et décidé d'essayer une des variantes d'Adam, appelée Adamax:
model.compile(loss='binary_crossentropy',
optimizer=keras.optimizers.Adamax(lr=0.001),
metrics=['acc'])
Après la compilation du modèle, on lance l'entraînement, et on mesure la précision à la fin de chaque époque avec l'échantillon de validation. J'utilise ici les 10 coeurs de mon CPU pour gérer les tâches de l'ImageDataGenerator, et deux GeForce GTX 1080 Ti pour TensorFlow.
Dans ces conditions, chaque époque prend environ une minute. Si cela vous prend beaucoup plus de temps, vous devriez vous assurer que vous êtes effectivement en train d'utiliser votre GPU pour TensorFlow. Vérifiez votre installation des drivers nvidia et de TensorFlow sous Linux ou Windows .
history = model.fit_generator(
train_dataset,
validation_data = val_dataset,
workers=10,
epochs=20,
)
Pour voir comment l'entraînement s'est déroulé, écrivons une petite fonction permettant de tracer la fonction de coût et la précision en fonction de l'époque, pour les échantillons d'entraînement et de validation:
def plot_history(history, yrange):
'''Trace le coût et la précision en fonction de l'époque,
pour les échantillons d'entraînement et de validation.
'''
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
# époques
epochs = range(len(acc))
# précision en fonction de l'époque
plt.plot(epochs, acc)
plt.plot(epochs, val_acc)
plt.title('Training and validation accuracy')
plt.ylim(yrange)
# coût en fonction de l'époque
plt.figure()
plt.plot(epochs, loss)
plt.plot(epochs, val_loss)
plt.title('Training and validation loss')
plt.show()
Et voici les résultats:
plot_history(history, (0.65, 1.))
On peut tirer les conclusions suivantes:
Nous pouvons difficilement augmenter encore le dropout, et nous avons donc besoin de plus de données pour améliorer les performances. Dans la section suivante, nous allons voir comment augmenter les données pour engendrer plus d'images d'entraînement à partir de celles que nous avons déjà. Cela sera beaucoup plus simple que de récupérer et d'étiqueter de nouvelles photos de chiens et de chats.
L'augmentation des données consiste à créer de nouveaux exemples pour l'entraînement à partir de ceux dont nous disposons déjà, de façon à augmenter artificiellement la taille de l'échantillon d'entraînement. C'est tout simple, grâce à l'ImageDataGenerator. Commençons par exemple par renverser la droite et la gauche dans nos images:
imgdatagen = ImageDataGenerator(
rescale = 1/255.,
horizontal_flip = True,
validation_split = 0.2,
)
Voyons l'effet de cette transformation sur une image donnée:
image = img.imread('cats/cat.12.jpg')
def plot_transform():
nrows, ncols = 2,4
fig = plt.figure(figsize=(ncols*3, nrows*3), dpi=90)
for i in range(nrows*ncols):
timage = imgdatagen.random_transform(image)
plt.subplot(nrows, ncols, i+1)
plt.imshow(timage)
plt.axis('off')
plot_transform()
Vous devriez pouvoir voir l'effet du renversement gauche-droite, à moins que vous n'ayez vraiment pas eu de chance!
Maintenant, mettons en place une transformation un peu plus complexe. Cette fois-ci, l'ImageDataGenerator va renverser gauche et droite, zoomer, et faire légèrement tourner les images, toujours de façon aléatoire:
imgdatagen = ImageDataGenerator(
rescale = 1/255.,
horizontal_flip = True,
zoom_range = 0.3,
rotation_range = 15.,
validation_split = 0.1,
)
plot_transform()
Nous voyons que ces transformations donnent de nouvelles images tout à fait acceptables. Nous pouvons donc ré-entraîner notre réseau avec l'augmentation des données. Il est important de noter que, du fait de la nature aléatoire des transformations, le réseau ne verra chaque image qu'une seule fois. Nous pouvons donc nous attendre à ce qu'il soit maintenant difficile de surentraîner le réseau.
batch_size = 30
height, width = (256,256)
train_dataset = imgdatagen.flow_from_directory(
os.getcwd(),
target_size = (height, width),
classes = ('dogs','cats'),
batch_size = batch_size,
subset = 'training'
)
val_dataset = imgdatagen.flow_from_directory(
os.getcwd(),
target_size = (height, width),
classes = ('dogs','cats'),
batch_size = batch_size,
subset = 'validation'
)
Avec l'augmentation, le dropout n'est sans doute plus autant nécessaire. J'ai donc réduit le taux de dropout de 0.9 à 0.2, mais je n'ai pas tenté d'optimiser ce paramètre.
model = keras.models.Sequential()
initializers = {
}
model.add(
keras.layers.Conv2D(
24, 5, input_shape=(256,256,3),
activation='relu',
)
)
model.add( keras.layers.MaxPooling2D(2) )
model.add(
keras.layers.Conv2D(
48, 5, activation='relu',
)
)
model.add( keras.layers.MaxPooling2D(2) )
model.add(
keras.layers.Conv2D(
96, 5, activation='relu',
)
)
model.add( keras.layers.Flatten() )
model.add( keras.layers.Dropout(0.2) )
model.add( keras.layers.Dense(
2, activation='softmax',
)
)
model.summary()
model.compile(loss='binary_crossentropy',
optimizer=keras.optimizers.Adamax(lr=0.001),
metrics=['acc'])
history_augm = model.fit_generator(
train_dataset,
validation_data = val_dataset,
# steps_per_epoch=10,
workers=10,
epochs=40,
)
plot_history(history_augm, (0.65, 1))
Comme vous pouvez le voir, avec l'augmentation des données, l'entraînement prend plus de temps mais le surentraînement est fortement réduit. Nous pouvons maintenant atteindre une précision de 92% sur l'échantillon de validation.
Nous pourrions continuer à régler les hyperparamètres du réseau pour limiter encore le surentraînement, par exemple en augmentant le taux de dropout et en entraînant plus longtemps. Mais j'imagine que nous ne parviendrons pas à dépasser une précision de 95% si on se limite à l'échantillon chiens et chats.
Heureusement, il y a d'autres possibilités, comme nous allons le voir.
De nombreuses personnes ont travaillé sur des problèmes de reconnaissance d'image, avec du matériel puissant et des échantillons d'images énormes.
Même si nous n'avons pas tout ça, nous pouvons utiliser leurs réseaux directement. Ceux-ci ont été pré-entraînés sur de très gros échantillons, ont une architecture profonde et complexe, et sont extrêmement précis. Ils peuvent être téléchargés avec tous leurs paramètres, dans l'état où ils se trouvaient à la fin de l'entraînement. Ainsi, nous pouvons facilement obtenir d'excellentes performances, sans avoir à entraîner le modèle nous-mêmes.
Voici la liste des modèles pré-entraînés disponibles dans keras .
J'ai décidé d'utiliser ResNet50 , un modèle entraîné sur l'échantillon ImageNet , qui contient 14 millions d'images dans 1000 catégories.
D'abord, en quelques lignes de code, on télécharge le modèle:
from keras.applications.resnet50 import ResNet50
from keras.preprocessing import image
from keras.applications.resnet50 import preprocess_input, decode_predictions
import numpy as np
model = ResNet50(weights='imagenet')
Ensuite, on crée une petite fonction pour évaluer le modèle pour une image donnée, et on appelle cette fonction pour quelques images de notre échantillon chiens et chats:
def evaluate(img_fname):
img = image.load_img(img_fname, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
preds = model.predict(x)
# print the probability and category name for the 5 categories
# with highest probability:
print('Predicted:', decode_predictions(preds, top=5)[0])
plt.imshow(img)
evaluate('dogs/dog.0.jpg')
evaluate('dogs/dog.1.jpg')
evaluate('dogs/dog.2.jpg')
Comme on peut le voir, les catégories les plus probables correspondent effectivement à des chiens, et le réseau est même pratiquement capable de reconnaître la race du chien!
Qu'en est-il des chats?
evaluate('cats/cat.0.jpg')
Là, ça ne marche pas aussi bien. La première catégorie est télévision, sans doute à cause de la mauvaise qualité de cette photo. Ensuite, on obtient des chiens de couleur proche de celle de ce chat, et finalement un chat. Pour l'image suivante, le réseau se comporte beaucoup mieux:
evaluate('cats/cat.1.jpg')
Maintenant, essayons avec des dessins de chien:
evaluate('Trash/dogs/dog.9188.jpg')
La catégorie de probabilité la plus élevée correspond effectivement à un chien! mais la catégorie suivantes est ... tronçonneuse. Et qu'en est-il de l'image de couverture de cet article?
# téléchargement de l'image depuis mon repo github:
import urllib.request as req
url = 'https://raw.githubusercontent.com/cbernet/maldives/master/dogs_vs_cats/datafrog_chien_chat.png'
req.urlretrieve(url, 'dog_cartoon.jpg')
evaluate('dog_cartoon.jpg')
Le réseau ne s'est pas laissé berner par le déguisement: c'est un chien!
Bon, c'est plutôt rigolo de jouer avec ResNet50, mais ce réseau n'est pas tout à fait adapter à notre problème, qui est de classifier des images en deux catégories, chien ou chat. Pour faire cela avec un modèle basé sur ImageNet, il faudrait être capable de savoir qu'une catégorie fine comme
Great_Dane
appartient en fait à la catégorie plus inclusive
dog
. Et tant que nous ne savons pas faire ça, nous ne pouvons pas quantifier les performances de ce modèle dans le contexte de notre problème.
Nous pourrions faire cela à la main, mais nous étudierons une solution plus élégante dans le prochain article
Dans cet article, vous avez appris à :
ResNet50 a été entraîné à classer des images dans 1000 catégories différentes.
Dans un futur article, nous verrons comment utiliser ResNet50 pour classer les images en deux catégories plus larges, chien et chat. Et nous verrons également comment utiliser le transfer learning.
N'hésitez pas à me donner votre avis dans les commentaires ! Je répondrai à toutes les questions.
Et si vous avez aimé cet article, vous pouvez souscrire à ma newsletter pour être prévenu lorsque j'en sortirai un nouveau. Pas plus d'un mail par semaine, promis!
Rejoignez ma mailing list pour plus de posts et du contenu exclusif: