Introduction : réseaux de neurones FCNN et CNN
Dans les TDs précédents, nous avons travaillé sur des applications de clasification.
Dans les TD1 et TD2, nous avons classifié des données 2D en 2 classes par la reconstruction d’une fonction
\(f: R^2 \rightarrow [0,1]\)
Dans le TD3, nous avons classifiée des données en 28*28=784 dimensions en 10 classes, par exemple pour classifier des images de chiffres manuscrits, par la reconstruction d’une fonction
\(f: R^{784} \rightarrow R^{10}\)
Dans ce TD, nous allons travailler sur une nouvelle architecture de réseaux de neurones profonds afin d’ouvrir le champs d’application au-delà de la classification : l’IA générative, la réduction de dimension, le débruitage, et bien d’autres.
Cette nouvelle architecture de réseaux de neurones profonds que nous allons traiter s’appelle les réseaux de neurones Autoencodeurs (AE) ou Autoencoders en anglais.
Ces réseaux de neurones sont un cas spécifique des architectures encodeur - décodeurs, qui encodent l’entrée en un espace à dimensions réduites appelé espace latent, et qui ensuite décodent depuis cet espace vers un espace de dimension supérieur.
Dans le cas des Auoencodeurs, le nombre de dimensions l’espace d’entrée et de sortie sont égales. Et au délà, pour quelques exemples, mêmes les données en entrée correspondent aux données de sortie.

