🧠 Deep Learning · Avancé

Classification d'images avec CNN

⏱ 60 minutes🔶 TensorFlow 2.16🔥 PyTorch 2.3

Les réseaux de neurones convolutifs (CNN) sont le standard pour la vision par ordinateur. Ce tutoriel implémente le même CNN de classification sur CIFAR-10 dans les deux frameworks pour comprendre leurs différences d'approche.

Le dataset CIFAR-10

60 000 images couleur 32×32 dans 10 catégories : avion, automobile, oiseau, chat, cerf, chien, grenouille, cheval, bateau, camion. Un benchmark classique pour valider des architectures CNN.

📖 Terme : Réseau de neurones convolutif (CNN)

Définition : Architecture de réseau de neurones spécialisée pour les images. Elle utilise des couches convolutives pour extraire des features spatiales hiérarchiquement : bords simples → textures → objets → concepts abstraits.

But : Traiter efficacement les images en exploitant leur structure spatiale 2D locale, contrairement aux réseaux classiques qui traitent les pixels indépendamment.

Pourquoi ici : Les CNN sont l'état de l'art pour la vision par ordinateur. Un MLP (Multi-Layer Perceptron) traditionnel sur pixels ignore la structure spatiale 2D — un filtre 3x3 détectant un bord doit apprendre indépendamment à chaque position, ce qui est inefficace et ne généralise pas bien.

Partie 1 — TensorFlow / Keras

tensorflow_cnn.py
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np

print(f"TensorFlow {tf.__version__}")
print(f"GPU disponible : {len(tf.config.list_physical_devices('GPU')) > 0}")

# ── Chargement et preprocessing des données ──
(X_train, y_train), (X_test, y_test) = keras.datasets.cifar10.load_data()

# Normaliser : [0, 255] → [0, 1]
X_train = X_train.astype('float32') / 255.0
X_test = X_test.astype('float32') / 255.0

# One-hot encoding des labels
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)

print(f"Train : {X_train.shape} | Test : {X_test.shape}")

# ── Data Augmentation ──
data_augmentation = keras.Sequential([
    layers.RandomFlip('horizontal'),
    layers.RandomRotation(0.1),
    layers.RandomZoom(0.1),
    layers.RandomTranslation(0.1, 0.1),
])

