PyTorch Batch Normalization et Dropout

La normalisation par lots et le Dropout sont deux des techniques de régularisation et de stabilisation les plus fondamentales dans l'entraînement des réseaux de neurones profonds. La première résout le problème du décalage des covariables internes pendant l'entraînement, rendant les réseaux plus profonds entraînables ; la seconde évite le surapprentissage en supprimant aléatoirement des neurones, ce qui améliore la généralisation du modèle. Les deux sont généralement utilisées ensemble et constituent des composants standard des réseaux de neurones modernes.


1. Normalisation par lots

1.1 Principes de base

Pendant l'entraînement des réseaux profonds, de petits changements dans les paramètres de la couche précédente sont continuellement amplifiés à mesure que les couches s'approfondissent, ce qui fait que la distribution d'entrée des couches suivantes ne cesse de changer — ce phénomène est appelédécalage des covariables internes. La normalisation par lots résout ce problème en standardisant la sortie de chaque couche, ce qui ramène de force les valeurs d'activation vers une distribution stable.

Formule de normalisation :

\[\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}\]

\[y_i = \gamma \hat{x}_i + \beta\]

où :

  • \(\mu_B\), \(\sigma_B^2\) sont la moyenne et la variance du lot courant
  • \(\epsilon\) est une petite constante pour éviter la division par zéro (par défaut1e-5)
  • \(\gamma\) et \(\beta\) sont des paramètres d'échelle et de décalage apprenables qui permettent au réseau de décider par lui-même la forme finale de la distribution

Avantages de la normalisation par lots :

  • Permet des taux d'apprentissage plus élevés, accélérant l'entraînement
  • Réduit la sensibilité à l'initialisation, rendant l'entraînement plus stable
  • A un certain effet de régularisation, réduisant la dépendance à Dropout
  • Atténue la disparition/l'explosion des gradients, rendant les réseaux plus profonds entraînables

1.2 BatchNorm1d / 2d / 3d

PyTorch fournit trois versions selon les dimensions d'entrée, avec une utilisation identique, ne différant que par la forme des données qu'elles traitent.

BatchNorm1d : pour les couches entièrement connectées / données de séquence

Exemple

import torch
import torch.nn as nn

# Forme d'entrée : (N, C) ou (N, C, L)
# N=batch_size, C=nombre de caractéristiques/canaux, L=longueur de séquence
bn1d = nn.BatchNorm1d(
    num_features=128,    # nombre de caractéristiques/canaux
    eps=1e-5,            # petite constante pour éviter la division par zéro (par défaut 1e-5)
    momentum=0.1,        # momentum pour la moyenne mobile (par défaut 0.1)
    affine=True,         # s'il faut apprendre gamma et beta (par défaut True)
    track_running_stats=True,  # s'il faut suivre la moyenne/variance courante (par défaut True)
)

# utilisé après les couches entièrement connectées
x = torch.randn(32, 128)     # (batch=32, features=128)
out = bn1d(x)
print(out.shape)              # torch.Size([32, 128])

# données de séquence (par ex., après convolution 1D)
x_seq = torch.randn(32, 128, 50)   # (batch, channels, seq_len)
out_seq = bn1d(x_seq)
print(out_seq.shape)               # torch.Size([32, 128, 50])

BatchNorm2d: pour les cartes de caractéristiques d'images CNN

Exemple

# Forme d'entrée : (N, C, H, W)
# normaliser indépendamment pour chaque canal C
bn2d = nn.BatchNorm2d(num_features=64)  # 64 canaux

x = torch.randn(32, 64, 28, 28)    # (lot, canaux, hauteur, largeur)
out = bn2d(x)
print(out.shape)                    # torch.Size([32, 64, 28, 28])

BatchNorm3d : pour convolution 3D (vidéo / imagerie médicale)

Exemple

# Forme d'entrée : (N, C, D, H, W)
bn3d = nn.BatchNorm3d(num_features=32)

x = torch.randn(4, 32, 16, 32, 32)    # (lot, canaux, profondeur, h, w)
out = bn3d(x)
print(out.shape)                        # torch.Size([4, 32, 16, 32, 32])

