demographics is two programs sharing one model.
demographics-train.shoddy fits an 8-100-1 tanh/identity neural
network (a small program that learns number patterns from examples) to a
200-row people data set. The network comes from the
neural machine. Training runs by
mini-batch SGD, which adjusts the network a little at a time over small
random batches of rows. The trainer then evaluates the result against 40
held-out test rows, rows kept aside and never trained on. Finally it
serializes the trained model (saves it as a file, binary, bit-exact,
self-describing) to dat/people-model.bin.
demographics.shoddy then loads that model and predicts
interactively. It prompts for a person (sex, age, state, political
leaning) and builds the model's 8-element input vector (age scaled by
100, state and politics one-hot: all zeros except a single 1 marking the
choice). Then it prints the predicted income. Answer
N at the ANOTHER prompt to exit.
This mill is a port of James McCaffrey's classic C# walkthrough,
Neural Network Regression from Scratch with Batch Training
(September 2023). The reference C# source is McCaffrey's and is not
redistributed here; see THIRD-PARTY-NOTICES.md. The port is
faithful to the architecture but not to the bugs. The C# reference's
TrainBatch reads its shuffled indices without ever offsetting
by the batch number. So it trains the first ten shuffled rows twenty times
per epoch (an epoch is one full pass over the training data) and skips
the rest. The header of demographics-train.shoddy documents
that defect and deliberately does not port it. The epoch count changed
too, on evidence. A seeded convergence sweep showed training plateaus by
epoch 1600, so the port stops there instead of the original's 2000: same
accuracy, four minutes less waiting.
From mills/demographics/, the wrapper is
./build.sh, or ./build.ps1 on Windows, with the
same subcommands:
./build.sh build # weave both programs into bin/ (plain ./build.sh works too)
./build.sh train # train the model — writes dat/people-model.bin, ~9 minutes
./build.sh run # predict interactively from the trained model
./build.sh clean # remove built binaries
Unlike the games, this mill is woven: the build compiles both
programs to self-contained assemblies (finished program files) in
bin/. Once built, they run with no toolchain at all:
dotnet bin/demographics.dll. Run from the mill's own
directory, because the data and model paths under dat/ are
relative. Train before you predict; the predictor aborts with a message if there
is no model. Seed(0) at the top of training makes every run
identical.
Mind the data's edges. The model was trained on 25–66-year-olds from Michigan, Nebraska, and Oklahoma with incomes scaled by $100,000. Predictions outside that neighborhood are extrapolation (guesses beyond the data the model ever saw) and should be weighed accordingly.
Everything neural comes from the
neural machine: the 8-100-1 network,
mini-batch SGD with gradients as net-shaped values, and the
NetSaveBin/NetLoadBin pair. That pair's binary
format is self-describing and bit-exact: what the trainer writes, the
predictor reproduces digit for digit. The epoch budget is itself a worked
example of measuring before tuning. One 8-100-1 epoch on 200 rows costs
about 350 ms through the machine's immutable matrix algebra, immutable
meaning every operation returns a new matrix instead of changing one in
place. The header of demographics-train.shoddy records the
convergence sweep (accuracy at epochs 1200, 1600, and 2000) that justified
stopping at 1600.
The game mills split along the effect line: pure core, thin shell. This mill splits along a different and equally classic seam: train once, predict many. Training costs nine minutes of matrix algebra. A prediction costs a keystroke. Bolting them into one program would mean one of two bad choices: retraining on every launch, or carrying the whole training apparatus (data loading, shuffling, the SGD loop, the convergence report) inside a program whose job is to ask four questions and print a number.
So demographics-train.shoddy and
demographics.shoddy are two separate programs, woven to two
separate assemblies. The interface between them is a file:
dat/people-model.bin, written by NetSaveBin and
read by NetLoadBin. Its self-describing binary format is
bit-exact, so the predictor computes with exactly the weights the
trainer found, digit for digit. That is the same shape every production ML
system takes (a training pipeline, a serialized model artifact, and a
lean inference program, the part that answers questions) at a scale where
you can read all of both sides in one sitting.
| Machine | Why | |
|---|---|---|
| neural | Both programs;
the reason the mill exists. It supplies the Net record and the
8-100-1 tanh/identity architecture, plus mini-batch SGD with gradients as
net-shaped values for the trainer. The NetSaveBin /
NetLoadBin pair's bit-exact format is the contract between
the two programs. (Kron, from matrix, builds the one-hot state and politics inputs. The mill includes matrix and file by name, because a machine's own includes are not part of its surface.) | |
| math | Both programs:
RoundTo trims raw model output to printable precision, in
the trainer's accuracy report and the predictor's income line. | |
| money | Predictor only:
Dollars and MoneyFmt turn the model's scaled
output (income × $100,000) into an exact, readable dollar figure
beside the raw prediction. | |
| file | In the trainer:
ReadLines pulls the dataset off disk; the CSV loading is the
mill's own work, not neural's. (Arrives through neural, which includes it,
so no Include of its own is needed.) | |
| str | In the trainer:
Split and Trim carve each CSV line into numbers,
and StartsWith skips comment lines. The predictor
Trims the typed answers. (Arrives through neural's include
chain.) | |
| seq | Taken
previews the first rows of the loaded set in the trainer. (Arrives through
neural's include chain. The predictor's numeric input is guarded by the
IsNumeric builtin.) |