Utiliser des réseaux de neurones convolutifs (CNN) avec des images complexes

1. Avant de commencer

Dans cet atelier de programmation, vous allez utiliser des convolutions pour classer des images de chevaux et d'êtres humains. Vous allez utiliser TensorFlow dans cet atelier pour créer un réseau de neurones convolutif qui apprend à reconnaître des chevaux et des humains, puis à les classer.

Conditions préalables

Si vous n'avez jamais créé de convolutions avec TensorFlow, vous pouvez suivre l'atelier de programmation Créer des convolutions et effectuer des pools, où nous vous présentons les convolutions et le pooling, et Créer des réseaux de neurones convolutifs (CNN) pour améliorer la vision par ordinateur, où nous verrons comment améliorer la reconnaissance d'images pour les ordinateurs.

Points abordés

  • Apprendre aux ordinateurs à reconnaître les caractéristiques d'une image dans laquelle le sujet n'est pas clair

Objectifs de l'atelier

  • Réseau de neurones convolutif qui permet de distinguer les images de chevaux des images d'êtres humains

Prérequis

Vous trouverez le code pour le reste de l'atelier de programmation en cours d'exécution dans Colab.

TensorFlow et les bibliothèques que vous avez installées dans l'atelier de programmation précédent doivent également être installées.

2. Premiers pas: récupérer les données

Pour ce faire, vous allez créer un classificateur de chevaux ou d'humains qui vous indiquera si une image donnée contient un cheval ou un être humain, et où le réseau est entraîné à reconnaître les caractéristiques qui déterminent lesquelles. Vous devrez traiter les données avant de pouvoir vous entraîner.

Commencez par télécharger les données:

!wget --no-check-certificate https://storage.googleapis.com/laurencemoroney-blog.appspot.com/horse-or-human.zip  -O /tmp/horse-or-human.zip

Le code Python suivant utilise la bibliothèque d'OS pour utiliser les bibliothèques de systèmes d'exploitation. Il vous permet d'accéder au système de fichiers et à la bibliothèque de fichiers ZIP, ce qui vous permet de décompresser les données.

import os
import zipfile
 
local_zip = '/tmp/horse-or-human.zip'
zip_ref = zipfile.ZipFile(local_zip, 'r')
zip_ref.extractall('/tmp/horse-or-human')
zip_ref.close()

Le contenu du fichier ZIP est extrait dans le répertoire de base /tmp/horse-or-human, qui contient les chevaux et les sous-répertoires humains.

En bref, l'ensemble d'entraînement correspond aux données utilisées pour indiquer au modèle de réseau de neurones qu'il représente à tel point et qu'il représente.

3. Utiliser l'outil ImageGenerator pour étiqueter et préparer les données

Vous ne citez pas les images de manière explicite comme des chevaux ou des humains.

Vous verrez plus tard une opération appelée"ImageDataGenerator". Il lit les images des sous-répertoires et les attribue automatiquement à un libellé à partir du nom de ce sous-répertoire. Par exemple, si vous créez un répertoire d'entraînement contenant un répertoire d'équitation et un répertoire d'humains, ImageDataGenerator étiquette les images de manière appropriée, ce qui réduit le nombre d'étapes de codage.

Définissez chacun de ces répertoires.

# Directory with our training horse pictures
train_horse_dir = os.path.join('/tmp/horse-or-human/horses')
 
# Directory with our training human pictures
train_human_dir = os.path.join('/tmp/horse-or-human/humans')

Découvrez à présent les noms de fichier des répertoires d'entraînements pour chevaux et humains:

train_horse_names = os.listdir(train_horse_dir)
print(train_horse_names[:10])
train_human_names = os.listdir(train_human_dir)
print(train_human_names[:10])

Recherchez le nombre total d'images équestres et humaines dans les répertoires:

print('total training horse images:', len(os.listdir(train_horse_dir)))
print('total training human images:', len(os.listdir(train_human_dir)))

4. Explorez les données

Jetez un œil à quelques photos pour vous faire une meilleure idée de ce à quoi elles ressemblent.

Commencez par configurer les paramètres matplot:

%matplotlib inline
 
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
 
# Parameters for our graph; we'll output images in a 4x4 configuration
nrows = 4
ncols = 4
 
# Index for iterating over images
pic_index = 0

Maintenant, affichez un lot de huit photos de chevaux et huit images représentant des humains. Vous pouvez réexécuter la cellule pour afficher chaque fois un nouveau lot.

# Set up matplotlib fig, and size it to fit 4x4 pics
fig = plt.gcf()
fig.set_size_inches(ncols * 4, nrows * 4)
 
