Discord

Checklist NOAI Singapore 2026 Final Assessment · Task 2

SOTA-AI Editorial

The Segmentation Fracture (Debugging U-Net)

Repair skip-connection shape mismatches in a padding-free U-Net and re-implement mean IoU from scratch.

Written
8 October 2026
Result
Forward-pass and metric checks passed on synthetic CPU inputs; no dataset-based or organizer score was measured.
Points
No points score reported; see result for the measured metric.
Olympiad
National Olympiad in Artificial Intelligence (NOAI), Singapore – organised by AI Singapore with NTU, 2026 · Final Assessment

Hints

Hint 1

Track the height and width after every valid convolution, pool, and transposed convolution.

Hint 2

Compare the skip and decoder sizes; compute centered top and left offsets from their difference.

Hint 3

Form per-class boolean masks and calculate intersection with AND and union with OR.

Hint 4

Skip a class only when its union is zero. A false-positive prediction makes the union nonzero even if the class is absent from the target.

The solution in one line

Center-crop the larger skip features for concatenation and average class IoUs computed from the stated intersection-over-union formula.

The editorial

NOAI Singapore 2026 — The Segmentation Fracture (Debugging U-Net)

Problem domain: Computer vision, semantic segmentation, metric implementation

Evaluation metric: 20 marks in the task statement: 10 for repairing the U-Net skip connection and explaining cropping trade-offs, and 10 for implementing mean IoU. A final integrity check has no marks. The released solution notebook labels the architecture answer cell “15 pts,” which conflicts with the task statement's 10-mark Part 1.

Abridged statement

Repair the released padding-free U-Net so encoder skip features can be center-cropped to the decoder dimensions and concatenated. Implement mean intersection over union (mIoU) directly with per-class intersections and unions, skipping a class only when its union is empty. Explain the information loss and alternatives associated with center cropping.

Data analysis

There are no satellite images or trained weights in the released task. The notebook uses a random input tensor with shape (B, 3, 256, 256) and two output classes, Urban and Forest. Its convolutions use valid padding, so each convolution shrinks the spatial dimensions. The local CPU forward check on (1, 3, 256, 256) produced logits of shape (1, 2, 110, 110). The notebook's illustrative comment mentions outputs such as 196 × 196, but the included architecture's actual sequence of valid convolutions yields 110 × 110.

Experiments

  • The local CPU forward pass completed and returned (1, 2, 110, 110) in about 0.6 seconds.
  • For the released “missing class” example, the formal union-based mIoU is 0.375. Class 0 has intersection 3 and union 4, so IoU is 0.75. Class 1 has a false-positive prediction and no target pixels, so its intersection is 0 and its union is 1, giving IoU 0. The mean is (0.75 + 0) / 2 = 0.375.
  • The notebook self-check instead expects 0.75 and says to skip class 1 because “Union = 0.” That statement is false for its provided tensors: the false positive makes class 1's union equal to 1. The expected 0.75 comes from skipping classes absent from the target even when they are predicted, which is not the stated union-based IoU.

Solution

The implementation center-crops the encoder skip tensor using symmetric integer offsets before concatenation. Its default calculate_miou flattens the masks, loops over classes, computes intersections and unions from boolean masks, and skips only empty unions. The function also exposes skip_classes_absent_from_truth=True as an optional compatibility switch for reproducing the notebook's visible 0.75 self-check; the reported result and chosen metric use the default union-based definition.

Score evidence and limits

No mark score was measured. The task statement gives a 20-mark maximum, but there is no organizer-grader result or released hidden test set. The recorded evidence is the local output shape and the explicit calculation above; the contradictory self-check cannot validate the formal metric. The official solution notebook's “15 pts” label for the architecture cell is another scoring inconsistency relative to the task statement's 10-mark allocation, so a full-mark claim is not supported.

Implementation and diagnostics

Each valid 3-by-3 convolution reduces each spatial dimension by two pixels, and pooling and transposed convolution alter the spatial size further. Cropping the larger encoder feature map at its center aligns it with the upsampled decoder map without changing the convolution configuration. The metric computes, for each class, intersection = (prediction_is_class & target_is_class).sum() and union = (prediction_is_class | target_is_class).sum().

Rules and dependencies: The checklist lists PyTorch (torch, torch.nn, torch.nn.functional) and NumPy as allowed, and prohibits scikit-learn, TorchMetrics, and other high-level metric wrappers. The implementation uses PyTorch and writes the metric directly.