Afficher les paramètres apprenables

Exemple

bn = nn.BatchNorm2d(64)

print(f"forme du poids (gamma) : {bn.weight.shape}")   # torch.Size([64])
print(f"forme du biais (beta) : {bn.bias.shape}")     # torch.Size([64])
print(f"forme de running_mean : {bn.running_mean.shape}")  # torch.Size([64])
print(f"forme de running_var : {bn.running_var.shape}")   # torch.Size([64])

# running_mean / running_var ne sont pas des paramètres entraînables
# Ce sont des statistiques de moyenne mobile utilisées lors de l'inférence
print(f"nombre de paramètres entraînables : {sum(p.numel() for p in bn.parameters())}")  # 128(64×2)

Signification du paramètre momentum

Exemple

# Le momentum de PyTorch est l'opposé de la compréhension habituelle :
# running_mean = (1 - momentum) × running_mean + momentum × batch_mean
# c.-à-d. que plus le momentum est grand, plus le poids des statistiques du lot courant est important

# valeur par défaut 0.1 : généralement approprié
# pour une petite taille de lot, envisagez de le réduire : momentum=0.01, afin d'éviter des statistiques instables
bn = nn.BatchNorm2d(64, momentum=0.01)

# momentum=None : utilise une moyenne mobile cumulée (CMA), adapté aux très petits lots
bn = nn.BatchNorm2d(64, momentum=None)

1.3 LayerNorm Normalisation de couche

PaireNormaliser toutes les caractéristiques d'un seul échantillonIl ne dépend pas du lot, adapté aux scénarios avec des tailles de lot petites ou variables.La méthode de normalisation standard pour les Transformers。

BatchNorm:在 batch 维度计算均值/方差(跨样本,同通道)
LayerNorm:在特征维度计算均值/方差(同样本,跨特征)

Exemple

import torch
import torch.nn as nn

# ── Pour les couches entièrement connectées / Transformer ────────────────────────
# normalized_shape : normaliser sur les dernières dimensions
ln = nn.LayerNorm(normalized_shape=512)

x = torch.randn(32, 10, 512)   # (batch, seq_len, embed_dim)
out = ln(x)
print(out.shape)                # torch.Size([32, 10, 512])

# ── Normaliser sur plusieurs dimensions ───────────────────────────────
ln_2d = nn.LayerNorm([64, 28, 28])   # normaliser sur les trois dimensions C, H, W
x = torch.randn(8, 64, 28, 28)
out = ln_2d(x)

# ── Utilisation typique dans un Transformer ──────────────────────
class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, num_heads, ff_dim):
        super().__init__()
        self.attn  = nn.MultiheadAttention(embed_dim, num_heads, batch_first=True)
        self.ff    = nn.Sequential(
            nn.Linear(embed_dim, ff_dim),
            nn.GELU(),
            nn.Linear(ff_dim, embed_dim),
        )
        self.norm1 = nn.LayerNorm(embed_dim)   # ← normaliser après chaque sous-couche
        self.norm2 = nn.LayerNorm(embed_dim)

    def forward(self, x):
        attn_out, _ = self.attn(x, x, x)
        x = self.norm1(x + attn_out)            # résiduel + normalisation
        x = self.norm2(x + self.ff(x))
        return x

1.4 GroupNorm Normalisation par groupes

Divisez les canaux en plusieurs groupesgroup, et normalisez au sein de chaque groupe. Cela ne dépend pas de la taille du lot et surpasse BatchNorm dans les scénarios de petits lots (par exemple, détection d'objets, segmentation d'images médicales).

Exemple

# num_groups : en combien de groupes diviser les canaux (doit diviser num_channels de manière égale)
# Cas particulier : num_groups=1 équivaut à LayerNorm
# num_groups=num_channels équivaut à InstanceNorm
gn = nn.GroupNorm(
    num_groups=32,       # nombre de groupes (couramment : 8, 16, 32)
    num_channels=256,    # nombre de canaux
    eps=1e-5,
    affine=True,
)

x = torch.randn(4, 256, 56, 56)    # Petit lot (4 images), 256 canaux
out = gn(x)
print(out.shape)                    # torch.Size([4, 256, 56, 56])

