Entre chien et chat (92% de précision)

Classifier des images de chats et de chiens avec une précision de 92%, sans transfer learning

Chien ou chat?

Introduction

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 :

  • construire et régler un réseau de neurones convolutionel avec keras pour le classement d'images.
  • choisir le bon optimiseur pour que votre réseau soit capable d'apprendre
  • utiliser l'ImageDataGenerator de Keras pour augmenter votre dataset et limiter le surentraînement.

Et en bonus, vous essaierez le réseau pré-entraîné ResNet50, juste pour voir ce que ça donne.

Faire tourner ce tuto

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:

  • les transferts avec les disques sur les machines virtuelles de Google Colab prennent trop de temps. Cela ralentit énormément l'entraînement des réseaux de neurones pour les datasets relativement gros et lus en continu depuis le disque, comme celui que nous allons utiliser.
  • les machines virtuelles n'ont pas assez de coeurs CPU pour supporter le pré-traitement des images que nous allons appliquer.

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.

Le dataset chiens et chats

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.

Initialisation

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.

In [2]:
# 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

Un premier coup d'oeil au dataset chiens et chats

Commençons par afficher la premier image de chaque catégorie :

In [11]:
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'))
Out[11]:
<matplotlib.image.AxesImage at 0x7f33ea1b7b38>

Ils sont bien mignons, mais allons plus loin et voyons quelle est la taille de nos images :

In [3]:
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())
image shape (374, 500, 3) maximum color level 255
image shape (280, 300, 3) maximum color level 255
image shape (396, 312, 3) maximum color level 255
image shape (414, 500, 3) maximum color level 255
image shape (375, 499, 3) maximum color level 255
image shape (144, 175, 3) maximum color level 255
image shape (303, 400, 3) maximum color level 255
image shape (499, 495, 3) maximum color level 255
image shape (345, 461, 3) maximum color level 255
image shape (425, 320, 3) maximum color level 247

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.

Toilettage : amélioration de la qualité du dataset

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 :

In [4]:
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 :

In [5]:
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
In [10]:
bad_dogs = load_images(bad_dog_ids, 'dog')
bad_cats = load_images(bad_cat_ids, 'cat')
In [11]:
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')
In [12]:
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 :

In [13]:
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:

In [20]:
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)
 
In [15]:
cleanup(bad_cat_ids,'cats')
moving to ../Trash/cats: cat.4688.jpg
moving to ../Trash/cats: cat.6215.jpg
moving to ../Trash/cats: cat.11565.jpg
moving to ../Trash/cats: cat.8470.jpg
moving to ../Trash/cats: cat.3216.jpg
moving to ../Trash/cats: cat.2939.jpg
moving to ../Trash/cats: cat.4833.jpg
moving to ../Trash/cats: cat.8456.jpg
moving to ../Trash/cats: cat.7377.jpg
moving to ../Trash/cats: cat.12272.jpg
moving to ../Trash/cats: cat.5418.jpg
In [16]:
cleanup(bad_dog_ids, 'dogs')
moving to ../Trash/dogs: dog.10190.jpg
moving to ../Trash/dogs: dog.10797.jpg
moving to ../Trash/dogs: dog.5604.jpg
moving to ../Trash/dogs: dog.10237.jpg
moving to ../Trash/dogs: dog.8736.jpg
moving to ../Trash/dogs: dog.6413.jpg
moving to ../Trash/dogs: dog.10161.jpg
moving to ../Trash/dogs: dog.8898.jpg
moving to ../Trash/dogs: dog.9517.jpg
moving to ../Trash/dogs: dog.10401.jpg
moving to ../Trash/dogs: dog.9188.jpg
moving to ../Trash/dogs: dog.11186.jpg

Si vous voulez restaurer votre dataset, décommentez les lignes suivantes et exécutez la cellule :

In [17]:
# restore('dogs')
# restore('cats')

Chargement du dataset chiens et chats avec keras

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.

In [23]:
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 :

In [24]:
iterator = gen.flow_from_directory(
    os.getcwd(), 
    target_size=(256,256), 
    classes=('dogs','cats')
)
Found 24977 images belonging to 2 classes.
In [25]:
# tous les itérateurs python ont une fonction next()
batch = iterator.next()
len(batch)
Out[25]:
2

Le batch a deux éléments. Quel est leur type?

In [27]:
print(type(batch[0]))
print(type(batch[1]))
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>

Deux tableaux numpy! parfait. On peut imprimer leur shape, le type des données stockées, et la valeur maximum de ces données :

