The problem in brief
A post office reads handwritten digits with scanners, and some scanners are broken. In subtask 1 you get 500 labelled 28 x 28 images from each of three scanners: a normal one (white digit on black), one that inverts the image (black on white), and one that inverts it on half of the image. In subtask 2 you get 500 images from each of eight scanners, the three above plus five strange ones: digits hidden in a colour-blindness (Ishihara) plate, barcodes, braille dots, Roman numerals photographed on paper, and Eastern Arabic numerals. For each subtask you submit one small TorchScript model, and at test time the model does not know which scanner an image came from. The twist is in the score: every parameter of the model costs points.
We scored our final models with the organisers' own pre-judging script on the practice round's test file (2 000 images per scanner). The test labels are not public, so we only report accuracies, never labels. Every design choice was made on a 20 % validation split of the training images.
How you are scored
For each subtask, with the accuracy averaged over scanners (each scanner weighs the same) and the number of parameters:
So 1 point of accuracy is worth exactly 7 500 parameters. In subtask 1 every point of above 90 gives 3 points, up to 30 at ; in subtask 2 every point of above 65 gives 2.8 points, up to 70 at . The starter notebook's baseline scores 1/30 (accuracy 0.91) and 0/70 (accuracy 0.65), according to the statement. There is no public leaderboard to compare with, so our target is simply the 100-point maximum.
First look at the data

- Scanners 2 and 3 are pure geometry. An inverted digit is , and a half-inverted one is on one half. The information is all there; only the colour convention changes.
- Scanners 4 to 8 are different writing systems. A barcode or a braille cell encodes the digit by a pattern that has nothing to do with its shape; the network has to learn a separate "alphabet" for each, from only 500 examples (50 per digit).
- Tiny training sets. 1 500 images for subtask 1 and 4 000 for subtask 2. A digit classifier trained on 1 500 MNIST images typically tops out around 96 to 97 %.
- The scanner identity is not given at test time, so a single model must handle all styles.
A first baseline
A small 3-layer CNN (8, 16 and 24 channels, about 5 000 parameters) with augmentation reproduces roughly the starter's level: on the official test it gets 0.893 accuracy on subtask 1 (, 0 points) and 0.599 on subtask 2 (0 points), with very poor accuracy on the Ishihara (14 %), braille (41 %) and Roman (27 %) scanners. It is too small to learn eight alphabets.
The key idea
Treat parameters as a currency, and spend them where they buy accuracy.
The score turns model design into an exchange rate: 7 500 parameters for 1 point of accuracy. Three consequences guided the solution.
- Depth is cheap, width is expensive. A 3 x 3 convolution from to channels costs parameters: doubling the width quadruples the cost. Adding a thin extra layer, on the other hand, enlarges the receptive field (the network sees the whole digit) for a few thousand parameters. Our validation sweep (figure below) shows it clearly: 3-layer networks need 40 000 parameters to reach 98 %, while 4-layer networks reach 99 % with 12 000.
- Fixed code is free. Only learnable weights are counted, so anything written as a fixed operation inside
forward()costs nothing. For subtask 1 we add an "ink" channel: the absolute difference between each pixel and the average colour of its 9 x 9 neighbourhood, which makes a digit look the same whatever the paper colour. Global average pooling (instead of a large dense layer) is another free saving: the classifier head is just weights. - Augmentation manufactures the broken scanners. For subtask 1 we randomly invert 30 % of the training images and half-invert another 30 % (random split position and direction), so the model sees every digit under every broken-scanner convention; plus small rotations, shifts and scalings for all images.

On the plot, a model is better when it sits on a higher grey line. The best point, 4 layers of 10, 20, 24 and 24 channels with the ink channel (11 890 parameters), reached 0.99 on validation, .
The full solution
- Scale the pixels to ; keep 100 images per scanner for validation.
- Network: blocks of
Conv 3x3 (no bias) + BatchNorm + ReLU, with 2 x 2 max-pooling between blocks, then global average pooling and one linear layer to 10 logits. Subtask 1: widths 10, 20, 24, 24 plus the free ink channel (11 890 parameters). Subtask 2: widths 16, 32, 32, 32 on the raw 3 channels (24 026 parameters); the ink channel and a free colour-contrast channel did not help there. - Augmentation: random affine transform (rotation up to 10 degrees, scale 0.9 to 1.1, shift up to 2 pixels, padding with the paper colour), and for subtask 1 the random inversions and half inversions.
- Training: 60 epochs of AdamW (learning rate with a one-cycle schedule), batch 64, cross-entropy with label smoothing 0.05; then retrain on all training images and export with
torch.jit.script. Each subtask takes 1 to 2 minutes on a CPU.
def normalise(self, x): # fixed code: costs 0 parameters
g = x.mean(1, keepdim=True)
bg = F.avg_pool2d(F.pad(g, (4, 4, 4, 4), mode="replicate"), 9, stride=1)
return torch.cat([x, (g - bg).abs() * 2.0], 1) # "ink" = distance to the local paper colour
Validation was too optimistic. With only 100 images per scanner, the subtask 1 validation accuracy (0.99) overestimated the test accuracy (0.959) by 3 points, i.e. 9 subtask points. A tiny validation set cannot separate models that differ by 1 %, which is exactly the resolution this score needs; repeated or cross-validated splits would have been a better guide.
Results
Official pre-judging script on the practice test file (2 000 images per scanner); models chosen on the validation split.
| model | params | macro accuracy | S | points |
|---|---|---|---|---|
| subtask 1, 3 layers 8-16-24 (baseline level) | 5 098 | 0.893 | 88.6 | 0 / 30 |
| subtask 1, 4 layers 10-20-24-24 + ink (final) | 11 890 | 0.959 | 94.4 | 13 / 30 |
| subtask 2, 3 layers 8-16-24 + ink (baseline level) | 5 242 | 0.599 | 59.2 | 0 / 70 |
| subtask 2, 4 layers 16-32-32-32 (final) | 24 026 | 0.927 | 89.5 | 68 / 70 |
Per scanner, the final subtask 2 model gets between 0.80 (Roman numerals, the hardest: real photos with varied backgrounds and strokes) and 0.99 (barcodes); the Ishihara plates reach 0.88. In total the two models score 81 / 100. Subtask 2 is nearly saturated; the 17 missing points are in subtask 1, where each extra point of accuracy on ordinary and inverted MNIST is worth 3 points, and where more training data per digit (only 150 images per digit across the three scanners) is the real bottleneck.
What to remember
- Read the score as an exchange rate. Here 7 500 parameters = 1 point of accuracy; compute the price of every design decision before making it.
- Depth is cheaper than width. Convolution cost grows with the product of channel counts, so a thin, deeper network gives more accuracy per parameter.
- Fixed preprocessing inside the model is free. Hand-written normalisations, extra input channels and global pooling add knowledge without adding weights.
- Augment towards the test distribution. Generating inverted and half-inverted digits from every image teaches the broken-scanner conventions far better than the 500 examples of each.
- Small validation sets lie at fine resolution. 100 images per scanner cannot tell 96 % from 99 %; when 1 % is worth 3 points, use several splits.