Emitting JSON is easy. The useful result is that supervised fine-tuning made a 4B model materially better at extracting the right values.
TL;DR
- On 97 held-out pages with human-corrected teacher drafts, fine-tuning Qwen3.5-4B raised page-macro value accuracy from 23.41% to 41.54%. The paired gain was 18.13 percentage points, with a 95% interval from +11.26 to +25.53 points.
- Schema-valid responses rose from 62.89% to 95.88%.
- A second, production-domain stage mixed 28,699 private examples with 15,313 public replay examples. It processed 50.07 million tokens in 3 hours 55 minutes on one 32 GB RTX 5090.
- Most labels came from other models, so this measures the extraction contract, not human-level accuracy or general model superiority.
The contract
The service accepts three inputs:
| |
JSON Schema defines the permitted keys, types, arrays, and required fields, and may change on every request. The evaluations below cover public pages and one private corpus, not every schema or domain.
Specialist extractors already exist, including NuExtract 2.0 and Schematron. I fine-tuned a local model for my exact prompt, Markdown representation, schemas, and pages, and to measure what one consumer GPU could buy.
Stage one: did fine-tuning teach the job?
The first stage used
scrapegraphai/scrapegraph-100k-finetuning.
GPT-5-nano generated its answers: useful pseudo-labels, not human ground truth.
After removing exact and normalized-content overlaps, I sealed 97 usable, human-corrected examples. They never informed checkpoint, prompt, or threshold selection.
The primary metric was page-macro normalized value accuracy: every page weighs equally, whether its schema has one field or fifty. Normalization ignores whitespace, not incorrect values.
| Model | Value accuracy | Schema valid | Key F1 | Exact pages |
|---|---|---|---|---|
| Base Qwen3.5-4B, BF16 | 23.41% | 62.89% | 84.65% | 5.15% |
| Local Qwen3.8-27B, Q4_K_M | 34.26% | 80.41% | 95.45% | 12.37% |
| Fine-tuned Qwen3.5-4B, BF16 | 41.54% | 95.88% | 96.73% | 16.49% |
Against the untouched 4B, value accuracy gained 18.13 points (paired 95% interval: +11.26 to +25.53) and schema validity gained 32.99 points. Failures stayed in the denominator.
The tuned 4B also beat my local quantized 27B by 7.28 points here, though paired tests were borderline or inconclusive. The narrow conclusion: teaching this task mattered more on this benchmark than adding general-purpose parameters.
Stage two: adapt it to production data without forgetting everything else
The final candidate continued from the public-corpus adapter. Its mixture was deliberately not 100% private data:
| Source | Rows | Purpose |
|---|---|---|
| Private production corpus | 28,699 | Domain vocabulary, attributes, and representative edge cases |
| Public ScrapeGraph replay | 15,313 | Preserve the general schema-to-JSON contract |
| Total | 44,012 | Domain adaptation with replay |
DeepSeek produced most private-corpus answers; GPT-5-nano produced the public replay. Replay was intended to reduce catastrophic forgetting, but without a replay ablation that remains a design choice, not a causal result.
Why 16k context?
An 8,192-token pilot retained 97.8% of the public corpus, but production pages were longer. A length audit and the 32 GB memory budget led to a 16,384-token cap; preflight counted 277.6 million input and 7.75 million answer tokens. The service may expose 32k at inference, but training stopped at 16k, so quality beyond it is unproven.
The final recipe
I used LoRA (small trainable updates to a frozen base model) and BF16. The Unsloth Qwen3.5 guide informed the memory choices; the run used Transformers, TRL, and PEFT.
| |
Gradient accumulation turns 16 one-page microbatches into one optimizer batch; checkpointing trades computation for memory. Assistant-only loss predicts the JSON answer, not every source token. Packing stayed off because it was much slower on this long-sequence workload.
| Final run | Measured value |
|---|---|
| Training tokens | 50,072,170 |
| Optimizer updates | 496 |
| Runtime | 3 h 55 m 15 s |
| Throughput | 3,546 tokens/s |
| Final aggregate loss | 0.119579 |
What survived domain adaptation?
On 3,159 source-separated private development rows, both BF16 and online FP8 returned parseable, schema-valid JSON for every row:
| Final model | Balanced accuracy | Exact field accuracy | Schema valid |
|---|---|---|---|
| BF16 | 97.46% | 85.73% | 100% |
| Online FP8 | 97.40% | 85.52% | 100% |
These numbers measure agreement with pseudo-labels, not human accuracy. The split separates source/entity groups but lacks reliable template fingerprints, so it cannot establish template generalization.
On a separate 97-page public regression, the final domain-adapted BF16 model reached 38.78% value accuracy and 96.91% schema validity. Online FP8 reached 36.14% and 94.85%. BF16 remains the quality reference; FP8 bought throughput at a small but measurable quality cost.
What this establishes
Supervised fine-tuning improved a frozen, human-corrected benchmark. One RTX 5090 then domain-adapted the 4B extractor at 16k in under four hours while retaining useful public-task performance.
It does not show that a specialist 4B generally beats a 27B, that pseudo-label agreement equals human correctness, or that valid JSON contains the right facts.
Part two is about exactly that: serving the model at queue scale without counting fast failures as progress.
Reproducibility notes
- The 97 labels are human-corrected teacher drafts, not independent ground truth.
- Intervals use paired, group-aware resampling; all failures remain in the denominator.
- Raw pages and predictions stay local; MLflow stores metrics, configuration, digests, and provenance.
- A frozen report preserves the public comparison and its caveats.