Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

✏️ Handwritten Digit Recognizer

Draw a digit in your browser and a neural network trained on MNIST reads it back — scikit-learn MLPClassifier + Streamlit.

Problem Statement

Reading handwritten digits is the classic first real computer-vision problem: the same digit looks different in every hand, so there is no simple rule that separates a sloppy 4 from a sloppy 9. This project trains a small neural network to classify 28x28 images of digits, then wraps it in a web app where anyone can draw a digit with the mouse and get a live prediction.

The interesting part is not the model — it is the gap between training data and reality. MNIST digits are carefully normalised; a mouse drawing is not. Most of the engineering here is the preprocessing that closes that gap (see Key Insights).

Dataset

The archive is downloaded automatically on first run into data/, which is gitignored — nobody needs an 11 MB blob in their clone.

Approach

Module What it does
src/data_loader.py Downloads mnist.npz once into data/ and returns (x_train, y_train, x_test, y_test) via numpy.load.
src/train.py Flattens images to 784 features, scales pixels to [0, 1], trains an MLPClassifier with one hidden layer of 128 units and early stopping, evaluates on the full test set, and writes the model, reports/metrics.json and the confusion-matrix figure.
src/predict.py Turns a raw canvas drawing into an MNIST-style 28x28 input (grayscale → threshold → crop → resize to 20 px → pad to 28x28 → recentre by centre of mass → scale) and runs the prediction.
app.py Streamlit UI: a 280x280 drawing canvas, a stroke-width slider, the predicted digit, a bar chart of all 10 class probabilities, and a preview of the 28x28 image the model actually sees.
tests/test_predict.py Headless tests for the preprocessing and end-to-end prediction path.

Training setup. The model is trained on a stratified 30,000-image subsample of the training set to keep training to about 20 seconds on a laptop, but it is always evaluated on the full, untouched 10,000-image test set — so the accuracy below is honest, not measured on data the model has seen.

Results

All numbers below are copied from reports/metrics.json, produced by running python -m src.train.

Metric Value
Test accuracy (10,000 unseen images) 97.11%
Macro-average F1 0.9709
Training images used 30,000 (stratified subsample)
Test images 10,000 (full test set)
Training time 20.3 s
Epochs run (early stopping, max 40) 29
Model file size 2.3 MB (models/mnist_mlp.joblib)

Per-digit performance

Digit Precision Recall F1 Support
0 0.9767 0.9857 0.9812 980
1 0.9842 0.9868 0.9855 1135
2 0.9729 0.9729 0.9729 1032
3 0.9711 0.9653 0.9682 1010
4 0.9743 0.9654 0.9698 982
5 0.9666 0.9742 0.9704 892
6 0.9737 0.9656 0.9696 958
7 0.9678 0.9640 0.9659 1028
8 0.9632 0.9671 0.9652 974
9 0.9585 0.9623 0.9604 1009

Highlights: 1 is the easiest digit (F1 0.9855) — it has the least shape variation. 9 is the hardest (F1 0.9604), and 8 is close behind (F1 0.9652).

Confusion matrix

How to Run

# 1. Clone and enter the project
git clone <your-repo-url>
cd digit-recognizer-streamlit

# 2. Create a virtual environment
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

# 3. Install dependencies
pip install -r requirements.txt

# 4. Launch the app (a trained model is already committed)
streamlit run app.py

Then open http://localhost:8501 and draw a digit.

Optional — retrain the model yourself (downloads MNIST on first run, ~20 s to train):

python -m src.train      # writes models/ and reports/
python -m pytest         # runs the test suite

Deploy to Streamlit Cloud (free)

  1. Push this repository to GitHub.
  2. Go to https://share.streamlit.io and sign in with your GitHub account.
  3. Click New app, pick this repository and branch (main).
  4. Set the main file path to app.py.
  5. Click Deploy.

The trained model (models/mnist_mlp.joblib, 2.3 MB) is committed to the repo, so no training happens in the cloud — the app just loads the file and starts serving. requirements.txt pins scikit-learn==1.9.0 so the model unpickles against exactly the version that created it.

Key Insights

  • Recentring the drawing matters more than the model does. MNIST digits are scaled to a 20x20 box and shifted so their centre of mass sits in the middle of a 28x28 field. A raw mouse drawing is off-centre and the wrong size, so preprocess_canvas reproduces that exact normalisation. Without this step the same 97%-accurate model gives near-random answers on hand-drawn input — the model isn't wrong, it is just being shown something that doesn't look like its training data.
  • 4 and 9 are the model's blind spot. The single largest error in the confusion matrix is 18 fours predicted as nines — more than double any other mistake. That matches intuition: a 4 with a closed top is a 9. 9 ends up with the lowest recall (0.9623) and the lowest precision (0.9585) of any digit.
  • Half the data got 97% of the way there. Training on 30,000 of the 60,000 images still reaches 97.11%, and early stopping halted at 29 of the allowed 40 epochs — the network stopped improving on its validation split before it ran out of budget. Extra data and epochs would give diminishing returns here; a convolutional network, not more of the same training, is the real next step.

Project Structure

digit-recognizer-streamlit/
├── app.py                      # Streamlit app (drawing canvas + prediction UI)
├── requirements.txt
├── README.md
├── .gitignore
├── data/                       # gitignored — mnist.npz downloaded on first run
├── models/
│   └── mnist_mlp.joblib        # trained model, committed for cloud deployment
├── reports/
│   ├── metrics.json            # machine-written metrics (source of every number above)
│   └── confusion_matrix.png
├── src/
│   ├── __init__.py
│   ├── data_loader.py          # download + load MNIST
│   ├── train.py                # train, evaluate, save artifacts
│   └── predict.py              # canvas preprocessing + prediction
└── tests/
    └── test_predict.py         # preprocessing + end-to-end prediction tests

About

Draw a digit, get a prediction - MNIST neural network (97.1% test accuracy) served live in a Streamlit app with center-of-mass canvas normalization.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages