Architectures profondes

Augustin Chevallier

1 Le problème du Vanishing Gradient

  • La rétropropagation multiplie les dérivées locales couche par couche (règle de la chaîne)
  • Avec une activation qui sature (sigmoïde, tanh), la dérivée est proche de 0 loin de 0
  • Sur un réseau profond, ce produit de petits termes s’écrase exponentiellement avec la profondeur

1.1 Norme du gradient

On empile des convolutions 3x3, activation sigmoïde, initialisation “naïve” (indépendante de la profondeur):

1.2 Conséquences

  • Les gradients des premières couches (proches de l’entrée) sont quasi nuls
  • Ces couches n’apprennent pas : le réseau profond est bloqué
  • Trois leviers pour corriger ça : l’initialisation, l’activation, l’architecture

2 Initialisation

2.1 Pourquoi l’initialisation compte

  • Poids initiaux trop petits : le signal, et le gradient, s’écrasent couche après couche
  • Poids initiaux trop grands : ils explosent
  • On va chercher \(Var(x_i) = Var(x_{i+1})\)

2.2 Initialisation Xavier (Glorot)

Pour les activations symétriques : tanh, sigmoïde.

\[\text{Var}(w) = \frac{2}{n_{in} + n_{out}}\]

  • \(n_{in}\), \(n_{out}\) : nombre d’entrées et de sorties de la couche
  • Pour une convolution \(k \times k\) : \(n_{in} = k^2 \cdot C_{in}\)
nn.init.xavier_uniform_(layer.weight)

2.3 Initialisation He (Kaiming)

Pour ReLU : la moitié des activations sont écrasées à 0, il faut compenser.

\[\text{Var}(w) = \frac{2}{n_{in}}\]

nn.init.kaiming_normal_(layer.weight, nonlinearity="relu")

Attention : ce n’est pas ce que fait PyTorch par défaut (slide suivante).

2.4 L’initialisation par défaut de PyTorch

Conv2d et Linear initialisent en réalité par une loi uniforme :

\[w \sim \mathcal{U}\!\left(-\frac{1}{\sqrt{n_{in}}},\; \frac{1}{\sqrt{n_{in}}}\right) \qquad\Longrightarrow\qquad \text{Var}(w) = \frac{1}{3\,n_{in}}\]

  • Héritage des premières versions de Torch, conservé par compatibilité ascendante
  • Le code écrit kaiming_uniform_(w, a=√5) : ce n’est pas He, c’est seulement la façon de retomber exactement sur la borne historique \(1/\sqrt{n_{in}}\)

2.5 Effet sur le vanishing gradient

Même CNN à 40 couches. Chaque initialisation est comparée à l’initialisation naïve, sur l’activation pour laquelle elle a été conçue :

2.6 Constat

  • Avec l’initialisation adaptée, le gradient reste du même ordre de grandeur sur toute la profondeur
  • Sans elle, il s’effondre de plusieurs ordres de grandeur avant d’atteindre les premières couches
  • Chaque formule va avec son activation : Xavier pour tanh/sigmoïde, He pour ReLU

3 ReLU vs le reste

3.1 Comparaison des activations

3.2 Avantages de ReLU

  • Pas de saturation pour \(x > 0\) : le gradient n’est pas atténué en traversant l’activation
  • Calcul très simple (comparé à l’exponentielle de la sigmoïde)
  • Favorise des activations parcimonieuses (beaucoup de zéros)

3.3 Inconvénient: dying ReLU

  • Si l’entrée d’un neurone est toujours négative, son gradient est toujours nul
  • Le neurone “meurt” : il ne sera plus jamais mis à jour
  • Solutions : Leaky ReLU / GELU, ou une initialisation adaptée (He)

4 LayerNorm / RMSNorm

4.1 Une limite de l’initialisation

  • Xavier/He choisissent une bonne initialisation
  • Pendant l’entraînement, les poids évoluent : rien ne garantit que cet équilibre reste vrai couche après couche
  • Idée : forcer la variance du signal à chaque couche, explicitement, pendant tout l’entraînement

