Residual Networks : Guide complet — Réseaux Résiduels et Connexions Sautantes
Résumé — Le ResNet, introduit par He et al. en 2015, est une architecture de réseau neuronal profond qui utilise les connexions sautantes (skip connections) pour permettre l’entraînement de réseaux de plus de 1000 couches. En apprenant des fonctions résiduelles plutôt que des transformations directes, le ResNet résoud le problème de dégradation (degradation problem) où les réseaux profonds voient leurs performances diminuer avec l’ajout de couches, non pas à cause du surapprentissage mais parce que l’optimisation devient impossible. Le ResNet a remporté ImageNet 2015 avec une erreur top-5 de 3.57% — surpassant la précision humaine estimée.
Principe mathématique
1. Le problème: dégradation des réseaux profonds
Avant ResNet, VGGNet (2014) avait 19 couches, GoogLeNet en avait 22. Intuitivement, plus de couches = plus de capacité. Mais en pratique, au-delà d’une certaine profondeur, la précision sur l’entraînement elle-même commence à diminuer. Ce n’est pas du surapprentissage (l’erreur de test est aussi pire) — c’est que la fonction identité (ne rien transformer) est plus difficile à apprendre qu’une fonction non-triviale.
2. La solution: apprentissage résiduel
Au lieu d’apprendre directement la fonction H(x) qu’une couche devrait réaliser, le ResNet apprend la fonction résiduelle F(x) = H(x) – x. La sortie devient :
y = H(x) = F(x, {W_i}) + x
Où F(x, {W_i}) est le réseau résiduel (les convolutions) et x est l’entrée ajoutée directement via la connexion sautante (skip connection). Si la transformation optimale est l’identité, le réseau apprend simplement F(x) = 0, ce qui est beaucoup plus facile que d’apprendre F(x) = x.
3. Pourquoi les gradients passent mieux
Sans skip connection, le gradient dans un réseau de L couches est :
dL/dx_1 = dL/dx_L · d x_L/d x_{L-1} · ... · d x_2/d x_1
Chaque terme d x_{i+1}/d x_i = d sigma(W_i · x_i)/d x_i peut être < 1 (vanishing) ou > 1 (exploding).
Avec skip connection, x_L = F(x_{L-1}) + x_{L-2}, donc :
dL/dx_l = dL/dx_L · (d F/d x_l + 1)
Le **« 1 » garantit que même si d F/d x_l est minuscule, le gradient passe directement. C’est l’autoroute du gradient.
4. Bottleneck architecture
Pour les réseaux très profonds (50+ couches), un block bottleneck à 3 couches remplace le block de 2 couches :
1x1 Conv (réduction) → 3x3 Conv (traitement) → 1x1 Conv (expansion)
La couche 1×1 réduit d’abord les canaux (ex: 256 → 64), la 3×3 travaille sur moins de canaux, et la dernière 1×1 restaure (64 → 256). Cela réduit les paramètres d’un facteur 3.5 par rapport à 2 convolutions 3×3 avec 256 canaux.
5. Pré-activation (ResNet v2)
Le ResNet v2 de 2016 inverse l’ordre: au lieu de Conv → BN → ReLU → Add, il propose BN → ReLU → Conv → Add. Cette pré-activation permet un flux de gradient encore plus propre car il n’y a plus de transformation non-linéaire sur le chemin de la somme.
Intuition
Imaginez une chaîne de production. Dans un réseau classique, chaque équipe (couche) doit transformer complètement le produit. Si une équipe fait une erreur, toute la chaîne est compromise.
Dans un ResNet, chaque équipe reçoit deux choses : le produit brut de l’étape précédente ET une « note de correction » à ajouter. Si le produit est déjà parfait, l’équipe ajoute zéro (F(x) = 0). Si une petite correction suffit, elle ajoute juste ce qu’il faut.
C’est comme demander à un élève de corriger un brouillon plutôt que de le réécrire depuis zéro — corriger est toujours plus facile que créer. Et le professeur (le gradient) peut communiquer directement avec chaque élève grâce au couloir de skip connection qui traverse tout le bâtiment.
Implémentation Python
1. ResNet-18 from scratch avec PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
"""Block de base ResNet (2 convolutions 3x3)."""
expansion = 1
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = x
if self.downsample:
identity = self.downsample(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += identity # Skip connection
out = self.relu(out)
return out
class Bottleneck(nn.Module):
"""Bottleneck block ResNet (1x1 → 3x3 → 1x1)."""
expansion = 4
def __init__(self, in_channels, out_channels, stride=1, downsample=None):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride, 1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.conv3 = nn.Conv2d(out_channels, out_channels * 4, 1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channels * 4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
def forward(self, x):
identity = self.downsample(x) if self.downsample else x
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
out += identity
return self.relu(out)
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=10, base_channels=64):
super().__init__()
self.in_channels = base_channels
self.conv1 = nn.Conv2d(3, base_channels, 7, 2, 3, bias=False)
self.bn1 = nn.BatchNorm2d(base_channels)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(3, 2, 1)
self.layer1 = self._make_layer(block, base_channels, layers[0], stride=1)
self.layer2 = self._make_layer(block, base_channels*2, layers[1], stride=2)
self.layer3 = self._make_layer(block, base_channels*4, layers[2], stride=2)
self.layer4 = self._make_layer(block, base_channels*8, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(base_channels*8 * block.expansion, num_classes)
def _make_layer(self, block, out_channels, num_blocks, stride):
downsample = None
if stride != 1 or self.in_channels != out_channels * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channels, out_channels * block.expansion, 1, stride, bias=False),
nn.BatchNorm2d(out_channels * block.expansion)
)
layers = [block(self.in_channels, out_channels, stride, downsample)]
self.in_channels = out_channels * block.expansion
for _ in range(1, num_blocks):
layers.append(block(self.in_channels, out_channels))
return nn.Sequential(*layers)
def forward(self, x):
x = self.relu(self.bn1(self.conv1(x)))
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x).flatten(1)
return self.fc(x)
# Créer les architectures classiques
def ResNet18(): return ResNet(BasicBlock, [2,2,2,2]) # 18 couches
def ResNet34(): return ResNet(BasicBlock, [3,4,6,3]) # 34 couches
def ResNet50(): return ResNet(Bottleneck, [3,4,6,3], base_channels=64) # 50 couches
# Comparaison de taille
models = {"ResNet18": ResNet18(), "ResNet50": ResNet50()}
for name, model in models.items():
params = sum(p.numel() for p in model.parameters())
print(f'{name}: {params/1e6:.1f}M paramètres')
# Entraînement sur CIFAR-10
model = ResNet18()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
criterion = nn.CrossEntropyLoss()
for epoch in range(100):
model.train()
for images, labels in train_loader:
preds = model(images)
loss = criterion(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()
print(f'Epoch {epoch} | LR: {scheduler.get_last_lr()[0]:.4f}')
Hyperparamètres
| Hyperparamètre | Valeur | Description |
|---|---|---|
| num_layers | 18/34/50/101/152 | Profondeur de l’architecture |
| base_channels | 64 | Nombre de canaux de la première couche |
| bottleneck | True pour 50+ | Utiliser des blocks bottleneck |
| drop_path_rate | 0.0-0.2 | Stochastic depth pour les très grands modèles |
Avantages
- Très profond : Permet l’entraînement de réseaux de 152, 1000+ couches, impossible auparavant.
- Facile à optimiser : Les skip connections stabilisent le gradient, rendant l’entraînement plus simple.
- Transfer learning : Les ResNets pré-entraînés (ImageNet) sont la base de la plupart des modèles de vision.
- Extensible : Le bottleneck architecture permet d’augmenter la profondeur sans explosion de paramètres.
Limites
- Coût computationnel : ResNet-152 nécessite 60M+ paramètres et des semaines d’entraînement.
- Redondance des features : Les skip connections créent des chemins redondants qui peuvent calculer les mêmes transformations.
- Mémory intensive : Les activations de toutes les couches doivent être mémorisées pour le backward, même si certaines sont sautées.
4 cas d’usage concrets
1. Classification d’images ImageNet
ResNet-50 et ResNet-152 sont des modèles de référence pour ImageNet. ResNet-50 atteint 76% top-1 accuracy, et ResNet-152 atteint 78%. Ils servent de point de départ pour toute tâche de vision.
2. Transfer learning pour la détection
Les modèles de détection d’objets (Faster R-CNN, Mask R-CNN) utilisent ResNet comme backbone. Les features pré-entraînées sur ImageNet sont finetunées pour les données spécifiques de détection, réduisant le temps d’entraînement de 10x.
3. Modèles médicaux
Dans l’imagerie médicale (radiographie, IRM), un ResNet pré-entraîné est finetuné sur quelques milliers d’images annotées, atteignant des performances de diagnostic comparables aux experts humains.
4. Embedding pour recherche d’images
Les features extraites d’un ResNet en avant-dernière couche (avant le classifieur) servent de vecteurs d’embedding pour la recherche d’images similaires : similarité cosinus entre embeddings d’images.
Conclusion
Le ResNet est sans conteste l’une des architectures les plus influentes de l’IA moderne. En introduisant les connexions sautantes, il a résoud un problème fondamental de l’apprentissage profond et ouvert la voie aux architectures ultra-profondes qui dominent aujourd’hui.
Même les architectures Transformer de Vision Transformers (ViT) s’inspirent des skip connections du ResNet — elles utilisent les mêmes mécanismes de raccourci entre leurs couches d’attention.
Voir aussi
- Implémentation de l’Algorithme de Cramer et Calcul de Déterminant en Python
- Optimisez Votre Code Python avec SOP et POS : Guide Complet pour les Débutants

