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.
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.
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.
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']
)
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.
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.
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.
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).
# ── 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}%")
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.
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.
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.
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)
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.
# ── 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')