Base v6: real hands
Real handwriting from 153 other people, piloted against a control and then trained into the base. The writer's test lines go from 25.2% to 17.6% error before personalising, where five kinds of fake ink had moved the score about a point.
TL;DR
WinThe question Font-made ink has stopped helping the way the app reads. Would real handwriting from other people, none of it the writer's, move the base model closer to reading the writer?
The model had only ever learned from handwriting faked from fonts, and new kinds of fake ink had stopped helping. We added real handwriting from 170 other people, a public research set, tried it in a small pilot against an unchanged control, then trained a new base with it. The writer's lines went from one letter in four wrong to fewer than one in five, before the model has seen any of them.
- Base v6 replaces v4. On the writer's 38 dev lines with the beam search: 15.3% against v4's 23.0%, −7.6 points (−10.0 to −5.1). On the 84 test lines, read once: 17.6% against v4's 25.2%, −7.6 points (−9.9 to −5.2).
- In the pilot, two epochs of real lines made v4 read the dev lines 6.7 to 7.7 points better with the beam search, where the best fake-ink change had moved it about 1. Unlike v4 and v5, the full run kept improving until epoch 7.
- Lowercase errors nearly halved (470 → 250) and English lines fell from 23.4% to 12.5%. What BRUSH lacks got no better: math 33.8% → 37.7%, accents, and errors on spaces doubled.
- Most BRUSH writers print: 0.25 loops per letter, like the fonts, against the writer's 0.58. Weighting towards loopy writers raised their share of lines from 14% to 40%, and helped the writer's lines a little more.
- The goal is 15% before personalising: 15.3% on the dev lines, 17.6% on the test lines. Pilot $0.64, run $1.13 by its own estimate. BRUSH is research-only, so the model stays here.
When a synthetic source stops paying, real data from many people can move what rounds of synthetic tweaks couldn't. Pilot it against a control, weight it towards your user, and list what it lacks: it fixes only what it contains.
Why read on Every file in the download could run code the moment it was opened. And the one thing that got clearly worse was the gaps between words.
1 The question
Five kinds of fake ink moved the app's reading by about a point, and base v5 didn't replace v4. Would real handwriting from other people move it more?
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. has learned only 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).: handwriting faked from fonts. The goal is a base that reads the writer at 15% or less before it sees any of their lines. Base v4 reads the writer's 38 dev lines at 23.0% 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., as the app reads.
Episode 10 tried five new kinds of fake ink. The best of them moved the beam score about a point, inside the noise, and the base trained on them, v5, read the dev lines +1.2 points (−0.8 to +3.3) against v4. It wasn't adopted.
Two thirds of v4's errors are lowercase letter shapes, and fonts only draw so many. Real hands have shapes no font draws. So this round adds real handwriting, from other people.
2 Five other hands
BRUSH: 27,649 lines from 170 people, written with a stylus, a point every 10 ms. Most print; a few loop.
BRUSH is a public set of online handwriting: each line is the pen's path as it was written, with its timing, not a picture. 170 people wrote English sentences with a stylus; the pen's position was recorded every 10 ms. The model reads timing, so only the original lines were kept, not the copies that resample them and drop it.
Writer 128 prints
22 pen lifts · this writer's median: 0.08 loops per letter
Writer 89 prints
16 pen lifts · this writer's median: 0.18 loops per letter
Writer 22 joins some letters
23 pen lifts · this writer's median: 0.28 loops per letter
Writer 80 joins and loops
20 pen lifts · this writer's median: 0.56 loops per letter
Writer 78 joins and loops
8 pen lifts · this writer's median: 1.00 loops per letter
BRUSH: Kotani, Tellex and Tompkin, Generating Handwriting via Decoupled Style Descriptors, ECCV 2020; https://dsd.cs.brown.edu/ — non-commercial research only
3 Opening files that can run code
Every BRUSH line is a Python pickle, and opening a pickle can run any code it names. The reader allows numpy's arrays and refuses the rest.
The download is 567 MB, 89,068 files. Each line is stored as a pickle: Python's own way of saving objects. Opening one rebuilds whatever it describes, and it can describe a call to anything, such as running a shell command.
So the reader never opens a file the plain way: it allows only numpy's array and type constructors, and refuses any file that names anything else. Its tests are made of real attacks: files that do run code when opened plainly, which the reader refuses without running them. All 27,649 lines read, from 170 writers, and every character in them is one the model writes.
4 Failure, caught by a test: one damaged file would have stopped the reading
A file that opens into something other than a line raised an error nothing caught. Now any damaged file counts as one unreadable line, and the reading goes on.
A test pass over the new code wrote files the reader should refuse. One opened into something other than a line (nothing at all, say), and the reader raised an error the conversion didn't catch: it caught only the errors of a refused or broken file. One such file would have stopped the whole conversion of BRUSH.
Now the reader refuses anything that isn't a line, and any error a damaged file can raise counts that file as unreadable, and the reading goes on. No BRUSH file is like that, so all 27,649 lines read either way: the fix is for the next download.
5 Their ink against the writer's
BRUSH loops about as little as the fonts: 0.25 times per letter, the writer 0.58. 16 of 170 writers, about one in ten, loop nearly as much.
| Ink | Loops | Pen lifts | Width | Slant |
|---|---|---|---|---|
| The writer | 0.58 | 0.88 | 0.40 | −9° |
| BRUSH | 0.25 | 1.17 | 0.45 | 0° |
| Fonts (v3 and v4's lines) | 0.22–0.27 | 0.9–1.0 | 0.43–0.48 | −6° to −9° |
On average BRUSH looks like the fonts, not like the writer. And it is fast: 0.17 s per letter with the pen down, against the writer's 0.67 s, about four times faster.
| Loops per letter | Writers |
|---|---|
| 0.00 to 0.05 | 0 |
| 0.05 to 0.10 | 6 |
| 0.10 to 0.15 | 17 |
| 0.15 to 0.20 | 33 |
| 0.20 to 0.25 | 11 |
| 0.25 to 0.30 | 35 |
| 0.30 to 0.35 | 17 |
| 0.35 to 0.40 | 12 |
| 0.40 to 0.45 | 10 |
| 0.45 to 0.50 | 5 |
| 0.50 to 0.55 | 8 |
| 0.55 to 0.60 | 3 |
| 0.60 to 0.65 | 1 |
| 0.65 to 0.70 | 1 |
| 0.70 to 0.75 | 3 |
| 0.75 to 0.80 | 0 |
| 0.80 to 0.85 | 1 |
| 0.85 to 0.90 | 2 |
| 0.90 to 0.95 | 0 |
| 0.95 to 1.00 | 0 |
| 1.00 to 1.05 | 1 |
| 1.05 to 1.10 | 2 |
| 1.10 to 1.15 | 0 |
| 1.15 to 1.20 | 0 |
| 1.20 to 1.25 | 0 |
| 1.25 to 1.30 | 1 |
| 1.30 to 1.35 | 0 |
| 1.35 to 1.40 | 0 |
| 1.40 to 1.45 | 1 |
Most people print. 16 of the 170 writers loop 0.55 times per letter or more, about one in ten; 13 as much as the writer or more. Over all their lines, the writers run from 0.12 loops per letter (the 10th percentile) through 0.27 to 0.50 (the 90th). The average hides the loopy few, so drawing writers evenly would mostly teach print.
6 How each line was prepared
Each choice comes from something an earlier round found: no smoothing, the writer's pace, a varied copy every draw, whole writers held out.
| Choice | Why |
|---|---|
| Original files, a point every 10 ms; a pause of 90 to 320 ms drawn between strokes | The model reads timing. BRUSH doesn't record pen lifts, and the writer's pauses are about this long. |
| No smoothing | Smoothing closed loops and turned every e into a hook (v5, pilot 3). |
| 17 whole writers (10%) held out, read after every epoch | To see whether the model reads hands it has never seen, not only the writer's. |
| Real lines as a set share of each epoch, each draw a varied copy | 24,873 real lines against 290,000 synthetic: without a share they'd be lost; without varying slant, size, jitter and speed, as personalising does, memorised. |
| Slowed to the writer's pace, or kept at each writer's own: a pilot arm | v3 gained partly by matching the writer's pace, and BRUSH writes about four times faster. |
| Writers drawn alike, or weighted towards the writer's loops: a pilot arm | Most BRUSH writers print; the writer loops. |
| The synthetic lines are v3 and v4's own, reused | v5: new synthetic sources didn't help the beam search. One change at a time. |
The varied copies are 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., the same kind personalising uses, so 24,873 lines can be drawn hundreds of thousands of times without being 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 two open questions, pace and which writers, became pilot arms.
17 whole writers, 2,776 lines, are never trained on. That leaves 24,873 lines from 153 writers to learn from, and a way to see whether the model reads hands it has never seen, not only the writer's.
7 Weighting towards the writer's loops
Drawing writers by how close they loop to the writer raises the loopy writers' share of lines from 14% to 40%. Every writer still appears.
Each writer's lines are drawn with a weight: highest for writers who loop about as much as the writer, never below 0.05, so the printers still appear, less often.
| A writer's loops per letter | Weight |
|---|---|
| 0.00 | 0.050 |
| 0.05 | 0.050 |
| 0.10 | 0.050 |
| 0.15 | 0.050 |
| 0.20 | 0.050 |
| 0.25 | 0.066 |
| 0.30 | 0.141 |
| 0.35 | 0.266 |
| 0.40 | 0.445 |
| 0.45 | 0.655 |
| 0.50 | 0.852 |
| 0.55 | 0.978 |
| 0.60 | 0.990 |
| 0.65 | 0.885 |
| 0.70 | 0.698 |
| 0.75 | 0.486 |
| 0.80 | 0.298 |
| 0.85 | 0.162 |
| 0.90 | 0.077 |
| 0.95 | 0.050 |
| 1.00 | 0.050 |
| 1.05 | 0.050 |
| 1.10 | 0.050 |
| 1.15 | 0.050 |
| 1.20 | 0.050 |
| Drawing | Share |
|---|---|
| every writer alike | 14.4% |
| weighted by the curve | 39.6% |
Whether that helps is for the pilot to say: one arm draws writers alike, one weighted, all else the same.
8 The pilot: real ink, four ways
From v4, 2 epochs, the same 80,000 fake lines; four arms add real ones. Every one reads the writer's dev lines 6.7 to 7.7 points better with the beam search.
As in episode 10: every arm starts from v4's weights The numbers inside the model that training adjusts. A trained model is just these numbers plus the code that uses them. Changing it: More weights can learn more, but need more data and training time, make the file bigger and reading slower. They grow with the square of the layer width. Here: v0: 0.18 million (1 × 128). v1: 0.97 million (3 × 128). v2: 2.15 million (3 × 192), 8.6 MB in full precision., trains 2 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. on the same 80,000 synthetic lines (v3 and v4's own), and is read on the writer's 38 dev lines. The control adds nothing. The others add real lines:
- 25% real: 25% of each epoch real, the writer's pace, writers alike.
- 50% real: 50%, the writer's pace, writers alike.
- 50%, weighted by loops: 50%, the writer's pace, writers weighted towards the writer's loops.
- 50%, writers' own pace: 50%, each BRUSH writer's own pace, writers alike.
Written before running: base v6 takes the arm with the lowest dev score with the beam search among those whose 95% interval against the control is below zero. If none is, the arm with the best point estimate that isn't worse than the control, and the result says so.
Every real arm's interval is below zero. The lowest: 50%, weighted by loops, 15.7% with the beam search, −7.7 points (−10.8 to −4.4). v6 takes it.
| Row | Value | Interval |
|---|---|---|
| base v4 itself | −0.4 | −2 to +1 |
| 25% real | −6.7 | −9 to −4 |
| 50% real | −7.1 | −10 to −4 |
| 50%, weighted by loops | −7.7 | −11 to −4 |
| 50%, writers' own pace | −7.5 | −10 to −5 |
| Row | Value | Interval |
|---|---|---|
| base v4 itself | −2.0 | −4 to 0 |
| 25% real | −7.7 | −10 to −5 |
| 50% real | −8.2 | −11 to −5 |
| 50%, weighted by loops | −9.4 | −12 to −6 |
| 50%, writers' own pace | −7.9 | −10 to −5 |
Every real arm is 6.7 to 7.7 points better with the beam search, every interval far below zero. With the language model, v4's 23.0% falls to 15.7% in the best arm, near the 15% goal, after two epochs at a small dose.
- More real ink helps a little: 16.6% with 25%, 16.3% with 50%.
- Weighting towards the writer's loops helps the writer's lines a little more (15.7%), and other hands a little less: 6.3% on the held-out writers against 5.6%. Expected: the writer's hand is the target.
- Pace barely matters with the beam search: each writer's own pace reads 15.9%, the writer's pace 16.3%; greedy, the other way round.
| Arm | Epoch 1, greedy | Epoch 2, greedy | Epoch 2, beam |
|---|---|---|---|
| control | 31.7% | 30.9% | 23.4% |
| 25% real | 23.2% | 23.2% | 16.6% |
| 50% real | 23.4% | 22.7% | 16.3% |
| 50%, weighted by loops | 22.3% | 21.5% | 15.7% |
| 50%, writers' own pace | 23.0% | 23.0% | 15.9% |
| Arm | Real lines | Other writers, 1 | Other writers, 2 | Synthetic | GPU |
|---|---|---|---|---|---|
| control | 0 | not read | not read | 0.38% | $0.04 |
| 25% real | 25,778 | 8.4% | 7.9% | 0.65% | $0.05 |
| 50% real | 77,334 | 6.2% | 5.6% | 0.91% | $0.09 |
| 50%, weighted by loops | 77,334 | 6.9% | 6.3% | 0.91% | $0.07 |
| 50%, writers' own pace | 77,334 | 6.0% | 5.5% | 0.85% | $0.09 |
The real arms read their own synthetic lines a little worse than the control (0.38%): fonts are no longer all they see.
- The pilot
- $0.64
- Modal's bill, 5 arms
- Its GPU
- $0.34
- the runs' own estimates
- All of v5's work
- $4.33
- three pilots and a run
Before it: nothing was running on Modal, and 525 leftovers of v5's pilots went from the storage volume. The pilot models are kept on the laptop; v3 and v4's synthetic lines stay, for reuse.
9 Fake ink moved it a point; real ink, seven
On the same 38 dev lines with the beam search: v5's fake-ink recipe −1.3 in its pilot, +1.2 at full scale. Two epochs of real lines: −6.7 to −7.7.
| Row | Value | Interval |
|---|---|---|
| fake ink: v5's pilot arm | −1.3 | −3 to 0 |
| fake ink: base v5 vs v4 | +1.2 | −1 to +3 |
| real ink: 25%, pilot | −6.7 | −9 to −4 |
| real ink: 50%, weighted by loops | −7.7 | −11 to −4 |
| real ink: base v6 vs v4 | −7.6 | −10 to −5 |
Three pilots of new fake ink, 16 arms, moved the beam score by about a point. Two epochs with real handwriting moved it seven, and the full run kept it. What the fonts taught, the language model already gave the beam search. Real hands bring letter shapes no font draws.
10 Other hands
Bases v3 to v5 read the 17 writers they never saw at 27 to 29%. A real arm, after 2 epochs, reads them at about 6%; base v6 at 2.9%, on an easy set.
| Row | Value | Interval |
|---|---|---|
| base v3, beam | 28.9% | |
| base v4, beam | 26.9% | |
| base v5, beam | 28.5% | |
| 25% real, greedy | 7.9% | |
| 50% real, greedy | 5.6% | |
| 50%, weighted by loops, greedy | 6.3% | |
| 50%, writers' own pace, greedy | 5.5% | |
| base v6, beam | 2.9% |
A base that has seen only fonts reads a stranger's hand about as badly as the writer's. After two epochs with other people's, it reads people it never saw at about 6%.
11 Base v6 replaces v4
By the rule set before training, v6 replaces v4: 15.3% on the dev lines with the beam search against v4's 23.0%, −7.6 points (−10.0 to −5.1).
By the rule set before the pilot, v6 takes 50%, weighted by loops: real lines as half of each epoch, at the writer's pace, writers weighted towards the writer's loops.
| Starts from | v4's weights |
|---|---|
| Synthetic lines | v3 and v4's 300,000, reused |
| Real lines | BRUSH, 50% of each epoch: about 290,000 varied draws of 24,873 lines, at the writer's pace, writers weighted towards the writer's loops |
| Training | 8 epochs, the learning rate easing from v4's 0.001 along a cosine |
| Epoch kept | the one that reads the dev lines best, greedy |
Eight epochs, not five: the pilot was still improving at epoch 2, and an epoch now holds twice the examples.
8 epochs on a rented A10G, 37 min. Unlike v4 and v5, whose dev scores went flat after an epoch or two, v6 kept improving until epoch 7: 22.9% to 19.7% on the writer's dev lines, read greedily. Real ink gives the model more to learn. The synthetic lines would have kept epoch 8.
| Epoch | The writer's dev lines | Other writers | Synthetic |
|---|---|---|---|
| 1 | 22.9% | 4.4% | 1.33% |
| 2 | 22.4% | 3.7% | 1.28% |
| 3 | 21.8% | 3.4% | 1.07% |
| 4 | 20.5% | 3.2% | 0.98% |
| 5 | 21.2% | 3.1% | 0.84% |
| 6 | 20.9% | 2.7% | 0.75% |
| 7 (kept) | 19.7% | 2.8% | 0.69% |
| 8 | 20.0% | 2.8% | 0.67% |
Written before training: v6 replaces v4 only if it reads the dev lines better with the beam search and the 95% interval of the paired difference is below zero. Then the 84 test lines are read once, v6 against v4 and v3, and the held-out writers with the beam search.
Dev lines, beam: 23.0% → 15.3%, −7.6 points (−10.0 to −5.1). v6 replaces v4. Prediction held
| Lines | v3 | v4 | v6 |
|---|---|---|---|
| The writer's dev lines (38): decides | 26.3% | 23.0% | 15.3% |
| The writer's test lines (84): read once | 27.5% | 25.2% | 17.6% |
| Other writers, never trained on (400 lines) | 28.9% | 26.9% | 2.9% |
| The 24 official test lines | 27.2% | 24.2% | 18.2% |
| Row | Value | Interval |
|---|---|---|
| Dev lines, beam | −7.6 | −10 to −5 |
| Dev lines, greedy | −9.3 | −12 to −6 |
| Test lines, beam | −7.6 | −10 to −5 |
| Test lines, beam, vs v3 | −10.0 | −13 to −7 |
| Other writers, beam | −24.0 | −26 to −22 |
The test lines agree: 25.2% → 17.6%, −7.6 points (−9.9 to −5.2). The same size of gain as on the dev lines, so it isn't the luck of choosing an epoch on them. Against v3: −10.0 points (−12.6 to −7.3). Read greedily, the model alone, the test lines go from 30.3% to 21.4%.
The goal was 15% before personalising. v6 reads the writer's dev lines at 15.3% and the test lines at 17.6%. English lines are already below it. Personalising from v6, with the writer's own lines, is the next measurement.
- Dev lines, beam
- 15.3%
- v4 23.0%
- Test lines, beam
- 17.6%
- v4 25.2%, v3 27.5%
- The 24 official lines
- 18.2%
- v4 24.2%, v3 27.2%
- Other writers, beam
- 2.9%
- v4 26.9%
- The pilot
- $0.64
- Modal's bill
- Base v6's run
- $1.13
- the run's own GPU estimate
- Pilot and run
- $1.77
- the run by its own estimate
Modal's per-app bill for the run lagged when this was written, so its cost is the GPU machine's own estimate: $1.13 for 37 min of training.
12 Where real ink helped
Lowercase errors nearly halved, 470 to 250, and English lines fell from 23.4% to 12.5%. The looped-l confusions are gone.
All 124 of the writer's lines, read by v4 and v6 with the beam search. The dev lines are among them, so this is for finding where the errors are, not a test figure.
| Lines | v4 | v6 |
|---|---|---|
| All lines | 25.5% | 17.9% |
| English | 23.4% | 12.5% |
| French | 25.4% | 18.2% |
| Mixed | 24.8% | 16.7% |
| Math | 33.8% | 37.7% |
| Kind | v4 | v6 |
|---|---|---|
| Lowercase letters | 470 | 250 |
| Punctuation | 64 | 33 |
| Capitals | 44 | 31 |
| Digits | 58 | 67 |
| Accents | 45 | 50 |
| Math symbols | 40 | 41 |
| Spaces | 44 | 87 |
Real hands fixed what fonts couldn't. Lowercase letters, two thirds of v4's errors, went from 470 errors to 250. English, the language BRUSH is written in, fell from 23.4% to 12.5%; French and mixed lines improved less.
v4's top confusions, greedy
- n→u 21
- t→l 20
- h→L 19
- i→e 18
- m→n 17
v6's
- n→u 15
- i→e 15
- é→e 15
- 0→o 12
- k→h 9
Written → read as, with how often. The looped letters fonts drew badly, t read as l and h as L, are gone from the top. n read as u is still first, less often.
13 Failure: what real ink didn't fix
What BRUSH doesn't have got no better: math went from 33.8% to 37.7%, digits and accents rose, and the errors on spaces doubled.
BRUSH is English sentences, mostly two words a line. What it lacks, v6 didn't learn:
- Math: 33.8% → 37.7%, the only kind of line that got worse. Digits 58 → 67 errors, math symbols 40 → 41; 0 read as o is now a top confusion.
- Accents: 45 → 50. é read as e is now one of the most common confusions.
- Spaces: 44 → 87 errors, doubled. Lines of two words show few gaps between words of the writer's kind.
A public set fixes what it contains. French, math and word spacing are the next step. Real ink with accents, from French writers; longer lines; or synthetic lines kept for math while real ink carries the letters.
14 What the app now shows
v6 is the base: personalising starts from it, and the browser reads with it. The Models page says what each model was built on and learned from, and draws the family.
Base v6 is now the base. Personalising starts from it, and the browser's model is v6: 8.60 MB, reading 10 of 10 check lines exactly as the trained model does, at 45.6 ms per line (95th percentile), inside the 50 ms budget. A smaller 8-bit copy read 6 of the 10 differently, so the full model is used.
- Built on: trained further from a base's weights, a new model trained from scratch, or fitted to the writer's lines from a base. For v6: trained further from Base v4's weights.
- What it learned from, with what is new since its base marked. For v6: "Real handwriting from other writers: BRUSH, 24,873 lines from 153 writers (17 more kept out to read other hands), as 290,000 varied draws, 50% of each epoch, at your pace, writers weighted towards your loops", and BRUSH's research-only licence under it.
- Per-epoch curves for the writer's dev lines and for "Other writers (never trained on)".
- Base v3 new model
- Base v4 trained further v6 was trained from it
- Base v5 trained further not adopted
- Base v6 trained further, with BRUSH current base
- Base v4 trained further v6 was trained from it
15 What carries over
When a synthetic source stops paying, real data from many people can move what rounds of synthetic changes couldn't. Pilot it, weight it towards the user, and check what it lacks.
- When tuning synthetic data stops paying, look for real data from many people, even if none of it is your user's. Here it moved the score several times as far as the best synthetic change, on lines the model never saw.
- Pilot it like any other source: a short run from the current model, against a control, read the way the product reads. This one cost $0.64 and predicted the full run.
- Weight a public set towards the person you're building for. Most people here print; the user loops. Weighting nearly tripled the loopy writers' share and helped the user's lines a little more.
- A public set fixes what it contains. Before relying on one, list what your users write that it doesn't: here math, accents and the gaps between words got no better.
- Hold out whole people, not lines, to see whether a model reads hands it has never met, and say how easy that held-out set is.
- Treat downloaded files as code until proven otherwise. A pickle can run anything; read it with a loader that allows only the types you expect, and test it with real attacks.