The Mills · Machine learning

demographics

Neural income prediction — mills/demographics

Neural income model chart

Summary

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.

The Original

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.

Running It

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.

How It's Built

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.

Why More Than One File

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.

The Machines It Uses

MachineWhy
neuralBoth 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.)
mathBoth programs: RoundTo trims raw model output to printable precision — in the trainer's accuracy report and the predictor's income line.
moneyPredictor only: Dollars and MoneyFmt turn the model's scaled output (income × $100,000) into an exact, readable dollar figure beside the raw prediction.
fileIn 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.)
strIn 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.)
seqTaken 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.)