Skip to main content

Réseaux de Neurones Convolutifs (CNNs)

Introduction aux CNNs

CNN

Les réseaux de neurones convolutifs (CNNs, ou ConvNets) sont une classe de réseaux de neurones profonds qui ont révolutionné le domaine de la vision par ordinateur. Bien qu'ils soient particulièrement adaptés au traitement des images, ils sont également utilisés avec succès pour d'autres types de données structurées, comme le son (spectrogrammes) ou le texte (séquences de mots).

Les Clés de l'Efficacité des CNNs

Les CNNs résolvent ces problèmes grâce à trois concepts clés :

  1. Convolution : Au lieu de connecter chaque neurone à tous les pixels de l'image, on utilise des filtres (ou noyaux) de convolution. Un filtre est une petite matrice de poids qui "glisse" sur l'image. À chaque position, on calcule le produit scalaire entre le filtre et la petite région de l'image sur laquelle il se trouve. Cela permet :

    • De réduire considérablement le nombre de paramètres (les poids du filtre sont partagés pour toutes les positions).
    • D'extraire des caractéristiques locales (le filtre ne "voit" qu'une petite partie de l'image à la fois).
    • D'obtenir une certaine invariance par translation (si un motif apparaît à différents endroits de l'image, le même filtre le détectera).
  2. Pooling (Sous-échantillonnage) : Après la convolution, on applique souvent une opération de pooling (par exemple, max pooling). Cela consiste à réduire la taille de la carte de caractéristiques (la sortie de la convolution) en prenant, par exemple, la valeur maximale dans des petites fenêtres (par exemple, 2x2). Cela permet :

    • De réduire encore le nombre de paramètres et le coût de calcul.
    • D'introduire une invariance supplémentaire aux petites translations.
    • D'augmenter le champ réceptif des neurones (la taille de la région de l'image d'entrée qu'ils "voient").
  3. Couches multiples : En empilant plusieurs couches de convolution et de pooling, les CNNs apprennent des caractéristiques de plus en plus complexes et abstraites :

    • Les premières couches détectent des caractéristiques simples (bords, coins).
    • Les couches suivantes combinent ces caractéristiques pour détecter des motifs plus complexes (textures, formes).
    • Les dernières couches combinent ces motifs pour détecter des objets entiers.

Pourquoi les CNNs sont-ils si Efficaces pour les Images ?

