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