pic_index += 8
next_horse_pix = [os.path.join(train_horse_dir, fname) 
                for fname in train_horse_names[pic_index-8:pic_index]]
next_human_pix = [os.path.join(train_human_dir, fname) 
                for fname in train_human_names[pic_index-8:pic_index]]
 
for i, img_path in enumerate(next_horse_pix+next_human_pix):
  # Set up subplot; subplot indices start at 1
  sp = plt.subplot(nrows, ncols, i + 1)
  sp.axis('Off') # Don't show axes (or gridlines)
 
  img = mpimg.imread(img_path)
  plt.imshow(img)
 
plt.show()
 

Voici quelques exemples d'images représentant des chevaux et des humains dans différentes positions et orientations:

6b6ebbc6e694ccd2.png

5. Définir le modèle

Commencez à définir le modèle.

Commencez par importer TensorFlow:

import tensorflow as tf

Ajoutez ensuite des couches convolutives et aplatissez le résultat final pour alimenter les couches densément reliées. Enfin, ajoutez les couches densément denses.

Notez que étant donné que vous êtes confronté à un problème de classification à deux classes (un problème de classification binaire), vous obtiendrez une activation sigmoïde pour votre réseau de sorte que le résultat de votre réseau soit une grande scalaire comprise entre 0 et 1, et que l'image actuelle soit codée par probabilité de classe 1 (plutôt que par la classe 0).

model = tf.keras.models.Sequential([
    # Note the input shape is the desired size of the image 300x300 with 3 bytes color
    # This is the first convolution
    tf.keras.layers.Conv2D(16, (3,3), activation='relu', input_shape=(300, 300, 3)),
    tf.keras.layers.MaxPooling2D(2, 2),
    # The second convolution
    tf.keras.layers.Conv2D(32, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    # The third convolution
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    # The fourth convolution
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    # The fifth convolution
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    # Flatten the results to feed into a DNN
    tf.keras.layers.Flatten(),
    # 512 neuron hidden layer
    tf.keras.layers.Dense(512, activation='relu'),
    # Only 1 output neuron. It will contain a value from 0-1 where 0 for 1 class ('horses') and 1 for the other ('humans')
    tf.keras.layers.Dense(1, activation='sigmoid')
])

L'appel de la méthode model.summary() affiche un résumé du réseau.

model.summary()

Les résultats sont disponibles ici:

Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 298, 298, 16)      448       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 149, 149, 16)      0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 147, 147, 32)      4640      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 73, 73, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 71, 71, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 35, 35, 64)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 33, 33, 64)        36928     
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 16, 16, 64)        0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 14, 14, 64)        36928     
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 7, 7, 64)          0         
_________________________________________________________________
flatten (Flatten)            (None, 3136)              0         
_________________________________________________________________
dense (Dense)                (None, 512)               1606144   
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 513       
=================================================================
Total params: 1,704,097
Trainable params: 1,704,097
Non-trainable params: 0

La colonne "Forme de sortie" montre comment la taille de votre mappage de caractéristiques évolue dans chaque couche successive. En raison de la marge intérieure, les couches de convolution réduisent légèrement la taille des cartes de caractéristiques, et chaque couche de regroupement s'en trouve divisée par deux.

6. Compiler le modèle

Configurez ensuite les spécifications pour l'entraînement du modèle. Entraînez votre modèle à la perte de binary_crossentropy, car il s'agit d'un problème de classification binaire et votre activation finale est un sigmoïde. (Pour en savoir plus sur les métriques de perte, consultez Passer au ML.) Utilisez l'optimiseur rmsprop avec un taux d'apprentissage de 0,001. Pendant l'entraînement, surveillez la justesse de la classification.

from tensorflow.keras.optimizers import RMSprop
 
model.compile(loss='binary_crossentropy',
              optimizer=RMSprop(lr=0.001),
              metrics=['acc'])

7. Entraîner le modèle à partir de générateurs

Configurer les générateurs de données qui lisent les images dans vos dossiers sources, les convertissent en Tensors float32 et les ajoutent à leur réseau (avec leurs étiquettes).

Vous disposez d'un générateur pour les images d'entraînement et d'un générateur pour les images de validation. Vos générateurs généreront des lots d'images de taille 300 x 300 et leurs étiquettes (binaires).

