OCR 与文档理解


文档摘要

OCR 与文档理解 本节摘要:OCR 是一条三阶段流水线——检测文本框、识别字符、再排出版式;每个现代系统都在重排或合并这三步。本节追踪经典流水线(检测→识别→版式)与现代端到端方案(Donut、Qwen-VL-OCR),从零实现 CTC 损失(无需字符级对齐就能训练序列模型)和一个微型 CRNN 识别器,用 PaddleOCR/EasyOCR 免训练解析生产文档,并讲清 OCR、版式解析、文档理解三层技能的边界。读完本节,你能为「从图里读字」到「从收据里抽出总金额」这条比想象更长的路,选对每一段的工具。 对应原课程:Phase 4 · Lesson 19 · (原英文 )。

OCR 与文档理解

本节摘要:OCR 是一条三阶段流水线——检测文本框、识别字符、再排出版式;每个现代系统都在重排或合并这三步。本节追踪经典流水线(检测→识别→版式)与现代端到端方案(Donut、Qwen-VL-OCR),从零实现 CTC 损失(无需字符级对齐就能训练序列模型)和一个微型 CRNN 识别器,用 PaddleOCR/EasyOCR 免训练解析生产文档,并讲清 OCR、版式解析、文档理解三层技能的边界。读完本节,你能为「从图里读字」到「从收据里抽出总金额」这条比想象更长的路,选对每一段的工具。

