iris is two programs sharing one model.
iris-train.shoddy fits a neural network — a program that
learns from examples by adjusting numbers called weights — to 120 rows of
Fisher's iris data. The network is the 4-8-3 tanh/softmax kind from the
neural machine: four inputs, a
hidden layer of eight, three outputs. It learns by mini-batch SGD with
momentum. (SGD is stochastic gradient descent — learning by many small
corrections; mini-batch means a few rows at a time.) The trainer then
evaluates the network against 30 held-out rows, kept away from training so
they can serve as a fair test. It prints a confusion matrix — a table of
true species against predicted — and writes the trained model to
dat/iris-model.bin: weights, head (the network's output
stage), and the input scaler, the recipe for rescaling raw
measurements. iris.shoddy then loads that model and
classifies interactively. It prompts for four measurements in centimetres
and names the species, with the probability it assigns to each of the
three. Answer N at the ANOTHER prompt to
exit.
It is the classification counterpart to demographics, and the contrast is the point. Demographics predicts a number — an income — and is scored on how close it gets. This mill predicts a category — a species — and is scored on how often it is right. It also reports how sure it was, which a regression network (one that predicts a number) has no way to express. Same machine, same two-program shape, different head.
It also demonstrates three things the machine could not do before:
Softmax outputs and cross-entropy loss.
Softmax turns the network's raw outputs into probabilities that add up to
one. Cross-entropy loss is the standard way to score those probabilities
against the true category. Both run through the same backward pass — the
step that works out each weight's correction — that regression uses.Scaler stored inside the model file,
so the predictor never has to know how the inputs were scaled.And it is fast. Demographics takes nine minutes to train; this
takes about four seconds. When you change
machines/neural.shoddy, this is the mill to run first.
R. A. Fisher's 1936 paper The Use of Multiple Measurements in Taxonomic Problems introduced the measurements that have been the canonical classification data set ever since: 150 flowers, 50 each of three species, four measurements apiece. It is used here precisely because the expected result is known. Setosa is linearly separable from the other two — one straight line divides it cleanly. Versicolor and virginica overlap along a handful of specimens, exactly as the chart above shows. A working network lands within a percent or two of what this one gets; anything wildly different is a bug, not a discovery.
The split is systematic: every fifth row of each species is held out,
and the other forty train. There is no shuffle, so the split is a property
of the file and anyone can reproduce it. That choice is deliberate, and
the header of dat/iris-train.dat records why. The obvious
alternative — take the last ten of each species — is a trap. The handful
of rows on the versicolor/virginica boundary sit early in their blocks, so
taking the tail puts every single hard case into training and
leaves a test set with nothing difficult in it. That split reports 100%
and has measured nothing.
From mills/iris/ — 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/iris-model.bin, ~4 seconds
./build.sh run # classify interactively from the trained model
./build.sh clean # remove built binaries
Like demographics, 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/iris.dll. Run from the mill's own directory,
because the data and model paths under dat/ are relative.
Train before you classify; the predictor aborts politely without a model.
Seed(0) at the top of training fixes the random starting
point, so every run is identical.
Seeded, the trained model scores 0.9833 on the training rows (118/120) and 0.90 on the held-out rows (27/30), and the confusion matrix says where the three errors went:
TRUE \ PRED SETOSA VERSICOLOR VIRGINICA
SETOSA 10 0 0
VERSICOLOR 0 10 0
VIRGINICA 0 3 7
Every error is a virginica called versicolor. All three are specimens whose petals fall inside versicolor's range — the overlap in the chart above, showing up exactly where the botany says it should. That gap between train and test is not a defect to tune away; it is the overlap. Configurations that drive training accuracy to 100% are memorizing those rows.
Two honest caveats belong with that number:
Everything neural comes from the
neural machine: the 4-8-3
architecture, mini-batch SGD with gradients as net-shaped values (a
gradient is the direction and size of each weight's next correction),
momentum (a velocity is a net too), and the
ModelSave/ModelLoad pair. The trainer's
hyperparameters — the training settings themselves — travel as a single
Plan value, ClassPlan(0.1, 0.9, 10, 300). So a
training run can be named and printed rather than smeared across seven
arguments.
The reporting is worth a look for its own sake. NetFit
calls the caller's report quotation — a block of code handed in as a value
— after every epoch. So the mill decides when to print and when
to pay for metrics. The confusion matrix is built from
matrix' Kron, the
equality indicator: the product of two Krons counts a row
only when it is both truly t and predicted p.
Like demographics this mill splits train once, predict many, and the interface between the halves is a file. But this one carries more than weights, and that difference is the mill's quietest lesson.
A network is not a predictor on its own. Whoever trained it also chose
a head, and chose how to centre and scale the inputs. A prediction made on
differently-scaled inputs is simply wrong, with nothing to announce it. In
demographics those decisions live in two places:
RowX in the trainer and PersonX in the predictor
each build the input vector by hand, in different files, with nothing
tying them together. They agree today. Reorder a column and nothing in the
repo would notice.
So dat/iris-model.bin holds the weights, the head, and the
scaler, and ModelPredict takes raw
centimetres and applies the scaling the file remembers. The
predictor contains no mention of scaling at all — there is no second copy
to drift. The trainer proves it rather than assuming it. Its closing
assertion feeds a raw row to the reloaded model and a hand-scaled row to
the trained net, and requires the two answers to be equal, not merely
close.
| Machine | Why | |
|---|---|---|
| neural | Both programs;
the reason the mill exists. The 4-8-3 tanh/softmax architecture and the
Classify head, NetFit with momentum and a
Plan, ScalerFit for the inputs, NetLogLoss
and NetClassAccuracy for the trainer's report, and the
ModelSave / ModelLoad pair whose bit-exact format is
the contract between the two programs. (Kron, from matrix, builds the one-hot species targets — a list with 1 in the true species' slot and 0 elsewhere. 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 loss and accuracy report and the predictor's probability
table. | |
| file | In the trainer:
ReadLines pulls the dataset off disk — the CSV loading
(comma-separated values, the plain-text table format) 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; in the predictor,
Trim tidies the typed answers, StrRep draws
the probability bars, and PadLeft / PadRight
align the report columns. (Arrives through neural's include
chain.) | |
| seq | Taken
previews the first rows of the loaded set and ZipWith pairs
truth with prediction for the confusion matrix. (Arrives through neural's
include chain. The predictor's numeric input is guarded by the
IsNumeric builtin.) |