Reading one hand
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.

TL;DR

Win and failure

The question Can a base model read the writer at about 15% error without personalising? Base v3 is at 27.5%. Before changing its data: what does more training on the same lines buy?

The base model reads the writer with about one letter in four wrong, and we want it nearer one in seven before it sees any of their lines. Before changing the fake handwriting it learns from, we trained it longer on exactly the lines it already had, to see what more training alone buys. It read the writer a little better, but the fake ink kept getting easier for it while the writer's stayed just as hard.

  • Base v4 reads the writer's 84 test lines at 25.2% error, from 27.5% for base v3: −2.4 points (95% interval −4.1 to −0.6), with none of the writer's lines in training.
  • It is v3 trained 12 more epochs on its own 300,000 synthetic lines, from its own weights: 31 minutes on a rented A10G, $0.89.
  • The synthetic error fell from 1.4% to 0.58%. The writer's dev error was best at epoch 2, then sat at 31–32% for ten epochs.
  • Base v3's mistakes are letter shapes: the writer's ink loops 0.58 times per letter, the synthetic ink 0.19 to 0.27. No preprocessing change helped.
  • Far from 15%: the next base has to change the data, not the training.
The lesson

Before changing the data, train longer on the old data as a control. When the synthetic score keeps falling and the real one doesn't, the next gain has to come from the data.

Why read on Chosen by its own synthetic lines, the run would have kept epoch 11. The chart where two error curves part ways after epoch 2, and what that says about the next model.

1 The question

Can a base model read the writer at about 15% error before personalising? Base v3 is at 27.5%. First, a control: the same lines, trained longer.

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. learns from synthetic data Training data written by code instead of collected from people: here, handwriting fonts turned into pen paths with timing. Changing it: It's unlimited and licence-clean, but if it doesn't look like real writing the model learns the wrong thing: the synthetic-to-real gap. Here: 300,000 lines for v2, from 134 fonts under open licences (OFL or Apache). alone, then is fitted to the writer. The better it reads them before that fitting, the less the fitting has to do. Base v3, from episode 7, gets about one letter in four wrong. The goal for the next bases is one in seven: 15% or less, with none of the writer's lines in training.

The obvious next step is to change the data. But v3's error on its own synthetic lines was still falling when its training stopped. So base v4 changes nothing but the training: v3, trained longer on exactly its own lines. Whatever later bases gain beyond v4 is then their data's doing, not extra training's.

2 Where v3 goes wrong: letter shapes

The writer loops and joins letters three times as often as the synthetic ink does, and that is where the mistakes are. Preprocessing isn't the cause.

Base v3 has seen none of the writer's lines, so all 124 of them could test it: 27.9% wrong with the beam search Decoding that keeps the best few partial readings at every step instead of committing to one letter at a time. Changing it: A wider beam gives the language model room to fix ambiguous letters, but costs time: the beam search is about 40% of v2's time per line. Here: Width 8, combining the network's scores with the language model and your lexicon., close to its official figure. Two thirds of the errors are lowercase letters, inside words; spaces are 90% right.

0.00.10.20.30.40.50.60.7self-crossings per letter, median line, math asideThe writer's ink0.58Font lines, cursive0.27Font lines, other0.22InkSight lines0.19
Loops per letter: places where the pen crosses its own line, in the writer's ink and in v3's synthetic ink
RowValueInterval
The writer's ink0.58
Font lines, cursive0.27
Font lines, other0.22
InkSight lines0.19
The writer joins letters in cursive, with looped l, b, f and h, and writes "th" as one stroke. Fonts traced into strokes lose narrow loops, and InkSight writes mostly print. 90% of the writer's lines loop more than the median synthetic line.

The most common mistakes are exactly those shapes:

t read as l
33
times, greedy
n read as u
25
times, greedy
h read as L
22
times, greedy
l read as b
16
times, greedy

A looped l reads as b, a looped h as L, a cursive n as u. Speed, size, slant and pen lifts already match the writer's. Preprocessing changes tried on the writer's ink (straightening the slant, smoothing, shortening pauses, reordering strokes, dropping tiny ones) moved the error by under a point or made it worse. Narrower ink read up to 2.6 points better: small, next to 27.

3 Measure first: dev lines choose, test lines judge

38 lines choose the epoch and decide whether v4 replaces v3; 84 other lines are read once, at the end. The rule was written before training.

A base model learns none of the writer's lines, so they can all be used to measure it, split by when they were recorded:

  • Dev lines: the first recording round, 38 lines (919 characters). They choose which epoch to keep and whether v4 replaces v3.
  • Test lines: the second round, 84 lines (2,250 characters), read once, for the model chosen.

On these test lines base v3 reads 27.5%, near the 27.9% on all lines. Two lines written across two rows are left out of both: the model reads one row.

4 The control run: 31 minutes, 89 cents

v3's own 300,000 lines, from v3's weights, the step size easing to zero. Every epoch came back to the laptop to be read.

The same 300,000 synthetic lines v3 learned from, still in the cloud's storage, so no new lines were made. Training starts from v3's weights, with the 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. starting at half v3's and easing to zero along a cosine curve, for 12 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..

The writer's ink never went to the cloud. Every epoch's weights came back, and the laptop read the dev lines with each, then kept the epoch that read them best.

Training
31 min
12 of 12 epochs on an A10G
GPU machine
$0.89
no line skipped
The day's v4 work
$1.90
with the lost launches (section 7)

5 Epoch by epoch: two curves part ways

The synthetic error kept falling, from 1.4% to 0.58%. The writer's dev error was best at epoch 2, then flat at 31–32%.