# ── Architecture CNN ──
def build_model():
    inputs = keras.Input(shape=(32, 32, 3))

    # Augmentation seulement en entraînement
    x = data_augmentation(inputs)

    # Bloc 1 — extraire les features basiques (bords, textures, coins)
    x = layers.Conv2D(32, (3, 3), padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Conv2D(32, (3, 3), padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.MaxPooling2D((2, 2))(x)
    x = layers.Dropout(0.25)(x)

    # Bloc 2 — features plus complexes
    x = layers.Conv2D(64, (3, 3), padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Conv2D(64, (3, 3), padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.MaxPooling2D((2, 2))(x)
    x = layers.Dropout(0.25)(x)

    # Bloc 3 — features abstraites
    x = layers.Conv2D(128, (3, 3), padding='same', activation='relu')(x)
    x = layers.BatchNormalization()(x)
    x = layers.GlobalAveragePooling2D()(x)  # Plus efficace que Flatten

    # Couche de classification
    x = layers.Dense(128, activation='relu')(x)
    x = layers.Dropout(0.5)(x)
    outputs = layers.Dense(10, activation='softmax')(x)

    return keras.Model(inputs, outputs)

model = build_model()
model.summary()

# ── Compilation ──
model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='categorical_crossentropy',
    metrics=['accuracy']
)
📖 Terme : Convolution (Conv2D)

Définition : Une convolution applique un filtre (petit noyau 3×3 ou 5×5) sur une image en le glissant position par position. À chaque position, on calcule la somme du produit élément-wise (convolution discrète). Une feature map est la collection de toutes ces sommes — elle représente la "détection" d'un pattern partout dans l'image.

But : Extraire des patterns locaux (bords horizontaux/verticaux, coins, textures) à partir des pixels bruts. Les 32 filtres en parallèle créent 32 feature maps, chacun détectant un pattern différent.

Pourquoi ici : La convolution exploite la structure spatiale 2D des images. Au lieu d'un neurone per pixel (millions de paramètres), on utilise quelques filtres partagés (centaines de paramètres) qui détectent des patterns partout — c'est efficace et généralise bien.

Conv2D(32, (3,3)) crée 32 filtres de taille 3×3. Chaque filtre glisse sur l'image 32×32 entière, producingune feature map 32×32. Les 32 feature maps sont empilées, créant une "image" de dimension 32×32×32. Les 32 filtres apprennent collectivement à détecter 32 patterns différents : bords, coins, textures simples, etc.
📖 Terme : Batch Normalization (BatchNorm)

Définition : Technique qui normalise les activations en chaque couche à moyenne zéro et variance un, indépendamment pour chaque channel/feature map. Pendant l'entraînement, on normalise par les statistiques du batch actuel ; en prédiction, on utilise les statistiques exponentiellement lissées (running mean/variance).

But : Accélérer l'entraînement, permettre des learning rates plus élevés, et améliorer la généralisation.

Pourquoi ici : Sans BatchNormalization, l'entraînement de CNNs profonds (>10 couches) est instable — les activations deviennent soit trop grandes soit trop petites. Avec BatchNormalization, l'entraînement converge souvent 2-3x plus vite et est bien plus stable.

BatchNormalization agit comme un stabilisateur : il empêche les activations de dériver vers des valeurs extrêmes. Si les activations deviennent très grandes, les gradients deviennent très petits (vanishing gradients) et l'optimisation stagne. BatchNorm remet constamment les activations à l'échelle correcte.
📖 Terme : Pooling

Définition : Opération qui réduit la dimension spatiale en abaissant l'image par un facteur (généralement 2). MaxPooling(2,2) divise l'image en fenêtres 2×2 et prend le maximum de chaque fenêtre.

But : Réduire la complexité spatiale et augmenter le champ réceptif (receptive field) des couches suivantes.

Pourquoi ici : Le pooling réduit le coût computationnel et la mémoire, et force le réseau à apprendre des features invariantes à de petites translations.

📖 Terme : Dropout

Définition : Technique de régularisation qui "éteint" aléatoirement une fraction des neurones pendant l'entraînement (p.ex. Dropout(0.5) éteint 50% des neurones à chaque forward pass). Pendant la prédiction/inference, tous les neurones sont actifs mais leurs poids sont redimensionnés par (1 - dropout_rate).

But : Empêcher la co-adaptation des neurones — aucun neurone ne peut compter sur ses voisins étant toujours actifs. Cela force le réseau à apprendre des features redondantes et robustes.

Pourquoi ici : Dropout est un régularisateur simple mais très efficace qui réduit l'overfitting sans augmenter la complexité du modèle. C'est particulièrement utile pour les couches fully connected (Dense).

Exemple de Dropout : Dropout(0.5) avec 256 neurones → en moyenne 128 neurones sont actifs par forward pass. Si un neurone apprend à dépendre de son voisin, que se passe-t-il quand ce voisin est "éteint"? Le neurone doit se rendre indépendant — c'est le mécanisme de régularisation.
tensorflow_training.py
# ── Callbacks ──
callbacks = [
    # Arrêter si pas d'amélioration après 10 epochs
    keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True),
    # Réduire le learning rate si plateau
    keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5, min_lr=1e-6),
    # Sauvegarder le meilleur modèle
    keras.callbacks.ModelCheckpoint('best_model.keras', save_best_only=True, monitor='val_accuracy'),
]

# ── Entraînement ──
# batch_size=64 : traiter 64 images à la fois (GPU parallélisation)
# validation_split=0.2 : réserver 20% pour validation pendant entraînement
history = model.fit(
    X_train, y_train,
    epochs=50,
    batch_size=64,
    validation_split=0.2,
    callbacks=callbacks,
    verbose=1
)

# ── Évaluation ──
test_loss, test_acc = model.evaluate(X_test, y_test)
print(f"\nPrécision sur le test set : {test_acc*100:.2f}%")
📖 Terme : Callback

Définition : Fonction callback exécutée automatiquement à intervals réguliers pendant l'entraînement (fin d'epoch, après N batches, etc.). Les callbacks permettent de monitorer, modifier ou arrêter l'entraînement sans modifier la boucle principale.

But : Ajouter du contrôle et de la flexibilité à l'entraînement automatiquement.

Pourquoi ici : Les callbacks comme EarlyStopping et ModelCheckpoint préviennent le surapprentissage et sauvegardent le meilleur modèle automatiquement — sans eux, vous devriez monitorter manuellement et décider quand arrêter.

📖 Terme : Epoch et Batch

Définition : Un epoch est un passage complet sur tout le dataset d'entraînement. Un batch est un petit sous-ensemble de données traité en une itération d'optimisation (forward + backward). Avec 50 000 images d'entraînement et batch_size=64, il y a ~781 itérations (batches) par epoch.

But : Diviser les données pour permettre l'optimisation stochastique (SGD) et la parallélisation GPU.

Pourquoi ici : Comprendre epochs et batches est crucial pour interpréter l'entraînement. Chaque batch produit un gradient, optimizer.step() met à jour les poids. Au bout de 781 batches, on a complété 1 epoch. Augmenter le batch_size réduit le bruit des gradients mais utilise plus de mémoire ; réduire l'augmente le bruit mais permet des learning rates plus faibles.

📖 Terme : Taux d'apprentissage (learning rate)

Définition : Hyperparamètre qui contrôle la taille des mises à jour des poids à chaque itération : poids = poids - learning_rate × gradient. Un taux trop élevé oscille ou diverge ; trop bas converge très lentement.