# GroupNorm n'est pas du tout affecté lorsque batch=1 (par ex., inférence sur une seule image)
x_single = torch.randn(1, 256, 56, 56)
out_single = gn(x_single)           # Fonctionne normalement

1.5 InstanceNorm Normalisation d'instance

PaireChaque canal de chaque échantillonNormalisé indépendamment, sans impliquer la dimension de lot. Couramment utilisée entransfert de style d'image— préserve les informations de style propres à chaque image, empêchant les statistiques des autres images du lot d'interférer.

Exemple

# InstanceNorm2d : normalise H×W pour chaque (N, C)
in2d = nn.InstanceNorm2d(
    num_features=64,
    affine=False,    # Par défaut False (n'apprend pas gamma/beta)
)

x = torch.randn(8, 64, 128, 128)
out = in2d(x)
print(out.shape)    # torch.Size([8, 64, 128, 128])

1.6 Comparaison des méthodes de normalisation

Méthode Dimension de normalisation Dépendance au lot Scénarios applicables
BatchNorm À travers le lot (même canal) Forte dépendance Classification d'images CNN (grand lot)
LayerNorm À travers les caractéristiques (même échantillon) Aucune dépendance Transformer, NLP, RNN
GroupNorm Canaux intra-groupe (même échantillon) Aucune dépendance Détection d'objets et segmentation sur petits lots
InstanceNorm Dimension spatiale (même échantillon et même canal) Aucune dépendance Transfert de style d'image, modèles génératifs

Référence rapide pour les suggestions de sélection

# batch_size >= 16, tâches d'image CNN → BatchNorm2d
# Transformer / BERT / GPT → LayerNorm
# batch_size < 8, détection/segmentation d'objets → GroupNorm(num_groups=32)
# Transfert de style d'image / CycleGAN → InstanceNorm2d

2. Dropout

2.1 Principes de base

Dropout met aléatoirement à 0 les sorties de certains neurones pendant l'entraînement (avec une probabilitép), forçant le réseau à ne pas dépendre d'un seul neurone, apprenant ainsi des représentations de caractéristiques plus robustes et distribuées et prévenant efficacement le surapprentissage.

Pendant l'entraînement :

\[y_i = \begin{cases} 0 & \text{avec probabilité } p \\ \dfrac{x_i}{1-p} & \text{avec probabilité } 1-p \end{cases}\]

Les neurones conservés sont mis à l’échelle par \(\frac{1}{1-p}\) pour que la valeur attendue reste inchangée (c’est-à-dire Dropout inversé).

Pendant l’inférence : Dropout Désactivé automatiquement, tous les neurones participent normalement aux calculs, ce qui équivaut à la moyenne d’ensemble de plusieurs sous-réseaux.

训练时示意(p=0.5):
输入:  [1.0,  2.0,  3.0,  4.0,  5.0]
掩码:  [ 1,    0,    1,    0,    1 ]   ← 随机生成
输出:  [2.0,  0.0,  6.0,  0.0, 10.0]  ← 保留的值乘以 1/(1-0.5)=2

推理时:
输出:  [1.0,  2.0,  3.0,  4.0,  5.0]  ← 原样输出

2.2 Dropout / Dropout2d / Dropout3d

Dropout : utilisé pour les couches entièrement connectées

Exemple

import torch
import torch.nn as nn

dropout = nn.Dropout(p=0.5)   # p: probabilité de mise à zéro

x = torch.ones(2, 10)
print("Mode d’entraînement :")
dropout.train()
print(dropout(x))    # Environ 50 % des valeurs sont 0, les valeurs conservées sont 2.0

print("\n"Mode d’évaluation :")
dropout.eval()
print(dropout(x))    # Toutes les valeurs sont 1.0, Dropout désactivé

Exemple de sortie :

训练模式:
tensor([[2., 0., 2., 0., 2., 0., 0., 2., 2., 0.],
        [0., 2., 0., 2., 0., 2., 2., 0., 0., 2.]])

评估模式:
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]])

Dropout2d : pour les cartes de caractéristiques CNN (abandon de canal entier)