Compute and footprint

The checked forward pass ran on CPU and took about 0.6 seconds for one random input. No dataset, pretrained model, GPU score, or training run is required or was measured.

Alternatives considered

  • Adding padding to the convolutions can preserve feature-map sizes, but changes the released padding-free architecture.
  • Interpolation or padding can align feature maps too, but changes how spatial information is represented.
  • Cropping the decoder tensor instead of the encoder skip can align the shapes, but the usual U-Net approach crops the larger skip tensor. Cropping discards border features, so it can lose edge context.
  • Skipping every class absent from the target, even when it has false-positive predictions, reproduces the notebook's erroneous self-check but is not the union-based IoU described in the task.

Sources and reuse

The source does not state a reuse license. This report paraphrases the task and the solution code is independently written; the notebooks are linked rather than copied into this repository.

Code

Solution code solution.py · 76 lines
"""Reference implementation for NOAI Singapore 2026 Programming Task 2."""

from __future__ import annotations

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


class BrokenUNet(nn.Module):
    """Released padding-free U-Net repaired with center-cropped skip features."""

    def __init__(self, in_channels: int = 3, out_classes: int = 2) -> None:
        super().__init__()
        self.enc1_1 = nn.Conv2d(in_channels, 64, kernel_size=3, padding=0)
        self.enc1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=0)
        self.pool1 = nn.MaxPool2d(2)

        self.enc2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=0)
        self.enc2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=0)
        self.pool2 = nn.MaxPool2d(2)

        self.bot1 = nn.Conv2d(128, 256, kernel_size=3, padding=0)
        self.bot2 = nn.Conv2d(256, 256, kernel_size=3, padding=0)

        self.upconv1 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
        self.dec1_1 = nn.Conv2d(256, 128, kernel_size=3, padding=0)
        self.dec1_2 = nn.Conv2d(128, 64, kernel_size=3, padding=0)
        self.final = nn.Conv2d(64, out_classes, kernel_size=1)

    @staticmethod
    def center_crop(layer: torch.Tensor, target_shape) -> torch.Tensor:
        """Center-crop a BCHW skip tensor to the target's spatial dimensions."""
        target_h, target_w = int(target_shape[-2]), int(target_shape[-1])
        height, width = layer.shape[-2:]
        if target_h > height or target_w > width:
            raise ValueError(
                f"target spatial shape {(target_h, target_w)} exceeds source {(height, width)}"
            )
        top = (height - target_h) // 2
        left = (width - target_w) // 2
        return layer[..., top : top + target_h, left : left + target_w]

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x1 = F.relu(self.enc1_1(x))
        x1 = F.relu(self.enc1_2(x1))
        x1_p = self.pool1(x1)

        x2 = F.relu(self.enc2_1(x1_p))
        x2 = F.relu(self.enc2_2(x2))
        x2_p = self.pool2(x2)

        bot = F.relu(self.bot1(x2_p))
        bot = F.relu(self.bot2(bot))

        d1 = self.upconv1(bot)
        x2_cropped = self.center_crop(x2, d1.shape)
        cat1 = torch.cat((x2_cropped, d1), dim=1)

        dec1 = F.relu(self.dec1_1(cat1))
        dec1 = F.relu(self.dec1_2(dec1))
        return self.final(dec1)


def calculate_miou(
    pred_mask: torch.Tensor,
    true_mask: torch.Tensor,
    num_classes: int = 2,
    *,
    skip_classes_absent_from_truth: bool = False,
) -> float:
    """Mean class IoU, skipping empty unions, per task definition.

    ``skip_classes_absent_from_truth=True`` reproduces the released notebook's
    contradictory self-check, which skips a class whenever it is absent from
    the target even when it has false-positive predictions. That convention is
    not the written union-based IoU, so the default remains the formal metric.
    """
    if pred_mask.shape != true_mask.shape:
        raise ValueError("pred_mask and true_mask must have identical shapes")
    pred_flat = pred_mask.reshape(-1)
    true_flat = true_mask.reshape(-1)
    ious: list[float] = []

    for cls in range(num_classes):
        pred_cls = pred_flat == cls
        true_cls = true_flat == cls
        if skip_classes_absent_from_truth and not bool(true_cls.any()):
            continue
        intersection = (pred_cls & true_cls).sum().item()
        union = (pred_cls | true_cls).sum().item()
        if union:
            ious.append(intersection / union)

    return float(sum(ious) / len(ious)) if ious else 0.0

Download solution.py

Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.