Formation ML / Deep learning

CNN (réseaux convolutifs)

Intermédiaire 50 min 13 sections

Un filtre glisse sur l'image et apprend lui-même ce qu'il faut regarder, couche après couche, jusqu'aux dix classes.

Objectifs d'apprentissage

  • Comprendre les opérations de convolution et de pooling
  • Connaître l'architecture typique d'un CNN
  • Implémenter un CNN simple avec scikit-learn et visualiser les concepts
  • Interpréter les filtres et les feature maps

Prérequis

Module Réseau de neurones recommandé

Le code de cette page s'exécute dans votre navigateur. Le premier lancement télécharge Python et ses bibliothèques.

Théorie

Pourquoi les CNN pour les images?

Les réseaux de neurones convolutifs (CNN) sont spécialisés pour traiter des données ayant une structure spatiale, comme les images.

Problème avec les réseaux classiques:

  • Une image 28x28 = 784 features
  • Perte de la structure spatiale (voisinage des pixels)
  • Trop de paramètres (explosion combinatoire)

Solution des CNN:

  • Convolution: Détecte des motifs locaux (bords, textures)
  • Pooling: Réduit la dimension, rend robuste aux translations
  • Hiérarchie: Motifs simples → motifs complexes

Applications:

  • Classification d'images (chats vs chiens)
  • Détection d'objets (voitures dans une photo)
  • Segmentation (délimiter les organes en imagerie médicale)
  • Reconnaissance faciale

