Prétraitement et évaluation des données avec Keras
Prétraitement des données
Le prétraitement des données est une étape essentielle en machine learning. Il consiste à transformer les données brutes en un format adapté à l'entraînement d'un modèle. Un bon prétraitement peut significativement améliorer les performances, la stabilité et la vitesse d'apprentissage d'un modèle.
Pourquoi le Prétraitement est-il Crucial ?
- Qualité des données : Les données du monde réel sont souvent bruitées, incomplètes ou incohérentes. Le prétraitement permet de nettoyer et de corriger ces imperfections.
- Compatibilité avec les modèles : Les algorithmes de machine learning ont souvent des exigences spécifiques concernant le format et l'échelle des données d'entrée. Le prétraitement permet de satisfaire ces exigences.
- Performance du modèle : Des données bien prétraitées conduisent généralement à de meilleures performances (exactitude, précision, etc.).
- Stabilité de l'entraînement : Un prétraitement approprié peut rendre l'entraînement plus stable et plus rapide.
- Interprétabilité : Certaines techniques de prétraitement peuvent rendre les données et les résultats du modèle plus faciles à interpréter.
Techniques de Prétraitement Courantes
Voici quelques-unes des techniques de prétraitement les plus courantes, avec des exemples et des indications sur leur utilisation :
1. Normalisation
- Principe : Mettre à l'échelle les données pour qu'elles aient une plage de valeurs commune (par exemple, entre 0 et 1, ou entre -1 et 1).
- Pourquoi ? :
- Empêche les variables ayant de grandes valeurs de dominer les calculs (en particulier dans les algorithmes basés sur la distance, comme les k-plus proches voisins, ou les algorithmes utilisant la descente de gradient).
- Accélère l'entraînement (la descente de gradient converge plus rapidement).
- Améliore la stabilité numérique.
- Méthodes :
-
Min-Max Scaling : Ramène les valeurs entre 0 et 1.
-
Standardisation (Z-score normalization) : Transforme les données pour qu'elles aient une moyenne de 0 et un écart-type de 1.
où est la moyenne et est l'écart-type.
-
- Quand l'utiliser ? :
- Presque toujours utile pour les réseaux de neurones (en particulier pour les couches d'entrée).
- Indispensable pour les algorithmes sensibles à l'échelle (SVM, k-NN, régression linéaire/logistique avec régularisation, etc.).
- Implémentation :
- Avec TensorFlow et Keras, vous pouvez utiliser les couches de prétraitement comme
tf.keras.layers.Normalization.
- Avec TensorFlow et Keras, vous pouvez utiliser les couches de prétraitement comme
2. Gestion des Valeurs Manquantes
- Principe : Traiter les valeurs manquantes dans les données (représentées par
NaNdans pandas, par exemple). - Pourquoi ? :
- La plupart des algorithmes de machine learning ne peuvent pas gérer les valeurs manquantes.
- Les valeurs manquantes peuvent contenir des informations importantes.
- Méthodes :
- Suppression : Supprimer les lignes ou les colonnes contenant des valeurs manquantes (à utiliser avec précaution, car cela peut entraîner une perte d'information).
- Imputation : Remplacer les valeurs manquantes par des valeurs estimées (par exemple, la moyenne, la médiane, la valeur la plus fréquente, ou une valeur prédite par un modèle).
- Quand l'utiliser ? : Dès que vous avez des valeurs manquantes dans vos données.
- Implémentation :
- Avec pandas:
dropna(),fillna().
3. Encodage des Variables Catégorielles
- Principe : Transformer les variables catégorielles (texte, catégories) en valeurs numériques.
- Pourquoi ? :
- La plupart des algorithmes de machine learning ne peuvent pas gérer directement les variables catégorielles.
- Méthodes :
- One-Hot Encoding : Crée une nouvelle colonne pour chaque catégorie, et met un 1 dans la colonne correspondant à la catégorie de l'exemple, et 0 ailleurs.
- Label Encoding : Attribue un entier unique à chaque catégorie (à utiliser avec précaution, car cela introduit une notion d'ordre qui n'existe peut-être pas).
- Autres méthodes : Target encoding, hashing, etc.
- Quand l'utiliser ? : Dès que vous avez des variables catégorielles dans vos données.
- Implémentation :
- Avec pandas :
get_dummies()(pour le one-hot encoding).
- Avec pandas :
4. Traitement des Valeurs Aberrantes (Outliers)
- Principe : Identifier et traiter les valeurs extrêmes qui peuvent perturber l'apprentissage du modèle.
- Pourquoi ? :
- Certains modèles (comme la régression linéaire) sont très sensibles aux valeurs aberrantes.
- Méthodes :
- Suppression : Supprimer les valeurs aberrantes (à utiliser avec précaution).
- Transformation : Transformer les données pour réduire l'impact des valeurs aberrantes (par exemple, utiliser une transformation logarithmique).
- Winsorisation/Troncature : Remplacer les valeurs extrêmes par des valeurs moins extrêmes (par exemple, les percentiles 1 et 99).
- Quand l'utiliser ? : Si vous suspectez la présence de valeurs aberrantes et que votre modèle y est sensible.
5. Réduction de Dimensionnalité
- Principe : Réduire le nombre de variables d'entrée tout en préservant autant d'information que possible.
- Pourquoi ?
- Simplifie le modèle.
- Réduit le risque de surapprentissage.
- Accélère l'entraînement.
- Peut améliorer les performances.
- Facilite la visualisation (si on réduit à 2 ou 3 dimensions).
- Méthodes :
- Analyse en Composantes Principales (PCA) : Technique linéaire très courante.
- t-SNE : Technique non linéaire, souvent utilisée pour la visualisation.
- Autres méthodes : Analyse Factorielle, Analyse Discriminante Linéaire (LDA), etc.
- Quand l'utiliser ? :
- Si vous avez beaucoup de variables d'entrée (haute dimensionnalité).
- Si vous suspectez que certaines variables sont redondantes ou peu informatives.
- Si vous voulez visualiser vos données en 2D ou 3D.
- Implémentation :
- Avec scikit-learn :
PCA,TSNE.
- Avec scikit-learn :
6. Création de Variables (Feature Engineering)
- Principe : Créer de nouvelles variables à partir des variables existantes, pour faciliter l'apprentissage du modèle.
- Pourquoi ? :
- Peut améliorer considérablement les performances du modèle.
- Permet d'introduire des connaissances expertes dans le modèle.
- Exemples :
- Créer des variables d'interaction (par exemple, le produit de deux variables).
- Combiner plusieurs variables (par exemple, calculer un ratio).
- Discrétiser des variables continues (par exemple, transformer l'âge en tranches d'âge).
- Extraire des informations de dates (jour de la semaine, mois, année, etc.).
- Quand l'utiliser ? :
- Toujours ! C'est souvent l'une des étapes les plus importantes pour améliorer les performances d'un modèle.
- Implémentation :
- Cela dépend du type de variable que vous voulez créer. Souvent, cela se fait avec des opérations pandas ou NumPy.
7. Traitement du Déséquilibre de Classes
- Principe : Ajuster le dataset d'apprentissage pour éviter le biais d'apprentissage.
- Pourquoi ? :
- Si une classe a beaucoup plus d'instances que d'autres, cela peut entraîner de mauvaises performances du modèle et donner une impression erronée de bonne performance.
- Méthodes :
- Sous-échantillonnage (undersampling) de la classe majoritaire
- Sur-échantillonnage (oversampling) de la classe minoritaire, par exemple avec l'algorithme SMOTE (Synthetic Minority Oversampling TEchnique)
- Utiliser des poids de classe pour pénaliser les erreurs.
- Quand l'utiliser ? :
- Lorsque les données sont déséquilibrées.
Le prétraitement des données est un domaine vaste et créatif. Il n'y a pas de recette unique. Les techniques à utiliser dépendent des données, du modèle et du problème que vous essayez de résoudre. L'expérimentation est essentielle !
Normalisation : illustration et importance
Reprenons la technique de normalisation et illustrons son importance avec un exemple concret et des visualisations.
Rappel : Qu'est-ce que la Normalisation ?
La normalisation consiste à mettre à l'échelle les variables numériques pour qu'elles aient une plage de valeurs commune. Les deux méthodes les plus courantes sont :
- Min-Max Scaling : Ramène toutes les valeurs entre 0 et 1.
- Standardisation (Z-score normalization) : Transforme les données pour qu'elles aient une moyenne de 0 et un écart-type de 1.
Pourquoi Normaliser ?
Imaginons un jeu de données très simple avec deux variables :
x1: Âge (en années), avec des valeurs entre 20 et 60.x2: Revenu annuel (en euros), avec des valeurs entre 10 000 et 100 000.
Si nous entraînons un modèle (par exemple, une régression linéaire ou un réseau de neurones) sur ces données sans normalisation, le modèle risque d'être dominé par la variable x2 (revenu), simplement parce que ses valeurs sont beaucoup plus grandes que celles de x1 (âge). Le modèle pourrait accorder une importance disproportionnée au revenu et négliger l'âge, ce qui conduirait à de mauvaises prédictions.
Visualisation (sans normalisation) :

Sur ce graphique, l'axe des revenus a une échelle beaucoup plus grande que l'axe des âges. Visuellement, il est difficile de voir une relation entre les deux variables.
Visualisation (après standardisation) :

Après standardisation, les deux variables ont la même échelle (moyenne 0, écart-type 1). On voit beaucoup mieux la relation entre les deux variables (qui est positive, dans cet exemple).
Impact sur la descente de gradient :
Sans normalisation, la fonction de perte aura une forme très "allongée" dans la direction de la variable ayant les plus grandes valeurs. La descente de gradient risque d'osciller beaucoup et de converger lentement.
Après normalisation, la fonction de perte aura une forme plus "ronde", ce qui facilite la descente de gradient et accélère la convergence.
Voici une illustration conceptuelle :

Il n'y a pas une unique méthode de normalisation, dans l'exemple ci-dessus, on a utilisé la standardisation, mais on aurait pu utiliser une autre méthode, par exemple, le Min-Max Scaling.
Normalisation : Illustration de l'Impact sur les Calculs
Reprenons l'exemple de la normalisation pour illustrer concrètement comment des variables avec des échelles différentes peuvent affecter les calculs d'un modèle, en particulier lors de la descente de gradient.
Données Non Normalisées
Reprenons le jeu de données très simple avec deux variables :
x1: Âge (en années), avec des valeurs entre 20 et 60.x2: Revenu annuel (en euros), avec des valeurs entre 10 000 et 100 000.
Modèle Simple (Régression Linéaire)
Supposons que nous voulions entraîner un modèle de régression linéaire très simple pour prédire une variable cible y à partir de x1 et x2 :
Initialisons les poids et à 0.1, et le biais à 0, et calculons la prédiction pour la première donnée (âge = 25, revenu = 20000) :
# Paramètres initiaux
w1 = 0.1
w2 = 0.1
b = 0
# Prédiction pour la première donnée (x1=25, x2=20000)
x1 = 25
x2 = 20000
y_pred = w1 * x1 + w2 * x2 + b
print(f"Prédiction (non normalisé) : {y_pred}")
Vous obtenez une prédiction de 2002.5.
Maintenant, calculons manuellement l'impact d'une petite variation de chaque poids sur la prédiction :
# Impact d'une petite variation de w1 (par exemple, +0.01)
delta_w1 = 0.01
y_pred_w1_plus = (w1 + delta_w1) * x1 + w2 * x2 + b
difference_w1 = y_pred_w1_plus - y_pred
print(f"Différence due à la variation de w1 : {difference_w1}")
# Impact d'une petite variation de w2 (par exemple, +0.01)
delta_w2 = 0.01
y_pred_w2_plus = w1 * x1 + (w2 + delta_w2) * x2 + b
difference_w2 = y_pred_w2_plus - y_pred
print(f"Différence due à la variation de w2 : {difference_w2}")
Vous constatez :
- Une variation de 0.01 sur
w1change la prédiction de 0.25. - Une variation de 0.01 sur
w2change la prédiction de 200 !
La variation de w2 a un impact 800 fois plus important sur la prédiction que la même variation de w1, simplement parce que la variable x2 (revenu) a des valeurs beaucoup plus grandes que x1 (âge).
Conséquence sur la Descente de Gradient
Lors de la descente de gradient, les mises à jour des poids sont proportionnelles au gradient de la fonction de perte. Si le gradient par rapport à w2 est beaucoup plus grand que le gradient par rapport à w1 (à cause de l'échelle des variables), la descente de gradient aura tendance à ajuster beaucoup plus fortement w2 que w1. Cela peut :
- Ralentir l'apprentissage : Le modèle va se concentrer sur l'ajustement de
w2et négligerw1, alors que les deux variables peuvent être importantes. - Créer des oscillations : La descente de gradient peut "zigzaguer" dans l'espace des paramètres, en sur-corrigeant alternativement
w2etw1. - Empêcher la convergence : Dans les cas extrêmes, le modèle peut ne pas converger du tout.
Données Standardisées
Maintenant, standardisons les données :
# Standardisation
age_std = (age - np.mean(age)) / np.std(age)
revenu_std = (revenu - np.mean(revenu)) / np.std(revenu)
X_normalise = np.stack([age_std, revenu_std], axis=-1)
print(X_normalise)
Répétons les calculs précédents avec les données standardisées :
# Prédiction pour la première donnée standardisée (x1_std, x2_std)
x1_std = age_std[0]
x2_std = revenu_std[0]
y_pred_std = w1 * x1_std + w2 * x2_std + b
print(f"Prédiction (standardisé) : {y_pred_std}")
# Impact d'une petite variation de w1
y_pred_w1_plus_std = (w1 + delta_w1) * x1_std + w2 * x2_std + b
difference_w1_std = y_pred_w1_plus_std - y_pred_std
print(f"Différence due à la variation de w1 (standardisé) : {difference_w1_std}")
# Impact d'une petite variation de w2
y_pred_w2_plus_std = w1 * x1_std + (w2 + delta_w2) * x2_std + b
difference_w2_std = y_pred_w2_plus_std - y_pred_std
print(f"Différence due à la variation de w2 (standardisé) : {difference_w2_std}")
Vous constaterez que :
- La prédiction initiale (
y_pred_std) est différente (car les données sont différentes). - Mais surtout, l'impact d'une variation de
w1et l'impact d'une variation dew2sont maintenant du même ordre de grandeur (-0.14 et -0.13, respectivement, dans cet exemple).
Conclusion
La normalisation (ici, la standardisation) a permis d'équilibrer l'influence des deux variables sur la prédiction. La descente de gradient pourra ajuster les deux poids de manière plus équilibrée, ce qui conduira à un apprentissage plus rapide, plus stable et potentiellement plus performant.
C'est pourquoi la normalisation est une étape de prétraitement cruciale dans de nombreux problèmes de machine learning, en particulier lorsque les variables d'entrée ont des échelles très différentes.
Normalisation en Pratique (TensorFlow/Keras)
Avec TensorFlow et Keras, vous pouvez utiliser la couche tf.keras.layers.Normalization pour normaliser vos données :
import tensorflow as tf
# Création de la couche de normalisation
norm_layer = tf.keras.layers.Normalization(axis=-1) # axis=-1 : Normalise chaque caractéristique indépendamment
# Adaptation de la couche aux données (calcul de la moyenne et de l'écart-type)
norm_layer.adapt(data) # 'data' doit être un tableau NumPy ou un tf.data.Dataset
# Utilisation de la couche dans un modèle
model = tf.keras.Sequential([
norm_layer, # Couche de normalisation
tf.keras.layers.Dense(units=64, activation='relu'),
tf.keras.layers.Dense(units=1)
])
L'avantage d'utiliser une couche de normalisation est que la normalisation fait partie intégrante du modèle. Vous n'avez pas besoin de normaliser les données manuellement avant de les passer au modèle (même lors de l'inférence).
La normalisation (ou standardisation) est presque toujours une bonne idée pour les réseaux de neurones, en particulier pour la couche d'entrée. Elle accélère l'entraînement, améliore la stabilité et conduit souvent à de meilleures performances.
Ensembles d'entraînement, de validation et de test
En machine learning supervisé, il est crucial de diviser votre jeu de données en (au moins) trois ensembles distincts :
- Ensemble d'entraînement (training set)
- Ensemble de validation (validation set)
- Ensemble de test (test set)
Pourquoi Diviser les Données ?
L'objectif principal est d'obtenir un modèle qui généralise bien, c'est-à-dire un modèle qui performe bien sur de nouvelles données, pas seulement sur les données qu'il a vues pendant l'entraînement.
- Surapprentissage (Overfitting) : Si vous entraînez et évaluez un modèle sur les mêmes données, vous risquez de surapprendre. Le modèle va mémoriser les données d'entraînement, y compris le bruit et les détails non pertinents, et il ne sera pas capable de généraliser correctement.
- Biais d'évaluation : Évaluer un modèle sur les données d'entraînement donne une estimation biaisée (trop optimiste) de sa performance réelle.
La division en ensembles distincts permet de simuler la situation réelle où le modèle sera utilisé sur de nouvelles données :
- Entraînement : On utilise l'ensemble d'entraînement pour ajuster les paramètres du modèle (poids et biais).
- Validation : On utilise l'ensemble de validation pour optimiser les hyperparamètres du modèle (par exemple, le nombre de couches, le nombre de neurones, le taux d'apprentissage, le taux de dropout, etc.) et pour sélectionner le meilleur modèle. On évalue le modèle sur l'ensemble de validation pendant l'entraînement (par exemple, à la fin de chaque époque), mais on n'utilise pas ces données pour mettre à jour les poids.
- Test : On utilise l'ensemble de test pour obtenir une estimation finale et non biaisée de la performance du modèle après que l'entraînement et l'optimisation des hyperparamètres sont terminés. On n'utilise l'ensemble de test qu'une seule fois, à la fin.
Proportions Courantes
Il n'y a pas de règle absolue pour les proportions, mais voici quelques pratiques courantes :
- Entraînement : 60-80%
- Validation : 10-20%
- Test : 10-20%
La proportion exacte dépend de la taille de votre jeu de données. Si vous avez beaucoup de données, vous pouvez vous permettre d'avoir des ensembles de validation et de test plus petits. Si vous avez peu de données, vous devrez peut-être utiliser une plus grande proportion pour l'entraînement.
Création des Ensembles avec TensorFlow
TensorFlow offre plusieurs façons de créer des ensembles d'entraînement, de validation et de test. Nous allons voir une méthode simple utilisant tf.data.Dataset.
Exemple (avec un jeu de données Iris) :
import tensorflow as tf
import numpy as np
import pandas as pd
# Chargement des données (comme dans l'exercice précédent)
df = pd.read_csv('iris.csv')
df = df.replace({"Iris-setosa": 0, 'Iris-versicolor': 1, 'Iris-virginica': 2})
X = df[['5.1', '3.5', '1.4', '0.2']].values
Y = df['Iris-setosa'].values
# Conversion en tf.data.Dataset
dataset = tf.data.Dataset.from_tensor_slices((X, Y))
# 1. On mélange le dataset
dataset = dataset.shuffle(buffer_size=len(X)) # Important : mélanger les données !
# 2. On divise en ensembles d'entraînement, de validation et de test
train_size = int(0.7 * len(X)) # 70% pour l'entraînement
val_size = int(0.15 * len(X)) # 15% pour la validation
test_size = len(X) - train_size - val_size # 15% pour le test
train_dataset = dataset.take(train_size)
val_dataset = dataset.skip(train_size).take(val_size)
test_dataset = dataset.skip(train_size + val_size)
# 3. On peut maintenant utiliser ces ensembles avec model.fit() :
# (Il faut d'abord créer un modèle et le compiler)
model.fit(train_dataset.batch(32), epochs=10, validation_data=val_dataset.batch(32))
# Affichage des tailles des ensembles
print(f"Taille de l'ensemble d'entraînement : {train_size}")
print(f"Taille de l'ensemble de validation : {val_size}")
print(f"Taille de l'ensemble de test : {test_size}")
Explications :
tf.data.Dataset.from_tensor_slices((X, Y)): Crée un objettf.data.Datasetà partir des tableaux NumPyX(caractéristiques) etY(étiquettes). Untf.data.Datasetest une structure de données TensorFlow optimisée pour gérer de grands jeux de données et effectuer des transformations.dataset.shuffle(buffer_size=len(X)): Mélange les données. C'est très important pour s'assurer que les ensembles d'entraînement, de validation et de test sont représentatifs de l'ensemble du jeu de données (et pour éviter que l'ordre des données n'influence l'apprentissage).- Calcul des tailles : On calcule le nombre d'exemples pour chaque ensemble en fonction des proportions souhaitées.
dataset.take(train_size): Prend lestrain_sizepremiers éléments du jeu de données mélangé pour créer l'ensemble d'entraînement.dataset.skip(train_size).take(val_size): Saute lestrain_sizepremiers éléments, puis prend lesval_sizesuivants pour créer l'ensemble de validation.dataset.skip(train_size + val_size): Saute lestrain_size + val_sizepremiers éléments pour créer l'ensemble de test.train_dataset.batch(32), etc. : (Non montré dans le code ci-dessus, mais important). Avant de passer les données au modèle, il faut les regrouper en batchs..batch(32)regroupe les exemples par lots de 32.model.fit(...): (Non montré dans le code ci-dessus, mais important). Vous pouvez maintenant passertrain_datasetetval_datasetdirectement àmodel.fit().
Il est crucial de mélanger les données avant de les diviser, surtout si elles sont ordonnées (par exemple, si toutes les instances d'une classe sont regroupées au début du fichier). Si vous ne mélangez pas, vous risquez de vous retrouver avec un ensemble d'entraînement qui ne contient qu'une partie des classes, ce qui rendrait l'apprentissage impossible.
Callbacks Keras
Les callbacks (littéralement "rappels" ou "fonctions de rappel") sont des objets que vous pouvez passer à la méthode fit() de votre modèle Keras pour personnaliser et étendre le comportement de l'entraînement. Les callbacks peuvent être appelés à différents moments du processus d'entraînement :
- Au début ou à la fin de l'entraînement.
- Au début ou à la fin de chaque époque.
- Au début ou à la fin de chaque batch.
Cela vous permet d'effectuer des actions comme :
- Enregistrer le modèle périodiquement (pour éviter de perdre votre travail en cas de crash).
- Arrêter l'entraînement prématurément si la performance sur l'ensemble de validation ne s'améliore plus (pour éviter le surapprentissage).
- Ajuster le taux d'apprentissage dynamiquement.
- Enregistrer des informations sur l'entraînement (perte, métriques) dans des fichiers journaux (logs).
- Et bien plus encore...
Callbacks Courants
Keras fournit plusieurs callbacks pré-définis (dans le module tf.keras.callbacks). Voici quelques-uns des plus utiles :
1. ModelCheckpoint
- Rôle : Enregistre le modèle (architecture et/ou poids) à intervalles réguliers pendant l'entraînement.
- Utilité :
- Récupérer le meilleur modèle (celui avec la meilleure performance sur l'ensemble de validation).
- Reprendre l'entraînement à partir d'un point de sauvegarde en cas d'interruption.
- Paramètres principaux :
filepath: Chemin du fichier où enregistrer le modèle.monitor: La quantité à surveiller (par exemple, 'val_loss', 'val_accuracy').save_best_only: SiTrue, seul le modèle avec la meilleure valeur de la quantité surveillée est enregistré.save_weights_only: SiTrue, seuls les poids du modèle sont enregistrés (pas l'architecture).mode: 'min' (pour la perte), 'max' (pour l'exactitude), ou 'auto'.save_freq:'epoch'ou integer.
Exemple :
checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
filepath='chemin/vers/mon_modele.h5', # Chemin du fichier
monitor='val_loss', # Surveiller la perte sur l'ensemble de validation
save_best_only=True, # Enregistrer seulement le meilleur modèle
mode='min', # On veut minimiser la perte
save_weights_only=False # On enregistre le modèle au complet
)
2. EarlyStopping
- Rôle : Arrête l'entraînement prématurément si la performance sur l'ensemble de validation ne s'améliore plus pendant un certain nombre d'époques.
- Utilité :
- Éviter le surapprentissage.
- Gagner du temps (ne pas continuer l'entraînement inutilement).
- Paramètres principaux :
monitor: La quantité à surveiller (par exemple, 'val_loss', 'val_accuracy').patience: Le nombre d'époques sans amélioration après lequel l'entraînement est arrêté.mode: 'min', 'max', ou 'auto'.restore_best_weights: SiTrue, le modèle est restauré à l'état où il avait la meilleure valeur de la quantité surveillée.
Exemple :
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy', # Surveiller l'exactitude sur l'ensemble de validation
patience=10, # Arrêter après 10 époques sans amélioration
mode='max', # On veut maximiser l'exactitude
restore_best_weights=True # Restaurer les meilleurs poids
)
3. ReduceLROnPlateau
- Rôle : Réduit le taux d'apprentissage lorsque la métrique surveillée cesse de s'améliorer.
- Utilité :
- Permet d'affiner l'optimisation lorsque l'on s'approche d'un minimum.
- Paramètres principaux :
monitor: La quantité à surveiller (par exemple, 'val_loss', 'val_accuracy').factor: Facteur par lequel le taux d'apprentissage sera réduit.patience: Nombre d'époques.mode: 'min', 'max', ou 'auto'.min_delta: Seuil pour considérer un changement comme une amélioration.cooldown: Nombre d'époques à attendre.min_lr: Limite inférieure.
4. LearningRateScheduler
- Rôle : Définit une fonction qui ajuste le taux d'apprentissage en fonction du numéro de l'époque.
- Utilité :
- Permet d'implémenter des stratégies d'ajustement du taux d'apprentissage plus complexes.
Comment Utiliser les Callbacks
Vous passez une liste de callbacks à la méthode fit() :
model.fit(x_train, y_train, epochs=100, validation_data=(x_val, y_val),
callbacks=[checkpoint_callback, early_stopping_callback])
Callbacks Personnalisés
Si les callbacks pré-définis ne suffisent pas, vous pouvez créer vos propres callbacks personnalisés en héritant de la classe tf.keras.callbacks.Callback. Cela dépasse le cadre de cette introduction, mais sachez que c'est possible.
Les callbacks sont un outil puissant pour contrôler et étendre le comportement de l'entraînement de vos modèles Keras. ModelCheckpoint et EarlyStopping sont presque toujours utiles.
Classification de chiffres manuscrits (MNIST) avec Keras
Dans cet exemple, nous allons construire un modèle de réseau de neurones avec Keras pour résoudre un problème classique de classification d'images : la reconnaissance de chiffres manuscrits à partir du jeu de données MNIST.
Qu'est-ce que MNIST ?
MNIST (Modified National Institute of Standards and Technology database) est un jeu de données très populaire en machine learning. Il est souvent utilisé comme "Hello, World!" du deep learning. Il contient 70 000 images en niveaux de gris de chiffres manuscrits (de 0 à 9), chacune de taille 28x28 pixels.
- 60 000 images sont utilisées pour l'entraînement.
- 10 000 images sont utilisées pour les tests.
Chaque image est associée à une étiquette (label) qui indique le chiffre représenté (un entier entre 0 et 9).
L'objectif est de construire un modèle qui, étant donné une image 28x28, prédit correctement le chiffre qu'elle représente. C'est un problème de classification multiclasse (10 classes possibles).
Code Complet (Notebook Jupyter)
# Importation des librairies
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
# Pour la reproductibilité (pas obligatoire, mais recommandé)
np.random.seed(42)
tf.random.set_seed(42)
Nous importons les librairies nécessaires : TensorFlow (et Keras), NumPy et Matplotlib. Nous fixons également les générateurs de nombres aléatoires de NumPy et TensorFlow pour la reproductibilité (afin que vous obteniez les mêmes résultats à chaque exécution).
# Chargement des données MNIST
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
print(f"Forme de x_train : {x_train.shape}")
print(f"Forme de y_train : {y_train.shape}")
print(f"Forme de x_test : {x_test.shape}")
print(f"Forme de y_test : {y_test.shape}")
Keras fournit un accès direct à plusieurs jeux de données classiques, dont MNIST. La fonction tf.keras.datasets.mnist.load_data() télécharge les données (si nécessaire) et les renvoie sous forme de tableaux NumPy :
x_train: Images d'entraînement (60 000 images, 28x28 pixels).y_train: Étiquettes d'entraînement (60 000 entiers entre 0 et 9).x_test: Images de test (10 000 images, 28x28 pixels).y_test: Étiquettes de test (10 000 entiers entre 0 et 9).
Nous affichons la forme (shape) de ces tableaux pour vérifier les dimensions.
# Visualisation de quelques images
plt.figure(figsize=(10, 2))
for i in range(10):
plt.subplot(1, 10, i+1)
plt.imshow(x_train[i], cmap='gray') # cmap='gray' pour afficher en niveaux de gris
plt.title(str(y_train[i]))
plt.axis('off') # Masquer les axes
plt.show()
Visualisons quelques images du jeu de données pour mieux comprendre à quoi elles ressemblent. Nous utilisons plt.imshow() de Matplotlib, en spécifiant cmap='gray' pour afficher les images en niveaux de gris. Chaque image est un tableau 28x28 de valeurs entre 0 et 255 (0 = noir, 255 = blanc).

# Prétraitement des données
# 1. Normalisation des images
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
# 2. Aplatissement des images (28x28 -> 784)
x_train = x_train.reshape(-1, 784) # -1 signifie "déduire la taille à partir des autres dimensions"
x_test = x_test.reshape(-1, 784)
print(f"Nouvelle forme de x_train : {x_train.shape}")
# 3. Conversion des étiquettes en entiers (déjà fait, mais on vérifie)
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)
Nous effectuons deux étapes de prétraitement :
-
Normalisation : Nous divisons les valeurs des pixels par 255.0 pour les ramener entre 0 et 1. Cela améliore la stabilité et la vitesse d'entraînement du réseau de neurones. Nous convertissons également les tableaux en
float32(par défaut, ils sont enuint8). -
Aplatissement (flattening) : Nous transformons chaque image 28x28 en un vecteur de taille 784 (28 * 28 = 784). Nous faisons cela car notre modèle (pour l'instant) utilisera des couches
Dense, qui attendent des entrées 1D.reshape(-1, 784)permet de faire cela automatiquement.
Nous vérifions que les étiquettes sont bien des entiers (elles le sont déjà, mais c'est une bonne pratique de vérifier).
# Création du modèle
model = tf.keras.Sequential([
tf.keras.layers.Dense(units=128, activation='relu', input_shape=(784,)),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(units=10, activation='softmax')
])
Nous créons un modèle Keras séquentiel avec :
- Une couche
Densecachée avec 128 neurones et une fonction d'activation ReLU.input_shape=(784,)indique que cette couche attend des entrées de taille 784 (nos images aplaties). - Une couche
Dropoutavec un taux de 0.2 (20% des neurones seront désactivés aléatoirement pendant l'entraînement) pour la régularisation. - Une couche
Densede sortie avec 10 neurones (un pour chaque chiffre de 0 à 9) et une fonction d'activation softmax. Softmax est utilisée car c'est un problème de classification multiclasse.
# Compilation du modèle
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
Nous compilons le modèle :
optimizer='adam': Nous utilisons l'optimiseur Adam, qui est un bon choix par défaut.loss='sparse_categorical_crossentropy': Nous utilisons l'entropie croisée catégorielle sparse. "Sparse" parce que nos étiquettes sont des entiers (0 à 9), pas des vecteurs one-hot.metrics=['accuracy']: Nous évaluons le modèle avec l'exactitude (accuracy).
# Création des callbacks
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=15,
mode='max',
restore_best_weights=True
)
monitor='val_accuracy': C'est la métrique que le callback surveille pour décider quand arrêter l'entraînement. Dans ce cas, c'est la précision de validation (val_accuracy).patience=15: C'est le nombre d'époques (epochs) sans amélioration de la métrique surveillée (val_accuracy) avant que l'entraînement ne soit arrêté. Dans ce cas, si la val_accuracy n'augmente pas pendant 15 époques consécutives, l'entraînement s'arrête.mode='max': Ce paramètre indique si le callback cherche à maximiser ou minimiser la métrique surveillée. Puisque nous surveillons val_accuracy, nous voulons la maximiser. Donc, le callback arrêtera l'entraînement si la val_accuracy ne s'augmente pas.restore_best_weights=True: Lorsqu'il est réglé sur True, le callback restaure automatiquement les poids du modèle à ceux de l'époque où la meilleure valeur de la métrique surveillée (val_accuracy) a été atteinte.
# Entraînement du modèle
history = model.fit(x_train, y_train, epochs=20, batch_size= 32, validation_split=0.2, callbacks=[early_stopping_callback])
Nous entraînons le modèle avec model.fit() :
x_train,y_train: Données d'entraînement (images et étiquettes).epochs=20: Nous effectuons 20 époques d'entraînement (20 passages complets sur les données).validation_split=0.2: Nous utilisons 20% des données d'entraînement comme ensemble de validation. Keras gère automatiquement cette division. C'est une alternative à la création manuelle d'un ensemble de validation.batch_size=32: Nous utilisons des mini-batch de taille 32.callbacks: On ajoute notre callback
# Visualisation des courbes d'apprentissage
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
Nous visualisons les courbes de perte et d'exactitude sur les données d'entraînement et de validation. Cela nous permet de voir si le modèle apprend correctement, s'il y a du surapprentissage (si la perte sur l'ensemble de validation commence à augmenter alors que la perte sur l'ensemble d'entraînement continue de diminuer), etc.


# Évaluation du modèle sur l'ensemble de test
loss, accuracy = model.evaluate(x_test, y_test, verbose=0)
print(f"Perte (test) : {loss:.4f}")
print(f"Exactitude (test) : {accuracy * 100:.2f}%")
Perte (test) : 0.0897
Exactitude (test) : 97.88%
Nous évaluons finalement le modèle sur l'ensemble de test (les 10 000 images que le modèle n'a jamais vues) pour obtenir une estimation non biaisée de sa performance en généralisation.
# Prédictions sur quelques images de test
predictions = model.predict(x_test[:10]) # Prédictions sur les 10 premières images
predicted_classes = np.argmax(predictions, axis=1) # On prend l'indice de la probabilité maximale
plt.figure(figsize=(10, 2))
for i in range(10):
plt.subplot(1, 10, i + 1)
plt.imshow(x_test[i].reshape(28, 28), cmap='gray')
plt.title(f"P:{predicted_classes[i]}\nR:{y_test[i]}", fontsize=9) # P : Prédiction, R : Réel
plt.axis('off')
plt.show()
Nous faisons des prédictions sur les 10 premières images de test et nous les affichons, avec la classe prédite et la vraie classe. Cela permet de visualiser concrètement ce que le modèle fait.

Pourquoi 128 Neurones dans la Couche Cachée ?
Dans l'exemple de code pour la classification MNIST, nous avons utilisé une couche cachée Dense avec 128 neurones :
tf.keras.layers.Dense(units=128, activation='relu', input_shape=(784,))
Il n'y a pas de réponse unique et définitive à la question "pourquoi 128 ?", mais voici les raisons principales :
-
Puissance de 2 : Il est courant (mais pas obligatoire) d'utiliser un nombre de neurones qui est une puissance de 2 (32, 64, 128, 256, 512, etc.). Cela peut parfois optimiser l'utilisation de la mémoire et des calculs sur certains matériels (GPU en particulier), mais ce n'est pas une règle stricte.
-
Heuristique et Expérimentation : Le choix du nombre de neurones dans une couche cachée est en grande partie un hyperparamètre du modèle. Il n'y a pas de formule magique pour le déterminer a priori. On procède souvent par tâtonnements, en commençant par une valeur raisonnable et en ajustant en fonction des résultats.
-
Compromis Complexité/Performance :
- Trop peu de neurones : Le modèle risque de ne pas être assez puissant pour apprendre la tâche (sous-apprentissage).
- Trop de neurones : Le modèle risque de surapprendre les données d'entraînement et de mal généraliser à de nouvelles données. Cela augmente également le temps de calcul et la consommation de mémoire.
-
Règle empirique (très approximative) : Une règle empirique (mais très approximative et à prendre avec beaucoup de précautions) est de choisir un nombre de neurones compris entre la taille de l'entrée (ici, 784) et la taille de la sortie (ici, 10). 128 se situe dans cette fourchette.
-
Valeur "commune" : 128 est une valeur relativement commune pour une première couche cachée dans des problèmes de classification d'images de complexité modérée comme MNIST. De nombreux tutoriels et exemples utilisent cette valeur, ce qui en fait un bon point de départ.
-
Expérience: Le nombre de neurones est souvent choisi grâce à des tests.
En résumé : 128 est une valeur raisonnable et courante pour une première couche cachée dans ce type de problème. C'est un bon compromis entre complexité et performance, et un bon point de départ pour l'expérimentation. Il est tout à fait possible qu'une autre valeur (64, 256, 512...) donne de meilleurs résultats pour ce problème spécifique, mais il faudrait tester pour le savoir.
Le choix du nombre de neurones (et du nombre de couches) est un hyperparamètre du modèle. Il n'y a pas de réponse unique. Il faut expérimenter, en commençant par des valeurs raisonnables et en ajustant en fonction des résultats (en utilisant un ensemble de validation !).
Pourquoi une Seule Couche Cachée ?
Dans l'exemple de code pour la classification MNIST, nous avons utilisé un modèle avec une seule couche cachée :
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=(28, 28)),
tf.keras.layers.Dense(units=128, activation='relu'),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(units=10, activation='softmax')
])
Voici les raisons pour lesquelles ce choix est raisonnable pour cet exemple :
-
Simplicité : Pour un premier exemple complet, il est préférable de commencer avec une architecture simple. Un réseau avec une seule couche cachée est plus facile à comprendre, à visualiser et à déboguer qu'un réseau plus profond.
-
MNIST est un problème "relativement" simple : Bien que la reconnaissance de chiffres manuscrits soit un problème non trivial, MNIST est un jeu de données "facile" par rapport aux standards actuels du Deep Learning. Les images sont petites (28x28), en niveaux de gris, et les chiffres sont relativement bien centrés et séparés. Un réseau peu profond peut déjà obtenir de très bonnes performances (plus de 95% d'exactitude).
-
Objectif pédagogique : L'objectif principal de cet exemple est d'illustrer les concepts de base de Keras (construction d'un modèle séquentiel, couches, fonctions d'activation, compilation, entraînement, évaluation, visualisation). Ajouter plus de couches compliquerait le code et l'explication sans apporter de valeur ajoutée significative pour l'apprentissage de ces concepts.
-
Point de départ pour l'expérimentation : Un modèle simple avec une seule couche cachée est un bon point de départ pour l'expérimentation. Une fois que vous avez un modèle de base qui fonctionne, vous pouvez essayer d'ajouter des couches, de modifier le nombre de neurones, d'utiliser d'autres fonctions d'activation, etc., et observer l'impact sur la performance.
Est-ce que cela signifie qu'une seule couche cachée est toujours suffisante pour MNIST ?
Non, absolument pas. Vous pouvez obtenir de meilleures performances avec des réseaux plus profonds (et avec d'autres types de couches, comme les couches convolutives, que nous verrons plus tard). Mais pour un premier exemple, et pour atteindre une exactitude déjà très élevée (97-98% est facilement atteignable), une seule couche cachée est un choix raisonnable.
En résumé :
- Une seule couche cachée est un choix pédagogique pour simplifier l'exemple.
- MNIST est un problème relativement simple, où un réseau peu profond peut déjà bien fonctionner.
- Ce modèle simple sert de base pour des expérimentations ultérieures.
Il est important de comprendre que le choix de l'architecture d'un réseau de neurones (nombre de couches, nombre de neurones, types de couches, etc.) est un processus itératif et expérimental. Il n'y a pas de recette miracle. Il faut essayer différentes architectures, évaluer leurs performances sur un ensemble de validation, et choisir celle qui donne les meilleurs résultats pour votre problème spécifique.
Exercice : Classification d'Images avec CIFAR-10 (Keras)
Dans cet exercice, vous allez vous attaquer à un problème de classification d'images plus complexe que MNIST : la classification d'images du jeu de données CIFAR-10. Vous utiliserez Keras et les concepts que nous avons vus pour construire, entraîner et évaluer un réseau de neurones multicouche.
Le Jeu de Données CIFAR-10
CIFAR-10 (Canadian Institute For Advanced Research 10) est un autre jeu de données classique en Machine Learning. Il contient 60 000 images couleur de 32x32 pixels, réparties en 10 classes :
- avion
- automobile
- oiseau
- chat
- cerf
- chien
- grenouille
- cheval
- bateau
- camion
Il y a 6 000 images par classe. Le jeu de données est divisé en 50 000 images d'entraînement et 10 000 images de test.
Différences avec MNIST :
- Images couleur : Les images CIFAR-10 sont en couleur (3 canaux : rouge, vert, bleu), alors que les images MNIST sont en niveaux de gris (1 canal).
- Plus petite résolution : 32x32 au lieu de 28x28.
- Objets plus complexes : Les objets dans CIFAR-10 sont plus variés et plus complexes que les chiffres manuscrits de MNIST.
- Plus de données d'entrainement
CIFAR-10 est un problème plus difficile que MNIST. Vous aurez besoin d'un modèle plus puissant pour obtenir de bonnes performances.
Ce que Vous Devez Faire
-
Chargement et Prétraitement des Données :
- Chargez le jeu de données CIFAR-10 en utilisant
tf.keras.datasets.cifar10.load_data(). - Explorez les données :
- Affichez les formes des tableaux d'entraînement et de test (
x_train,y_train,x_test,y_test). - Visualisez quelques images avec
plt.imshow(). Attention, les images sont en couleur !
- Affichez les formes des tableaux d'entraînement et de test (
- Prétraitez les données :
- Normalisez les valeurs des pixels (entre 0 et 1).
- Convertissez les étiquettes en entiers (si ce n'est pas déjà le cas).
- Aplatissez les images (32x32x3 -> 3072) ou non (à vous de décider, mais justifiez votre choix !).
- Chargez le jeu de données CIFAR-10 en utilisant
-
Construction du Modèle :
- Créez un modèle Keras
Sequential. - Ajoutez plusieurs couches
Dense. Combien ? Combien de neurones par couche ? C'est à vous d'expérimenter ! - Utilisez des fonctions d'activation non linéaires (ReLU, Leaky ReLU, etc.) pour les couches cachées.
- Ajoutez éventuellement des couches
Dropoutpour la régularisation. - La couche de sortie doit avoir 10 neurones (un pour chaque classe) et une fonction d'activation
softmax.
- Créez un modèle Keras
-
Compilation du Modèle :
- Choisissez un optimiseur (
Adamest un bon point de départ). - Choisissez la fonction de perte appropriée (
sparse_categorical_crossentropyoucategorical_crossentropy, selon le format de vos étiquettes). - Utilisez l'exactitude (
accuracy) comme métrique.
- Choisissez un optimiseur (
-
Entraînement du Modèle :
- Entraînez le modèle sur les données d'entraînement.
- Utilisez un ensemble de validation (soit en utilisant
validation_splitdansmodel.fit(), soit en créant un ensemble de validation séparé). - Choisissez un nombre d'époques et une taille de batch appropriés. Combien ? Pourquoi ?
- Utilisez des callbacks :
ModelCheckpointpour enregistrer le meilleur modèle.EarlyStoppingpour arrêter l'entraînement prématurément si nécessaire.
-
Évaluation du Modèle :
- Évaluez le modèle sur l'ensemble de test.
- Affichez la perte et l'exactitude sur l'ensemble de test.
-
Visualisation et Analyse :
- Affichez les courbes de perte et d'exactitude (entraînement et validation).
- Y a-t-il des signes de surapprentissage ? Si oui, comment pourriez-vous le réduire ?
- Affichez quelques prédictions du modèle sur des images de test (avec les vraies étiquettes). Le modèle fait-il des erreurs ? Sur quelles classes ?
Points d'Attention et Pistes de Réflexion
- Architecture du modèle : Combien de couches cachées ? Combien de neurones par couche ? Quelle fonction d'activation pour les couches cachées ? Faut-il utiliser du dropout ? Si oui, avec quel taux ? Il faut essayer.
- Fonction de perte :
sparse_categorical_crossentropyoucategorical_crossentropy? Quelle est la différence ? - Optimiseur : Adam est un bon choix par défaut, mais vous pouvez essayer d'autres optimiseurs.
- Taux d'apprentissage : Faut-il le modifier par rapport à la valeur par défaut ?
- Nombre d'époques : Combien d'époques sont nécessaires ? Comment éviter le surapprentissage ?
- Taille du batch : Quelle taille de batch utiliser ?
- Normalisation
- One-hot encoding
Il n'y a pas de solution unique pour cet exercice. L'objectif est d'expérimenter, d'observer les résultats, et de comprendre l'impact de vos choix sur la performance du modèle. Documentez vos expérimentations et justifiez vos choix.
CIFAR-10 est significativement plus difficile que MNIST. Ne soyez pas surpris si vous n'obtenez pas une exactitude de 99% du premier coup ! Une exactitude de 70-80% avec un réseau de neurones à couches denses (sans convolution) est déjà un résultat raisonnable.
Solution
Solution
Voici une solution possible pour l'exercice de classification CIFAR-10. Gardez à l'esprit qu'il n'y a pas de solution unique, et que vous pouvez obtenir des résultats différents (et potentiellement meilleurs !) en faisant d'autres choix.
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
# Pour la reproductibilité
np.random.seed(42)
tf.random.set_seed(42)
# 1. Chargement et prétraitement des données
# Chargement
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
# Exploration
print(f"Forme de x_train : {x_train.shape}") # (50000, 32, 32, 3) : 50000 images, 32x32 pixels, 3 canaux (RGB)
print(f"Forme de y_train : {y_train.shape}") # (50000, 1)
print(f"Forme de x_test : {x_test.shape}") # (10000, 32, 32, 3)
print(f"Forme de y_test : {y_test.shape}") # (10000, 1)
# Visualisation de quelques images
plt.figure(figsize=(10, 2))
for i in range(10):
plt.subplot(1, 10, i + 1)
plt.imshow(x_train[i]) # Pas besoin de cmap='gray' car les images sont en couleur
plt.title(str(y_train[i][0])) # y_train[i] est un tableau [[y]], on prend y_train[i][0]
plt.axis('off')
plt.show()
# Prétraitement
# Normalisation
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
# Aplatissement des images (32x32x3 -> 3072)
x_train = x_train.reshape(-1, 32 * 32 * 3) # 3072 = 32 * 32 * 3
x_test = x_test.reshape(-1, 32 * 32 * 3)
print(f"Nouvelle forme de x_train : {x_train.shape}")
# Conversion des étiquettes en entiers (déjà fait, mais on vérifie)
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)
Explications (Chargement et Prétraitement) :
tf.keras.datasets.cifar10.load_data(): Charge le jeu de données CIFAR-10 directement depuis Keras.- Exploration :
- On affiche les formes des tableaux pour comprendre la structure des données. Notez que les images sont en 32x32 pixels avec 3 canaux de couleur (Rouge, Vert, Bleu).
- On visualise quelques images avec
plt.imshow(). Contrairement à MNIST, nous n'avons pas besoin decmap='gray'car les images sont en couleur. Notez aussi quey_train[i]est un tableau[[y]], donc on accède à la valeur avecy_train[i][0].
- Prétraitement :
- Normalisation : On divise par 255.0 pour ramener les valeurs des pixels entre 0 et 1.
- Aplatissement : On transforme chaque image 32x32x3 en un vecteur de taille 3072 (32 * 32 * 3). C'est un choix : on aurait pu garder la structure 2D des images (pour utiliser des couches convolutives, par exemple), mais pour cet exercice, nous utilisons des couches
Dense, qui nécessitent des entrées 1D. - Conversion des étiquettes : On vérifie que les étiquettes sont bien des entiers.
# 2. Construction du modèle
model = tf.keras.Sequential([
tf.keras.layers.Dense(units=256, activation='relu', input_shape=(3072,)),
tf.keras.layers.Dropout(rate=0.3),
tf.keras.layers.Dense(units=128, activation='relu'),
tf.keras.layers.Dropout(rate=0.3),
tf.keras.layers.Dense(units=10, activation='softmax')
])
# 3. Compilation
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
# 4. Callbacks
model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
filepath='./checkpoints/cifar10_model_{epoch:02d}.weights.h5',
monitor='val_accuracy',
save_best_only=True,
mode='max',
save_weights_only=True,
save_freq='epoch')
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=15,
mode='max',
restore_best_weights=True
)
Explications (Construction et Compilation) :
- Modèle :
- Nous créons un modèle séquentiel avec deux couches cachées
Dense, avec respectivement 256 et 128 neurones, et une activation ReLU. Ces valeurs sont des choix raisonnables, mais on pourrait expérimenter avec d'autres architectures. - Nous ajoutons des couches
Dropoutaprès chaque couche cachée pour la régularisation (réduction du surapprentissage). - La couche de sortie a 10 neurones (une pour chaque classe) et une activation softmax.
- Nous créons un modèle séquentiel avec deux couches cachées
- Compilation :
- Optimiseur :
adam. - Fonction de perte :
sparse_categorical_crossentropy(car nos étiquettes sont des entiers). - Métrique :
accuracy.
- Optimiseur :
- Callbacks:
- On ajoute les callbacks pour le checkpoint et l'early stopping
# 5. Entraînement
history = model.fit(x_train, y_train, epochs=100, batch_size=64,
validation_split=0.15, verbose=2,
callbacks=[model_checkpoint_callback, early_stopping_callback])
Explications (Entraînement) :
x_train,y_train: Données d'entraînement.epochs=100: Nous entraînons pendant 100 époques (mais l'entraînement peut s'arrêter avant grâce àEarlyStopping).batch_size=64: Nous utilisons des mini-batchs de taille 64.validation_split=0.15: Nous utilisons 15% des données d'entraînement pour la validation.verbose=2: Affiche une ligne par époque avec les informations sur la perte et l'exactitude.callbacks: On ajoute nos callbacks
# 6. Visualisation
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.show()
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend()
plt.show()
Explications (Visualisation) :
Nous traçons les courbes de perte et d'exactitude sur les données d'entraînement et de validation. Cela nous permet de :
- Vérifier si le modèle apprend (la perte doit diminuer, l'exactitude doit augmenter).
- Détecter le surapprentissage (si la perte sur la validation commence à augmenter alors que la perte sur l'entraînement continue de diminuer).
- Vérifier l'efficacité des callbacks
# 7. Évaluation
loss, accuracy = model.evaluate(x_test, y_test, verbose=0)
print(f"Perte (test) : {loss:.4f}")
print(f"Exactitude (test) : {accuracy * 100:.2f}%")
Explications (Évaluation) :
Nous évaluons le modèle sur les données de test pour obtenir une estimation non biaisée de sa performance en généralisation.
# 8. Prédictions
predictions = model.predict(x_test[:10])
predicted_classes = np.argmax(predictions, axis=1)
plt.figure(figsize=(10, 2))
for i in range(10):
plt.subplot(1, 10, i + 1)
plt.imshow(x_test[i].reshape(32, 32, 3)) # On remet l'image en forme 32x32x3
plt.title(f"P:{predicted_classes[i]}\nR:{y_test[i][0]}", fontsize=9)
plt.axis('off')
plt.show()
Explications (Prédictions) :
- Nous faisons des prédictions sur les 10 premières images de test.
model.predict()renvoie les probabilités pour chaque classe. Nous utilisonsnp.argmax(predictions, axis=1)pour obtenir l'indice de la classe prédite (celle avec la probabilité la plus élevée).- Nous affichons les images, avec la classe prédite et la vraie classe, pour visualiser les résultats.
- Nous devons remettre l'image en 32x32x3.
Analyse :
Il est important de noter que :
- Ce n'est qu'une solution possible. Il y a de nombreuses façons d'améliorer ce modèle (plus de couches, d'autres fonctions d'activation, d'autres techniques de régularisation, etc.).
- Les performances que vous obtiendrez peuvent varier légèrement en raison de l'initialisation aléatoire des poids et de la nature stochastique de l'entraînement.
- On entraine le modèle mais on ne sauvegarde pas les poids ici.
L'objectif principal de cet exercice est de mettre en pratique les concepts de Keras et de se familiariser avec un problème de classification d'images plus complexe que MNIST.