In [29]:
print(batch[0].shape)
print(batch[0].dtype)
print(batch[0].max())
print(batch[1].shape)
print(batch[1].dtype)
(32, 256, 256, 3)
float32
255.0
(32, 2)
float32

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 :

In [30]:
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))
Out[30]:
<matplotlib.image.AxesImage at 0x7faa1c06b4a8>

Et voici l'étiquette correspondante :

In [31]:
batch[1][0]
Out[31]:
array([1., 0.], dtype=float32)

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.

Affichage d'animaux pour la validation des étiquettes

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 :

In [32]:
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')
In [33]:
plot_images(iterator.next())

Vous pouvez ré-exécuter la cellule précédente pour contrôler autant d'images que vous le souhaitez.

Création des échantillons d'entraînement et de test avec l'ImageDataGenerator

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

  • tous les niveaux de couleur soient normalisés à 1, car les réseaux de neurones se comportent mieux lorsqu'ils traitent des variables d'entrée de l'ordre de 1.
  • 20% des images soient utilisées pour la validation, et 80% pour l'entraînement.
In [34]:
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é.

In [35]:
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'
)
Found 19983 images belonging to 2 classes.
Found 4994 images belonging to 2 classes.

Le réseau de neurones convolutionnel

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.

In [36]:
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()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 252, 252, 24)      1824      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 126, 126, 24)      0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 122, 122, 48)      28848     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 61, 61, 48)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 57, 57, 96)        115296    
_________________________________________________________________
flatten (Flatten)            (None, 311904)            0         
_________________________________________________________________
dropout (Dropout)            (None, 311904)            0         
_________________________________________________________________
dense (Dense)                (None, 2)                 623810    
=================================================================
Total params: 769,778
Trainable params: 769,778
Non-trainable params: 0
_________________________________________________________________

Voici les différences principales par rapport au modèle utilisé pour la reconnaissance de chiffres manuscrits:

  • nous avons deux neurones dans la dernière couche softmax au lieu de 10, car nous avons deux catégories.
  • les images en entrée sont beaucoup plus grandes, 256x256 au lieu de 28x28. Il faut noter que l' input_shape de la première couche doit être adaptée au format des images fournies par le générateur.
  • j'ai ajouté une troisième couche convolutionnelle, et j'extrais plus de caractéristiques à chaque couche.
  • j'ai augmenté le taux de dropout de 0.4 à 0.9 pour réduire le surentraînement.

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:

In [5]:
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 .

In [6]:
history = model.fit_generator(
    train_dataset, 
    validation_data = val_dataset,
    workers=10,
    epochs=20,
)
Epoch 1/20
667/667 [==============================] - 52s 78ms/step - loss: 0.5967 - acc: 0.6691 - val_loss: 0.5249 - val_acc: 0.7437
Epoch 2/20
667/667 [==============================] - 48s 72ms/step - loss: 0.5108 - acc: 0.7454 - val_loss: 0.4759 - val_acc: 0.7717
Epoch 3/20
667/667 [==============================] - 48s 71ms/step - loss: 0.4644 - acc: 0.7815 - val_loss: 0.4619 - val_acc: 0.7827
Epoch 4/20
667/667 [==============================] - 47s 71ms/step - loss: 0.4261 - acc: 0.8056 - val_loss: 0.4290 - val_acc: 0.8008
Epoch 5/20
667/667 [==============================] - 49s 73ms/step - loss: 0.3907 - acc: 0.8240 - val_loss: 0.4173 - val_acc: 0.8180
Epoch 6/20
667/667 [==============================] - 47s 71ms/step - loss: 0.3675 - acc: 0.8381 - val_loss: 0.3869 - val_acc: 0.8350
Epoch 7/20
667/667 [==============================] - 47s 71ms/step - loss: 0.3457 - acc: 0.8488 - val_loss: 0.3641 - val_acc: 0.8368
Epoch 8/20
667/667 [==============================] - 48s 73ms/step - loss: 0.3196 - acc: 0.8674 - val_loss: 0.4048 - val_acc: 0.8168
Epoch 9/20
667/667 [==============================] - 49s 73ms/step - loss: 0.3057 - acc: 0.8712 - val_loss: 0.3460 - val_acc: 0.8524
Epoch 10/20
667/667 [==============================] - 49s 74ms/step - loss: 0.2897 - acc: 0.8788 - val_loss: 0.3495 - val_acc: 0.8506
Epoch 11/20
667/667 [==============================] - 49s 74ms/step - loss: 0.2725 - acc: 0.8858 - val_loss: 0.3393 - val_acc: 0.8570
Epoch 12/20
667/667 [==============================] - 49s 73ms/step - loss: 0.2616 - acc: 0.8910 - val_loss: 0.3331 - val_acc: 0.8622
Epoch 13/20
667/667 [==============================] - 49s 73ms/step - loss: 0.2418 - acc: 0.9012 - val_loss: 0.3336 - val_acc: 0.8634
Epoch 14/20
667/667 [==============================] - 49s 73ms/step - loss: 0.2323 - acc: 0.9041 - val_loss: 0.3286 - val_acc: 0.8698
Epoch 15/20
667/667 [==============================] - 48s 73ms/step - loss: 0.2159 - acc: 0.9127 - val_loss: 0.3345 - val_acc: 0.8664
Epoch 16/20
667/667 [==============================] - 48s 72ms/step - loss: 0.2145 - acc: 0.9124 - val_loss: 0.3245 - val_acc: 0.8734
Epoch 17/20
667/667 [==============================] - 48s 72ms/step - loss: 0.2038 - acc: 0.9182 - val_loss: 0.3167 - val_acc: 0.8688
Epoch 18/20
667/667 [==============================] - 47s 71ms/step - loss: 0.1917 - acc: 0.9227 - val_loss: 0.3108 - val_acc: 0.8748
Epoch 19/20
667/667 [==============================] - 48s 72ms/step - loss: 0.1812 - acc: 0.9282 - val_loss: 0.3360 - val_acc: 0.8748
Epoch 20/20
667/667 [==============================] - 48s 71ms/step - loss: 0.1771 - acc: 0.9289 - val_loss: 0.3170 - val_acc: 0.8744

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:

