ResNet : Guide Complet — Réseaux Résiduels et Connexions Sautantes

ResNet : Guide Complet — Réseaux Résiduels et Connexions Sautantes

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

  1. Très profond : Permet l’entraînement de réseaux de 152, 1000+ couches, impossible auparavant.
  2. Facile à optimiser : Les skip connections stabilisent le gradient, rendant l’entraînement plus simple.
  3. Transfer learning : Les ResNets pré-entraînés (ImageNet) sont la base de la plupart des modèles de vision.
  4. Extensible : Le bottleneck architecture permet d’augmenter la profondeur sans explosion de paramètres.

Limites

  1. Coût computationnel : ResNet-152 nécessite 60M+ paramètres et des semaines d’entraînement.
  2. Redondance des features : Les skip connections créent des chemins redondants qui peuvent calculer les mêmes transformations.
  3. 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


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.