Les images ont des propriétés spécifiques qui rendent les CNNs particulièrement adaptés :

  1. Structure locale : Les pixels proches les uns des autres sont généralement plus corrélés que les pixels éloignés. Un objet est composé de sous-parties (par exemple, un visage est composé d'yeux, d'un nez, d'une bouche), qui sont elles-mêmes composées de caractéristiques plus petites (bords, coins, textures). Les CNNs exploitent cette structure locale en utilisant des filtres locaux (noyaux de convolution).

  2. Invariance par translation : Un objet reste le même objet, même s'il est déplacé dans l'image. Les CNNs apprennent des caractéristiques qui sont (en partie) invariantes à la position de l'objet dans l'image.

  3. Hiérarchie de caractéristiques : Les images ont une structure hiérarchique. Les pixels se combinent pour former des arêtes, les arêtes se combinent pour former des motifs, les motifs se combinent pour former des parties d'objets, et ainsi de suite. Les CNNs, avec leurs multiples couches, sont capables d'apprendre cette hiérarchie de caractéristiques.

Différences avec les Réseaux Entièrement Connectés (Dense)

Dans un réseau entièrement connecté (comme ceux que nous avons utilisés jusqu'à présent), chaque neurone d'une couche est connecté à tous les neurones de la couche précédente. Cela pose plusieurs problèmes pour les images :

  • Explosion du nombre de paramètres : Une image, même petite (par exemple, 28x28 pixels en niveaux de gris), a déjà 784 valeurs d'entrée. Si la première couche cachée a 100 neurones, cela fait déjà 784 * 100 = 78400 poids, plus 100 biais. Pour des images plus grandes et en couleur, ce nombre devient rapidement ingérable.
  • Perte de l'information spatiale : En aplatissant l'image en un vecteur, on perd complètement l'information sur la structure 2D de l'image (la proximité des pixels).

flatten

  • Non-invariance par translation : Un réseau entièrement connecté apprendrait des caractéristiques différentes selon leur position dans l'image.
info

Les CNNs ont permis des avancées majeures dans de nombreux domaines, notamment la classification d'images, la détection d'objets, la segmentation d'images, la reconnaissance faciale, et bien d'autres. Comprendre leur fonctionnement est essentiel pour quiconque souhaite travailler en vision par ordinateur ou, plus généralement, avec des données structurées en grilles.

Convolution

La convolution est l'opération fondamentale des réseaux de neurones convolutifs (CNNs). C'est ce qui donne aux CNNs leur puissance pour traiter des données structurées en grilles, comme les images.

Intuition : Extraction de Caractéristiques Locales

Imaginez que vous ayez une image et que vous vouliez détecter des caractéristiques locales (features), comme des bords, des coins, des textures, etc. Une approche naïve serait de regarder chaque pixel individuellement, mais cela ne tiendrait pas compte du contexte spatial (les pixels voisins).

La convolution, elle, consiste à faire "glisser" un petit filtre (aussi appelé noyau, kernel) sur l'image, et à calculer une valeur pour chaque position du filtre. Ce filtre agit comme un "détecteur de caractéristique".

Analogie : Tampon Encreur

Imaginez un tampon encreur avec un motif (par exemple, une petite croix). Vous appliquez ce tampon à différents endroits d'une feuille de papier. À chaque endroit, le tampon laisse une empreinte (plus ou moins marquée) qui dépend de la correspondance entre le motif du tampon et le motif présent sur la feuille à cet endroit.

  • La feuille de papier : C'est votre image d'entrée.
  • Le tampon encreur : C'est le filtre (noyau) de convolution.
  • L'empreinte : C'est la sortie de la convolution pour une position donnée du filtre.
  • Le motif du tampon: Représente les arrêtes, les coins et les textures
  • L'encre: Représente l'activation, c'est-à-dire l'intensité

Opération Mathématique

Mathématiquement, la convolution discrète 2D (pour une image en niveaux de gris) se définit comme suit :

Soit :

  • II : L'image d'entrée (une matrice de taille H×WH \times W).
  • KK : Le filtre (noyau) de convolution (une petite matrice de taille KH×KWK_H \times K_W).
  • (i,j)(i, j) : Les coordonnées d'un pixel dans l'image.
  • (m,n)(m, n) : Les indices du filtre.

La sortie de la convolution, SS, à la position (i,j)(i, j) est donnée par :

S(i,j)=mnI(i+m,j+n)K(m,n)S(i, j) = \sum_{m} \sum_{n} I(i + m, j + n) \cdot K(m, n)

En pratique:

  • On "retourne" souvent le noyau avant de faire le calcul, mais par abus de langage on parle quand même de convolution.
  • On calcule un produit scalaire entre le noyau et la zone de l'image.

Explication :

  • On centre le filtre KK sur le pixel (i,j)(i, j) de l'image II.
  • On multiplie chaque élément du filtre par l'élément correspondant de l'image (sous le filtre).
  • On somme tous ces produits.
  • Le résultat est la valeur de la sortie SS à la position (i,j)(i, j).
  • On répète ce processus pour toutes les positions possibles du filtre sur l'image.

Exemple Numérique (1D pour simplifier)

Imaginons une "image" 1D (un vecteur) :

I = [1, 2, 3, 4, 5]

Et un filtre 1D :

K = [-1, 0, 1]

Calculons la convolution (en supposant un "padding" de 0, c'est-à-dire qu'on ajoute des zéros aux extrémités de l'image) :

  • Position 1 : (-1 * 0) + (0 * 1) + (1 * 2) = 2
  • Position 2 : (-1 * 1) + (0 * 2) + (1 * 3) = 2
  • Position 3 : (-1 * 2) + (0 * 3) + (1 * 4) = 2
  • Position 4 : (-1 * 3) + (0 * 4) + (1 * 5) = 2

La sortie serait : S = [2, 2, 2, 2]

Dans cet exemple, le filtre [-1, 0, 1] calcule une approximation de la dérivée de l'image 1D. Il détecte les variations d'intensité.

Convolution en 2D (Image)

Pour une image 2D, le principe est le même, mais le filtre est aussi une matrice 2D.

Exemple de filtre 3x3 :

K = [[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]

Ce filtre particulier est un filtre de détection de contours (il calcule une approximation du Laplacien de l'image).

Termes Importants

  • Filtre (Noyau, Kernel) : La petite matrice qui glisse sur l'image. Ses valeurs sont les paramètres appris par le CNN.
  • Carte d'activation (Feature Map) : La sortie de la convolution pour un filtre donné. C'est une "image" qui représente l'activation de ce filtre à différents endroits de l'image d'entrée.
info

La convolution est une opération linéaire. C'est la combinaison de la convolution avec des fonctions d'activation non linéaires qui donne aux CNNs leur puissance.

Visualisation

Imaginons une image représentée par une matrice 5x5 de pixels, où chaque pixel a une valeur d'intensité (par exemple, en niveaux de gris de 0 à 255). Nous allons appliquer un filtre (noyau) de convolution 3x3 sur cette image.

Image 5x5 (I) :

[[10, 15, 20, 25, 30],
[40, 45, 50, 55, 60],
[70, 75, 80, 85, 90],
[100, 105, 110, 115, 120],
[130, 135, 140, 145, 150]]

Filtre 3x3 (K) (exemple de filtre de détection de contours horizontaux):

[[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]]

Le calcul de la convolution pour une petite zone (en haut à gauche de l'image) :

[10, 15, 20]   [-1, -2, -1]   
[40, 45, 50] * [ 0, 0, 0] = 10*-1 + 15*-2 + 20*-1 + 40*0 + 45*0 + 50*0 + 70*1 + 75*2 + 80*1 = 240
[70, 75, 80] [ 1, 2, 1]

Pooling (Sous-Échantillonnage)

Après la convolution, une autre opération clé dans les CNNs est le pooling (ou sous-échantillonnage, subsampling). Le pooling réduit la dimensionnalité des cartes de caractéristiques (feature maps) produites par la convolution, tout en préservant (en principe) les informations les plus importantes.

Qu'est-ce que le Pooling ?

Le pooling consiste à :

  1. Faire glisser une petite fenêtre (par exemple, 2x2 pixels) sur la carte de caractéristiques.
  2. Pour chaque position de la fenêtre, calculer une seule valeur à partir des valeurs présentes dans la fenêtre.
  3. Remplacer la fenêtre par cette valeur unique.

Il existe plusieurs types de pooling, mais les plus courants sont :

  • Max Pooling : Prend la valeur maximale dans la fenêtre.
  • Average Pooling : Prend la valeur moyenne dans la fenêtre.

Exemple : Max Pooling 2x2

Imaginons une carte de caractéristiques 4x4 :

[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]

Appliquons un max pooling 2x2 (sans chevauchement, nous y reviendrons) :

  1. Fenêtre 1 : [[1, 2], [5, 6]] -> Max = 6
  2. Fenêtre 2 : [[3, 4], [7, 8]] -> Max = 8
  3. Fenêtre 3 : [[9, 10], [13, 14]] -> Max = 14
  4. Fenêtre 4 : [[11, 12], [15, 16]] -> Max = 16

La sortie du max pooling est :

[[ 6,  8],
[14, 16]]

La carte de caractéristiques a été réduite de moitié dans chaque dimension (de 4x4 à 2x2).

Pourquoi le Pooling est-il Utile ?

  1. Réduction de la dimensionnalité : Le pooling réduit considérablement le nombre de paramètres et le coût de calcul dans les couches suivantes du réseau.
  2. Invariance aux petites translations : Si une caractéristique est légèrement déplacée dans l'image d'entrée, la sortie du max pooling a de bonnes chances de rester la même (surtout si le déplacement est inférieur à la taille de la fenêtre de pooling). Cela rend le modèle plus robuste aux petites variations de position des objets.
  3. Augmentation du champ réceptif : En réduisant la taille des cartes de caractéristiques, le pooling augmente le champ réceptif des neurones des couches suivantes. Cela signifie qu'un neurone d'une couche supérieure "voit" une plus grande région de l'image d'entrée qu'un neurone d'une couche inférieure.
  4. Extraction des caractéristiques les plus saillantes : Le max pooling, en particulier, a tendance à extraire les caractéristiques les plus fortes (les valeurs les plus élevées dans chaque fenêtre).

Paramètres du Pooling

  • Taille de la fenêtre (pool size) : Généralement 2x2 ou 3x3.
  • Enjambée (stride) : Le nombre de pixels dont on décale la fenêtre à chaque étape. Si l'enjambée est égale à la taille de la fenêtre, il n'y a pas de chevauchement entre les fenêtres. Si elle est plus petite, il y a chevauchement.
  • Type de pooling : Max pooling ou average pooling (ou autres variantes moins courantes).
  • Padding : Rarement utilisé avec le pooling.

Pooling en Keras

En Keras, vous pouvez ajouter des couches de pooling à votre modèle avec :

  • tf.keras.layers.MaxPooling2D (pour les images)
  • tf.keras.layers.AveragePooling2D (pour les images)
  • tf.keras.layers.MaxPooling1D (pour les séquences 1D)
  • tf.keras.layers.AveragePooling1D (pour les séquences 1D)

Exemple :

model = tf.keras.Sequential([
tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)), # Max pooling 2x2
tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(10, activation='softmax')
])

Dans cet exemple, MaxPooling2D(pool_size=(2, 2)) réduit de moitié la taille des cartes de caractéristiques produites par les couches Conv2D.

Max Pooling vs. Average Pooling

  • Max Pooling :
    • Plus courant en pratique.
    • Tendance à mieux préserver les caractéristiques saillantes.
    • Introduit une invariance plus forte aux petites translations.
  • Average Pooling :
    • Moins utilisé pour les images.
    • Peut être utile pour certaines tâches où l'on veut préserver l'information moyenne (par exemple, dans certains cas en traitement du signal).
info

Le pooling est une opération clé des CNNs. Il réduit la dimensionnalité, introduit une invariance aux petites translations et aide à extraire les caractéristiques les plus importantes. Le max pooling est généralement préféré à l'average pooling pour les images.

Padding

Le padding est une technique utilisée en convolution (et parfois en pooling) qui consiste à ajouter des pixels (généralement de valeur zéro) sur les bords de l'image d'entrée avant d'appliquer le filtre de convolution.

Pourquoi le Padding est-il Utile ?

Sans padding, il y a deux problèmes principaux :

  1. Réduction de la taille des cartes de caractéristiques : À chaque couche de convolution, la taille de la carte de caractéristiques diminue. Si le filtre est de taille F×FF \times F, la sortie sera plus petite que l'entrée de (F1)(F - 1) pixels en largeur et en hauteur (si l'enjambée est de 1). Avec de nombreuses couches de convolution, la carte de caractéristiques deviendrait très petite, limitant la profondeur du réseau.

  2. Perte d'information sur les bords : Les pixels situés sur les bords de l'image d'entrée sont moins utilisés que les pixels centraux lors de la convolution. Le filtre ne "voit" les pixels du bord qu'un petit nombre de fois, alors qu'il "voit" les pixels centraux beaucoup plus souvent. Cela signifie que l'information contenue dans les pixels du bord est sous-utilisée, et peut être perdue.

Le padding permet de résoudre ces deux problèmes.

Types de Padding

Il existe plusieurs types de padding, mais les deux plus courants sont :

1. Padding Zéro (Zero Padding)

C'est le type de padding le plus courant. On ajoute des pixels de valeur zéro autour de l'image.

  • 'valid' (pas de padding) : C'est le comportement par défaut si vous ne spécifiez pas de padding. La sortie est plus petite que l'entrée.

  • 'same' (padding pour conserver la taille) : On ajoute suffisamment de zéros pour que la sortie ait la même taille que l'entrée (si l'enjambée est de 1). C'est le type de padding le plus utilisé en pratique.

    Pour un filtre de taille F×FF \times F (où FF est impair), on ajoute (F1)/2(F - 1) / 2 pixels de zéros de chaque côté.

    Exemple : Si vous avez une image de 28x28 et un filtre 3x3, avec padding='same', Keras ajoutera une ligne et une colonne de zéros de chaque côté de l'image, résultant en une image "paddée" de 30x30. La convolution produira alors une sortie de 28x28.

2. Autres Types de Padding (Moins Courants)

  • Réplication des bords : On répète les valeurs des pixels du bord.
  • Réflexion : On reflète l'image sur les bords.
  • Padding constant : On ajoute des pixels avec une valeur constante (pas nécessairement zéro).

Padding en Keras

En Keras, vous spécifiez le type de padding dans les couches de convolution (par exemple, Conv2D) :

import tensorflow as tf

# Pas de padding (sortie plus petite que l'entrée)
model = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), padding='valid', activation='relu')