But : Équilibrer la vitesse de convergence et la stabilité.

Pourquoi ici : Le learning_rate est l'hyperparamètre le plus important. Ici, on utilise 0.001 (1e-3) avec l'optimiseur Adam qui adapte dynamiquement le learning rate par feature. ReduceLROnPlateau réduit le learning rate si la perte stagne — une stratégie courante en pratique.

La boucle fit() gère automatiquement pour chaque batch : 1) forward pass (calcul des prédictions), 2) calcul de la loss (fonction d'erreur), 3) backward pass (backpropagation, calcul des gradients), 4) mise à jour des poids. Keras abstrait tout cela derrière model.fit(), tandis que PyTorch vous demande de coder manuellement cette boucle. TensorFlow/Keras est donc plus haut-niveau et productif pour les cas standards.

Partie 2 — PyTorch

pytorch_cnn.py
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

device = torch.device('cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu')
print(f"Device : {device}")

# ── Transforms et augmentation ──
train_transforms = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomCrop(32, padding=4),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.4914, 0.4822, 0.4465],
        std=[0.2023, 0.1994, 0.2010]
    )
])
test_transforms = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.4914, 0.4822, 0.4465], std=[0.2023, 0.1994, 0.2010])
])

# ── Datasets et DataLoaders ──
train_set = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=train_transforms)
test_set = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=test_transforms)

train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=4)
test_loader = DataLoader(test_set, batch_size=64, shuffle=False, num_workers=4)
📖 Terme : PyTorch DataLoader

Définition : Classe PyTorch qui itère sur un dataset en batches, avec batching automatique, shuffling (mélange), et parallel data loading (multi-workers) qui chargent les données sur CPU en parallèle.

But : Gérer efficacement le chargement, l'augmentation et la préparation des données pendant l'entraînement sans goulot d'étranglement.

Pourquoi ici : shuffle=True en train mélange aléatoirement les données chaque epoch — c'est crucial pour l'optimisation stochastique (SGD). Les num_workers parallélisent le chargement et l'augmentation des données sur CPU tandis que le GPU traite les batches — double speedup.

pytorch_model.py
# ── Architecture CNN PyTorch ──
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()

        self.features = nn.Sequential(
            # Bloc 1
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.Conv2d(32, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),
            nn.Dropout2d(0.25),
            # Bloc 2
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2, 2),
            nn.Dropout2d(0.25),
        )

        self.classifier = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),  # Global Average Pooling
            nn.Flatten(),
            nn.Linear(64, 128),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        x = self.features(x)
        return self.classifier(x)

model = CNN().to(device)
print(f"Paramètres : {sum(p.numel() for p in model.parameters()):,}")

# ── Training loop PyTorch ──
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)

def train_epoch(model, loader, optimizer, criterion, device):
    model.train()
    total_loss, correct = 0, 0
    for inputs, labels in loader:
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        correct += (outputs.argmax(1) == labels).sum().item()
    return total_loss / len(loader), correct / len(loader.dataset)

for epoch in range(50):
    train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion, device)
    scheduler.step()
    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1}/50 — Loss: {train_loss:.4f} | Acc: {train_acc*100:.2f}%")

torch.save(model.state_dict(), 'cnn_cifar10.pth')
La boucle d'entraînement PyTorch est explicite et manuelle : 1) itérer sur les batches du DataLoader, 2) forward pass (inputs → model → outputs), 3) calculer la loss (cross-entropy), 4) backward pass (loss.backward() calcule les gradients via autograd), 5) optimizer.step() met à jour les poids. Après chaque epoch, scheduler.step() adapte le learning rate (CosineAnnealingLR ramène progressivement le LR à zéro). C'est plus verbeux que model.fit() en Keras, mais offre plus de contrôle et de clarté pour les cas avancés.

TensorFlow vs PyTorch : différences clés

Comparaison rapide des deux frameworks : TensorFlow / Keras : - API haut niveau très productive (model.fit, model.evaluate) - TensorFlow Serving pour le déploiement production mature - TensorFlow Lite pour le mobile bien soutenu - Meilleur écosystème de déploiement (TFX pipeline) - Moins flexible pour des architectures non-standard (boucle de training très cachée) PyTorch : - Graphe dynamique → plus intuitif pour le debug et experimentation - Préféré dans la recherche académique (la plupart des papers ML publiés) - Plus flexible pour des architectures expérimentales et custom - TorchScript pour la production (mais moins mature que TF Serving) - Boucle d'entraînement manuelle (verbeux mais transparent)
Pour les projets de production en entreprise, TensorFlow/Keras offre une voie de déploiement plus mature et un meilleur écosystème. Pour la recherche et l'expérimentation, PyTorch est plus flexible et dominant dans la communauté ML académique (95% des papers ML utilisent PyTorch).