Comme vous le savez peut-être déjà, les données qui transitent par les réseaux de neurones doivent généralement être normalisées de façon à faciliter leur traitement par ce dernier. (Il est courant qu'il insère des pixels bruts dans un réseau de neurones convolutif.) Dans votre cas, vous prétraitez vos images en normalisant les valeurs de pixels dans la plage [0, 1] (au départ, toutes les valeurs se trouvent dans la plage [0, 255]).

Dans Keras, cela peut être fait via la classe keras.preprocessing.image.ImageDataGenerator en utilisant le paramètre de redimensionnement. Cette classe ImageDataGenerator vous permet d'instancier des générateurs de lots d'images augmentés (et leurs étiquettes) via .flow(data, labels) ou .flow_from_directory(directory). Ces générateurs peuvent ensuite être utilisés avec les méthodes de modèle Keras qui acceptent des générateurs de données en tant qu'entrées : fit_generator, evaluate_generator et predict_generator.

from tensorflow.keras.preprocessing.image import ImageDataGenerator
 
# All images will be rescaled by 1./255
train_datagen = ImageDataGenerator(rescale=1./255)
 
# Flow training images in batches of 128 using train_datagen generator
train_generator = train_datagen.flow_from_directory(
        '/tmp/horse-or-human/',  # This is the source directory for training images
        target_size=(300, 300),  # All images will be resized to 150x150
        batch_size=128,
        # Since we use binary_crossentropy loss, we need binary labels
        class_mode='binary')

8. Faire la formation

Entraînez le modèle pendant 15 époques. Cette opération peut prendre quelques minutes.

history = model.fit(
      train_generator,
      steps_per_epoch=8,  
      epochs=15,
      verbose=1)

Notez les valeurs par époque.

La perte et la précision sont un excellent indicateur de la progression de l'entraînement. Il s'agit d'une estimation de la classification des données d'entraînement, qui est ensuite comparée à l'étiquette connue pour calculer le résultat. La justesse correspond à la partie correcte des approximations.

Epoch 1/15
9/9 [==============================] - 9s 1s/step - loss: 0.8662 - acc: 0.5151
Epoch 2/15
9/9 [==============================] - 8s 927ms/step - loss: 0.7212 - acc: 0.5969
Epoch 3/15
9/9 [==============================] - 8s 921ms/step - loss: 0.6612 - acc: 0.6592
Epoch 4/15
9/9 [==============================] - 8s 925ms/step - loss: 0.3135 - acc: 0.8481
Epoch 5/15
9/9 [==============================] - 8s 919ms/step - loss: 0.4640 - acc: 0.8530
Epoch 6/15
9/9 [==============================] - 8s 896ms/step - loss: 0.2306 - acc: 0.9231
Epoch 7/15
9/9 [==============================] - 8s 915ms/step - loss: 0.1464 - acc: 0.9396
Epoch 8/15
9/9 [==============================] - 8s 935ms/step - loss: 0.2663 - acc: 0.8919
Epoch 9/15
9/9 [==============================] - 8s 883ms/step - loss: 0.0772 - acc: 0.9698
Epoch 10/15
9/9 [==============================] - 9s 951ms/step - loss: 0.0403 - acc: 0.9805
Epoch 11/15
9/9 [==============================] - 8s 891ms/step - loss: 0.2618 - acc: 0.9075
Epoch 12/15
9/9 [==============================] - 8s 902ms/step - loss: 0.0434 - acc: 0.9873
Epoch 13/15
9/9 [==============================] - 8s 904ms/step - loss: 0.0187 - acc: 0.9932
Epoch 14/15
9/9 [==============================] - 9s 951ms/step - loss: 0.0974 - acc: 0.9649
Epoch 15/15
9/9 [==============================] - 8s 877ms/step - loss: 0.2859 - acc: 0.9338

9. Tester le modèle

Exécutez maintenant une prédiction à l'aide du modèle. Celui-ci vous permet de sélectionner un ou plusieurs fichiers à partir de votre système de fichiers. Il les importera et les exécutera ensuite dans le modèle pour indiquer s'il s'agit d'un cheval ou d'un être humain.

Vous pouvez télécharger des images depuis Internet dans votre système de fichiers pour les essayer ! Notez que le réseau peut faire de nombreuses erreurs malgré le fait que la précision de l'entraînement est supérieure à 99%.

Cela est dû à un phénomène surnommé surapprentissage, qui signifie que le réseau de neurones est entraîné avec des données très limitées (environ 500 images de chaque classe). Il est très utile pour reconnaître les images qui ressemblent à celles de l'ensemble d'entraînement, mais cela peut beaucoup échouer avec les images qui ne figurent pas dans cet ensemble.

Cela vous permet de prouver que plus vous entraînez, plus votre réseau final sera performant.

Il existe de nombreuses techniques permettant d'améliorer votre entraînement, malgré des données limitées, y compris une augmentation de l'image, mais cela dépasse le cadre de cet atelier de programmation.

import numpy as np
from google.colab import files
from keras.preprocessing import image
 
uploaded = files.upload()
 
for fn in uploaded.keys():
 
  # predicting images
  path = '/content/' + fn
  img = image.load_img(path, target_size=(300, 300))
  x = image.img_to_array(img)
  x = np.expand_dims(x, axis=0)
 
  images = np.vstack([x])
  classes = model.predict(images, batch_size=10)
  print(classes[0])
  if classes[0]>0.5:
    print(fn + " is a human")
  else:
    print(fn + " is a horse")

Par exemple, supposons que vous souhaitez effectuer un test avec cette image:

9e07a57ff3be7a82.jpeg

Voici ce que produit Colab:

77b678e70b00702a.png

Bien qu'il s'agisse d'une image de dessin animé, il est toujours bien classé.

L'image suivante classe également correctement:

C9213173d9f3d83c.jpeg

F2844DA737a1a2f2.png

Essayez vos propres images et explorez-les !

10. Visualiser les représentations intermédiaires

Pour avoir un aperçu des fonctionnalités que votre CNN a apprises, il suffit de visualiser la façon dont une entrée se transforme à mesure qu'elle passe par CNN.

Choisissez une image aléatoire dans l'ensemble d'entraînement, puis générez une figure où chaque ligne correspond à la sortie d'un calque et chaque image de la ligne est un filtre spécifique dans cette carte de caractéristiques de sortie. Réexécutez cette cellule pour générer des représentations intermédiaires pour différentes images d'entraînement.

import numpy as np
import random
from tensorflow.keras.preprocessing.image import img_to_array, load_img
 
# Let's define a new Model that will take an image as input, and will output
# intermediate representations for all layers in the previous model after
# the first.
successive_outputs = [layer.output for layer in model.layers[1:]]
#visualization_model = Model(img_input, successive_outputs)
visualization_model = tf.keras.models.Model(inputs = model.input, outputs = successive_outputs)
# Let's prepare a random input image from the training set.
horse_img_files = [os.path.join(train_horse_dir, f) for f in train_horse_names]
human_img_files = [os.path.join(train_human_dir, f) for f in train_human_names]
img_path = random.choice(horse_img_files + human_img_files)
 
img = load_img(img_path, target_size=(300, 300))  # this is a PIL image
x = img_to_array(img)  # Numpy array with shape (150, 150, 3)
x = x.reshape((1,) + x.shape)  # Numpy array with shape (1, 150, 150, 3)
 
# Rescale by 1/255
x /= 255
 
# Let's run our image through our network, thus obtaining all
# intermediate representations for this image.
successive_feature_maps = visualization_model.predict(x)
 
# These are the names of the layers, so can have them as part of our plot
layer_names = [layer.name for layer in model.layers]
 
# Now let's display our representations
for layer_name, feature_map in zip(layer_names, successive_feature_maps):
  if len(feature_map.shape) == 4:
    # Just do this for the conv / maxpool layers, not the fully-connected layers
    n_features = feature_map.shape[-1]  # number of features in feature map
    # The feature map has shape (1, size, size, n_features)
    size = feature_map.shape[1]
    # We will tile our images in this matrix
    display_grid = np.zeros((size, size * n_features))
    for i in range(n_features):
      # Postprocess the feature to make it visually palatable
      x = feature_map[0, :, :, i]
      x -= x.mean()
      if x.std()>0:
        x /= x.std()
      x *= 64
      x += 128
      x = np.clip(x, 0, 255).astype('uint8')
      # We'll tile each filter into this big horizontal grid
      display_grid[:, i * size : (i + 1) * size] = x
    # Display the grid
    scale = 20. / n_features
    plt.figure(figsize=(scale * n_features, scale))
    plt.title(layer_name)
    plt.grid(False)
    plt.imshow(display_grid, aspect='auto', cmap='viridis')

Voici des exemples de résultats:

e078d1bc9662c92f.png

Comme vous pouvez le voir, vous passez des pixels bruts des images à des représentations de plus en plus abstraites et compactes. Les représentations en aval commencent à mettre en évidence ce qui intéresse le réseau, et elles indiquent de moins en moins de fonctionnalités activées. La plupart sont définies sur zéro. C'est ce qu'on appelle la parité. La parcimonie de la représentation est une fonctionnalité essentielle du deep learning.

Ces représentations contiennent de moins en moins d'informations sur les pixels d'origine de l'image, mais de plus en plus de détails sur la classe de l'image. Un réseau de neurones convolutif (ou un réseau profond en général) est en quelque sorte un pipeline de distillation d'information.

11. Félicitations

Vous avez appris à utiliser les réseaux de neurones convolutifs pour améliorer les images complexes. Pour savoir comment améliorer vos modèles de vision par ordinateur, consultez Utiliser les réseaux de neurones convolutifs (CNN) avec des ensembles de données volumineux pour éviter le surapprentissage.