# Padding 'same' (sortie de même taille que l'entrée, si stride=1)
model = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), padding='same', activation='relu')
  • padding='valid' : Pas de padding (équivalent à ne pas spécifier padding).
  • padding='same' : Padding zéro pour que la sortie ait la même taille que l'entrée (si l'enjambée, stride, est de 1).

Exemple : Impact du Padding

Imaginons une image 5x5 et un filtre 3x3.

Sans padding ('valid') :

La sortie sera une carte de caractéristiques 3x3.

Avec padding='same' :

Keras ajoutera une ligne et une colonne de zéros de chaque côté de l'image, résultant en une image "paddée" de 7x7. La sortie de la convolution sera alors une carte de caractéristiques 5x5 (la même taille que l'entrée).

info

Le padding est une technique simple mais essentielle dans les CNNs. Le padding 'same' (avec des zéros) est le plus couramment utilisé, car il permet de conserver la taille des cartes de caractéristiques et de ne pas perdre d'information sur les bords.

Visualisation

Voici une illustration du padding "same" avec un filtre 3x3 et une image 5x5:

Image originale (5x5):

1  2  3  4  5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25

Image paddée avec des 0 (7x7) pour un padding "same" et un filtre 3x3:

0  0  0  0  0  0  0
0 1 2 3 4 5 0
0 6 7 8 9 10 0
0 11 12 13 14 15 0
0 16 17 18 19 20 0
0 21 22 23 24 25 0
0 0 0 0 0 0 0

Stride (Enjambée)

Le stride (ou enjambée) est un paramètre qui contrôle la manière dont le filtre de convolution se déplace sur l'image d'entrée. Il détermine de combien de pixels le filtre se décale à chaque étape.

Qu'est-ce que le Stride ?

Jusqu'à présent, nous avons implicitement supposé que le filtre se déplaçait d'un pixel à la fois (enjambée de 1). Mais on peut utiliser une enjambée plus grande.

  • Stride = 1 : Le filtre se déplace d'un pixel à la fois. C'est le cas le plus courant.
  • Stride = 2 : Le filtre se déplace de deux pixels à la fois. La carte de caractéristiques de sortie sera environ deux fois plus petite (en largeur et en hauteur) que la carte d'entrée (en supposant qu'il n'y a pas de padding).
  • Stride = s : Le filtre se déplace de s pixels à la fois.

Pourquoi Utiliser un Stride > 1 ?

  1. Réduction de la dimensionnalité : Un stride plus grand réduit la taille de la carte de caractéristiques de sortie. Cela peut être utile pour réduire le coût de calcul et le nombre de paramètres dans les couches suivantes du réseau.
  2. Augmentation du champ réceptif : Avec un stride plus grand, chaque neurone de la couche suivante "voit" une plus grande région de l'image d'entrée (à travers le filtre).

Exemple : Stride = 1 vs. Stride = 2

Imaginons une image 5x5 et un filtre 3x3, sans padding.

