sanasn/qwen2.5-1.5b-ocr-correction
Qwen2.5-1.5B-Instruct — Post-OCR Correction (QLoRA)
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.
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:
NOISY Name: Diva Nadaf| DO: 2/07/1987 | ID: VA8472357 | Address: 137, Br gade Road, Mangaluru 560014
TUNED Name: Divya Nadaf | DOB: 20/07/1987 | ID: VA8472357 | Address: 137, Brigade Road, Mangaluru 560014
CLEAN Name: Divya Nadaf | DOB: 27/07/1987 | ID: VA8472357 | Address: 137, Brigade Road, Mangaluru 560014Every 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 ErithiosData
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:
- visual confusion pairs —
rn↔m,cl↔d,0↔O,1↔l↔I,5↔S,8↔B - character dropout
- spurious word splits and merges
- punctuation substitution
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
Loss was masked to the assistant turn only (train_on_responses_only), so no gradient is spent learning to predict the corrupted input.
Usage
from unsloth import FastLanguageModel
model, tok = FastLanguageModel.from_pretrained(
"USERNAME/qwen2.5-1.5b-ocr-correction", max_seq_length=512, load_in_4bit=True)
FastLanguageModel.for_inference(model)
SYS = ("You correct OCR errors. Return only the corrected text, "
"with no explanation, preamble, or quotes.")
msgs = [{"role": "system", "content": SYS},
{"role": "user", "content": "the docurnent was s1gned"}]
ids = tok.apply_chat_template(msgs, return_tensors="pt",
add_generation_prompt=True).to("cuda")
print(tok.decode(model.generate(ids, max_new_tokens=256, do_sample=False)[0][ids.shape[1]:],
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.
- English only, sequences under 512 tokens.
