Cheat sheets

OCR and Document Understanding

Train CTC OCR, pick a document stack, retrieve pages with MaxSim and approve fields against exact source spans.

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

Classical OCR pipeline#

clientImagemodelText detectionDB, EAST, CRAFTstoreBounding boxesword or lineprocessCrop each regionfixed heightmodelRecognitionCRNN + CTCstoreText stringsprocessLayout orderingreading orderclientReading-order textclientImagemodelText detectionDB, EAST, CRAFTstoreBounding boxesword or lineprocessCrop each regionfixed heightmodelRecognitionCRNN + CTCstoreText stringsprocessLayout orderingreading orderclientReading-order text

From Lesson 04.19

Tasks in code#

TaskCode
CTC lossctc_loss(log_probs, targets, ...)
Greedy CTC decodegreedy_ctc_decode(log_probs, blank=0)
Line recogniserTinyCRNN(vocab_size=40, hidden=128, feat=32)
Production OCRPaddleOCR(lang="en").ocr(image_path)
LayoutLMv3 inputlayoutlm_input(tokens)
Donut JSON schemadonut_schema("invoice")
Top-k pages by MaxSimretrieve(query, pages, k=3)

From Lesson 04.19, 04.19 code/main.py, 12.22 code/main.py, 12.23 code/main.py, 19.04 code/main.py

Train a CRNN with CTC#

vocab = ["_"] + list("0123456789abcdefghijklmnopqrstuvwxyz")blank at index 0
model = TinyCRNN(vocab_size=len(vocab))
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
imgs, targets, target_lens = build_batch(strings, vocab)height 32
log_probs = model(imgs)(W', N, vocab)
input_lens = torch.full((8,), log_probs.size(0), dtype=torch.long)
loss = F.ctc_loss(log_probs, targets, input_lens, target_lens,
blank=0, reduction="mean", zero_infinity=True)
opt.zero_grad(); loss.backward(); opt.step()~3 to ~0.2 in 200 steps

From Lesson 04.19, 04.19 code/main.py

MaxSim late interaction#

def maxsim(query_tokens, patches):
s = 0.0
for q in query_tokens:one vector per query token
best = max(cosine(q, p) for p in patches)best page patch
s += bestsum over query tokens
return s
scored = [(p.doc_id, maxsim(query.tokens, p.patches)) for p in pages]
scored.sort(key=lambda x: -x[1])top-k pages
idx = build_index(prune=True)DocPruner on, 50% patches
hits = idx.retrieve(q, k=3)

From 12.23 code/main.py, 19.04 code/main.py

Commands#

CommandWhat it does
python3 cli.py sample.txt --schema schema.json --output output/review.htmlBuilds the HTML review desk and a JSON report
--proposals <file>Loads external OCR or model candidates
--approve <file>Applies decisions downloaded from the HTML desk
python3 scripts/project_test.py document-extraction-desk --all --solution --strictGrades the reference solution
python main.pyRuns the capstone MaxSim and DocPruner scaffold
npm startRuns the viewer self-check once and exits 0
npm run serveStarts the viewer HTTP server on 127.0.0.1

From document-extraction-desk/README.md, solution/cli.py, 19.04 code/ts/README.md, 19.04 code/main.py

Abbreviations#

CTC
Connectionist Temporal Classification. Trains without per-timestep labels.
CRNN
conv features + BiLSTM + CTC. The 2015 baseline.
VLM-OCR
a vision-language model trained or prompted for OCR.
DocVQA
the document VQA standard. The most-cited score.
ViDoRe
Visual Document Retrieval Evaluation. Metric is nDCG@5.
PQ
product quantization. Shrinks storage ~8x.

From Lesson 04.19, Lesson 12.22, Lesson 12.23

Vision-first document QA#

1Ingestrender each PDF page to a 1536x2048 PNG. Keep{doc_id, page_num, image_path}.2Embedrun ColQwen2.5-v0.2. Get ~2048 patch vectors ofdim 128 per page.3PruneDocPruner keeps the highest-signal half of thepatches.4Queryembed query tokens. Run MaxSim. Return the top-kpages.5Synthesizegive the query and the top-5 page images to theVLM. Cite (doc_id, page).6Evidencedraw the cited bounding boxes as overlays in theviewer.7OCR fallbackrun Nougat or dots.ocr on equation-dense pages.8Evalreport ViDoRe v3 nDCG@5 and M3DocVQA againstOCR-then-text.

From Lesson 19.04

Formulas and numbers#

CTC collapse
merge repeats, then remove blanks. "h h h _ _ e e l l _ l l o _ _" gives "hello".
CER
Levenshtein distance divided by the reference length. WER is the same per word.
MaxSim
\sum_i \max_j \cos(q_i, p_j) over query tokens q_i and page patches p_j.
ColPali storage
50 pages x 729 patches x 128 dims x 4 bytes = ~18 MB raw, ~4 MB after PQ.
Text-RAG storage
50 chunks x 768 dims x 4 bytes = ~150 kB. ColPali is ~30x more.
ViDoRe
ColPali-v1 scores ~80% nDCG@5. Text-RAG scores ~50-60%.

From Lesson 04.19, Lesson 12.23

Defaults and limits#

SettingDefaultNote
CER target< 2%On clean scans
CRNN input height32CNN pools height to 1. Width is time
Page render180 DPIPyMuPDF in the capstone
DocPruner50%Under 0.5% accuracy loss
Schema fields1 to 30Lowercase names, unique labels
Document size100000Characters at most
Candidates300Proposals at most per review

From Lesson 04.19, Lesson 19.04, solution/main.py

Pick a document stack#

Printed invoices at scale
LayoutLMv3 + rules. Cost-efficient.
Mixed, handwritten, forms
VLM-native (PaliGemma 2 or Qwen2.5-VL).
arXiv papers
Nougat for math, a VLM for figures.
Regulated
OCR pipeline + VLM validator as a cross-check.
Receipts with fixed structure
fine-tune Donut.
Pure-text documents
text-RAG. It is simpler and uses less storage.
Visually rich PDFs
ColPali for quality, VisRAG for scale.

From Lesson 12.22, Lesson 12.23, Lesson 04.19

Troubleshooting#

SymptomCauseFix
"Approval belongs to another document version"Text changed, sourceSha256 differsReview the new version again
"Proposal does not match its exact source span"Quote is not at start, endFix the offsets of the proposal
"Use an ISO date: YYYY-MM-DD"Date field text is not ISOCorrect the source text
Field state is ambiguousTwo Seats quotesChoose one span with --approve
Donut fails on a paperOCR-free models are specialistsUse Nougat for papers
Chart data missing in RAGOCR drops chart dataEmbed page images with ColPali

From solution/main.py, solution/cli.py, docs/en.md, Lesson 12.22, Lesson 12.23

Questions#

Why CTC?
It trains without character-level alignment.
When do OCR pipelines still win?
Massive scan scale, deterministic failures, auditable output.
Why MaxSim over a mean?
Each query token picks its best patch. Irrelevant patches do not wash it out.
Do offsets prove the value is right?
No. They prove provenance. A reviewer confirms the field.

From Lesson 04.19, Lesson 12.22, Lesson 12.23, document-extraction-desk/README.md