Skip to main content

Modèles Multicouches

Intérêt des Modèles Multicouches (Réseaux de Neurones Profonds)

Jusqu'à présent, nous avons principalement travaillé avec des modèles à une seule couche (perceptron, régression linéaire/logistique). Il est temps d'introduire les modèles multicouches, également appelés réseaux de neurones profonds (deep neural networks).

Pourquoi Plusieurs Couches ?

  1. Représentations Hiérarchiques :

    • Dans un réseau multicouche, chaque couche apprend une représentation différente des données d'entrée.
    • Les premières couches apprennent des caractéristiques de bas niveau (par exemple, des contours, des textures dans une image).
    • Les couches suivantes combinent ces caractéristiques de bas niveau pour former des caractéristiques de plus haut niveau (par exemple, des formes, des objets).
    • Les dernières couches utilisent ces caractéristiques de haut niveau pour prendre la décision finale (classification, régression, etc.).

    Cette capacité à apprendre des représentations hiérarchiques est l'une des clés du succès du Deep Learning.

  2. Apprentissage de Fonctions Complexes :

    • Un réseau à une seule couche ne peut représenter que des fonctions linéaires (ou des séparations linéaires, si l'on ajoute une fonction d'activation).
    • En empilant plusieurs couches, on peut apprendre des fonctions beaucoup plus complexes, non linéaires.
    • Théoriquement, un réseau de neurones avec une seule couche cachée (suffisamment grande) et une fonction d'activation non linéaire peut approximer n'importe quelle fonction continue (théorème d'approximation universelle). Cependant, en pratique, les réseaux profonds sont souvent plus efficaces et plus faciles à entraîner, car ils nécessitent moins de paramètres pour atteindre la même performance.
  3. Abstraction et Généralisation :

    • Les réseaux profonds apprennent à extraire des caractéristiques abstraites et invariantes aux variations des données d'entrée.
    • Cela leur permet de mieux généraliser à de nouvelles données (données qu'ils n'ont jamais vues pendant l'entraînement).
  4. Efficacité :

    • Pour certaines tâches, un réseau profond peut atteindre une performance similaire à celle d'un réseau peu profond avec beaucoup moins de neurones.
    • Cela signifie que les réseaux profonds peuvent être plus économes en termes de paramètres et de calcul.

Exemple : Reconnaissance d'Images

Imaginez que vous vouliez construire un réseau de neurones pour reconnaître des visages sur des images :

  • Premières couches : Apprennent à détecter des caractéristiques simples comme des bords, des coins, des textures.
  • Couches intermédiaires : Combinent ces caractéristiques pour détecter des parties de visage (yeux, nez, bouche).
  • Couches supérieures : Combinent ces parties de visage pour reconnaître un visage entier, et éventuellement identifier la personne.

Sans couches multiples, le réseau devrait apprendre directement la relation entre les pixels individuels de l'image et la présence/absence d'un visage, ce qui est extrêmement difficile.

Inconvénients Potentiels

  • Complexité : Les réseaux profonds sont plus complexes à concevoir, à entraîner et à interpréter que les modèles simples.
  • Risque de surapprentissage : Les réseaux profonds ont beaucoup de paramètres, ce qui les rend plus susceptibles de surapprendre les données d'entraînement. Il faut utiliser des techniques de régularisation (comme le dropout) pour atténuer ce risque.
  • Coût de calcul : L'entraînement de réseaux profonds peut nécessiter beaucoup de temps et de ressources de calcul (GPU, TPU).
  • Gradient qui s'évanouit/explose: il faut faire attention à ce problème.
    • Problèmes de gradients : Il faut faire attention aux problèmes de gradients qui s'évanouissent ou explosent, notamment en utilisant des fonctions d'activation adaptées (comme ReLU) et des techniques comme le batch normalization.
info

L'ajout de couches cachées permet à un réseau de neurones d'apprendre des représentations hiérarchiques et des fonctions complexes, ce qui est essentiel pour résoudre des problèmes du monde réel. Cependant, cela introduit aussi des défis en termes de conception, d'entraînement et de régularisation.

Couches (Dense, Activation, Dropout, etc.)

Dans l'API séquentielle de Keras, un modèle est construit en empilant des couches (layers). Chaque couche effectue une transformation sur les données qu'elle reçoit en entrée et produit des données en sortie, qui sont transmises à la couche suivante.

Keras offre une grande variété de couches pré-définies, chacune adaptée à différents types de tâches et de données. Voici quelques-unes des couches les plus courantes :

Dense (Couche Entièrement Connectée)

C'est la couche la plus fondamentale. Elle implémente une transformation linéaire suivie d'une fonction d'activation :

output=activation(dot(input,kernel)+bias)output = activation(dot(input, kernel) + bias)

où :

  • input : Le tenseur d'entrée.
  • kernel : La matrice des poids (de forme (input_dim, units)).
  • bias : Le vecteur de biais (de forme (units,)).
  • activation : La fonction d'activation (par exemple, relu, sigmoid, tanh).
  • dot : Produit matriciel
  • units: La dimension de l'espace de sortie

Paramètres principaux :

  • units : Le nombre de neurones dans la couche (la dimensionnalité de l'espace de sortie). C'est le seul paramètre obligatoire.
  • activation : La fonction d'activation à appliquer. Si None (par défaut), aucune fonction d'activation n'est appliquée (la transformation est purement linéaire).
  • use_bias : Booléen, indique si la couche utilise un biais ou non (par défaut, True).
  • kernel_initializer : L'initialiseur pour la matrice des poids (nous y reviendrons).
  • bias_initializer : L'initialiseur pour le biais.
  • input_shape : La forme du tenseur d'entrée (nécessaire uniquement pour la première couche du modèle).

Exemple :

import tensorflow as tf

model = tf.keras.Sequential([
tf.keras.layers.Dense(units=6, activation='relu', input_shape=(2,)), # Première couche (couche d'entrée)
tf.keras.layers.Dense(units=3, activation='softmax') # Deuxième couche (couche de sortie)
])

Dans cet exemple, la première couche Dense a 6 neurones, une fonction d'activation ReLU et reçoit des entrées de forme (2,). La deuxième couche Dense a 3 neurones et une fonction d'activation softmax.

note

input_shape n'est nécessaire que pour la première couche du modèle. Keras déduit automatiquement les formes des entrées des couches suivantes.

neuralNetwork263

Activation (Couche d'Activation)

Bien que l'on puisse spécifier la fonction d'activation directement dans une couche Dense, il est parfois utile d'utiliser une couche Activation séparée. Cela permet de mieux contrôler l'ordre des opérations et d'appliquer des fonctions d'activation plus complexes.

Exemple :

import tensorflow as tf

model = tf.keras.Sequential([
tf.keras.layers.Dense(units=6, input_shape=(2,)),
tf.keras.layers.Activation('relu'),
tf.keras.layers.Dense(units=3),
tf.keras.layers.Activation('softmax')
])
note

Cette approche est équivalente à spécifier l'activation directement dans la couche Dense, mais elle peut être utile pour des architectures plus complexes où vous souhaitez appliquer des activations de manière plus flexible.

Dropout (Couche d'Extinction)

Le dropout est une technique de régularisation puissante et largement utilisée pour entraîner les réseaux de neurones, en particulier les réseaux profonds (avec de nombreuses couches). Son but principal est de réduire le surapprentissage (overfitting).

Qu'est-ce que le Surapprentissage ?

Rappelons que le surapprentissage se produit lorsqu'un modèle apprend "trop bien" les données d'entraînement, au point de mémoriser des détails spécifiques et du bruit qui ne sont pas représentatifs des données réelles. Un modèle surappris a une très bonne performance sur les données d'entraînement, mais une mauvaise performance sur de nouvelles données (données de test ou de validation).

Principe du Dropout

L'idée du dropout est étonnamment simple :

  • Pendant l'entraînement, à chaque itération (et pour chaque mini-batch), on "éteint" aléatoirement une fraction des neurones de la couche à laquelle le dropout est appliqué. "Éteindre" un neurone signifie que sa sortie est mise à zéro, comme s'il n'existait pas.
  • La fraction des neurones éteints est déterminée par un paramètre appelé taux de dropout (dropout rate), généralement un nombre entre 0 et 1 (par exemple, 0.2, 0.5). Un taux de dropout de 0.2 signifie que 20% des neurones seront désactivés aléatoirement à chaque étape.
  • À chaque itération, les neurones désactivés sont différents. C'est un processus aléatoire.
  • Il ne faut pas confondre les "neurones mourants" des ReLU (qui sont une conséquence de la fonction d'activation) avec les neurones désactivés du Dropout (qui sont une technique de régularisation intentionnelle).
  • Pendant l'inférence (prédiction sur de nouvelles données), le dropout n'est pas appliqué. Tous les neurones sont actifs. Cependant, pour compenser le fait que plus de neurones sont actifs pendant l'inférence qu'pendant l'entraînement, les sorties des neurones sont multipliées par (1taux de dropout)(1 - \text{taux de dropout}). Ceci est géré automatiquement par la couche Dropout de Keras.

Pourquoi le Dropout Fonctionne-t-il ?

Le dropout a plusieurs effets bénéfiques :

  1. Empêche la co-adaptation des neurones : En désactivant aléatoirement des neurones, le dropout force le réseau à apprendre des représentations redondantes. Aucun neurone ne peut se fier à la présence d'un autre neurone en particulier. Le réseau devient plus robuste et moins sensible aux variations des données d'entrée.

  2. Effet d'ensemble (ensemble learning) : On peut voir le dropout comme une forme d'apprentissage d'ensemble (ensemble learning). À chaque itération, on entraîne un sous-réseau différent (puisque les neurones désactivés changent). Le modèle final peut être vu comme une combinaison de tous ces sous-réseaux.

  3. Régularisation implicite : Le dropout introduit du bruit dans le processus d'apprentissage, ce qui a un effet régularisateur. Il empêche le modèle de s'adapter trop précisément aux données d'entraînement.

Dropout en Keras : tf.keras.layers.Dropout

En Keras, le dropout est implémenté sous forme de couche : tf.keras.layers.Dropout.

Paramètre principal :

  • rate : Le taux de dropout (la fraction des entrées à mettre à zéro).

Exemple :

model = tf.keras.Sequential([
tf.keras.layers.Dense(units=8, activation='relu', input_shape=(3,)),
tf.keras.layers.Dropout(rate=0.2), # Dropout de 20%
tf.keras.layers.Dense(units=4, activation='softmax')
])

Dans cet exemple, 20% des sorties de la première couche Dense seront mises à zéro aléatoirement pendant l'entraînement.

Conseils d'Utilisation

  • Le dropout est généralement utilisé dans les couches cachées, pas dans la couche d'entrée ni dans la couche de sortie (sauf cas particuliers).
  • Un taux de dropout courant se situe entre 0.2 et 0.5.
  • Il est courant d'utiliser un taux de dropout plus élevé dans les couches plus profondes du réseau.
  • Le dropout est souvent combiné avec d'autres techniques de régularisation (régularisation L1/L2, augmentation de données).
  • Le dropout est une technique d'entraînement, on ne l'utilise pas lors de l'inférence (prédiction).
warning

Le dropout est une technique puissante, mais il faut l'utiliser avec discernement :

  • Un taux de dropout trop élevé peut empêcher le modèle d'apprendre correctement (sous-apprentissage)
  • Un taux trop faible peut ne pas suffire à prévenir le surapprentissage
  • Il est souvent nécessaire d'ajuster ce paramètre par validation croisée

Autres Couches Courantes

Keras offre de nombreuses autres couches, que nous découvrirons au fur et à mesure de nos besoins :

  • Couches convolutives (Conv1D, Conv2D, Conv3D) : Pour traiter des données structurées (séries temporelles, images, vidéos).
  • Couches de pooling (MaxPooling1D, MaxPooling2D, AveragePooling1D, etc.) : Pour réduire la dimensionnalité des données.
  • Couches récurrentes (SimpleRNN, LSTM, GRU) : Pour traiter des séquences de données.
  • Couches de normalisation (BatchNormalization) : Pour accélérer l'entraînement et améliorer la performance.
  • Couches d'embedding (Embedding) : Pour représenter des données catégorielles sous forme de vecteurs denses.
  • Couches de fusion (Add, Concatenate, Multiply, etc.) : Pour combiner les sorties de plusieurs couches.
info

La documentation officielle de Keras fournit une liste complète de toutes les couches disponibles, avec des explications détaillées et des exemples d'utilisation.

Construction de Modèles Plus Complexes

Jusqu'à présent, nous avons utilisé l'API séquentielle de Keras pour construire des modèles très simples (régression linéaire, portes logiques). Nous allons maintenant voir comment construire des modèles un peu plus complexes, toujours avec l'API séquentielle.

Empiler Plus de Couches

La manière la plus simple de rendre un modèle plus complexe est d'ajouter des couches cachées. Rappelez-vous qu'avec plus de couches, le réseau peut apprendre des représentations plus abstraites et des fonctions plus complexes.

Exemple : Un réseau avec deux couches cachées

import tensorflow as tf

model = tf.keras.Sequential([
tf.keras.layers.Dense(units=10, activation='relu', input_shape=(8,)), # Première couche cachée
tf.keras.layers.Dense(units=15, activation='relu'), # Deuxième couche cachée
tf.keras.layers.Dense(units=4, activation='softmax') # Couche de sortie
])

Ce modèle a :

  • Une couche d'entrée (implicite) qui reçoit des entrées de forme (8,).
  • Une première couche cachée avec 10 neurones et une activation ReLU.
  • Une deuxième couche cachée avec 15 neurones et une activation ReLU.
  • Une couche de sortie avec 4 neurones et une activation softmax (pour la classification multi-classe).

multi

Combiner Différents Types de Couches

Vous pouvez combiner différents types de couches dans un modèle séquentiel, en fonction de vos besoins.

Exemple : Un réseau avec dropout

model = tf.keras.Sequential([
tf.keras.layers.Dense(units=10, activation='relu', input_shape=(8,)),
tf.keras.layers.Dropout(rate=0.2), # Dropout après la première couche cachée
tf.keras.layers.Dense(units=15, activation='relu'),
tf.keras.layers.Dropout(rate=0.5), # Dropout après la deuxième couche cachée
tf.keras.layers.Dense(units=4, activation='softmax')
])

Ce modèle ajoute des couches Dropout après chaque couche Dense cachée, pour réduire le surapprentissage.

Choisir le Nombre de Couches et le Nombre de Neurones

Il n'y a pas de règle magique pour déterminer le nombre optimal de couches et de neurones pour un problème donné. C'est souvent un processus d'expérimentation. Voici quelques conseils :

  • Commencez simple : Commencez avec un modèle relativement petit (peu de couches, peu de neurones) pour avoir une base de référence.
  • Augmentez progressivement la complexité : Ajoutez des couches et/ou des neurones, et observez l'impact sur la performance (sur un ensemble de validation !).
  • Surveillez le surapprentissage : Si la performance sur les données d'entraînement est beaucoup plus élevée que sur les données de validation, votre modèle est probablement trop complexe (ou vous avez besoin de plus de données ou de régularisation).
  • Règle empirique (très approximative) :
    • Le nombre de neurones dans une couche cachée est souvent compris entre le nombre d'entrées et le nombre de sorties.
    • On peut utiliser un nombre décroissant de neurones dans les couches successives (par exemple, 64, puis 32, puis 16). Mais ce n'est pas une règle absolue.
  • Considérations pratiques :
    • Plus le réseau est profond, plus il est susceptible de rencontrer des problèmes de gradients
    • Les réseaux très profonds peuvent nécessiter des techniques comme le batch normalization ou les skip connections (que nous verrons plus tard)

Exercice : Classification Non Linéaire avec un Réseau Multicouche

Dans cet exercice, vous allez créer un réseau de neurones multicouche avec Keras pour résoudre un problème de classification non linéairement séparable. Vous utiliserez l'API séquentielle de Keras et les concepts que nous avons vus jusqu'à présent (couches Dense, fonctions d'activation, compilation, entraînement). Nous allons générer les données nous-mêmes.

Problème : Classification de Données en Forme de Cercles Concentriques

Nous allons générer un jeu de données synthétiques où les points appartiennent à deux classes, disposées en cercles concentriques. Ce type de données n'est pas linéairement séparable.

Consignes

  1. Génération des données :
    • Utilisez ce stub de départ qui permet de générer les données
Stub de départ : Génération des données
import numpy as np
import matplotlib.pyplot as plt

def generate_circles_data(n_samples=500, noise=0.05, factor=0.5):
"""Génère des données en forme de cercles concentriques.

Args:
n_samples (int): Nombre total de points.
noise (float): Écart-type du bruit gaussien ajouté.
factor (float): Facteur d'échelle entre les cercles (doit être < 1).

Returns:
tuple: (X, Y), où X est un tableau de forme (n_samples, 2) contenant les
coordonnées des points, et Y est un tableau de forme (n_samples,)
contenant les étiquettes (0 ou 1).
"""
n_samples_outer = n_samples // 2
n_samples_inner = n_samples - n_samples_outer

# Cercle extérieur (classe 0)
theta_outer = np.linspace(0, 2 * np.pi, n_samples_outer)
r_outer = np.ones(n_samples_outer) # Rayon = 1
x_outer = r_outer * np.cos(theta_outer)
y_outer = r_outer * np.sin(theta_outer)

# Cercle intérieur (classe 1)
theta_inner = np.linspace(0, 2 * np.pi, n_samples_inner)
r_inner = np.ones(n_samples_inner) * factor # Rayon = factor
x_inner = r_inner * np.cos(theta_inner)
y_inner = r_inner * np.sin(theta_inner)

# Ajout du bruit
x_outer += np.random.normal(0, noise, n_samples_outer)
y_outer += np.random.normal(0, noise, n_samples_outer)
x_inner += np.random.normal(0, noise, n_samples_inner)
y_inner += np.random.normal(0, noise, n_samples_inner)

# Concaténation
X = np.vstack((np.stack((x_outer, y_outer), axis=-1), np.stack((x_inner, y_inner), axis = -1)))
Y = np.hstack((np.zeros(n_samples_outer), np.ones(n_samples_inner)))

return X, Y
  1. Visualisez les données:

    • Afin de mieux comprendre la problématique, visualisez les données générées.
    • Vous pouvez utiliser matplotlib pour créer un scatter plot des points colorés par classe
  2. Création du modèle :

    • Créez un modèle Keras Sequential.
    • Ajoutez au moins deux couches Dense :
      • Une couche cachée avec un certain nombre de neurones (par exemple, 8, 16, 32... Expérimentez !) et une fonction d'activation non linéaire (par exemple, relu).
      • Une couche de sortie avec un seul neurone (classification binaire) et une fonction d'activation sigmoid.
    • L'entrée doit avoir la forme (2,) (deux coordonnées : x1 et x2).
  3. Compilation du modèle :

    • Utilisez l'optimiseur Adam.
    • Utilisez la fonction de perte binary_crossentropy.
    • Ajoutez l'exactitude ('accuracy') comme métrique.
  4. Entraînement du modèle :

    • Entraînez le modèle sur les données X et Y.
    • Choisissez un nombre d'époques suffisant (par exemple, 100, 200...).
  5. Visualisation :

    • Tracez la courbe de perte.
    • Tracez la courbe d'exactitude.
    • Affichez les points de données, colorés en fonction de leur classe réelle.
    • Affichez la frontière de décision.
  6. Analyse

    • Observez les courbes de perte et d'exactitude. Le modèle converge-t-il ?
    • Observez la frontière de décision. Est-elle linéaire ? Le modèle parvient-il à séparer (approximativement) les deux classes ?
    • Expérimentez :
      • Changez le nombre de neurones dans la couche cachée.
      • Ajoutez une deuxième couche cachée.
      • Changez la fonction d'activation de la couche cachée (par exemple, essayez tanh).
      • Modifiez le taux d'apprentissage.
    • Quel est l'impact de ces modifications sur la performance et la frontière de décision ?
tip

Enregistrez toutes vos observations dans votre notebook pour pouvoir les consulter et les comparer plus tard.

Solution
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

def generate_circles_data(n_samples=500, noise=0.05, factor=0.5):
"""Génère des données en forme de cercles concentriques.

Args:
n_samples (int): Nombre total de points.
noise (float): Écart-type du bruit gaussien ajouté.
factor (float): Facteur d'échelle entre les cercles (doit être < 1).

Returns:
tuple: (X, Y), où X est un tableau de forme (n_samples, 2) contenant les
coordonnées des points, et Y est un tableau de forme (n_samples,)
contenant les étiquettes (0 ou 1).
"""
n_samples_outer = n_samples // 2
n_samples_inner = n_samples - n_samples_outer

# Cercle extérieur (classe 0)
theta_outer = np.linspace(0, 2 * np.pi, n_samples_outer)
r_outer = np.ones(n_samples_outer) # Rayon = 1
x_outer = r_outer * np.cos(theta_outer)
y_outer = r_outer * np.sin(theta_outer)

# Cercle intérieur (classe 1)
theta_inner = np.linspace(0, 2 * np.pi, n_samples_inner)
r_inner = np.ones(n_samples_inner) * factor # Rayon = factor
x_inner = r_inner * np.cos(theta_inner)
y_inner = r_inner * np.sin(theta_inner)

# Ajout du bruit
x_outer += np.random.normal(0, noise, n_samples_outer)
y_outer += np.random.normal(0, noise, n_samples_outer)
x_inner += np.random.normal(0, noise, n_samples_inner)
y_inner += np.random.normal(0, noise, n_samples_inner)

# Concaténation
X = np.vstack((np.stack((x_outer, y_outer), axis=-1), np.stack((x_inner, y_inner), axis = -1)))
Y = np.hstack((np.zeros(n_samples_outer), np.ones(n_samples_inner)))

return X, Y

# 1. Données
X, Y = generate_circles_data(n_samples=500, noise=0.05, factor=0.5)

# 2. Modèle
model = tf.keras.Sequential([
tf.keras.layers.Dense(units=8, activation='relu', input_shape=(2,)), # Couche cachée
tf.keras.layers.Dense(units=1, activation='sigmoid') # Couche de sortie
])

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

# 4. Entraînement
history = model.fit(X, Y, epochs=100, verbose=0)

# 5. Visualisation
plt.plot(history.history['loss'])
plt.title('Courbe de perte')
plt.xlabel('Époques')
plt.ylabel('Perte')
plt.show()

plt.plot(history.history['accuracy'])
plt.title('Courbe d\'exactitude')
plt.xlabel('Époques')
plt.ylabel('Exactitude')
plt.show()

plt.scatter(X[:, 0], X[:, 1], c=Y, cmap='viridis', marker='o')
plt.title('Données originales')
plt.show()

x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))

Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

plt.contourf(xx, yy, Z, levels=[0, 0.5, 1], colors=['red', 'blue'], alpha=0.4)
plt.scatter(X[:, 0], X[:, 1], c=Y, cmap='viridis', edgecolors='k')
plt.title('Frontière de Décision (Réseau Multicouche)')
plt.show()