Entraîner un modèle PyTorch : Loss, optimizer et training loop
Maintenant que vous savez construire des réseaux de neurones, il est temps de les entraîner ! L’entraînement consiste à ajuster les poids du réseau pour minimiser une fonction de perte (loss) sur vos données. Dans cet article, nous allons couvrir les loss functions, les optimizers, et implémenter une boucle d’entraînement complète.
Vue d’ensemble du processus d’entraînement
L’entraînement d’un réseau de neurones suit ce cycle :
- Forward pass : Passer les données dans le modèle → obtenir prédictions
- Calculer la loss : Comparer prédictions vs vraies valeurs
- Backward pass : Calculer les gradients (backpropagation)
- Update des poids : Ajuster les paramètres avec l’optimizer
- Répéter pour toutes les données (epoch)

Loss functions : Mesurer l’erreur
La loss function (fonction de perte) mesure à quel point les prédictions du modèle s’écartent des vraies valeurs. L’objectif de l’entraînement est de minimiser cette loss.
Classification multi-classe : CrossEntropyLoss
La loss la plus utilisée pour classification multi-classe (MNIST, ImageNet, etc.) :
import torch
import torch.nn as nn
# CrossEntropyLoss = LogSoftmax + NLLLoss
criterion = nn.CrossEntropyLoss()
# Prédictions du modèle (logits)
outputs = torch.tensor([[2.0, 1.0, 0.1], # Prédiction classe 0
[0.5, 2.5, 0.2]]) # Prédiction classe 1
# Vraies classes (indices)
targets = torch.tensor([0, 1])
# Calculer la loss
loss = criterion(outputs, targets)
print(f"CrossEntropyLoss : {loss.item():.4f}") # 0.4170
# Note : pas besoin d'appliquer softmax avant !
# CrossEntropyLoss le fait automatiquement
LogSoftmax + NLLLoss en une seule opération optimisée et numériquement stable. Important : Les outputs doivent être des logits (scores bruts), pas des probabilités ! N’appliquez pas softmax avant.Avec poids de classes (classes déséquilibrées)
# Dataset déséquilibré : classe 0 = 90%, classe 1 = 10%
# Donner plus d'importance à la classe minoritaire
class_weights = torch.tensor([1.0, 9.0]) # Classe 1 pèse 9x plus
criterion = nn.CrossEntropyLoss(weight=class_weights)
outputs = torch.randn(32, 2) # 32 exemples, 2 classes
targets = torch.randint(0, 2, (32,))
loss = criterion(outputs, targets)
print(f"Weighted loss : {loss.item():.4f}")
Classification binaire : BCEWithLogitsLoss
Pour classification binaire (spam/pas spam, chien/chat) :
# BCEWithLogitsLoss = Sigmoid + BCELoss
criterion = nn.BCEWithLogitsLoss()
# Prédictions (logits)
outputs = torch.tensor([[2.0], [-1.0], [0.5]]) # 3 exemples
# Vraies valeurs (0 ou 1)
targets = torch.tensor([[1.0], [0.0], [1.0]])
loss = criterion(outputs, targets)
print(f"BCEWithLogitsLoss : {loss.item():.4f}")
# Alternative si vous avez déjà des probabilités (après sigmoid)
outputs_probs = torch.sigmoid(outputs)
criterion_bce = nn.BCELoss()
loss_bce = criterion_bce(outputs_probs, targets)
Préférez BCEWithLogitsLoss à BCELoss car elle combine sigmoid + BCE en une seule opération, ce qui est plus stable numériquement et évite des problèmes de gradient.
Régression : MSELoss et L1Loss
Pour prédire des valeurs continues (prix, température, âge) :
# Mean Squared Error (L2)
mse_criterion = nn.MSELoss()
predictions = torch.tensor([2.5, 0.0, 2.1, 7.8])
targets = torch.tensor([3.0, -0.5, 2.0, 8.0])
mse_loss = mse_criterion(predictions, targets)
print(f"MSE Loss : {mse_loss.item():.4f}") # 0.0650
# Mean Absolute Error (L1) - plus robuste aux outliers
l1_criterion = nn.L1Loss()
l1_loss = l1_criterion(predictions, targets)
print(f"L1 Loss : {l1_loss.item():.4f}") # 0.2500
# Smooth L1 Loss (Huber) - compromis entre MSE et L1
smooth_l1 = nn.SmoothL1Loss()
smooth_loss = smooth_l1(predictions, targets)
print(f"Smooth L1 Loss : {smooth_loss.item():.4f}")
Autres loss functions utiles
# KL Divergence (pour distributions de probabilités)
kl_div = nn.KLDivLoss()
# Cosine Embedding Loss (pour similarity learning)
cosine_loss = nn.CosineEmbeddingLoss()
# Triplet Loss (pour face recognition, retrieval)
triplet_loss = nn.TripletMarginLoss()
# CTC Loss (pour speech recognition, OCR)
ctc_loss = nn.CTCLoss()
Loss personnalisée
Vous pouvez créer vos propres loss functions :
def custom_loss(predictions, targets):
"""
Custom loss : MSE + régularisation L1
"""
mse = ((predictions - targets) ** 2).mean()
l1_reg = predictions.abs().mean()
return mse + 0.01 * l1_reg
# Utilisation
predictions = torch.randn(32, 10)
targets = torch.randn(32, 10)
loss = custom_loss(predictions, targets)
print(f"Custom loss : {loss.item():.4f}")
Optimizers : Ajuster les poids
L’optimizer utilise les gradients calculés par backpropagation pour mettre à jour les poids du modèle.
SGD (Stochastic Gradient Descent)
L’optimizer le plus basique : w = w - lr * gradient
import torch.optim as optim
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10)
)
# SGD basique
optimizer = optim.SGD(model.parameters(), lr=0.01)
# SGD avec momentum (accélère la convergence)
optimizer = optim.SGD(
model.parameters(),
lr=0.01,
momentum=0.9, # Momentum (0.9 est standard)
weight_decay=1e-4 # L2 regularization
)
Adam (Adaptive Moment Estimation)
Le plus populaire : adapte le learning rate par paramètre
# Adam : combine momentum + RMSprop
optimizer = optim.Adam(
model.parameters(),
lr=1e-3, # Learning rate (0.001 par défaut)
betas=(0.9, 0.999), # Momentum coefficients
eps=1e-8, # Stabilité numérique
weight_decay=0 # L2 regularization (0 = pas de reg)
)
AdamW (Adam avec Weight Decay Découplé)
Version améliorée d’Adam, recommandée pour Transformers :
# AdamW : weight decay découplé (meilleure régularisation)
optimizer = optim.AdamW(
model.parameters(),
lr=1e-3,
betas=(0.9, 0.999),
eps=1e-8,
weight_decay=0.01 # Weight decay séparé du gradient
)
Quel optimizer choisir ? Adam/AdamW : Défaut pour la plupart des cas (CNN, Transformer, MLP). Convergence rapide, peu de tuning. SGD avec momentum : Peut donner de meilleurs résultats finaux sur CNN (ImageNet) mais nécessite plus de tuning. AdamW : Préféré pour Transformers/NLP. RMSprop : Bon pour RNN.
Autres Optimizers
# RMSprop (pour RNNs)
optimizer = optim.RMSprop(model.parameters(), lr=0.01, alpha=0.99)
# Adagrad (learning rate décroissant)
optimizer = optim.Adagrad(model.parameters(), lr=0.01)
# AdaGrad (variante améliorée)
optimizer = optim.Adadelta(model.parameters(), lr=1.0)
Learning Rate par groupe de paramètres
Différents learning rates pour différentes parties du modèle :
# Exemple : fine-tuning avec LR différencié
optimizer = optim.Adam([
{'params': model[0].parameters(), 'lr': 1e-5}, # Première couche : LR faible
{'params': model[2].parameters(), 'lr': 1e-3} # Dernière couche : LR élevé
])
La boucle d’entraînement (Training Loop)
Maintenant, assemblons tout dans une boucle d’entraînement complète !
Training loop basique
import torch
import torch.nn as nn
import torch.optim as optim
# 1. Préparer le modèle, loss, optimizer
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 10)
)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# 2. Données d'entraînement (exemple simplifié)
train_data = torch.randn(1000, 784) # 1000 exemples
train_labels = torch.randint(0, 10, (1000,))
# 3. Training loop
num_epochs = 10
batch_size = 32
model.train() # Mode entraînement
for epoch in range(num_epochs):
epoch_loss = 0.0
num_batches = len(train_data) // batch_size
for i in range(num_batches):
# Mini-batch
start_idx = i * batch_size
end_idx = start_idx + batch_size
inputs = train_data[start_idx:end_idx]
targets = train_labels[start_idx:end_idx]
# Forward pass
outputs = model(inputs)
loss = criterion(outputs, targets)
# Backward pass
optimizer.zero_grad() # Reset gradients
loss.backward() # Calculer gradients
optimizer.step() # Update poids
epoch_loss += loss.item()
# Stats epoch
avg_loss = epoch_loss / num_batches
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")
Training loop avec DataLoader
Version réaliste avec PyTorch DataLoader :
from torch.utils.data import TensorDataset, DataLoader
# Créer un Dataset
dataset = TensorDataset(train_data, train_labels)
# DataLoader (gère batching, shuffling, parallélisation)
train_loader = DataLoader(
dataset,
batch_size=32,
shuffle=True, # Mélanger les données
num_workers=2 # Paralléliser le chargement
)
# Training loop
def train_one_epoch(model, dataloader, criterion, optimizer, device):
model.train()
total_loss = 0.0
for batch_idx, (data, targets) in enumerate(dataloader):
# Déplacer sur GPU si disponible
data, targets = data.to(device), targets.to(device)
# Forward pass
outputs = model(data)
loss = criterion(outputs, targets)
# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(dataloader)
# Utilisation
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
for epoch in range(num_epochs):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
print(f"Epoch {epoch+1}, Train Loss: {train_loss:.4f}")
Validation et métriques
Toujours valider sur un ensemble de validation séparé !
def validate(model, dataloader, criterion, device):
model.eval() # Mode évaluation
total_loss = 0.0
correct = 0
total = 0
with torch.no_grad(): # Désactiver autograd
for data, targets in dataloader:
data, targets = data.to(device), targets.to(device)
# Forward pass
outputs = model(data)
loss = criterion(outputs, targets)
total_loss += loss.item()
# Accuracy
_, predicted = outputs.max(1)
correct += predicted.eq(targets).sum().item()
total += targets.size(0)
avg_loss = total_loss / len(dataloader)
accuracy = 100.0 * correct / total
return avg_loss, accuracy
# Utilisation
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
for epoch in range(num_epochs):
# Training
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
# Validation
val_loss, val_acc = validate(model, val_loader, criterion, device)
print(f"Epoch {epoch+1}/{num_epochs}")
print(f" Train Loss: {train_loss:.4f}")
print(f" Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
Toujours valider ! Ne jamais évaluer sur les données d’entraînement. Split vos données : 70-80% train, 10-15% validation, 10-15% test. La validation permet de détecter l’overfitting et de tuner les hyperparamètres.
Learning Rate Scheduling
Réduire le learning rate pendant l’entraînement améliore souvent les performances finales.
StepLR : Réduire tous les N epochs
from torch.optim.lr_scheduler import StepLR
optimizer = optim.Adam(model.parameters(), lr=0.1)
# Réduire LR par 0.1 tous les 10 epochs
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
for epoch in range(100):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
scheduler.step() # Update learning rate
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch+1}, LR: {current_lr:.6f}, Loss: {train_loss:.4f}")
CosineAnnealingLR : Décroissance cosinus
from torch.optim.lr_scheduler import CosineAnnealingLR
optimizer = optim.Adam(model.parameters(), lr=0.1)
scheduler = CosineAnnealingLR(optimizer, T_max=100) # 100 epochs
for epoch in range(100):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
scheduler.step()
print(f"Epoch {epoch+1}, LR: {optimizer.param_groups[0]['lr']:.6f}")
ReduceLROnPlateau : Réduire si plateau
from torch.optim.lr_scheduler import ReduceLROnPlateau
optimizer = optim.Adam(model.parameters(), lr=0.01)
# Réduire LR si validation loss ne s'améliore pas pendant 5 epochs
scheduler = ReduceLROnPlateau(
optimizer,
mode='min', # Minimiser la metric
factor=0.5, # Réduire par 0.5
patience=5, # Attendre 5 epochs
verbose=True
)
for epoch in range(100):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
val_loss, val_acc = validate(model, val_loader, criterion, device)
scheduler.step(val_loss) # Basé sur validation loss
print(f"Epoch {epoch+1}, Val Loss: {val_loss:.4f}")
OneCycleLR : Super-convergence
from torch.optim.lr_scheduler import OneCycleLR
optimizer = optim.Adam(model.parameters(), lr=0.01)
# OneCycleLR : augmente puis diminue le LR en 1 cycle
scheduler = OneCycleLR(
optimizer,
max_lr=0.1, # LR maximum
steps_per_epoch=len(train_loader),
epochs=num_epochs
)
for epoch in range(num_epochs):
for batch_idx, (data, targets) in enumerate(train_loader):
# ... training step ...
scheduler.step() # Appeler à chaque batch !
Scheduler recommandé : CosineAnnealingLR est un excellent choix par défaut (utilisé dans ResNet, Vision Transformers). ReduceLROnPlateau si vous voulez adapter automatiquement. OneCycleLR pour convergence ultra-rapide (nécessite tuning).
Sauvegarder et charger le modèle
Sauvegardez vos modèles entraînés pour les réutiliser !
Sauvegarder le Modèle Complet
# Sauvegarder tous les états
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
'train_loss': train_loss,
'val_loss': val_loss,
'val_acc': val_acc
}, 'checkpoint.pth')
print("Modèle sauvegardé !")
Charger le modèle
# Créer le modèle (même architecture)
model = SimpleNet(input_size=784, hidden_size=128, output_size=10)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
# Charger les états
checkpoint = torch.load('checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
epoch = checkpoint['epoch']
print(f"Modèle chargé depuis epoch {epoch}")
# Reprendre l'entraînement
model.train()
# ... continuer training ...
Sauvegarder seulement les poids (inférence)
# Pour inférence seulement
torch.save(model.state_dict(), 'model_weights.pth')
# Charger
model = SimpleNet(784, 128, 10)
model.load_state_dict(torch.load('model_weights.pth'))
model.eval()
Early Stopping
Arrêter l’entraînement si la validation ne s’améliore plus :
class EarlyStopping:
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience
self.min_delta = min_delta
self.counter = 0
self.best_loss = None
self.early_stop = False
def __call__(self, val_loss):
if self.best_loss is None:
self.best_loss = val_loss
elif val_loss > self.best_loss - self.min_delta:
self.counter += 1
if self.counter >= self.patience:
self.early_stop = True
else:
self.best_loss = val_loss
self.counter = 0
# Utilisation
early_stopping = EarlyStopping(patience=10)
for epoch in range(100):
train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
val_loss, val_acc = validate(model, val_loader, criterion, device)
early_stopping(val_loss)
if early_stopping.early_stop:
print(f"Early stopping à epoch {epoch+1}")
break
print(f"Epoch {epoch+1}, Val Loss: {val_loss:.4f}")
Boucle d’entraînement complète
Voici un exemple complet intégrant tout :
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
from torch.utils.data import DataLoader
def train_model(model, train_loader, val_loader, num_epochs=50, device='cuda'):
"""
Entraîne un modèle PyTorch de façon complète
"""
# Setup
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs)
# Tracking
best_val_acc = 0.0
history = {'train_loss': [], 'val_loss': [], 'val_acc': []}
model = model.to(device)
for epoch in range(num_epochs):
# Training
model.train()
train_loss = 0.0
for data, targets in train_loader:
data, targets = data.to(device), targets.to(device)
outputs = model(data)
loss = criterion(outputs, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
train_loss /= len(train_loader)
# Validation
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for data, targets in val_loader:
data, targets = data.to(device), targets.to(device)
outputs = model(data)
loss = criterion(outputs, targets)
val_loss += loss.item()
_, predicted = outputs.max(1)
correct += predicted.eq(targets).sum().item()
total += targets.size(0)
val_loss /= len(val_loader)
val_acc = 100.0 * correct / total
# Learning rate scheduling
scheduler.step()
# Save best model
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'val_acc': val_acc
}, 'best_model.pth')
# History
history['train_loss'].append(train_loss)
history['val_loss'].append(val_loss)
history['val_acc'].append(val_acc)
# Print progress
print(f"Epoch [{epoch+1}/{num_epochs}]")
print(f" Train Loss: {train_loss:.4f}")
print(f" Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")
print(f" LR: {optimizer.param_groups[0]['lr']:.6f}")
print(f" Best Val Acc: {best_val_acc:.2f}%")
print()
return history
# Utilisation
# model = YourModel()
# history = train_model(model, train_loader, val_loader, num_epochs=50)
Visualiser l’entraînement
import matplotlib.pyplot as plt
def plot_training_history(history):
"""
Visualise les courbes d'entraînement
"""
epochs = range(1, len(history['train_loss']) + 1)
plt.figure(figsize=(12, 4))
# Loss
plt.subplot(1, 2, 1)
plt.plot(epochs, history['train_loss'], 'b-', label='Train Loss')
plt.plot(epochs, history['val_loss'], 'r-', label='Val Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.grid(True)
# Accuracy
plt.subplot(1, 2, 2)
plt.plot(epochs, history['val_acc'], 'g-', label='Val Accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Validation Accuracy')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('training_history.png', dpi=150)
plt.show()
# Utilisation
# plot_training_history(history)
Exercices pratiques
Training loop pour MNIST
Complétez cette boucle d’entraînement pour MNIST :
# Télécharger MNIST (nécessite torchvision)
# pip install torchvision
from torchvision import datasets, transforms
# Charger MNIST
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
val_dataset = datasets.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)
# Créer un modèle simple
model = nn.Sequential(
nn.Flatten(),
nn.Linear(28*28, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10)
)
# Entraîner pendant 10 epochs
# Objectif : atteindre >97% d'accuracy
Comparer optimizers
Comparez SGD, Adam et AdamW sur le même modèle/données. Quel optimizer converge le plus vite ? Lequel atteint la meilleure accuracy finale ?
Implémenter gradient clipping
Ajoutez du gradient clipping pour éviter les gradient explosions :
# À ajouter après loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
Conclusion
Dans cet article, vous avez maîtrisé l’entraînement de modèles PyTorch :
- ✅ Loss functions : CrossEntropy, BCE, MSE, et customs
- ✅ Optimizers : SGD, Adam, AdamW
- ✅ Training loop complète avec forward/backward/update
- ✅ Validation et calcul de métriques (accuracy)
- ✅ Learning rate scheduling (StepLR, Cosine, ReduceLROnPlateau)
- ✅ Sauvegarder et charger des modèles
- ✅ Early stopping et best model tracking
Prochaine étape : Dans le prochain article, nous allons apprendre à gérer les données efficacement avec Datasets et DataLoaders. Vous découvrirez comment charger des images, appliquer des transformations, et faire de la data augmentation.
Pour aller plus loin :
- Explorez la documentation sur les optimizers
- Lisez notre guide sur le transfer learning pour réutiliser des modèles pré-entraînés
- Découvrez les techniques d’optimisation avancée (mixed precision, distributed training)
Exercice complet : Entraînez un classifieur MNIST de A à Z. Expérimentez avec différents learning rates (1e-4, 1e-3, 1e-2), optimizers (SGD, Adam), et schedulers. Visualisez les courbes de loss/accuracy. Sauvegardez le meilleur modèle et évaluez-le sur le test set. Objectif : >98% d’accuracy !