A simple MNIST digit classifier built from scratch:
- Python handles data loading, training loop orchestration, and evaluation
- C++ implements core neural network math (dense layers, ReLU, softmax-cross-entropy, SGD)
pybind11bridges Python and C++
- Input:
784(28x28 flattened image) - Hidden layer:
128units + ReLU - Output:
10logits (digits 0-9) - Loss: softmax + cross-entropy
- Optimizer: SGD
- Load MNIST IDX files from
data/usingneuralpy/data.py. - Shuffle training data with
shuffle_data(...)inneuralpy/utils.py. - For each sample in
neuralpy/train.py:
- Forward pass:
Dense(784->128) -> ReLU -> Dense(128->10) - Compute loss: softmax + cross-entropy
- Backward pass: softmax gradient -> dense backward -> ReLU backward -> dense backward
- Update parameters with SGD (
w -= lr * grad)
- After training, run inference on test data in
neuralpy/main.pyand compute test accuracy.
epochs = 5lr = 0.01in_dim = 784hidden_dim = 128out_dim = 10
- Change training length and learning rate in
neuralpy/main.pywheretrain(...)is called:epochs=...lr=...
- Change architecture dimensions in
neuralpy/train.pyinsidetrain(...):in_dimhidden_dimout_dim
Example:
# neuralpy/main.py
w1, b1, w2, b2 = train(train_images, train_labels, epochs=10, lr=0.005)# neuralpy/train.py
hidden_dim = 256Notes:
- You do not need to rebuild C++ just to change these numeric hyperparameters.
- If you changed C++ code in
neuralcpp/orneuralbinding/binding.cpp, runbuild.pyagain.
neuralpy/main.py- entry point (load data, train, test)neuralpy/train.py- training loopneuralpy/data.py- MNIST IDX file loadersneuralpy/utils.py- one-hot, init, shuffle, accuracy helpersneuralcpp/*.cpp/neuralcpp/*.h- NN math implementationneuralbinding/binding.cpp- pybind module bindingsbuild.py- compiles C++ extension moduledata/- MNIST IDX files
MNIST-NN-No-Libraries/
├── README.md
├── build.py
├── data/
│ ├── train-images.idx3-ubyte
│ ├── train-labels.idx1-ubyte
│ ├── t10k-images.idx3-ubyte
│ └── t10k-labels.idx1-ubyte
├── neuralbinding/
│ ├── binding.cpp
│ └── neuralbinding*.so (generated)
├── neuralcpp/
│ ├── dense.h
│ ├── dense.cpp
│ ├── relu.h
│ ├── relu.cpp
│ ├── loss.h
│ ├── loss.cpp
│ ├── optimizer.h
│ ├── optimizer.cpp
│ ├── math_utils.h
│ └── math_utils.cpp
└── neuralpy/
├── main.py
├── train.py
├── data.py
├── utils.py
└── neuralbinding*.so (copied for runtime)
- Python 3.12+ recommended
- C++17 compiler (
c++) pybind11
python3 -m venv venv
source venv/bin/activate
pip install pybind11From project root:
./venv/bin/python build.pyTo run main.py from neuralpy/, move/copy the built module into neuralpy/:
cp neuralbinding/neuralbinding*.so neuralpy/cd neuralpy
../venv/bin/python main.pyExpected output includes per-epoch loss/accuracy and final test accuracy.
- Training is slow in this setup. A full run can take about 30 minutes to 1 hour depending on machine performance.
- Test workflow:
- Run
build.py - Drag/copy the generated
.sofile intoneuralpy/ - Run
main.py
Epoch 1: loss = 0.2244, accuracy = 0.9334
Epoch 2: loss = 0.0985, accuracy = 0.9695
Epoch 3: loss = 0.0671, accuracy = 0.9794
Epoch 4: loss = 0.0496, accuracy = 0.9845
Epoch 5: loss = 0.0366, accuracy = 0.9888
Test Accuracy: 0.9767