Dropout2dAbandonne aléatoirement des canaux entiers, c'est-à-dire que toutes les positions spatiales d'un canal sont mises à zéro ensemble. Cela convient mieux aux caractéristiques convolutionnelles qu'un Dropout point par point, car les activations de pixels adjacents sont fortement corrélées, ce qui rend l'abandon point par point moins efficace.

Exemple

dropout2d = nn.Dropout2d(p=0.3)

# Entrée : (N, C, H, W)
x = torch.ones(4, 16, 8, 8)    # 4 images, 16 canaux
out = dropout2d(x)
print(out.shape)                # torch.Size([4, 16, 8, 8])

# Vérifier : abandon de canal entier
# État d'abandon des canaux pour l'image 0 (non nul = canal conservé, tout à zéro = canal abandonné)
kept_channels = (out[0].sum(dim=(1, 2)) != 0).sum().item()
print(f"Image 0 a conservé {kept_channels}/16 canaux")

Dropout3d : utilisé pour la convolution 3D

Exemple

# Entrée : (N, C, D, H, W), tout le volume de caractéristiques (D, H, W) est supprimé
dropout3d = nn.Dropout3d(p=0.2)
x = torch.ones(2, 8, 16, 16, 16)
out = dropout3d(x)
print(out.shape)   # torch.Size([2, 8, 16, 16, 16])

Recommandations pour choisir la valeur p

Exemple

# Couches entièrement connectées (couches cachées larges) : Dropout plus élevé
nn.Dropout(p=0.5)     # Valeur classique, standard pour les couches cachées

# Couches entièrement connectées (couches cachées étroites) ou petits réseaux : Dropout plus faible
nn.Dropout(p=0.2)     # Éviter le sous-apprentissage

# Dropout2d pour les couches convolutionnelles : généralement plus faible
nn.Dropout2d(p=0.1)   # Les couches convolutionnelles ont déjà un certain effet de régularisation

# Dropout avant la tête de classification (par ex., EfficientNet / ViT)
nn.Dropout(p=0.3)     # Habituellement entre 0.2 et 0.5

# Dropout d'attention Transformer / FFN
nn.Dropout(p=0.1)     # Valeur par défaut de BERT, les Transformers utilisent généralement des valeurs plus petites

2.3 AlphaDropout

Spécifiquement conçu pourla fonction d'activation SELUla variante Dropout ajuste automatiquement la moyenne et la variance après la suppression de neurones, préservant la propriété d'auto-normalisation.

Exemple

# Utilisé avec SELU (réseau auto-normalisé SELU + AlphaDropout)
class SelfNormalizingNet(nn.Module):
    def __init__(self, in_dim, hidden_dim, num_classes):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),   # ← Utilisé avec SELU
            nn.Linear(hidden_dim, hidden_dim),
            nn.SELU(),
            nn.AlphaDropout(p=0.1),
            nn.Linear(hidden_dim, num_classes),
        )

    def forward(self, x):
        return self.net(x)

Remarque :AlphaDropout n'a de sens que lorsqu'on utilise la fonction d'activation SELU ; avec d'autres fonctions d'activation, utilisez directement Dropout standard.


3. Mode d'entraînement et mode d'évaluation

Le comportement de BatchNorm et Dropout pendant l'entraînement et l'évaluationest complètement différent, et il faut basculer correctement entre les deux ; c'est l'une des erreurs les plus courantes chez les débutants.

Comparaison des différences de comportement

Composant model.train() model.eval()
BatchNorm Normalise en utilisant la moyenne/variance du lot courant ;Met à jour running_mean/var Normalise en utilisant running_mean/var ;Aucune mise à jour
Dropout Met aléatoirement à zéro (avec une probabilité p) Désactivé, tous les neurones produisent normalement leur sortie

Utilisation correcte

Exemple

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(128, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 10),
)

# ── Phase d'entraînement ──────────────────────────────────────
model.train()    # ← Passer en mode entraînement (état par défaut)

for inputs, labels in train_loader:
    optimizer.zero_grad()
    outputs = model(inputs)   # BN utilise les statistiques du batch, Dropout désactive aléatoirement
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

