Discord

Checklist OAI 2026 Final (Stage III) · Task 3

SOTA-AI Editorial

Pixels

Classify MNIST digits from masked images in which only 10 pixels are visible, using a CNN trained on full images.

Written
3 October 2026
Released validation result
63.45% accuracy
Estimated points
100/100
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

Confirm how many visible pixels each example has and plot them by class.

Hint 2

Estimate where each digit class tends to place visible pixels.

Hint 3

Smooth those spatial patterns to tolerate small handwriting shifts.

Hint 4

Thicken the points, extract pretrained CNN features at several scales, and blend their scores with a spatial likelihood.

The solution in one line

Combine class-specific spatial likelihoods with an RBF SVC over raw/dilated CNN features and the CNN’s 5×5-view logits.

The editorial

Dataset analysis and EDA

The training archive has 500 images and the validation archive has 2,000. Every image inspected has exactly ten nonzero visible pixels. The local NPZ files contain images and labels; the notebook text also mentions an originals field, but it is absent from these files and the solution does not rely on it. The training label counts for digits 0–9 are [57, 53, 43, 59, 32, 40, 45, 67, 47, 57]; validation counts are [180, 223, 210, 214, 190, 185, 199, 207, 184, 208].

The visible pixel coordinates carry most of the shape information. I compared class-conditional spatial maps and treated the input as a small point set. A direct full-image CNN prediction has a substantial input-distribution change, but local max filtering makes those points resemble thicker digit strokes and lets the supplied model contribute useful features.

Solution strategy and experiments

The final rule combines spatial evidence with the supplied CNN:

  1. Smoothed class likelihood: sum the visible training intensities by class and pixel location, smooth each 28×28 map with a Gaussian filter (σ=0.9), add a small pseudocount, and score the query’s visible pixels by log likelihood. Query intensities are square-root weighted.
  2. Blurred-mask RBF SVC: blur the sparse image with σ=1.5 and classify it with an RBF SVC (C=10).
  3. CNN feature SVC: apply the loaded CNN to the raw, 3×3 max-filtered, and 5×5 max-filtered image. Concatenate the three 128-dimensional penultimate activations and train an RBF SVC (C=1). Use the 5×5-view CNN logits as a fourth score.

Standardize each ten-class score vector per image and combine them as density + 0.4 × blurred_SVC + 1.25 × CNN_feature_SVC + 0.3 × 5x5_CNN_logits.

Method Validation accuracy
Raw-pixel RBF SVC 36.7%
Raw-pixel multinomial Naive Bayes 40.9%
Gaussian-smoothed spatial likelihood 56.6%
Supplied CNN on raw sparse image 19.3%
Supplied CNN on 5×5 max-filtered image 44.9%
RBF SVC on CNN features from raw, 3×3, and 5×5 views 59.7%
Spatial likelihood + blurred-mask SVC + CNN-feature SVC + CNN logits 63.45%

The combined result clears the notebook’s 60% full-credit threshold. The local Python runner did not have PyTorch, so I loaded the supplied state dict and reproduced the network’s Conv/ReLU/Pool/Linear forward pass in NumPy for these experiments. I did not cross-check numerical parity against PyTorch in this runner; the reference notebook implementation uses the provided PyTorch model directly.

Implementation and compute footprint

The classifiers are fit once, lazily, from train_imgs and train_labels, then reused for each test image. They use CPU: one RBF SVC on 500 vectors of length 784 and another on 500 concatenated 384-feature CNN representations. Each query runs three CNN views together. The NumPy experiment over the released validation set completed in under 20 seconds; the final notebook’s PyTorch runtime was not timed in this runner. The secret test set was not available.

Other approaches worth testing include point-cloud nearest neighbors, pairwise-distance features, sparse-mask augmentation, and alternate max-filter sizes. Point-cloud Chamfer k-NN reached about 50% on this validation set; pairwise displacement histograms did not beat the selected blend.

Code

Solution code solution.py · 92 lines
import numpy as np
import torch

# Fit once from the labeled sparse training set, then reuse the models for all
# validation/test images. This combines spatial statistics with the supplied CNN.
_pixel_models_cache = None

def _extract_pixel_cnn_features(images, model):
    """Return the penultimate CNN activations for a batch of sparse images."""
    device = next(model.parameters()).device
    chunks = []
    with torch.no_grad():
        for start in range(0, len(images), 128):
            batch = torch.as_tensor(images[start:start + 128], dtype=torch.float32, device=device)
            batch = (batch - 0.1307) / 0.3081
            chunks.append(model.net[:9](batch).cpu().numpy())
    return np.concatenate(chunks, axis=0)

