The World Vol 5 · Bodies and Brains
ch 54 / 105
Chapter 54

Where the Weights Come From

The flat brain row

The controller runs, and every score it produces is zero. With every weight and bias set to zero, tanh receives zero twelve times and the output row returns a six-way tie forever.

A brain here is one flat row of 378 numbers with no internal structure a copier has to respect: drawn from a stream of its own, written to a file exactly as it stands, and judged only from the outside. The creature lives with that row; the valley judges the whole result.

Gradient training needs a target answer before it can move one weight. A creature in this valley has no correct action printed beside a tick. It has a life, a death and a line in the books, so selection can compare whole brains without opening them.

The 378 positions already have meaning from the forward pass: hidden weights, hidden biases, output weights, output biases. The page reads those offsets, draws numbers at a scale set by each neuron's fan-in, writes the row, and reads it back byte for byte.

The 378 positions

Count them before writing anything. Each of the twelve middle neurons reads all twenty-four inputs, so it owns twenty-four weights: 24 × 12 = 288. Each of those twelve also owns one bias, a number added to its sum whatever the inputs are doing: 12 more. Each of the six output neurons reads all twelve middle values, so it owns twelve weights: 12 × 6 = 72. Each of the six owns a bias: 6 more. Add them. 288 + 12 + 72 + 6 = 378. That is the whole brain, and every one of those numbers has to come from somewhere.

They go in one slice. Not a struct holding four slices, and not twelve neuron values each holding a little slice of their own, because of what actually happens to a brain in this world. It gets allocated once. It gets copied. It gets compared with another brain. It gets written to a file and read back. It gets altered in ways that have no interest in which position is a weight and which is a bias. Every one of those operations is a single loop over one contiguous block of numbers, or a single call, and every one of them turns into four cases the moment the brain is four things instead of one. At three thousand creatures, four allocations apiece is twelve thousand allocations where there could be three thousand.

What the flat row costs is that a position on its own means nothing. W[295] is a bias only because 295 falls between 288 and 300, and it falls between them only because the network is 24 inputs wide with 12 in the middle. The offsets have to be worked out from the three sizes somewhere, and that somewhere already exists: the forward pass cuts W into its four blocks on every single run, and it gets the four cut points from four methods that take no arguments at all. This page adds none of its own. Everything below that needs to know where the biases start asks the same four methods the forward pass asks, because a second function computing the same offsets is a second thing that has to stay true, and it will not.

◆ Recall — the four lines everything below reads through

internal/mind already holds the count, the four offsets, and the allocation that follows from both. They are used constantly from here on, so here they are again before the new file starts.

// internal/mind/net.go

// Weights is how many numbers a network of these three sizes owns.
func Weights(in, hid, out int) int {
	return in*hid + hid + hid*out + out
}

// New is a network of these sizes with every weight left at zero.
func New(in, hid, out int) Net {
	return Net{In: in, Hid: hid, Out: out,
		W: make([]float64, Weights(in, hid, out))}
}

// HidW, HidB, OutW and OutB are where each of the four blocks starts in
// W. Nothing else anywhere works out an offset into the slice.
func (n Net) HidW() int { return 0 }
func (n Net) HidB() int { return n.In * n.Hid }
func (n Net) OutW() int { return n.HidB() + n.Hid }
func (n Net) OutB() int { return n.OutW() + n.Hid*n.Out }
▣ Build · stage 1 — a bench that works out no offsets of its own
// Command brains is chapter 54's bench: where a controller's 378
// numbers come from, how big each one is allowed to be, what it costs
// to write them down, and why nothing here computes a gradient. Every
// mode is arithmetic on drawn numbers; no valley is read and no gram
// moves.
package main

import (
	"bufio"
	"crypto/sha256"
	"encoding/hex"
	"flag"
	"fmt"
	"math"
	"os"

	"theworld/internal/mind"
)

