A C++ feedforward neural network trained on the MNIST handwritten digit dataset, with a raylib GUI for drawing digits and running live predictions.
The network is implemented from scratch using Eigen. Saved weights reach 95.4% test accuracy — you can often do better by tuning layer sizes or the early-stopping threshold in neural_net.cpp:
if (epoch_accuracy > best_accuracy + 0.003)| Flow Chart | Video Explanation |
|---|---|
![]() |
![]() |
- Custom MLP — configurable layer sizes, ReLU hidden layers, softmax output
- Cross-entropy training — mini-batch SGD (batch size 32)
- Early stopping — held-out validation split with best-weight checkpointing
- Model persistence — save / load weights to
data/best_model.bin - Interactive GUI — draw digits with the mouse, preprocess like MNIST, classify in real time
- Background training — load or train on a worker thread while the window stays responsive
Default network: 784 → 256 → 64 → 10
| Component | Detail |
|---|---|
| Input | 28×28 grayscale image (784 pixels) |
| Hidden layers | ReLU |
| Output layer | Softmax |
| Loss | Cross-entropy |
| Optimizer | Stochastic gradient descent |
| Learning rate | 0.005 |
| Batch size | 32 |
| Split | Images |
|---|---|
| Training | 48,000 |
| Validation | 12,000 |
| Test (official MNIST) | 10,000 |
To match MNIST normalization:
- Capture the 280×280 drawing canvas
- Find the bounding box of white ink pixels
- Crop to that box
- Scale to fit inside a 20×20 region (aspect ratio preserved)
- Paste into a 28×28 black image, centered by center of mass at
(13.5, 13.5) - Feed the resulting vector to the network
The right panel in the GUI shows the processed 28×28 input the network actually sees.
cmake -B build
cmake --build build
./build/mainWait for "Training not done, please wait" to disappear before submitting a drawing.
Edit the constructor in src/main.cpp (line 23):
NeuralNet net({784, 256, 64, 10}, 32, true, true);| Argument | Meaning |
|---|---|
{784, 256, 64, 10} |
Layer sizes (first must be 784, last must be 10) |
32 |
Batch size |
save_read |
true — load weights from data/best_model.bin on startup (skipped if missing or architecture mismatch); false — always reinitialize and train |
save_write |
true — overwrite data/best_model.bin on quit when training improves validation accuracy; false — leave the file unchanged |
| Input | Action |
|---|---|
| Left mouse drag | Draw on the canvas (white on black) |
Enter |
Preprocess drawing and run prediction |
C |
Clear the canvas |
Q |
Quit (saves model weights on exit) |
Predictions appear below the processed-image preview on the right.
data/best_model.bin layout:
| Field | Type | Description |
|---|---|---|
best_accuracy |
float |
Best validation accuracy |
num_layers |
int |
Number of entries in layer_sizes |
layer_sizes[] |
int × num_layers |
Network architecture |
| Per layer | double weights, then double biases |
Row-major weights followed by biases |
.png)
