Self-Attention pour Images : Guide Complet — Réseaux Non-Locaux

Self-Attention pour Images : Guide Complet — Réseaux Non-Locaux

Self-Attention pour Images : Guide Complet — Réseaux Non-Locaux

Résumé

La self-attention pour images, introduite par Wang et al. dans leurs travaux pionniers sur les non-local neural networks, représente une avancée majeure dans l’architecture des réseaux de vision par ordinateur. Contrairement aux convolutions classiques qui opèrent uniquement sur des voisinages locaux, la self-attention établit des connexions entre toutes les paires de positions dans une image, permettant ainsi de capturer des dépendances à longue distance essentielles pour de nombreuses tâches de vision.

Ce guide complet explore en profondeur le mécanisme de self-attention appliqué aux images, depuis ses fondements mathématiques jusqu’à son implémentation pratique avec PyTorch. Nous verrons comment les couches non-locales complètent les CNN traditionnels, pourquoi ce mécanisme est devenu un composant incontournable des architectures modernes de vision, et comment l’intégrer efficacement dans vos propres modèles. Que vous soyez un chercheur en apprentissage profond ou un ingénieur souhaitant améliorer la précision de vos modèles de segmentation, ce guide vous fournira toutes les clés nécessaires pour maîtriser la self-attention visuelle dans vos projets de machine learning.

Principe Mathématique

La formulation non-locale

Le cœur de la self-attention pour images réside dans la couche non-locale, une opération fondamentale qui révolutionne la façon dont les réseaux traitent l’information spatiale. Une couche non-locale calcule pour chaque position i de l’image :

y_i = (1 / C(x)) · Σ_j f(x_i, x_j) · g(x_j)

Décomposons cette formule essentielle composante par composante :

  • f(x_i, x_j) = exp(x_i^T · W_theta^T · W_phi · x_j) est la fonction de similarité entre les positions i et j. Elle mesure à quel point deux régions de l’image sont liées sémantiquement. L’exponentielle garantit que les scores de similarité restent strictement positifs, et le produit matriciel projeté permet au réseau d’apprendre quelles dimensions sont importantes pour la comparaison.
  • g(x_j) = W_g · x_j est la transformation de valeur, qui projette chaque position dans un espace de représentation optimisé pour l’agrégation.
  • C(x) = Σ_j f(x_i, x_j) est le facteur de normalisation, qui assure que les contributions de toutes les positions sommées forment une moyenne pondérée cohérente.

Cette formulation diffère radicalement des opérations convolutives classiques. Dans une convolution standard, chaque pixel ne reçoit d’informations que de ses voisins immédiats — typiquement un voisinage de 3×3 ou 5×5 pixels. La self-attention, elle, connecte chaque pixel à tous les autres pixels de l’image, capturant des relations globales qui seraient autrement inaccessibles aux filtres convolutifs.

Comparaison formelle avec la convolution

Pour bien comprendre la complémentarité entre ces deux opérations, considérons leurs champs récepteurs théoriques :

Opération Champ récepteur Complexité Parallélisme
Convolution 3×3 9 pixels locaux O(k² · C²) Élevé
Convolution 5×5 25 pixels locaux O(k² · C²) Élevé
Self-Attention Tous les pixels O(N² · C) Élevé

k est la taille du noyau, C le nombre de canaux, et N le nombre total de positions (hauteur × largeur). La self-attention sacrifie l’efficacité computationnelle locale pour un champ récepteur global et complet.

Les matrices de projection Q, K, V

Dans l’implémentation pratique, les poids W_theta, W_phi et W_g correspondent respectivement aux matrices de projection :

  • Query (Q = W_theta · x) : ce que la position i recherche dans le reste de l’image
  • Key (K = W_phi · x) : ce que chaque position j offre comme information
  • Value (V = W_g · x) : l’information effective à agréger

Le score d’attention entre deux positions se calcule alors par softmax(Q · K^T / √d_k), où d_k est la dimension des clés. Cette formulation, popularisée par le mécanisme d’attention de transformers, est directement applicable aux images en traitant chaque position spatiale comme un token.

Intuition : La Loupe et le Recul

Pour comprendre intuitivement la différence entre convolution et self-attention, imaginez que vous regardez une photographie en deux temps :

