Model · Oct 2026 · runs in your browser
MLP in NumPy
The network drawn on the front page. Three weight matrices, ReLU, softmax, and a backward pass written out by hand with no autograd. Trained on MNIST on a laptop CPU; the weights below are the ones running in your browser.
- Test accuracy
- 98.69%
- Parameters
- 109,386
- Weights
- 107 KB, int8
- Training time
- 22 s, CPU
Try it
Waiting for a digit
Inference is a plain JavaScript forward pass over the exported weights, no server and no ML runtime. Your drawing is cropped, scaled to fit 20×20 and centred by its centre of mass in a 28×28 frame, which is how the MNIST digits themselves were prepared.
Training run
Test accuracy sits above training accuracy for the whole run. That is not the model generalising better than it fits: training batches are randomly shifted by up to 2 pixels, the test set is not, so the two numbers are measured on different distributions.
Results
| Metric | Value | Source |
|---|---|---|
| Test accuracy, 10,000 images | 98.69% | training/mlp_numpy/log.csv, epoch 20 |
| Test loss | 0.0435 | training/mlp_numpy/log.csv, epoch 20 |
| Train accuracy (augmented) | 98.39% | training/mlp_numpy/log.csv, epoch 20 |
| Test accuracy after int8 export | 98.65% | public/models/mlp/meta.json |
| Wall-clock training time | 22.3 s | training/mlp_numpy/log.csv, seconds column |
How it works
Forward. Each layer computes h = relu(W·x + b); the last one skips the ReLU and feeds a softmax. 784 inputs, hidden layers of 128 and 64, ten outputs: 109,386 parameters, initialised with He scaling so activations keep their size through the ReLUs.
Backward. Softmax and cross-entropy together have a clean gradient, p − onehot(y). From there each layer hands back dW = δᵀ·h, db = Σδ, and passes δ·W ⊙ [z > 0] to the layer before it. A unit test perturbs every parameter of a small network by ±10⁻⁶ and checks the analytic gradient against the finite difference.
Training. SGD with momentum 0.9, batch 128, learning rate 0.05 on a cosine decay to zero over 20 epochs, weight decay 0.00005 on the weights only, random shifts of up to 2 pixels. Seed 0; rerunning the script reproduces the log exactly.
Export. Weights are quantised to int8 with one scale per output neuron, which cuts the download to a quarter and costs 0.04 points of accuracy.
Reproduce
- Hardware
- 12th Gen Intel(R) Core(TM) i5-12450H, CPU only, NumPy 1.26.4
- Commit
- dc58af1
- Data
- MNIST, 60,000 train / 10,000 test, downloaded and MD5-checked by the script
Check the backward pass
cd training/mlp_numpy && python -m pytest test_gradcheck.pyTrain and export
python training/mlp_numpy/train.py