In [10]:
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:

In [9]:
plot_history(history, (0.65, 1.))

On peut tirer les conclusions suivantes:

  • la précision d'entraînement ne s'approche pas de 100%, à cause du fort taux de dropout.
  • le réseau commence à être surentraîné dès l'époque 6, malgré le dropout.
  • la précision de validation sature autour de 87%, ce qui n'est pas si mauvais.

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.

Augmentation des données.

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:

In [2]:
imgdatagen = ImageDataGenerator(
    rescale = 1/255., 
    horizontal_flip = True, 
    validation_split = 0.2,
)

Voyons l'effet de cette transformation sur une image donnée:

In [3]:
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:

In [4]:
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.

In [5]:
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'
)
Found 22481 images belonging to 2 classes.
Found 2496 images belonging to 2 classes.

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.

In [6]:
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()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 252, 252, 24)      1824      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 126, 126, 24)      0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 122, 122, 48)      28848     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 61, 61, 48)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 57, 57, 96)        115296    
_________________________________________________________________
flatten (Flatten)            (None, 311904)            0         
_________________________________________________________________
dropout (Dropout)            (None, 311904)            0         
_________________________________________________________________
dense (Dense)                (None, 2)                 623810    
=================================================================
Total params: 769,778
Trainable params: 769,778
Non-trainable params: 0
_________________________________________________________________
In [7]:
model.compile(loss='binary_crossentropy',
              optimizer=keras.optimizers.Adamax(lr=0.001),
              metrics=['acc'])
