PyTorch · ONNX Runtime · WebAssembly
I built this to actually understand the full pipeline — not just run someone else's notebook. The weights you're running right now came out of my own training loop. No server, no API call. Just WebAssembly in your browser.
Try it
Model Architecture
Why I built this
Most ML tutorials hand you a pre-trained model and a notebook. You run the cells, get 99% accuracy, and learn almost nothing about what actually happened. I wanted to go through the full pipeline myself — from writing the training loop to serving inference in a browser — so I could understand where each decision matters and where things break.
The goal was not to build the best MNIST classifier. It was to build a complete system I can reason about — training, export, preprocessing, and runtime — with no hidden steps.
Preprocessing pipeline
The preprocessing step is where most browser-based ML demos silently break. If the normalization in your browser code doesn't exactly match your training pipeline, the model sees a completely different input distribution and confidence scores become meaningless.
My PyTorch training uses Normalize((0.1307,), (0.3081,)) — the MNIST dataset's channel mean and standard deviation. In the browser, every drawing goes through the same transform: bounding box crop, scale to 20px on the longest side, center in a 28×28 canvas with 4px padding, then (pixel / 255 − 0.1307) / 0.3081 per pixel.
Getting this right required debugging with side-by-side comparisons between the Python and JavaScript preprocessing output. A common mistake is forgetting the centering step — MNIST digits are centered in their bounding boxes, and the model expects that.
Failure cases & limitations
The model is strong on clean, centered digits but has predictable failure modes. Understanding where it breaks is more interesting than the accuracy number.
A model's failure modes tell you more than its accuracy. 99% on the test set sounds impressive until you realize the test set comes from the same distribution as the training data. Real-world input — messy mouse drawings — is always harder. The gap between benchmark accuracy and deployed reliability is where engineering judgment matters most.
What I'd improve next
I intentionally kept this project small. The point was not to build the most accurate digit classifier — it was to own every step of the pipeline and know exactly where the tradeoffs are. A more complex model would improve accuracy but would obscure the learning. I'd rather ship something I fully understand.
How it runs in your browser
Trained from scratch in PyTorch on the MNIST dataset
I added data augmentation after noticing the baseline model struggled with slightly off-center digits. Random rotation and translation helped generalize to messier handwriting.
Exported to ONNX, then run via WebAssembly — no server needed
The whole inference pipeline runs on your device. The model file is 4.6 MB — loaded once, cached by the browser. Drawing-to-prediction latency is under 5 ms on most machines.