A feed-forward network assembled from parts rather than imported: dense and dropout layers, six activation functions, five loss functions, and mini-batch training with momentum and early stopping — all in NumPy. PyTorch appears exactly once in the repository, to download MNIST; nothing in the model touches it.
Calling a framework’s fit method teaches you the API. Writing the backward pass teaches you why the API has the shape it does. This is the second one: forward and backward for every layer, the chain rule applied by hand, and gradients that are wrong in obvious ways until they are not.
The recorded run reaches 98.9% training and 97.9% validation accuracy on MNIST after 150 epochs — close enough to a framework baseline to show the mathematics is right, which is the only claim a project like this should make.
Dense layers with a choice of weight initialisation, dropout that knows the difference between training and inference, six activations, five losses including Huber and the numerically stable softmax-with-cross-entropy pairing, mini-batch shuffling, momentum, and early stopping on validation loss.
The layers share one small interface — forward, backward, update — so a network is a list of things that satisfy it. That constraint is what keeps the code readable at this size, and it is the same constraint every framework arrives at.
A drawing canvas ships alongside the training code: pick a saved model, draw a digit with the mouse, and see the prediction and its confidence.
This matters more than it looks. MNIST digits are centred, size-normalised and drawn with a specific stroke weight; a mouse-drawn digit is none of those. The gap between a held-out score and what happens on genuinely new input is the single most useful thing a beginner can be shown, and it is much more convincing to draw it yourself than to read about distribution shift.