// layout prints where each of the four blocks starts, which run of
// numbers each neuron owns, and what one thought costs.
func layout(in, hid, out, creatures int) {
	n := mind.New(in, hid, out)
	fmt.Printf("brains: one %d-%d-%d controller as a flat row of numbers\n\n", in, hid, out)
	fmt.Printf("  %-16s %7s %8s %8s   %s\n", "part", "count", "from", "to", "arithmetic")
	fmt.Printf("  %-16s %7d %8d %8d   %d inputs x %d hidden\n",
		"hidden weights", n.HidB()-n.HidW(), n.HidW(), n.HidB(), in, hid)
	fmt.Printf("  %-16s %7d %8d %8d   one per hidden neuron\n",
		"hidden biases", n.OutW()-n.HidB(), n.HidB(), n.OutW())
	fmt.Printf("  %-16s %7d %8d %8d   %d hidden x %d out\n",
		"output weights", n.OutB()-n.OutW(), n.OutW(), n.OutB(), hid, out)
	fmt.Printf("  %-16s %7d %8d %8d   one per output neuron\n",
		"output biases", len(n.W)-n.OutB(), n.OutB(), len(n.W))
	fmt.Printf("  %-16s %7d\n", "the whole brain", len(n.W))

	fmt.Printf("\n  hidden neuron j reads W[%d + j*%d : %d + j*%d + %d]\n",
		n.HidW(), in, n.HidW(), in, in)
	fmt.Printf("  output neuron k reads W[%d + k*%d : %d + k*%d + %d]\n",
		n.OutW(), hid, n.OutW(), hid, hid)
	for _, j := range []int{0, 1, hid - 1} {
		fmt.Printf("    hidden %2d: W[%3d:%3d]   bias W[%d]\n",
			j, n.HidW()+j*in, n.HidW()+(j+1)*in, n.HidB()+j)
	}
	for _, k := range []int{0, out - 1} {
		fmt.Printf("    output %2d: W[%3d:%3d]   bias W[%d]\n",
			k, n.OutW()+k*hid, n.OutW()+(k+1)*hid, n.OutB()+k)
	}

	muls := in*hid + hid*out
	fmt.Printf("\n  one tick of one brain: %d multiplications, %d additions, %d tanh\n",
		muls, muls+hid+out, hid)
	fmt.Printf("  %d creatures, all thinking once: %d multiplications a tick\n",
		creatures, muls*creatures)
}
$ go run ./cmd/brains -mode row
brains: one 24-12-6 controller as a flat row of numbers

  part               count     from       to   arithmetic
  hidden weights       288        0      288   24 inputs x 12 hidden
  hidden biases         12      288      300   one per hidden neuron
  output weights        72      300      372   12 hidden x 6 out
  output biases          6      372      378   one per output neuron
  the whole brain      378

  hidden neuron j reads W[0 + j*24 : 0 + j*24 + 24]
  output neuron k reads W[300 + k*12 : 300 + k*12 + 12]
    hidden  0: W[  0: 24]   bias W[288]
    hidden  1: W[ 24: 48]   bias W[289]
    hidden 11: W[264:288]   bias W[299]
    output  0: W[300:312]   bias W[372]
    output  5: W[360:372]   bias W[377]

  one tick of one brain: 360 multiplications, 378 additions, 12 tanh
  3000 creatures, all thinking once: 1080000 multiplications a tick

Read the printing code against the table it printed. Not one from and not one to in that output is arithmetic this file did: they are n.HidW(), n.HidB(), n.OutW() and n.OutB(), asked and printed. The counts are subtractions between two of them. Change the middle layer from twelve neurons to twenty and every number on the page moves: the first block grows to 480, the biases start at 480, the whole brain becomes 626. Nothing here has to be edited for that to come out right, and nothing here can drift away from what the forward pass believes, because it is asking the forward pass what it believes.

Note the last two lines as well. The multiplications are the entire arithmetic cost of a brain thinking once, and three thousand of them come to a little over a million a tick. That is a number to hold on to.

The 378 numbers of one brain, laid out as four spans of a single row A long horizontal bar standing for one brain's flat slice of 378 numbers, divided into four blocks in proportion to their sizes. The first and much the largest block runs from position 0 to 288 and holds the hidden weights. A very narrow block from 288 to 300 holds the twelve hidden biases. A block from 300 to 372 holds the output weights. A very narrow block from 372 to 378 holds the six output biases. Below, the first block is drawn again zoomed out into twelve equal segments, one per hidden neuron, the first covering positions 0 to 24 and the last covering 264 to 288. ONE BRAIN: 378 NUMBERS IN ONE UNDIVIDED ROW W[0:288] W[300:372] 0 288 300 372 378 W[ 0:288] 288 hidden weights, 24 to a neuron W[288:300] 12 hidden biases, one to a neuron W[300:372] 72 output weights, 12 to a neuron W[372:378] 6 output biases, one to a neuron THE FIRST BLOCK, ONE NEURON AT A TIME 0 1 2 3 4 5 6 7 8 9 10 11 W[0:24] W[264:288] twelve neurons, twenty-four weights each, end to end
Figure 54.1 — the blocks are drawn to scale, which is the point of drawing them at all: three quarters of a brain is the weights between the sensor row and the middle layer, and the eighteen biases together are a sliver. The bottom bar is the first block opened up. Hidden neuron 7 owns W[168:192] and its bias is W[295], and both of those addresses are 24, 12 and 6 multiplied and added.

The fan-in scale

A weight has to be some number, and the range it is drawn from is a decision. Between −1 and 1 looks harmless. Every sense arrives already between −1 and 1, so weights on the same scale seem to match. Work out what the middle layer then has to add up and the trouble appears.

One middle neuron adds twenty-four products and a bias. Suppose every input is at the top of its range and every weight came out at 1: the sum is 24, and tanh of 24 is 1 to fifteen decimal places. Suppose instead the weights come out half positive and half negative and cancel exactly: the sum is 0. Neither of those is what happens. What happens is a walk of twenty-five steps, some forward and some back, and the useful question is how far from the start such a walk usually finishes.

