Reading one hand
Episode 08 · 2 Nov 2026

One recipe, 136 training runs

How personal v3.1-stable got its six settings: two searches on a rented GPU, a lucky winner caught, five seeds per finalist, and $1.99 for a model that reads the writer at 6.8% error.

TL;DR

Method

The question Personalising has six settings that pull against each other. Which values did v3.1-stable end up with, what evidence put each one there, and what did finding them cost?

To fit the general model to one writer, six settings have to be chosen, and they pull against each other. A search tried 120 combinations on a rented GPU; its winner turned out to be lucky, so the finalists were each trained five times and the plan that was best on average won. The result reads the writer's test lines with 41 wrong letters of 600, for less than two dollars.

  • Personal v3.1-stable reads the 24 test lines at 6.8% error (41 letters of 600), from 10.3% for v2.1-personal, and reads 95% of lines within 45.1 ms.
  • Its plan: 8 gentle epochs, 70 varied copies of each of the writer's 68 lines, 361 synthetic lines mixed in, nothing frozen. Over five seeds it averages 11.4% on the dev lines.
  • Two searches, 120 trials, found two valleys: short and hot (2 epochs), long and gentle (8). Freezing any layer never helped.
  • The short plan's 10.9% was luck: made again from its recipe, it scored 15.6%, and over six runs it averaged 13.9%.
  • 136 training runs, about an hour of rented GPU, $1.99 on the bill. The same search on the laptop: 12 hours or more.
The lesson

Treat a hyperparameter search as a shortlist, not an answer: widen it when the best sits at the edge of a range, and rerun the finalists with several seeds before believing any of them.

Why read on Even if all 60 trials of a search were the same plan, the best of them would look 2.3 spreads better than the plan really is. How that caught the winner, and the fifteen-run chart that chose instead.

1 The question

Episode 7 told all of iteration 3. This one follows one step of it, fitting the model to the writer, through every training run it took.

The base model The general reader, trained on lots of synthetic handwriting before it ever sees yours. Changing it: A better base gives a better personal model: it starts lower and learns your hand faster. Alone it can still read your writing badly. Here: From v1 on, trained on a cloud GPU from a recipe; v2's base alone gets 43.6% of your letters wrong, 13.1% once personalised. reads handwriting in general. fine-tuning Continuing to train an already-trained model for a short while on new data: here, your own lines. Changing it: It adapts a general model to your hand from very little data. Too long, or with too high a learning rate, and it memorises your lines or forgets general writing. Here: 40 varied copies of each of your 25 lines, plus 2,000 synthetic lines replayed. It took v2 from 43.6% to 13.1% errors. trains it a little more on one writer's own lines, so it learns their hand. How base v3 was built is in episode 7; here it is the starting point.

Personalising has six settings, and the right value of each depends on the others. This episode is the lab notebook of how they were chosen: every search trial, every repeat, what each cost, and why the recipe says what it says.

Personal v3.1-stable, test lines
6.8%
41 of 600 letters; v2.1-personal: 62
Training runs to choose it
136
120 search trials, then 16 models
Rented GPU time
64 min
an A10G, rented by the second
On the bill
$1.99
for all of personalising

2 The day, step by step

Thirteen steps between the base model and the release. Most cost nothing; the two searches cost most; two steps went wrong and are kept here.

  1. Pick the starting pointfree
    Base v3: 27.2% on the test lines before personalising, the same size and speed as v2.1-personal, and it has never seen a line of the writer's.
  2. Join both recording roundsfree
    85 training lines (one round had been uploaded twice: 40 duplicates kept once). A fifth of each language, 17 lines, is set aside as dev lines; 68 are learned from.
  3. Time the laptopfree
    At most 12.9 lines a second: a thorough search here would take 12 hours or more. So the search goes to a rented GPU (section 5).
  4. First attempt stops before the GPUfree
    Making the synthetic lines to mix in failed: they needed the bank of hand-like words the base's own lines were made with. A bug, fixed, with a test that fails without the fix.
  5. Search 1: 60 trials19 min · $0.52
    Best: 10.9% on the dev lines, against 13.4% for v3's own plan. But at the edge of two ranges (section 6).
  6. Search 2: 60 trials, wider ranges31 min · $0.84
    Best: 10.4%, in the middle of the new ranges (section 7).
  7. Make both bests again, test what they forget$0.04
    Two letters apart, so a rule written beforehand decides: the one that forgets less. The short plan becomes the recipe for v3.1 (section 8).
  8. v3.1 from its recipe: 15.6%$0.01
    The same plan that had scored 10.9% twice. Nothing had changed (section 9).
  9. Twelve repeats: three plans, four more seeds each$0.29
    The long plan has the best average (section 10). On the way, a connection slip (section 14).
  10. Make v3.1-stable$0.03
    Its own recipe: the long plan, seed 0. 10.43% on the dev lines, epoch for epoch the same as before.
  11. Tune the decoder on the dev linesfree, on the laptop
    10.2% → 9.3% on the dev lines.
  12. Read the test lines, oncefree
    6.8% with the default decoder, 7.8% with the tuned one: the defaults stay (section 13).
  13. Time it, export itfree
    Within the 50 ms budget on every timing run; an 8.6 MB file for the browser that reads exactly as the training code on 10 of 10 check lines.

3 Six dials, in plain words

Each setting trades one risk against another: learn the hand too little, or learn these 68 lines too well and forget the rest.

