Régularisation#

Comme nous l’avons vu dans les chapitres précédents, l’une des forces des réseaux neuronaux est qu’ils peuvent approximer n’importe quelle fonction continue lorsqu’un nombre suffisant de paramètres est utilisé. Lors de l’utilisation d’approximateurs universels dans des contextes d’apprentissage automatique, un risque connexe important est celui du surajustement (overfitting) aux données d’apprentissage. Plus formellement, étant donné un jeu de données d’apprentissage \(\mathcal{D}_t\) tiré d’une distribution inconnue \(\mathcal{D}\), les paramètres du modèle sont optimisés de manière à minimiser le risque empirique :

\[ \mathcal{R}_e(\theta) = \frac{1}{|\mathcal{D}_t|} \sum_{(x_i, y_i) \in \mathcal{D}_t} \mathcal{L}(x_i, y_i ; m_\theta) \]

alors que le véritable objectif est de minimiser le « vrai » risque :

\[ \mathcal{R}(\theta) = \mathbb{E}_{x, y \sim \mathcal{D}} \mathcal{L}(x, y; m_\theta) \]

et les deux objectifs n’ont pas le même minimiseur.

Pour éviter cet écueil, il faut utiliser des techniques de régularisation, telles que celles présentées ci-après.

Early stopping#

Comme illustré ci-dessous, on peut observer que l’entraînement d’un réseau neuronal pendant un trop grand nombre d”epochs peut conduire à un surajustement. Notez qu’ici, le risque réel est estimé grâce à l’utilisation d’un ensemble de validation qui n’est pas vu pendant l’entraînement.

Hide code cell source

import numpy as np
import pandas as pd

%config InlineBackend.figure_format = 'svg'
%matplotlib inline
import matplotlib.pyplot as plt
from notebook_utils import prepare_notebook_graphics
import keras
from keras.utils import to_categorical
from myst_nb import glue
prepare_notebook_graphics()
iris = pd.read_csv("../data/iris.csv", index_col=0)
iris = iris.sample(frac=1)
y = to_categorical(iris["target"])
X = iris.drop(columns=["target"])
X -= X.mean(axis=0)
X /= X.std(axis=0)
import keras
from keras.layers import Dense, InputLayer
from keras.models import Sequential
from keras.utils import set_random_seed


set_random_seed(0)
model = Sequential([
    InputLayer(input_shape=(4, )),
    Dense(units=256, activation="relu"),
    Dense(units=256, activation="relu"),
    Dense(units=256, activation="relu"),
    Dense(units=3, activation="softmax")
])

n_epochs = 100
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
h = model.fit(X, y, validation_split=0.3, epochs=n_epochs, batch_size=30, verbose=0)

Hide code cell source

plt.plot(np.arange(1, n_epochs + 1), h.history["loss"], label="Apprentissage")
plt.plot(np.arange(1, n_epochs + 1), h.history["val_loss"], label="Validation")
plt.axhline(y=np.min(h.history["val_loss"]), color="k", linestyle="dashed")
plt.xlim([0, 102])
plt.ylabel("Loss")
plt.xlabel("Epochs")
plt.legend()

glue("epoch_best_model", np.argmin(h.history["val_loss"]) + 1, display=False)
../../_images/d791f22ccb6a8b66de7070168485a5f234d2565e6e9106a829396146eb78293f.svg

Ici, le meilleur modèle (en termes de capacités de généralisation) semble être le modèle à l”epoch np.int64(42). En d’autres termes, si nous avions arrêté le processus d’apprentissage après l”epoch np.int64(42), nous aurions obtenu un meilleur modèle que si nous utilisons le modèle entraîné pendant 70 epochs.

C’est toute l’idée derrière la stratégie d”early stopping, qui consiste à arrêter le processus d’apprentissage dès que la perte de validation cesse de s’améliorer. Cependant, comme on peut le voir dans la visualisation ci-dessus, la perte de validation a tendance à osciller, et on attend souvent plusieurs epochs avant de supposer que la perte a peu de chances de s’améliorer dans le futur. Le nombre d”epochs à attendre est appelé le paramètre de patience.

Dans keras, l’arrêt anticipé peut être configuré via un callback, comme dans l’exemple suivant :

from keras.callbacks import EarlyStopping


set_random_seed(0)
model = Sequential([
    InputLayer(input_shape=(4, )),
    Dense(units=256, activation="relu"),
    Dense(units=256, activation="relu"),
    Dense(units=256, activation="relu"),
    Dense(units=3, activation="softmax")
])

cb_es = EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True)

n_epochs = 100
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
h = model.fit(X, y, 
              validation_split=0.3, epochs=n_epochs, batch_size=30, 
              verbose=0, callbacks=[cb_es])

Hide code cell source

plt.plot(np.arange(1, len(h.history["loss"]) + 1), h.history["loss"], label="Apprentissage")
plt.plot(np.arange(1, len(h.history["val_loss"]) + 1), h.history["val_loss"], label="Validation")
plt.axhline(y=np.min(h.history["val_loss"]), color="k", linestyle="dashed")
plt.xlim([0, 102])
plt.ylabel("Loss")
plt.xlabel("Epochs")
plt.legend()

glue("epoch_best_model_es", np.argmin(h.history["val_loss"]) + 1, display=False)
../../_images/659d3fe9f1e09389cd4378bd2557502a58c6f174fc94666fffa660b0ba71c522.svg

Et maintenant, même si le modèle était prévu pour être entraîné pendant 70 epochs, l’entraînement est arrêté dès qu’il atteint 10 epochs consécutives sans amélioration de la perte de validation, et les paramètres du modèle sont restaurés comme les paramètres du modèle à l”epoch np.int64(42).

Pénalisation de la perte#

Une autre façon importante d’appliquer la régularisation dans les réseaux neuronaux est la pénalisation des pertes. Un exemple typique de cette stratégie de régularisation est la régularisation L2. Si nous désignons par \(\mathcal{L}_r\) la perte régularisée par L2, elle peut être exprimée comme suit :

\[ \mathcal{L}_r(\mathcal{D} ; m_\theta) = \mathcal{L}(\mathcal{D} ; m_\theta) + \lambda \sum_{\ell} \| \theta^{(\ell)} \|_2^2 \]

\(\theta^{(\ell)}\) est la matrice de poids de la couche \(\ell\).

Cette régularisation tend à réduire les grandes valeurs des paramètres pendant le processus d’apprentissage, ce qui est connu pour aider à améliorer la généralisation.

En keras, ceci est implémenté comme :

from keras.regularizers import L2

λ = 0.01

set_random_seed(0)
model = Sequential([
    InputLayer(input_shape=(4, )),
    Dense(units=256, activation="relu", kernel_regularizer=L2(λ)),
    Dense(units=256, activation="relu", kernel_regularizer=L2(λ)),
    Dense(units=256, activation="relu", kernel_regularizer=L2(λ)),
    Dense(units=3, activation="softmax")
])

n_epochs = 100
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
h = model.fit(X, y, validation_split=0.3, epochs=n_epochs, batch_size=30, verbose=0)

Hide code cell source

plt.plot(np.arange(1, len(h.history["loss"]) + 1), h.history["loss"], label="Apprentissage")
plt.plot(np.arange(1, len(h.history["val_loss"]) + 1), h.history["val_loss"], label="Validation")
plt.axhline(y=np.min(h.history["val_loss"]), color="k", linestyle="dashed")
plt.ylabel("Loss")
plt.xlabel("Epochs")
plt.legend();
../../_images/3227dc3aee19c0dcc79f4d87aba7b15c99a89b6a59b377425a7be010c5ab363b.svg

DropOut#

Figure made with TikZ

Fig. 1 Illustration du mécanisme de DropOut. Afin d’entraîner un modèle donné (à gauche), à chaque minibatch, une proportion donnée de neurones est choisie au hasard pour être « désactivée » et le sous-réseau résultant est utilisé pour l’étape d’optimisation en cours (cf. figure de droite, dans laquelle 40% des neurones – colorés en gris – sont désactivés).

Dans cette section, nous présentons la stratégie DropOut, qui a été introduite dans [Srivastava et al., 2014]. L’idée derrière le DropOut est d’éteindre certains neurones pendant l’apprentissage. Les neurones désactivés changent à chaque minibatch de sorte que, globalement, tous les neurones sont entraînés pendant tout le processus.

Le concept est très similaire dans l’esprit à une stratégie utilisée pour l’entraînement des forêts aléatoires, qui consiste à sélectionner aléatoirement des variables candidates pour chaque division d’arbre à l’intérieur d’une forêt, ce qui est connu pour conduire à de meilleures performances de généralisation pour les forêts aléatoires. La principale différence ici est que l’on peut non seulement désactiver les neurones d’entrée mais aussi les neurones de la couche cachée pendant l’apprentissage.

Dans keras, ceci est implémenté comme une couche, qui agit en désactivant les neurones de la couche précédente dans le réseau :

from keras.layers import Dropout

set_random_seed(0)
switchoff_proba = 0.3
model = Sequential([
    InputLayer(input_shape=(4, )),
    Dropout(rate=switchoff_proba),
    Dense(units=256, activation="relu"),
    Dropout(rate=switchoff_proba),
    Dense(units=256, activation="relu"),
    Dropout(rate=switchoff_proba),
    Dense(units=256, activation="relu"),
    Dropout(rate=switchoff_proba),
    Dense(units=3, activation="softmax")
])

n_epochs = 100
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
h = model.fit(X, y, validation_split=0.3, epochs=n_epochs, batch_size=30, verbose=0)

Hide code cell source

plt.plot(np.arange(1, len(h.history["loss"]) + 1), h.history["loss"], label="Apprentissage")
plt.plot(np.arange(1, len(h.history["val_loss"]) + 1), h.history["val_loss"], label="Validation")
plt.axhline(y=np.min(h.history["val_loss"]), color="k", linestyle="dashed")
plt.ylabel("Loss")
plt.xlabel("Epochs")
plt.legend();
../../_images/5ed0f2e15aa6bdf8e113bae7347cd28789292c87131f17c071ae9109fa799914.svg

Exercice #1

En observant les valeurs de perte dans la figure ci-dessus, pouvez-vous expliquer pourquoi la perte de validation est presque systématiquement inférieure à celle calculée sur le jeu d’apprentissage ?

Références#

[SHK+14]

Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, and Ruslan Salakhutdinov. Dropout: a simple way to prevent neural networks from overfitting. Journal of Machine Learning Research, 15(56):1929–1958, 2014. URL: http://jmlr.org/papers/v15/srivastava14a.html.