# ── Phase d'évaluation/validation ─────────────────────────────────
model.eval()     # ← Basculer en mode évaluation (très important !)

with torch.no_grad():              # Désactiver aussi le calcul du gradient
    for inputs, labels in val_loader:
        outputs = model(inputs)    # BN utilise les statistiques courantes, Dropout est désactivé
        # ...

# ── Inférence sur une seule image ──────────────────────────────────
model.eval()
with torch.inference_mode():       # Plus rapide que no_grad, uniquement pour l'inférence
    output = model(single_input.unsqueeze(0))
    pred   = output.argmax(1).item()

Vérifier si le basculement de mode est correct

Exemple

# Utilisez ce code pour vérifier que BN et Dropout fonctionnent dans les modes corrects
x = torch.randn(8, 128)

model.train()
out_train_1 = model(x)
out_train_2 = model(x)
print("Mode d'entraînement : même sortie deux fois ?", torch.allclose(out_train_1, out_train_2))
# Faux ← Le masque de Dropout diffère à chaque fois, les sorties diffèrent (normal)

model.eval()
out_eval_1 = model(x)
out_eval_2 = model(x)
print("Mode d'évaluation : même sortie deux fois ?", torch.allclose(out_eval_1, out_eval_2))
# Vrai ← Dropout est désactivé, BN utilise des statistiques fixes, la sortie est déterministe (normal)

4. Placement dans le réseau

Placement de BatchNorm

Il existe deux écoles de pensée sur le placement de BatchNorm, toutes deux utilisées en pratique :

École 1 : Avant la fonction d’activation (article original)

# Conv → BN → ReLU
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.BatchNorm2d(64),      # BN avant activation
    nn.ReLU(inplace=True),
)

École 2 : Après la fonction d’activation (certaines recherches suggèrent de meilleures performances)

# Conv → ReLU → BN
nn.Sequential(
    nn.Conv2d(32, 64, 3, padding=1),
    nn.ReLU(inplace=True),
    nn.BatchNorm2d(64),      # BN après activation
)

Conseil pratique :Suivez la conception de l’article que vous référencez ; si aucune exigence particulière n’est requise, Conv → BN → ReLU est plus courant.

Placement de Dropout

Réseaux entièrement connectés : après la fonction d'activation

nn.Sequential(
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.5),         # Après ReLU
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(128, 10),      # Généralement pas de Dropout avant la couche de sortie
)

CNN : après BN (si BN est utilisé, le Dropout peut être omis)

# Remarque : BN a lui-même un effet de régularisation ; si BN est déjà présent dans un CNN, le Dropout n'est pas nécessaire dans les couches convolutionnelles
# Le Dropout n'est généralement ajouté qu'avant les couches entièrement connectées ou la tête de classification
nn.Sequential(
    nn.Conv2d(64, 128, 3, padding=1),
    nn.BatchNorm2d(128),
    nn.ReLU(inplace=True),
    # Généralement pas de Dropout ici (BN fournit déjà une régularisation)
)

Transformer : à l'intérieur des couches FFN et d'attention

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),  # Ajouter un Dropout à l'intérieur du FFN
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout),  # Ajouter encore une fois avant la sortie
        )
    def forward(self, x):
        return self.net(x)

BN et Dropout doivent-ils être utilisés ensemble ?

Réseaux entièrement connectés : peuvent être utilisés ensemble

nn.Sequential(
    nn.Linear(512, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),         # BN + Dropout coexistent, courant et efficace
    nn.Linear(256, 10),
)

Réseaux convolutionnels : on choisit généralement l’un ou l’autre, ou on n’utilise Dropout qu’en tête.

Raison : l’opération de normalisation de BN et Dropout peuvent provoquer un problème de « décalage de variance ». C’est-à-dire que pendant l’entraînement, Dropout modifie la variance des valeurs d’activation, tandis que pendant l’inférence, Dropout est désactivé, ce qui fait que les statistiques vues par BN pendant l’inférence ne sont pas cohérentes avec celles vues pendant l’entraînement.

Pratique recommandée :

