Réseau
Nous allons voir comment mettre en place un réseau dans la librairie Pytorch.
Modèles
Sequential
Le modèle Sequential de PyTorch permet de définir un réseau en empilant les couches les unes à la suite des autres. Les traitements sont effectués de manière séquentielle : la sortie d’une couche correspond à l’entrée de la suivante. Voici un exemple :
nbClasses = ...
model = nn.Sequential(
nn.Flatten(), # (N, 1, L, H) → (N, LxH)
nn.Linear(28*28, 128), # 1ère couche FC
nn.ReLU(),
nn.Linear(128, nbClasses) # 2ème couche FC (sortie)
)
Ce réseau accepte des images 1x28x28 en entrée, les transforme en 1 vecteur 1D de 28x28 valeurs, applique une première couche FC, puis une ReLU() et une deuxième couche FC pour obtenir des valeurs correspondant aux scores associés aux différentes classes.
Custom
Le modèle Custom offre un contrôle total sur les traitements effectués par le réseau. Si cela semble un poil plus complexe à utiliser : création d’une classe, mise en place de différentes fonctions, cette approche permet de :
Concevoir des architectures complexes : chemins multiples, conditions…
Regrouper les couches par thématique : features / classifier
Contrôler finement le réseau : gel de couches, têtes multiples…
Positionner un breakpoint, pour débogguer votre chaîne de traitements
Convertissons l’exemple précédent en custom model :
import torch
import torch.nn as nn
class CustomModel(nn.Module):
def __init__(self, nbClasses):
super().__init__()
self.flatten = nn.Flatten()
self.fc1 = nn.Linear(28*28, 128)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(128, nbClasses)
def forward(self, x):
x = self.flatten(x)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
model = CustomModel(10)
Nous remarquons deux choses :
Le réseau est construit en dérivant la classe nn.Module
La passe Forward est gérée par la fonction forward(x)
Quelles sont les facilités apportées par l’héritage de nn.Module ?
Gestion des couches : En écrivant self.fc1 = …, la couche fc1 est automatiquement associée au modèle. Cela est pratique car, par exemple, lorsque l’on transfère le modèle vers un GPU, toutes les couches répertoriées sont automatiquement déplacées.
model(x) : Lorsque l’on écrit y = model(x), cet appel est redirigé vers model.forward(x), PyTorch se charge ensuite de gérer l’autograd
Avertissement
L’écriture de self.mylayer = … est indispensable. Si vous écrivez seulement mylayer = …, la couche n’est pas enregistrée dans le réseau et ses paramètres ne seront ni entraînés, ni conservés, ce qui conduit à des comportements erratiques.
Test
Prenons un exemple en utilisant une image de MNIST contenant 10 classes :
from torchvision import datasets, transforms
dataset = datasets.MNIST(root="data", train=True, download=True, transform=transforms.ToTensor() )
image, label = dataset[0]
print(image.shape)
>> torch.Size([1, 28, 28])
image = image.unsqueeze(0) # (1, 28, 28) => (1, 1, 28, 28) -> batch of 1 image 1x28x28
logits = model(image)
print(logits.shape)
>> torch.Size([1, 10])
Quelques remarques :
Nous extrayons une image de la base MNIST de dimension [1,28,28]
Le réseau accepte un batch de N images, soit un tenseur de la forme [N,nbChannels,L,H]
Pour traiter une seule image, il faut donc ajouter une dimension avec la fonction unsqueeze pour obtenir un tenseur de taille [1,1,28,28]
Le logit correspond à la sortie brute du réseau (avant application d’un softmax), soit un vecteur correspondant aux 10 classes
Comme nous traitons un batch contenant 1 image, nous récupérons un logit de taille [1,10]
Modes
Tout réseau, custom ou sequential dispose des fonctionnalités suivantes :
model.train() : bascule le réseau en mode entraînement
model.eval() : bascule le réseau en mode évaluation
En mode entraînement, certaines couches comme Dropout ou BatchNorm adoptent un comportement spécifique nécessaire à l’apprentissage. A contrario, en mode évaluation, ces mêmes couches sont désactivées ou figées afin de garantir des prédictions stables et cohérentes. Cette distinction est essentielle pour obtenir des performances fiables lors de la phase de test ou d’inférence.
Poids
Les poids contenus dans chaque couche du modèle peuvent être récupérés à l’aide de la méthode model.parameters(). On peut par exemple, utiliser cette fonction pour transmettre l’ensemble des poids à un optimiseur afin qu’il les mettent à jour lors de la backpropagation.
Sauvegarde du meilleur modèle
Fonctions
Les fonctions model.state_dict() et model.load_state_dict(…) permettent respectivement d’extraire et de charger l’ensemble des paramètres du réseau. La méthode state_dict() retourne un dictionnaire contenant tous les poids et biais des différentes couches du modèle, identifiés par leur nom. Ce dictionnaire peut être enregistré sur le disque afin de conserver l’état du réseau après l’entraînement. À l’inverse, load_state_dict(…) permet de restaurer ces paramètres dans un modèle ayant la même architecture, ce qui rend possible la reprise d’un entraînement ou l’utilisation d’un modèle pré-entraîné pour l’inférence.
Mise en place
Lorsqu’un meilleur modèle est trouvé, par exemple lorsque l’on bat la meilleure Loss trouvée, il est utile de faire un snapshot de l’apprentissage actuel. Pour cela, il faut sauvegarder tous les éléments qui évoluent d’une epoch à l’autre :
Le numéro de l’epoch !
Les poids du réseau
La meilleure Loss connue, histoire de ne pas écraser le fichier de sauvegarde au redémarage suivant
Les paramètres internes de l’optimiseur
Voici un code exemple :
for epoch in range(nb_epochs):
model.train()
...
if val_loss < best_loss:
best_loss = val_loss
torch.save({
"epoch": epoch,
"best_loss": best_loss,
"model_state": model.state_dict(),
"optim_state": optimizer.state_dict(),
}, "checkpoint_best.pth")
Lors du lancement du script d’apprentissage, on vérifie la présence d’un fichier de sauvegarde. S’il est trouvé, on recharge les différentes informations :
if os.path.exists("checkpoint_best.pth"):
info = torch.load("checkpoint_best.pth")
model.load_state_dict(info["model_state"])
optimizer.load_state_dict(info["optim_state"])
best_loss = info["best_loss"]
start_epoch = info["epoch"] + 1
Chargement d’un modèle officiel
Principe
PyTorch met à disposition, via torchvision, un ensemble de modèles standards déjà entraînés sur de grands jeux de données (principalement ImageNet). Ces modèles servent de base solide pour l’inférence ou le transfer learning. Parmi les plus connus, on trouve : ResNet, VGG, AlexNet, DenseNet, MobileNet.
Chargement
Le chargement se fait en une ligne :
from torchvision import models
model = models.resnet18(weights="DEFAULT")
L’architecture est créée automatiquement
Les poids pré-entraînés sont téléchargés puis chargés
Le modèle est prêt à l’emploi
Si vous faîtes uniquement de l’inférence, il faut exécuter : model.eval() pour désactiver les couches de Dropout/BatchNorm toujours présentes dans la sauvegarde.
Reset
Si par exemple, on veut faire un entraînement from scratch, il est possible de charger un modèle existant et de réinitialiser ses poids pour conserver uniquement l’architecture :
for module in model.modules():
if hasattr(module, "reset_parameters"):
module.reset_parameters()
Pour cela :
On parcourt la liste des couches existantes
Pour chaque couche, on vérifie si elle dispose d’une fonction interne reset_parameters, par exemple, Relu n’en a pas !
Si c’est le cas, on appelle alors cette fonction