Aller au contenu principal

Module 6 — ResNet et les connexions résiduelles

Le module 5 s'est terminé sur un constat troublant : VGG19 n'est pas beaucoup meilleur que VGG16, et un VGG plus profond fait pire. C'est étrange, parce qu'un réseau à 50 couches est au moins aussi expressif qu'un réseau à 20 couches : il suffirait que les 30 couches supplémentaires apprennent l'identité pour retomber exactement sur les performances du plus court. Or l'optimisation par descente de gradient n'y arrive pas. Ce module explique pourquoi, présente la solution qu'ont apportée He, Zhang, Ren et Sun en 2015, et démontre à quel point elle change la donne.

La dégradation, ce n'est pas du surajustement

Un premier réflexe est de suspecter le surajustement : un réseau plus profond aurait trop de capacité et mémoriserait le jeu d'entraînement. C'est faux, et c'est ce qui rend le phénomène remarquable.

Sur CIFAR-10, un réseau « à la VGG » à 20 couches atteint 8 % d'erreur d'entraînement. Le même réseau à 56 couches atteint 11 % d'erreur d'entraînement — pas seulement de validation, mais bien d'entraînement. Le réseau plus profond n'apprend pas même les données qu'on lui donne, alors qu'un sous-réseau plus court y arriverait. Ce n'est pas une insuffisance de capacité, c'est une insuffisance de l'algorithme d'optimisation.

Les auteurs de ResNet ont nommé ce phénomène la dégradation. Il apparaît avec la profondeur, indépendamment du surajustement, et bloque la progression du CNN pendant deux ans.

Le bloc résiduel, en une équation

L'idée est d'un extrême minimalisme : au lieu d'imposer à deux couches d'apprendre une transformation H(x)H(x), on leur demande d'apprendre le résidu F(x)=H(x)xF(x) = H(x) - x, et on ajoute xx à la sortie.

y=F(x)+xy = F(x) + x
Bloc résiduel de ResNet. L'entrée passe par deux convolutions 3x3 entrecoupées de BatchNorm et ReLU, tandis qu'un raccourci contourne le bloc pour ajouter l'entrée à la sortie avant une ReLU finale.Entrée xConv 3x3BN + ReLUConv 3x3BNraccourci (identité)x est ajouté à la somme+ReLU → Sortie
Le raccourci ne traverse aucune couche apprise. Si les deux convolutions sont mises à zéro, le bloc devient l'identité, ce qui garantit qu'ajouter des blocs ne peut pas dégrader les performances.

L'astuce est double :

  • Point d'initialisation : quand les poids sont proches de zéro à l'initialisation, F(x)0F(x) \approx 0 et le bloc calcule yxy \approx x. Le point de départ de l'optimisation est donc l'identité, pas une transformation aléatoire. Empiler des blocs ne peut pas être pire que de ne rien empiler.
  • Autoroute du gradient : dans la passe arrière, la dérivée y/x=F/x+1\partial y / \partial x = \partial F / \partial x + 1. Le +1+1 garantit qu'un gradient de la sortie remonte à l'entrée sans être divisé par les poids intermédiaires. Même après cinquante couches, le gradient de la première couche reste comparable à celui de la dernière.
from tensorflow import keras
from tensorflow.keras import layers

