import plotly.express as px
import torch
import random
import plotly.graph_objects as go
from ipywidgets import interact, interactive, fixed, interact_manual
import math
from IPython.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))
Régularisation¶
La dernière fois...¶
Notions¶
- Notre premier modèle de deep learning : le perceptron multi-couche
- Permet de calculer une sortie pour une entrée donnée
- Évaluation d'un modèle avec une fonction de coût
- Permet de dire si la sortie du modèle est loin de la réalité suivant une métrique
- Une méthodologie pour optimiser la fonction de coût : la descente de gradient (stochastique)
- Un algorithme pour calculer les gradients : la back propagation
Schéma¶
Est-ce que la fonction de coût est une garantie de qualité ?¶
- La fonction de coût n'est pas nécessairement corrélée à 100% à notre objectif business
- On va rarement nous demander de réduire la RMSE ou l'entropie croisée de nos clients
- Cependant, la vraie fonction objectif n'est pas souvent utilisable en pratique : trop longue à calculer, demande l'annotation d'humaine, pas différentiable, pas de définition mathématique claire...
- Une valeur faible sur un jeu de données en veut pas forcément dire qu'elle sera faible sur un autre jeu de données
- On aimerais que notre réseau de neurone se généralise
- Il va falloir utiliser un jeu d'entraînement, de validation et de test
Procédure d'entrainement et d'évaluation classique¶
Algorithme
- Partage de notre dataset en sous ensembles d'entrainement (train), de validation (val) et de test (test)
- Tant que !(condition d'arrêt)
- Entrainer le réseau sur le train (en général 1 epoch, ou X itérations)
- Évaluation sur le val (coût + autres métriques automatiques)
- Évaluation sur le train (autres métriques automatiques)
- Évaluation sur le test (coût + autres métriques automatiques + humains)
Train/Val/Test¶
- Généralement, on partage (split) aléatoirement notre dataset en trois sous-groupes train/val/test contenant 80%/10%/10% des données
- Peut varier suivant la taille des données, on veut que l'évaluation soit suffisemment rapide tout en étant précise
- On peut descendre à <1% pour le dev et test pour les très gros jeux de données
- Si l'on compare plusieurs baselines, bien utiliser les mêmes train/val/test
- Bien s'assurer qu'il n'y ait pas de données communes entre les sous-groupes (valeur dupliquée par exemple)
- Dans certains cas, le split ne doit pas être aléatoire
- Données temporelle : split par ordre chronologique
- val/test sur une nouveauté retrouvée en pratique : nouvelle personne, nouveau arrière plan, ...
- Essayer d'avoir un dev et un test similaires
Découpage temporel¶
# Données complètes
X = torch.arange(-5, 5, 0.05)
Y = X + 3 * torch.sin(5 * X)
fig = px.scatter(x=X, y=Y)
fig.show()
# Prendre un test aléatoire est trop simple...
X = torch.arange(-5, 5, 0.05)
Y = X + 3 * torch.sin(5 * X)
index = torch.IntTensor(random.choices(list(range(len(X))), k=int(len(X) / 10.0 * 9.0)))
index_bool = torch.ones(X.shape[0], dtype=bool)
index_bool[index] = False
fig = go.Figure(
data=[
go.Scatter(x=X[index_bool], y=Y[index_bool], mode='markers', marker_color="red", name="test"),
go.Scatter(x=X[~index_bool], y=Y[~index_bool], mode='markers', marker_color="blue", name="train")
]
)
fig.show()
# Un découpage temporel permet de mieux valider la générabilité
X = torch.arange(-5, 5, 0.05)
Y = X + 3 * torch.sin(5 * X)
index = torch.IntTensor(list(range(len(X)))[:int(len(X) / 10.0 * 8.0)])
index_bool = torch.ones(X.shape[0], dtype=bool)
index_bool[index] = False
fig = go.Figure(
data=[
go.Scatter(x=X[index_bool], y=Y[index_bool], mode='markers', marker_color="red", name="test"),
go.Scatter(x=X[~index_bool], y=Y[~index_bool], mode='markers', marker_color="blue", name="train")
]
)
fig.show()
Découpage contextuel¶
- La compétition "The Nature Conservancy Fisheries Monitoring" Kaggle : Identifier automatiquement des poissons
- Beaucoup d'approches avaient un très bon score sur le test publique mais des résultats médiocres sur le test final. Pourquoi ?
- Le test final contenait de nouveaux bateaux. Les modèles avaient appris à reconnaitre des bateaux en arrière plan qui pêchent un type de poisson précis.
Évaluation¶
- Nous pouvons réutiliser le loss pour avoir une première approximation des performances du systèmes
- En pratique, nous faisons l'évaluation sur des métriques proches de notre objectif (mais non utilisables dans la fonction de coût)
- Précision, Rappel, AUC (Area Under the Curve), accuracy, ...
- Sur le test final, il est souvent utile de demander à un humain d'évaluer les résultats s'il n'y a pas de métrique claire ou si le jeu de donnée est incomplet ou erroné
Matrice de confusion¶
On compte dans une matrice le nombre d'occurences des couples (prédiction, vrai valeur).
Matrice de confusion - Exemple¶
Métrique : Accuracy¶
L'accuracy correspond au taux de prédictions correctes
$$accuracy = \frac{TP + TN}{TP + TN + FP + FN}$$
Métrique : Précision¶
La précision nous donne le taux de prédictions correctes dans celles prédites comme correctes
$$precision = \frac{TP}{TP + FP}$$
À utiliser quand on veut être sur que si la réponse est positive, alors elle est correcte. Le fait de prédire tous les positifs comme positifs n'est pas important.
Ex: Un système pour investir en bourse, on ne veut pas forcemment investir à chaque fois que la bourse augmente, mais on aimerait investir uniquement dans ce cas.
Remarque : On peut se permettre de dire "je ne sais pas" en faisant une prédiction négative et répondre à peu de prédictions positivement pour avoir un très bon score.
Métrique : Rappel¶
Le rappel mesure le taux d'élements positifs prédits comme tels par le modèle
$$rappel = \frac{TP}{TP + FN}$$
Cette métrique se concentre sur le fait de détecter toutes les occurrences positives, quitte à faire des erreurs sur les négatives.
Ex: Détection d'aliments contaminés dans une chaîne de production. Un seul produit défectueux peut avoir des conséquences sanitaires graves.
Remarque : Pour avoir un score parfait, il suffit de tout prédire en positif.
Métrique : F1¶
Le score F1 est un compromis entre la précision et le rappel
$$F1 = \frac{precision \times rappel}{precision + rappel}$$
Il permet d'éviter les cas très faciles pour la précision et le rappel.
Métrique : Courbe ROC¶
La courbe ROC consiste à tracer le taux de vrai positif en fonction du taux de faux positifs (parfois, on trace aussi la précision en fonction du rappel).
Pour en extraire une métrique, on prend l'aire sous la courbe (AUC = Area Under the Curve) : plus elle est grande, meilleur est le modèle.
Remarque : Comment faire varier TP et FP ? On peut jouer sur la valeur de seuil, usuellement à 0.5.
fig = px.area(x=[0, 1], y=[0, 1])
fig.update_layout(xaxis_title='FPR', yaxis_title='TPR', title="Classificateur aléatoire")
fig.show()
Remarque : Une courbe en dessous de la courbe aléatoire peut être corrigée en inversant les prédictions.
fig = px.area(x=[0, 1], y=[1, 1])
fig.update_layout(xaxis_title='FPR', yaxis_title='TPR', title="Classificateur parfait")
fig.show()
fig = px.area(x=[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
y=[0.1, 0.32, 0.40, 0.45, 0.7, 0.81, 0.89, 0.93, 0.94, 0.98, 1.0])
fig.update_layout(xaxis_title='FPR', yaxis_title='TPR', title="Cas général")
fig.show()
Évaluation humaine¶
Pour être sur de notre évaluation, la manière la plus simple et efficace est de demander à des humains d'annoter les résultats.
Il y a des nombreuses manières de faire l'annotation, par exemple une annotation binaire (vrai/faux) ou multi-critère (dire pourquoi c'est faux).
Même dans ce cas, il se peut que les humains ne soient pas d'accord sur l'annotation. On demande alors à plusieurs humains d'annoter la même prédiction et on utilise un score d'accord (ex: le kappa de Cohen).
Particulièrement utilse dans les sciences sociales
Choix des hyperparamètres¶
Les hyperparamètres sont des paramètres de notre système qui ne sont pas appris mais décidés par un humain.
Exemples : learning rate, nombre de couches et de neurones, ...
On les trouve en essayant plusieurs combinaisons et en comparant les performances sur le jeu de validation (et pas de test !).
Surapprentissage¶
Le surapprentissage (ou overfitting) est un phénomène qui se traduit par de bonnes performances sur le jeu d'entrainement, mais par des performances dégradées sur des valeurs non vues (par exemple, le jeu de validation ou de test).
Remarque : Si l'on fait beaucoup d'expériences pour trouver des hyperparamètres, il est possible de finir par overfit sur le jeu de validation !
X = torch.arange(-2, 2, 0.1)
Y = X * X
X_test = torch.Tensor([-1, -0.8, -0.4, 0.1, 0.2, 0.3, 0.7, 1.1])
Y_test = X_test * X_test + (torch.rand(len(X_test)) - 0.5) / 2 # On ajoute du bruit
fig = go.Figure([
go.Scatter(x=X_test, y=Y_test, mode="markers", marker_color="red", name="dataset"),
go.Scatter(x=X_test, y=Y_test, mode="lines", line_color="blue", name="Modèle")
])
fig.update_layout(title="Overfit pour une régression")
fig.show()
X = torch.arange(-2, 2, 0.1)
Y = X * X
X_test = torch.Tensor([-1, -0.8, -0.4, 0.1, 0.2, 0.3, 0.7, 1.1])
Y_test = X_test * X_test + (torch.rand(len(X_test)) - 0.5) / 2 # On ajoute du bruit
fig = go.Figure([
go.Scatter(x=X_test, y=Y_test, mode="markers", marker_color="red", name="dataset"),
go.Scatter(x=X, y=Y, mode="lines", line_color="blue", name="Modèle")
])
fig.update_layout(title="Modèle plus généralisable")
fig.show()
X = torch.arange(-1, 1, 0.01)
Y = torch.sigmoid(100*X)
X_test = [-0.9, -0.8, -0.75, -0.7, -0.68, -0.61, -0.59, -0.54, 0.53, 0.60, 0.69, 0.71, 0.74, 0.76, 0.79, 0.84]
Y_test = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
fig = go.Figure([
go.Scatter(x=X_test, y=Y_test, mode="markers", marker_color="red", name="dataset"),
go.Scatter(x=X, y=Y, mode="lines", line_color="blue", name="Modèle")
])
fig.add_vrect(x0="-0.5", x1="0.5",
annotation_text="Pas de données", annotation_position="top left",
fillcolor="red", opacity=0.25, line_width=0)
fig.update_layout(title="Modèle de classification trop confiant dans une zone sans donnée")
fig.show()
X = torch.arange(-1, 1, 0.01)
Y = torch.sigmoid(10*X)
X_test = [-0.9, -0.8, -0.75, -0.7, -0.68, -0.61, -0.59, -0.54, 0.53, 0.60, 0.69, 0.71, 0.74, 0.76, 0.79, 0.84]
Y_test = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]
fig = go.Figure([
go.Scatter(x=X_test, y=Y_test, mode="markers", marker_color="red", name="dataset"),
go.Scatter(x=X, y=Y, mode="lines", line_color="blue", name="Modèle")
])
fig.add_vrect(x0="-0.5", x1="0.5",
annotation_text="Pas de données", annotation_position="top left",
fillcolor="red", opacity=0.25, line_width=0)
fig.update_layout(title="Modèle de classification surement plus généralisable")
fig.show()
La courbe bleue représente la probabilité d'appartenir à la classe 1.
Comment détecter un surapprentissage ?¶
Comparez vos métriques entre le train et le dev.
- Si train >> dev => overfit
- Si train > dev => ok ou overfit
- Si train ~ dev => ok ou underfit ou dev trop simple
- Si dev << train => Votre dev a un problème (trop facile)
Sous-apprentissage / Underfit¶
Le sous-apprentissage est un phénophène qui apparait quand les performances du modèles ne sont pas "suffisemment bonnes", c'est-à-dire que l'apprentissage n'est pas terminé ou le modèle n'est pas assez complexe ou est incorrectement paramétré.
X = torch.arange(-2, 2, 0.1)
Y = X * 0 + 0.5
X_test = torch.Tensor([-1, -0.8, -0.4, 0.1, 0.2, 0.3, 0.7, 1.1])
Y_test = X_test * X_test + (torch.rand(len(X_test)) - 0.5) / 2 # On ajoute du bruit
fig = go.Figure([
go.Scatter(x=X_test, y=Y_test, mode="markers", marker_color="red", name="dataset"),
go.Scatter(x=X, y=Y, mode="lines", line_color="blue", name="Modèle")
])
fig.update_layout(title="Modèle linéaire pour des données quadratiques = sous-apprentissage")
fig.show()
Comment détecter un sous-apprentissage ?¶
Il faut définir ce que signifie des performances "suffisemment bonnes". Souvent, cela signifie des performances comparables à un humain.
On peut donc faire annoter des données en entrée par des humains pour qu'ils nous donnent la sortie. Ensuite, on calcule les performances des humains (souvent pas 100% pour des tâches un peu complexes).
- Si train et dev << humain => underfit
- Si train et dev ~ humain => ok
- Si train et dev >> humain => Fantastique !
Réduire le sur-apprentissage avec la régularisation¶
Qu'est-ce que la régularisation ?¶
La régularisation est un ensemble de techniques visant à réduire les erreurs de généralisation (sur le dev et test) en ayant le moins d'impact possible sur l'erreur d'entrainement (sur le train).
Contraindre les poids par la fonction de coût¶
Une première méthode pour contrôler la générabilité consiste à imposer des contraintes sur les poids. Elles peuvent s'intégrer directement dans la fonction de coût.
$$J(W) = \frac{1}{n} \sum_{i=1}^n \mathcal{L}(f(x^{(i)}; W), y^{(i)}) + \lambda * R(W)$$
où $\lambda$ est un hyperparamètre donnant plus ou moins d'importance à la régularisation et $R(W)$ est une fonction pénalisant les poids dans certaines configurations.
Fonctions de régularisation classiques¶
- La régularisation L2 : $R(W) = ||W||_2^2 = \sum_{i,j}W_{i,j}^2$
- Force les poids à être petits
- La régularisation L1 : $R(W) = |W| = \sum_{i,j}| W_{i,j} |$
- Crée de la sparsité dans les poids (beaucoup de 0)
Exemples¶
- $x = [1, 1, 1, 1]$
- $w_1 = [1, 0, 0, 0]$, $|| w_1 ||_2^2 = 1$, $| w_1 | = 1$, $w_1^Tx = 1$
- $w_2 = [0.25, 0.25, 0.25, 0.25]$, $|| w_1 ||_2^2 = 0.25$, $| w_1 | = 1$, $w_1^Tx = 1$
- $w_3 = [4, -1, -1, -1]$, $|| w_1 ||_2^2 = 19$, $| w_1 | = 7$, $w_1^Tx = 1$
Gradient de la régularisation L2¶
$$J_{reg}(W) = \frac{1}{n} \sum_{i=1}^n \mathcal{L}(f(x^{(i)}; W), y^{(i)}) + \lambda * R(W) = J(W) + \lambda * R(W)$$
$$\nabla J_{reg}(W) = \nabla J(W) + 2 \lambda W$$
Ce qui nous donne dans la descente de gradient :
$$W_{t+1} = W_{t} - \eta J(W_t) - \alpha W_t$$
avec $\alpha = 2 \eta \lambda$
Weight decay¶
Le weight decay consiste à retirer un peu de $W_t$ à chaque mise à jour, ce qui est équivalent à une régularisation L2 pour des optimisateurs simples.
Remarque : Souvent, vous verrez mentionné le weight decay dans les arguments de l'optimisateur. Dans ce cas, ce n'est pas la peine de rajouter de la régularisation L2.
Optimisation avancée¶
Problèmes avec la descente de gradient stochastique¶
- Convergence lente quand la pente est grande suivant une direction et petite suivant une autre.
- Le gradient fait de grands aller-retours dans un cas, et de petits sauts dans l'autre
- Reste bloqué dans des minimum locaux et des points col
- Le gradient est nul là-bas
- L'aspect stochastique crée du bruit
def show_sgd():
X = torch.arange(-2, 2, 0.1)
Y = torch.arange(-2, 2, 0.1)
Z = []
for x in X:
temp = []
for y in Y:
temp.append(10 * x * x + y * y)
Z.append(temp)
Z = torch.Tensor(Z)
X_descent, Y_descent, Z_descent = [-1.5], [-1.5], [11 * 1.5 * 1.5]
lr = 0.095
for _ in range(10):
X_descent.append(X_descent[-1] - lr * 2 * 10 * X_descent[-1])
Y_descent.append(Y_descent[-1] - lr * 2 * Y_descent[-1])
Z_descent.append(10 * X_descent[-1] * X_descent[-1] + Y_descent[-1] * Y_descent[-1])
fig = go.Figure(data=[
go.Surface(z=Z.T, x=X, y=Y)
])
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
highlightcolor="limegreen", project_z=True))
fig.add_trace(
go.Scatter3d(x=X_descent, y=Y_descent, z=Z_descent, marker_color="red"))
fig.show()
show_sgd()
def show_local_minimum():
x = torch.arange(-2, 2, 0.01)
t = -2
t_descent = [t]
y_descent = [2.0 - 1 * t - 2 * t ** 2 + 0.16 * t ** 3 + 0.5 * t**4]
lr = 0.2
for _ in range(10):
t = t_descent[-1]
grad = -1 - 4 * t + 0.16 * 3 * t ** 2 + 2 * t ** 3
t = t - lr * grad
t_descent.append(t)
y_descent.append(2.0 - 1 * t - 2 * t ** 2 + 0.16 * t ** 3 + 0.5 * t**4)
fig = go.Figure([
go.Scatter(x=x, y=2.0 - 1 * x - 2 * x ** 2 + 0.16 * x ** 3 + 0.5 * x**4, name="fonction"),
go.Scatter(x=t_descent, y=y_descent, mode="lines+markers", marker=dict(size=10, color="red"), name="valeur")
])
fig.show()
show_local_minimum()
Ajouter de l'inertie avec le Momentum¶
- Idée : On peut garder une trace des mouvements précédents en construisant une vitesse de déplacement
- Dévier de cette vitesse est compliqué
- La vitesse est la moyenne glissante des gradients
$$v_0 = 0$$ $$v_{t+1} = \rho * v_t + \nabla J(W)$$ $$W_{t+1} = W_t - \alpha v_{t+1}$$
def show_momentum():
X = torch.arange(-2, 2, 0.1)
Y = torch.arange(-2, 2, 0.1)
Z = []
for x in X:
temp = []
for y in Y:
temp.append(10 * x * x + y * y)
Z.append(temp)
Z = torch.Tensor(Z)
X_descent, Y_descent, Z_descent = [-1.5], [-1.5], [11 * 1.5 * 1.5]
lr = 0.095
rho = 0.5
v_x, v_y = 0, 0
for _ in range(10):
v_x = rho * v_x + 2 * 10 * X_descent[-1]
v_y = rho * v_y + 2 * Y_descent[-1]
X_descent.append(X_descent[-1] - lr * v_x)
Y_descent.append(Y_descent[-1] - lr * v_y)
Z_descent.append(10 * X_descent[-1] * X_descent[-1] + Y_descent[-1] * Y_descent[-1])
fig = go.Figure(data=[
go.Surface(z=Z.T, x=X, y=Y)
])
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
highlightcolor="limegreen", project_z=True))
fig.add_trace(
go.Scatter3d(x=X_descent, y=Y_descent, z=Z_descent, marker_color="red"))
fig.show()
show_momentum()
def show_momentum_local_mimumum():
x = torch.arange(-2, 2, 0.01)
t = -2
t_descent = [t]
y_descent = [2.0 - 1 * t - 2 * t ** 2 + 0.16 * t ** 3 + 0.5 * t**4]
lr = 0.2
rho = 0.5
v = 0
for _ in range(10):
t = t_descent[-1]
grad = -1 - 4 * t + 0.16 * 3 * t ** 2 + 2 * t ** 3
v = rho * v + grad
t = t - lr * v
t_descent.append(t)
y_descent.append(2.0 - 1 * t - 2 * t ** 2 + 0.16 * t ** 3 + 0.5 * t**4)
fig = go.Figure([
go.Scatter(x=x, y=2.0 - 1 * x - 2 * x ** 2 + 0.16 * x ** 3 + 0.5 * x**4, name="fonction"),
go.Scatter(x=t_descent, y=y_descent, mode="lines+markers", marker=dict(size=10, color="red"), name="valeur")
])
fig.show()
show_momentum_local_mimumum()
Normaliser les gradients avec Adagrad¶
Idée : On veut normaliser chaque dimension par la somme des gradients dans cette direction.
$$G_0 = 0$$ $$G_t = G_{t-1} + \nabla J_t(W)^2$$ (chaque élément est mis au carré) $$W_t = W_{t-1} - \eta * \frac{\nabla J_t(W)}{\sqrt{G_t} + \epsilon}$$
On utilise $\epsilon$ pour éviter une division par 0.
def show_adagrad():
X = torch.arange(-2, 2, 0.1)
Y = torch.arange(-2, 2, 0.1)
Z = []
for x in X:
temp = []
for y in Y:
temp.append(10 * x * x + y * y)
Z.append(temp)
Z = torch.Tensor(Z)
X_descent, Y_descent, Z_descent = [-1.5], [-1.5], [11 * 1.5 * 1.5]
lr = 0.4
g_x = 0
g_y = 0
for _ in range(10):
grad_x = 2 * 10 * X_descent[-1]
grad_y = 2 * Y_descent[-1]
g_x = g_x + grad_x * grad_x
g_y = g_y + grad_y * grad_y
X_descent.append(X_descent[-1] - lr * grad_x / (math.sqrt(g_x) +1e-7))
Y_descent.append(Y_descent[-1] - lr * grad_y / (math.sqrt(g_y) + 1e-7))
Z_descent.append(10 * X_descent[-1] * X_descent[-1] + Y_descent[-1] * Y_descent[-1])
fig = go.Figure(data=[
go.Surface(z=Z.T, x=X, y=Y)
])
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
highlightcolor="limegreen", project_z=True))
fig.add_trace(
go.Scatter3d(x=X_descent, y=Y_descent, z=Z_descent, marker_color="red"))
fig.show()
show_adagrad()
Normaliser en donnant plus d'importance aux points récents : RMSProp¶
Idée : Au lieu de prendre la somme des normes commes dans AdaGrad, on veut donner plus d'importance aux gradients récents.
$$G_0 = 0$$ $$G_t = \delta * G_{t-1} + (1 - \delta) * \nabla J_t(W)^2$$ $$W_t = W_{t-1} - \eta * \frac{\nabla J_t(W)}{\sqrt{G_t} + \epsilon}$$
def show_rmsprop():
X = torch.arange(-2, 2, 0.1)
Y = torch.arange(-2, 2, 0.1)
Z = []
for x in X:
temp = []
for y in Y:
temp.append(10 * x * x + y * y)
Z.append(temp)
Z = torch.Tensor(Z)
X_descent, Y_descent, Z_descent = [-1.5], [-1.5], [11 * 1.5 * 1.5]
lr = 0.4
g_x = 0
g_y = 0
delta = 0.1
for _ in range(10):
grad_x = 2 * 10 * X_descent[-1]
grad_y = 2 * Y_descent[-1]
g_x = delta * g_x + (1 - delta) * grad_x * grad_x
g_y = delta * g_y + (1 - delta) * grad_y * grad_y
X_descent.append(X_descent[-1] - lr * grad_x / (math.sqrt(g_x) +1e-7))
Y_descent.append(Y_descent[-1] - lr * grad_y / (math.sqrt(g_y) + 1e-7))
Z_descent.append(10 * X_descent[-1] * X_descent[-1] + Y_descent[-1] * Y_descent[-1])
fig = go.Figure(data=[
go.Surface(z=Z.T, x=X, y=Y)
])
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
highlightcolor="limegreen", project_z=True))
fig.add_trace(
go.Scatter3d(x=X_descent, y=Y_descent, z=Z_descent, marker_color="red"))
fig.show()
show_rmsprop()
Combiner l'inertie avec la normalisation : ADAM¶
Idée : On veut à la fois le comportement du momentum et de RMSProp
$$moment^1_0 = 0$$ $$moment^2_0 = 0$$ $$moment^1_t = \beta_1 * moment^1_{t-1} + (1 - \beta_1) * \nabla J_t(W)$$ (momentum) $$moment^1_t = \beta_2 * moment^2_{t-1} + (1 - \beta_2) * \nabla J_t(W)^2$$ (RMSProp) $$moment\_debiaisé^1_t = \frac{moment^1_t}{1 - \beta_1^t}$$ $$moment\_debiaisé^2_t = \frac{moment^2_t}{1 - \beta_2^t}$$ $$W_t = W_{t-1} - \eta \frac{moment\_debiaisé^1}{\sqrt{moment\_debiaisé^2} + \epsilon}$$
def show_adam():
X = torch.arange(-2, 2, 0.1)
Y = torch.arange(-2, 2, 0.1)
Z = []
for x in X:
temp = []
for y in Y:
temp.append(10 * x * x + y * y)
Z.append(temp)
Z = torch.Tensor(Z)
X_descent, Y_descent, Z_descent = [-1.5], [-1.5], [11 * 1.5 * 1.5]
lr = 0.1
m1_x, m1_y, m2_x, m2_y = 0, 0, 0, 0
beta1, beta2 = 0.9, 0.999
for t in range(1, 11):
grad_x = 2 * 10 * X_descent[-1]
grad_y = 2 * Y_descent[-1]
m1_x = beta1 * m1_x + (1-beta1) * grad_x
m1_y = beta1 * m1_y + (1-beta1) * grad_y
m2_x = beta2 * m2_x + (1-beta2) * grad_x * grad_x
m2_y = beta2 * m2_y + (1-beta2) * grad_y * grad_y
m1_x_unbais = m1_x / (1 - beta1 ** t)
m1_y_unbais = m1_y / (1 - beta1 ** t)
m2_x_unbais = m2_x / (1 - beta2 ** t)
m2_y_unbais = m2_y / (1 - beta2 ** t)
X_descent.append(X_descent[-1] - lr * m1_x_unbais / (math.sqrt(m2_x_unbais) +1e-7))
Y_descent.append(Y_descent[-1] - lr * m1_y_unbais / (math.sqrt(m2_y_unbais) + 1e-7))
Z_descent.append(10 * X_descent[-1] * X_descent[-1] + Y_descent[-1] * Y_descent[-1])
fig = go.Figure(data=[
go.Surface(z=Z.T, x=X, y=Y)
])
fig.update_traces(contours_z=dict(show=True, usecolormap=True,
highlightcolor="limegreen", project_z=True))
fig.add_trace(
go.Scatter3d(x=X_descent, y=Y_descent, z=Z_descent, marker_color="red"))
fig.show()
show_adam()
En pratique¶
Par défaut, on va utiliser Adam avec comme paramètres $\beta_1 = 0.9$, $\beta_2 = 0.999$ et un taux d'apprentissage variant entre $1*10^{-3}$ et $1*10^{-4}$ (1e-3, 5e-4, et 1e-4 par exemple).
ADAM (et SGD, Adagrad, RMSProp) sont implémentés dans Pytorch !
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from torch import nn
import torch.nn.functional as f
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
iris = load_iris()
x = iris.data
y = iris.target
# Découpage du dataset
# Sklearn ne permet pas de couper en trois directement
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.25)
# On crée des tenseurs
x_train=torch.FloatTensor(x_train)
x_val=torch.FloatTensor(x_val)
x_test=torch.FloatTensor(x_test)
y_train=torch.LongTensor(y_train)
y_val=torch.LongTensor(y_val)
y_test=torch.LongTensor(y_test)
class LinearModel(nn.Module):
def __init__(self, in_dim=4, hidden_dim=10, out_dim=3):
super().__init__()
self.linear1 = nn.Linear(in_dim, hidden_dim)
self.linear2 = nn.Linear(hidden_dim, out_dim)
def forward(self, x):
out1 = f.relu(self.linear1(x))
return self.linear2(out1)
losses_train = []
losses_val = []
accuracies = []
model = LinearModel()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters())
# Boucle d'entrainement
for epoch in range(5000):
# Ici, on ne fait pas de batch
optimizer.zero_grad() # On met les gradients à 0
outputs = model(x_train)
loss = criterion(outputs, y_train)
loss.backward()
optimizer.step() # Fait tout pour nous
# Validation
with torch.no_grad():
val_outputs = model(x_val)
loss_val = criterion(val_outputs, y_val)
losses_val.append(loss_val.item())
# On prend l'indice avec la plus haute valeur
_, predicted = torch.max(val_outputs, 1)
accuracy = accuracy_score(y_val, predicted)
accuracies.append(accuracy)
losses_train.append(loss.item())
# Test
with torch.no_grad():
test_outputs = model(x_test)
_, predicted = torch.max(test_outputs, 1)
# precision / recall / f1 score
precision = precision_score(y_test, predicted, average='weighted')
recall = recall_score(y_test, predicted, average='weighted')
f1 = f1_score(y_test, predicted, average='weighted')
print(f'Test Accuracy: {accuracy * 100:.2f}%')
Test Accuracy: 100.00%
fig = go.Figure([
go.Scatter(x=list(range(len(losses_train))), y=losses_train, name="train"),
go.Scatter(x=list(range(len(losses_val))), y=losses_val, name="val"),
])
fig.show()
fig = go.Figure([
go.Scatter(x=list(range(len(accuracies))), y=accuracies, name="Val accuracy"),
])
fig.show()
En résumé¶
- Procédure d'entraînement
- Overfit, underfit
- Découpage du dataset et cas particuliers
- Métriques de classification
- Régularisation et weight decay
- Le moment
- Optimiseur : ADAM, AdaGrad, RMSProp
Ressources additionnelles¶
- http://web.archive.org/web/20240314024529/https://introtodeeplearning.com/slides/6S191_MIT_DeepLearning_L1.pdf
- https://m2dsupsdlclass.github.io/lectures-labs/slides/02_backprop/index.html#64
- https://www.deeplearningbook.org/contents/regularization.html
- https://web.eecs.umich.edu/~justincj/slides/eecs498/WI2022/598_WI2022_lecture04.pdf
- https://dataflowr.github.io/website/modules/4-optimization-for-deep-learning/