La convolution, c’est comme regarder une image avec une loupe. Lorsque vous promenez une loupe sur une photographie, vous voyez chaque détail avec une grande précision — les textures du pelage d’un chat, les nervures d’une feuille, les contours d’un bâtiment. Mais votre champ de vision est limité : vous ne pouvez pas voir simultanément le coin en haut à gauche et le coin en bas à droite de l’image. La convolution fonctionne exactement de cette manière. Ses filtres scrutent méticuleusement chaque région locale, apprenant progressivement des motifs de plus en plus complexes au fil des couches empilées. Cependant, même dans un réseau profond de cent couches, connecter deux pixels éloignés nécessite un cheminement long et indirect à travers toutes les couches intermédiaires.

La self-attention, c’est comme prendre du recul pour voir l’ensemble. Lorsque vous reculez d’un tableau, votre œil perçoit instantanément les relations entre des éléments très éloignés : vous remarquez que le sujet principal se trouve dans le tiers gauche selon la règle des tiers, que la lumière provient d’une source en haut à droite créant des ombres cohérentes, ou que deux personnages dans la scène se regardent mutuellement à travers un espace vide. La self-attention réalise exactement cette opération : elle connecte directement le coin en haut à gauche avec le coin en bas à droite, sans passer par des intermédiaires.

Les deux approches sont nécessaires et complémentaires. La loupe est indispensable pour les textures fines, les bords précis, les motifs répétitifs — tout ce qui relève du détail local. Le recul est essentiel pour la composition globale, les relations sémantiques entre objets éloignés, la cohérence structurelle de la scène. Un réseau qui n’utiliserait que des convolutions manquerait de vision globale ; un réseau qui n’utiliserait que de la self-attention gaspillerait des ressources computationnelles énormes pour des détails que des filtres 3×3 captureraient facilement. C’est précisément cette complémentarité qui rend les non-local networks si efficaces : ils intègrent la self-attention au sein d’architectures convolutives existantes, sans les remplacer.

Implémentation Python avec PyTorch

Bloc Non-Local de base

Commençons par implémenter le composant fondamental : le bloc NonLocalBlock avec une version embarquée efficace (embedded Gaussian), qui est la variante la plus couramment utilisée en pratique.

import torch
import torch.nn as nn
import torch.nn.functional as F


