A handwritten-digit recognizer built with no Python, no TensorFlow, no NumPy, and no ML libraries at all. The matrix math, the neural network, the backpropagation, and even the drawing canvas are all written by hand in C++20.
Train it, then draw a digit right in your terminal and watch the network guess what you drew.
- Custom
Matrixclass with operator overloading, transpose, dot product, and element-wise ops. It is the only math tool the network uses. - Feed-forward neural network (input → hidden → output) trained with backpropagation and stochastic gradient descent.
- Xavier (Glorot) weight initialization using a self-seeded Mersenne Twister.
- Interactive terminal canvas: draw a digit with your mouse, press a key, and get a prediction.
- Fast to train: the default 6 epochs take only a few seconds in a Release build, so you can play with it right away.
- Self-contained: a small 20×20 digit dataset is included, with nothing to download.
| Layer | Size | Meaning |
|---|---|---|
| Input | 400 | One neuron per pixel of the 20×20 image |
| Hidden | 100 | Learns intermediate features of the strokes |
| Output | 10 | One neuron per digit, 0 to 9 |
Every neuron uses a sigmoid activation. The learning rate is 0.3. Weights are initialized with Xavier normal initialization: random values drawn from a normal distribution with mean 0 and standard deviation sqrt(2 / (n_in + n_out)), where n_in and n_out are the sizes of the two layers a weight matrix connects. This network has no bias terms.
The prediction is simply the output neuron with the highest activation.
Each layer multiplies its inputs by a weight matrix and squashes the result with a sigmoid:
hidden = sigmoid(W_ih · input)
output = sigmoid(W_ho · hidden)
For every training image the network:
- Makes a prediction with the forward pass.
- Compares it to the target, a "one-hot" vector such as
[0,0,0,1,0,0,0,0,0,0]for a 3, to get the output error. - Sends that error backwards through the transposed weights to work out how much each hidden neuron was responsible for it.
- Nudges each weight matrix to reduce the error:
ΔW_ho = lr · (error_out ⊙ out ⊙ (1 − out)) · hiddenᵀ
ΔW_ih = lr · (error_hidden ⊙ hidden ⊙ (1 − hidden)) · inputᵀ
The out ⊙ (1 − out) term is the derivative of the sigmoid, and ⊙ is element-wise multiplication. Training is online: weights update after every single image, with no batching, and the whole training set is repeated for several epochs (6 by default).
Pixel values (0 to 255) are rescaled to the range 0.01 to 1.0. Avoiding exact zeros keeps the weight updates from dying out on blank pixels. The drawing canvas uses the same scaling, so what you draw looks to the network exactly like a training image.
digits_dataset/ contains two CSV files:
| File | Images | Per digit |
|---|---|---|
digits_train.csv |
1,041 | ~104 |
digits_test.csv |
100 | 10 |
Each row is label, p0, p1, ..., p399: the digit (0 to 9) followed by 400 grayscale pixel values (0 to 255) of a 20×20 image in row-major order.
- Windows: the interactive canvas uses the Win32 API (
GetAsyncKeyState,GetCursorPos,SetCursorPos) for input. - Visual Studio with the C++ desktop workload and C++20 support (the code uses
<format>andstd::clamp). - A terminal with ANSI escape code support (Windows Terminal or a recent console), since the canvas and colors rely on them.
- Clone the repository:
git clone https://github.com/elevenMiles/digit-recognizer-cpp.git
- Open
digit_recognizer_neural_net.slnxin Visual Studio. - The project is set to platform toolset
v145. If your Visual Studio doesn't have it, right-click the project → Properties → General → Platform Toolset and pick the one you have installed. - Choose Release | x64 and press Ctrl+F5 (Run without debugging).
The program loads the dataset from
digits_dataset\using a relative path, so it must run with the project folder as its working directory. This is Visual Studio's default.
On launch the network trains for a few seconds (Debug builds are slower), then the drawing canvas appears.
The mouse acts as a relative "joystick": the system cursor is kept at the centre of the screen, and your mouse movement moves a cursor on the 20×20 canvas.
| Key | Action |
|---|---|
| Mouse | Move the cursor |
D (hold) |
Draw |
C |
Clear the canvas |
Space |
Ask the network to guess |
X |
Exit |
Tips for the best results: draw the digit large, roughly centred, and with a continuous stroke, the way the training digits look.
.
├── main.cpp # Data loading, training, testing, interactive canvas loop
├── NeuralNetwork.h # NeuralNetwork class declaration
├── NeuralNetwork.cpp # Network: forward pass (query) and backpropagation (train)
├── Matrix.h / Matrix.cpp # Hand-written linear algebra
├── Random.h # Self-seeding Mersenne Twister and normal distribution helper
├── Canvas.h # ASCII renderer for the terminal canvas
├── DigitBuffer.h # Drawing buffer, brush, and conversion to network input
└── digits_dataset/
├── digits_train.csv
└── digits_test.csv
Random.h is based on the self-seeding Mersenne Twister from learncpp.com, extended with a normal distribution helper used for weight initialization.