Stride = 1 :

La sortie sera une carte de caractéristiques 3x3. Le filtre est appliqué à 9 positions différentes.

Stride = 2 :

La sortie sera une carte de caractéristiques 2x2. Le filtre est appliqué à seulement 4 positions différentes.

Voici une illustration du calcul (en supposant un padding 'valid'):

StrideImage (I)Filtre (K)Calcul de l'output (S)
1[[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]]
[[a, b, c],
[d, e, f],
[g, h, i]]
S[0, 0] = a*1 + b*2 + c*3 + d*6 + e*7 + f*8 + g*11 + h*12 + i*13
S[0, 1] = a*2 + b*3 + c*4 + d*7 + e*8 + f*9 + g*12 + h*13 + i*14
S[0, 2] = ...
S[1, 0] = ...
... (9 calculs au total)
2[[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]]
[[a, b, c],
[d, e, f],
[g, h, i]]
S[0, 0] = a*1 + b*2 + c*3 + d*6 + e*7 + f*8 + g*11 + h*12 + i*13
S[0, 1] = a*3 + b*4 + c*5 + d*8 + e*9 + f*10 + g*13 + h*14 + i*15
S[1, 0] = a*11 + b*12 + c*13 + d*16 + e*17 + f*18 + g*21 + h*22 + i*23
S[1, 1] = ... (4 calculs au total)

Stride en Keras

En Keras, vous spécifiez le stride dans les couches de convolution (par exemple, Conv2D) :

import tensorflow as tf

# Stride de 1 (par défaut)
model = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), strides=(1, 1), padding='valid', activation='relu')

# Stride de 2
model = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), strides=(2, 2), padding='valid', activation='relu')
  • strides=(1, 1) : Stride de 1 en largeur et en hauteur (cas par défaut).
  • strides=(2, 2) : Stride de 2 en largeur et en hauteur.
  • strides=(1, 2) : Stride de 1 en largeur et de 2 en hauteur (moins courant).

Relation entre Taille d'Entrée, Taille de Sortie, Padding, Stride et Taille du Filtre

Il existe une formule qui relie la taille de la carte de caractéristiques de sortie (OO) à la taille de l'entrée (II), la taille du filtre (FF), le padding (PP) et le stride (SS) :

O=IF+2PS+1O = \frac{I - F + 2P}{S} + 1

Cette formule est valable pour chaque dimension (largeur et hauteur).

Exemple :

  • Image d'entrée : 28x28
  • Filtre : 3x3
  • Padding : 'same' (donc P=(F1)/2=1P = (F - 1) / 2 = 1)
  • Stride : 2

O=283+212+1=272+1=13.5+1=14.5O = \frac{28 - 3 + 2 * 1}{2} + 1 = \frac{27}{2} + 1 = 13.5 + 1 = 14.5

Comme la taille de sortie doit être un entier, Keras arrondit vers le bas dans ce cas. La sortie sera donc de taille 14x14.

Conseils d'Utilisation

  • Stride = 1 : C'est le cas le plus courant. Il permet d'extraire des caractéristiques à toutes les positions de l'image.
  • Stride > 1 :
    • Utilisé pour réduire la dimensionnalité (alternative au pooling).
    • Peut être utilisé dans les premières couches d'un CNN pour réduire rapidement la taille des cartes de caractéristiques.
    • Peut être utilisé pour augmenter le champ réceptif.
  • Stride < Taille du filtre : C'est le cas le plus courant (chevauchement).
  • Stride = Taille du filtre : Pas de chevauchement.
  • Stride > Taille du filtre : "Trous" dans la carte de caractéristiques (rarement utilisé, sauf dans les convolutions "à trous" ou "dilatées").
info

Le stride est un paramètre important des couches de convolution. Un stride plus grand réduit la dimensionnalité de la sortie, mais peut aussi faire perdre de l'information. Il faut trouver le bon équilibre en fonction de la tâche et des données.

Couches Multiples : Apprentissage de Caractéristiques Hiérarchiques

L'un des concepts clés des réseaux de neurones convolutifs (et du Deep Learning en général) est l'idée d'empiler plusieurs couches pour apprendre des représentations hiérarchiques des données.

Principe

Dans un CNN, on ne se contente généralement pas d'une seule couche de convolution suivie d'une couche de pooling. On enchaîne plusieurs de ces blocs (convolution + activation + pooling), souvent suivis de quelques couches entièrement connectées (Dense) à la fin.

