Travaux pratiques - Modèles de segmentation¶
L’objectif de cette séance de trvaux pratiques est de mettre en œuvre un réseau de neurones convolutif profond pour la segmentation sémantique des images. Nous verrons comment adapter les CNN classiques à cette tâche de prédiction dense et localisée, sur un jeu de données annoté de photos d’animaux.
# Téléchargement du jeu de données Oxford-IIT-Pets en version réduite
!wget -nc https://cedric.cnam.fr/vertigo/Cours/ml2/oxford-iit-pets-small-128.tar.xz
# Décompression du jeu de données
!tar -xf oxford-iit-pets-small-128.tar.xz && rm oxford-iit-pets-small-128.tar.xz # on supprime l'archive
Chargement du jeu de données Oxford-IIT-Pets-small¶
Pour cette séance, nous allons commencer par illustrer la mise en forme d’un jeu de données avec TensorFlow/Keras. En effet, jusqu’à présent, nous avons surtout utilisé des jeux de données préintégrés à la bibliothèque.
Question
Observez la structure du dossier oxford-iit-pets-small/
.
Comment est-il organisé ? Où se trouvent les images ? Où se trouvent les
étiquettes ?
import tensorflow as tf
Nous allons créer un jeu de données
TensorFlow
permettant de lire les images. Il s’agit d’une instance de la classe
tf.data.Dataset
, qui implémente l’interface des générateurs Python.
Pour ce faire, nous pouvons commencer par lire le fichier
trainval.txt
qui contient la liste des fichiers à utiliser pour
l’apprentissage.
# Créé un Dataset TensorFlow à partir de la liste des exemples d'apprentissage
filenames = tf.data.TextLineDataset("oxford-iit-pets-small-128/trainval.txt")
# Extraire seulement les noms de fichier
filenames = filenames.map(lambda fname: tf.strings.split(fname)[0])
# Transforme les noms des exemples en chemins vers les fichiers JPEG
images = filenames.map(lambda fname: tf.strings.join(["oxford-iit-pets-small-128/images/", fname, ".jpg"]))
print(images)
On peut observer que la variable images
est un objet de type
MapDataset
qui renvoie des chaînes de caractère (tf.string
).
Chacune de ces chaînes correspond au chemin vers un fichier au format
JPEG (une image du jeu de données). Nous pouvons maintenant appliquer la
méthode map
pour lire le contenu du fichier au format binaire
(fonction tf.io.read_file
) puis décoder l’image
(tf.io.decode_image
).
# Lecture des fichiers JPEG et décodage en matrice, puis normalisation entre 0 et 1
def set_im_shape(t):
t.set_shape((128, 128, 3))
return t
def set_l_shape(t):
t.set_shape((128, 128))
return t
images = images.map(tf.io.read_file).map(lambda f: tf.io.decode_image(f, channels=3)).map(lambda im: tf.cast(im, "float32") / 255.).map(set_im_shape)
print(images)
Le jeu de données images
renvoie bien maintenant une collection
MapDataset
. Chaque élément du jeu de données est une image de
dimensions (128, 128, 3) et de type float32
.
Nous pouvons appliquer la même séquence d’opérations pour créer la collection qui contiendra les masques de segmentation des images. Ces masques sont des images, contenant pour chaque pixel un entier encodant la classe associée au pixel: 0 pour le fond (background), 1 pour un animal, 2 pour une bordure.
# Transforme les noms des exemples en chemins vers les fichiers PNG (masques de segmentation)
masks = filenames.map(lambda fname: tf.strings.join(["oxford-iit-pets-small-128/annotations/", fname, ".png"]))
# Lecture des fichiers PNG et décodage en matrice, on soustrait 1 pour que les classes numérotées [1,2,3] deviennent [0,1,2]
masks = masks.map(tf.io.read_file).map(tf.io.decode_image).map(lambda m: (m-1)[:,:,0]).map(set_l_shape)
Question
Quelles sont les dimensions et le type des éléments renvoyés
par le jeu de données masks
?
Finalement, nous pouvons assembler les jeux de données ensemble à l’aide
de la fonction zip
pour créer une collection dataset
qui
renverra, pour chaque exemple, le couple (image, masque).
# Concatène les deux Dataset ensemble pour produire des paires (image, masque)
dataset = tf.data.Dataset.zip((images, masks))
for im, mask in dataset:
print(im)
print(mask)
break
Question
À l’aide de Matplotlib, afficher quelques exemples d’images et de cartes sémantiques associées. Quelle est la signification des trois classes 0, 1 et 2 ?
Implémentation d’un modèle de segmentation¶
Nous pouvons maintenant implémenter un modèle de segmentation. Il s’agit
d’un modèle entièrement convolutif, c’est-à-dire sans aucune couche
entièrement connectée (Dense
). Le modèle sera composé d’un bloc
encodeur qui réduit la dimension spatiale, et d’un bloc décodeur qui
reconstitue la dimension spatiale jusqu’à obtenir une sortie de mêmes
dimensions que l’image de départ. Le tenseur de sortie aura en revanche
\(K=3\) canaux (un canal par classe). Pour un pixel donné, la valeur
\(y[i,j]\) de la sortie sera un vecteur de trois éléments
représentant la probabilité (après softmax) d’appartenir à la classe 0,
à la classe 1 ou à la classe .
La fonction get_model
implémente une architecture similaire à celle
de U-Net. Contrairement aux modèles précédemment définis, cette fonction
utilise l’interface fonctionnel de
Keras qui permet de décrire
un modèle par les opérations appliquées sur un tenseur x
.
Question
Dans la fonction get_model
, à quoi correspond la variable
previous_block_activation
? Comment est-elle utilisée ? De quelle
type d’architecture s’agit-il ?
from tensorflow import keras
from tensorflow.keras import layers
def get_model(img_size, num_classes):
inputs = keras.Input(shape=img_size + (3,))
# Bloc d'entrée: stride 2 pour diminuer la résolution
x = layers.Conv2D(32, 3, strides=2, padding="same")(inputs)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
previous_block_activation = x # Stockage du résidu
# Encodeur
# 3 blocs: un bloc à 64 canaux, un bloc à 128 canaux, un bloc à 256 canaux
# Les 3 blocs utilisent la même structure: Convolution + Convolution + MaxPooling
for filters in [64, 128, 256]:
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(filters, 3, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(filters, 3, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling2D(3, strides=2, padding="same")(x)
# Réduction de la dimension du résidu
residual = layers.Conv2D(filters, 1, strides=2, padding="same")(previous_block_activation)
x = layers.add([x, residual]) # Ajout du résidu
previous_block_activation = x # Stockage du résidu pour le bloc suivant
# Décodeur
# Principe identique à l'encodeur mais dans le sens inverse
for filters in [256, 128, 64, 32]:
x = layers.Activation("relu")(x)
x = layers.Conv2DTranspose(filters, 3, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.Conv2DTranspose(filters, 3, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.UpSampling2D(2)(x)
# Augmentation de la dimension du résidu
residual = layers.UpSampling2D(2)(previous_block_activation)
residual = layers.Conv2D(filters, 1, padding="same")(residual)
x = layers.add([x, residual]) # Ajout du résidu
previous_block_activation = x # Stockage du résidu pour le bloc suivant
# Couche finale (convolution 2D 1x1 pour la classification)
outputs = layers.Conv2D(num_classes, 1, activation="softmax", padding="same")(x)
return keras.Model(inputs, outputs)
model = get_model((128, 128), 3)
model.summary()
Entraînement du modèle¶
Pour entraîner le modèle, nous devons spécifier avec TensorFlow la taille de batch ainsi que quelques paramètres sur le chargement des données :
train_batches = (
dataset
.cache() # stocke les données en cache (permet d'éviter des lectures trop nombreuses sur le disque)
.shuffle(1000) # mélange les données aléatoirement
.batch(8) # taille du batch
.prefetch(buffer_size=tf.data.AUTOTUNE)) # charge de façon asynchrone le batch suivant à chaque itération
test_batches = dataset.batch(16)
Question
Compléter le code ci-dessous pour compiler le modèle puis
lancer son apprentissage sur le jeu d’entraînement. On entraînera le
modèle sur cinq époques, en utilisant la SparseCategoricalCrossentropy
.
Pour les métriques, on affichera à la fois l”accuracy ainsi que l’intersection sur union.
Comme la classe numéro 2 correspond à la « bordure » de la segmentation, on souhaite ne pas apprendre cette classe. On peut désactiver son calcul dans l’entropie croisée en utilisant le bon paramètre. Voir la documentation de Keras à ce sujet.
# À compléter
iou = tf.keras.metrics.IoU(num_classes, target_class_ids=[0], sparse_y_pred=False)
loss = tf.keras.losses.SparseCategoricalCrossentropy(...)
model.compile(...)
Question
À l’aide de la méthode predict_on_batch
, calculer les
cartes sémantiques prédites pour quelques exemples du jeu de données de
test. Afficher ces prédictions à l’aide de Matplotlib.
Question
À l’aide de Keras, évaluer les performances de segmentation (accuracy et IoU) du modèle sur le jeu de test.