4.2 LayerNorm

Pour chaque exemple pris séparément, on normalise sur ses \(d\) features \(x = (x_1, \dots, x_d)\) :

\[\mu = \frac{1}{d}\sum_{i=1}^{d} x_i \qquad\qquad \sigma^2 = \frac{1}{d}\sum_{i=1}^{d} (x_i - \mu)^2 \qquad \qquad \hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}}\]

Sortie: \[y_i = \gamma_i\,\hat{x}_i + \beta_i\]

  • Par construction, \(\hat{x}\) est alors de moyenne \(0\) et d’écart-type \(1\)
  • \(\epsilon\) (typiquement \(10^{-5}\)) évite la division par zéro
  • \(\gamma, \beta \in \R^d\) : paramètres appris
  • Sur une image (\(C\) canaux, \(H \times W\)) : \(d = C \cdot H \cdot W\), on normalise sur tout l’exemple

4.3 RMSNorm

Variante simplifiée : pas de centrage, seulement une mise à l’échelle : \[\text{RMS}(x)=\sqrt{\frac{1}{n}\sum_i x_i^2+\epsilon}, \qquad y=\gamma\,\frac{x}{\text{RMS}(x)}\]

  • Moins cher à calculer (pas de moyenne à soustraire)
  • Performances similaires à LayerNorm en pratique
  • Utilisé dans la plupart des LLM récents (LLaMA, T5, GPT-NeoX…)

4.4 CNN profond, initialisation naïve

ReLU, même initialisation naïve pour les trois, avec et sans normalisation:

5 Connexions résiduelles

5.1 Le problème de dégradation

  • Même avec une bonne initialisation et ReLU, les très grands réseaux restent difficiles à entraîner
  • Constat empirique (He et al., ResNet, 2015) : au-delà d’une certaine profondeur, ajouter des couches augmente l’erreur d’entraînement
  • Ce n’est pas de l’overfitting (l’erreur d’entraînement augmente aussi) : le réseau devient simplement plus dur à optimiser

5.2 Residual connection

Pour une partie (un bloc) du réseau \(y = F(x)\)

  • Au lieu d’apprendre \(y = F(x)\) (toute la transformation), on apprend \(y = x + F(x)\) (juste un résidu)
  • La jacobienne du bloc devient alors : \[J = I + J_F\]
  • Même si \(J_F \to 0\) (couche saturée, mal initialisée…), le gain reste \(\approx 1\) grâce à l’identité
  • Dans le produit \(J_{l+1}\cdots J_n\), chaque facteur reste donc proche de \(I\)
  • Si \(F = 0\) le bloc se comporte comme l’identité: jamais pire qu’un réseau moins profond

5.3 Bloc résiduel convolutif

  • Deux convolutions, avec une ReLU entre les deux, puis addition avec l’entrée
  • Nécessite \(C_{in} = C_{out}\) pour pouvoir additionner directement

\[y = x + \text{Conv2D}(\text{ReLu}(\text{Conv2D}(x)))\]

5.4 Pourquoi deux convolutions:

  • une couche de convolution est une operation linéaire
  • on peut écrire \(\text{Conv2D}(x) = Wx\)
  • Option 1: \(y = (x + \text{Conv2d}x) = ((W + I)\,x\big)\): encore une convolution
  • Option 2: \(y = x + \text{Relu}(\text{Conv2D}(x))\)
  • probleme: Relu est \(\geq 0\) on ne peut ajouter que des valeurs positives!
  • il faut une operation linéaire apres Relu et avant la connection résiduelle

5.5 Exemple

class ResidualConvBlock(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        self.relu = nn.ReLU()
        nn.init.kaiming_normal_(self.conv1.weight, nonlinearity="relu")
        nn.init.kaiming_normal_(self.conv2.weight, nonlinearity="relu")

    def forward(self, x):
        out = self.relu(self.conv1(x))
        out = self.conv2(out)
        return self.relu(x + out)  # connexion résiduelle