Pourquoi ?

  • Premières couches : Les premières couches de convolution (proches de l'entrée) apprennent à détecter des caractéristiques de bas niveau très locales, comme :

    • Des bords (horizontaux, verticaux, diagonaux).
    • Des coins.
    • Des petites textures.
    • Des taches de couleur.

    Ces caractéristiques sont détectées par de petits filtres (par exemple, 3x3 ou 5x5).

  • Couches intermédiaires : Les couches suivantes combinent ces caractéristiques de bas niveau pour détecter des caractéristiques de plus haut niveau, comme :

    • Des formes plus complexes (cercles, carrés, etc.).
    • Des parties d'objets (un œil, une roue, une oreille, etc.).
  • Couches profondes : Les couches encore plus profondes combinent ces caractéristiques de niveau intermédiaire pour détecter des caractéristiques encore plus abstraites, comme :

    • Des objets entiers (un visage, une voiture, un chien, etc.).
    • Des concepts de haut niveau.
  • Couche de sortie:

    • La couche de sortie prend les caractéristiques et les envoie vers un classificateur.

Chaque couche apprend une représentation différente et de plus en plus abstraite des données d'entrée.

Analogie

Imaginez que vous appreniez à reconnaître des objets dans une image :

  1. Niveau 1 (bas niveau) : Vous apprenez à identifier les bords, les lignes, les changements de couleur.
  2. Niveau 2 (intermédiaire) : Vous apprenez à combiner ces bords et ces lignes pour former des formes simples (cercles, carrés, triangles).
  3. Niveau 3 (intermédiaire) : Vous apprenez à combiner ces formes simples pour former des parties d'objets (un œil, une roue, une porte).
  4. Niveau 4 (haut niveau) : Vous apprenez à combiner ces parties d'objets pour reconnaître des objets entiers (un visage, une voiture, une maison).

Un CNN fait quelque chose de similaire, de manière automatique, en apprenant les filtres appropriés à chaque couche.

Champ Réceptif

Un concept important est le champ réceptif (receptive field) d'un neurone. Le champ réceptif d'un neurone est la région de l'image d'entrée qui influence la sortie de ce neurone.

  • Dans la première couche de convolution, le champ réceptif d'un neurone est petit (égal à la taille du filtre).
  • Dans les couches suivantes, le champ réceptif augmente, car chaque neurone "voit" une région de la carte de caractéristiques de la couche précédente, qui elle-même correspond à une région plus grande de l'image d'entrée.
  • Le pooling augmente aussi le champ réceptif.

Grâce à l'empilement de couches et à l'augmentation progressive du champ réceptif, les CNNs peuvent apprendre des caractéristiques globales à partir d'informations locales.

info

L'empilement de couches de convolution et de pooling permet aux CNNs d'apprendre des représentations hiérarchiques des données, des caractéristiques locales de bas niveau aux caractéristiques globales de haut niveau. C'est l'une des clés de leur succès en vision par ordinateur.

Architecture d'un CNN Typique

Bien qu'il existe de nombreuses variantes, un réseau de neurones convolutif (CNN) typique pour la classification d'images suit généralement une structure de base :

1. Entrée (Input) :

  • L'image à traiter.
  • Dimensions : Hauteur x Largeur x Canaux (par exemple, 32x32x3 pour une image couleur CIFAR-10, 28x28x1 pour une image MNIST en niveaux de gris).

2. Bloc(s) Convolutif(s) :

  • Une succession de couches :

    • Convolution (Conv2D) : Applique des filtres (noyaux) à l'entrée pour extraire des caractéristiques locales.
    • Fonction d'activation (généralement ReLU) : Introduit des non-linéarités.
    • Pooling (MaxPooling2D ou AveragePooling2D) : Réduit la dimensionnalité et introduit une invariance aux petites translations.
  • Plusieurs de ces blocs peuvent être empilés.

3. Aplatissement (Flatten) :

  • Transforme la sortie multidimensionnelle du dernier bloc convolutif en un vecteur 1D.

4. Bloc(s) Entièrement Connecté(s) (Dense) :

  • Une ou plusieurs couches Dense (entièrement connectées), souvent avec une fonction d'activation (ReLU).
  • Ces couches combinent les caractéristiques extraites par les blocs convolutifs pour prendre la décision finale.

5. Couche de Sortie (Output) :

  • Une couche Dense avec :
    • Autant de neurones qu'il y a de classes (pour la classification).
    • Une fonction d'activation appropriée :
      • Softmax pour la classification multiclasse.
      • Sigmoïde pour la classification binaire.
      • Aucune activation (linéaire) pour la régression.

Schéma général :

INPUT -> [[CONV -> RELU]*N -> POOLING?]*M -> [FC -> RELU]*K -> FC -> SOFTMAX

Où :

  • INPUT : Image d'entrée.
  • CONV : Couche de convolution.
  • RELU : Fonction d'activation ReLU (ou une variante).
  • POOLING : Couche de pooling (souvent max pooling).
  • FC : Couche entièrement connectée (Dense).
  • SOFTMAX : Fonction d'activation softmax (pour la classification multiclasse).
  • N : Nombre de répétitions du bloc CONV -> RELU (souvent entre 1 et 5).
  • M : Nombre de blocs convolutifs (peut être 0, 1, 2, 3...).
  • K: Nombre de couches cachées.
info

Cette architecture générale est un point de départ. Il existe de nombreuses variations et optimisations possibles. Le choix de l'architecture dépend du problème spécifique, de la quantité de données disponibles, des ressources de calcul, etc.

Utilisation de Couches de Convolution (Conv2D) et de Pooling (MaxPooling2D) en Keras

Nous avons vu les concepts théoriques de la convolution et du pooling. Voyons maintenant comment utiliser concrètement les couches correspondantes en Keras.

Couche Conv2D

La couche tf.keras.layers.Conv2D est la couche de convolution 2D de Keras, utilisée pour traiter des images (ou, plus généralement, des données structurées en grilles 2D).

Paramètres principaux :

  • filters : Le nombre de filtres (noyaux) de convolution à apprendre. C'est le seul paramètre obligatoire. Chaque filtre produira une carte de caractéristiques distincte.
  • kernel_size : La taille des filtres (noyaux). C'est un entier ou un tuple de deux entiers (hauteur, largeur). Par exemple, kernel_size=3 est équivalent à kernel_size=(3, 3). Les tailles courantes sont 3x3, 5x5, 7x7 (surtout dans les premières couches).
  • strides : L'enjambée (stride) de la convolution. Un entier ou un tuple de deux entiers. La valeur par défaut est (1, 1).
  • padding : 'valid' (pas de padding) ou 'same' (padding pour que la sortie ait la même taille que l'entrée, si strides=1).
  • activation : La fonction d'activation à appliquer après la convolution (par exemple, 'relu', 'sigmoid', 'tanh', ou None pour aucune activation).
  • input_shape : La forme des données d'entrée (seulement nécessaire pour la première couche du modèle). Pour une image couleur, ce serait (hauteur, largeur, canaux), par exemple (32, 32, 3) pour une image CIFAR-10.
  • kernel_initializer : L'initialiseur des poids du filtre.
  • bias_initializer : L'initialiseur du biais.
  • use_bias: Booléen.

Exemple :

import tensorflow as tf

# Couche Conv2D avec 32 filtres 3x3, stride de 1, padding 'same', et activation ReLU
conv_layer = tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), strides=(1, 1),
padding='same', activation='relu', input_shape=(32, 32, 3))

Couche MaxPooling2D

La couche tf.keras.layers.MaxPooling2D effectue un max pooling 2D.

Paramètres principaux :

  • pool_size : La taille de la fenêtre de pooling (un entier ou un tuple de deux entiers). La valeur par défaut est (2, 2).
  • strides : L'enjambée du pooling. Si None (par défaut), l'enjambée est égale à la taille de la fenêtre (pas de chevauchement).
  • padding : 'valid' ou 'same'. Généralement, on utilise 'valid' avec le pooling (pas de padding).

Exemple :

# Max pooling 2x2 avec stride de 2 (pas de chevauchement)
max_pool_layer = tf.keras.layers.MaxPooling2D(pool_size=(2, 2)) # Stride est à (2,2)

Intégration dans un Modèle Séquentiel

model = tf.keras.Sequential([
tf.keras.layers.Conv2D(filters=32, kernel_size=3, padding='same', activation='relu', input_shape=(32, 32, 3)),
tf.keras.layers.MaxPooling2D(pool_size=2), # Stride par défaut = pool_size

tf.keras.layers.Conv2D(filters=64, kernel_size=3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(), # pool_size=(2, 2) par défaut

tf.keras.layers.Conv2D(filters=128, kernel_size=3, padding='same', activation='relu'),
tf.keras.layers.MaxPooling2D(),

tf.keras.layers.Flatten(),
tf.keras.layers.Dense(128, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(10, activation='softmax')
])

Explications :

  • Trois blocs convolutifs : Le modèle a trois blocs convolution + max pooling.
  • Nombre de filtres croissant : Le nombre de filtres augmente avec la profondeur (32, 64, 128). C'est une pratique courante.
  • kernel_size=3 : On utilise des filtres 3x3 (très courant). On aurait aussi pu écrire kernel_size=(3, 3).
  • padding='same' : On utilise le padding 'same' pour que les cartes de caractéristiques aient la même taille que les entrées (avant le pooling).
  • activation='relu' : On utilise la fonction d'activation ReLU après chaque convolution.
  • MaxPooling2D(pool_size=2) : On utilise un max pooling 2x2 (avec un stride de 2 par défaut, donc pas de chevauchement).
  • Flatten : On aplatit la sortie du dernier bloc convolutif avant de la passer aux couches denses.
  • Couches Dense : Une couche dense cachée (128 neurones, ReLU) et une couche de sortie (10 neurones, softmax).
  • Dropout : On ajoute du dropout pour la régularisation.
info

Ce code montre comment utiliser les couches Conv2D et MaxPooling2D pour construire un CNN simple pour la classification d'images. C'est une architecture de base que vous pouvez adapter et étendre pour différents problèmes.

Implémentation d'un CNN avec Keras pour la Classification d'Images (MNIST)

Nous allons maintenant implémenter un réseau de neurones convolutif (CNN) simple avec Keras pour classer les images du jeu de données MNIST. Contrairement à l'exemple précédent où nous avions aplati les images en vecteurs, nous allons conserver la structure 2D des images et utiliser des couches de convolution.

Notebook Jupyter

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

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()

# Normalisation des images (entre 0 et 1)
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# Conversion des étiquettes en entiers
y_train = y_train.astype(np.int32)
y_test = y_test.astype(np.int32)

# Ajout d'une dimension pour le canal (images en niveaux de gris -> 1 canal)
x_train = x_train[..., tf.newaxis] # Ajoute une dimension à la fin : (60000, 28, 28) -> (60000, 28, 28, 1)
x_test = x_test[..., tf.newaxis] # (10000, 28, 28) -> (10000, 28, 28, 1)

print(f"Forme de x_train : {x_train.shape}") # (60000, 28, 28, 1)

Explications (Chargement et Prétraitement) :

  • Nous chargeons les données MNIST comme précédemment.
  • Nous normalisons les pixels entre 0 et 1.
  • Différence importante : Nous n'aplatissons pas les images. Nous conservons leur structure 2D (28x28).
  • Ajout d'un canal : Nous ajoutons explicitement une dimension de canal aux images. Les CNNs attendent des entrées de forme (hauteur, largeur, canaux). Pour des images en niveaux de gris, il y a un seul canal. Pour des images couleur, il y en aurait 3 (rouge, vert, bleu). x_train[..., tf.newaxis] est une façon d'ajouter une dimension à la fin du tableau.
# 2. Construction du modèle CNN

model = tf.keras.Sequential([
tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units=10, activation='softmax')
])

# 3. Compilation du modèle
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])

# 4. Callbacks
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=10,
mode='max',
restore_best_weights=True
)

Explications (Construction et Compilation) :

  • Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)) :
    • Première couche de convolution.
    • 32 filtres de taille 3x3.
    • Fonction d'activation ReLU.
    • input_shape=(28, 28, 1) : Important ! Nous indiquons que les images d'entrée sont de taille 28x28 avec 1 canal (niveaux de gris).
  • MaxPooling2D(pool_size=(2, 2)) : Max pooling 2x2 (réduit la taille des cartes de caractéristiques par 2).
  • Conv2D(filters=64, kernel_size=(3, 3), activation='relu') : Deuxième couche de convolution (64 filtres 3x3).
  • MaxPooling2D(pool_size=(2, 2)) : Deuxième couche de max pooling.
  • Flatten() : Aplatit la sortie du dernier pooling en un vecteur 1D, pour pouvoir la connecter à la couche dense de sortie.
  • Dense(units=10, activation='softmax') : Couche de sortie avec 10 neurones (un pour chaque chiffre) et une activation softmax.
  • Compilation:
    • optimizer, loss et metrics sont les mêmes.
# 5. Entraînement du modèle
history = model.fit(x_train, y_train, epochs=20, batch_size=64,
validation_split=0.1, verbose=2,
callbacks=[early_stopping_callback])

Explications (Entraînement) :

L'entraînement est similaire à ce que nous avons fait précédemment. Notez que nous n'avons pas besoin de remodeler les données, car les couches Conv2D et MaxPooling2D attendent des entrées 2D (plus le canal).

# 6. 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()

Explications (Visualisation) :

On affiche les courbes, pour vérifier si l'apprentissage ce fait correctement.

# 7. Évaluation du modèle
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) :

On évalue le modèle sur l'ensemble de test.

# 8. Prédictions sur quelques images de test
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, :, :, 0], cmap='gray') # Affichage de l'image (on sélectionne le canal 0)
plt.title(f"P:{predicted_classes[i]}\nR:{y_test[i]}", fontsize=9)
plt.axis('off')
plt.show()

Explications (Prédictions) :

  • Nous faisons des prédictions sur les 10 premières images de test.
  • Nous affichons ces images, avec la classe prédite et la vraie classe.
  • Notez que pour afficher l'image avec plt.imshow(), nous devons sélectionner le canal de couleur (qui est la dernière dimension). Comme nos images sont en niveaux de gris, il n'y a qu'un seul canal (canal 0), donc nous utilisons x_test[i, :, :, 0].

Analyse :

Ce modèle CNN, malgré sa simplicité, devrait obtenir une exactitude très élevée sur MNIST (supérieure à 98%, voire 99%). Cela montre la puissance des CNNs pour traiter les images, même avec une architecture relativement simple.

Pour aller plus loin :

  • Vous pouvez expérimenter avec :
    • Plus de couches de convolution.
    • Différentes tailles de filtres.
    • Différents nombres de filtres.
    • Différents types de pooling (average pooling).
    • L'ajout de couches Dropout supplémentaires.
    • Différents optimiseurs ou taux d'apprentissage.

Justification de l'Architecture du Modèle CNN

Reprenons l'architecture de notre modèle CNN pour MNIST :

model = tf.keras.Sequential([
tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1)),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units=10, activation='softmax')
])

Expliquons en détail les choix que nous avons faits pour chaque couche :

1. tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1))

  • Conv2D : Nous commençons par une couche de convolution, car c'est l'opération fondamentale des CNNs pour extraire des caractéristiques locales des images.
  • filters=32 : Nous utilisons 32 filtres. Cela signifie que cette couche apprendra 32 filtres différents (32 détecteurs de caractéristiques différents). C'est un nombre raisonnable pour une première couche convolutive. On commence souvent avec un nombre relativement faible de filtres dans les premières couches.
  • kernel_size=(3, 3) : Nous utilisons des filtres 3x3. C'est une taille de filtre très courante, et un bon compromis entre la capacité à détecter des motifs locaux et le coût de calcul. Des filtres plus petits (par exemple, 1x1) captureraient moins d'informations spatiales, tandis que des filtres plus grands (par exemple, 5x5 ou 7x7) auraient plus de paramètres à apprendre et seraient plus coûteux en calcul.
  • activation='relu' : Nous utilisons la fonction d'activation ReLU, qui est le choix standard pour les couches cachées des CNNs (elle aide à prévenir le problème du gradient qui s'évanouit et est rapide à calculer).
  • input_shape=(28, 28, 1) : C'est la forme des images d'entrée : 28 pixels de hauteur, 28 pixels de largeur, et 1 canal (car les images MNIST sont en niveaux de gris). Il est essentiel de spécifier input_shape pour la première couche d'un modèle séquentiel.

2. tf.keras.layers.MaxPooling2D(pool_size=(2, 2))

  • MaxPooling2D : Nous utilisons une couche de max pooling pour réduire la dimensionnalité des cartes de caractéristiques (et introduire une invariance aux petites translations).
  • pool_size=(2, 2) : Une fenêtre de pooling 2x2 est la taille la plus courante. Elle divise par deux la hauteur et la largeur des cartes de caractéristiques.

3. tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu')

  • Deuxième couche de convolution : Nous ajoutons une deuxième couche de convolution pour apprendre des caractéristiques plus complexes, en combinant les caractéristiques de bas niveau détectées par la première couche.
  • filters=64 : Nous augmentons le nombre de filtres (64). Il est courant d'augmenter le nombre de filtres dans les couches plus profondes d'un CNN, car les caractéristiques deviennent plus abstraites et plus nombreuses.
  • kernel_size=(3, 3) et activation='relu' : Nous conservons la même taille de filtre et la même fonction d'activation que pour la première couche.

4. tf.keras.layers.MaxPooling2D(pool_size=(2, 2))

  • Une deuxième couche de max pooling, identique à la première.

5. tf.keras.layers.Flatten()

  • Aplatissement : Avant de passer aux couches denses, nous devons aplatir les cartes de caractéristiques (qui sont en 2D) en un vecteur 1D. La couche Flatten fait cela.

6. tf.keras.layers.Dense(units=10, activation='softmax')

  • Couche de sortie : Une couche dense avec 10 neurones (un pour chaque chiffre de 0 à 9) et une activation softmax. Softmax produit une distribution de probabilité sur les 10 classes.

Pourquoi Deux Blocs Convolution + Pooling ?

Nous aurions pu utiliser une seule couche de convolution + pooling. Cependant, avec deux blocs, le réseau peut apprendre une hiérarchie de caractéristiques plus riche :

  • La première couche convolutive apprend des caractéristiques très locales (bords, coins).
  • La deuxième couche convolutive combine ces caractéristiques locales pour apprendre des motifs plus complexes (combinaisons de bords, textures).

Ajouter plus de blocs convolutifs permettrait d'apprendre des caractéristiques encore plus abstraites, mais pour un problème relativement simple comme MNIST, deux blocs suffisent souvent.

Pourquoi pas Plus de Couches Denses ?

Nous aurions pu ajouter une ou plusieurs couches Dense cachées après la couche Flatten et avant la couche de sortie. Cela augmenterait la capacité du modèle à apprendre des fonctions complexes. Cependant, pour MNIST, une seule couche Dense de sortie est souvent suffisante, surtout après l'extraction de caractéristiques effectuée par les couches convolutives.

info

L'architecture que nous avons choisie est une architecture CNN simple mais efficace pour MNIST. Elle est un bon point de départ. Vous pouvez l'améliorer en expérimentant avec :

  • Le nombre de blocs convolutifs.
  • Le nombre de filtres dans chaque couche convolutive.
  • La taille des filtres.
  • Le type de pooling (max pooling, average pooling).
  • L'ajout de couches Dropout (que nous avons vues) ou d'autres techniques de régularisation (comme BatchNormalization, que nous verrons plus tard).
  • L'ajout de couches Dense cachées après la couche Flatten.

Les CNNs vidéo

Cette vidéo explique les CNN

Exercice : Classification d'Images CIFAR-10 avec un CNN (Keras)

Dans cet exercice, vous allez construire, entraîner et évaluer un réseau de neurones convolutif (CNN) pour classer les images du jeu de données CIFAR-10. Cet exercice est similaire à celui que vous avez fait précédemment avec des couches Dense, mais vous utiliserez maintenant des couches Conv2D et MaxPooling2D pour exploiter la structure spatiale des images.

Rappel : CIFAR-10

CIFAR-10 contient 60 000 images couleur de 32x32 pixels, réparties en 10 classes :

  1. avion
  2. automobile
  3. oiseau
  4. chat
  5. cerf
  6. chien
  7. grenouille
  8. cheval
  9. bateau
  10. camion

Ce que Vous Devez Faire

  1. Chargement et Prétraitement des Données :

    • Chargez CIFAR-10 avec tf.keras.datasets.cifar10.load_data().
    • Normalisez les images (pixels entre 0 et 1).
    • Important : Contrairement à l'exercice précédent, ne pas aplatir les images. Conservez la structure 32x32x3.
    • Les étiquettes sont déjà des entiers (de 0 à 9), donc vous n'avez pas besoin de les convertir en one-hot encoding (sauf si vous choisissez d'utiliser categorical_crossentropy comme fonction de perte).
  2. Construction du Modèle CNN :

    • Utilisez l'API séquentielle de Keras (tf.keras.Sequential).
    • Construisez un CNN avec au moins deux blocs convolutifs :
      • Couche Conv2D (choisissez le nombre de filtres, la taille du noyau, l'activation).
      • Couche MaxPooling2D.
      • Répétez ce bloc au moins une fois (vous pouvez ajouter plus de blocs).
    • Aplatissez la sortie du dernier bloc convolutif (Flatten).
    • Ajoutez une ou plusieurs couches Dense (à vous de choisir le nombre et la taille).
    • Ajoutez éventuellement des couches Dropout.
    • La couche de sortie doit avoir 10 neurones et une activation softmax.
  3. Compilation du Modèle :

    • Optimiseur : Adam (vous pouvez essayer d'autres optimiseurs).
    • Fonction de perte : sparse_categorical_crossentropy (si les étiquettes sont des entiers) ou categorical_crossentropy (si vous les convertissez en one-hot).
    • Métrique : accuracy.
  4. Entraînement du Modèle :

    • Utilisez model.fit().
    • Choisissez un nombre d'époques et une taille de batch.
    • Utilisez un ensemble de validation (validation_split ou un ensemble séparé).
  5. Évaluation du Modèle :

    • Évaluez le modèle sur l'ensemble de test (model.evaluate()).
    • Affichez la perte et l'exactitude.
  6. Visualisation :

    • Affichez les courbes de perte et d'exactitude (entraînement et validation).
    • Affichez quelques prédictions (images de test, vraies étiquettes, étiquettes prédites).
  7. Expérimentation (Important !) :

    • Essayez différentes architectures :
      • Plus ou moins de blocs convolutifs.
      • Différents nombres de filtres.
      • Différentes tailles de noyau.
      • Différentes fonctions d'activation.
      • Avec/sans Dropout.
      • Différentes tailles de batch.
      • Différents taux d'apprentissage.
    • Observez l'impact de ces changements sur :
      • La performance (perte, exactitude).
      • La vitesse d'entraînement.
      • Le surapprentissage (différence entre entraînement et validation).

Conseils

  • Commencez par une architecture simple, puis augmentez progressivement la complexité.
  • Surveillez attentivement les courbes d'apprentissage (perte et exactitude) pour détecter le surapprentissage ou le sous-apprentissage.
  • N'hésitez pas à vous inspirer d'architectures CNN connues (LeNet, AlexNet, VGG...), mais adaptez-les à la taille des images CIFAR-10 (32x32).
  • Documentez vos expérimentations (notez les architectures que vous essayez, les paramètres, et les résultats obtenus).
info

Le but de cet exercice est de vous familiariser avec la construction et l'entraînement de CNNs avec Keras, et d'expérimenter avec différentes architectures pour comprendre leur impact sur la performance. CIFAR-10 est un problème plus difficile que MNIST, donc ne vous découragez pas si vous n'obtenez pas immédiatement une exactitude très élevée.

Solution

Solution

Voici une solution possible pour l'exercice de classification CIFAR-10 avec un CNN. N'oubliez pas 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

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()

# Normalisation
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# 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)

# Les étiquettes sont des tableaux 2D (shape (N, 1)), on les convertit en 1D (shape (N,))
y_train = y_train.flatten()
y_test = y_test.flatten()

Explications (Chargement et Prétraitement) :

  • tf.keras.datasets.cifar10.load_data() : Charge le jeu de données CIFAR-10.
  • Normalisation : On divise par 255.0 pour ramener les valeurs des pixels entre 0 et 1.
  • Forme des étiquettes : Les étiquettes sont initialement des tableaux de forme (nombre d'exemples, 1). On les convertit en tableaux 1D avec .flatten(). Cela n'est pas strictement nécessaire avec sparse_categorical_crossentropy, mais c'est une bonne pratique pour éviter des erreurs potentielles.
  • On ne fait pas de Flatten ici pour l'image.
# 2. Construction du modèle CNN

model = tf.keras.Sequential([
tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu', padding='same', input_shape=(32, 32, 3)),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding='same'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

tf.keras.layers.Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding='same'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units=128, activation='relu'),
tf.keras.layers.Dropout(rate=0.5),
tf.keras.layers.Dense(units=10, activation='softmax')
])

# 3. Compilation
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])

# 4. Callbacks
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=10,
mode='max',
restore_best_weights=True
)

Explications (Construction et Compilation) :

  • Architecture :
    • Trois blocs convolutifs :
      • Conv2D(..., input_shape=(32, 32, 3)) : La première couche doit spécifier la forme d'entrée (hauteur, largeur, canaux). Ici, 32x32x3 (images couleur).
      • filters=32, puis filters=64, puis filters=128 : Le nombre de filtres augmente avec la profondeur.
      • kernel_size=(3, 3) : Filtres 3x3.
      • activation='relu' : Activation ReLU.
      • padding='same' : On utilise le padding 'same' pour conserver la taille des cartes de caractéristiques après chaque convolution.
      • MaxPooling2D(pool_size=(2, 2)) : Max pooling 2x2 après chaque convolution.
    • Flatten() : Aplatit la sortie du dernier bloc convolutif.
    • Une couche Dense cachée (128 neurones, ReLU).
    • Dropout(rate=0.5) : Dropout pour la régularisation.
    • Couche de sortie Dense (10 neurones, softmax).
  • Compilation :
    • Optimiseur : adam.
    • Fonction de perte : sparse_categorical_crossentropy (car les étiquettes sont des entiers).
    • Métrique : accuracy.
  • Callbacks:
    • EarlyStopping pour stopper l'entrainement si plus d'amélioration.
# 5. Entraînement
history = model.fit(x_train, y_train, epochs=50, batch_size=64,
validation_split=0.15, verbose=2,
callbacks=[early_stopping_callback])

Explications (Entraînement) :

  • epochs=50 : Nous entraînons pendant 50 époques au maximum (mais EarlyStopping peut arrêter l'entraînement avant).
  • batch_size=64 : Taille de mini-batch de 64.
  • validation_split=0.15 : 15% des données d'entraînement sont utilisées pour la validation.
  • verbose=2 : Affichage des informations d'entraînement.
  • callbacks: Ajout des 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) :

  • Affichage des courbes
# 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) :

  • Evaluation du modèle sur l'ensemble de test
# 8. Prédictions (sur quelques images)

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]) # Affichage de l'image originale (déjà en 32x32x3)
plt.title(f"P:{predicted_classes[i]}\nR:{y_test[i]}", fontsize=9)
plt.axis('off')
plt.show()

Explications (Prédictions) :

  • model.predict(x_test[:10]) : Prédictions sur les 10 premières images de test.
  • np.argmax(predictions, axis=1) : On prend l'indice de la classe prédite (celle avec la probabilité la plus élevée).
  • Affichage des images, des classes prédites et des vraies classes.
  • On affiche l'image originale

Analyse et pistes d'amélioration :

  • Ce modèle devrait obtenir une exactitude d'environ 70-75% sur l'ensemble de test (après entraînement complet et sans surapprentissage excessif). C'est un résultat raisonnable pour un CNN relativement simple.

  • Pour améliorer la performance, vous pourriez essayer :

    • Plus de couches de convolution.
    • Plus de filtres par couche.
    • Différentes tailles de noyau.
    • Batch Normalization.
    • D'autres fonctions d'activation (Leaky ReLU, etc.).
    • Un taux d'apprentissage plus petit (éventuellement avec un ReduceLROnPlateau callback).
    • L'augmentation de données (data augmentation, que nous n'avons pas vue).
    • Modifier le batch size
    • Modifier la valeur de patience

    Il est important d'expérimenter et de comparer les résultats sur l'ensemble de validation.