∑ Interlude — the usual size of a long sum

Start with the size of one drawn number. A weight taken evenly from between −1 and 1 is sometimes 0.9 and sometimes 0.03, so no single value describes it. The measure that does is the one Len already applies to the two numbers of a vector: square them, average the squares, take the square root back off. Do that to a great many numbers drawn evenly between −1 and 1 and the answer is 0.5774, which is the square root of one third. Call that the usual size and write it s.

Now add n of them together, each one independent of the others. Their usual sizes do not add. What adds is the squares, so the sum of n numbers of usual size s has usual size s multiplied by the square root of n. Twenty-four inputs and one bias make twenty-five numbers going into a middle neuron, so in plain figures:

usual sum = 0.5774 × √25 = 0.5774 × 5 = 2.8868

That is the same as √(25 ÷ 3), and it is the number the run below predicts before it measures anything. What does tanh do with a sum of 2.8868? It returns 0.9938. The squash is flat there. A middle neuron sitting at 0.9938 answers almost the same 0.99-something whatever its inputs do, and a neuron that always says the same thing has quietly become a nineteenth bias.

The repair is division. Draw each weight evenly between −1 and 1 as before, then divide it by the square root of how many numbers its neuron adds up. Call that count the fan, f. Every weight shrinks by √f, so the usual sum shrinks by √f too:

usual sum = √((f + 1) ÷ 3) ÷ √f

At a fan of 24 that is 2.8868 ÷ 4.899 = 0.5893, and tanh of 0.5893 is 0.5292, which is on the sloped part of the curve where a change in the inputs still changes the answer. The better property is what happens as the fan grows. At a fan of 384 the division gives 0.5781, and at a fan of a million it would give 0.5774. The rule is a rule instead of a number: it holds at any layer size, so a network whose sizes change never needs its weights re-tuned by hand.

sthe usual size of one drawn number, ignoring its sign: 0.5774 for a draw between −1 and 1
nhow many numbers are being added together
fthe fan: how many inputs one neuron reads, 24 in the middle layer and 12 in the output layer
√xthe square root of x: the number that gives x when multiplied by itself
tanh(x)the squash on the middle layer, flattening toward −1 and 1 at either end
▣ Build · stage 2 — the draw, and the stream it comes off
// internal/mind/weights.go
package mind

import (
	"encoding/binary"
	"math"
	"math/rand/v2"
)

// Fan is how many numbers a neuron in each row adds up: a hidden neuron
// reads all In of the inputs, an output neuron reads all Hid of the
// middle values. It is the only thing the size of a drawn weight
// depends on, and it is read off the sizes rather than stated anywhere.
func (n Net) Fan() (hid, out int) { return n.In, n.Hid }

// StreamBrain is where a brain's numbers come from: stream 14 on the
// world's seed, continuing the registry the terrarium opened. Nothing
// else in the world draws from it, so a valley that gains a creature
// does not move a plant's numbers.
const StreamBrain = 14

// Draw fills a brain's whole row from one stream, in flat-slice order,
// one number per draw. Every number lands between -1 and 1 and is then
// divided by the square root of the fan its neuron adds up, so a
// hidden neuron reading twenty-four inputs gets smaller weights than an
// output neuron reading twelve. The order is the contract: a brain is a
// fixed number of steps down the stream, and two runs of one seed hand
// the same creature the same brain.
func (n Net) Draw(r *rand.Rand) {
	hid, out := n.Fan()
	small := [2]float64{1 / math.Sqrt(float64(hid)), 1 / math.Sqrt(float64(out))}
	split := n.OutW()
	for i := range n.W {
		layer := 0
		if i >= split {
			layer = 1
		}
		n.W[i] = (r.Float64()*2 - 1) * small[layer]
	}
}

// Brains opens the one generator a valley draws its brains from.
func Brains(seed uint64) *rand.Rand {
	return rand.New(rand.NewPCG(seed, StreamBrain))
}

// Bits is the row as raw bytes, every number in flat-slice order,
// eight bytes each, most significant first. It is what a digest is
// taken over: two brains agree here or they are two different brains.
func (n Net) Bits() []byte {
	out := make([]byte, 8*len(n.W))
	for i, w := range n.W {
		binary.BigEndian.PutUint64(out[8*i:], math.Float64bits(w))
	}
	return out
}

The single comparison i >= split is what splits the row into the two fans, and split is n.OutW(), the same offset the forward pass slices at. Everything before it belongs to a middle neuron reading twenty-four inputs; everything from it onward belongs to an output neuron reading twelve, biases included, since a bias is added into the same sum as the weights beside it and has to arrive on the same scale. Note the loop draws once for every position, all 378 of them, biases and all. That is not an accident of writing: a brain has to be a fixed number of steps down the stream, because otherwise the tenth creature born in a run gets different numbers depending on what the ninth one turned out to be.