class NonLocalBlock(nn.Module):
    """
    Bloc Non-Local (Self-Attention spatiale) pour les images.

    Implémente la variante 'embedded Gaussian' décrite par
    Wang et al. (CVPR 2018). Ce module se place typiquement
    après un bloc résiduel dans un ResNet, capturant les
    dépendances à longue distance que les convolutions ne
    peuvent pas modéliser directement.

    Args:
        in_channels : nombre de canaux d'entrée
        reduction : facteur de réduction pour la dimension interne
                    (par défaut 2, donc dimension_interne = in_channels // 2)
        sub_sample : facteur de sous-échantillonnage spatial (1, 2 ou 4)
                     Permet de réduire la taille de la matrice d'attention
    """

    def __init__(self, in_channels, reduction=2, sub_sample=1):
        super(NonLocalBlock, self).__init__()
        self.sub_sample = sub_sample
        inter_channels = max(in_channels // reduction, 1)

        # Trois transformations linéaires 1×1 pour Q, K, V
        self.theta = nn.Conv2d(
            in_channels, inter_channels,
            kernel_size=1, stride=1, padding=0, bias=False
        )
        self.phi = nn.Conv2d(
            in_channels, inter_channels,
            kernel_size=1, stride=1, padding=0, bias=False
        )
        self.g = nn.Conv2d(
            in_channels, inter_channels,
            kernel_size=1, stride=1, padding=0, bias=False
        )

        # Convolution 1×1 de sortie pour projeter dans l'espace d'origine
        self.W_z = nn.Conv2d(
            inter_channels, in_channels,
            kernel_size=1, stride=1, padding=0, bias=False
        )
        # Initialisation à zéro pour garantir que le bloc commence
        # comme une opération identité
        nn.init.zeros_(self.W_z.weight)

        # Normalisation par lots (batch norm)
        self.bn = nn.BatchNorm2d(in_channels)

        # Pooling pour le sous-échantillonnage
        if sub_sample > 1:
            self.phi_pool = nn.MaxPool2d(kernel_size=sub_sample)
            self.g_pool = nn.MaxPool2d(kernel_size=sub_sample)
        else:
            self.phi_pool = None
            self.g_pool = None

    def forward(self, x):
        """
        Forward pass du bloc non-local.

        Args:
            x : tenseur d'entrée de forme (B, C, H, W)

        Returns:
            tenseur de sortie de même forme que l'entrée
        """
        batch_size = x.size(0)

        # --- Query : projection de toutes les positions ---
        theta_out = self.theta(x)
        theta_out = theta_out.view(batch_size, theta_out.size(1), -1)
        theta_out = theta_out.permute(0, 2, 1)  # (B, N, C_inter)

        # --- Key : projection avec sous-échantillonnage optionnel ---
        phi_input = self.phi_pool(x) if self.phi_pool else x
        phi_out = self.phi(phi_input)
        phi_out = phi_out.view(batch_size, phi_out.size(1), -1)

        # --- Value : projection avec sous-échantillonnage optionnel ---
        g_input = self.g_pool(x) if self.g_pool else x
        g_out = self.g(g_input)
        g_out = g_out.view(batch_size, g_out.size(1), -1)
        g_out = g_out.permute(0, 2, 1)  # (B, N', C_inter)

        # --- Calcul de la matrice d'attention ---
        # Attention scores : (B, N, C) @ (B, C, N') → (B, N, N')
        attention = torch.bmm(theta_out, phi_out)
        # Normalisation softmax le long de N' (les positions sources)
        attention = F.softmax(attention, dim=-1)

        # --- Agrégation des valeurs pondérées ---
        # (B, N, N') @ (B, N', C) → (B, N, C)
        y = torch.bmm(attention, g_out)
        # (B, N, C) → (B, C, N) → (B, C, H, W)
        y = y.permute(0, 2, 1).contiguous()
        y = y.view(batch_size, -1, x.size(2), x.size(3))

        # --- Projection de sortie et connexion résiduelle ---
        y = self.W_z(y)
        y = self.bn(y)
        output = y + x

        return output

Intégration dans un ResNet

L’intérêt principal des non-local networks est qu’ils s’intègrent dans des architectures existantes. Voici comment insérer un bloc non-local dans un ResNet-50 standard, typiquement au niveau du stade 4 (résolution la plus faible, où le coût computationnel de l’attention est le plus gérable) :

class ResNet50NonLocal(nn.Module):
    """
    ResNet-50 enrichi avec un bloc non-local au stade 4.
    Cette combinaison tire parti des convolutions pour les
    détails locaux et de la self-attention pour les
    dépendances à longue distance.
    """

    def __init__(self, num_classes=1000):
        super(ResNet50NonLocal, self).__init__()

        resnet = torch.hub.load('pytorch/vision:v0.13.0',
                                'resnet50', pretrained=True)

        # Les trois premières étapes restent convolutives
        self.conv1 = resnet.conv1
        self.bn1 = resnet.bn1
        self.relu = resnet.relu
        self.maxpool = resnet.maxpool
        self.layer1 = resnet.layer1
        self.layer2 = resnet.layer2
        self.layer3 = resnet.layer3

        # Stade 4 : on ajoute la self-attention ici
        self.layer4 = resnet.layer4
        self.nonlocal_block = NonLocalBlock(
            in_channels=2048,
            reduction=2,
            sub_sample=1
        )

        self.avgpool = resnet.avgpool
        self.fc = nn.Linear(2048, num_classes)

    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)

        # Self-attention après les convolutions du stade 4
        x = self.nonlocal_block(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

Comparaison avec/sans self-attention en segmentation sémantique

Pour illustrer concrètement l’apport de la self-attention, considérons une tâche de segmentation sémantique sur l’ensemble de données Cityscapes. L’objectif est de classifier chaque pixel d’une image urbaine (route, piéton, véhicule, bâtiment, etc.).

class SegmentationHeadWithAttention(nn.Module):
    """
    Tête de segmentation comparant les performances avec
    et sans bloc non-local pour démontrer l'apport de la
    self-attention.
    """

    def __init__(self, in_channels=2048, num_classes=19,
                 use_attention=True):
        super(SegmentationHeadWithAttention, self).__init__()
        self.use_attention = use_attention

        # Réduction de canaux
        self.reduce = nn.Sequential(
            nn.Conv2d(in_channels, 512, kernel_size=3,
                      padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
        )

        # Bloc non-local optionnel pour capturer le contexte global
        if use_attention:
            self.nonlocal_block = NonLocalBlock(
                in_channels=512,
                reduction=2,
                sub_sample=2
            )

        # Convolution finale de prédiction
        self.classifier = nn.Conv2d(512, num_classes,
                                    kernel_size=1)

    def forward(self, x):
        x = self.reduce(x)
        if self.use_attention:
            x = self.nonlocal_block(x)
        x = self.classifier(x)
        x = F.interpolate(x, scale_factor=8,
                          mode='bilinear',
                          align_corners=True)
        return x

Analyse de l’apport de la self-attention

Sur Cityscapes, les modèles équipés de blocs non-locaux rapportent typiquement une amélioration de +1.5 à +2.5 points de mIoU (mean Intersection over Union) par rapport à leur contrepartie purement convolutive. Les gains sont particulièrement notables pour :

  • Les grandes structures : routes, ciel, bâtiments — objets qui s’étendent sur de grandes portions de l’image
  • Les objets fragmentés : clôtures, poteaux, végétation — où la cohérence sémantique entre parties éloignées est cruciale
  • Les scènes encombrées : rues bondées où la compréhension du contexte global aide à distinguer les piétons des véhicules

Hyperparamètres Clés

Le bloc non-local possède plusieurs hyperparamètres qui influencent significativement ses performances et son coût computationnel :

reduction (facteur de réduction)

Ce paramètre contrôle la dimension interne du bloc non-local, c’est-à-dire le nombre de canaux intermédiaires utilisés pour les projections Q, K et V. Une valeur de reduction=2 est le choix standard et représente un bon compromis entre expressivité et mémoire.

  • reduction=1 : aucune réduction. Maximum d’expressivité mais coût mémoire quadratique. Peut être prohibitif pour des canaux nombreux.
  • reduction=2 : recommandé pour la plupart des applications. Réduit les canaux de moitié, diminuant significativement la mémoire requise pour la matrice d’attention.
  • reduction=4 ou plus : pour les modèles très larges ou les ressources limitées. Perte progressive de capacité de modélisation.
  • reduction=8 : généralement trop agressif. La réduction trop sévère diminue la capacité du réseau à distinguer des relations subtiles entre positions.

sub_sample (sous-échantillonnage spatial)

Le sous-échantillonnage réduit la résolution spatiale sur laquelle on calcule les clés et les valeurs. Avec sub_sample=2, on divise par 4 le nombre de positions (divisé par 2 en hauteur et en largeur).

  • sub_sample=1 : aucune réduction. La matrice d’attention est de taille (H×W) × (H×W). Précis mais coûteux.
  • sub_sample=2 : pool MaxPool2d(2). Réduction de 75 % du nombre de positions. Souvent le meilleur compromis.
  • sub_sample=4 : pool MaxPool2d(4). Pour les résolutions élevées où le plein coût serait prohibitif.

Choix du stade d’intégration

Stade du ResNet Résolution Coût Attention Recommandation
Stade 2 (56×56) Élevée Très élevé Déconseillé sauf ressources abondantes
Stade 3 (28×28) Moyenne Modéré Bon compromis pour certains cas d’usage
Stade 4 (14×14 ou 7×7) Faible Gérable Choix par défaut recommandé

L’intuition derrière ce choix est simple : plus la résolution est faible, plus le nombre de paires de positions à comparer est réduit. À 7×7 (stade 4 d’un ResNet-50), on n’a que 49 positions à comparer deux à deux, soit 2 401 paires — parfaitement gérable en GPU même avec une grande taille de batch.

Avantages et Limites

Avantages majeurs

  1. Champ récepteur global natif : Contrairement aux convolutions qui nécessitent un empilement profond de couches pour élargir progressivement leur champ récepteur, la self-attention atteint une portée globale en une seule opération. C’est un avantage décisif pour les tâches où la compréhension contextuelle est primordiale.
  2. Modélisation explicite des relations à longue distance : Dans une scène urbaine, comprendre qu’une voiture garée à gauche et un piéton traversant à droite appartiennent à la même scène de circulation nécessite une vision globale. La self-attention établit directement ces connexions sémantiques sans avoir besoin de passer par des couches intermédiaires.
  3. Complémentarité naturelle avec les CNN : Les non-local networks ne remplacent pas les convolutions ; ils les complètent. Les convolutions excellent dans l’extraction de caractéristiques locales (bords, textures, motifs), tandis que la self-attention capture la structure globale. Ensemble, elles forment une représentation visuelle riche et à plusieurs échelles.
  4. Flexibilité d’intégration : Le bloc non-local peut être inséré à n’importe quel endroit d’un réseau existant, simplement comme un module supplémentaire avec connexion résiduelle. Aucune modification architecturale profonde n’est nécessaire.
  5. Améliorations documentées : Les travaux originaux de Wang et al. démontrent des améliorations significatives sur la classification d’images (ImageNet), la détection d’objets (COCO), et la segmentation vidéo (YouTube-VOS).

Limites et défis

  1. Coût computationnel quadratique : La complexité O(N²) en fonction du nombre de positions N est le principal défaut. Pour une image de 224×224 pixels, N = 50 176, ce qui donnerait une matrice d’attention de plus de 2,5 milliards d’éléments. Les non-local networks sont donc presque toujours appliqués sur des cartes de caractéristiques à résolution réduite.
  2. Consommation mémoire élevée : La matrice d’attention complète doit être stockée en mémoire pour le calcul du gradient pendant l’entraînement. Pour des cartes intermédiaires de grande taille, cela peut rapidement saturer la mémoire GPU.
  3. Absence de biais inductif spatial : Contrairement aux convolutions qui intègrent naturellement un biais vers la localité spatiale et l’invariance par translation, la self-attention traite toutes les paires de positions de manière identique. Cela peut être un inconvénient lorsque la structure spatiale locale est informative.
  4. Stabilité d’entraînement : L’ajout de couches non-locales peut parfois rendre l’entraînement instable, nécessitant un ajustement du taux d’apprentissage et une initialisation soignée des poids de sortie (d’où l’initialisation à zéro dans notre implémentation).
  5. Interprétabilité partielle : Bien que les cartes d’attention puissent être visualisées pour comprendre quelles positions le réseau considère comme liées, l’interprétation reste difficile dans les couches profondes où les représentations deviennent de plus en plus abstraites.

4 Cas d’Usage Concrets

1. Segmentation sémantique d’images médicales

En imagerie médicale (IRM, scanner, radiographie), les structures anatomiques s’étendent souvent sur des régions étendues de l’image. Un bloc non-local permet au réseau de comprendre qu’une région claire dans le coin supérieur gauche et une autre dans le coin inférieur droit appartiennent au même organe. Cette capacité à établir des connexions globales cohérentes améliore significativement la précision des segmentations, notamment pour les tumeurs dont les bords peuvent être flous et dispersés.

Configuration recommandée : NonLocalBlock avec reduction=2, sub_sample=2, intégré après le dernier bloc convolutif de l’encodeur d’un U-Net. Sur l’ensemble de données BraTS pour la segmentation de tumeurs cérébrales, on observe typiquement une amélioration de +2 % du Dice score.

2. Détection d’objets dans les images satellitaires

Les images satellitaires présentent des particularités uniques : résolution extrêmement élevée, objets de tailles très variées (des minuscules véhicules aux vastes zones agricoles), et besoin de comprendre les relations spatiales à grande échelle (une route qui traverse un paysage, un port maritime connecté à la mer). La self-attention excelle dans ce contexte car elle permet au détecteur de relier des indices visuels éloignés — par exemple, associer une trace de roue sur un chemin à des véhicules situés plusieurs kilomètres plus loin.

Configuration recommandée : Plusieurs blocs non-locaux (sub_sample=2) insérés dans le FPN (Feature Pyramid Network) à différentes échelles, permettant une attention multi-échelle qui capture à la fois les relations locales dans les zones urbaines denses et les relations globales dans les zones rurales.

3. Super-résolution d’images

La super-résolution consiste à reconstruire une image haute résolution à partir d’une version basse résolution. Les méthodes basées sur des convolutions se heurtent à une limitation fondamentale : pour reconstruire un détail précis, elles doivent inférer l’information manquante uniquement à partir du voisinage local. La self-attention brise cette limitation en permettant au modèle de chercher des indices de texture similaires n’importe où dans l’image.

Configuration recommandée : NonLocalBlock avec reduction=4 pour limiter le coût (les réseaux de super-résolution ont souvent de nombreuses couches), intégré dans un réseau RCAN ou EDSR. Sur le benchmark DIV2K, l’ajout d’un bloc non-local améliore le PSNR de +0,15 dB en moyenne.

4. Classification d’actions dans les vidéos

Les vidéos introduisent une dimension supplémentaire : le temps. Un bloc non-local étendu aux trois dimensions spatiales-temporelles (H×W×T) permet de capturer des dépendances spatio-temporelles complexes. Par exemple, pour reconnaître l’action « ouvrir une porte », le réseau doit relier la main qui s’approche de la poignée (t=1) à la porte qui s’ouvre (t=5). La self-attention établit directement ces connexions temporelles sans nécessiter un réseau récurrent ou un empilement de convolutions 3D profondes.

Configuration recommandée : NonLocalBlock3D avec sub_sample=2 sur la dimension temporelle également, intégré dans un réseau I3D ou SlowFast. Sur le jeu de données Kinetics-400, les réseaux équipés de non-local blocks atteignent environ +1,5 % de précision top-1.

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.