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,
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 politely without
a 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 priced 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 precisely the weights the
trainer found, digit for digit. That's 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.) |