Problème : Minimiser une fonction \(f(x)\) de \(\mathbb{R}\) dans \(\mathbb{R}\).
Exemple : \(f(x) = x^2\), dont le minimum est à \(x = 0\).
Initialisation : \(x_0\)
Mise à jour des paramètres : \[
x_{n+1} = x_{n} - \eta \frac{df}{dx}(x_n)
\] où \(\eta\) est le learning rate.
Convergence : \(x_n\) converge vers un minimum local sous certaines conditions (dont \(\eta \rightarrow 0\))
import numpy as npimport matplotlib.pyplot as plt# Définition de la fonction et de son gradientdef f(x):return x**2def grad_f(x):return2*x# Paramètres de la descente de gradientx_init =8# Point de départlearning_rate =0.1# Taux d'apprentissageepochs =10# Nombre d'itérations# Stocker les valeurs pour l'illustrationx_vals = [x_init]f_vals = [f(x_init)]# Descente de gradientx = x_initfor i inrange(epochs): x = x - learning_rate * grad_f(x) # Mise à jour du paramètre x_vals.append(x) # Sauvegarder la valeur de x f_vals.append(f(x)) # Sauvegarder la valeur de f(x)# Générer des points pour dessiner la fonctionx_range = np.linspace(-10, 10, 400)y_range = f(x_range)# Tracer la fonction et les étapes de la descente de gradientplt.figure(figsize=(8, 6))plt.plot(x_range, y_range, label="$f(x) = x^2$", color="blue")# Visualisation des points de la descente de gradientplt.scatter(x_vals, f_vals, color="red", zorder=5)plt.plot(x_vals, f_vals, color="red", linestyle="--", label="Descente de gradient", zorder=4)# Annotations pour montrer les étapesfor i, (x, y) inenumerate(zip(x_vals, f_vals)): plt.text(x, y, f"Step {i}", fontsize=8, verticalalignment='bottom')# Mise en forme de la figureplt.title("Illustration de la descente de gradient en 1D")plt.xlabel("$x$")plt.ylabel("$f(x)$")plt.axhline(0, color="black",linewidth=0.5)plt.axvline(0, color="black",linewidth=0.5)plt.legend()plt.grid(True)plt.show()
1.5 Descente de gradient
Problème : Minimiser une fonction \(f(x)\) de \(\mathbb{R}^d\) dans \(\mathbb{R}\).
Exemple : \(f(x) = \|x\|^2\), dont le minimum est à \(x = 0\).
Initialisation : \(x_0 \in \mathbb{R}^d\)
Mise à jour des paramètres : \[
x_{n+1} = x_{n} - \eta \nabla f(x_n)
\] où \(\eta\) est le learning rate.
import numpy as npimport matplotlib.pyplot as plt# Perturbed quadratic functiondef f(x, y):return x**2+ y**2+0.5* np.sin(3* x) +0.1* np.sin(3* y)# Gradient of the perturbed quadratic functiondef grad_f(x, y): df_dx =2*x +3/2* np.cos(3* x) df_dy =2*y +0.3* np.cos(3* y)return np.array([df_dx, df_dy])# Parameters for gradient descentlearning_rate =0.1epochs =20x_init, y_init =4, 4# Starting point# Store the values for visualizationx_vals = [x_init]y_vals = [y_init]f_vals = [f(x_init, y_init)]# Perform gradient descentx, y = x_init, y_initfor i inrange(epochs): grad = grad_f(x, y) x -= learning_rate * grad[0] y -= learning_rate * grad[1]# Record the new positions x_vals.append(x) y_vals.append(y) f_vals.append(f(x, y))# Generate grid points for contour plotx_range = np.linspace(-5, 5, 400)y_range = np.linspace(-5, 5, 400)X, Y = np.meshgrid(x_range, y_range)Z = f(X, Y)# Plot the contour linesplt.figure(figsize=(8, 6))contours = plt.contour(X, Y, Z, levels=30, cmap='viridis')plt.clabel(contours)# Plot the gradient descent pathplt.plot(x_vals, y_vals, 'ro-', markersize=3, label='Gradient Descent')# Add arrows to show direction of descentfor i inrange(1, len(x_vals), 20): plt.arrow(x_vals[i-1], y_vals[i-1], x_vals[i]-x_vals[i-1], y_vals[i]-y_vals[i-1], head_width=0.2, head_length=0.2, fc='red', ec='red')# Set up plot labels and formattingplt.title("Gradient Descent on Perturbed Quadratic Function")plt.xlabel('$x$')plt.ylabel('$y$')plt.axhline(0, color='black', linewidth=0.5)plt.axvline(0, color='black', linewidth=0.5)plt.grid(True)plt.legend()plt.show()
1.6 Descente de gradient stochastique
Problème : Minimiser \(f(\mathbf{x})\) alors que \(\nabla f\) est coûteux à calculer
On suppose que l’on dispose d’un estimateur \(G\) de \(\nabla f\), sans biais: \[E[G(x)] = \nabla f(x)\]
On suppose \(G\) moins coûteux
On appelle \(G\) le gradient stochastique
SGD
On échantillonne \(G(x_n)\)
Mise à jour des paramètres : \[
x_{n+1} = x_{n} - \eta G(x_n)
\]
import numpy as npimport matplotlib.pyplot as plt# Perturbed quadratic functiondef f(x, y):return x**2+ y**2+0.5* np.sin(3* x) +0.1* np.sin(3* y)# Gradient of the perturbed quadratic function with noise (stochastic)def grad_f_stochastic(x, y): df_dx =2* x +3/2* np.cos(3* x) + np.random.normal(scale=2) df_dy =2* y +0.3* np.cos(3* y) + np.random.normal(scale=2)return np.array([df_dx, df_dy])# Parameters for stochastic gradient descentlearning_rate =0.1epochs =20x_init, y_init =4, 4# Starting point# Store the values for visualizationx_vals = [x_init]y_vals = [y_init]f_vals = [f(x_init, y_init)]# Perform stochastic gradient descentx, y = x_init, y_initfor i inrange(epochs): grad = grad_f_stochastic(x, y) x -= learning_rate * grad[0] y -= learning_rate * grad[1]# Record the new positions x_vals.append(x) y_vals.append(y) f_vals.append(f(x, y))# Generate grid points for contour plotx_range = np.linspace(-5, 5, 400)y_range = np.linspace(-5, 5, 400)X, Y = np.meshgrid(x_range, y_range)Z = f(X, Y)# Plot the contour linesplt.figure(figsize=(8, 6))contours = plt.contour(X, Y, Z, levels=30, cmap='viridis')plt.clabel(contours)# Plot the stochastic gradient descent pathplt.plot(x_vals, y_vals, 'ro-', markersize=3, label='SGD Path')# Add arrows to show direction of stochastic descentfor i inrange(1, len(x_vals), 5): plt.arrow(x_vals[i-1], y_vals[i-1], x_vals[i]-x_vals[i-1], y_vals[i]-y_vals[i-1], head_width=0.2, head_length=0.2, fc='red', ec='red')# Set up plot labels and formattingplt.title("Stochastic Gradient Descent (SGD) on Perturbed Quadratic Function")plt.xlabel('$x$')plt.ylabel('$y$')plt.axhline(0, color='black', linewidth=0.5)plt.axvline(0, color='black', linewidth=0.5)plt.grid(True)plt.legend()plt.show()
1.7 SGD dans la pratique
Fonction de coût:
modèle \(f_\theta\)
on cherche à minimiser sur \(\theta\)
on somme le coût sur toutes les données \((x_i,y_i)\) du dataset d’entraînement \[
l(\theta) = \sum_i distance(f_\theta(x_i),y_i)
\]
Gradient stochastique:
on découpe le dataset en batches\(B_1,...,B_k\)
On choisit un batch au hasard
On évalue la fonction de coût sur ce batch
C’est bien sans biais! (si les batches sont de même taille)
2 Differentiation automatique
2.1 Objectif:
Pour calculer le gradient de la fonction de coût:
différences finies : trop lent !
sur papier: bien trop compliqué; il faudrait refaire le calcul dès que le modèle change.
Solution:
la différentiation automatique avec pytorch !
permet de calculer automatiquement des dérivées d’expressions mathématiques / programmes informatiques
2.2 Principe:
Comment cela fonctionne:
pytorch observe les opérations mathématiques effectuées par l’évaluation du modèle : addition, multiplication, exponentielle, fonction trigonométrique, etc
pytorch écrit le modèle comme une composition de ces fonctions mathématiques usuelles (*)
pytorch connaît les dérivées de ces fonctions mathématiques usuelles
pytorch utilise alors la règle de dérivée de composée de fonctions pour évaluer le gradient
(*) c’est une simplification: en réalité pytorch crée un arbre décrivant l’expression mathématique du modèle
2.3 Différentielle d’une composée
Soit \(f_1:\mathbb{R}^n \rightarrow \mathbb{R}\), \(f_2:\mathbb{R}^n \rightarrow \mathbb{R}^n\) et \(f_3:\mathbb{R}^n \rightarrow \mathbb{R}^n\) posons \[f=f_1\circ f_2\circ f_3 \]
Pour calculer la différentielle de \(f\) en \(x\), posons: \[A_1=J_{f_1}(f_2of_3(x))\in M_{n,1}(\mathbb{R})\]\[A_2=J_{f_2}(f_3(x))\in M_{n,n}(\mathbb{R})\]\[A_3=J_{f_3}(x)\in M_{n,n}(\mathbb{R})\] Le calcul de la différentielle nous donne: \[J_f(x)=A_1 A_2 A_3\]
2.4 Coût du calcul de \(A_1 A_2 A_3\):
Il y a deux manières de faire ce produit matriciel, l’une est plus coûteuse que l’autre:
Forward:\(J_f(x)=A_1 (A_2 A_3)\)
Posons \(B=A_2A_3\)
Coût de calcul de \(B\): \(n^3\) ; coût de calcul de \(A_1B\): \(n^2\)
Coût total: \(O(n^3)\)
Backward:\(J_f(x)=(A_1 A_2) A_3\)
Posons \(C=A_1A_2\)
Coût de calcul de \(C\): \(n^2\) ; coût de calcul de \(CA_3\) : \(n^2\)
2.5 Différentiation automatique : Forward vs Backward
Les bibliothèques de différentiation automatique :
proposent 2 modes: forward et backward
le mode forward est plus rapide si l’espace de départ est plus petit que celui d’arrivée
le mode backward est plus rapide si l’espace d’arrivée est plus petit que l’espace de départ
Pour l’apprentissage:
l’espace d’arrivée de la fonction de coût est \(\mathbb{R}\)
on choisit donc toujours le mode backward
2.6 Avec pytorch
import torch# Define a tensor with requires_grad=True to track computationsx = torch.tensor(3.0, requires_grad=True)y = x**2+ xprint("y =",y.item())y.backward()# The gradient of y with respect to x is stored in x.gradprint(f"dy/dx = {x.grad.item()} at x =",x.item())
y = 12.0
dy/dx = 7.0 at x = 3.0
on initialise \(x = 3\)
on indique que l’on souhaite calculer des gradients par rapport à \(x\) avec requires_grad=True
on calcule \(y = x^2 + x\)
on effectue une differentiation automatique backward
Résultat:
y.backward() évalue \(dx/dy\) en \(x = 3\)
le résultat est stocké dans x.grad
2.7 Attention
Pytorch accumule les gradients
Si on évalue le gradient de deux expressions dépendant d’un tenseur \(x\), les deux gradients seront sommés!
C’est très utile pour pytorch
Mais il ne faut pas oublier de remettre les gradients à 0 entre deux calculs indépendants !
3 Apprentissage avec pytorch
3.1 Charger les données
Pytorch distingue 2 concepts :
le Dataset, qui charge le dataset à la demande
la classe DataLoader qui permet d’itérer le dataset et de créer des batches (pour l’algorithme de descente de gradient stochastique)
3.2 Dataset: depuis un tenseur
charge un dataset depuis un tableau de données et un tableau de labels
pour accéder à la taille du dataset : __len__()
pour accéder au i-ème élément : __getitem(i)__
from torch.utils.data import TensorDataset# Exemple de dataset simpledata = torch.tensor([[1.0], [2.0], [3.0], [4.0]])labels = torch.tensor([[0.0], [0.0], [1.0], [1.0]])# Création d'un TensorDatasetdataset = TensorDataset(data, labels)print("length:", dataset.__len__())print("elem 0:", dataset.__getitem__(0))
length: 4
elem 0: (tensor([1.]), tensor([0.]))
3.3 Dataset: custom
Pour charger un dataset, il faut implémenter la classe dataset comme suit :
from torch.utils.data import Datasetclass CustomDataset(Dataset):def__init__(self, some_args, transform=None, target_transform=None):# initialize whatever you need heredef__len__(self):#return the length of the datasetdef__getitem__(self, idx):#load data at index = idx data = .... label = ....return data, label
Usage:
__init__ : initialise le dataset. Par exemple avec le répertoire où se trouve le dataset
__len__ : retourne la taille du dataset
__get_item__ : retourne une donnée du dataset
Custom vs TensorDataset:
permet de charger les éléments à la demande depuis des fichiers
utile si le dataset prend trop de place dans la mémoire RAM
3.4 DataLoader
from torch.utils.data import DataLoader, TensorDataset# Exemple de dataset simpledata = torch.tensor([[1.0], [2.0], [3.0], [4.0]])labels = torch.tensor([[0.0], [0.0], [1.0], [1.0]])# Création d'un TensorDatasetdataset = TensorDataset(data, labels)# Initialisation du DataLoaderdataloader = DataLoader(dataset, batch_size=2, shuffle=True)# Iteration sur le DataLoaderfor batch in dataloader:print("data:",batch[0],"labels:",batch[1])
import torchimport torch.nn as nnimport torch.nn.functional as Fclass SimpleNN(nn.Module):def__init__(self):super(SimpleNN, self).__init__()self.flatten = nn.Flatten()# Définir les poids et biais pour trois couchesself.w1 = nn.Parameter(torch.randn(28*28,512))self.b1 = nn.Parameter(torch.randn(512))self.w2 = nn.Parameter(torch.randn(512, 512))self.b2 = nn.Parameter(torch.randn(512))self.w3 = nn.Parameter(torch.randn(512, 10))self.b3 = nn.Parameter(torch.randn(10))def forward(self, x): x =self.flatten(x)# Appliquer les transformations et les fonctions d'activation x = F.relu(x.matmul(self.w1) +self.b1) x = F.relu(x.matmul(self.w2) +self.b2) x = x.matmul(self.w3) +self.b3return x# Instancier le modèlemodel = SimpleNN()
3.6 Fonction de coût
Pour évaluer le coût sur un batch de données:
dataloader = DataLoader(training_data, batch_size=2, shuffle=True)loss_function= torch.nn.CrossEntropyLoss()# Calculer la perte pour un batchfor batch_data, batch_labels in dataloader: outputs = model(batch_data) loss = loss_function(outputs, batch_labels)print(f'Loss: {loss.item()}')break# Calcul de la perte pour un seul batch
Loss: 3317.5478515625
3.7 descente de gradient
train_loader = DataLoader(dataset=training_data, batch_size=len(training_data), shuffle=True)full_batch_data, full_batch_labels =next(iter(train_loader))learning_rate =0.01num_steps =6for i inrange(num_steps):# Zero the gradients model.zero_grad()# Forward pass outputs = model(full_batch_data) loss = loss_function(outputs, full_batch_labels)# Backward pass loss.backward()# Update weights manuallywith torch.no_grad():for param in model.parameters(): param -= learning_rate * param.gradprint(f'step [{i+1}/{num_steps}], Loss: {loss.item():.4f}')
torch.optim.SGD: fonction d’optimisation SGD de pytorch
optimizer.zero_grad(): mise à 0 du gradient
optimizer.step(): mise à jour des paramètres du modèle
4 Problèmes de l’algorithme d’apprentissage
4.1 Mauvais learning rate
4.1.1 Problème de Taux d’Apprentissage Inadapté
Le taux d’apprentissage est un hyperparamètre clé qui contrôle la vitesse à laquelle un modèle ajuste ses poids pendant l’entraînement.
Un taux d’apprentissage trop élevé peut entraîner des oscillations autour du minimum global ou même empêcher la convergence.
Un taux d’apprentissage trop faible ralentit le processus d’entraînement, risquant de piéger le modèle dans un minimum local ou de stagner.
# Import des bibliothèques nécessairesimport torchimport torch.nn as nnimport torch.optim as optimimport matplotlib.pyplot as plt# Génération de données artificielles pour un problème de régressionx = torch.unsqueeze(torch.linspace(-5, 5, 100), dim=1)y = x.pow(2) +0.2* torch.rand(x.size()) # Fonction quadratique avec un peu de bruit# Définition d'un modèle simple de régressionclass SimpleNet(nn.Module):def__init__(self):super(SimpleNet, self).__init__()self.hidden = nn.Linear(1, 10)self.predict = nn.Linear(10, 1)def forward(self, x): x = torch.relu(self.hidden(x))returnself.predict(x)# Initialisation du modèlemodel = SimpleNet()criterion = nn.MSELoss()
# Taux d'apprentissage élevémodel_high_lr = SimpleNet() # Réinitialisation du modèleoptimizer_high_lr = optim.SGD(model_high_lr.parameters(), lr=0.1) # Taux d'apprentissage élevé# Entraînement avec un taux d'apprentissage élevélosses_high_lr = []for epoch inrange(50): prediction = model_high_lr(x) loss = criterion(prediction, y) optimizer_high_lr.zero_grad() loss.backward() optimizer_high_lr.step() losses_high_lr.append(loss.item())# Visualisation de la perte#plt.plot(losses_high_lr, label="High Learning Rate (0.1)")#plt.xlabel("Epoch")#plt.ylabel("Loss")#plt.title("Oscillations dues à un Taux d'Apprentissage Trop Élevé")#plt.legend()#plt.show()
# Taux d'apprentissage faiblemodel_low_lr = SimpleNet() # Réinitialisation du modèleoptimizer_low_lr = optim.SGD(model_low_lr.parameters(), lr=0.0001) # Taux d'apprentissage faible# Entraînement avec un taux d'apprentissage faiblelosses_low_lr = []for epoch inrange(50): prediction = model_low_lr(x) loss = criterion(prediction, y) optimizer_low_lr.zero_grad() loss.backward() optimizer_low_lr.step() losses_low_lr.append(loss.item())# Visualisation de la perte#plt.plot(losses_low_lr, label="Low Learning Rate (0.0001)")#plt.xlabel("Epoch")#plt.ylabel("Loss")#plt.title("Lente Convergence due à un Taux d'Apprentissage Trop Faible")#plt.legend()#plt.show()
# Taux d'apprentissage optimalmodel_optimal_lr = SimpleNet() # Réinitialisation du modèleoptimizer_optimal_lr = optim.SGD(model_optimal_lr.parameters(), lr=0.01) # Taux d'apprentissage optimal# Entraînement avec un taux d'apprentissage optimallosses_optimal_lr = []for epoch inrange(50): prediction = model_optimal_lr(x) loss = criterion(prediction, y) optimizer_optimal_lr.zero_grad() loss.backward() optimizer_optimal_lr.step() losses_optimal_lr.append(loss.item())# Visualisation de la perte#plt.plot(losses_optimal_lr, label="Optimal Learning Rate (0.01)")#plt.xlabel("Epoch")#plt.ylabel("Loss")#plt.title("Convergence avec un Taux d'Apprentissage Optimal")#plt.legend()#plt.show()
# Comparaison de tous les taux d'apprentissageplt.plot(losses_high_lr, label="High LR (0.1)", linestyle='--')plt.plot(losses_low_lr, label="Low LR (0.0001)", linestyle='--')plt.plot(losses_optimal_lr, label="Optimal LR (0.01)", linestyle='-')plt.xlabel("Epoch")plt.ylabel("Loss")plt.yscale("log")plt.title("Comparaison des Taux d'Apprentissage")plt.legend()plt.show()
4.1.2 Learning schedule
Les schedulers de learning rate ajustent automatiquement le taux d’apprentissage au cours de l’entraînement.
2 grands types:
réduire le learning rate au cours du temps pour converger vers un minimum (exemple: StepLR)
réduire le learning rate, puis le remonter, pour sortir des minimums locaux (exemple: CosineAnnealingWarmRestarts)
import torchimport torch.optim as optimimport torch.nn as nnimport matplotlib.pyplot as plt# Modèle simple et données fictivesmodel = nn.Linear(10, 1)criterion = nn.MSELoss()optimizer = optim.SGD(model.parameters(), lr=0.1)# Paramètres d'entraînementepochs =100train_errors_step = []train_errors_cosine_restart = []learning_rates_step = []learning_rates_cosine_restart = []# Schedulersscheduler_step = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)scheduler_cosine_restart = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)# Entraînement avec StepLRfor epoch inrange(epochs):# Simulation d'une erreur d'entraînement décroissante train_loss =1/ (epoch +1) + torch.rand(1).item() *0.05 train_errors_step.append(train_loss) learning_rates_step.append(optimizer.param_groups[0]["lr"]) scheduler_step.step()# Réinitialisation de l'optimiseur et du scheduler pour CosineAnnealingWarmRestartsoptimizer = optim.SGD(model.parameters(), lr=0.1)scheduler_cosine_restart = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)# Entraînement avec CosineAnnealingWarmRestartsfor epoch inrange(epochs):# Simulation d'une erreur d'entraînement décroissante train_loss =1/ (epoch +1) + torch.rand(1).item() *0.05 train_errors_cosine_restart.append(train_loss) learning_rates_cosine_restart.append(optimizer.param_groups[0]["lr"]) scheduler_cosine_restart.step(epoch) # Utiliser epoch comme paramètre pour le redémarrage# Visualisation des learning rates et erreurs d'entraînementplt.figure(figsize=(14, 5))# Plot des learning ratesplt.subplot(1, 2, 1)plt.plot(range(epochs), learning_rates_step, label='StepLR')plt.plot(range(epochs), learning_rates_cosine_restart, label='CosineAnnealingWarmRestarts')plt.xlabel('Epoch')plt.ylabel('Learning Rate')plt.title('Learning Rate Schedulers')plt.legend()plt.grid(True)# Plot des erreurs d'entraînement#plt.subplot(1, 2, 2)#plt.plot(range(epochs), train_errors_step, label='StepLR')#plt.plot(range(epochs), train_errors_cosine_restart, label='CosineAnnealingWarmRestarts')#plt.xlabel('Epoch')#plt.ylabel('Training Error')#plt.yscale("log")#plt.title('Training Error with Cosine Annealing + Restarts')#plt.legend()#plt.grid(True)plt.tight_layout()plt.show()
/tmp/ipykernel_1634263/4228098559.py:28: UserWarning: Detected call of `lr_scheduler.step()` before `optimizer.step()`. In PyTorch 1.1.0 and later, you should call them in the opposite order: `optimizer.step()` before `lr_scheduler.step()`. Failure to do this will result in PyTorch skipping the first value of the learning rate schedule. See more details at https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate
scheduler_step.step()
4.2 Saddle points
4.2.1 Saddle points
Saddle Point : Point où le gradient est nul mais qui n’est ni un minimum ni un maximum
Minimum Local : Point où le gradient est nul et représente un minimum de la fonction de perte
Problème : La descente de gradient peut s’arrêter ou ralentir aux points selle
import numpy as npimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3D# Génération d'une surface avec un point sellex = np.linspace(-2, 2, 100)y = np.linspace(-2, 2, 100)X, Y = np.meshgrid(x, y)Z = X**2- Y**2# Fonction en forme de selle : z = x^2 - y^2fig = plt.figure()ax = fig.add_subplot(111, projection='3d')ax.plot_surface(X, Y, Z, cmap='coolwarm')ax.set_xlabel('X')ax.set_ylabel('Y')ax.set_zlabel('Z')plt.title("Illustration d'un Point Selle")plt.show()
4.2.2 Solution: changer d’optimizer
Descente de gradient classique : Reste bloquée facilement aux points selle
Adam Optimizer :
Utilise des moments pour adapter les pas de gradient
Aide à échapper aux points selle et à optimiser plus rapidement
import torchimport torch.optim as optimimport numpy as npimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3D# Fonction avec minimum local autour de l'originedef saddle_function(x, y):return x**2- y**2+0.1* (x**4+ y**4)# Générer les données pour la surfacex_vals = np.linspace(-2.5, 2.5, 100)y_vals = np.linspace(-2.5, 2.5, 100)X, Y = np.meshgrid(x_vals, y_vals)Z = X**2- Y**2+0.1* (X**4+ Y**4) # Fonction modifiée# Initialisation pour l'optimisationx = torch.tensor(-1.5, requires_grad=True)y = torch.tensor(-0.0001, requires_grad=True)# Optimizer SGD (descente de gradient classique)optimizer_sgd = optim.SGD([x, y], lr=0.1)trajectory_sgd = []loss = saddle_function(x, y)trajectory_sgd.append((x.item(), y.item(), loss.item()))for _ inrange(50): optimizer_sgd.zero_grad() loss = saddle_function(x, y) loss.backward() optimizer_sgd.step() trajectory_sgd.append((x.item(), y.item(), loss.item()))# Réinitialiser x et y pour Adamx = torch.tensor(-1.5, requires_grad=True)y = torch.tensor(-0.0001, requires_grad=True)# Optimizer Adamoptimizer_adam = optim.Adam([x, y], lr=0.1)trajectory_adam = []loss = saddle_function(x, y)trajectory_adam.append((x.item(), y.item(), loss.item()))for _ inrange(50): optimizer_adam.zero_grad() loss = saddle_function(x, y) loss.backward() optimizer_adam.step() trajectory_adam.append((x.item(), y.item(), loss.item()))# Visualiser les trajectoires avec la surface (3D plot)fig = plt.figure(figsize=(14, 6))# Plot 1: Surface en 3D avec trajectoiresax1 = fig.add_subplot(121, projection='3d')ax1.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.6)sgd_x, sgd_y, sgd_z =zip(*trajectory_sgd)adam_x, adam_y, adam_z =zip(*trajectory_adam)ax1.plot(sgd_x, sgd_y, sgd_z, marker='o', color='blue', label='SGD Trajectory')ax1.plot(adam_x, adam_y, adam_z, marker='o', color='orange', label='Adam Trajectory')ax1.set_title("SGD vs Adam, 50 steps")ax1.set_xlabel('X')ax1.set_ylabel('Y')ax1.set_zlabel('Loss')ax1.legend()# Plot 2: Contour 2D avec couleurs et trajectoires projetéesax2 = fig.add_subplot(122)contour = ax2.contourf(X, Y, Z, levels=50, cmap='coolwarm', alpha=0.7)plt.colorbar(contour, ax=ax2, label='Loss Value')ax2.plot(sgd_x, sgd_y, marker='o', color='blue', label='SGD Trajectory')ax2.plot(adam_x, adam_y, marker='o', color='orange', label='Adam Trajectory')ax2.set_title("SGD vs Adam, 50 steps")ax2.set_xlabel('X')ax2.set_ylabel('Y')ax2.legend()plt.tight_layout()plt.show()
4.3 Overfitting
4.3.1 Early stopping
Arrête l’entraînement quand la performance sur le set de validation cesse de s’améliorer
Empêche le modèle de trop s’ajuster aux données d’entraînement
Critère de patience :
Nombre d’epochs où l’on tolère une stagnation de la performance
import torchimport torch.nn as nnimport torch.optim as optimimport matplotlib.pyplot as pltfrom sklearn.model_selection import train_test_split# Create synthetic datax = torch.linspace(-1, 1, 200).reshape(-1, 1)y = x**3+0.2* torch.sin(5* x) +1* torch.randn(x.size()) # Complex function with noise# Split the dataset into training and test setsx_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)# Define a more complex modelclass ComplexNet(nn.Module):def__init__(self):super(ComplexNet, self).__init__()self.fc1 = nn.Linear(1, 128)self.fc2 = nn.Linear(128, 128)self.fc3 = nn.Linear(128, 128)self.fc4 = nn.Linear(128, 1)def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) x = torch.relu(self.fc3(x))returnself.fc4(x)# Define a function for training the model with or without early stoppingdef train_model_with_early_stopping(model, optimizer, loss_fn, early_stopping=True, patience=10, max_epochs=1000): best_loss =float('inf') patience_counter =0 train_losses = [] test_losses = []for epoch inrange(max_epochs):# Train the model optimizer.zero_grad() y_pred_train = model(x_train) loss_train = loss_fn(y_pred_train, y_train) loss_train.backward() optimizer.step()# Calculate the loss on the test setwith torch.no_grad(): y_pred_test = model(x_test) loss_test = loss_fn(y_pred_test, y_test) train_losses.append(loss_train.item()) test_losses.append(loss_test.item())# Early stopping criterionif early_stopping:if loss_test < best_loss: best_loss = loss_test patience_counter =0# reset the counterelse: patience_counter +=1if patience_counter >= patience:print(f"Early stopping at epoch {epoch +1}")breakreturn train_losses, test_losses# Initialize the modelsmodel_early_stopped = ComplexNet()optimizer_early_stopped = optim.SGD(model_early_stopped.parameters(), lr=0.05)loss_fn = nn.MSELoss()model_no_early_stopping = ComplexNet()optimizer_no_early_stopping = optim.SGD(model_no_early_stopping.parameters(), lr=0.05)# Train both modelstrain_losses_early, test_losses_early = train_model_with_early_stopping(model_early_stopped, optimizer_early_stopped, loss_fn, early_stopping=True)train_losses_no_early, test_losses_no_early = train_model_with_early_stopping(model_no_early_stopping, optimizer_no_early_stopping, loss_fn, early_stopping=False)# Plot the resultsfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))# Training loss plotax1.plot(train_losses_no_early, label='No Early Stopping', linestyle='--')ax1.plot(train_losses_early, label='Early Stopped')ax1.set_xlabel("Epochs")ax1.set_ylabel("Training Loss")ax1.legend()ax1.set_title("Training Loss")# Test loss plotax2.plot(test_losses_no_early, label='No Early Stopping', linestyle='--')ax2.plot(test_losses_early, label='Early Stopped')ax2.set_xlabel("Epochs")ax2.set_ylabel("Test Loss")ax2.legend()ax2.set_title("Test Loss")plt.tight_layout()plt.show()
Early stopping at epoch 30
4.3.2 Regularisation
Ajoute une pénalité à la fonction de coût pour des poids trop élevés
Réduit la complexité du modèle et favorise la généralisation
Dropout : Désactive aléatoirement des neurones pendant l’entraînement
Avantages :
Diminue la variance
Empêche le surajustement aux données d’entraînement
import torchimport torch.nn as nnimport torch.optim as optimimport matplotlib.pyplot as pltfrom sklearn.model_selection import train_test_split# Création d'un jeu de données uniquex = torch.linspace(-1, 1, 20).reshape(-1, 1)y = x**3+0.2* torch.sin(5* x) +1* torch.randn(x.size()) # Complex function with noise# Séparation du jeu de données en ensembles d'entraînement et de testx_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)# Définition d'un modèle plus complexeclass ComplexNet(nn.Module):def__init__(self):super(ComplexNet, self).__init__()self.fc1 = nn.Linear(1, 128)self.fc2 = nn.Linear(128, 128)self.fc3 = nn.Linear(128, 128)self.fc4 = nn.Linear(128, 1)def forward(self, x): x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) x = torch.relu(self.fc3(x))returnself.fc4(x)# Modèle sans régularisationmodel_no_reg = ComplexNet()optimizer_no_reg = optim.SGD(model_no_reg.parameters(), lr=0.02)scheduler_no_reg = optim.lr_scheduler.StepLR(optimizer_no_reg, step_size=200, gamma=0.5)# Modèle avec régularisation L2 (weight decay)model_with_reg = ComplexNet()optimizer_with_reg = optim.SGD(model_with_reg.parameters(), lr=0.02, weight_decay=0.1)scheduler_with_reg = optim.lr_scheduler.StepLR(optimizer_with_reg, step_size=200, gamma=0.5)# Entraînement des deux modèlesepochs =500loss_fn = nn.MSELoss()train_losses_no_reg, test_losses_no_reg = [], []train_losses_with_reg, test_losses_with_reg = [], []for epoch inrange(epochs):# Modèle sans régularisation optimizer_no_reg.zero_grad() y_pred_train = model_no_reg(x_train) loss_train = loss_fn(y_pred_train, y_train) loss_train.backward() optimizer_no_reg.step() scheduler_no_reg.step() train_losses_no_reg.append(loss_train.item())# Calcul de la perte sur le jeu de testwith torch.no_grad(): y_pred_test = model_no_reg(x_test) loss_test = loss_fn(y_pred_test, y_test) test_losses_no_reg.append(loss_test.item())# Modèle avec régularisation optimizer_with_reg.zero_grad() y_pred_train_reg = model_with_reg(x_train) loss_train_reg = loss_fn(y_pred_train_reg, y_train) loss_train_reg.backward() optimizer_with_reg.step() scheduler_with_reg.step() train_losses_with_reg.append(loss_train_reg.item())# Calcul de la perte sur le jeu de test (avec régularisation)with torch.no_grad(): y_pred_test_reg = model_with_reg(x_test) loss_test_reg = loss_fn(y_pred_test_reg, y_test) test_losses_with_reg.append(loss_test_reg.item())# Affichage des courbes de pertefig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))# Perte sur l'ensemble d'entraînementax1.plot(train_losses_no_reg, label='Sans régularisation')ax1.plot(train_losses_with_reg, label='Avec régularisation L2', linestyle='--')ax1.set_xlabel("Epochs")ax1.set_ylabel("Training Loss")ax1.legend()ax1.set_title("Perte sur l'ensemble d'entraînement")# Perte sur l'ensemble de testax2.plot(test_losses_no_reg, label='Sans régularisation')ax2.plot(test_losses_with_reg, label='Avec régularisation L2', linestyle='--')ax2.set_xlabel("Epochs")ax2.set_ylabel("Test Loss")ax2.legend()ax2.set_title("Perte sur l'ensemble de test")plt.tight_layout()plt.show()
5 Généralisation
5.1 Le paradoxe
Théorie classique (VC dimension, Rademacher complexity…) : plus de paramètres que d’exemples \(\Rightarrow\) sur-apprentissage garanti
Les réseaux modernes ont souvent bien plus de paramètres que d’exemples d’entraînement
Pourtant, ils généralisent très bien en pratique
La théorie classique prédit l’échec ; l’expérience dit le contraire
5.2 Zhang et al. (ICLR 2017)
“Understanding deep learning requires rethinking generalization”
Expérience : entraîner un CNN avec des labels aléatoires (aucune structure, pur bruit)
Résultat : le réseau atteint \(0\%\) d’erreur d’entraînement — il mémorise parfaitement le bruit
Le même réseau, mêmes hyperparamètres, généralise bien sur de vraies données
Conclusion : la capacité du réseau suffit à tout mémoriser ; les mesures classiques de capacité ne peuvent donc pas, à elles seules, expliquer pourquoi il généralise sur de vraies données
5.3 Double descent
Intuition classique : plus de capacité \(\Rightarrow\) d’abord ça aide, puis ça nuit (courbe en U, biais-variance)
Observation empirique : au-delà du point d’interpolation (le modèle peut ajuster parfaitement l’entraînement), l’erreur de test rebaisse
Deux régimes séparés par un pic d’erreur : “classique” (sous-paramétré) puis “moderne” (sur-paramétré)
Observé sur de nombreuses architectures, y compris de simples modèles linéaires à features aléatoires
import numpy as npimport matplotlib.pyplot as pltrng = np.random.default_rng(0)n_train, n_test, d =40, 200, 20X_train = rng.normal(size=(n_train, d))X_test = rng.normal(size=(n_test, d))beta_true = rng.normal(size=d)noise_std =0.5y_train = X_train @ beta_true + noise_std * rng.normal(size=n_train)y_test = X_test @ beta_true + noise_std * rng.normal(size=n_test)max_p =500W = rng.normal(size=(d, max_p)) / np.sqrt(d)def features(X, p):return np.cos(X @ W[:, :p]) # features aléatoiresp_list = [1, 2, 5, 10, 20, 30, 35, 38, 39, 40, 41, 42, 45, 50, 60, 80, 120, 200, 300, 500]train_errors, test_errors = [], []for p in p_list: Ztr, Zte = features(X_train, p), features(X_test, p) beta_hat, *_ = np.linalg.lstsq(Ztr, y_train, rcond=None) # solution de norme minimale si p > n_train train_errors.append(np.mean((Ztr @ beta_hat - y_train) **2)) test_errors.append(np.mean((Zte @ beta_hat - y_test) **2))plt.figure(figsize=(8, 4))plt.plot(p_list, train_errors, marker="o", label="Erreur d'entraînement")plt.plot(p_list, test_errors, marker="o", label="Erreur de test")plt.axvline(n_train, color="grey", linestyle="--", label="Seuil d'interpolation ($p=n$)")plt.xscale("log")plt.yscale("log")plt.xlabel("Nombre de features (capacité du modèle)")plt.ylabel("Erreur quadratique moyenne")plt.title("Double descent : régression sur features aléatoires")plt.legend()plt.grid(True)plt.show()
5.4 Constat
Pic d’erreur de test exactement au point d’interpolation (\(p \approx n\))
Passé ce point, ajouter encore plus de paramètres… aide à nouveau
Exactement le phénomène de double descent
5.5 Soyons honnêtes
Il n’existe pas aujourd’hui de théorie complète expliquant pourquoi les réseaux sur-paramétrés généralisent
Plusieurs pistes (marge, platitude des minima, biais implicite de SGD, NTK…) mais aucune ne fait consensus
Ce que l’expérience montre est, pour l’instant, en avance sur ce que la théorie explique