对应原课程:Phase 4 · Lesson 19 · ocr-document-understanding(原英文 phases/04-computer-vision/19-ocr-document-understanding/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 追踪经典 OCR 流水线(检测→识别→版式)与现代端到端替代(Donut、Qwen-VL-OCR)。
  2. 为序列到序列 OCR 训练实现 CTC(连接时序分类)损失
  3. 用 PaddleOCR 或 EasyOCR 免训练做生产级文档解析。
  4. 区分 OCR、版式解析、文档理解,按任务选对工具。

一、问题与直觉

满是文字的图像无处不在:收据、发票、证件、扫描书、表单、白板、路牌、截图。从中抽取结构化数据——不只是字符,而是「这是总金额」——是最高价值的应用视觉问题之一。

这个领域分三层技能:

  1. OCR 本体:把像素变成文字。
  2. 版式解析:把 OCR 输出按区域分组(标题、正文、表格、页眉)。
  3. 文档理解:从版式里抽取结构化字段(invoice_total = $42.50)。

每层都有经典与现代两套做法,而「我要图里的字」与「我要这张收据的总金额」之间的差距,比大多数团队以为的大得多。

经典流水线

  • 文本检测产出每行或每词的四边形。
  • 识别把每个区域裁到固定高度,跑 CNN + BiLSTM + CTC 产出字符序列。
  • 版式重建阅读顺序(拉丁文系自上而下、自左而右;阿拉伯文、日文不同)。

CTC 一段话讲清

OCR 识别从定长特征图产出变长序列。CTC(Graves 等,2006)让你无需字符级对齐就能训练。模型在每个时间步输出(词表 + blank)上的分布;CTC 损失对所有「合并重复、去掉 blank 后等于目标文本」的对齐做边缘化。

原始输出: "h h h _ _ e e l l _ l l o _ _" 合并重复去 blank 后: "hello"

CTC 是 2015 年 CRNN 能用的原因,2026 年仍在训大多数生产 OCR 模型。

现代端到端模型

  • Donut(Kim 等,2022)——ViT 编码器 + 文本解码器,读图直接吐 JSON,无文本检测器、无版式模块。
  • TrOCR——ViT + transformer 解码器,做行级 OCR。
  • Qwen-VL-OCR / InternVL——为 OCR 微调的完整视觉语言模型,2026 年复杂文档上精度最佳。
  • PaddleOCR——经典 DB + CRNN 流水线的成熟生产包,仍是开源主力。

端到端模型要更多数据和算力,但跳过多阶段流水线的误差累积。

版式解析

对结构化文档,跑版式检测器(LayoutLMv3、DocLayNet),给每个区域打标签:标题、段落、图、表、脚注。阅读顺序就变成「按版式顺序遍历区域、拼接」。

表单用键值抽取模型(富视觉文档用 Donut,纯扫描用 LayoutLMv3),输入图像+检测到的文本+位置,预测结构化键值对。

评估指标

  • 字符错误率(CER)——编辑距离 / 参考长度,越低越好,生产目标干净扫描 < 2%。
  • 词错误率(WER)——词级同理。
  • 结构化字段 F1——键值任务,衡量 {invoice_total: 42.50} 是否正确出现。
  • JSON 编辑距离——端到端文档解析,Donut 论文引入归一化树编辑距离。

二、从零实现

步骤 1:CTC 损失 + 贪心解码

import torch import torch.nn as nn import torch.nn.functional as F def ctc_loss(log_probs, targets, input_lengths, target_lengths, blank=0): """ log_probs: (T, N, C) 词表含 blank(索引 0)上的 log-softmax targets: (N, S) int 目标(无 blank) input_lengths: (N,) 每样本使用的时间步 target_lengths: (N,) 每样本目标长度 """ return F.ctc_loss(log_probs, targets, input_lengths, target_lengths, blank=blank, reduction="mean", zero_infinity=True) def greedy_ctc_decode(log_probs, blank=0): """ log_probs: (T, N, C) log-softmax 返回: 索引序列列表(去 blank、合并重复) """ preds = log_probs.argmax(dim=-1).transpose(0, 1).cpu().tolist() out = [] for seq in preds: decoded = [] prev = None for idx in seq: if idx != prev and idx != blank: decoded.append(idx) prev = idx out.append(decoded) return out

F.ctc_loss 在可用时用高效 CuDNN 实现。贪心解码比束搜索简单,CER 通常只差 1% 以内。

步骤 2:微型 CRNN 识别器

最小 CNN + BiLSTM 做行 OCR。

class TinyCRNN(nn.Module): def __init__(self, vocab_size=40, hidden=128, feat=32): super().__init__() self.cnn = nn.Sequential( nn.Conv2d(1, feat, 3, 1, 1), nn.BatchNorm2d(feat), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(feat, feat * 2, 3, 1, 1), nn.BatchNorm2d(feat * 2), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(feat * 2, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True), nn.MaxPool2d((2, 1)), nn.Conv2d(feat * 4, feat * 4, 3, 1, 1), nn.BatchNorm2d(feat * 4), nn.ReLU(inplace=True), nn.MaxPool2d((2, 1)), ) self.rnn = nn.LSTM(feat * 4, hidden, bidirectional=True, batch_first=True) self.head = nn.Linear(hidden * 2, vocab_size) def forward(self, x): # x: (N, 1, H, W) f = self.cnn(x) # (N, C, H', W') f = f.mean(dim=2).transpose(1, 2) # (N, W', C) h, _ = self.rnn(f) return F.log_softmax(self.head(h).transpose(0, 1), dim=-1) # (W', N, vocab)

定高输入(CNN 把高度 max-pool 到 1),宽度作 CTC 的时间维。

步骤 3:合成 OCR

生成黑底白数字串做端到端冒烟测试。

import numpy as np def synthetic_line(text, height=32, char_width=16): W = char_width * len(text) img = np.ones((height, W), dtype=np.float32) for i, c in enumerate(text): x = i * char_width shade = 0.0 if c.isalnum() else 0.5 img[6:height - 6, x + 2:x + char_width - 2] = shade return img def build_batch(strings, vocab): H = 32 W = 16 * max(len(s) for s in strings) imgs = np.ones((len(strings), 1, H, W), dtype=np.float32) target_lengths = [] targets = [] for i, s in enumerate(strings): imgs[i, 0, :, :16 * len(s)] = synthetic_line(s) ids = [vocab.index(c) for c in s] targets.extend(ids) target_lengths.append(len(ids)) return torch.from_numpy(imgs), torch.tensor(targets), torch.tensor(target_lengths) vocab = ["_"] + list("0123456789abcdefghijklmnopqrstuvwxyz") imgs, targets, lengths = build_batch(["hello", "world"], vocab) print(f"图像: {imgs.shape} 目标: {targets.shape} 长度: {lengths.tolist()}")

真实 OCR 数据集会加字体、噪声、旋转、模糊、颜色,但上面的流水线完全一致。

步骤 4:训练骨架

model = TinyCRNN(vocab_size=len(vocab)) opt = torch.optim.Adam(model.parameters(), lr=1e-3) for step in range(200): strings = ["abc" + str(step % 10)] * 4 + ["xyz" + str((step + 1) % 10)] * 4 imgs, targets, target_lens = build_batch(strings, vocab) log_probs = model(imgs) # (W', 8, vocab) input_lens = torch.full((8,), log_probs.size(0), dtype=torch.long) loss = ctc_loss(log_probs, targets, input_lens, target_lens, blank=0) opt.zero_grad(); loss.backward(); opt.step()

在这套平凡合成数据上,200 步内损失应从 ~3 掉到 ~0.2。

三、框架对比

三条生产路径:

  • PaddleOCR——成熟、快、多语言,一行用法:paddleocr.PaddleOCR(lang="en").ocr(image_path)
  • EasyOCR——Python 原生、多语言、PyTorch 主干。
  • Tesseract——经典,模型挣扎的老扫描件仍有用。

端到端文档解析用 Donut 或 VLM:

from transformers import DonutProcessor, VisionEncoderDecoderModel processor = DonutProcessor.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2") model = VisionEncoderDecoderModel.from_pretrained("naver-clova-ix/donut-base-finetuned-cord-v2")

收据、发票、结构可复用的表单,微调 Donut;任意文档或带推理的 OCR,Qwen-VL-OCR 这类 VLM 是当前默认。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-ocr-stack-picker.md:一个提示词——给定文档类型、语言、结构,在 Tesseract / PaddleOCR / Donut / VLM-OCR 之间挑。
  • skill-ctc-decoder.md:一个技能——从零写出贪心和束搜索 CTC 解码器,含长度归一化。

五、练习

  1. (简单) 在 5 位随机数字串上训 TinyCRNN 500 步,报告留出集 CER。
  2. (中等) 用束搜索(beam_width=5)替换贪心解码,报告 CER 差值。束搜索在哪些输入上占优?
  3. (困难) 用 PaddleOCR 处理 20 张收据,抽取明细项,对手工标注的 {item_name, price} 对算 F1。

本节要点回顾

  1. 三层技能:OCR 本体(像素→字)、版式解析(分组区域)、文档理解(抽结构化字段)。
  2. 经典三阶段流水线——检测(DB/EAST/CRAFT)→ 识别(CRNN+CTC)→ 版式排序。
  3. CTC 让无需对齐训练——对每个时间步输出(词表+blank)分布,边缘化所有合并重复、去 blank 后等于目标的对齐。
  4. CRNN = CNN+BiLSTM+CTC——2015 基线,2026 仍是大多数生产 OCR 主力。
  5. 端到端跳过误差累积——Donut(ViT+解码器直接吐 JSON)、TrOCR、Qwen-VL-OCR(VLM,复杂文档 SOTA)。
  6. 版式解析——LayoutLMv3/DocLayNet 给区域打标签;表单用键值抽取(Donut/LayoutLMv3)。
  7. 评估:CER(干净扫描<2%)、WER、结构化字段 F1、JSON 树编辑距离。
  8. 生产工具:PaddleOCR(主力)、EasyOCR(Python 原生)、Tesseract(老扫描件);Donut/VLM 做端到端解析。

下一节进入图像检索与度量学习——把图像映成嵌入,学一个让同类靠近、异类远离的距离。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U