-
Notifications
You must be signed in to change notification settings - Fork 0
MNIST and OCR
nuNN includes a command-line MNIST experiment and a Windows drawing application. Together they cover the complete path from binary dataset files to normalized vectors, mini-batch training, held-out accuracy, JSON persistence, interactive inference, and topology visualization.

Each MNIST image is 28 by 28 grayscale pixels. DigitData::toVect() flattens the row-major bytes and normalizes them to [0, 1].

The complete conversion in mnist.cc is:
void DigitData::toVect(nu::Vector& values) const noexcept
{
values.resize(data().size());
for (size_t i = 0; i < data().size(); ++i)
values[i] = double((unsigned char)data()[i]) / 255.0;
}Thus:
28 x 28 bytes -> 784 floating-point inputs in [0, 1]

labelToTarget() produces ten entries with one at the label index:
target.resize(10);
std::fill(target.begin(), target.end(), 0.0);
target[getLabel() % 10] = 1.0;A class index is categorical, not a continuous scalar. One-hot targets prevent the model from interpreting digit 8 as numerically close to digit 9.
TrainingData receives the label and image filenames, then load():
- verifies label magic
0x00000801and image magic0x00000803; - checks that image and label counts match;
- reads row and column dimensions;
- pairs every label with one
DigitDataimage.
Public declarations are in mnist.h; parsing is in mnist.cc.
The dataset directory must contain the four names used by default:
train-labels.idx1-ubyte
train-images.idx3-ubyte
t10k-labels.idx1-ubyte
t10k-images.idx3-ubyte
Custom filenames are available through the command-line options shown by mnist_test --help.
The current defaults come directly from mnist_test.cc:
| Setting | Default |
|---|---|
| model | MlpMatrixNN |
| topology | 784 -> 300 -> 10 |
| hidden activation | sigmoid |
| output activation | sigmoid |
| cost | MSE |
| learning rate | 0.025 |
| momentum | 0.50 |
| epochs | 100 |
| batch size | 100 |
| backend | Auto |
Start a default run with:
mnist_test -p /path/to/mnistThe matrix model is the default because the 60,000-sample training set benefits from batched matrix operations.
# Classic neuron-by-neuron MLP and online updates
mnist_test -p /path/to/mnist --mlp
# Matrix MLP, force Eigen/CPU
mnist_test -p /path/to/mnist --backend cpu
# Matrix MLP, require ArrayFire/OpenCL
mnist_test -p /path/to/mnist --backend opencl
# Matrix MLP with online SGD
mnist_test -p /path/to/mnist --batch 1
# Cross-entropy, ReLU hidden layer, custom topology and schedule
mnist_test -p /path/to/mnist \
--use_cross_entropy \
--activation relu \
--hidden_layer 300 \
--hidden_layer 100 \
--learningRate 0.01 \
--epoch_cnt 50Each --hidden_layer occurrence appends a hidden layer. The output remains sigmoid so it is compatible with MSE or binary cross-entropy. The class prediction is the index of the largest of the ten outputs.
The matrix training loop converts DigitData objects into two parallel batches:
std::vector<std::vector<double>> batchInputs;
std::vector<std::vector<double>> batchTargets;
for (const auto& digit : trainingSet.data()) {
nu::Vector input;
nu::Vector target;
digit->toVect(input);
digit->labelToTarget(target);
batchInputs.push_back(input.to_stdvec());
batchTargets.push_back(target.to_stdvec());
if (batchInputs.size() == batchSize) {
net.trainBatch(batchInputs, batchTargets);
batchInputs.clear();
batchTargets.clear();
}
}
if (!batchInputs.empty())
net.trainBatch(batchInputs, batchTargets);The final partial batch must be flushed. Omitting it silently drops samples whenever the dataset size is not an exact multiple of the batch size.
At each epoch the example reshuffles training data, reports epoch time and throughput, evaluates the test set, and keeps the best observed error rate.
The executable reports:
- error and success rate on test digits;
- MSE and cross-entropy calculated from test outputs;
- change in MSE from the previous epoch;
- epoch duration and samples per second;
- best error rate and the epoch that achieved it.
Training updates use the selected cost function. Reporting both losses does not mean both are optimized simultaneously.
The test set must never be passed to backPropagate() or trainBatch(). Its role is to estimate generalization after training updates.
Both MLP paths use JSON stream persistence in the current implementation:
mnist_test -p /path/to/mnist --save mnist.json
mnist_test -p /path/to/mnist --load mnist.json --skip_trainingA saved model should contain topology, activation choices, cost function, weights, and biases. Validate a round trip by comparing outputs from the trained object and a fresh loaded object on the same normalized digit.
Visualize the saved model with nunn_topo:
nunn_topo --load mnist.json --save mnist.svg
nunn_topo --load mnist.json --save mnist.dotDOT output works without Graphviz. SVG, PNG, and PDF require dot in PATH. Large topologies use a compact representation by default; --full draws every node and edge.
The Windows application in examples/ocr_test can:
- load legacy
.netand JSON MLP models; - resample a drawing into a 28 by 28 input;
- show the ten output activations and predicted digit;
- train
MlpNNorMlpMatrixNNfrom the MNIST files; - select
Auto,CPU, orOpenCLfor the matrix model; - chart cost convergence during training;
- remember dataset and save paths.
The important engineering boundary is preprocessing:
mouse strokes
-> canvas
-> resampled 28 x 28 grayscale image
-> 784 normalized values
-> same forward pass used by mnist_test
A model can score well on MNIST and still struggle with drawings whose centering, scale, stroke width, or contrast differs from the training distribution.
The GUI searches model locations used by the build and packages, including:
bin/models
bin
../share/nunn/nets
../share/nunn/models
On eligible Windows OpenCL builds, ocr_launcher.cpp selects an OpenCL-capable executable when the runtime is usable and falls back to the CPU executable otherwise. This keeps missing optional runtime DLLs from becoming a silent startup failure.
Relevant sources:
- training, inference, charting, settings:
ocr_test.cpp; - runtime selection:
ocr_launcher.cpp; - packaging rules:
CMakeLists.txt.
- Record model type, hidden layers, activation, loss, learning rate, momentum, batch size, backend, and epoch count.
- Confirm that every input has 784 values in
[0, 1]and every target has ten entries. - Shuffle only the training data.
- Evaluate on the untouched test set after each epoch or at a fixed interval.
- Save the best model to an explicit filename.
- Load it into a fresh object and compare outputs for the same test digit.
- Confirm that
ocr_testreports the intended filename and a 784-input topology. - Diagnose drawing-domain mismatch separately from model persistence.
Use Training and Diagnostics for convergence patterns and Neural Networks for the exact MLP, batch, backend, and persistence APIs.