Discord

Checklist OAI 2026 Final (Stage III) · Task 1

SOTA-AI Editorial

Decision Localisation

Produce saliency heatmaps for a fixed binary ResNet-18 classifier that overlap as much as possible with the COCO segmentation mask of the detected object.

Written
3 October 2026
Released validation result
Not measured; the image archive and model are not present in this workspace
Estimated points
Unverified
Olympiad
Polish Olympiad in Artificial Intelligence (Olimpiada Sztucznej Inteligencji), 2026 · Final (Stage III)

Measured results use the released validation sets and the evaluator logic in each notebook. They do not establish secret-test performance.

Hints

Hint 1

Which internal model layer still preserves image location?

Hint 2

How can the positive logit’s gradient identify influential channels?

Hint 3

How should the coarse activation map be resized and normalized?

The solution in one line

Use the positive-class gradient to weight the final convolutional maps, then upsample and normalize the Grad-CAM heatmap.

The editorial

Dataset analysis and EDA

The notebook describes 1,200 validation images from a COCO2017 subset, each resized to 224×224, along with masks and a binary ResNet-18 classifier. The hidden test set has the same size and format. Its loader uses ImageNet normalization and the model exposes a final convolutional feature map through model.encoder.

The supplied image archive and model checkpoint were not present in the workspace, so I could not inspect mask coverage, overlay examples, or measure a validation IoU. With those files available, the useful checks are mask area and shape distributions, image/mask overlays, positive-class logits, and whether activation peaks fall on the object or its context.

Solution strategy

Use Grad-CAM on the last ResNet block. For feature map A and positive-class logit s, compute channel weights as the spatial mean of ∂s/∂A; take the ReLU of their weighted feature-map sum; then bilinearly resize and normalize it to the input dimensions. The notebook evaluator thresholds returned values at 0.5.

The evaluator wraps the solution in torch.no_grad(), so the implementation explicitly re-enables gradients inside the function. It uses one forward pass and one gradient calculation per image and does not update model weights.

Result and diagnostics

The implementation is in the notebook, but its IoU and score remain unverified until the Google Drive files referenced by the notebook are available. There is also a scoring-text inconsistency in the starter notebook: its prose says IoU ≤0.25 earns zero, while the executable scoring function maps mean IoU 0.191 to 0 points and 0.245 to 100 points, clamped to that range. The implementation should be judged with the executable evaluator unless the organizers clarify the prose.

Compute and alternatives

Grad-CAM needs one forward/backward pass per image; the notebook allows GPU execution and a three-minute limit. Alternatives to compare on the released validation set are Grad-CAM++, LayerCAM, and occlusion maps. Occlusion can require many more forward passes. None of these comparisons was measured without the archive and checkpoint.

Code

Solution code solution.py · 25 lines
import torch
import torch.nn.functional as F

# Grad-CAM over the final ResNet block.  The returned map is normalized to [0, 1];
# the evaluator thresholds it at 0.5.
def your_solution(img, model):
    import torch.nn.functional as F

    _, H, W = img.shape
    device = next(model.parameters()).device
    model.eval()

    # The evaluator wraps this function in no_grad(), so explicitly re-enable
    # autograd. Requiring an input gradient also keeps the graph when model
    # parameters have been frozen.
    with torch.enable_grad():
        x = img.detach().unsqueeze(0).to(device).requires_grad_(True)
        feature_map = model.encoder(x)
        pooled = model.pool(feature_map).flatten(1)
        logit = model.head(pooled).reshape(-1)[0]
        gradient = torch.autograd.grad(logit, feature_map)[0]

        channel_weights = gradient.mean(dim=(2, 3), keepdim=True)
        cam = torch.relu((channel_weights * feature_map).sum(dim=1, keepdim=True))
        cam = F.interpolate(cam, size=(H, W), mode="bilinear", align_corners=False)[0, 0]
        cam = torch.nan_to_num(cam)
        lo, hi = cam.min(), cam.max()
        cam = (cam - lo) / (hi - lo + 1e-8)

    return cam.detach()

Download solution.py

Written by Gonçalo Franco; added here on 4 October 2026. Editorials are written by members of the SOTA community, not by the olympiad. The official statement and scoring win wherever an editorial differs.