Option A : utiliser uniquement BN dans les couches convolutionnelles, et uniquement Dropout dans les têtes entièrement connectées/de classification

Exemple

class ConvNet(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1),
            nn.BatchNorm2d(64),       # Partie convolutionnelle : uniquement BN
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.AdaptiveAvgPool2d(1),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Dropout(0.5),          # Partie entièrement connectée : uniquement Dropout
            nn.Linear(256, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))

5. Exemples de réseaux complets

Exemple 1 : réseau de classification entièrement connecté (BN + Dropout)

Exemple

import torch
import torch.nn as nn

class MLPClassifier(nn.Module):
    """
Classifieur entièrement connecté avec BatchNorm1d et Dropout
Adapté aux tâches de classification de données tabulaires
    """

    def __init__(self, in_dim, hidden_dims, num_classes, dropout=0.5):
        super().__init__()
        layers = []
        prev_dim = in_dim

        for hidden_dim in hidden_dims:
            layers += [
                nn.Linear(prev_dim, hidden_dim),
                nn.BatchNorm1d(hidden_dim),
                nn.ReLU(inplace=True),
                nn.Dropout(dropout),
            ]
            prev_dim = hidden_dim

        layers.append(nn.Linear(prev_dim, num_classes))
        self.net = nn.Sequential(*layers)

    def forward(self, x):
        return self.net(x)


model = MLPClassifier(
    in_dim=784,
    hidden_dims=[512, 256, 128],
    num_classes=10,
    dropout=0.4
)

# Vérifier la forme de sortie
x = torch.randn(32, 784)
model.eval()
print(model(x).shape)   # torch.Size([32, 10])

Exemple 2 : Classification d'images CNN (principalement BN)

Exemple

class ConvBNBlock(nn.Module):
    """Bloc convolutionnel standard : Conv → BN → ReLU"""
    def __init__(self, in_ch, out_ch, stride=1):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(out_ch),    # bias=False : le bêta de BN sert déjà de biais
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.block(x)


class SmallCNN(nn.Module):
    """Petit CNN pour CIFAR-10"""
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            ConvBNBlock(3, 32),               # 32×32 → 32×32
            ConvBNBlock(32, 64, stride=2),    # 32×32 → 16×16
            ConvBNBlock(64, 128, stride=2),   # 16×16 → 8×8
            ConvBNBlock(128, 256, stride=2),  # 8×8 → 4×4
            nn.AdaptiveAvgPool2d(1),          # 4×4 → 1×1
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Dropout(0.5),                  # Utiliser Dropout uniquement dans les couches entièrement connectées
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.classifier(self.features(x))


model = SmallCNN(num_classes=10)
x = torch.randn(16, 3, 32, 32)
model.eval()
print(model(x).shape)   # torch.Size([16, 10])

Exemple 3 : Boucle d'entraînement complète

Exemple

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR

DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
EPOCHS = 50

model     = SmallCNN(num_classes=10).to(DEVICE)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS)

def run_epoch(model, loader, optimizer=None, train=True):
    if train:
        model.train()    # ← BN utilise les statistiques de lot, Dropout s'active
    else:
        model.eval()     # ← BN utilise les statistiques courantes, Dropout est désactivé

    total_loss, correct = 0.0, 0

    ctx = torch.enable_grad() if train else torch.no_grad()
    with ctx:
        for imgs, labels in loader:
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)

            if train:
                optimizer.zero_grad()

            outputs = model(imgs)
            loss    = criterion(outputs, labels)

            if train:
                loss.backward()
                optimizer.step()

            total_loss += loss.item() * imgs.size(0)
            correct += (outputs.argmax(1) == labels).sum().item()

    n = len(loader.dataset)
    return total_loss / n, correct / n


best_acc = 0.0
for epoch in range(1, EPOCHS + 1):
    train_loss, train_acc = run_epoch(model, train_loader, optimizer, train=True)
    val_loss,   val_acc   = run_epoch(model, val_loader,   train=False)
    scheduler.step()

    print(f"Époque {epoch:2d}/{EPOCHS} | "
          f"Entraînement {train_loss:.4f}/{train_acc:.4f} | "
          f"Validation {val_loss:.4f}/{val_acc:.4f}")

    if val_acc > best_acc:
        best_acc = val_acc
        torch.save(model.state_dict(), 'best.pth')