The bench checks the interlude's prediction against a measurement, and it does it on a layer of four thousand neurons instead of twelve. Twelve is a small enough sample that one lucky draw moves the answer by a lot, and the claim being tested is about the arithmetic, not about one brain. Every neuron is fed the noisiest row its inputs allow: every entry at the far end of its range, alternating sign so nothing cancels for a tidy reason. Both halves of the table are the same draw off the same stream, so the only difference between them is the division.

▣ Build · stage 3 — the same layer at five different fans
// loud is the noisiest row a layer's inputs allow: every entry at the
// far end of its range, alternating in sign so nothing cancels for a
// tidy reason.
func loud(n int) []float64 {
	x := make([]float64, n)
	for i := range x {
		x[i] = 1
		if i%2 == 1 {
			x[i] = -1
		}
	}
	return x
}

// spread is what a whole layer of sums came to: the usual size of one
// of them, measured the way the interlude defines it, the average size
// of what tanh made of it, and the share of the layer tanh answered
// past the pin with.
func spread(sums []float64) (usual, squashed, pinned float64) {
	for _, z := range sums {
		usual += z * z
		t := math.Abs(mind.Tanh(z))
		squashed += t
		if t > 0.99 {
			pinned++
		}
	}
	n := float64(len(sums))
	return math.Sqrt(usual / n), squashed / n, pinned / n
}
$ go run ./cmd/brains -mode fan
brains: a layer of 4000 neurons fed the loudest row its inputs allow,
  drawn off stream 14 and used twice: as drawn, then divided by the root of the fan

  inputs    root    predict    usual   |tanh|  pinned    predict    usual   |tanh|  pinned
       4   2.000      1.291    1.306   0.6372      4%      0.645    0.653   0.4300      0%
      12   3.464      2.082    2.098   0.7525     21%      0.601    0.606   0.4050      0%
      24   4.899      2.887    2.877   0.8156     36%      0.589    0.587   0.3957      0%
      96   9.798      5.686    5.743   0.9046     64%      0.580    0.586   0.3967      0%
     384  19.596     11.328   11.422   0.9520     82%      0.578    0.583   0.3937      0%

  left half: every number between -1 and 1. right half: the same numbers over the root of the fan.
  predict is sqrt((inputs+1)/3) times the size the numbers were drawn at.
  usual is the measured one: every sum squared, averaged, and the root taken back off.
  pinned is the share of the layer whose tanh came back past 0.99.

Read the two predict columns against the two usual columns first, because that is the interlude being checked: 2.887 predicted and 2.877 measured at twenty-four inputs, 11.328 predicted and 11.422 measured at three hundred and eighty-four. The arithmetic was done on paper and the run agrees with it to about a percent, which is what four thousand samples buys.

Now read the pinned columns, which say what it costs. Left, with the division left out, more than a third of the layer is past 0.99 at twenty-four inputs and four fifths of it is past 0.99 at three hundred and eighty-four. Every pinned neuron has stopped reporting: it will say 0.995 to this sensor row and 0.996 to a completely different one, and its contribution to the six scores is very nearly a constant. Right, with the division in, the pinned column reads 0% at every size on the table and the average middle value sits near 0.40 whatever the fan. This valley's own controller is the 24 line, so it is the mild case, and the rule is what keeps it mild.

One brain is 378 of those draws taken in a row, and the second brain starts where the first one stopped. What that buys is the whole reason a world made of drawn numbers can be replayed, so it gets measured here instead of assumed.

▣ Build · stage 4 — four brains off stream 14, twice
// digest is the sixteen hex digits this bench compares brains by,
// folded the way the valley folds its own state: sha256 over the raw
// bytes of every weight in order, cut to the first sixteen. Two brains
// agree here only if every one of their numbers is the same number
// down to the last bit.
func digest(n mind.Net) string {
	sum := sha256.Sum256(n.Bits())
	return hex.EncodeToString(sum[:])[:16]
}

// brainAt is the k'th brain a valley draws: open stream 14 on the world
// seed, throw away the brains born before this one, and take the next
// row of numbers off it.
func brainAt(seed uint64, k, in, hid, out int) mind.Net {
	r := mind.Brains(seed)
	n := mind.New(in, hid, out)
	n.Squash = mind.Tanh
	for range k {
		n.Draw(r)
	}
	n.Draw(r)
	return n
}
$ go run ./cmd/brains -mode draw
brains: 4 of them drawn off stream 14 on world seed 5

  brain         W[0]         W[1]       W[377]   digest
      0   0.11373768   0.15953022   0.06711743   a978c376398aa14e
      1   0.12787853  -0.08717750  -0.15217619   a6ce5d024e4641ad
      2   0.13539839   0.03748572   0.24593342   c90a2c7c021463d2
      3  -0.00090827   0.11102333  -0.08975907   36797ad6205e4801

  the same seed again, from a generator that has never been used:
      0   0.11373768   0.15953022   0.06711743   a978c376398aa14e
      1   0.12787853  -0.08717750  -0.15217619   a6ce5d024e4641ad
      2   0.13539839   0.03748572   0.24593342   c90a2c7c021463d2
      3  -0.00090827   0.11102333  -0.08975907   36797ad6205e4801

  4 of 4 brains came back identical, bit for bit
  brain 3 skipped to directly: 36797ad6205e4801