0.00%0.50%1.00%1.50%123456789101112epochCER, held-out synthetic linesbase v3 · 1.40%base v4 · 0.58%
Synthetic lines like the ones it trains on: every epoch reads them better, and the error halves by the end.
0%10%20%30%40%123456789101112epochCER, the writer's dev linesbase v3 · 34%base v4 · 31%
The writer's ink: every epoch reads it better than v3, but after epoch 2 it stops improving, however much better the synthetic lines read.
Every epoch. The dev error is read greedy decoding Reading the model's output by taking the top guess at every point, then applying CTC's rules. Changing it: Fast and simple, but it can't use spelling knowledge, so it produces things like "1%e" for "the". Here: Greedy CER is the network on its own: 18.4% for v2 personalised, against 13.1% with the beam search., the network alone.
EpochHeld-out synthetic linesThe writer's dev lines
base v3 (start)1.4%33.7%
10.96%31.6%
2 (kept)0.97%28.9%
30.87%31.6%
40.84%32.1%
50.80%31.1%
60.72%31.9%
70.69%32.3%
80.63%31.8%
90.61%31.6%
100.59%31.0%
110.58%31.0%
120.58%30.7%

Chosen by its own synthetic lines, the run would have kept epoch 11. The model goes on learning v3's lines better, and from epoch 3 on that no longer reaches the writer's hand: the synthetic-to-real gap The difference between how well a model reads the synthetic data it trained on and how well it reads real writing. Changing it: A model can score almost perfectly on synthetic lines and still read real ink badly, so only a test on real writing counts. Closing it means making the synthetic data look more real. Here: v1's base: 0.1% errors on synthetic lines, 58.3% on yours., measured epoch by epoch.

6 Win, but small: 2.4 points on the test lines

Base v4 reads the 84 test lines at 25.2%, from 27.5%: a real step, and far from 15%.

In letters: 619 wrong of 2,250 for base v3, 566 for base v4. Paired line by line, the step is −2.4 points (−4.1 to −0.6). It shows with and without the language model, so it is the network reading better. On the 24 test lines of earlier episodes, all from the second round, base v4 reads 24.2% against 27.2%.

−8−6−4−20+2difference in CER, points; below zero, v4 makes fewer errorsno changeTest lines, beam−2.4Test lines, greedy−2.6Dev lines, beam−3.4Dev lines, greedy−4.8Epoch 12, dev, beam−2.5Epoch 6, dev, beam−1.2
Base v4 minus base v3, paired line by line, with 95% bootstrap intervals
RowValueInterval
Test lines, beam−2.4−4 to −1
Test lines, greedy−2.6−4 to −1
Dev lines, beam−3.4−6 to −1
Dev lines, greedy−4.8−7 to −2
Epoch 12, dev, beam−2.5−5 to 0
Epoch 6, dev, beam−1.2−4 to +2
The kept epoch on both sets of lines, and two epochs not kept, on the dev lines. Epoch 6 alone can't be told from v3.
Predicted before the run

Written before training: v4 replaces v3 only if it reads the dev lines better with the beam search and the 95% interval of the paired difference is below zero. The test lines are read once, for the model chosen.

Observed

Dev lines: 26.3% → 23.0%, −3.4 points (−5.5 to −1.3). v4 replaces v3. The test lines agree: −2.4 points (−4.1 to −0.6). Prediction held

What it says: more training alone is worth about 2 points on the writer's hand, and then it has run out. It took the edge off one shape: on all the writer's lines, t read as l fell from 33 times to 20. But n still reads as u, h as L, m as n, and lowercase letters are still most of the errors. What's left is what v3's lines don't show, looped and joined letters. The next base changes the data, and has to beat v4, not v3. Since the dev error peaked early, 4 to 6 epochs from v4 should be enough for it.

7 Failure: two launches lost before any GPU

Reusing v3's lines made every chunk finish at once, and 50 MB of statistics coming back together dropped the connection. Twice.

Before training, each of the 60 chunks of lines is measured in the cloud and its statistics are sent back, to check the data against the writer's ink. When v3 made its chunks, they finished at different times. Reused, all 60 finished together, about 0.9 MB each: roughly 50 MB at once.

Twice the connection dropped about 14 minutes in, before any GPU had started: about $0.20 together. The fix: each chunk writes its statistics to the cloud's storage, and the laptop reads them one by one. Measuring the 60 chunks then took 38 seconds, and the next launch ran through.

8 What carries over

Run the cheap control before the expensive change, and watch the real score, not the synthetic one.

Beyond this project
  • Before changing the data, train longer on the old data. It is cheap, and it tells the next change's gain apart from more training's.
  • Choose epochs by the data you care about. Here the synthetic lines would have kept epoch 11; the writer's lines chose epoch 2.
  • When the score on training-like data keeps falling and the real one is flat, the model is learning what the real data doesn't need. More epochs won't close that gap.
  • Look at the mistakes before guessing their cause. Testing preprocessing changes on the real ink ruled them out in minutes, and the confusions pointed at the shapes.
  • Write the rule before the run, and keep a test set out of every choice. The best of 12 dev scores is optimistic; the test lines aren't.

Previous · 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.

Method

Next · Episode 10 · 2 Nov 2026

Base v5: five new kinds of fake ink, and why v4 stays

New kinds of synthetic handwriting, piloted in 16 arms against controls on the writer's real lines. Three changes made the model alone read better, by up to 4 points. None helped the way the app reads, and base v5 didn't replace v4.

Failure