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éfaut
1e-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.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
# 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
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
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
# 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.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
# 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
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
# 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.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
# 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
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
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
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.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
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)
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)
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.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)
# 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
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.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
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.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
"""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.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
# 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
# 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
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
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
nn.Conv2d(64, 128, 3, padding=1, bias=True), # Par défaut True
nn.BatchNorm2d(128),
Code correct
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
# 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
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
# 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
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
# 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
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 |