Avantages:

  • Invariance aux translations (un chat reste un chat ou qu'il soit)
  • Partage des poids (moins de paramètres)
  • Capture la hiérarchie des features
Théorie

Schéma: Architecture d'un CNN

Les couches d'un CNN:

flowchart LR I["Image
28x28x1"] C1["Conv
+ ReLU"] P1["MaxPool"] C2["Conv
+ ReLU"] P2["MaxPool"] F["Flatten"] D["Dense"] O["Sortie
10 classes"] I --> C1 --> P1 --> C2 --> P2 --> F --> D --> O class I ml-node-secondary class O ml-node-accent

Opération de convolution:

flowchart TD subgraph Input ["Image 5x5"] I["1 0 1 0 1
0 1 0 1 0
1 0 1 0 1
0 1 0 1 0
1 0 1 0 1"] end subgraph Filter ["Filtre 3x3"] F["1 0 1
0 1 0
1 0 1"] end subgraph Output ["Feature Map 3x3"] O["Résultat de
la convolution"] end Input -->|"×"| Filter --> Output class Filter ml-node-main class Output ml-node-accent

Le filtre "glisse" sur l'image et calcule un produit scalaire à chaque position.

Exemple concret avec code couleur - Opération de convolution:

Région de l'image (3x3):

$\begin{bmatrix} \textcolor{#3498db}{1} & \textcolor{#3498db}{0} & \textcolor{#3498db}{2} \\ \textcolor{#3498db}{0} & \textcolor{#3498db}{1} & \textcolor{#3498db}{0} \\ \textcolor{#3498db}{1} & \textcolor{#3498db}{0} & \textcolor{#3498db}{1} \end{bmatrix}$

Filtre (kernel) de détection de bords:

$\begin{bmatrix} \textcolor{#9B7AC4}{-1} & \textcolor{#9B7AC4}{0} & \textcolor{#9B7AC4}{1} \\ \textcolor{#9B7AC4}{-1} & \textcolor{#9B7AC4}{0} & \textcolor{#9B7AC4}{1} \\ \textcolor{#9B7AC4}{-1} & \textcolor{#9B7AC4}{0} & \textcolor{#9B7AC4}{1} \end{bmatrix}$

Calcul du produit scalaire (convolution):

$\text{Sortie} = \sum (\text{image} \times \text{filtre})$

$= (\textcolor{#3498db}{1} \times \textcolor{#9B7AC4}{-1}) + (\textcolor{#3498db}{0} \times \textcolor{#9B7AC4}{0}) + (\textcolor{#3498db}{2} \times \textcolor{#9B7AC4}{1})$

$+ (\textcolor{#3498db}{0} \times \textcolor{#9B7AC4}{-1}) + (\textcolor{#3498db}{1} \times \textcolor{#9B7AC4}{0}) + (\textcolor{#3498db}{0} \times \textcolor{#9B7AC4}{1})$

$+ (\textcolor{#3498db}{1} \times \textcolor{#9B7AC4}{-1}) + (\textcolor{#3498db}{0} \times \textcolor{#9B7AC4}{0}) + (\textcolor{#3498db}{1} \times \textcolor{#9B7AC4}{1})$

$= \textcolor{#e74c3c}{-1} + \textcolor{#27ae60}{2} + \textcolor{#e74c3c}{-1} + \textcolor{#27ae60}{1} = \textcolor{#27ae60}{\mathbf{1}}$

Interprétation: Valeur positive = bord vertical détecte!

Légende des couleurs:

  • $\textcolor{#3498db}{Bleu}$: Pixels de l'image
  • $\textcolor{#9B7AC4}{Violet}$: Poids du filtre (kernel)
  • $\textcolor{#27ae60}{Vert}$: Contributions positives
  • $\textcolor{#e74c3c}{Rouge}$: Contributions négatives
Avancé Exercice manuel: À vous de calculer!

Objectif: Comprendre les opérations CNN à la main (convolution, pooling).

CONTEXTE

Image 4x4 en niveaux de gris :

| | | | | |---|---|---|---| | 1 | 2 | 3 | 0 | | 0 | 1 | 2 | 3 | | 3 | 0 | 1 | 2 | | 2 | 3 | 0 | 1 |

Filtre (kernel) 2x2 de détection de bords :

| | | |---|---| | 1 | -1 | | 1 | -1 |

PARTIE 1 : Convolution

Convolution = somme des produits élément par élément

1.1) Appliquez le filtre en position (0,0) (coin supérieur gauche) 1.2) Appliquez le filtre en position (0,1) 1.3) Calculez toute la feature map (sortie 3x3)

PARTIE 2 : Max Pooling 2x2

2.1) Appliquez max pooling 2x2 sur la feature map obtenue 2.2) Quelle est la taille de sortie ?

PARTIE 3 : Interprétation

3.1) Que détecte ce filtre (différence gauche-droite) ? 3.2) Pourquoi le pooling est-il utile ?

Avancé Solution de l'exercice manuel

SOLUTION DÉTAILLÉE

PARTIE 1 : Convolution

1.1) Position (0,0) :

$$\begin{bmatrix} 1 & 2 \\ 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & -1 \\ 1 & -1 \end{bmatrix}$$

$$= 1(1) + 2(-1) + 0(1) + 1(-1) = 1 - 2 + 0 - 1 = \textcolor{#e74c3c}{-2}$$

1.2) Position (0,1) :

$$\begin{bmatrix} 2 & 3 \\ 1 & 2 \end{bmatrix} * \begin{bmatrix} 1 & -1 \\ 1 & -1 \end{bmatrix}$$

$$= 2 - 3 + 1 - 2 = \textcolor{#e74c3c}{-2}$$

1.3) Feature map complète (3x3) :

| | | | |---|---|---| | -2 | -2 | 0 | | -2 | -2 | 0 | | 0 | 0 | -2 |

PARTIE 2 : Max Pooling 2x2

2.1) Pooling sur la feature map :

  • Zone haut-gauche : max(-2,-2,-2,-2) = -2
  • Zone haut-droite : max(-2,0,-2,0) = 0
  • Zone bas-gauche : max(-2,0,0,0) = 0
  • Zone bas-droite : max(-2,0,-2,-2) = 0

Wait - la feature map est 3x3, le pooling 2x2 avec stride 2 donne une sortie 1x1 ou 2x2 avec padding.

Avec stride 2 sans padding sur 3x3 :

$$\text{Sortie} = \lfloor\frac{3-2}{2}\rfloor + 1 = 1$$

Résultat : $\boxed{\begin{bmatrix} -2 \end{bmatrix}}$ (1x1)

2.2) Taille : $\textcolor{#27ae60}{1 \times 1}$

PARTIE 3 : Interprétation

3.1) Le filtre détecte les transitions horizontales (bords verticaux).

  • Valeur négative = plus clair à gauche
  • Valeur positive = plus clair à droite
  • Zéro = pas de transition

3.2) Max Pooling :

  • Réduit la taille (moins de paramètres)
  • Conserve les features les plus fortes
  • Invariance aux petites translations

$\boxed{\text{CNN = Convolution (détection) + Pooling (réduction)}}$

Légende : $\textcolor{#e74c3c}{Rouge}$: valeurs de convolution

Code

Explorer les données (chiffres)

print("=" * 70)
print("       EXPLORATION DU DATASET DE CHIFFRES MANUSCRITS")
print("=" * 70)

print("""
Les CNN sont nes pour traiter des IMAGES. Nous allons travailler avec
un dataset classique: des chiffres manuscrits (version simplifiee).

POURQUOI CE DATASET EST PARFAIT POUR APPRENDRE LES CNN:
  • Images petites (4x4 = 16 pixels) → rapide a traiter
  • 10 classes (0-9) → classification multiclasse
  • Patterns visuels distincts → facile a interpreter
""")

print("\n" + "=" * 70)
print("1. APERCU DU DATASET")
print("=" * 70)
display(df.head(10), title="Premieres lignes du dataset de chiffres")

print("""
STRUCTURE DES DONNEES:
  • Chaque ligne = une image de chiffre
  • Colonnes pixel_* = valeurs des pixels (0=noir, 15=blanc)
  • Colonne 'label' = le chiffre represente (0-9)
""")

print("\n" + "=" * 70)
print("2. DIMENSIONS")
print("=" * 70)

# Separer features et labels
X = df.drop('label', axis=1).values
y = df['label'].values

n_samples, n_pixels = X.shape
img_size = int(np.sqrt(n_pixels))

print(f"""
DIMENSIONS DU DATASET:
  • Nombre d'images: {n_samples}
  • Pixels par image: {n_pixels}
  • Taille d'une image: {img_size}x{img_size} pixels

NOTE: En production, les images sont plus grandes:
  • MNIST: 28x28 = 784 pixels
  • CIFAR-10: 32x32x3 = 3 072 valeurs
  • ImageNet: 224x224x3 = 150 528 valeurs!
""")

print("\n" + "=" * 70)
print("3. DISTRIBUTION DES CLASSES")
print("=" * 70)

print("""
Combien d'exemples avons-nous pour chaque chiffre?
""")

unique, counts = np.unique(y, return_counts=True)
print("-" * 45)
print(f"{'Chiffre':<10} {'Nb images':<12} {'Distribution'}")
print("-" * 45)

for digit, count in zip(unique, counts):
    pct = count / len(y) * 100
    bar = "█" * int(pct / 2)
    print(f"{digit:<10} {count:<12} {pct:5.1f}% {bar}")

print("-" * 45)
print(f"{'TOTAL':<10} {len(y):<12} 100.0%")

# Verifier l'equilibre
balance_ratio = max(counts) / min(counts)
print(f"""

ANALYSE DE L'EQUILIBRE:
  • Classe la plus frequente: {max(counts)} images
  • Classe la moins frequente: {min(counts)} images
  • Ratio: {balance_ratio:.2f}
""")

if balance_ratio < 1.5:
    print("  → Dataset bien equilibre! Toutes les classes sont representees.")
else:
    print("  → Desequilibre detecte. Certaines classes sont sous-representees.")

print("\n" + "=" * 70)
print("4. STATISTIQUES DES PIXELS")
print("=" * 70)

print(f"""
VALEURS DES PIXELS:
  • Minimum: {X.min():.0f} (noir complet)
  • Maximum: {X.max():.0f} (blanc complet)
  • Moyenne: {X.mean():.2f}
  • Ecart-type: {X.std():.2f}

INTERPRETATION:
  Les valeurs sont entre 0 (noir) et ~16 (blanc).
  Les pixels forment des MOTIFS que le CNN va apprendre a reconnaitre.
""")

print("\n" + "=" * 70)
print("              PRET POUR LA VISUALISATION DES CHIFFRES!")
print("=" * 70)
Ctrl+Entrée

Cliquez sur « Exécuter » pour voir le résultat.

Code

Visualiser les chiffres

print("=" * 70)
print("       VISUALISATION DES CHIFFRES MANUSCRITS")
print("=" * 70)

print("""
Visualisons les donnees pour comprendre ce que le CNN doit apprendre.
Chaque chiffre a des caracteristiques visuelles distinctes.
""")

# Separer features et labels
X = df.drop('label', axis=1).values
y = df['label'].values
img_size = int(np.sqrt(X.shape[1]))

print("\n" + "=" * 70)
print("1. UN EXEMPLE DE CHAQUE CHIFFRE (0-9)")
print("=" * 70)

fig, axes = plt.subplots(2, 5, figsize=(14, 6))

for i, ax in enumerate(axes.flat):
    # Trouver un exemple du chiffre i
    idx = np.where(y == i)[0][0]
    image = X[idx].reshape(img_size, img_size)

    ax.imshow(image, cmap='gray', interpolation='nearest')
    ax.set_title(f'Chiffre: {i}', fontsize=12, fontweight='bold')
    ax.axis('off')

    # Ajouter une bordure coloree
    for spine in ax.spines.values():
        spine.set_edgecolor('#9B7AC4')
        spine.set_linewidth(2)

plt.suptitle('Exemples de chiffres manuscrits (4x4 pixels)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

print("""
OBSERVATIONS:
  • Chaque chiffre a une forme caracteristique
  • Les pixels clairs (blancs) dessinent le chiffre
  • Les pixels sombres (noirs) forment le fond

CE QUE LE CNN DOIT APPRENDRE:
  Le CNN va detecter des MOTIFS (patterns) qui distinguent chaque chiffre:
  - Lignes verticales: 1, 4, 7
  - Boucles: 0, 6, 8, 9
  - Angles: 4, 7
  - Courbes: 2, 3, 5, 6, 9
""")

print("\n" + "=" * 70)
print("2. VARIABILITE D'UN MEME CHIFFRE")
print("=" * 70)

print("""
Un meme chiffre peut etre ecrit de differentes facons.
Le CNN doit etre ROBUSTE a ces variations!
""")

# Montrer plusieurs exemples du chiffre 3
target_digit = 3
indices = np.where(y == target_digit)[0][:8]

fig, axes = plt.subplots(1, 8, figsize=(14, 2.5))
fig.suptitle(f'8 facons differentes d\'ecrire le chiffre {target_digit}', fontsize=13, fontweight='bold')

for ax, idx in zip(axes, indices):
    image = X[idx].reshape(img_size, img_size)
    ax.imshow(image, cmap='gray', interpolation='nearest')
    ax.axis('off')

plt.tight_layout()
plt.show()

print("""
DEFI POUR LE CNN:
  Malgre ces differences, le CNN doit reconnaitre
  que tous ces exemples representent le meme chiffre!

C'est la que la CONVOLUTION aide:
  → Detecte les motifs LOCAUX (bords, courbes)
  → Invariante a la position (grace au pooling)
""")

print("\n" + "=" * 70)
print("3. VISUALISATION EN GRILLE DE PIXELS")
print("=" * 70)

print("""
Regardons un chiffre pixel par pixel pour comprendre
comment les valeurs numeriques forment l'image:
""")

# Prendre le premier exemple
image = X[0].reshape(img_size, img_size)
label = y[0]

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Image avec valeurs
ax1 = axes[0]
im = ax1.imshow(image, cmap='gray', interpolation='nearest')
ax1.set_title(f'Chiffre {label} - Valeurs des pixels', fontsize=12, fontweight='bold')

# Afficher les valeurs
for i in range(img_size):
    for j in range(img_size):
        color = 'white' if image[i, j] < 8 else 'black'
        ax1.text(j, i, f'{image[i,j]:.0f}', ha='center', va='center',
                color=color, fontsize=10, fontweight='bold')
ax1.axis('off')

# Histogramme des valeurs
ax2 = axes[1]
ax2.hist(image.flatten(), bins=16, range=(0, 16), color='#9B7AC4', edgecolor='white', alpha=0.7)
ax2.set_xlabel('Valeur du pixel', fontsize=11)
ax2.set_ylabel('Frequence', fontsize=11)
ax2.set_title('Distribution des valeurs de pixels', fontsize=12, fontweight='bold')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"""
LECTURE DU GRAPHIQUE:
  • Gauche: l'image avec la valeur numerique de chaque pixel
  • Droite: histogramme montrant la repartition des valeurs

Le chiffre est "dessine" par les pixels de haute valeur (clairs).
""")

print("\n" + "=" * 70)
print("              VISUALISATION TERMINEE!")
print("=" * 70)
Ctrl+Entrée

Cliquez sur « Exécuter » pour voir le résultat.

Contenu verrouillé
6 / 13

Continuez votre apprentissage

Vous avez exploré 6 sections de ce module. Connectez-vous pour débloquer le reste du cours, incluant les exercices pratiques et les solutions.

Ctrl ou Cmd plus Entrée exécute le code. Tab indente ; Échap puis Tab quitte l'éditeur.