A Model With Nothing to Learn From
Training a document-processing model comes down to the data you feed it.
Volume alone isn’t the key. To learn the right thing, a model needs a representative sample of what real documents look like, across the range of variation it has to handle.
I had none of that on hand.
The reality was that I had a document-processing model to train and no usable set of documents. It was worse because we were in a time crunch and looking down a 120-document minimum.
There was only one thing to do: build them myself using synthetic data. Invoices were a format I knew well, so invoices it was.
The Options That Didn't Work
Two obvious paths, neither one workable.
- Use real invoices. I didn't have a usable set to work from.
- Generate purely random data. A model learns nothing useful from noise, and unbounded values would break as often as they would teach. The data had to vary the way real invoices vary while staying structured enough to label and train on consistently.
That second point is the whole design problem, so it's where the real work went.
Controlled Variation, Not Chaos
A useful training set had to be two things at once.
- Diverse, so the model saw the full set of invoice types instead of memorizing a single shape.
- Consistent, so the same fields always showed up in a form the model could be trained to extract.
The move that made both true: I built four invoice layouts that all carried the same set of extracted fields, then varied everything else. Vendors, addresses, totals, and line items changed within real bounds. The fields the model had to learn stayed fixed across all four formats.
That is what a representative sample meant here: controlled variation across the fields that matter, shown in four formats, so the model learned the same lesson from several angles.
How It's Built
The system ran on a spreadsheet and a Google Apps Script, in five parts.
- 01
Source data
I generated the underlying field values in Mockaroo, then cleaned and formatted them in Google Sheets: 24 structured fields with 200 rows of source values each, covering vendor details, customer details, financial data, and invoice metadata.
- 02
Line-item engine
A separate sheet defined item types with bounded quantity and price ranges, injected into each invoice so the totals stayed internally consistent.
- 03
Templates
Four invoice layouts, each with three line items and the same extracted fields, so the output varied in format without changing what the model had to learn.
- 04
Controlled randomization
Spreadsheet
RAND()drove the variation, always held inside the range each field allowed, with a forced refresh between documents so no two came out alike. - 05
Automation
A Google Apps Script stepped through the layouts, froze a fresh set of values, exported each invoice to PDF, and cleaned up after itself, so a batch could run unattended.
Output Examples
Four layouts, the same fields across all of them, values randomized inside real bounds. A few samples:
Invoice Samples
Where It Fought Back
Making that script produce clean, genuinely varied PDFs took far longer than the design did. Four problems, each with its own fix.
The random values wouldn't refresh on their own.
RAND() didn't recalculate reliably before the script grabbed values, so documents came out repeating each other. I forced it: a FLUSH() paired with a tuned delay, long enough to let the randomizer finish before anything was captured.
Live formulas had to be frozen mid-run.
A document is only stable once its values stop moving. Before each export, the script overwrote the current values, turning live formulas into a fixed snapshot so each PDF captured a single draw rather than re-rolling.
Two layouts broke in opposite ways.
One format corrupted on export unless it was trimmed to a hard bounding box, with every stray row and column past the layout deleted. The other three broke whenever anything was deleted, so they got a freeze-only path that touched nothing. I had to diagnose both fragilities and give each its own handling inside the same run.
The platform would export only the entire workbook.
Apps Script exports the entire spreadsheet to PDF, not a single sheet. To get one clean invoice, the script hid every other sheet, exported, then restored them, wrapped so that any failure still put the workbook back the way it found it before the next document.
Impact
Produced the 120 documents the training run required, evenly split across four layouts, each with randomized values inside real bounds.
Replaced what would have been a full night of manual document-building with an unattended batch run, seconds per document instead of minutes of hand-setup and export.
The model trained correctly on the set. The data was sufficiently representative to teach from, which was the whole point.
The labeling stayed hands-on. Even with generation automated, I hand-selected the extracted fields on every single invoice to build the training labels.
What I'd Improve
Configurable run controls
The document counts and layout selection are hardcoded. The loop generated ten per layout, so reaching 120 meant editing it by hand to run the extra passes. A small control panel in the sheet (how many, which layouts, naming) would make each run a setting instead of a code change.
Run logging
Tracking each run (timestamp, count, layout, and any failed exports) would make batches easier to trust and debug.
Less reliance on timing
The forced refresh leans on a fixed delay. A more direct way to force recalculation would make runs both faster and less fragile.
On the AI
AI helped me plan the approach, think through the design, and write the Apps Script.
The source values came from Mockaroo and were cleaned in Google Sheets, and the generator itself is fully deterministic.
Nothing in the pipeline uses AI to generate data, which keeps the output consistent and repeatable.