U-Net: Image Segmentation with U-Shaped Networks

U-Net : Guide Complet — Segmentation d'Images par Réseaux en U

U-Net: Complete Guide — Image Segmentation with U-Shaped Networks

Summary — U-Net, introduced by Ronneberger, Fischer, and Brox in 2015, is a neural network architecture designed for semantic image segmentation. Unlike classification, which assigns a single label to an image, segmentation assigns a label to every pixel. U-Net uses a U-shaped architecture composed of an encoder (contraction) and a decoder (expansion) connected by skip connections. Its name comes from its characteristic U shape in diagrams. It was originally designed for medical image segmentation (cells under a microscope) but has become the reference architecture for all segmentation tasks.


Mathematical Principle

1. Encoder-Decoder Architecture

The encoder is a sequence of downsampling blocks: each block applies two 3×3 convolutions followed by a ReLU then a 2×2 max pooling. At each step, the number of channels doubles and the spatial resolution is divided by 2:

Encoder step: C, H, W → 2C, H/2, W/2

The decoder does the opposite: it increases resolution via transposed convolutions (or upsampling + convolution) and reduces channels:

Decoder step: 2C, H/2, W/2 → C, H, W

2. Skip Connections by Concatenation

At each level of the decoder, the decoder features are concatenated with the corresponding encoder features:

D_l = Conv(Concat(Upsample(D_{l+1}), E_l))

This concatenation is crucial: the encoder contains high-resolution information (precise contours, fine textures) that is lost during downsampling. The decoder recovers it directly rather than having to reconstruct it from a blurry summary.

3. Segmentation Loss

The most commonly used loss for segmentation is Dice Loss, based on the Dice coefficient:

Dice = 2 · |P ∩ G| / (|P| + |G|)
Dice Loss = 1 - Dice

Where P is the set of predicted positive pixels and G is the set of ground truth pixels. Dice is particularly well-suited for imbalanced data (e.g., small tumors in large medical images). In practice, a mixture is often used:

Total Loss = alpha · BCE + (1 - alpha) · Dice Loss

4. Typical U-Shaped Structure

Input: 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 (output: num_classes × 512 × 512)

Intuition

Imagine a funnel. The descending part (encoder) looks at the image more and more globally: first individual pixels, then contours, then shapes, then entire objects. This is semantic understanding: “it’s a cat.”

The ascending part (decoder) does the reverse: it takes this abstract understanding and reconstructs the precise location of each pixel: “the cat is here, exactly at these coordinates, with these precise contours.”

Skip connections are like shortcuts: when the decoder needs to reconstruct fine details (the cat’s fur, the whiskers), it can directly consult the high-resolution features from the encoder at the same level, instead of having to guess everything from a very abstract summary of the bottleneck.


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)

        # Encoder: downsampling
        for f in features:
            self.downs.append(DoubleConv(in_channels, f))
            in_channels = f

        # Bottleneck
        self.bottleneck = DoubleConv(features[-1], features[-1] * 2)

        # Decoder: 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 = []
        # Encoder
        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]
        # Decoder
        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)


# Training
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}')

Hyperparameters

Hyperparameter Typical Value Description
num_classes 1 (binary) to 20+ Number of segmentation classes
base_channels 32 or 64 Channels in the first layer
depth 4 or 5 Number of downsampling levels
dropout 0.0-0.3 Dropout in the bottleneck only

Advantages

  1. Low data requirements: The original U-Net was trained on only 30 images and produced excellent results thanks to data augmentation and the efficient architecture.
  2. Precise segmentation: Skip connections preserve fine details, enabling pixel-perfect contour segmentation.
  3. Flexible architecture: Adaptable to any segmentation task (binary, multi-class, 3D with Conv3D).
  4. Fast inference: A single pass is enough to segment the entire image, unlike patch-based methods.

Limitations

  1. Memory: Skip connections require storing encoder features during the entire forward pass, which can be memory-intensive for large images.
  2. Fixed size: The classic U-Net requires images of a size compatible with the number of downsamplings (divisible by 2^depth). The adaptive version requires careful padding.
  3. No global context: The receptive field is limited by depth. To capture long-range relationships, attention mechanisms must be added.

Evolutions and Variants

U-Net has inspired numerous variants that have extended its capabilities:

Attention U-Net (2018)

Adds attention modules to skip connections: instead of naively concatenating encoder features, the Attention Gate learns to spatially weight which areas of the encoder are relevant for each decoder level. Result: better accuracy on medical tasks, less noise in irrelevant areas.

U-Net++ (2018)

Introduces dense connections between skip connections, creating a network of multi-level shortcuts. Instead of a single encoder → decoder skip, U-Net++ has skips of skips, enabling richer multi-scale fusion. The cost: more parameters and increased training time.

3D U-Net (2016)

Replaces 2D convolutions with 3D convolutions to process medical volumes (3D MRI scans, CT). The logic is identical but the operations are in 3 spatial dimensions.

nnU-Net (2020)

“No-new-Net” — nnU-Net is a framework that automatically configures the U-Net architecture (patch size, depth, training parameters) based on dataset characteristics. It won most medical segmentation challenges without any architectural modifications, purely through automatically optimized configuration.


4 Concrete Use Cases

1. Tumor Segmentation in Medical Imaging

The original use case for U-Net. On brain MRIs, U-Net segments tumors (gliomas) pixel by pixel with Dice accuracy > 0.85, helping neurosurgeons plan interventions.

2. Industrial Defect Detection

In semiconductor manufacturing, U-Net segments microscopic defects on silicon wafers. Pixel-perfect precision enables classification of defect type (scratch, contamination, crack).

3. Autonomous Scene Segmentation

Autonomous cars use U-Net variants to segment the road, pedestrians, vehicles, and traffic lights in real time from camera feeds.

4. Satellite Analysis

U-Net segments satellite images to classify land use: forests, urban areas, water, agricultural crops. Applications in environmental monitoring and urban planning.


Conclusion

U-Net has become the reference architecture for image segmentation in less than 10 years. Its elegant combination of encoder-decoder with skip connections by concatenation is now reproduced in almost all modern segmentation architectures (Attention U-Net, U-Net++, nnU-Net, SegFormer).


See Also


About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →