Stop the search rewarding candidates that change nothing - #101
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Seven commits, one thread: the search was spending most of itself on candidates that changed nothing, and rewarding itself for it. Diagnosed from a run whose output had a blue dot, a zigzag tail and a canvas that had wandered off its own viewBox.
The core defect
58% of candidates measured identically to their parent on all four objectives — 14,296 of 24,767 in one run — while none were byte-identical, so the worker's comparison (#85) caught none of them. Identical objectives cannot be ranked against the parent, so such a candidate is admitted wherever the parent sits and reports back as a survivor.
reordered elements, the operator that produces them most reliably, had taken 74% of the policy's weight.Rejecting on measures rather than bytes, and charging the operator for the draw: 0.0% in the verification run (4 of 14,057),
reordered elementsback to the floor, and the operator mix inverted toward operators that change something.Three ways a mutation could only do harm
Colours were invented.
mutate_strokeassignedrandom.choice(_NAMED_SVG_COLORS)— 18 CSS names. On a drawing in blacks and greys, fifteen of eighteen can only be wrong; that is the stray blue dot and the brown ring round the eye. Now colour is fudged, never chosen:mutate_colormoves a hex value by at most ±8 per channel (was ±60, which random-walks a grey into brown), skips anything that is not hex, andmutate_strokeno longer sets colours at all — it adds an outline in the shape's own fill, or removes one.Moves were written as transforms. A transform is a second description of where a thing is, laid over the first: it accumulated — 23 stacked translates on one background rect, which is how the canvas drifted — and it left coordinates saying one thing while the drawing did another, so every later mutation read a position that was not where the element appeared.
mutate_translatenow edits the element's own numbers: position attributes,pointspairs, and absolute path coordinates, with an arc's radii, rotation and flags left alone.Some markup could not be edited at all. Which forms a model reaches for is a property of the model: one model's seeds carried 147 elements in relative path commands, which describe an offset from wherever the pen is and so cannot be moved.
normalize_svgnow runs on everything the model returns — relative paths become absolute,polygon/polylinebecomepath, presentation properties move out ofstyle, named colours become hex, short hex becomes long. Verified pixel-identical across 20 real seeds from two models, and idempotent.The epoch criterion this started as
--epoch-eval-patiencearmed at 5, counted in evaluator checks. The unit matters: a generation is 100 accepted candidates, so a threshold in generations moves with the acceptance rate and with--pool-size, and one below a single check interval fires before a check can intervene at all.--seedsis 5 and fixed, rather thanpool_size // 10, which tied the LLM budget to a number chosen for unrelated reasons.Scripts
Checking them found four breakages.
plot_run.pycarried@functools.cacheon a plotting function — unhashable arguments, so it raised on every invocation, and had done since commit e51beb1.clean_runs.pykept zero nodes and deleted every drawing, because bare-id filenames parse asscore = infand the next line treatsinfas invalid.bench_search.pyread a column that no longer exists.The fourth was in
src:save_nodewrote0.0for any measure a node lacked.front_scoreexists only on the nodes the evaluator saw, so every other node recorded a perfect evaluator score —metrics.pywarns about this hazard for objectives and the CSV writer did it anyway. Now blank. That also showedfront_scorenever reaches lineage.csv at all, since a row is written when a candidate is admitted, before the evaluator has seen anything; the bench reads the evaluator curve fromstats.csvinstead.Measured
reordered elementsweightThe final run also produced the tail feathers for the first time, and the eye highlight inside the disc rather than bitten out of the rim.
Not established
The last run changed the model as well (
gpt-5.6-terra), so its score is not attributable to these fixes alone. The colour and normalization commits landed after it, and are unrun. And 35 of its 50 LLM calls bought no measurable gain, so--epochs 10is mostly wasted spend — worth its own look.