def bloc_residuel(x, canaux):
raccourci = x
x = layers.Conv2D(canaux, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.Conv2D(canaux, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Add()([x, raccourci])
return layers.ReLU()(x)

Le raccourci ajoute une addition à chaque bloc, sans un seul paramètre appris.

Ajuster le raccourci quand la forme change

Le raccourci n'ajoute rien si sa forme correspond à celle de la sortie. Quand un bloc sous-échantillonne (pas 2) ou change le nombre de canaux, il faut aligner. Deux options :

  • Projection linéaire : une convolution 1x1 à pas 2 sur le raccourci, sans activation. Peu de paramètres, comportement propre.
  • Remplissage par zéros : dupliquer les canaux avec des zéros et sous-échantillonner à l'identique. Sans paramètre, mais rarement utilisé aujourd'hui.
def bloc_residuel_projete(x, canaux, pas=2):
raccourci = layers.Conv2D(canaux, 1, strides=pas, use_bias=False)(x)
raccourci = layers.BatchNormalization()(raccourci)

x = layers.Conv2D(canaux, 3, strides=pas, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU()(x)
x = layers.Conv2D(canaux, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Add()([x, raccourci])
return layers.ReLU()(x)

C'est la « variante B » de l'article original, aujourd'hui standard.

Le bloc bottleneck, pour aller plus profond

Empiler 152 couches en blocs de deux convolutions 3x3 devient trop coûteux. Les auteurs proposent un bloc à trois couches — 1x1, 3x3, 1x1 — appelé bottleneck, dont la particularité est de compresser puis rétablir le nombre de canaux :

  • Une 1x1 divise les canaux par 4 (par exemple 25664256 \to 64).
  • Une 3x3 opère sur cette version compressée (économie principale).
  • Une 1x1 rétablit les 256 canaux.

Le coût total du bloc est du même ordre qu'un bloc à deux 3x3 sur les 256 canaux directs, mais avec trois couches et deux ReLU supplémentaires — donc plus de capacité pour le même budget. C'est ce bloc qui rend ResNet50 et ResNet152 faisables.

Les familles ResNet

Le nombre au bout du nom donne la profondeur totale, en comptant les convolutions et la couche dense finale :

ModèleBlocsBloc utiliséParamètresErreur ImageNet top-5
ResNet182, 2, 2, 2de base (3x3, 3x3)11,7 M10,9 %
ResNet343, 4, 6, 3de base21,8 M8,6 %
ResNet503, 4, 6, 3bottleneck25,6 M7,0 %
ResNet1013, 4, 23, 3bottleneck44,6 M6,4 %
ResNet1523, 8, 36, 3bottleneck60,2 M6,2 %

Deux lectures :

  • ResNet50 dépasse ResNet34 avec à peine plus de paramètres, uniquement grâce au bottleneck.
  • Passer de ResNet50 à ResNet152 triple la profondeur pour un gain de moins d'un point. Sur un jeu réel, l'écart est rarement rentable.

Sur CIFAR-10, l'usage est différent : on adapte l'architecture avec une entrée 3x3, trois étages de sous-échantillonnage seulement, et beaucoup moins de canaux. Le « ResNet20 » CIFAR fait 20 couches et 270 000 paramètres, et dépasse 91 % de précision de validation.

Pré-activation, la variante qui a suivi

En 2016, la même équipe a publié une révision — la pré-activation — où l'ordre des opérations change : BN → ReLU → Conv → BN → ReLU → Conv → Add. Le raccourci relie les entrées bruts (non normalisés, non activés) directement à la sortie du bloc. Le gradient traverse le réseau encore plus proprement, et les ResNet à 1000 couches, jouables en démonstration, deviennent envisageables.

En pratique, pour un projet ordinaire, la différence est mineure sous 100 couches. La version pré-activation est cependant devenue la norme dans presque toutes les architectures postérieures — DenseNet, EfficientNet, ConvNeXt — même quand elles n'ont plus grand-chose à voir avec ResNet.

Le bloc résiduel est aujourd'hui un réflexe

Avant d'écrire une architecture de plus de 10 couches, ajouter des connexions résiduelles est le premier geste — même sans changer le reste. Le coût est nul, le bénéfice est presque garanti. Un CNN moderne sans raccourci n'existe pratiquement plus.

En résumé

  • La dégradation est un phénomène d'optimisation, pas de surajustement : un réseau plus profond apprend moins bien son jeu d'entraînement qu'un plus court, faute d'un chemin propre pour le gradient.
  • Un bloc résiduel ajoute l'identité comme point d'initialisation (F(x)0yxF(x) \approx 0 \Rightarrow y \approx x) et une autoroute du gradient (dérivée F/x+1\partial F / \partial x + 1).
  • Le bottleneck (1x1, 3x3, 1x1) rend les ResNet à plus de 50 couches faisables en coût constant par unité de profondeur.
  • ResNet50 est le point d'équilibre standard pour le transfert vers une nouvelle tâche : c'est celui qu'on affinera au module 9 sur le jeu de déchets à trier.

Module suivant : Inception et MobileNet, où l'architecture cherche à extraire plus par FLOP dépensé plutôt qu'à empiler.