U-Net : Guide complet — Segmentation d’Images par Réseaux en U
Résumé — Le U-Net, introduit par Ronneberger, Fischer et Brox en 2015, est une architecture de réseau neuronal conçue pour la segmentation sémantique d’images. Contrairement à la classification qui attribue une seule étiquette à une image, la segmentation attribue une étiquette à chaque pixel. Le U-Net utilise une architecture en U composée d’un encodeur (contraction) et d’un décodeur (expansion) reliés par des connexions sautantes (skip connections). Son nom vient de sa forme caractéristique en U dans les diagrammes. Il a été conçu originellement pour la segmentation d’images médicales (cellules au microscope) mais est devenu l’architecture de référence pour toute tâche de segmentation.
Principe mathématique
1. Architecture encodeur-décodeur
L’encodeur est une suite de downsampling blocks: chaque block applique deux convolutions 3×3 suivies d’un ReLU puis d’un max pooling 2×2. À chaque étape, le nombre de canaux double et la résolution spatiale est divisée par 2:
Étape encodeur: C, H, W → 2C, H/2, W/2
Le décodeur fait l’inverse: il augmente la résolution via des transposed convolutions (ou upsampling + convolution) et réduit les canaux:
Étape décodeur: 2C, H/2, W/2 → C, H, W
2. Skip connections par concaténation
À chaque niveau du décodeur, les features du décodeur sont concaténées avec les features correspondantes de l’encodeur:
D_l = Conv(Concat(Upsample(D_{l+1}), E_l))
Cette concaténation est cruciale: l’encodeur contient des informations à haute résolution (contours précis, textures fines) qui sont perdues lors du downsampling. Le décodeur les récupère directement plutôt que de devoir les reconstruire à partir d’un résumé flou.
3. Loss de segmentation
La perte la plus utilisée pour la segmentation est le Dice Loss, basé sur le coefficient de Dice:
Dice = 2 · |P ∩ G| / (|P| + |G|)
Dice Loss = 1 - Dice
Où P est l’ensemble des pixels prédits positifs et G l’ensemble des pixels de ground truth. Le Dice est particulièrement adapté aux données déséquilibrées (ex: petites tumeurs dans de grandes images médicales). En pratique, on utilise souvent un mélange:
Loss totale = alpha · BCE + (1 - alpha) · Dice Loss
4. Structure en U typique
Entrée: 3 × 512 × 512
↓ Conv 64 → ReLU → Conv 64 → ReLU → Pool
↓ Conv 128 → ReLU → Conv 128 → ReLU → Pool
↓ Conv 256 → ReLU → Conv 256 → ReLU → Pool
↓ Conv 512 → ReLU → Conv 512 → ReLU → Pool
↓ Conv 1024 → ReLU → Conv 1024 (bottleneck)
↑ Transposed 512 → Concat → Conv 512 × 2
↑ Transposed 256 → Concat → Conv 256 × 2
↑ Transposed 128 → Concat → Conv 128 × 2
↑ Transposed 64 → Concat → Conv 64 × 2
→ Conv 1×1 (sortie: num_classes × 512 × 512)
Intuition
Imaginez un entonnoir. La partie descendante (encodeur) regarde l’image de plus en plus globalement: d’abord les pixels individuels, puis les contours, puis les formes, puis les objets entiers. C’est la compréhension sémantique: « c’est un chat ».
La partie montante (décodeur) fait le chemin inverse: elle prend cette compréhension abstraite et reconstruit la localisation précise de chaque pixel: « le chat est ici, exactement à ces coordonnées, avec ces contours précis ».
Les skip connections sont comme des raccourcis: quand le décodeur doit reconstruire les détails fins (les poils du chat, les moustaches), il peut consulter directement les features de haute résolution de l’encodeur au même niveau, au lieu de tout deviner à partir d’un résumé très abstrait du bottleneck.
Implémentation Python
import torch
import torch.nn as nn
import torch.nn.functional as F
class DoubleConv(nn.Module):
"""2 convolutions 3x3 + ReLU + BatchNorm."""
def __init__(self, in_ch, out_ch):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True)
)
def forward(self, x):
return self.conv(x)
class UNet(nn.Module):
def __init__(self, in_channels=3, num_classes=1, features=[64, 128, 256, 512]):
super().__init__()
self.downs = nn.ModuleList()
self.ups = nn.ModuleList()
self.pool = nn.MaxPool2d(2, 2)
# Encodeur: downsampling
for f in features:
self.downs.append(DoubleConv(in_channels, f))
in_channels = f
# Bottleneck
self.bottleneck = DoubleConv(features[-1], features[-1] * 2)
# Décodeur: upsampling
for f in reversed(features):
self.ups.append(nn.ConvTranspose2d(f * 2, f, 2, stride=2))
self.ups.append(DoubleConv(f * 2, f))
self.final_conv = nn.Conv2d(features[0], num_classes, 1)
def forward(self, x):
skip_connections = []
# Encodeur
for down in self.downs:
x = down(x)
skip_connections.append(x)
x = self.pool(x)
# Bottleneck
x = self.bottleneck(x)
skip_connections = skip_connections[::-1]
# Décodeur
for i in range(0, len(self.ups), 2):
x = self.ups[i](x)
skip = skip_connections[i // 2]
if x.shape != skip.shape:
x = F.interpolate(x, size=skip.shape[2:])
x = torch.cat([skip, x], dim=1)
x = self.ups[i + 1](x)
return self.final_conv(x)
# Dice Loss
def dice_loss(pred, target, smooth=1.0):
pred = torch.sigmoid(pred)
intersection = (pred * target).sum()
return 1 - (2. * intersection + smooth) / (pred.sum() + target.sum() + smooth)
# Entraînement
model = UNet(in_channels=3, num_classes=1)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
model.train()
for images, masks in dataloader:
preds = model(images)
bce = F.binary_cross_entropy_with_logits(preds, masks)
dice = dice_loss(preds, masks)
loss = 0.5 * bce + 0.5 * dice
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Epoch {epoch} | BCE: {bce:.4f} | Dice: {dice:.4f}')
Hyperparamètres
| Hyperparamètre | Valeur typique | Description |
|---|---|---|
| num_classes | 1 (binaire) à 20+ | Nombre de classes de segmentation |
| base_channels | 32 ou 64 | Canaux de la première couche |
| depth | 4 ou 5 | Nombre de niveaux de downsampling |
| dropout | 0.0-0.3 | Dropout dans le bottleneck uniquement |
Avantages
- Peu de données nécessaires : Le U-Net original était entraîné sur seulement 30 images et produisait des résultats excellents grâce à l’augmentation de données et à l’architecture efficace.
- Segmentation précise : Les skip connections préservent les détails fins, permettant une segmentation pixel-parfait des contours.
- Architecture flexible : Adaptable à toute tâche de segmentation (binaire, multi-classes, 3D avec Conv3D).
- Rapide à l’inférence : Une seule passe suffit pour segmenter toute l’image, contrairement aux méthodes par patch.
Limites
- Mémoire : Les skip connections nécessitent de stocker les features de l’encodeur pendant tout le forward pass, ce qui peut être coûteux en mémoire pour de grandes images.
- Taille fixe : Le U-Net classique nécessite des images de taille compatible avec le nombre de downsamplings (divisible par 2^depth). La version adaptable nécessite un padding soigneux.
- Pas de contexte global : Le receptive field est limité par la profondeur. Pour capturer des relations à longue distance, il faut ajouter des mécanismes d’attention.
Évolutions et variantes
Le U-Net a inspiré de nombreuses variantes qui ont étendu ses capacités:
Attention U-Net (2018)
Ajoute des modules d’attention aux skip connections: au lieu de concaténer bêtement les features de l’encodeur, le Attention Gate apprend à pondérer spatialement quelles zones de l’encodeur sont pertinentes pour chaque niveau du décodeur. Résultat: meilleure précision sur les tâches médicales, moins de bruit dans les zones non pertinentes.
U-Net++ (2018)
Introduit des connexions denses entre les skip connections, créant un réseau de raccourcis à plusieurs niveaux. Au lieu d’un seul saut encodeur → décodeur, U-Net++ a des sauts de sauts, permettant une fusion multi-échelle plus riche. Le prix: plus de paramètres et un temps d’entraînement accru.
3D U-Net (2016)
Remplace les convolutions 2D par des convolutions 3D pour traiter des volumes médicaux (scans IRM 3D, CT). La logique est identique mais les opérations sont en 3 dimensions spatiales.
nnU-Net (2020)
« No-new-Net » — nnU-Net est un framework qui configure automatiquement l’architecture U-Net (taille de patch, profondeur, paramètres d’entraînement) en fonction des caractéristiques du dataset. Il a gagné la plupart des challenges de segmentation médicale sans aucune modification architecturale, juste grâce à une configuration optimisée automatiquement.
4 cas d’usage concrets
1. Segmentation de tumeurs en imagerie médicale
Le cas d’usage originel du U-Net. Sur les IRM cérébrales, le U-Net segmente les tumeurs (gliomes) pixel par pixel avec une précision Dice > 0.85, aidant les neurochirurgiens à planifier les interventions.
2. Détection de défauts industriels
Dans la fabrication de semi-conducteurs, le U-Net segmente les défauts microscopiques sur les wafers de silicium. La précision pixel-parfait permet de classifier le type de défaut (rayure, contamination, fissure).
3. Segmentation de scenes autonomes
Les voitures autonomes utilisent des variantes de U-Net pour segmenter la route, les piétons, les véhicules, les feux tricolores en temps réel à partir des caméras.
4. Analyse de satellite
Le U-Net segmente les images satellites pour classifier l’occupation des sols: forêts, zones urbaines, eau, cultures agricoles. Application en surveillance environnementale et urbanisme.
Conclusion
Le U-Net est devenu l’architecture de référence pour la segmentation d’images en moins de 10 ans. Sa combinaison élégante d’encodeur-décodeur avec des skip connections par concaténation est maintenant reproduite dans presque toutes les architectures de segmentation modernes (Attention U-Net, U-Net++, nnU-Net, SegFormer).
Voir aussi
- Multiplier des chaînes de caractères avec Python
- Maîtrisez l’Art du Reverse Engineering en Python : Guide Complet pour Débutants et Experts

