Connexions résiduelles¶
Les connexions résiduelles (ou skip connections) ont été introduite dans l'article Deep Residual Learning for Image Recognition. C'est une technique qui a permis l'utilisation de réseaux profonds ce qui n'était pas vraiment possible avant.
Depuis, les connexions résiduelles sont partout :
Dans ce cours, nous allons voir pourquoi ces connexions résiduelles sont si importantes et nous allons comprendre leur interêt de manière intuitive. Ce notebook s'inspire du cours de fastai.
Intuition¶
L'article sur les connexions résiduelles part d'un constat : Même en utilisant la batchnorm, un réseau profond avec un nombre supérieur de couches est moins performant qu'un réseau moins profond avec moins de couches (en supposant les autres points identiques et pour un réseau peu profond déjà relativement profond ie 20 couches) et ce que ce soit sur les données de training ou de validation (donc il ne s'agit pas d'un problème d'overfitting).
Figure extraite de l'article original.
De manière intuitive, cela paraît assez abberant. Imaginons que l'on remplace nos 36 couches supplémentaires par des fonctions identité (ne font aucune transformation sur l'entrée) alors le réseau de 56 couches devrait être aussi bon que le réseau de 20 couches. Mais en pratique, ce n'est pas le cas et l'optimisation n'arrive même pas à transformer ces 36 couches en identité.
Une manière de comprendre les connexions résiduelles est de considérer qu'elles ajoutent directement l'identité à la transformation. Au lieu du classique x=layer(x)
, on utilise x=x+layer(x)
. En pratique, l'ajout de cette skip connections permet une bien meilleure optimisation.
Une autre façon de voir les choses et qui explique le terme "résiduel" est de voir la transformations comme étant y=x+layer(x)
ce qui équivaut à y-x=layer(x)
. Le modèle n'a plus pour objectif de prédire $y$ mais plutôt de minimiser la différence entre la sortie voulue et l'entrée. C'est de là que vient le terme "résiduel" qui signifie "le reste de la soustraction".
Le universal approximation theorem stipule qu'un réseau de neurones suffisament large peut apprendre n'importe quelle fonction. Mais entre ce qu'il est possible de faire théoriquement et ce qu'on arrive à faire en pratique, il y a un énorme gouffre. Une bonne partie de la recherche en deep learning a pour objectif de réduire ce gouffre et les connexions résiduelles sont un énorme pas en avant dans cette direction.
Le block Resnet¶
Pour entrer un peu plus dans les détails, prenons l'exemple du block resnet qui est la première version des connexions résiduelles et qui s'attaque aux réseaux de neurones convolutifs. Au lieu d'avoir x=x+conv(x)
à chaque étape, on a x=x+conv2(conv1(x))
ce qui correspond à cette figure :
Le truc avec les réseaux convolutifs, c'est qu'on souhaite diminuer le résolution et augmenter le nombre de filtres avec la profondeur du réseau. Les connexions résiduelles ne permettent pas ça car on ne peut pas sommer des tenseurs de tailles différentes. En pratique, on peut modifier le tenseur provenant de la connexion résiduelle :
- Pour diminuer la résolution, il suffit d'appliquer une opération de pooling (Max ou Average).
- Pour augmenter le nombre de filtres, on utilise une convolution avec des filtres de taille $1 \times 1$ ce qui correspond à un simple produit scalaire.
Convolution $1 \times 1$ : Par rapport à une convolution classique, la convolution 1x1 simplifie la transformation des canaux de l'image sans mélanger les informations spatiales, fonctionnant principalement comme une opération de réduction de dimensionnalité ou de réajustement des canaux.
Voilà comment on pourrait implémenter le block de resnet en pytorch :
import torch.nn as nn
import torch.nn.functional as F
class ResBlock(nn.Module):
def __init__(self, ni, nf, stride=1):
self.convs = nn.Sequential(
nn.Conv2d(ni, nf, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.Conv2d(nf, nf, kernel_size=3, stride=1, padding=1)
)
# Si le nombre de filtre de l'entrée et de la sortie ne sont pas les mêmes
self.idconv = nn.Identity() if ni==nf else nn.Conv2d(ni, nf, kernel_size=1, stride=1)
# Si le stride est différent de 1, on utilise une couche de pooling (average)
self.pool =nn.Identity() if stride==1 else nn.AvgPool2d(2, ceil_mode=True)
def forward(self, x):
return F.relu(self.convs(x) + self.idconv(self.pool(x)))
Note : On utilise la fonction d'activation après l'ajout de la partie résiduelle car on considère le ResBlock comme une couche à part entière.
L'exemple des connexions résiduelles illustre combien il est crucial pour les chercheurs de pratiquer et d'expérimenter avec les réseaux de neurones (au lieu de faire uniquement de la théorie).
En pratique, il a été démontré dans le papier Visualizing the Loss Landscape of Neural Nets que les connexions résiduelles ont pour effet de lisser la fonction de loss ce qui explique que l'optimisation se passe beaucoup mieux.
Point sur le block bottleneck¶
Un autre type de block a été introduit dans le papier Deep Residual Learning for Image Recognition. Il s'agit du block bottleneck qui ressemble à ça :
A gauche le block resnet de base et à droite le block bottleneck.
Ce block contient plus de convolutions mais est en fait plus rapide que le block resnet de base et c'est grâce aux convolutions $1 \times 1$ qui sont très rapides. Le gros avantage de ce block est qu'il permet d'augmenter le nombre de filtres sans augmenter le temps de traitement (et même en le réduisant). C'est ce block là qui est utilisé pour les versions les plus profondes de resnet (50, 101 et 152 couches) alors que le block de base est utilisé pour les versions moins profondes (18 et 34 couches).
Note : En pratique, si l'on utilise les couches bottleneck sur les architectures moins profondes (18 et 34 couches), on a en général des meilleurs résultats qu'avec les resnet block classique. Pourtant, dans la littérature, la plupart des gens continuent d'utiliser la version avec le resnet block. Parfois les habitudes restent ancrées mais cela montre l'importance de se questionner sur les "choses que tout le monde sait".
Pour conclure, les couches résiduelles sont une avancée majeure dans le deep learning et il est conseillé de les utiliser dès que votre réseau commence à devenir profond.