<!-- Automatically generated by Staverse -->
<!-- Author: NeaByteLab | Date: 2026-03-04T09:33:57Z | Title: Teaching a Machine to Read Digits by Hand | Source: https://neabyte.com/projects/teaching-a-machine-to-read-digits-by-hand.md -->

## Numbers Learned by Machine

Digits drawn by hand never sit still, so a model reading them must learn the wobble in each stroke before naming even one digit.

The engine behind this demo began as bare matrix math, grew a stack of layers and an optimizer, then a full training loop, and only later earned the right to claim it could see anything at all. Every piece was written from scratch so the path from raw pixels to a trained model stayed honest, with nothing at all tucked behind a framework that already knew every answer.

A recognizer that reaches high accuracy proves the parts underneath actually work together, from the forward pass through to the gradient that nudges each weight closer to right. That result turned a quiet pile of numeric code into something alive.

## Owning Every Layer

Reaching for a mature library means trusting a black box to handle the gradient, the optimizer, and the edge cases that decide whether a model learns or quietly stalls. That trust ships features fast, yet it leaves a gap where understanding must be.

Owning every layer closes that gap and turns each bug into a lesson instead of a wall to climb around, one small fix at a time.

Writing the numerics by hand meant the matrix routines, the activation functions, and the loss all sat in one readable place. Nothing was borrowed, so nothing stayed mysterious, and the training loop could be traced line by line from the first pixel to the last weight update. That openness made the engine slower to build, though easier to trust once the numbers moved.

![A layered engine with numerics at the base, trainable layers above, and a full network at the top](/projects/teaching-a-machine-to-read-digits-by-hand/image-1.webp)

## Walls in the Math

Getting the gradient right was the first wall, since one flipped sign or a misplaced transpose sends the loss climbing instead of falling. Every backward pass had to match the forward pass, or the model would drift into noise. Debugging that silent error means checking the math against tiny inputs, watching each number, and trusting nothing until the loss bends down.

Pure processor math turned into the second wall once the matrices grew, because a large multiply on a single core drags every training step to a crawl. Speed had to come from somewhere without breaking the careful numerics already sitting in place.

Proving the model truly learned, rather than simply memorized, was the quiet challenge that mattered most of all in the end here.

## Complete, Fast, Honest

Clear goals shaped the effort from the start. The engine had to train real models outside the usual language ecosystem, run across many kinds of graphics hardware rather than one vendor, and stay ready for the heavy compute that large models demand.

Completeness came first because a half-built engine cannot prove anything. Dense layers, activations, a cross-entropy loss, and an optimizer all needed to exist and cooperate, so a caller could stack a network, feed batches, and watch the loss fall. Only then would a demo really carry weight, since the accuracy would rest on parts understood down to the last multiply.

Speed came next, since heavy models only stay usable when a single codebase runs fast on whatever graphics hardware is present.

## Tiers With a Seam

The plan split the engine into tiers that each did one job, so reading stayed clean. Low-level numerics handled matrices and activations, a middle tier held trainable layers with their own weights, and a top tier composed those layers into a network. Keeping the tiers apart meant a change in one rarely rippled into another, leaving a clean seam for a faster backend later.

The same tier also held the parts for sequence and language models, general enough to reshape toward many different tasks later.

A compute shader took over the heavy matrix multiply while the rest of the engine stayed as it was, so the reliable version and the fast one could run side by side and be compared. Any drift between the two paths showed up at once as a mismatch.

![A slow processor path and a fast graphics path sharing the same matrix interface behind one seam](/projects/teaching-a-machine-to-read-digits-by-hand/image-2.webp)

## Pixels Into Ten Scores

Execution started with the smallest network that could still prove the whole point. A stack mapped raw image pixels through two hidden layers and out to ten scores, one per digit, with a rectified activation between steps and a cross-entropy loss.

Training fed the pixels in batches, let the loss guide each weight, and repeated the cycle until the numbers finally settled.

The public surface stayed deliberately small, so building and training the model read like plain steps rather than a maze of options. A network is stacked layer by layer, an optimizer is attached, and one call runs a step returning the loss. Saving the weights and loading them later shares the same surface, so the trained model reads a fresh image and names the digit.

```typescript
// Stack a classifier, attach an optimizer, and run one training step
const model = new Network()
model.addLayer(new Layer(784, 128, 'relu'))
model.addLayer(new Layer(128, 64, 'relu'))
model.addLayer(new Layer(64, 10, 'none'))
const loss = model.trainStepClassification(pixels, labels, 0.002, { optimizer })
```

![A small dense network turning pixel rows into ten class scores during a training step](/projects/teaching-a-machine-to-read-digits-by-hand/image-3.webp)

## Chance Gave Way to Skill

Results arrived fast once the pieces fit. A model that guessed at random started near one in ten, exactly what chance gives across ten classes, and then climbed hard as training took hold. The very first pass already cleared the mid-eighties, the later passes pushed well past ninety, and the accuracy peaked around ninety-five on images the model had never seen before.

| Stage             | Accuracy | Loss   |
| ----------------- | -------- | ------ |
| Before training   | 10.0%    | —      |
| After one pass    | 88.0%    | 0.5252 |
| Peak at pass five | 95.5%    | 0.0528 |
| After six passes  | 94.5%    | 0.0358 |

Loss told the same story from the other side, sliding from roughly half a unit down to a few hundredths as the weights sharpened over each pass. The confusion matrix showed a strong diagonal, with rare mistakes landing where a human eye slips too.

| Matrix Size | Processor Path | Graphics Path | Speedup |
| ----------- | -------------- | ------------- | ------- |
| 256 × 256   | 5.8 ms         | 1.0 ms        | 5.8×    |
| 512 × 512   | 43 ms          | 1.0 ms        | 43×     |
| 1024 × 1024 | 7.3 s          | 57 ms         | 128×    |

Speed came from the graphics path, and it grew sharply with matrix size, so the big multiply that once crawled finished quicker.

> [!NOTE]
> Still experimental and scoped to one clean digit at a time, the exact trained weights behind this demo ship as a small model file of a few megabytes. Download the [live model](/projects/teaching-a-machine-to-read-digits-by-hand/demo-model.json), load it straight into the engine, and hand it a fresh image to read, so the same recognizer that scored on the test set runs on any machine with the runtime available.

|                                                           Training Run                                                            |                                                             Reading a Seven                                                             |                                                             Reading a Two                                                             |
| :-------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
| ![Training loss falling and accuracy rising across passes](/projects/teaching-a-machine-to-read-digits-by-hand/image-demo-1.webp) | ![The model reading a handwritten seven and naming it correctly](/projects/teaching-a-machine-to-read-digits-by-hand/image-demo-2.webp) | ![The model reading a handwritten two and naming it correctly](/projects/teaching-a-machine-to-read-digits-by-hand/image-demo-3.webp) |

## Speed Has a Threshold

Graphics hardware only pays off past a certain size, so the smallest layers actually ran faster on the plain processor path here.

That threshold shaped every later decision about where to send the work. Tiny matrices carry too much setup cost on the graphics side, since moving data across and launching a shader eats any gain, while a large multiply hides that cost under the sheer volume of arithmetic it does. Knowing where the line sits let the engine pick the right path for each shape it met.

Proving real learning also demanded more than a single accuracy number alone. A confusion matrix and a held-out test set turned a vague claim into real evidence, showing not just how often the model was right but exactly where it stumbled and why.

## A Model Worth Keeping

Bare arithmetic grew, step by careful step, into something that reads a handwritten number and answers with steady confidence.

What began as a pile of matrix routines now stacks into layers, trains against real data, and leans on the graphics card when the work runs heavy. Every stage stayed readable, so the path from a raw pixel to a named digit stays clear the whole way.

The result stays honest about its limits, reading one clean digit at a time while the heavier sequence and language side stays experimental and has yet to converge. Lighter models get trained often, the same code runs across many kinds of graphics hardware, and the aim keeps reaching for the best this language allows. Trained weights save and load in one small file.

<!-- CITATION POLICY -->
<!-- Any use, including AI training data, must cite the original source, author, and date. -->
<!-- Author: NeaByteLab | Date: 2026-03-04T09:33:57Z | Title: Teaching a Machine to Read Digits by Hand | Source: https://neabyte.com/projects/teaching-a-machine-to-read-digits-by-hand.md -->
