Skip to content

U-Net Cookbook

This page collects practical U-Net recipes that build on the synthetic demo and tested UNet2D implementation. For the architecture walkthrough, start with the U-Net overview. For the complete implementation, read the full code.

All examples on this page use synthetic tensors unless a public, properly licensed dataset is explicitly configured. Do not add private medical images, PHI, patient identifiers, DICOM headers, or clinical data.

The examples use torch.randn for synthetic image-like tensors, torch.randint for synthetic masks, UNet2D for the model, model.eval() for inference-style examples, and torch.no_grad() when gradients are not needed.

Check Yourself

Before running each recipe, predict the output shape and decide whether the example needs sigmoid, softmax, or no activation. If an example uses a loss, identify whether the target should be shaped like (B, 1, H, W) or (B, H, W).

Binary Segmentation With One Output Channel

Use out_channels=1 when the model should produce one foreground-vs-background logit map per image. The output shape is (B, 1, H, W).

import torch

from medseg_architectures import UNet2D

model = UNet2D(in_channels=1, out_channels=1, features=(8, 16))
model.eval()

x = torch.randn(2, 1, 48, 64)
with torch.no_grad():
    logits = model(x)
    probabilities = torch.sigmoid(logits)

assert logits.shape == (2, 1, 48, 64)
assert probabilities.shape == logits.shape

During training, pass the raw logits to a logits-aware binary loss such as torch.nn.BCEWithLogitsLoss.

Multiclass Segmentation With N Output Channels

Use out_channels=N when each pixel belongs to exactly one of N classes. The model returns one raw logit map per class, so the output shape is (B, N, H, W).

import torch

from medseg_architectures import UNet2D

num_classes = 4
model = UNet2D(in_channels=1, out_channels=num_classes, features=(16, 32, 64))
model.eval()

x = torch.randn(2, 1, 64, 64)
with torch.no_grad():
    logits = model(x)
    probabilities = torch.softmax(logits, dim=1)

assert logits.shape == (2, num_classes, 64, 64)
assert probabilities.shape == logits.shape

During training, pass multiclass logits directly to a loss such as torch.nn.CrossEntropyLoss, with target masks shaped (B, H, W).

Choose in_channels From The Input Tensor

Set in_channels to match the channel dimension of the tensor you feed to the model. In PyTorch image layout, the input shape is (B, C, H, W).

import torch

from medseg_architectures import UNet2D

grayscale = torch.randn(2, 1, 64, 64)
rgb = torch.randn(2, 3, 64, 64)
multi_modal = torch.randn(2, 4, 64, 64)

gray_model = UNet2D(in_channels=1, out_channels=2)
rgb_model = UNet2D(in_channels=3, out_channels=2)
multi_modal_model = UNet2D(in_channels=4, out_channels=2)

gray_model.eval()
rgb_model.eval()
multi_modal_model.eval()

with torch.no_grad():
    gray_logits = gray_model(grayscale)
    rgb_logits = rgb_model(rgb)
    multi_modal_logits = multi_modal_model(multi_modal)

assert gray_logits.shape == (2, 2, 64, 64)
assert rgb_logits.shape == (2, 2, 64, 64)
assert multi_modal_logits.shape == (2, 2, 64, 64)

A multi-modal tensor can stack modalities along the channel dimension, such as four aligned synthetic channels shaped (B, 4, H, W).

Choose Feature Widths

The features sequence controls encoder widths from shallow to deep. Larger values usually increase capacity, parameter count, and memory use.

from medseg_architectures import UNet2D

tiny = UNet2D(in_channels=1, out_channels=2, features=(8, 16))
baseline = UNet2D(in_channels=1, out_channels=2, features=(16, 32, 64))
wider = UNet2D(in_channels=1, out_channels=2, features=(32, 64, 128))
  • features=(8, 16): fastest tiny model for CPU-friendly experiments.
  • features=(16, 32, 64): default educational baseline used by the demo.
  • features=(32, 64, 128): wider model with higher memory and compute cost.

Keep the sequence shallow-to-deep and use positive integers only.

Keep Logits Separate From Probabilities

UNet2D returns raw logits instead of applying sigmoid or softmax inside the model. This keeps the model compatible with stable PyTorch losses that combine activation and loss computation.

import torch

from medseg_architectures import UNet2D

x = torch.randn(2, 1, 32, 32)

binary_model = UNet2D(in_channels=1, out_channels=1, features=(8, 16))
multiclass_model = UNet2D(in_channels=1, out_channels=3, features=(8, 16))
binary_model.eval()
multiclass_model.eval()

with torch.no_grad():
    binary_logits = binary_model(x)
    binary_probabilities = torch.sigmoid(binary_logits)
    multiclass_logits = multiclass_model(x)
    multiclass_probabilities = torch.softmax(multiclass_logits, dim=1)

assert binary_probabilities.shape == (2, 1, 32, 32)
assert multiclass_probabilities.shape == (2, 3, 32, 32)

Use probabilities for interpretation, thresholds, or metrics. Keep raw logits for losses such as BCEWithLogitsLoss and CrossEntropyLoss.

Handle Odd Input Sizes

Repeated pooling and upsampling can create one-pixel mismatches for odd spatial sizes. The implementation aligns decoder tensors to skip tensor sizes, so the final logits preserve the input height and width.

import torch

from medseg_architectures import UNet2D

model = UNet2D(in_channels=1, out_channels=2, features=(16, 32, 64))
model.eval()

x = torch.randn(1, 1, 65, 73)
with torch.no_grad():
    logits = model(x)

assert logits.shape == (1, 2, 65, 73)

This is useful for synthetic checks and real preprocessing pipelines where image sizes are not always divisible by every pooling level.

Compute Simple Dice And IoU

These tiny metric helpers work on synthetic binary tensors. They threshold binary probabilities into predicted masks before computing overlap. They compute one pooled value for the whole synthetic batch, so use them only as a shape and formula sketch.

import torch

logits = torch.randn(2, 1, 32, 32)
target = torch.randint(0, 2, (2, 1, 32, 32)).float()

pred = (torch.sigmoid(logits) > 0.5).float()
eps = 1e-6

intersection = (pred * target).sum()
pred_sum = pred.sum()
target_sum = target.sum()
union = ((pred + target) > 0).float().sum()

dice = (2 * intersection + eps) / (pred_sum + target_sum + eps)
iou = (intersection + eps) / (union + eps)

assert 0.0 <= dice <= 1.0
assert 0.0 <= iou <= 1.0

For multiclass experiments, compute per-class masks first, then apply the same overlap idea class by class. For real experiments, report per-case and per-class values, state the empty-mask convention, and choose thresholds on validation data rather than on the test set.

Run A Synthetic Forward Pass

This compact forward pass mirrors the repository demo: one synthetic grayscale batch enters the model, and the output keeps the same batch size, height, and width while changing the channel count to out_channels.

import torch

from medseg_architectures import UNet2D

torch.manual_seed(7)

model = UNet2D(in_channels=1, out_channels=2, features=(16, 32, 64))
model.eval()

x = torch.randn(1, 1, 65, 73)
with torch.no_grad():
    logits = model(x)

assert logits.shape == (1, 2, 65, 73)