In [8]:
history_augm = model.fit_generator(
    train_dataset, 
    validation_data = val_dataset,
    # steps_per_epoch=10,
    workers=10,
    epochs=40,
)
Epoch 1/40
750/750 [==============================] - 157s 210ms/step - loss: 0.6404 - acc: 0.6483 - val_loss: 0.6010 - val_acc: 0.6775
Epoch 2/40
750/750 [==============================] - 152s 203ms/step - loss: 0.5433 - acc: 0.7251 - val_loss: 0.4905 - val_acc: 0.7648
Epoch 3/40
750/750 [==============================] - 152s 203ms/step - loss: 0.4931 - acc: 0.7637 - val_loss: 0.4796 - val_acc: 0.7664
Epoch 4/40
750/750 [==============================] - 152s 202ms/step - loss: 0.4619 - acc: 0.7806 - val_loss: 0.4358 - val_acc: 0.8025
Epoch 5/40
750/750 [==============================] - 154s 205ms/step - loss: 0.4272 - acc: 0.8059 - val_loss: 0.4098 - val_acc: 0.8113
Epoch 6/40
750/750 [==============================] - 151s 202ms/step - loss: 0.4021 - acc: 0.8178 - val_loss: 0.4086 - val_acc: 0.8205
Epoch 7/40
750/750 [==============================] - 153s 203ms/step - loss: 0.3769 - acc: 0.8323 - val_loss: 0.3560 - val_acc: 0.8425
Epoch 8/40
750/750 [==============================] - 154s 206ms/step - loss: 0.3549 - acc: 0.8431 - val_loss: 0.3507 - val_acc: 0.8425
Epoch 9/40
750/750 [==============================] - 153s 204ms/step - loss: 0.3430 - acc: 0.8492 - val_loss: 0.4002 - val_acc: 0.8093
Epoch 10/40
750/750 [==============================] - 157s 209ms/step - loss: 0.3198 - acc: 0.8639 - val_loss: 0.3425 - val_acc: 0.8502
Epoch 11/40
750/750 [==============================] - 157s 209ms/step - loss: 0.3044 - acc: 0.8719 - val_loss: 0.3208 - val_acc: 0.8610
Epoch 12/40
750/750 [==============================] - 153s 203ms/step - loss: 0.2943 - acc: 0.8748 - val_loss: 0.2948 - val_acc: 0.8754
Epoch 13/40
750/750 [==============================] - 152s 203ms/step - loss: 0.2809 - acc: 0.8813 - val_loss: 0.3034 - val_acc: 0.8602
Epoch 14/40
750/750 [==============================] - 153s 204ms/step - loss: 0.2675 - acc: 0.8890 - val_loss: 0.2778 - val_acc: 0.8854
Epoch 15/40
750/750 [==============================] - 152s 203ms/step - loss: 0.2613 - acc: 0.8920 - val_loss: 0.2964 - val_acc: 0.8830
Epoch 16/40
750/750 [==============================] - 152s 202ms/step - loss: 0.2498 - acc: 0.8964 - val_loss: 0.3105 - val_acc: 0.8754
Epoch 17/40
750/750 [==============================] - 153s 205ms/step - loss: 0.2421 - acc: 0.9012 - val_loss: 0.2590 - val_acc: 0.8874
Epoch 18/40
750/750 [==============================] - 152s 202ms/step - loss: 0.2398 - acc: 0.9020 - val_loss: 0.3462 - val_acc: 0.8409
Epoch 19/40
750/750 [==============================] - 152s 202ms/step - loss: 0.2327 - acc: 0.9025 - val_loss: 0.2589 - val_acc: 0.9002
Epoch 20/40
750/750 [==============================] - 153s 204ms/step - loss: 0.2230 - acc: 0.9097 - val_loss: 0.2503 - val_acc: 0.9018
Epoch 21/40
750/750 [==============================] - 152s 203ms/step - loss: 0.2156 - acc: 0.9098 - val_loss: 0.2424 - val_acc: 0.9022
Epoch 22/40
750/750 [==============================] - 150s 200ms/step - loss: 0.2145 - acc: 0.9125 - val_loss: 0.2580 - val_acc: 0.8982
Epoch 23/40
750/750 [==============================] - 152s 203ms/step - loss: 0.2049 - acc: 0.9178 - val_loss: 0.2428 - val_acc: 0.8994
Epoch 24/40
750/750 [==============================] - 153s 203ms/step - loss: 0.2071 - acc: 0.9154 - val_loss: 0.2391 - val_acc: 0.8966
Epoch 25/40
750/750 [==============================] - 150s 200ms/step - loss: 0.2007 - acc: 0.9180 - val_loss: 0.2328 - val_acc: 0.9099
Epoch 26/40
750/750 [==============================] - 152s 203ms/step - loss: 0.1987 - acc: 0.9197 - val_loss: 0.2234 - val_acc: 0.9111
Epoch 27/40
750/750 [==============================] - 155s 206ms/step - loss: 0.1885 - acc: 0.9221 - val_loss: 0.2139 - val_acc: 0.9107
Epoch 28/40
750/750 [==============================] - 151s 201ms/step - loss: 0.1897 - acc: 0.9218 - val_loss: 0.2000 - val_acc: 0.9235
Epoch 29/40
750/750 [==============================] - 153s 203ms/step - loss: 0.1782 - acc: 0.9277 - val_loss: 0.2214 - val_acc: 0.9038
Epoch 30/40
750/750 [==============================] - 152s 202ms/step - loss: 0.1759 - acc: 0.9272 - val_loss: 0.2241 - val_acc: 0.9087
Epoch 31/40
750/750 [==============================] - 153s 204ms/step - loss: 0.1742 - acc: 0.9301 - val_loss: 0.2173 - val_acc: 0.9071
Epoch 32/40
750/750 [==============================] - 153s 204ms/step - loss: 0.1718 - acc: 0.9311 - val_loss: 0.2239 - val_acc: 0.9135
Epoch 33/40
750/750 [==============================] - 153s 203ms/step - loss: 0.1639 - acc: 0.9340 - val_loss: 0.2108 - val_acc: 0.9139
Epoch 34/40
750/750 [==============================] - 152s 203ms/step - loss: 0.1635 - acc: 0.9333 - val_loss: 0.2072 - val_acc: 0.9199
Epoch 35/40
750/750 [==============================] - 151s 202ms/step - loss: 0.1587 - acc: 0.9366 - val_loss: 0.1987 - val_acc: 0.9183
Epoch 36/40
750/750 [==============================] - 151s 202ms/step - loss: 0.1601 - acc: 0.9360 - val_loss: 0.1984 - val_acc: 0.9219
Epoch 37/40
750/750 [==============================] - 153s 204ms/step - loss: 0.1541 - acc: 0.9377 - val_loss: 0.2222 - val_acc: 0.9127
Epoch 38/40
750/750 [==============================] - 152s 203ms/step - loss: 0.1573 - acc: 0.9366 - val_loss: 0.1832 - val_acc: 0.9275
Epoch 39/40
750/750 [==============================] - 154s 206ms/step - loss: 0.1532 - acc: 0.9388 - val_loss: 0.1987 - val_acc: 0.9203
Epoch 40/40
750/750 [==============================] - 153s 204ms/step - loss: 0.1502 - acc: 0.9386 - val_loss: 0.2028 - val_acc: 0.9239
In [11]:
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.

Utilisation d'un modèle pré-entraîné : ResNet50

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:

In [3]:
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:

In [8]:
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)
In [11]:
evaluate('dogs/dog.0.jpg')
Predicted: [('n02102318', 'cocker_spaniel', 0.29664052), ('n02097298', 'Scotch_terrier', 0.14396854), ('n02097130', 'giant_schnauzer', 0.14393643), ('n02110627', 'affenpinscher', 0.10783979), ('n02088094', 'Afghan_hound', 0.04753604)]
In [12]:
evaluate('dogs/dog.1.jpg')
Predicted: [('n02099849', 'Chesapeake_Bay_retriever', 0.87790024), ('n02105412', 'kelpie', 0.06544642), ('n02099712', 'Labrador_retriever', 0.008923257), ('n02106550', 'Rottweiler', 0.005405719), ('n02099429', 'curly-coated_retriever', 0.004976345)]
In [13]:
evaluate('dogs/dog.2.jpg')
Predicted: [('n02108551', 'Tibetan_mastiff', 0.2795359), ('n02097474', 'Tibetan_terrier', 0.21642059), ('n02106030', 'collie', 0.21163173), ('n02106166', 'Border_collie', 0.06243812), ('n02108000', 'EntleBucher', 0.040746134)]

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?

In [14]:
evaluate('cats/cat.0.jpg')
Predicted: [('n04404412', 'television', 0.10631036), ('n02094258', 'Norwich_terrier', 0.10413598), ('n02085620', 'Chihuahua', 0.075974055), ('n02093991', 'Irish_terrier', 0.07585583), ('n02123045', 'tabby', 0.07348687)]

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:

In [15]:
evaluate('cats/cat.1.jpg')
Predicted: [('n02123045', 'tabby', 0.6946943), ('n02123159', 'tiger_cat', 0.18619044), ('n02124075', 'Egyptian_cat', 0.06427032), ('n02127052', 'lynx', 0.00819175), ('n03958227', 'plastic_bag', 0.004613503)]

Maintenant, essayons avec des dessins de chien:

In [17]:
evaluate('Trash/dogs/dog.9188.jpg')
Predicted: [('n02088466', 'bloodhound', 0.11124672), ('n03000684', 'chain_saw', 0.1000548), ('n03814639', 'neck_brace', 0.04567196), ('n03825788', 'nipple', 0.035572648), ('n03803284', 'muzzle', 0.030304618)]

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?

In [21]:
# 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')
Predicted: [('n02106662', 'German_shepherd', 0.27522734), ('n02113023', 'Pembroke', 0.17356005), ('n03803284', 'muzzle', 0.12873776), ('n02109047', 'Great_Dane', 0.06609615), ('n02114712', 'red_wolf', 0.060678747)]

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

Conclusion

Dans cet article, vous avez appris à :

  • construire et régler un réseau de neurones convolutionel avec keras pour le classement d'images.
  • choisir le bon optimiseur pour que votre réseau soit capable d'apprendre
  • utiliser l'ImageDataGenerator de Keras pour augmenter votre dataset et limiter le surentraînement.
  • utiliser un modèle pré-entraîné avec Keras.

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!

Retour


Encore plus de data science et de machine learning !

Rejoignez ma mailing list pour plus de posts et du contenu exclusif:

Je ne partagerai jamais vos infos.
Partagez si vous aimez cet article: