À la fin de ce chapitre, vous saurez :
autograd.Lorsqu’on entraîne un réseau de neurones, l’objectif est de minimiser l’erreur entre les prédictions du modèle et les valeurs attendues. Cette erreur est mesurée par une fonction de perte (loss function en anglais).
Une fonction de perte prend en entrée :
et retourne un nombre réel qui indique "à quel point le modèle s'est trompé".
Par conséquent, plus la perte est grande → plus le modèle se trompe et plus la perte est petite → plus le modèle est proche de la bonne réponse.
La fonction de perte est essentielle pour plusieurs raisons :
On appelle régression le cas où le modèle doit prédire une valeur numérique par exemple : la température demain, la taille d’une personne, etc.
Dans ce cas, la fonction de perte la plus utilisée est l’erreur quadratique moyenne (MSE de l'anglais Mean Squared Error) :
$$L(y, \hat{y}) = \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2,$$où :
La fonction MSE calcule la moyenne des erreurs au carré de toutes les données.
Pour utiliser la fonction MSE dans PyTorch, on peut utiliser la classe nn.MSELoss(). Pour cela, il faut d'abord importer le module torch.nn qui contient les fonctions de perte :
import torch.nn as nn
Exemple :
# Valeurs réelles et prédictions
y_true = torch.tensor([2.0, 3.0, 4.0])
y_pred = torch.tensor([2.5, 2.7, 4.2])
# Définition de la fonction de perte MSE
loss_fn = nn.MSELoss()
# Calcul de la perte
loss = loss_fn(y_pred, y_true)
print(loss)
On appelle classification le cas où le modèle doit prédire à quelle catégorie appartient la donnée parmi plusieurs possibles par exemple : "chat" ou "chien", ou bien "spam" ou "non spam", etc.
Dans ce cas, la fonction de perte la plus courante est l'entropie croisée (Cross-Entropy Loss en anglais). Elle compare la probabilité prédite par le modèle et la vraie catégorie (donnée par les données d’apprentissage) :
$$L(y, \hat{y}) = -\sum_{i=1}^n y_i \log(\hat{y}_i),$$où :
La fonction d'entropie croisée mesure la distance entre la distribution de probabilité prédite par le modèle et la distribution de probabilité réelle (la vraie classe). La présence de la somme permet de prendre en compte toutes les classes. Mais, dans le cas du one-hot encoding, seul le terme correspondant à la vraie classe reste (puisque tous les autres \(y_i\) valent 0).
L'entropie croisée est utilisée car :
Prenons un exemple où on a 3 classes possibles : "Chat", "Chien", "Oiseau". Nous avons :
Alors :
$$L = - \big( 1 \cdot \log(0.7) + 0 \cdot \log(0.2) + 0 \cdot \log(0.1) \big)$$Les termes multipliés par 0 disparaissent :
$$L = -\log(0.7)$$👉 La perte est faible car le modèle a donné une forte probabilité à la bonne classe.
Si au contraire le modèle avait prédit : \(\hat{y} = [0.2, 0.7, 0.1]\) :
$$L = -\log(0.2)$$👉 La perte serait plus grande, car la probabilité attribuée à la bonne classe ("Chat") est faible.
Pour utiliser la fonction Cross-Entropy Loss dans PyTorch, on peut utiliser la classe nn.CrossEntropyLoss() du module torch.nn.
# Définition de la fonction de perte
loss_fn = nn.CrossEntropyLoss()
# Cas 1 : le modèle prédit correctement (forte valeur pour "Chat")
logits1 = torch.tensor([[2.0, 1.0, 0.1]]) # sortie brute du modèle qui sera convertie à l'aide d'une fonction de PyTorch en probabilités
y_true = torch.tensor([0]) # la vraie classe est "Chat" (indice 0)
loss1 = loss_fn(logits1, y_true)
print("Perte (bonne prédiction) :", loss1.item())
# Cas 2 : le modèle se trompe (forte valeur pour "Chien")
logits2 = torch.tensor([[0.2, 2.0, 0.1]]) # sortie brute du modèle qui sera convertie à l'aide d'une fonction de PyTorch en probabilités
loss2 = loss_fn(logits2, y_true)
print("Perte (mauvaise prédiction) :", loss2.item())
L’optimisation est l’étape qui permet d’ajuster les paramètres du modèle pour qu’il réalise mieux la tâche demandée.
L’idée est simple :
C’est un processus itératif qui se répète jusqu’à ce que le modèle apprenne correctement.
L’algorithme d’optimisation le plus courant est la descente de gradient (ou Gradient Descent en anglais).
Imaginons une montagne :
- La hauteur correspond à la valeur de la fonction de perte.
- Le but est de descendre la montagne pour atteindre la vallée (la perte minimale).
- Le gradient indique la pente : on suit la pente descendante pour réduire la perte.
Formule de mise à jour des paramètres :
$$\theta_{new} = \theta_{old} - \eta \cdot \nabla_\theta L(\theta)$$où :
Prenons un exemple très simple : nous voulons ajuster un seul paramètre \(a\) pour approximer une fonction.
Supposons que le modèle soit une droite passant par l’origine :
$$f(x) = a x$$Nous avons une donnée d’apprentissage :
On part du paramètre initial : \(a = 0\).
1. Fonction de perte
On utilise l’erreur quadratique (MSE) pour mesurer l’écart entre la prédiction et la vraie valeur :
$$L(a) = (f(x) - y)^2 = (a * 2 - 4)^2$$2. Calcul du gradient
On dérive la perte par rapport à \(a\) :
$$\frac{\partial L}{\partial a} = 2 * (a * 2 - 4) * 2 = 8a - 16$$3. Mise à jour avec descente de gradient
On choisit un taux d’apprentissage \(\eta = 0.1\) et on applique la formule :
$$a_{new} = a_{old} - \eta \cdot \frac{\partial L}{\partial a}$$4. Exemple numérique
👉 Après une étape, \(a\) se rapproche déjà de la bonne valeur (qui devrait être \(a = 2\) pour que \(f(x) = 2 * 2 = 4\)).
En répétant plusieurs mises à jour, \(a\) converge vers 2, et la perte devient de plus en plus faible.
PyTorch fournit le module torch.optim qui implémente plusieurs algorithmes d’optimisation. Dans PyTorch, l’algorithme de descente de gradient est appelé SGD (Stochastic Gradient Descent) et peut être importé via torch.optim.SGD :
import torch.optim as optim
On reprend le modèle simple :
# Données
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
y = torch.tensor([2.0, 4.0, 6.0, 8.0])
a = torch.tensor([0.0], requires_grad=True)
# Optimiseur : descente de gradient
optimizer = optim.SGD([a], lr=0.1)
# Fonction de perte : MSE
loss_fn = nn.MSELoss()
for i in range(10):
# 1. Remettre les gradients à zéro avant de recalculer
optimizer.zero_grad()
# 2. Calcul de la prédiction
y_pred = a * x
# 3. Calcul de la perte avec MSE
loss = loss_fn(y_pred, y)
# 4. Calcul automatique des gradients
loss.backward()
# 5. Mise à jour du paramètre a
optimizer.step()
print(f"Iter {i+1}: a = {a.item()}, loss = {loss.item()}")
Explications des nouvelles lignes de code :
optimizer.zero_grad() : remet à zéro les gradients calculés lors de la dernière itération.
Sinon, PyTorch additionne les gradients à chaque backward(), ce qui fausserait les calculs.optimizer.step() : applique la mise à jour des paramètres selon la règle de la descente de gradient :
\(a_{new} = a_{old} - lr * \frac{\partial loss}{\partial a}\).Dans cet exemple, SGD converge très vite car le problème est simple.
Adam est un autre algorithme d'optimisation qui adapte le pas pour chaque paramètre grâce à une moyenne mobile des gradients (\(m_t\) ) et une moyenne mobile des carrés des gradients (\(v_t\)).
On définit :
La mise à jour des paramètres est alors :
$$\theta_{\text{new}} = \theta_{\text{old}} - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}$$💡 Interprétation :
Différences entre Adam et la descente de gradient classique (SGD) :
- SGD applique la même règle de mise à jour pour tous les paramètres à chaque itération : \(\theta_{new} = \theta_{old} - lr * \frac{\partial L}{\partial \theta}\).
- Adam adapte le taux d'apprentissage pour chaque paramètre individuellement, en utilisant des moyennes mobiles des gradients et des carrés des gradients.
Cela permet souvent une convergence plus rapide et plus stable.- La syntaxe PyTorch reste très similaire : on utilise toujours
optimizer.zero_grad(),loss.backward()etoptimizer.step(). On peut reprendre le même modèle simple que précédemment à titre d'exemple.
⚠️ Remarque : Dans le cadre de ce cours, nous utiliserons principalement Adam pour sa robustesse et sa facilité d'utilisation. Nous allons surtout utiliser l'implémentation de ADAM dans Pytorch sans avoir à recoder les équations. Elles sont énoncées à titre informatif.
Dans PyTorch, Adam est implémenté via torch.optim.Adam :
# Données
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
y = torch.tensor([2.0, 4.0, 6.0, 8.0])
a = torch.tensor([0.0], requires_grad=True)
# Optimiseur : Adam
optimizer = torch.optim.Adam([a], lr=0.1)
# Fonction de perte : MSE
loss_fn = nn.MSELoss()
for i in range(50):
optimizer.zero_grad() # remise à zéro des gradients
y_pred = a * x
loss = loss_fn(y_pred, y) # perte MSE
loss.backward() # calcul automatique des gradients
optimizer.step() # mise à jour du paramètre
print(f"Iter {i+1}: a = {a.item()}, loss = {loss.item()}")
💡 Remarques :
Dans cet exercice, vous allez implémenter une boucle d'entraînement simple pour ajuster les paramètres d'une droite aux données fournies.
On vous donne les données suivantes :
# Données bruitées suivantes
import numpy as np
x = np.random.rand(1000)
y_true = x * 1.54 + 12.5 + np.random.rand(1000)*0.2
Objectif : Trouver une droite de la forme :
$$y = f(x) =a x + b$$où : \(a\) et \(b\) sont des paramètres appris automatiquement en minimisant l'erreur entre les prédictions du modèle et les données réelles.
Consigne : Écrire un programme qui ajuste les paramètres \(a\) et \(b\) de la droite aux données fournies en utilisant PyTorch.
1) Dans un premier temps, vous pouvez faire une boucle de 10000 itérations et coder vous-même la fonction de perte.
2) Affichez les paramètres appris \(a\) et \(b\).
3) Ensuite, trouvez un moyen plus intelligent d'arrêter l'entraînement de telle sorte que le modèle converge avec le minimum d'itérations.
4) Affichez le nombre d'itérations nécessaires pour converger.
5) Tracez les données réelles et les données prédites pour comparer visuellement le résultat.
6) Utilisez la fonction de perte MSE fournie par PyTorch et affichez les paramètres appris \(a\) et \(b\).
7) Vérifiez que le résultat des paramètres et le tracé sont similaires à ceux obtenus avec la boucle d'entraînement manuelle.
Remarque : Pour utiliser matplotlib, vous devez l'installer avec la commande suivante :
pip install matplotlib
Puis, vous pouvez l'importer dans votre code avec :
import matplotlib.pyplot as plt
%matplotlib inline #À ajouter si vous utilisez Jupyter Notebook
Astuce :
torch.optim.Adam) avec un taux d'apprentissage (learning rate) de 1e-3.torch.mean((y_true - y_pred) ** 2)) ou loss = torch.sum((y_pred - y_true) ** 2) / y_true.shape[0]).Résultat attendu : Vous devez obtenir un graphique où :
- les points bleus correspondent aux données réelles (y_true),
- et une droite rouge correspond aux prédictions (y_pred).
Exemple d’affichage attendu :
Objectif :
L'objectif est le même que celui de l'exercice précédent (faire de la régression linéaire), mais cette fois-ci, vous allez utiliser une fonction de perte de type valeur absolue (MAE de l'anglais Mean Absolute Error) au lieu de la MSE. L’idée de cet exercice est de comparer deux optimisateurs SGD et Adam.
Consignes : Implémenter une boucle d'entraînement pour ajuster les paramètres d'une droite aux données fournies dans l'exercice précédent en utilisant une fonction de perte de type valeur absolue et en réutilisant l'implémentation de l'exercice précédent.
1) Réutilisez la boucle d'entraînement de l’exercice précédent qui s'arrête au bout de 2500 itérations et qui utilise un learning rate de 0.01.
2) Remplacez la fonction de perte MSE par une fonction de perte de type MAE. Il faudra chercher dans la documentation comment l'implémenter dans PyTorch.
3) Testez avec l’optimiseur SGD puis avec l’optimiseur Adam.
4) Pour chaque optimiseur, affichez les paramètres appris \(a\) et \(b\).
5) Tracez les données réelles et les données prédites pour comparer visuellement les résultats.
6) Comparez les deux méthodes : que constatez-vous en termes de stabilité et de vitesse de convergence ?
7) Expliquez quel optimiseur est meilleur et pourquoi?
8) Essayez de modifier le taux d'apprentissage (learning rate) pour voir son impact sur la convergence ainsi que le nombre d'itérations nécessaires.
Astuce :
nn.L1Loss().Résultat attendu : Vous devez obtenir des valeurs pour les paramètres proches de :
et un graphique similaire à celui ci-dessous :