6. Guide de réglage des hyperparamètres

Réglage des paramètres BatchNorm

Exemple

# ── momentum (le paramètre nécessitant le plus souvent un ajustement) ─────────────────
# La valeur par défaut 0.1 fonctionne dans la plupart des cas

# batch_size est très petit (< 8) → les statistiques sont instables → réduire le momentum
nn.BatchNorm2d(64, momentum=0.01)

# De nombreuses étapes d'entraînement, distribution des données stable → on peut augmenter le momentum de manière appropriée
nn.BatchNorm2d(64, momentum=0.2)

# ── affine=False (ne pas apprendre gamma/beta) ───────────────
# Rarement utilisé, on garde généralement la valeur par défaut True
# Sauf si vous ne voulez pas explicitement que BN modifie la mise à l'échelle et le décalage des caractéristiques
nn.BatchNorm2d(64, affine=False)

# ── track_running_stats=False ──────────────────────
# Chaque inférence utilise également les statistiques du batch courant (le comportement en entraînement et en inférence est cohérent)
# Convient aux scénarios particuliers tels que l'apprentissage en ligne et les données en streaming
nn.BatchNorm2d(64, track_running_stats=False)

Stratégies de réglage de Dropout

Exemple

# ── Ajuster p en fonction du degré de surapprentissage ───────────────────────────
# Précision d'entraînement bien plus élevée que la précision de validation (surapprentissage sévère) → augmenter p
# Précision d'entraînement et précision de validation proches mais toutes deux faibles (sous-apprentissage) → diminuer p ou supprimer Dropout

# ── Référence des valeurs p recommandées ───────────────────────────────
p_values = {
    'Grandes couches cachées entièrement connectées (> 1024)': 0.5,
    'Couches cachées entièrement connectées moyennes (256~1024)': 0.3,
    'Petites couches cachées entièrement connectées (< 256)': 0.2,
    'Couches convolutionnelles CNN (Dropout2d)': 0.1,
    'Avant la tête de classification': 0.3,
    'Transformer FFN': 0.1,
    'Attention Transformer': 0.1,
}

# ── Dropout dynamique (ajusté au fur et à mesure de l'entraînement) ──────────────────
# Dans certaines pratiques, p est petit au début de l'entraînement puis augmenté plus tard (renforcer la régularisation à mesure que le modèle devient plus complexe)
class DynamicDropoutNet(nn.Module):
    def __init__(self, initial_p=0.1):
        super().__init__()
        self.dropout = nn.Dropout(initial_p)
        self.fc = nn.Linear(256, 10)

    def set_dropout(self, p):
        self.dropout.p = p

    def forward(self, x):
        return self.fc(self.dropout(x))

model = DynamicDropoutNet(initial_p=0.1)
# Au milieu de l'entraînement, augmenter Dropout
model.set_dropout(0.4)

7. Erreurs courantes et mises en garde

Erreur 1 : Oublier de passer en mode eval()

Code incorrect

# Erreur : model.eval() n'est pas appelé pendant l'évaluation
model.train()   # État par défaut
for imgs, labels in val_loader:
    with torch.no_grad():
        outputs = model(imgs)   # Dropout effectue toujours des abandons aléatoires ! BN continue de se mettre à jour !
        # Les résultats ne sont pas déterministes, la précision est artificiellement faible, et running_mean/var est pollué

Code correct

# Correct : changer explicitement de mode avant l'évaluation
model.eval()
with torch.no_grad():
    outputs = model(imgs)    # Sortie déterministe

Erreur 2 : Ajouter un biais à une couche Conv puis utiliser BN

Code incorrect

# Redondant : le biais de Conv sera annulé par le beta de BN, ce qui gaspille des paramètres
nn.Conv2d(64, 128, 3, padding=1, bias=True),   # Par défaut True
nn.BatchNorm2d(128),

Code correct

# Correct : désactiver bias lorsque Conv est suivi de BN
nn.Conv2d(64, 128, 3, padding=1, bias=False),  # ← bias=False
nn.BatchNorm2d(128),

