Draw a digit from 0-9 and watch a PyTorch CNN, trained on MNIST, predict what you wrote — along with its confidence for every possible digit.
| File | Purpose |
|---|---|
model.py |
The CNN architecture: 2 conv layers + max-pooling + dropout + 2 fully-connected layers. |
train_model.py |
Downloads MNIST via torchvision, trains the model, and saves digit_model.pth. |
digit_app.py |
The Tkinter desktop drawing app: draw a digit, it's preprocessed into MNIST-style 28x28 input, and the trained model predicts it. |
streamlit_app.py |
The same experience as a browser-based web app (uses streamlit-drawable-canvas for drawing). |
Two frontends are included, sharing the same model.py and trained
checkpoint — pick whichever you prefer:
Web (Streamlit) — recommended, no extra OS packages needed:
pip install -r requirements.txt
python train_model.py # downloads MNIST and trains — a few minutes on CPU
streamlit run streamlit_app.pyOpens in your browser at http://localhost:8501.
Desktop (Tkinter):
pip install -r requirements.txt
python train_model.py # downloads MNIST and trains — a few minutes on CPU
python digit_app.py # opens the drawing apptrain_model.py only needs to be run once — it saves digit_model.pth,
which digit_app.py loads on every subsequent run. Test-set accuracy is
typically ~99% after 6 epochs.
- You draw on a 280x280 black canvas with a white brush.
- The drawing is cropped to its bounding box, padded back to a square with a margin (so the digit doesn't touch the edges, like real MNIST digits), then resized down to 28x28.
- Pixel values are normalized using MNIST's mean/std (
0.1307/0.3081) — the same normalization the model was trained with. - The 28x28 tensor is passed through the CNN, and a softmax turns the raw logits into a probability distribution over digits 0-9.
The app also shows you exactly what the model sees — the actual 28x28 image after preprocessing — which is a good way to understand why a messy drawing sometimes gets misclassified: what you drew and what the model receives can look surprisingly different at low resolution.
Input (1x28x28)
-> Conv2d(1->32, 3x3) -> ReLU -> MaxPool2d(2x2) # 28x28 -> 14x14
-> Conv2d(32->64, 3x3) -> ReLU -> MaxPool2d(2x2) # 14x14 -> 7x7
-> Dropout(0.25)
-> Flatten -> Linear(64*7*7 -> 128) -> ReLU -> Dropout(0.5)
-> Linear(128 -> 10) # raw logits, softmax applied at inference