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.

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:

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 560014

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:

  • 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

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

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.
Downloads last month
13
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for sanasn/qwen2.5-1.5b-ocr-correction

Adapter
(1306)
this model