Les Autoencodeurs, ce sont des réseaux ne neurones
- avec apprentissage auto-supervisé (self-supervised learning).
Les couches dans ces réseaux de neurones peuvent être pleinement connectées (fully connected), et impliquer des couches convolutionnelles (convolutional layers), entre autres.
Les Autoencodeurs reconstruisent donc une fonction \[ f(X) = X \]
avec \(X \in R^m\), en passant par un espace latent \(Z \in R^n\) avec \(m>>n\). La fonction f(X) est composée de deux fonctions :
- une fonction d’encodage \(E: R^{m} \rightarrow Z\), et
- une fonction de décodage \(D: Z \rightarrow R^{m}\).
et donc \[ f(X) = D(E(X)) \]
Comme nous allons le voir dans ce TD, les Autoencodeurs sont particulièrement performants car ils sont capables réduire le nombre de dimensions de données tout en gardant des caractéristiques clées des données, ce qui compacte les données dans des données dans l’espace latent, ce qui peut servir de modifier l’espace latent pour en créer de nouvelles données.
Dans ce TD, nous allons (de nouveau) travailler sur le jeu de données de MNIST, mais non pas pour la classification, mais pour la réduction de dimensions, la génération de données, le débruitage, et pour comprendre ce qui est cet espace latent.
Le premier objectif de ce TD est de comprendre le principe des architectures encodeurs-décodeurs, à l’exemple des Autoencodeurs.
Le deuxième objectif est de comprendre le concept de l’espace latent (parfois appelé bottleneck).
Le troisième objectif est d’implémenter des Autoencodeurs dans un notebook Jupyterlab python, en utilisant les bibliothèques vu dans le dernier TD (Tensorflow keras, Matplotlib, Numpy et Pandas), de voir des exemples d’applications, et de “naviguer” dans l’espace latent.
Contrairement aux deux derniers TDs où les numéros d’exercices étaient organisés strictement pour faire comprendre les différents étapes dans le cycle de vie du machine learning, dans ce TD, il y aura 4 exercices (en sus de la préparation de l’environnement ainsi que de la collection et préparation de données) :
- Un modèle d’autoencodeur profond pleinement connecté
- Utiliser cet autoencodeur pour le débruitage
- Explorer l’espace latent
- Un autoencodeur convolutionnel
Exercice 4.0.A : Préparation de l’environnement JupyterHub
Nous allons maintenant travailler sur le JupyterHub comme dans les derniers TDs. Avec votre navigateur préféré, ouvrez l’URL suivant et authentifiez-vous :
https://jupyterhub.emi.u-bordeaux.fr/
Sur votre espace, vous avez déjà créée un dossier s’appelant deeplearning. Dans ce dossier, créez un sous-dossier td4. Dans ce sous-dossier, créez un nouveau notebook sous le nom td4-autoencodeurs.ipynb.
Nous allons importer maintenant toutes les bibliothèques dont on aura besoin pour ce TD. Pour cela, créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
# calcul numérique et analyse de données
import numpy as np
import pandas as pd
# plotting
import matplotlib.pyplot as plt
# tensorflow keras
import tensorflow as tf
from tensorflow import keras
from keras import layers
from keras.layers import Dense
from keras.layers import Conv2D
from keras.models import Sequential
from keras.optimizers import AdamVérifiez qu’il n’y ait pas d’erreurs, sinon faites signe au plus vite.
Exercice 4.0.B : Collecter et préparer les données
Créez une nouvelle cellule Markdown dans votre notebook, et ajouter le titre suivant, et exécutez la cellule :
# 1. Collecter et préparer les données
Dans ce TD, nous allons charger le jeu de données MNIST sans les étiquettées, qui est séparé en données d’entrainement et de données de test.
Pour cela, créez une nouvelle cellule Code dans votre notebook, ajouter le code suivant et exécutez la cellule :
# Lire des données de la bibliothèque keras
(X_train, _), (X_test, _) = keras.datasets.mnist.load_data()
# Montrer le format de données
print("Training Images:", X_train.shape)
print("Testing Images:", X_test.shape)Notez encore une fois que nous chargeons uniquement les données \(X\), et non pas étiquettes des données \(Y\) (contrairement à la classification de données du TD précédent) !
Nous allons maintenant normaliser et aplatir les données de forme \((28,28)\) à un vecteur de forme \((784)\)
# Nettoyer et transformer les données
X_train_normalized = X_train.astype('float32') / 255
X_test_normalized = X_test.astype('float32') / 255
# applatir les données (de 28x28 à 784)
X_train_normalized_flattened = X_train_normalized.reshape((60000, 784))
X_test_normalized_flattened = X_test_normalized.reshape((10000, 784))Jetons un coup d’oeil sur une donnée d’entraînement pour se rappeler avec la bibliothèque Matplotlib :
# Explorer les données
no_donnee_entrainement = 0
plt.imshow(X_train_normalized[no_donnee_entrainement], cmap="gray")Par rapport au TD précédent, nous avons changé la fonction de transfert pour la couleur (cmap), pour montrer la données en niveau de gris (gray).
- Pour les autoencodeurs, pourquoi avons-nous pas besoin de données \(Y\) (qui étaient les étiquettes dans les 3 derniers TD) ?
1 Autoencodeur profond pleinement connecté
Exercice 4.1 : Un premier autoencodeur
Pour la lisibilité de votre notebook, créez une nouvelle cellule Markdown et ajoutez le titre suivant, et exécutez la cellule :
# 1. Autoencodeur profond pleinement connecté
Nous allons créer trois cellules Code dans votre notebook, une première pour le encodeur, une deuxième pour le décodeur, et une troisième pour le autoencodeur (qui est composé des deux premiers) :
Créez donc une cellule Code pour le encodeur :
# Partie encodeur : créer un modèle sequentiel
model_encoder_deep = Sequential()
model_encoder_deep.add(layers.Dense(units=256, activation='relu', input_shape=(28*28,),name="layer1"))
model_encoder_deep.add(layers.Dense(units=128, activation='relu',name="layer2"))
model_encoder_deep.add(layers.Dense(units=32, activation='relu',name="layer3"))
model_encoder_deep.summary()Et une cellule Code pour le décodeur :
# Partie décodeur : créer un modèle sequentiel
model_decoder_deep = Sequential()
model_decoder_deep.add(layers.Dense(units=128, activation='relu', input_shape=(32,),name="layer4"))
model_decoder_deep.add(layers.Dense(units=256, activation='relu', name="layer5"))
model_decoder_deep.add(layers.Dense(units=28*28, activation='sigmoid', name="layer6"))
model_decoder_deep.summary()Ainsi qu’une cellule Code pour le Autoencodeur :
#Autoencodeur profond : modèle sequentiel composé du encodeur et décodeur
model_autoencoder_deep = Sequential([model_encoder_deep, model_decoder_deep])
# Optimisation Adam avec un taux d'apprentissage spécifique
custom_optimizer = Adam(learning_rate=0.001)
# Compiler le réseau avec une fonction de perte d'entropie croisée et taux de prédictions correctes
model_autoencoder_deep.compile(optimizer=custom_optimizer, loss='binary_crossentropy')
model_autoencoder_deep.summary()Exécutez les trois cellules.
Combien de couches à cet Autoencodeur (couches cachées + couche de sortie) ?
Combien de dimensions a l’espace latent (aussi appelé bottleneck ?
Combien de paramètres a la partie encodeur ?
Combien de paramètresa la partie décodeur ?
Combien de paramètres a cet autoencodeur au total ?
Nous allons maintenant entrainer l’autoencodeur. Pour cela, créez une cellule Code comme suit :
# Entrainer un modèle
history_autoencoder_deep = model_autoencoder_deep.fit(X_train_normalized_flattened, X_train_normalized_flattened, epochs=20, validation_split=0.375, batch_size=256)Notez la spécification de X_train_normalized_flattened pour l’entrée et la sortie.
- Analysez la sortie de votre cellule. Quelle est la perte des données de validation après la dernière époque (val loss) ?
Comme d’habitude, nous pouvons maintenant regarder les courbes d’apprentissage. En cas de besoin pour d’autres entrainements, nous définissions une fonction que nous allons appeler directement, pour qu’on puisse la réutiliser après. Créez une cellule Code comme suit :
# Evaluer l'apprentissage d'un modèle
def montrerCourbesApprentissage(history):
#list all data in history
print(history.history.keys())
plt.plot(history.history['loss'], label="Training loss")
plt.plot(history.history['val_loss'], label="Validation loss")
plt.title("Données d'entrâinement (FC)")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
montrerCourbesApprentissage(history_autoencoder_deep)Nous allons maintenant faire la prédiction de l’Autoencodeur sur toutes les données d’entraînements. Créez donc une cellule Code comme suit :
# Prédictions
predictions_donnees = model_autoencoder_deep.predict(X_train_normalized_flattened)Finalement, nous pouvons voir ce que l’autoencodeur nous prédit pour une donnée spécifique. Créer donc une cellule code comme suit :
# Montrer une prédiction
plt.imshow(predictions_donnees[no_donnee_entrainement].reshape(28,28), cmap="gray")Afin de pouvoir comparer plusieurs données d’entrée avec leurs sorties correspondantes, nous allons définir et utiliser la fonction suivante :
def montrerExemples(model, X, titre=""):
print(model.input_shape[1:])
for i in range(5):
print(X[i].shape)
plt.subplot(2,5,i+1)
plt.imshow(X[i].reshape(28,28), cmap="gray")
if (i==0):
plt.title("Données d'entrée (X)")
pred = model.predict(np.expand_dims(X[i], axis=0))
plt.subplot(2,5,i+5+1)
plt.imshow(pred.reshape(28,28), cmap="gray")
if (i==0):
plt.title("Sorties prédites (f(X))")
plt.suptitle(titre)
montrerExemples(model_autoencoder_deep, X_test_normalized_flattened, "Autoencoder (deep)")- Dans ces exemples, est-ce que les données de sortie prédites correspondent exactement aux données d’entrées ?
2 Entrainer l’autoencodeur pour le débruitage
Par la suite, nous allons maintenant utiliser le même autoencodeur pour le débruitage, c’est à dire pour enlever du “bruit” du signal des images. L’idée est que nous allons entrainer l’autoencodeur avec du bruit à l’entrée, et les images sans bruit à la sortie :
Exercice 4.2 : Débruitage
Dans un premier temps, nous allons ajouter du bruit dans les données. Créez donc une cellule Code :
noise_factor = 0.5
X_train_normalized_flattened_noisy = X_train_normalized_flattened + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=X_train_normalized_flattened.shape)
X_test_normalized_flattened_noisy = X_test_normalized_flattened + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=X_test_normalized_flattened.shape)
X_train_normalized_flattened_noisy = np.clip(X_train_normalized_flattened_noisy, 0., 1.)
X_test_normalized_flattened_noisy = np.clip(X_test_normalized_flattened_noisy, 0., 1.)Et nous allons explorer notre donnée bruitée :
plt.imshow(X_train_normalized_flattened_noisy[no_donnee_entrainement].reshape(28,28), cmap="gray")Nous allons maintenant entrainer une copie de l’autoencodeur pour le débruitage. Pour cela, créez une cellule Code comme suit :
model_autoencoder_deep_debruitage = model_autoencoder_deep
history_autoencoder_deep_debruitage = model_autoencoder_deep_debruitage.fit(X_train_normalized_flattened_noisy, X_train_normalized_flattened, epochs=20, validation_split=0.375, batch_size=256)Notez que cette fois-ci, la spécification de X_train_normalized_flattened_noisy pour l’entrée et X_train_normalized_flattened pour la sortie.
Nous pouvons maintenant étudier le débruitage sur quelques exemples :
montrerExemples(model_autoencoder_deep_debruitage, X_test_normalized_flattened_noisy)Est-ce que le bruit a disparu ?
Est-ce que la reconstruction des données est aussi bien que quand il n’y avait pas de bruit ?
- Un autoencodeur convolutionnel
3 Explorer l’espace latent
Nous allons maintenant explorer l’espace latent.
Exercice 4.3 : Explorer l’espace latent
Nous allons créer trois cellules Code pour définir un nouveau Autoencodeur qui n’a seulement 2 dimensions en espace latent. Comme cela, nous allons pouvoir explorer cet espace.
Créez donc une cellule Code pour le encodeur :
# Partie encodeur : créer un modèle sequentiel
model_encoder_deep_latent2d = Sequential()
model_encoder_deep_latent2d.add(layers.Dense(units=256, activation='relu', input_shape=(28*28,),name="layer1"))
model_encoder_deep_latent2d.add(layers.Dense(units=128, activation='relu',name="layer2"))
model_encoder_deep_latent2d.add(layers.Dense(units=64, activation='relu',name="layer3"))
model_encoder_deep_latent2d.add(layers.Dense(units=2, activation='relu',name="layer4"))
model_encoder_deep_latent2d.summary()Et une cellule Code pour le décodeur :
# Partie décodeur : créer un modèle sequentiel
model_decoder_deep_latent2d = Sequential()
model_decoder_deep_latent2d.add(layers.Dense(units=64, activation='relu', input_shape=(2,),name="layer4"))
model_decoder_deep_latent2d.add(layers.Dense(units=128, activation='relu', name="layer5"))
model_decoder_deep_latent2d.add(layers.Dense(units=256, activation='relu', name="layer5c"))
model_decoder_deep_latent2d.add(layers.Dense(units=28*28, activation='sigmoid', name="layer6"))
model_decoder_deep_latent2d.summary()Ainsi qu’une cellule Code pour le Autoencodeur :
#Autoencodeur profond : modèle sequentiel composé du encodeur et décodeur
model_autoencoder_deep_latent2d = Sequential([model_encoder_deep_latent2d, model_decoder_deep_latent2d])
# Optimisation Adam avec un taux d'apprentissage spécifique
custom_optimizer = Adam(learning_rate=0.001)
# Compiler le réseau avec une fonction de perte d'entropie croisée et taux de prédictions correctes
model_autoencoder_deep_latent2d.compile(optimizer=custom_optimizer, loss='binary_crossentropy')
#model_autoencoder.compile(optimizer=custom_optimizer, loss='mean_squared_error', metrics=['accuracy'])
model_autoencoder_deep_latent2d.summary()Exécutez les trois cellules.
- Est-ce que cet autoencodeur a plus de paramètres que l’autoencodeur précédent ?
Nous allons maintenant entrainer l’autoencodeur. Pour cela, créez une cellule Code comme suit :
# Entrainer un modèle
history_autoencoder_deep_latent2d = model_autoencoder_deep_latent2d.fit(X_train_normalized_flattened, X_train_normalized_flattened,
epochs=20,
validation_split=0.375, batch_size=256)et montrer quelques exemples :
montrerExemples(model_autoencoder_deep_latent2d, X_test_normalized_flattened, "Autoencodeur avec espace latent 2D")Nous allons maintenant prédire la localisation dans l’espace latent de données, et délimiter cet espace :
x_train_encoded = model_encoder_deep_latent2d.predict(X_train_normalized_flattened, batch_size=128)
print(min(x_train_encoded[:, 1],))
print(max(x_train_encoded[:, 1],))
print(min(x_train_encoded[:, 0]))
print(max(x_train_encoded[:, 0]))et nous afficher les données :
plt.figure(figsize=(6, 6))
plt.scatter(x_train_encoded[:, 0], x_train_encoded[:, 1])
plt.show()La répartition de données dans l’espace latent peut paraître étrange, mais tout deviendra plus clair lorsque nous allons charger les étiquettes des données.
# charger les étiquettes
(_, Y_train), (_, Y_test) = keras.datasets.mnist.load_data()
print("Training Labels:", Y_train.shape)
print("Test Labels:", Y_test.shape)Nous pouvons maintenant colorer les données selon leurs étiquettes :
plt.figure(figsize=(6, 6))
plt.scatter(x_train_encoded[:, 0], x_train_encoded[:, 1], c=Y_train, cmap=plt.get_cmap('rainbow', 10))
plt.colorbar()
plt.show()Voici la création de quelques chiffres depuis cet espace latent, qui n’étaient même pas dans les jeux de données de départ :
# Exemple de
# Display a 2D manifold of the digits
n = 15 # figure with 15x15 digits
digit_size = 28
figure = np.zeros((digit_size * n, digit_size * n))
grid_x = np.linspace(0, 50, n)
grid_y = np.linspace(0, 50, n)
for i, yi in enumerate(reversed(grid_x)):
for j, xi in enumerate(grid_y):
z_sample = np.array([[xi, yi]])
x_decoded = model_decoder_deep_latent2d.predict(z_sample)
digit = x_decoded[0].reshape(digit_size, digit_size)
figure[i * digit_size: (i + 1) * digit_size,
j * digit_size: (j + 1) * digit_size] = digit
plt.figure(figsize=(10, 10))
plt.imshow(figure)
plt.show()4 Un autoencodeur convolutionnel
Nous allons maintenant définir un autoencoder convolutionnel.
Exercice 4.4 : Un autoencodeur convolutionnel
Nous allons créer trois cellules Code pour définir un nouveau Autoencodeur, cette fois-ci un autoencodeur convolutionnel.
Créez donc une cellule Code pour le encodeur :
# Partie encodeur : créer un modèle sequentiel
model_encoder_deep_convolutional = Sequential()
model_encoder_deep_convolutional.add(layers.Conv2D(16, (3, 3), activation='relu', padding='same', input_shape=(28, 28, 1)))
model_encoder_deep_convolutional.add(layers.MaxPooling2D((2, 2), padding='same'))
model_encoder_deep_convolutional.add(layers.Conv2D(8, (3, 3), activation='relu', padding='same'))
model_encoder_deep_convolutional.add(layers.MaxPooling2D((2, 2), padding='same'))
model_encoder_deep_convolutional.add(layers.Conv2D(8, (3, 3), activation='relu', padding='same'))
model_encoder_deep_convolutional.add(layers.MaxPooling2D((2, 2), padding='same'))
model_encoder_deep_convolutional.summary()Et une cellule Code pour le décodeur :
# Partie décodeur : créer un modèle sequentiel
model_decoder_deep_convolutional = Sequential()
model_decoder_deep_convolutional.add(layers.Conv2D(8, (3, 3), activation='relu', padding='same', input_shape=(4,4,8)))
model_decoder_deep_convolutional.add(layers.UpSampling2D((2, 2)))
model_decoder_deep_convolutional.add(layers.Conv2D(8, (3, 3), activation='relu', padding='same'))
model_decoder_deep_convolutional.add(layers.UpSampling2D((2, 2)))
model_decoder_deep_convolutional.add(layers.Conv2D(16, (3, 3), activation='relu'))
model_decoder_deep_convolutional.add(layers.UpSampling2D((2, 2)))
model_decoder_deep_convolutional.add(layers.Conv2D(1, (3, 3), activation='sigmoid', padding='same'))
model_decoder_deep_convolutional.summary()Ainsi qu’une cellule Code pour le Autoencodeur :
#Autoencodeur profond : modèle sequentiel composé du encodeur et décodeur
model_autoencoder_deep_convolutional = Sequential([model_encoder_deep_convolutional, model_decoder_deep_convolutional])
custom_optimizer = Adam(learning_rate=0.001)
# Compiler le réseau avec une fonction de perte d'entropie croisée et taux de prédictions correctes
model_autoencoder_deep_convolutional.compile(optimizer=custom_optimizer, loss='binary_crossentropy')
#model_autoencoder_deep_convolutional.build((28, 28, 1,1))
model_autoencoder_deep_convolutional.summary()- Combien de dimensions a l’espace latent ?
Créez deux cellules code pour entrainer cet autoencodeur, et pour montrer quelques exemples de prédiction.
5 Compétition
Exercice 4.5 : Compétition de modèle autoencodeur
Nous souhaitons trouver une architecture de réseau pour réduire la perte de données de validation (val_loss) pour un autoencodeur avec un espace latent de seulement 2 dimensions, comme dans l’exercice 4.3.
Définissez différentes architectures, et entrainez le réseau. La limite de nombre d’epochs est de 50.
6 Pour aller plus loin
Exercice optionnel 6.1 :
Dans le TD 4.1, nous avions un espace latent à plus que 2 dimensions. Il serait très intéressant de faire des prédictions de deux données en utilisant l’encodeur (par exemple une image qui ressemble à un ‘1’ et une image qui ressemble à un ‘7’), de faire une interpolation dans cet espace latent (p.ex. 10 données intermédiaires), et d’utiliser le décodeur pour faire des prédictions. Ceci générera une interpolation entre les deux chiffres, mais cette fois-ci dans un espace latent a plus de dimensions que pour l’exercice 4.3 où l’espace latent n’avait que 2 dimensions.
Exercice optionnel 6.2 :
Dans le TD 4.4, nous avions défini un autoencodeurs un espace latent à plus que 2 dimensions. Il serait très intéressant de faire des prédictions de deux données en utilisant l’encodeur (par exemple une image qui ressemble à un ‘1’ et une image qui ressemble à un ‘7’), de faire une interpolation dans cet espace latent (p.ex. 10 données intermédiaires), et d’utiliser le décodeur pour faire des prédictions. Ceci générera une interpolation entre les deux chiffres, mais cette fois-ci dans un espace latent a plus de dimensions que pour l’exercice 4.3 où l’espace latent n’avait que 2 dimensions.
7 TD à rendre
A rendre sur moodle avant le 24 Novembre 2025 à 23h59. Pour cela, il y a des liens sur la page du cours (https://moodle.u-bordeaux.fr/course/view.php?id=22721) :