def _get_pixel_models():
    global _pixel_models_cache
    if _pixel_models_cache is not None:
        return _pixel_models_cache

    from scipy.ndimage import gaussian_filter, maximum_filter
    from sklearn.svm import SVC

    if hasattr(train_imgs, "detach"):
        train_x = train_imgs.detach().cpu().numpy().astype(np.float32)[:, 0]
        train_y = train_labels.detach().cpu().numpy().astype(int)
    else:
        train_x = np.asarray(train_imgs, dtype=np.float32)[:, 0]
        train_y = np.asarray(train_labels, dtype=int)

    # Class-conditional spatial density of visible pixels. Smooth the maps to
    # tolerate the one-pixel shifts common in handwritten digits.
    log_density = []
    for cls in range(10):
        class_images = train_x[train_y == cls]
        counts = class_images.sum(axis=0)
        counts = gaussian_filter(counts, sigma=0.9) + 1e-3
        density = counts / counts.sum()
        log_density.append(np.log(density).reshape(-1))
    log_density = np.asarray(log_density, dtype=np.float32)

    # An RBF SVC on blurred sparse masks captures local point geometry.
    blurred_train = np.stack([gaussian_filter(im, sigma=1.5) for im in train_x])
    pixel_svm = SVC(C=10, kernel="rbf", gamma="scale", decision_function_shape="ovr")
    pixel_svm.fit(blurred_train.reshape(len(blurred_train), -1), train_y)

    # The CNN was trained on complete digits. Max-filter views make the sparse
    # observations look more like connected strokes; train a small SVC on its
    # penultimate features from raw, 3x3-dilated, and 5x5-dilated views.
    dilated3 = np.stack([maximum_filter(im, size=3) for im in train_x])[:, None]
    dilated5 = np.stack([maximum_filter(im, size=5) for im in train_x])[:, None]
    cnn_views = [train_x[:, None], dilated3, dilated5]
    hidden_views = [_extract_pixel_cnn_features(view, model) for view in cnn_views]
    hidden_train = np.concatenate(hidden_views, axis=1)
    hidden_svm = SVC(C=1, kernel="rbf", gamma="scale", decision_function_shape="ovr")
    hidden_svm.fit(hidden_train, train_y)

    _pixel_models_cache = (log_density, pixel_svm, hidden_svm)
    return _pixel_models_cache

def _standardize_pixel_scores(scores):
    return (scores - scores.mean(axis=1, keepdims=True)) / (
        scores.std(axis=1, keepdims=True) + 1e-9
    )

def solution(img):
    """Classify a 28x28 MNIST image showing only ten visible pixels."""
    from scipy.ndimage import gaussian_filter, maximum_filter

    if hasattr(img, "detach"):
        image = img.detach().cpu().numpy().astype(np.float32)
    else:
        image = np.asarray(img, dtype=np.float32)
    image = np.squeeze(image)

    if "train_imgs" not in globals() or "train_labels" not in globals():
        batch = torch.as_tensor(image, dtype=torch.float32).reshape(1, 1, 28, 28)
        predicted, _ = predict(batch, model)
        return int(predicted[0].item())

    log_density, pixel_svm, hidden_svm = _get_pixel_models()
    flat = np.maximum(image, 0).reshape(1, -1)
    density_scores = np.sqrt(flat) @ log_density.T

    blurred = gaussian_filter(image, sigma=1.5).reshape(1, -1)
    pixel_svm_scores = pixel_svm.decision_function(blurred)

    dilated3 = maximum_filter(image, size=3)
    dilated5 = maximum_filter(image, size=5)
    views = np.stack([image, dilated3, dilated5])[:, None]
    device = next(model.parameters()).device
    with torch.no_grad():
        batch = torch.as_tensor(views, dtype=torch.float32, device=device)
        batch = (batch - 0.1307) / 0.3081
        hidden = model.net[:9](batch)
        cnn_scores = model.net[9](hidden[2:3]).cpu().numpy()
        hidden_features = hidden.cpu().numpy().reshape(1, -1)
    hidden_svm_scores = hidden_svm.decision_function(hidden_features)

    scores = (
        _standardize_pixel_scores(density_scores)
        + 0.4 * _standardize_pixel_scores(pixel_svm_scores)
        + 1.25 * _standardize_pixel_scores(hidden_svm_scores)
        + 0.3 * _standardize_pixel_scores(cnn_scores)
    )
    return int(np.argmax(scores[0]))

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.