AI & ML / UCT · CSC3022 / 2024
Handwritten digit recognition
From image preprocessing to held-out prediction: a PyTorch digit classifier with inspectable training curves and class-level errors.
UCT / COURSE CONTEXT / 2024
CSC3022F · C++ and Machine Learning
C++ language and memory-model foundations followed by Python-based machine-learning implementation.
ENGINEERING PERSPECTIVE
Inside the work
A complete small-scale classification workflow: turn image data into a trained decision function, then check predictions on unseen digits. The engineering challenge is consistency between data preparation, tensor shapes, optimisation and evaluation.
- PyTorch
- Tensor operations, automatic differentiation, fully connected layers and Adam optimisation.
- MNIST / torchvision
- Labelled 28-by-28 digit images and a consistent loading/normalisation path.
Prepare pixels
Load MNIST, convert images to tensors and apply grayscale normalisation.
- Representation
- Flattening gives 784 inputs; ReLU hidden layers learn nonlinear combinations rather than hand-written digit rules.
- Loss conventions
- CrossEntropyLoss expects logits. The original extra softmax is retained as a limitation; the labelled enhancement compares the convention explicitly.
- Generalisation
- Training loss and held-out accuracy answer different questions. One run does not establish robustness across random seeds.
What the demonstration establishes
The existing experiment controls compare recorded runs, learning curves and per-class errors. The original 95.71% test result is historical, not a guarantee for arbitrary handwriting.
Evidence behind this page
Classifier source, named three-page submissions and retained training log; new controlled experiments are explicitly post-project work.
What it is
Built a feedforward classifier for handwritten digits, including data loading, minibatch training, test evaluation and prediction for a local image.
Why it was built
An introductory learning problem with a complete path from image pixels to training and held-out predictions: recognise ten handwritten digit classes.
My contribution
Individual submitted classifier, training loop, evaluation and local-image prediction, supported by the source and named submission. PyTorch and MNIST are external frameworks/data, not original inventions.
How it works
Flatten 28 × 28 images into 784 inputs, then use 128- and 64-unit ReLU hidden layers and a 10-class output. The original uses Adam at 0.001, batches of 32 and seven epochs.
- 01MNIST
- 02Normalise
- 03784 → 128 → 64 → 10
- 04Train with Adam
- 05Evaluate / predict
Technical implementation
- ToTensor and grayscale normalisation prepare 28x28 input; flattening produces 784 features.
- Two fully connected hidden layers use ReLU, followed by a ten-class softmax. Adam, a 0.001 learning rate and batches of 32 drive seven original training epochs.
- CrossEntropyLoss already expects logits. The retained extra softmax is a documented design limitation, not silently rewritten historical work.
- Cleanup adds explicit dataset paths, subset/epoch controls, non-interactive prediction and bounded CPU smoke verification.
Controlled enhancement experiments
NEW / PORTFOLIO REPRODUCIBILITY EXPERIMENTS
Same architecture and initial seed. Fixed 10,000 training, 2,000 validation and 2,000 official held-out test examples. Three epochs; Adam at 0.001. Validation is disjoint from training. These are not the original full-data results.
| Configuration | Batch | Test accuracy | Train + validation |
|---|---|---|---|
| Original softmaxoriginal-softmax-32 | 32 | 89.80% | 10.9s |
| Logitslogits-32 | 32 | 91.30% | 7.9s |
| Logitslogits-128 | 128 | 90.10% | 8.4s |
| Epoch | Train loss | Validation |
|---|---|---|
| 1 | 0.689 | 0.365 |
| 2 | 0.357 | 0.332 |
| 3 | 0.289 | 0.272 |

First four correct and first four wrong predictions in fixed test order, logits/batch32. Not selected for visual quality. Confidence was not retained and is not inferred. One seed, limited subsets and no test-driven tuning; timings are machine-specific and include validation.
Removing softmax before CrossEntropyLoss changes the objective to its intended logits convention. Batch128 makes fewer optimiser updates per epoch: it ran faster here but had lower accuracy than batch32. This is a small controlled comparison, not a universal ranking.
Results & evidence
- The original training log records 95.71% MNIST test accuracy.
- The cleaned entry point supports explicit dataset paths and a bounded, non-interactive smoke run.
Engineering challenges
Image shape, normalisation and label interpretation must agree across training and single-image prediction. Historical test accuracy and a tiny smoke run answer different questions.
Scope & limitations
The 95.71% result is historical. The submitted softmax-before-cross-entropy design is retained and documented; no saved checkpoint or seeded repeatability evidence was supplied.
What I learned
How preprocessing, tensor dimensions, minibatch optimisation and evaluation fit together; why loss/output conventions and reproducibility matter.
What I would improve now
The new controlled experiments above add logits, fixed seeds and per-class confusion counts. Next: repeat across seeds, save checkpoints and compare a convolutional baseline on a predeclared evaluation protocol.