A LoRA adapter that repairs OCR-style character corruption in text. Given garbled
input, it returns the corrected text and nothing else.
Headline: character error rate 0.0747 → 0.0325 (56% reduction).
Word error rate 0.4231 → 0.0748 (82% reduction).
The un-tuned base model degrades the input (CER 0.1889).
Results
150 held-out examples, greedy decoding, identical prompt for base and tuned.
Split
Noisy input
Base model
Fine-tuned
Overall CER
0.0747
0.1889
0.0325
Prose CER
0.0750
0.2003
0.0411
Structured CER
0.0740
0.1590
0.0098
Overall WER
0.4231
0.3345
0.0748
Why the base model makes things worse
The instruction-tuned base behaves like a copy-editor rather than a corrector. It
rewrites formatting (replacing field delimiters with line breaks), "corrects"
proper nouns it believes it knows better (Mangaluru → Mangalore), adds quotation
marks, and restructures sentences. Fluent output, unfaithful reconstruction. Most
of its CER is edits nobody asked for.
Fine-tuning teaches the constraint the prompt alone could not: change only what is
broken, preserve everything else exactly.
Structured fields beat prose
Contrary to expectation, the structured split improved more. The template is fixed
and the vocabulary closed, so the model learns the schema and the finite entity set
and restores both reliably. Prose has open vocabulary and stays harder.
The failure that CER hides
Digits carry no linguistic context, so they cannot be recovered by inference —
only guessed. Example:
Every field restored, schema recovered, one digit wrong. That is 2 characters out
of ~90, so CER barely registers it — but on an identity document it is a total
field failure. CER is the wrong metric for structured extraction; field-level
exact match is the honest one, and any production system should route
low-confidence numeric fields to human review rather than trusting a reconstruction.
Residual prose errors are the same class of problem. Where corruption destroys the
information outright, the model produces something fluent instead of something
correct:
NOISY Argos in Cyprus: tbe re wals a tempilne ,of Apollo Erithios
TUNED Argos in Cyprus: the temples of Apollo Erithios
CLEAN Argos in Cyprus: there was a temple of Apollo Erithios
Data
Synthetic. 2000 pairs, split 1700 / 150 / 150.
70% prose from English Wikipedia
30% structured records — name, date of birth, ID number, address
Corruption was not uniform random noise. Uniform noise produces non-words that
are trivially fixable by nearest-dictionary-match, which inflates the result.
Instead, a weighted confusion table models real OCR failure modes:
Sampling: 55% confusion substitution, 20% dropout, 15% whitespace error,
10% insertion. Corruption rate was calibrated to land input CER near 0.075 —
low enough that the text stays recoverable, high enough to leave headroom.
Training
Base
unsloth/Qwen2.5-1.5B-Instruct-bnb-4bit
Method
QLoRA, 4-bit
LoRA
r=16, alpha=16, dropout=0, all attention + MLP projections
Epochs
2 (~425 steps)
LR
2e-4, linear, 10 warmup steps
Batch
2 × 4 grad accum
Max seq
512
Optimizer
adamw_8bit, weight decay 0.01
Hardware
free Colab T4, fp16
Loss was masked to the assistant turn only (train_on_responses_only), so no
gradient is spent learning to predict the corrupted input.
Usage
python
1from unsloth import FastLanguageModel
23model, tok = FastLanguageModel.from_pretrained(4"USERNAME/qwen2.5-1.5b-ocr-correction", max_seq_length=512, load_in_4bit=True)5FastLanguageModel.for_inference(model)67SYS =("You correct OCR errors. Return only the corrected text, "8"with no explanation, preamble, or quotes.")910msgs =[{"role":"system","content": SYS},11{"role":"user","content":"the docurnent was s1gned"}]12ids = tok.apply_chat_template(msgs, return_tensors="pt",13 add_generation_prompt=True).to("cuda")14print(tok.decode(model.generate(ids, max_new_tokens=256, do_sample=False)[0][ids.shape[1]:],15 skip_special_tokens=True))
Limitations
Corruption is synthetic, modeled on real OCR error modes but not sampled from
a real OCR engine. Performance on genuine scanner output is unverified. The next
step is rendering text to degraded images, running Tesseract, and re-evaluating
against real error distributions.
The baseline is zero-shot. A few-shot prompted baseline would be a fairer
comparison and would likely land between the two numbers reported here.
Structured results are optimistic. Names, streets, and cities come from small
closed lists, so the model can memorize the entity set. Real documents have open
vocabulary and would be harder.
Single seed, single test set of 150 examples. No confidence intervals.