Four brains, four digests, no two alike, and the whole set repeats exactly when a fresh generator is opened on the same seed. The last line is the property that matters most and is easiest to lose. It was produced by brainAt: open the stream, throw away three brains' worth of draws without keeping a single number, then draw the fourth. The digest is 36797ad6205e4801, the same as the fourth brain in the table above. Because every brain costs exactly 378 draws, position in the stream is arithmetic, and a creature born fourth gets the fourth brain whatever happened to the three before it.

The saved brain file

Everything so far lives in memory and dies with the process. That is fine while a run is a few seconds of bench, and useless the moment a valley is stopped and started again, or a brain has to be looked at by something that is not this program. The row has to become a file.

One line of JSON per brain, appended, never rewritten. It is the same discipline the world's event log has kept since it was first opened: a file made of lines can be appended to safely, read one item at a time without holding the whole thing in memory, counted with wc -l, and diffed. A single JSON document wrapping every brain in one array has none of those properties and buys nothing in exchange.

What goes on the line is the row and the three sizes, and nothing else. Not the seed the brain was drawn from, because a seed is a recipe for a row only for as long as nothing has since touched the numbers, and a file that stores a recipe instead of the thing quietly stops being true the first time it is wrong. Not a version stamp, because the three sizes already say everything a reader has to check.

▣ Build · stage 5 — one brain to a line, and the check on the way back in
// internal/mind/file.go
package mind

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
)

// Brain is one controller written down: the name it answers to, the
// three sizes it was built at, and its whole row of numbers. Nothing
// else is stored, because nothing else is the brain.
type Brain struct {
	ID  int       `json:"id"`
	In  int       `json:"in"`
	Hid int       `json:"hid"`
	Out int       `json:"out"`
	W   []float64 `json:"w"`
}

// Save writes brains one JSON object to a line, in the order they are
// handed over. A row whose length disagrees with the sizes beside it is
// refused here, where the caller still knows which brain it was.
func Save(w io.Writer, brains []Brain) error {
	enc := json.NewEncoder(w)
	for _, b := range brains {
		if want := Weights(b.In, b.Hid, b.Out); len(b.W) != want {
			return fmt.Errorf("save brain %d: %d numbers for a %d-%d-%d net, which needs %d",
				b.ID, len(b.W), b.In, b.Hid, b.Out, want)
		}
		if err := enc.Encode(b); err != nil {
			return fmt.Errorf("save brain %d: %w", b.ID, err)
		}
	}
	return nil
}

