Skip to content

Latest commit

 

History

13 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Arabic OCR Post-Correction

A 0.5B language model that repairs Arabic OCR output — the step between "we scanned the archive" and "the archive is searchable."

raw OCR    وأضا ف أن "بريطايا يمكن أن تؤثر على الاتحاد الأوروبى عبر الـعمل هع شركائها".
corrected  وأضاف أن "بريطانيا يمكن أن تؤثر على الاتحاد الأوروبي عبر العمل مع شركائها".

Model: Sheeda/arabic-ocr-post-correction-0.5b


The problem

Arabic OCR fails differently from Latin OCR, and the reason is structural.

Many Arabic letters share an identical skeleton (rasm) and are told apart only by dots (i'jam). ب ت ث ن are the same stroke. A scanner that loses a dot to a faded patch, or invents one from a speck of dust, changes the letter. The same is true across the alphabet: ج ح خ, د ذ, ر ز, س ش, ص ض, ط ظ, ع غ, ف ق.

Arabic is also cursive, so gaps between words are narrow and the scanner routinely splits one word in two or fuses two into one.

The consequence, measured on this repo's data:

rate
characters wrong 8.1%
words wrong 41.1%

A word is only correct if every character in it is correct, so a small amount of character damage breaks four words in ten. Search, indexing and extraction all operate on words. An archive at 41% word error is scanned, stored, indexed — and unsearchable. That gap is what this closes.

The approach

No paired Arabic OCR corpus exists — not on Kaggle, not on the HuggingFace Hub. So the training data is generated: take clean Arabic text, break it the way a scanner breaks it, and train the model to undo the damage.

The corruption (src/ocr_noise.py) is not random noise. It is weighted toward how the script actually fails:

corruption weight
dot / skeleton confusion (ب↔ت↔ث, ف↔ق, …) 42%
word splitting and merging 22%
dropped or spurious character 16%
kashida, hallucinated diacritics, transposition 20%

Severity is sampled per example between 4% and 18%, so one model handles clean prints and degraded historical pages alike.

Why 0.5B. The output is a near-copy of the input with local repairs — a narrow task that doesn't need a large model. It also means the thing runs on-premise on CPU, which is the deployment mode that matters for archives that cannot leave the building.

Greedy decoding, always. There is exactly one correct answer; sampling only invents text that was never on the page.

Results

200 held-out segments, greedy decoding.

CER WER
Raw OCR (do nothing) 0.0808 0.4112
Untuned Qwen2.5-0.5B 1.8123 2.2680
Finetuned (57k pairs) 0.0724 0.1765
Finetuned + guardrail 0.0671 0.2042

Word error rate falls 41.1% → 20.4%, a 50.3% reduction, while character accuracy improves 16.9% at the same time.

The untuned base model scores CER 1.81. Above 1.0 means the output is not a damaged version of the truth but unrelated text — it answers the instruction conversationally instead of restoring. Every gain is therefore attributable to the finetune, not to Qwen already knowing Arabic.

Training scale was the dominant variable

training pairs CER WER segments made worse
0 (untuned) 1.8123 2.2680 200/200
2,000 0.2139 0.3751 170/200
20,000 0.1083 0.2327 115/200
56,931 0.0724 0.1765 70/200

Loss 1.976 → 1.751 → 1.637; token accuracy 0.667 → 0.700 → 0.717.

It improves every severity band

severity n baseline CER model CER
light (<8%) 62 0.0438 0.0404
medium (8–14%) 82 0.0813 0.0713
heavy (>14%) 56 0.1211 0.1092

On the 123/200 segments it helps, CER drops 0.0816 → 0.0377. 20/200 come back exactly correct.

The guardrail

A generative model can rewrite rather than repair. apply_guardrail rejects any correction that diverges from its input by more than a CER threshold and keeps the original instead. Divergence is measured against the input, never the reference, so it runs in production. The threshold is an operating dial:

policy CER WER corrections kept
raw OCR 0.0808 0.4112
accept everything 0.0724 0.1765 200/200
drift ≤ 0.15 0.0667 0.2676 133/200
drift ≤ 0.20 0.0671 0.2042 180/200
drift ≤ 0.30 0.0695 0.1781 195/200

drift ≤ 0.20 is the default: near-best on both metrics at once. Tighten it where nothing may be made worse, loosen it where findability outweighs character fidelity.

Reproduced on a second corpus

Arabic BERT Corpus (56,946 pairs, baseline CER 0.0838 / WER 0.3900) follows the same trajectory at 2,000 pairs: untuned 2.2092 → tuned 0.1956. The behaviour is a property of training scale, not of one dataset.

Hardware and training cost

Trained on a single NVIDIA RTX 5070 Ti Laptop GPU — 12 GB VRAM, Blackwell (sm_120), native bf16, torch 2.11 + CUDA 12.8.

full run 56,931 pairs × 2 epochs, 80 minutes
throughput 21.6 samples/s
peak VRAM under 12 GB
base model Qwen2.5-0.5B-Instruct, bf16 (no quantisation)
method LoRA r=32, α=64, all attention + MLP projections
schedule effective batch 32, lr 2e-4 cosine

Two settings were chosen by measurement rather than convention:

  • Gradient checkpointing off. It trades ~30% throughput for memory, and a 0.5B model with LoRA doesn't need it here: 16.9 → 21.6 samples/s.
  • Batch size 8, not 16. At 16 the activations exceed VRAM and Windows silently spills to system RAM instead of raising OOM — throughput collapses to 2.6 samples/s. Eight is 8× faster.

It also trains on a free Colab T4. train.py detects that Turing has no bf16 and falls back to fp16 automatically.

Run it

Open In Colab

Locally:

pip install -r requirements.txt
python src/build_dataset.py --input data/raw --output data/processed
python src/train.py --config configs/qwen05b_lora.yaml
python src/evaluate.py --adapter outputs/arabic-ocr-post-correction/final --max-drift 0.20
python src/infer.py --adapter outputs/arabic-ocr-post-correction/final --text "<noisy arabic>"

The data pipeline needs no GPU stack — pip install pyyaml is enough to run python src/ocr_noise.py and watch the corruption model work.

Layout

src/ocr_noise.py       the Arabic OCR confusion model
src/build_dataset.py   corpus -> (noisy, clean) pairs; auto-detects file format
src/train.py           LoRA finetuning
src/evaluate.py        CER/WER vs. baseline, by severity
src/infer.py           correction, batched, plus the guardrail
src/metrics.py         CER/WER, dependency-free
configs/               hyperparameters
notebooks/             Colab training notebook
results/               every number above, plus per-segment predictions

Limitations

  • The corruption is synthetic. It models Arabic OCR failure from the structure of the script; it is not a recording of a real engine's output. Calibrating against real Tesseract or PaddleOCR output on scanned pages is the honest next step, and until that is done these numbers describe performance on synthetic corruption.
  • Trained on Modern Standard Arabic news text. Dialectal and heavily classical text are out of distribution.
  • The guardrail catches wholesale rewrites, not small confident wrong edits. Those need token-level confidence or a lexicon check.
  • Weights are not committed; the adapter lives on the HuggingFace Hub.

License

MIT — see LICENSE. The base model's license governs the finetuned weights independently, as does the license of the corpus you train on.

About

Arabic OCR post-correction with a 0.5B SLM — synthetic supervision from a script-aware confusion model, scored on CER/WER against the raw OCR baseline

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages