The Mills · Machine learning

iris

Neural species classification — mills/iris

Iris petal scatter plot, three species in three colours

Summary

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.

Why This Mill Exists

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:

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.

The Data

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.

Running It

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.

What It Gets, And How To Read It

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:

How It's Built

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.

The Seam Between The Two Programs

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.

The Machines It Uses

MachineWhy
neuralBoth 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.)
mathBoth programs: RoundTo trims raw model output to printable precision — in the trainer's loss and accuracy report and the predictor's probability table.
fileIn 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.)
strIn 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.)
seqTaken 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.)