// Load reads back what Save wrote, one line at a time, and checks every
// row against the sizes written beside it. The scanner is given a
// megabyte because one line here is thousands of numbers long and the
// default would stop reading in the middle of a brain.
func Load(r io.Reader) ([]Brain, error) {
	var out []Brain
	sc := bufio.NewScanner(r)
	sc.Buffer(make([]byte, 0, 64*1024), 1<<20)
	for line := 1; sc.Scan(); line++ {
		var b Brain
		if err := json.Unmarshal(sc.Bytes(), &b); err != nil {
			return nil, fmt.Errorf("brain file line %d: %w", line, err)
		}
		if want := Weights(b.In, b.Hid, b.Out); len(b.W) != want {
			return nil, fmt.Errorf("brain file line %d: %d numbers for a %d-%d-%d net, which needs %d",
				line, len(b.W), b.In, b.Hid, b.Out, want)
		}
		out = append(out, b)
	}
	if err := sc.Err(); err != nil {
		return nil, fmt.Errorf("brain file: %w", err)
	}
	return out, nil
}
$ go run ./cmd/brains -mode file
brains: 4 of them written to brains.jsonl, 31040 bytes

  the first line, cut off at 96 characters:
  {"id":0,"in":24,"hid":12,"out":6,"w":[0.11373767798729006,0.15953021871777115,0.1650562778221637...

  brain                drawn            read back  same
      0     a978c376398aa14e     a978c376398aa14e  true
      1     a6ce5d024e4641ad     a6ce5d024e4641ad  true
      2     c90a2c7c021463d2     c90a2c7c021463d2  true
      3     36797ad6205e4801     36797ad6205e4801  true

  4 of 4 rows survived the trip to disk with every bit intact
  W[0] drawn      0.11373767798729006
  W[0] read back  0.11373767798729006

The digests are the claim. They are taken over the raw eight bytes of every number in order, not over the printed text, so two rows agreeing here have agreed bit for bit. All four survived, and the reason is the seventeen digits in the last two lines: encoding/json writes the shortest run of decimal digits that reads back as exactly the same float64, and 0.11373767798729006 is that run for this number. It is not a rounded version of the weight. It is the weight.

The size is honest about what the format costs. Four brains take 31,040 bytes, so one brain is 7,760, against the 3,024 bytes its 378 numbers occupy in memory. Text is about two and a half times the price of the numbers, and it buys a file a person can open, search, and compare line by line without a tool. For thousands of brains a run that trade stays reasonable; for millions it would not, and the digest is what would tell you when to change it.

There is an obvious improvement available here, and it is the one to run rather than argue about. Seventeen digits a number is unreadable. Four decimal places is what every table in this book prints, it lines up in columns, and a weight of 0.0660 is surely the same weight as 0.06595034566088936 for any purpose a creature has.

⚠ Worked failure — the file a person can read
// cmd/brains/main.go — inside tidy: one brain written twice, the
// second time with every number put through -places decimals
	n := brainAt(seed, which, in, hid, out)

	round := mind.New(n.In, n.Hid, n.Out)
	round.Squash = n.Squash
	for i, w := range n.W {
		s := fmt.Sprintf("%.*f", places, w)
		var v float64
		fmt.Sscanf(s, "%g", &v)
		round.W[i] = v
	}
$ go run ./cmd/brains -mode tidy -brain 11 -steps 200
brains: brain 11 written twice, once whole and once to 4 places

                                  W[0] digest
  as drawn         0.06595034566088936 0a9391b5cbf1a897
  as written                     0.066 8156971184afbceb

  378 of the 378 numbers came back a different number
  the largest move is W[82], -0.04145000774514353 to -0.0415, a gap of 5.00e-05

  the 9 rays swept together in 201 steps, and the two brains asked at each:
      rays  as drawn  reloaded     margin   reloaded
    0.8650      walk      walk   0.001817   0.001858
    0.8700      walk      walk   0.000905   0.000945
    0.8750     right      walk   0.000004   0.000036
    0.8800     right     right   0.000909   0.000869
    0.8850     right     right   0.001810   0.001771

  the two brains disagreed on 1 of the 201 steps
  only the steps where a winner's margin fell under 0.002 are printed

The symptom is the second digest. 0a9391b5cbf1a897 went in and 8156971184afbceb came out, so the brain that was loaded is not the brain that was saved, and every run built on the loaded one has left the run that saved it. The middle lines say how far: all 378 numbers moved, and the largest single move is five hundred-thousandths, at position 82.

Five hundred-thousandths sounds like nothing, and the objection deserves an answer rather than a shrug. The answer is in the last block. Sweeping the nine ray distances in two hundred steps instead of ten finds the place where this brain changes its mind, at a ray reading of 0.8750, and there the winning score is ahead of the runner-up by four millionths. The rounding is more than ten times that gap. One of the two hundred and one steps comes out differently: the whole brain turns right, the reloaded brain walks, and the reloaded one is not even hesitant about it, holding walk ahead by nine times the margin the whole brain gave right. Every other step agrees, which is the trap. A fault that shows up in one case in two hundred is a fault that gets committed.

The general rule is short. A stored number is only as good as the smallest difference the program will act on. These six scores are compared with each other and nothing else, so there is no size below which a difference stops mattering; two scores can be as close as float64 allows and the action still turns on which is larger. Any rounding at all in the file puts a floor under the precision of the world and leaves you guessing where it will bite. Write the numbers whole, and let the file be ugly.

The missing target answer

Now the question this chapter has been circling. The weights are drawn and never improved. Why is there no training loop anywhere in these creatures, when training is the entire reason networks are interesting? The run below is the argument. It takes one drawn brain and a sensor row typed out by hand, holds every entry of that row still except the nine ray distances, and walks those from a plant pressed against the eye out to a valley with nothing in range.

▣ Build · stage 6 — six scores answering a changing valley
// Where each reading sits in the sensor row, in the order the senses
// were fixed in: nine rays, two numbers to a ray, then the two slopes,
// then the store and the speed. This bench never reads a valley, so
// these are the only positions it needs.
const (
	rays    = 9              // vision rays in the fan
	senses  = 2*rays + 4 + 2 // and the whole row they open
	wetX    = 2 * rays       // the moisture slope, going east
	wetY    = wetX + 1       // and going south
	foodX   = wetY + 1       // the standing-tissue slope, going east
	foodY   = foodX + 1      // and going south
	storeAt = foodY + 1      // the store, as a share of full
	speedAt = storeAt + 1    // the speed, as a share of top
)

// eye is where ray i's two numbers sit: how far it went at eye(i), and
// what it met at eye(i)+1.
func eye(i int) int { return 2 * i }

// The action table, in the order the six output scores are read in.
// The two turns are shortened here to keep a column narrow.
var actions = [6]string{"rest", "walk", "sprint", "left", "right", "bite"}

// The row every swept question below is asked with: twenty-four numbers
// typed here rather than read off a valley. A creature standing still
// with a quarter of a store, on ground that gets wetter and greener to
// the east, with nine rays that have met nothing at all.
func row() []float64 {
	in := make([]float64, senses)
	for i := range rays {
		in[eye(i)] = 0.5
		in[eye(i)+1] = 0.0
	}
	in[wetX], in[wetY] = 0.10, -0.05
	in[foodX], in[foodY] = 0.30, 0.15
	in[storeAt] = 0.24
	in[speedAt] = 0.00
	return in
}

// scores runs one sensor row through a brain and hands back the six
// numbers, the action that wins, and how far ahead of the runner-up it
// finished.
func scores(n mind.Net, in, hid, out []float64) (best int, margin float64) {
	n.Forward(in, hid, out)
	best, second := 0, 1
	if out[1] > out[0] {
		best, second = 1, 0
	}
	for i := 2; i < len(out); i++ {
		switch {
		case out[i] > out[best]:
			best, second = i, best
		case out[i] > out[second]:
			second = i
		}
	}
	return best, out[best] - out[second]
}
$ go run ./cmd/brains -mode sweep -brain 11 -what rays
brains: brain 11 off stream 14, digest 0a9391b5cbf1a897
  one sensor row held still but for "rays", swept from 0.00 to 1.00

     rays      rest      walk    sprint      left     right      bite   picks     margin
     0.00   -0.1599    0.3069   -0.0597   -0.0350    0.1138   -0.1192   walk      0.1931
     0.10   -0.1164    0.3108   -0.0695   -0.0617    0.1401   -0.1396   walk      0.1706
     0.20   -0.0732    0.3139   -0.0783   -0.0887    0.1664   -0.1599   walk      0.1475
     0.30   -0.0304    0.3164   -0.0862   -0.1158    0.1924   -0.1799   walk      0.1240
     0.40    0.0116    0.3182   -0.0932   -0.1427    0.2177   -0.1995   walk      0.1006
     0.50    0.0527    0.3195   -0.0994   -0.1692    0.2420   -0.2184   walk      0.0775
     0.60    0.0926    0.3204   -0.1048   -0.1948    0.2651   -0.2365   walk      0.0552
     0.70    0.1311    0.3208   -0.1095   -0.2195    0.2868   -0.2537   walk      0.0340
     0.80    0.1681    0.3209   -0.1136   -0.2430    0.3069   -0.2698   walk      0.0140
     0.90    0.2035    0.3207   -0.1172   -0.2652    0.3252   -0.2848   right     0.0045
     1.00    0.2373    0.3204   -0.1203   -0.2859    0.3418   -0.2987   right     0.0214

  2 of the 6 actions won a step, and the winner changed hands once

Every one of the six columns moves smoothly. Not one of them jumps, and the column that wins most of the table barely moves at all: walk travels from 0.3069 to 0.3209 and turns back down again, a span of fourteen thousandths across the whole sweep. The picks column does something else. It holds at walk for nine rows, and on the tenth it becomes right and stays. Look at the margin beside it. At 0.80 the winner is ahead by 0.0140; one row later the winner is a different action, ahead by 0.0045. The behaviour did not bend. It switched.

That switch is the first of three things standing between this world and a gradient. A gradient is a statement about slope: change this weight by a very small amount and the thing you care about changes by so much, in this direction. The thing cared about here is which action the creature takes, and the last piece of the pipeline that produces it keeps the largest of the six scores and throws the rest away. Keeping a largest has no slope. Nudge a weight and either the same action wins by a slightly different margin, in which case nothing changed, or a different action wins, in which case everything did. There is no small change in behaviour to divide by a small change in weight.

The second obstacle is worse and simpler. To measure how wrong an output was you must know what the right output was. Nobody knows. There is no table saying that on tick 4,102 a browser standing on wet soil with a stand two cells to its left and a store at 0.62 should have scored walk at 0.71. Such a table would be a hand-written policy copied into six numbers, and a network trained to imitate a hand-written policy is a slow way to run the hand-written policy. The interesting behaviour is exactly the behaviour nobody can write down.

The third is that the only honest measure of a creature is separated from its weights by the entire world. Whether a row of 378 numbers was any good is answered by whether the creature carrying it was still standing at the end of the year, and between the weights and that answer sit thousands of ticks, a valley of plants growing and being eaten, a calendar, other creatures, and a lot of luck. There is no chain of multiplications running from the answer back to W[82].

Selection asks for none of that. It needs an ordering: some way to say this brain did better than that one, which the store of energy in a living creature supplies for free. And it needs a copy: some way to make a new brain out of an old one, which is a single copy of 378 float64s. It never asks which weight was responsible, never needs a target, and never has to differentiate anything. The price is that it learns far more slowly than a gradient would, and it pays that price in a currency this world has in quantity: ticks nobody is watching.

One number gives that trade its scale. A brain thinking once costs 360 multiplications, and three thousand of them cost 1,080,000 a tick. Computing a gradient means walking the network backwards afterwards, touching all 378 weights again for every creature, on top of a forward pass the tick is already paying for. Selection adds nothing per tick at all. Its cost lands somewhere else entirely, and nowhere inside the 50 milliseconds a tick has to spend on creatures.

Why selection treats the row whole

Strip the network out and a pattern is left that turns up whenever the thing judging a design sits outside the design. If every judgement is made on the whole candidate, then the parts never have to be addressed by name, and the representation should be as featureless as it can be: a fixed-length block of numbers, laid out by arithmetic, with no pointers, no nesting, and no invariants that a copier has to know about. Copying is one call. Comparing is one loop. Writing it down is one line. Altering it is picking a position and putting a different number there.

Compare that with a representation carrying structure: a tree of neurons, a graph of connections held together by pointers. Every one of those operations becomes a piece of code that has to understand the structure, and every one is a place to get it wrong. This valley already made the same choice twice without calling attention to it. The picture on screen is one flat block of pixels addressed by row times width plus column. The soil under the valley is two numbers a cell in one flat block, which is what lets a whole terrarium be folded into a digest in a few lines. A brain is the third.

Laid-out-by-arithmetic carries an obligation, and this chapter is the place it shows. The arithmetic has to live in exactly one function, and every reader of the block has to go through that function, however tempting it is to write In*Hid again in the file you happen to be in. Two copies of a layout agree perfectly until the day one of them is edited.

There is a standard in the file half of it as well, and it is not about neural networks at all. When you save state that a program will act on, the question to ask is not "how many digits look sensible" but "what is the smallest difference this program can behave differently on". For a score compared only against other scores, the answer is: any difference at all. So the file keeps everything, and the check that it kept everything is a digest over the bits, not an eyeball over the text.

✓ Checkpoint — the row, the draw, and the file
  • Given a network of 24 inputs, 12 middle neurons and 6 outputs, produce 378 from the four multiplications, and say which positions hold the biases without looking anything up.
  • Handed a middle layer of 20 neurons instead of 12, state the new offsets — 480, 500, 620 — and the new total of 626, and name the four methods the bench asked to print them.
  • Say why a weight is divided by the square root of its neuron's fan, predict a usual sum of 2.8868 for twenty-four inputs drawn without that division, and name what tanh does to a number that size.
  • Explain why Draw takes exactly 378 numbers off the stream even though the biases could be started at zero, in terms of what the tenth creature born in a run receives.
  • Shown a saved-and-reloaded brain whose digest changed, check the printed precision of the file before suspecting the loader.
  • Give the three reasons no gradient is computed here, and say what selection needs instead of a target and a slope.
⚡ Exercises — try first, then reveal
Exercise 1 — move the middle layer. Run go run ./cmd/brains -mode row -hidden 20. Predict all four offsets and the total before you look, then predict what happens to the digests in -mode draw -hidden 20.

Hidden weights become 24 × 20 = 480 and start at 0. Hidden biases are 20 and start at 480. Output weights become 20 × 6 = 120 and start at 500. Output biases are 6 and start at 620, and the brain is 626 numbers long. Not a line of the bench had to be edited to print any of that, because it prints what the four methods answer. The digests in -mode draw all change, and not only because the row is longer: a brain now costs 626 draws instead of 378, so brain 1 starts at a different place in stream 14 and is a different brain even in the positions the two layouts share.

Exercise 2 — find the precision that is safe. Run -mode tidy -brain 11 -steps 200 at -places 5, then 6, then 8, then 10, and find the first setting at which the digest still changes but the two brains never disagree. Then argue about whether that setting is safe.

Five places is already enough: the largest move drops from 5.00e-05 to 4.97e-06 and the two brains agree on all 201 steps, while the digest keeps changing at six, at eight, and at ten. It is easy to find a number of places those 201 steps agree at, because 201 steps sample a very small part of what a creature will actually meet. The argument is the point. Somewhere in a thousand years of ticks a decision will be made on a margin smaller than whatever floor you install, and the run where it happens will not announce itself. The digest changing is the honest signal, and it changes at every setting short of the whole number. Nothing below "keep all of it" can be defended, only got away with.

Exercise 3 — make a deaf layer on purpose. Using the fan table's left half, work out what fan would put the usual sum at 10 for weights drawn between −1 and 1, then run -mode fan and read the pinned column at that size. Then say what a creature with such a middle layer would do.

A usual sum of 10 needs √((f+1) ÷ 3) = 10, so f + 1 = 300 and the fan is 299. The table brackets it: at 96 inputs the predicted sum is 5.686 and 64% of the layer is pinned, and at 384 it is 11.328 and 82% is pinned. A layer that far gone reports almost the same twelve numbers for every sensor row it is ever shown, so the six output scores are almost the same six numbers too, and the creature picks the same action in every situation it will ever be in. It is the zero-weight brain from the first page of this chapter with extra weights: it has senses, it reads them, and its behaviour does not depend on them.

A brain now exists, it is reproducible from a seed, and it can be put on disk and brought back exactly. What it has never been given is a real sensor row. The twenty-four numbers the bench swept were typed into a Go file by hand, and the six scores it printed were read by a table of labels rather than by anything that can charge a store or take a gram off a stand. Both ends are still loose: the senses that fill the row belong to a creature standing on a cell in The Hollow, and the six numbers have to become one legal, paid-for action.