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 , on leur demande d'apprendre le résidu , et on ajoute à la sortie.
L'astuce est double :
- Point d'initialisation : quand les poids sont proches de zéro à l'initialisation, et le bloc calcule . 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 . Le 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 ).
- 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èle | Blocs | Bloc utilisé | Paramètres | Erreur ImageNet top-5 |
|---|---|---|---|---|
| ResNet18 | 2, 2, 2, 2 | de base (3x3, 3x3) | 11,7 M | 10,9 % |
| ResNet34 | 3, 4, 6, 3 | de base | 21,8 M | 8,6 % |
| ResNet50 | 3, 4, 6, 3 | bottleneck | 25,6 M | 7,0 % |
| ResNet101 | 3, 4, 23, 3 | bottleneck | 44,6 M | 6,4 % |
| ResNet152 | 3, 8, 36, 3 | bottleneck | 60,2 M | 6,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.