Personalising starts from a model that already reads well and nudges it toward one hand. Every setting decides how hard, how long, or with what, and each fails in both directions:

learning rate How big a step each weight takes after every batch. Changing it: Too small and training creeps, wasting GPU time. Too big and it overshoots: the loss bounces or blows up. It's usually the first setting to tune. Here: 0.002 for the base models; 0.001 for personalising, which starts near a good solution and shouldn't be knocked away from it.

How big a step each weight takes after every batch of 32 lines.

Too small the model hardly leaves the base.

Too big it overshoots, and can knock loose what the base knew.

Ended at 0.0017 (v3's plan: 0.001)

Varied copies

Each of the writer's lines is shown as written plus this many copies, each a little augmentation Making extra training examples by changing existing ones in ways that keep the answer the same: slant, size, speed, jitter, dropped points. Changing it: It teaches the model which differences don't matter, so it generalises from few examples. Too extreme, and the examples stop looking like real writing. Here: Each of the writer's lines becomes 40 variations; half of v2's synthetic lines are augmented too. in slant, size and speed.

Too few it learns the exact strokes, not the hand.

Too many longer epochs, and the writer's lines crowd out the rest.

Ended at 70 (v3's plan: 40)

Synthetic lines replayed Mixing some of the original training data back in while fine-tuning on new data. Changing it: It keeps the general skill while the model learns the new data; too much and the new data has less effect. Here: 2,000 synthetic lines alongside your lines.

Lines of the base's own kind, mixed in to remind it of everything else.

Too few it catastrophic forgetting When a model trained on new data loses what it knew before. Changing it: A personalised model could read your hand and nothing else. Mixing old data back in (replay) and a small learning rate prevent it. Here: Checked every personalisation: error on synthetic lines stays flat. the general skill.

Too many the writer's lines have less effect.

Ended at 361 (v3's plan: 2,000)

epoch One full pass over every training line. Changing it: More epochs keep lowering the training error until it levels off. Past that point they cost time for nothing, or the model starts memorising the training lines. The best epoch is the one that scores best on held-out lines, not the last one. Here: v1's base ran 29 epochs of about 97,000 lines (about 1.5 minutes each on a T4); v2's ran 12 epochs of 290,000 lines (about 12 minutes each). Personalising stops after 6 epochs or 10 minutes.

Passes over all of it.

Too few the hand isn't learned yet.

Too many it overfitting When a model learns its training examples themselves rather than the pattern behind them, so it does much better on lines it has seen than on new ones. Changing it: It makes training scores look better than reality. The cures are more or more varied data, augmentation, fewer epochs, and picking the epoch on held-out lines. Here: v2 personalised reads its own training lines with 4.5% errors and the test lines with 13.1%. the 68 lines.

Ended at 8 (v3's plan: 6)

Timing hidden (dropout Hiding part of the input or the network at random, during training only. Changing it: It stops the model leaning on any one point or feature, so it generalises better. Too much and it learns slowly. Here: The time feature (dt) is hidden on 20% of points.)

The share of pen points whose timing is hidden from the model during training.

Too little it leans on this writer's exact pace.

Too much it learns slowly.

Ended at 14% (v3's plan: 20%)

Layers frozen

How many of the base's lowest LSTM A recurrent layer: it reads a sequence one step at a time and keeps a memory (its state) of what it has seen. Changing it: Well suited to pen paths, but each step waits for the one before, which limits how much a GPU can speed it up. Here: Each layer has four gates (input, forget, cell, output) deciding what to keep and what to forget. layers are kept as they are.

None every weight can change: the most to gain and to lose.

All three only the top of the model learns the hand.

Ended at 0 (v3's plan: 0)

A hand-picked plan existed: v3's own, from the base's recipe. The question was whether a search could beat it, by how much, and whether the gain would hold.

4 A ruler for small differences

Settings are judged on 17 lines the model never learns from. On them one letter is 0.23 points, and a single reading wobbles by several letters.

The writer's lines are split three ways, and each pile has one job:

Learned from
68
both rounds, the fit
Dev lines
17
441 characters: settings are chosen on these
Test lines
24
600 characters: read once, at the end

The dev set A second set of held-out lines, for choosing settings and the best epoch. Changing it: It keeps the test set untouched, so the final score isn't flattered by choices made while looking at it. Here: A fifth of the writer's training lines, held out: the decoder and the personalising settings are tuned on them, never on the test lines. are a fifth of each language, picked by a fingerprint of each line's name, so every run of the day held out the same 17. One wrong letter of 441 is 0.23%: two plans half a point apart differ by two letters. Keep that ruler in mind for every number below.

The dev score is also noisy from epoch to epoch. The release's own run jumped by up to 10 letters between one epoch and the next (13.2% → 11.8% → 14.1% → 12.0% → 13.8% → 12.2% → 11.3% → 10.4%). So a single dev reading is a measurement with a wobble of several letters, not a fixed truth.

Which epoch is kept, and why it isn't chosen on the dev lines

Each run keeps the epoch that reads its 68 training lines best, a tie going to the later epoch: that keeps the model close to the hand without drifting far. The dev lines are left to do one job, scoring the plan, and every plan is scored the same way.

For the long plan both ways agree: its training lines read perfectly from epoch 7, so epoch 8 is kept. For one run of v3's plan they don't: it kept epoch 5, where its training lines read best.

5 Measured first: why the search left the laptop

On this laptop personalising tops out near 13 lines a second, and more cores don't help. A rented GPU ran the same code 38 to 71 times faster.

Before choosing where to search, the laptop was timed. Personalising this model can't use more than about two processor cores, and running several at once barely adds up:

05101520lines per second, higher is better (several runs: their total)1 run, 2 threads12.51 run, 4 threads12.91 run, 8 threads12.82 runs at once16.64 runs at once18.8
Personalising speed on this laptop's processor, in lines per second
RowValueInterval
1 run, 2 threads12.5
1 run, 4 threads12.9
1 run, 8 threads12.8
2 runs at once16.6
4 runs at once18.8
Two runs side by side manage 1.33 times the speed of one, four runs 1.5 times. Searching here would mean one trial after another.

One model of the final plan reads 5,177 lines an epoch for 8 epochs. At the laptop's best that is at least 54 min of training; on the rented A10G it took 52 seconds. A 60-trial search here: 12 hours or more.

The GPU benchmark had promised about 1,500 lines a second, roughly 100 times the laptop. The personalising runs themselves read between 490 and 918 lines a second per epoch: 38 to 71 times faster. Less than promised, and still the difference between a search in minutes and one overnight.

6 Search 1: sixty trials draw a map

A search tried 60 plans, stopping weak ones early. Its best, 10.9%, sat at the edge of two ranges: the map was cut off where the treasure might be.

Each trial is a real personalising run, about a minute on the GPU, scored on the dev lines. Trial 0 is v3's own plan, so the search can't end worse than it. Optuna, an open-source search library, picks the rest:

How the search decides what to try next

The first ten or so trials are spread at random across the six ranges, to get a rough picture.

Then it learns from them. It splits the trials so far into the better ones and the rest, and proposes settings that look like the better group more than the worse. It looks at all six together, because a learning rate that suits 12 epochs doesn't suit 2. (The method is called TPE, a tree-structured Parzen estimator.)

Weak trials stop early. After each epoch, from the fifth trial on, a trial doing worse than the median of earlier trials at the same epoch is stopped. Of the 58 trials stopped across both searches, 51 were stopped after their first epoch.

10%15%20%25%0.000100.000300.0010.0030.01learning rate (each step ×10 is the same distance)CER on the 17 dev lines, lower is bettersearch 1's ceilingSearch 1Search 2ran every epochstopped earlyv3's plantrial 53: 10.9%trial 52: 10.4%
All 120 trials of both searches: dev error rate against the learning rate tried
GroupTriallearning rate (each step ×10 is the same distance)CER on the 17 dev lines, lower is betterRan
Search 1v3's plan0.00113%every epoch
Search 1t10.0008616%every epoch
Search 1t20.0005516%every epoch
Search 1t30.0009225%every epoch
Search 1t40.002125%stopped early
Search 1t50.0001621%stopped early
Search 1t60.0002822%stopped early
Search 1t70.001115%every epoch
Search 1t80.001515%stopped early
Search 1t90.0003417%stopped early
Search 1t100.002712%every epoch
Search 1t110.001914%every epoch
Search 1t120.003413%every epoch
Search 1t130.001416%stopped early
Search 1t140.003712%every epoch
Search 1t150.004212%every epoch
Search 1t160.004312%every epoch
Search 1t170.002313%every epoch
Search 1t180.004512%every epoch
Search 1t190.003615%stopped early
Search 1t200.003914%stopped early
Search 1t210.001616%stopped early
Search 1t220.004613%every epoch
Search 1t230.00211%every epoch
Search 1t240.004414%every epoch
Search 1t250.004520%stopped early
Search 1t260.00214%stopped early
Search 1t270.001816%stopped early
Search 1t280.002914%stopped early
Search 1t290.002215%stopped early
Search 1t300.001916%stopped early
Search 1t310.004215%stopped early
Search 1t320.003714%stopped early
Search 1t330.002115%stopped early
Search 1t340.001115%stopped early
Search 1t350.002914%stopped early
Search 1t360.001515%stopped early
Search 1t370.003114%every epoch
Search 1t380.002414%stopped early
Search 1t390.002613%every epoch
Search 1t400.004915%stopped early
Search 1t410.004114%stopped early
Search 1t420.00214%stopped early
Search 1t430.003212%every epoch
Search 1t440.002916%stopped early
Search 1t450.003114%stopped early
Search 1t460.00212%every epoch
Search 1t470.00216%stopped early
Search 1t480.002214%every epoch
Search 1t490.003912%every epoch
Search 1t500.002913%every epoch
Search 1t510.003614%every epoch
Search 1t520.004714%every epoch
Search 1trial 53: 10.9%0.004211%every epoch
Search 1t540.003213%every epoch
Search 1t550.004112%every epoch
Search 1t560.004615%stopped early
Search 1t570.003214%stopped early
Search 1t580.00415%stopped early
Search 1t590.003513%every epoch
Search 2t00.00113%every epoch
Search 2t10.001317%every epoch
Search 2t20.0007518%every epoch
Search 2t30.001424%every epoch
Search 2t40.003623%stopped early
Search 2t50.0001721%stopped early
Search 2t60.0003421%stopped early
Search 2t70.001715%every epoch
Search 2t80.002516%every epoch
Search 2t90.0004318%stopped early
Search 2t100.004314%every epoch
Search 2t110.006513%every epoch
Search 2t120.002913%every epoch
Search 2t130.004714%every epoch
Search 2t140.0009817%stopped early
Search 2t150.007414%every epoch
Search 2t160.008916%stopped early
Search 2t170.005813%every epoch
Search 2t180.003514%every epoch
Search 2t190.0007113%every epoch
Search 2t200.009416%stopped early
Search 2t210.0008715%stopped early
Search 2t220.0004714%every epoch
Search 2t230.001113%every epoch
Search 2t240.0005516%stopped early
Search 2t250.0002317%stopped early
Search 2t260.001113%every epoch
Search 2t270.0005115%stopped early
Search 2t280.0006516%stopped early
Search 2t290.0003515%stopped early
Search 2t300.006118%stopped early
Search 2t310.002112%every epoch
Search 2t320.001616%stopped early
Search 2t330.00113%every epoch
Search 2t340.003113%every epoch
Search 2t350.00212%every epoch
Search 2t360.002112%every epoch
Search 2t370.00212%every epoch
Search 2t380.002814%every epoch
Search 2t390.002912%every epoch
Search 2t400.00213%every epoch
Search 2t410.001515%stopped early
Search 2t420.003213%every epoch
Search 2t430.0008312%every epoch
Search 2t440.001213%stopped early
Search 2t450.0008115%stopped early
Search 2t460.001515%stopped early
Search 2t470.001315%stopped early
Search 2t480.001614%stopped early
Search 2t490.002211%every epoch
Search 2t500.00215%stopped early
Search 2t510.002811%every epoch
Search 2trial 52: 10.4%0.001710%every epoch
Search 2t530.00213%every epoch
Search 2t540.001512%every epoch
Search 2t550.001513%stopped early
Search 2t560.002814%stopped early
Search 2t570.003214%stopped early
Search 2t580.001914%stopped early
Search 2t590.002114%stopped early
Filled dots ran every epoch; rings were stopped early, at the score they had then, which is why they sit high. Both searches crowd toward rates between about 0.001 and 0.005, and the first search's best (grey, named) sits just under its ceiling.

Search 1's best used 2 epochs, the fewest it allowed, and a learning rate 95% of the way up its range, measured the way the search drew it. A best at the edge of a range says the real best may lie past it.

Search 1's five best trials that ran every epoch: short and hot, all of them.
TrialDev CERLearning rateCopiesMixed inEpochsTiming hidden
5310.9%0.0042191,37021%
2311.1%0.002262,31732%
4911.8%0.0039241,630210%
1612.0%0.0043281,171213%
1412.2%0.0037221,87345%

And one setting was settled for good: freezing never helped. Of the 14 trials across both searches that kept any of the base's layers frozen and ran every epoch, the best read 12.5%. With nothing frozen, each search did 7 and 9 letters better. The writer's hand needs the low layers too (episode 7 has the chart).

7 Search 2: a wider map, and a second valley

With room past both edges, the best landed in the middle instead: 8 gentle epochs, 10.4%. Two different recipes, two letters apart.

The second search allowed learning rates up to 0.01 and as few as 1 epoch. It was a study of its own: the search method only learns from trials that shared the same ranges.

Past the old edges there was nothing to find. The 6 trials above the old ceiling scored 13.2% at best, and the 3 one-epoch trials 13.2%. Instead, the search found a second valley:

10%15%20%25%123456789101112epochs plannedCER on the 17 dev lines, lower is betterSearch 1Search 2ran every epochstopped earlyshortlong
The trials that ran every epoch, by how many epochs they planned: two valleys
GroupTrialepochs plannedCER on the 17 dev lines, lower is betterRan
Search 1t0613%every epoch
Search 1t1716%every epoch
Search 1t2616%every epoch
Search 1t3225%every epoch
Search 1t7915%every epoch
Search 1t10312%every epoch
Search 1t11514%every epoch
Search 1t12213%every epoch
Search 1t14412%every epoch
Search 1t15212%every epoch
Search 1t16212%every epoch
Search 1t17313%every epoch
Search 1t18312%every epoch
Search 1t22413%every epoch
Search 1t23311%every epoch
Search 1t24214%every epoch
Search 1t37214%every epoch
Search 1t39313%every epoch
Search 1t43412%every epoch
Search 1t46212%every epoch
Search 1t48414%every epoch
Search 1t49212%every epoch
Search 1t50213%every epoch
Search 1t51414%every epoch
Search 1t52314%every epoch
Search 1short211%every epoch
Search 1t54213%every epoch
Search 1t55312%every epoch
Search 1t59313%every epoch
Search 2t0613%every epoch
Search 2t1717%every epoch
Search 2t2518%every epoch
Search 2t3224%every epoch
Search 2t7915%every epoch
Search 2t8916%every epoch
Search 2t10214%every epoch
Search 2t11113%every epoch
Search 2t12513%every epoch
Search 2t13314%every epoch
Search 2t15114%every epoch
Search 2t17213%every epoch
Search 2t18114%every epoch
Search 2t19513%every epoch
Search 2t22214%every epoch
Search 2t23713%every epoch
Search 2t26713%every epoch
Search 2t31512%every epoch
Search 2t33813%every epoch
Search 2t34513%every epoch
Search 2t35712%every epoch
Search 2t36612%every epoch
Search 2t37612%every epoch
Search 2t38714%every epoch
Search 2t39612%every epoch
Search 2t40713%every epoch
Search 2t42713%every epoch
Search 2t43712%every epoch
Search 2t49811%every epoch
Search 2t51611%every epoch
Search 2long810%every epoch
Search 2t53713%every epoch
Search 2t54712%every epoch
Search 1 (grey) spent its trials on 2 to 4 epochs; search 2 (blue) found that 6 to 8 gentler epochs do as well or better. Every trial above 15% kept layers frozen, but one that made only 5 copies of each line.
Search 2's five best: long and gentle, with many copies and few synthetic lines.
TrialDev CERLearning rateCopiesMixed inEpochsTiming hidden
5210.4%0.001770361814%
4911.3%0.0022751,006818%
5111.3%0.00284857963%
3711.6%0.0024992667%
5411.8%0.001553526717%

Which setting mattered most? Optuna estimates the share of the spread in scores that each setting explains. The answer changed with the map:

0%10%20%30%40%50%importance, by Optuna's estimateLearning rate, search 120%Learning rate, search 234%Copies per line, search 146%Copies per line, search 216%Synthetic lines mixed in, search 13%Synthetic lines mixed in, search 225%Epochs, search 121%Epochs, search 215%Timing hidden, search 19%Timing hidden, search 24%Layers frozen, search 10%Layers frozen, search 26%
Share of the spread in dev scores each setting explains, in each search
RowValueInterval
Learning rate, search 120%
Learning rate, search 234%
Copies per line, search 146%
Copies per line, search 216%
Synthetic lines mixed in, search 13%
Synthetic lines mixed in, search 225%
Epochs, search 121%
Epochs, search 215%
Timing hidden, search 19%
Timing hidden, search 24%
Layers frozen, search 10%
Layers frozen, search 26%
In search 1 the number of copies explained most; in search 2, with twice the room for the learning rate, the learning rate and the synthetic lines did. Importance belongs to the ranges searched as much as to the setting.

8 What mixing in synthetic lines buys: a forgetting test

The long plan's batches are 93% the writer's ink, so it forgets more of the base's skill. A rule written beforehand used that to break the tie, for the short plan.

The two bests learn from very different diets. Counting what one epoch holds (the writer's 68 lines with their copies, beside the synthetic lines):

What one epoch of each plan holds, and how many weight updates a whole run makes (batches of 32).
PlanWriter's linesSynthetic linesWriter's shareUpdates in the run
Short plan1,3601,32550.7%168
v3's plan2,7881,93459.0%888
Long plan4,82834993.3%1,296

A plan that is nearly all the writer risks catastrophic forgetting When a model trained on new data loses what it knew before. Changing it: A personalised model could read your hand and nothing else. Mixing old data back in (replay) and a small learning rate prevent it. Here: Checked every personalisation: error on synthetic lines stays flat. what the base knew. A few synthetic lines are held out of each run to check, but for the long plan that is 12 lines: far too few to see a change of a point or two. So both bests were read on 2,000 synthetic lines that neither had ever seen:

0%1%2%3%4%5%CER on unseen synthetic lines (no language model), lower keeps moreBase v3, all1.37%Base v3, math1.09%Short plan, all3.58%Short plan, math2.20%Long plan, all4.17%Long plan, math2.62%
Error on 2,000 unseen synthetic lines: base v3 and the two bests, overall and per language
RowValueInterval
Base v3, all1.37%
Base v3, math1.09%
Short plan, all3.58%
Short plan, math2.20%
Long plan, all4.17%
Long plan, math2.62%
Both personal models forget some of the base's skill; the long plan a little more, in every language (English 3.64% against 4.33%, French 3.48% against 4.12%, mixed 4.25% against 4.61%).

The two bests were 2 letters apart on the dev lines, inside the noise. The rule, written before measuring: within 3 letters, keep the plan that forgets less. The short plan won, and its values were written into the recipe for personal v3.1.

It was a sensible rule. It was about to be overturned by something no rule had foreseen.

9 Failure: the same recipe, 21 letters worse

Made from its recipe, the short plan scored 15.6% instead of 10.9%. GPU training isn't exactly repeatable, and a search's best is partly luck.

Personal v3.1 was trained from its recipe: same plan, same seed, same lines, same GPU type. It read the dev lines at 15.6%. The same plan had scored 10.9% twice, in the search and as a candidate.

Short plan, seed 0, twice10.0%12.0%14.0%16.0%12345678epochLong plan, seed 0, twice10.0%12.0%14.0%16.0%12345678epoch
The same plan and seed trained twice: dev error after each epoch
PlanRunAfter each epochKept
Short plan, seed 0, twiceas a candidate11.3%, 10.9%epoch 2: 10.9%
Short plan, seed 0, twiceas v3.113.2%, 15.6%epoch 2: 15.6%
Long plan, seed 0, twiceas a candidate13.2%, 11.8%, 14.1%, 12.0%, 13.8%, 12.2%, 11.3%, 10.4%epoch 8: 10.4%
Long plan, seed 0, twiceas v3.1-stable13.2%, 11.8%, 14.1%, 12.0%, 13.8%, 12.2%, 11.3%, 10.4%epoch 8: 10.4%
Left: the two runs part from the first epoch (50 and 58 wrong letters) and end 21 letters apart. Right: the long plan, made again later as v3.1-stable, retraced its earlier run letter for letter: the two lines lie on top of each other.

Why: the loss this model learns from is added up in parallel on the GPU, in an order that can change from run to run, and nothing in training asked the GPU to be exactly repeatable. Rounding then differs in the last digit, and training can amplify that from the first epoch. Sometimes it doesn't (the long plan repeated exactly); sometimes it does, and a short plan at a high learning rate amplifies it most.

The winner's curse: why the best of many noisy trials is the one to trust least

Every score is part plan, part luck. A search keeps the lowest score, so it favours plans that are good, and also runs that were lucky. How lucky? If every trial were the same plan, with run-to-run spread s, the best of n would on average look this much better than the plan really is:

Expected luck of the best of n runs of one plan, in spreads (the mean of the lowest of n normal draws).
Runs152060120
Luck of the best-0.00 s1.16 s1.87 s2.32 s2.57 s

The trials of a search aren't one plan repeated, so this overstates it. But with the short plan's spread of 1.8 points, the best of 60 copies would look 4.2 points better than its average. The real short plan's 10.9% sat 3.1 points under its own average: well within what luck alone could do.

10 Five seeds each: judge a plan by its mean and its spread

Over five runs, the long plan averages 11.4%, v3's plan 12.8%, the short plan 13.9%. The lucky winner was the worst of the three on average.

A seed The starting number for every random choice: which font, which word, how much slant. Changing it: Same recipe and same seed give the same data, so a version can be rebuilt. Different seeds show how much a result is luck. Here: Pinned in each recipe. now picks the varied copies and seeds training, so a plan can be run several times on purpose. Each finalist and v3's own plan, as the reference, ran with four more seeds:

Long plan, 8 epochs10.0%12.0%14.0%16.0%12345678epochv3's plan, 6 epochs10.0%12.0%14.0%16.0%12345678epochShort plan, 2 epochs10.0%12.0%14.0%16.0%12345678epoch
Every run of the three plans: dev error after each epoch, the ringed dot marking the epoch kept
PlanRunAfter each epochKept
Long plan, 8 epochsseed 013.2%, 11.8%, 14.1%, 12.0%, 13.8%, 12.2%, 11.3%, 10.4%epoch 8: 10.4%
Long plan, 8 epochsseed 113.8%, 12.9%, 11.8%, 10.7%, 11.1%, 11.1%, 11.1%, 10.9%epoch 8: 10.9%
Long plan, 8 epochsseed 212.7%, 12.2%, 12.7%, 11.3%, 12.2%, 12.9%, 11.1%, 10.7%epoch 8: 10.7%
Long plan, 8 epochsseed 312.9%, 12.9%, 12.7%, 13.2%, 11.6%, 14.1%, 11.1%, 12.2%epoch 8: 12.2%
Long plan, 8 epochsseed 413.4%, 12.5%, 11.8%, 11.3%, 12.2%, 12.2%, 12.2%, 12.7%epoch 8: 12.7%
v3's plan, 6 epochsseed 114.3%, 14.7%, 14.1%, 12.9%, 12.0%, 13.4%epoch 5: 12.0%
v3's plan, 6 epochsseed 213.6%, 12.5%, 13.4%, 14.7%, 12.2%, 13.4%epoch 6: 13.4%
v3's plan, 6 epochsseed 314.3%, 13.4%, 12.9%, 12.9%, 13.2%, 12.9%epoch 6: 12.9%
v3's plan, 6 epochsseed 414.1%, 13.8%, 13.8%, 14.1%, 13.8%, 12.9%epoch 6: 12.9%
Short plan, 2 epochsseed 011.3%, 10.9%epoch 2: 10.9%
Short plan, 2 epochsseed 0, again13.2%, 15.6%epoch 2: 15.6%
Short plan, 2 epochsseed 112.9%, 15.6%epoch 2: 15.6%
Short plan, 2 epochsseed 216.6%, 14.3%epoch 2: 14.3%
Short plan, 2 epochsseed 313.8%, 12.9%epoch 2: 12.9%
Short plan, 2 epochsseed 414.5%, 14.3%epoch 2: 14.3%
Blue is the long plan's seed 0, later the release. The long plan's runs end between 10.4% and 12.7%; the short plan's, after only two epochs, between 10.9% and 15.6%. Every run zigzags: the dev score moves by several letters from one epoch to the next.
Every run, dev CER of its kept epoch (17 lines, 441 characters). v3's plan is averaged over seeds 1 to 4; its seed 0, trial 0 of the searches, read 13.4%.
PlanRuns, by seedMeanSpread (sd)
Long plan0: 10.4% · 1: 10.9% · 2: 10.7% · 3: 12.2% · 4: 12.7%11.4%1.0 points
v3's plan1: 12.0% · 2: 13.4% · 3: 12.9% · 4: 12.9%12.8%0.6 points
Short plan0: 10.9% · 0, again: 15.6% · 1: 15.6% · 2: 14.3% · 3: 12.9% · 4: 14.3%13.9%1.8 points

The long plan beats v3's plan by 1.4 points on average, and the short plan by 2.6. Divided by their own uncertainty (Welch's t), those gaps are 2.7 and 2.9: past 2, a gap is rarely luck, though five runs is still a small sample.

Note who came last on average: the short plan, the search's original winner, 1.1 points behind the hand-picked plan it was meant to replace. v3's own plan was the steadiest (spread 0.6 points), just not the best.

11 The recipe, value by value

Every value of v3.1-stable, what it means in lines and steps, and the evidence that put it there.

The pick got a recipe of its own, written before the model was made and never edited after. v3.1's recipe stays as the record of the lucky plan; v3.1-stable's keeps both searches, the superseded tie-break and all fifteen repeat runs with their scores.

Personal v3.1-stable's recipe.
Valuev3.1-stableIn practiceWhy
Start frombase v33 × 192 network, 2.15 million weights27.2% before personalising; never saw the writer's lines
Linesboth rounds68 learned from, 17 held outmore of the writer's lines is the biggest lever; dev lines to choose with
Learning rate0.001671constant, every batchsearch 2's best; the lowest mean over five seeds
Copies per line704,828 of the writer's lines an epochsearch 2's five best used 48 to 75
Synthetic lines mixed in361349 trained on, 12 held out to checksearch 2's five best used 361 to 1,006; it forgets a little more (section 8)
Epochs8162 batches each, 1,296 updatesthe second valley; every epoch ran
Timing hidden14.3%of pen points, in training onlythe searched value; it mattered least in search 2 (4%)
Layers frozen0every weight learnseach frozen layer made every search's best worse
Seed0picks the copies, seeds trainingthe plan's best run of five
Time limit60 minneeds about a minutethe epochs end training, never the clock
Wherea rented A10G69 s, $0.03section 5
Decoderthe defaultsthe tuned settings are kept, unusedtuned won on dev lines, lost on test lines (section 13)

The learning rate reads 0.0016710470002597796 in the recipe. Nobody chose those digits: the search draws from a continuous range, and the recipe keeps exactly what it drew, so the model can be made again from it.

12 Win: 6.8% on the test lines, read once

v3.1-stable gets 41 of 600 test letters wrong, against 62 for v2.1-personal: better on 13 lines, worse on 3.

Character error rate
6.8%
95% interval 4.6% to 9.2%
Word error rate
23.6%
from 32.9%
95% of lines read within
45.1 ms
budget 50 ms

The test lines are the same 24 that v2.1-personal was judged on, so the two can be compared line by line. Each square below is a letter of a test line; the shaded ones are how many that model got wrong on that line.

v2.1-personal62 of 600 wrongen1frmath1mixed1en5fr1math1en7fr2mixed5math1en1fr8math4mixed3en2fr2math4enfr2mixed3math2en3fr3
v2.1-personal: Wrong letters on each of the 24 test lines
LineLanguageLettersWrong
1en321
2fr260
3math111
4mixed271
5en305
6fr331
7math231
8en337
9fr262
10mixed315
11math81
12en271
13fr248
14math234
15mixed313
16en262
17fr272
18math144
19en350
20fr222
21mixed323
22math62
23en263
24fr273
v3.1-stable41 of 600 wrongenfrmathmixed1en4frmath2en3fr2mixed5mathen1fr3math1mixed2en3fr3math3enfr2mixed2math2en1fr1
v3.1-stable: Wrong letters on each of the 24 test lines
LineLanguageLettersWrong
1en320
2fr260
3math110
4mixed271
5en304
6fr330
7math232
8en333
9fr262
10mixed315
11math80
12en271
13fr243
14math231
15mixed312
16en263
17fr273
18math143
19en350
20fr222
21mixed322
22math62
23en261
24fr271
One row per test line, in the same order on both sides; the tag is its language. The counts are real, the positions of the shaded squares are not: a missed or extra letter counts too.

Paired line by line, the step is −3.5 points (−6.0 to −1.3), and the interval stays clear of zero. It was made from fewer of the writer's lines: 68, where v2.1-personal learned from all 85.

Per language on the 24 test lines. The last column is v3.1-stable read by the network alone, without the language model.
Languagev2.1-personalv3.1-stableNo language model
English9.1%5.7%7.7%
French9.7%5.9%8.6%
Mixed9.9%8.3%9.1%
Math15.3%9.4%8.2%

Math is the one kind of line read better without the language model A model of which letters tend to follow which, learned from plain text. Changing it: It fixes readings the network finds ambiguous. Weighted too heavily, it overrules what you actually wrote, such as rare words and names. Here: A character model (each letter given the 4 before it) from 120,000 Tatoeba sentences per language, weighted 0.5. (8.2% against 9.4%): it learned which letters follow which from ordinary text, and pulls formulas toward words. What the remaining mistakes look like:

Six of v3.1-stable's worst readings on the test lines, with the language model and without it.
WrittenReadNetwork alone
Δx ≠ 0√x + 0√x + 0
A = πr² ≈ 28.3A = ∑ r² = 28.3A = π r² = 28.3
Draft du mail pour Hélène, ASAPDraft du mail pour Hélène, 49/12Draft du mail pour Hélème, $S/12
Buy a gift for Yasmin, max $40Buy a gift for yas men, max d40Buy a gift for yasmin, max 940
Facture n° 58 : 230,00 €Facture n° 58: 230 000 €Facture n° 58: 230 000 €
Unpack the boxes in room 9Unpach the boce in room 9Unpach the boce in roioi 9

Symbols, units and names: Δ read as √, "max $40" as "max d40", "ASAP" as "49/12". The network alone gets π right, and "Yasmin" all but its capital, where the language model talks it out of them.

On all 39 test lines of both rounds, v3.1-stable reads 8.6%. The 8% goal is met on the 24, but the interval reaches 9.2%: 24 lines can't prove "under 8%" yet. The comparison with every earlier model is in episode 7.

13 Not kept: a decoder tuned on the dev lines

Tuning the decoder gained 4 letters on the dev lines and lost 6 on the test lines. The defaults stay, and they keep within the speed budget too.

The decoder turns the network's letter scores into text, guided by the language model. Its six settings were searched on the same dev lines (100 trials, on the laptop), keeping the fastest settings within half a point of the most accurate.

v3.1-stable read with each decoder.
DecoderDev linesTest lines (24)Both rounds (39)
Default (kept)10.2%6.8%8.6%
Tuned9.3%7.8%9.4%

4 letters gained on 441, 6 lost on 600. Four letters is inside the dev lines' wobble (section 4), and settings fitted to 17 lines learn their quirks. Most of the search's attention went to two settings: how early to drop unlikely readings (38%) and how much to trust the language model (20%).

The tuned decoder was faster, though, and speed has a budget: 95% of lines read within 50 ms. The first timing of the defaults, 49.2 ms, was taken while the site was being built on the same machine. So the two decoders were timed in turn, three times each, so that any load fell on both:

35 ms40 ms45 ms50 ms55 msp95 per line, model and decoder, lower is faster50 ms budgetDefault, run 147.3 msDefault, run 245.6 msDefault, run 346.1 msTuned, run 152.2 msTuned, run 241.7 msTuned, run 341.7 ms
Time within which 95% of the 24 test lines are read, the two decoders timed alternately on a busy machine
RowValueInterval
Default, run 147.3 ms
Default, run 245.6 ms
Default, run 346.1 ms
Tuned, run 152.2 ms
Tuned, run 241.7 ms
Tuned, run 341.7 ms
The machine's load stayed between 5.78 and 6.34 throughout. The defaults stayed within the budget every time; the tuned settings went over once. The final reading with the defaults: 45.1 ms.
Rules written down before measuring, and what they decided
  • Two bests within 3 letters: keep the one that forgets less. It picked the short plan, then the five-seed repeats replaced it with a better rule: the lowest mean.
  • Keep tuned decoder settings only if they also win on the test lines. They didn't: defaults.
  • Fewest errors first, if 95% of lines stay within 50 ms on a quiet machine. The defaults did, three times out of three.

14 What it cost: $1.99

Two searches took most of the money. A finished model costs about 3 cents and a minute; the whole of personalising cost 13% of the base model.

$0.00$0.25$0.50$0.75$1.00dollars, estimated per callSearch 2, 60 trials$0.84Search 1, 60 trials$0.5212 repeat runs$0.292 candidates$0.04v3.1-stable$0.03v3.1$0.01
What each step of personalising cost on the rented GPU, by the runs' own estimates
RowValueInterval
Search 2, 60 trials$0.84
Search 1, 60 trials$0.52
12 repeat runs$0.29
2 candidates$0.04
v3.1-stable$0.03
v3.1$0.01
The estimates add up to $1.73. Modal's bill for the day's 20 personalising calls came to $1.99. The gap is time the estimates don't count: each estimate is the seconds the work itself ran, while the bill also pays for every container starting up, the first build of its software, the idle minute before it shuts down, and the calls that failed or hung.
One finished model
$0.03
about a minute on the GPU
All of personalising
$1.99
13% of base v3's $15.06
The same search on the laptop
12 h +
and the laptop busy all along

15 Making it again, and what's next

The recipe makes the model again, within the noise of GPU training. Three things to try next, each with a measurement waiting.

Everything that made v3.1-stable is pinned: the base, both rounds of lines, the dev split, the plan and its seed, the GPU type. Made twice, the long plan gave the same score after every epoch, down to the same 46 wrong letters at the end. But GPU training isn't guaranteed to repeat exactly, so a fresh run of the plan should be expected anywhere between its best and worst of five: 10.4% to 12.7% on the dev lines.

Next, and how we'll know
  • Math without the letter language model, or with one that knows formulas: the network alone already reads math lines at 8.2% against 9.4%.
  • A third recording round, so a model refitted on all 85 lines, with no dev lines held out, can be judged against this one on lines neither has seen.
  • More test lines, to tell whether the error is really under 8%: on 24 lines the interval still reaches 9.2%.
What this doesn't show
  • Which part of the step did most: the new base, both rounds of lines and the searched plan all changed at once.
  • Whether the plan suits a different writer: it was chosen on this writer's 17 dev lines.
  • How the plan would rank on a bigger dev set, where luck has less room.

16 What carries over

Six habits for choosing training settings when the data is small and every score is noisy.

Beyond this project
  • Measure before renting, and measure the right thing: here, more processor cores did nothing, and the GPU personalised well under its benchmark speed.
  • Put a ruler on every score. With 441 dev characters, one letter is 0.23 points, and a single reading wobbles by several letters between epochs.
  • When a search's best sits at the edge of a range, widen it as a new search; don't believe the edge.
  • Read importances as a property of the ranges searched, not of the setting: widen a range and its importance changes.
  • Treat a search's winner as a shortlist. Make it again, then run every finalist with several seeds and pick by the mean: here the winner came last.
  • Write the rule before the measurement, and the recipe before the model, and never edit either afterwards: when the luck showed, a new recipe replaced the old one instead of hiding it.

Previous · Episode 07 · 2 Nov 2026

Iteration 3, value by value

A personal model at 6.8% error, a third fewer than before. Behind it: a base that starts 16 points better, a plan chosen by 120 searched trials, and the lucky run that changed how we choose.

Win and failure

Next · Episode 09 · 2 Nov 2026

Base v4: the same lines, trained longer

Before changing what the base model learns from, a control: v3 trained 12 more epochs on its own lines. 2.4 points better on the writer's test lines for 89 cents, and a chart that shows more training has run out.

Win and failure