Erreur 3 : Utilisation de BatchNorm avec batch size=1

Code incorrect

# Erreur : BN ne peut pas calculer la variance lorsque batch=1, l'entraînement produira NaN ou inf
# L'inférence sur un seul échantillon est correcte (utilise les statistiques courantes), mais l'entraînement sur un seul échantillon plantera
model.train()
x = torch.randn(1, 64, 32, 32)
out = bn2d(x)    # ← batch=1 en mode entraînement provoquera une erreur ou produira NaN

Solution

# Solution 1 : Utiliser GroupNorm à la place
nn.GroupNorm(num_groups=32, num_channels=64)

# Solution 2 : Utiliser LayerNorm
nn.LayerNorm([64, 32, 32])

# Solution 3 : Passer à eval() pendant l'inférence, n'affecte pas BN
model.eval()
out = bn2d(x)    # batch=1 en mode inférence est complètement normal

Erreur 4 : Dropout provoque un décalage de variance dans BN

Problème

# Problème : Dropout modifie la variance des valeurs d'activation pendant l'entraînement
# Pendant l'inférence, Dropout est désactivé, et BN utilise le running_var d'entraînement (contenant l'effet de Dropout)
# Les statistiques ne correspondent pas, les résultats d'inférence sont biaisés
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.Dropout2d(0.5),        # ← Utiliser Dropout dans les couches convolutives
    nn.BatchNorm2d(128),      # ← La variance vue par BN est modifiée par Dropout
)

Solution

# Solution 1 : BN d'abord, Dropout après
nn.Sequential(
    nn.Conv2d(64, 128, 3),
    nn.BatchNorm2d(128),      # BN d'abord (non affecté par Dropout)
    nn.ReLU(),
    nn.Dropout2d(0.1),        # Dropout après
)

# Solution 2 : Utiliser uniquement BN dans les couches convolutives et uniquement Dropout dans les couches entièrement connectées (la plus recommandée)

Erreur 5 : Oublier de passer les couches BN gelées en mode eval

Code incorrect

# En apprentissage par transfert, lors du gel du backbone, les statistiques courantes (running stats) pré-entraînées de BN ne doivent pas être mises à jour
# Sinon, la nouvelle distribution des données polluera les statistiques pré-entraînées

# Erreur : les paramètres sont gelés, mais les running_mean/var de BN sont toujours mis à jour
model = models.resnet50(weights='IMAGENET1K_V2')
for param in model.parameters():
    param.requires_grad = False   # Seuls les paramètres apprenables sont gelés ; les statistiques de BN sont toujours mises à jour

Code correct

# Solution correcte : passer aussi les couches BN du backbone en mode eval
def freeze_bn(model):
    for module in model.modules():
        if isinstance(module, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
            module.eval()              # Corrige running_mean/var
            for param in module.parameters():
                param.requires_grad = False

model = models.resnet50(weights='IMAGENET1K_V2')
freeze_bn(model)          # Geler toutes les BN
model.fc = nn.Linear(model.fc.in_features, 10)  # Seule la nouvelle couche FC est entraînable

Liste de contrôle de diagnostic rapide

Phénomène Cause possible Élément à vérifier
La perte fluctue de manière imprévisible Oubli de [model.eval()]model.eval() Confirmer l'appel à [model.eval()] avant l'évaluationeval()
Écart important entre la précision d'entraînement/validation Dropout pDropout p trop grand Essayez de diminuer ppou supprimez Dropout
NaN/inf à l'entraînement BatchNorm utilisé avec batch=1 Passez à GroupNorm ou LayerNorm
Mauvaises performances en apprentissage par transfert Statistiques glissantes BN polluées Appelez [.eval()] sur les couches BN du backboneeval()
Résultats d'inférence du modèle instables Vous n'avez pas basculé vers eval() pendant l'inférenceeval() Vous devez appeler [model.eval()] avant l'inférencemodel.eval()
Nombre excessif de paramètres Conv Conv+BN n'a pas désactivé le biais Définir [bias=False]bias=False
Autres extensions