Elliot Sones.Machine Learning, from scratch

Convolutional Neural Network.

97.88%
Part 2 of 4 · Two-digit classification (0-99)

Same task, harder version: one hundred classes instead of ten. Convolutions written from scratch in NumPy first, then the same model in PyTorch to compare, which makes it very clear what the framework is actually doing for you.

Write a two-digit number below and see how it reads it.

Goal
Scale digit recognition to two-digit numbers (0-99)
Dataset
Paired-MNIST 28×56 images (concatenated digits, 00-99 labels)
Architecture
Conv(3×3,16) → ReLU → MaxPool → Conv(3×3,32) → ReLU → MaxPool → FC(256) → Dropout(0.4) → FC(100)
Result
97.88% test accuracy (10,000 samples)
Try it live
Draw a two-digit number from 00 to 99 · the actual model, running right here in your browser
loading weights · 3.2 mb
Prediction
··
Model input · 28×56
··
··
··
··
··
Top 5 of 100 classes · averaged over 7 augmented views
How I built it
MNIST-100: 28×56 images of handwritten digits 0-99, made by concatenating MNIST pairs
MNIST-100: 28×56 images of handwritten digits 0-99, made by concatenating MNIST pairs

Scale the MLP approach to recognizing two-digit numbers using convolutional neural networks.

Why CNNs?

CNNs are different from MLPs because they have convolutional layers designed to recognize spatial patterns in images.

Convolutional layers building feature maps
Convolutional layers building feature maps

Kernels (small 3×3 filters) slide across the image, building a feature map that shows where certain visual patterns occur.

Max pooling keeps only the strongest signals
Max pooling keeps only the strongest signals

Pooling layers (usually MaxPool) reduce spatial dimensions, keeping only the strongest signals. Activation functions (ReLU, GELU) determine which patterns actually matter. After spatial feature extraction, the data is flattened and passed into fully connected layers, just like an MLP.

Through deeper layers, CNNs build increasingly complex representations: from simple edges to high-level features.

Process

The MLP baseline

First I tested the MLP from the previous section:

  • ~99% train vs ~87% dev, which is clear overfitting
  • The MLP treats pixels independently and lacks spatial bias, so small shifts degraded performance
Training loss over epochs
Training loss over epochs
Train vs dev accuracy: a widening gap
Train vs dev accuracy: a widening gap
Generalization gap (train minus dev)
Generalization gap (train minus dev)

The CNN implementation

  • Data & normalization: scale to [0,1], standardize with training-set mean/std
  • Augmentation: random horizontal shifts (±2 px), mild contrast/brightness jitter
  • Optimization: Adam (lr=1e-3), He init, L2 regularization (λ=1e-4), batch size 256, up to 20 epochs
  • Early stopping: patience=5, min_delta=1e-3 on dev accuracy
  • From-scratch ops: NumPy-only conv via im2col/col2im, max-pooling, dropout, vectorized softmax cross-entropy, full backward pass

Results

Test accuracy: 97.88% (10,000 samples)

Hardest classes: 29 (90.2%), 97 (93.4%), 39 (93.8%), 33 (94.1%), 70 (94.1%)

CNN training loss
CNN training loss
Train vs dev accuracy
Train vs dev accuracy
Generalization gap (train minus dev)
Generalization gap (train minus dev)