olmOCR 不是传统 OCR,是 5 段流水线喂给 7B 视觉模型

发布于 2026年07月02日 01:41 #OCR#Github 解读 原文链接

olmOCR 不是传统 OCR,是 5 段流水线喂给 7B 视觉模型 封面图
  • olmOCR 是 AI2 推出的文档转文本项目,18178 Star、1498 Fork,Apache-2.0 协议,v0.4.27 版本,3 月刚发布
  • 五段流水线架构:预处理、布局分析、文本检测、文本识别、后处理,每步可独立优化
  • 7B 视觉模型是核心差异:相比传统 OCR 的像素级识别,用大模型理解文档语义、表格、公式和版式
  • 区别于 Tesseract、PaddleOCR 等传统方案:主打 PDF 和图片文档的干净纯文本输出,保留文档结构

上周翻 AI2 的仓库,看到一个叫 olmocr 的项目,18178 个 star,1498 个 fork。点进去,定位只有一句话,把 PDF 和图片文档转成干净的纯文本。最新版本停在 v0.4.27,3 月刚发,Apache-2.0 协议,仓库还活着。

说真的,OCR 这条赛道挤得要命,Tesseract 老牌,PaddleOCR 国货之光,MinerU、Marker、DeepSeek-OCR 一堆新秀挤在一起。olmocr 能在这片红海杀出来,靠的不是又一个规则引擎。

它根本没在做传统 OCR。

每一页 PDF,先被拍成一张栅格图,然后丢给一个 7B 参数的视觉语言模型去读。整条流水线的设计目标很朴素,每百万页转换成本不到 200 美元,而且能无人值守地批量跑。我把源码翻了一遍,发现真正有意思的不是那个 7B 模型,是包着它的那 5 段流水线。每一段都做了一个不太寻常的决策,合在一起才解释了它为什么能又便宜又稳。

第一道闸,整本 PDF 先被拦在门外

流水线的入口在 olmocr/pipeline.py。它不挑食,PDF、PNG、JPEG 都吃,甚至支持直接从 S3 拉 tar 打包的整批 PDF,丢到临时目录里一个个拆开。背后还有一个 work_queue.py,把任务切成 work item,本地跑或者扔到 Beaker 集群上跑都行。

但真正值得说的一道闸,是那个可选的过滤器 PdfFilter。你加上 --apply_filter 它才生效,默认是关的。一旦打开,它会做三件事,拦掉表单 PDF、拦掉 SEO 垃圾(download、epub、casino、viagra 这类词命中比例超过 0.004 就算垃圾)、拦掉非英文文档。

问题就出在最后这一条。pipeline.py 第 91 行把过滤器写死了

get_pdf_filter = cache(lambda: PdfFilter(languages_to_keep={Language.ENGLISH, None}, ...))

语言白名单是硬编码的 {Language.ENGLISH, None},命令行没暴露任何参数让你改成中文或者多语言。你要处理中文 PDF,要么别开这个过滤器,要么直接改源码。README 的特性列表里写着「支持公式、表格、手写、复杂排版」,唯独没提过滤器默认只放行英文。issue 区 #184 就有人反馈中文图片输出乱码,这种沉默的默认值,批量清洗语料的时候很容易踩坑。

你想想看,一个打着通用文档 OCR 旗号的工具,内置过滤器却默认只认英文,这落差多少有点大。

把页面拍成 1288 像素的栅格图

第二段是渲染,代码在 olmocr/data/renderpdf.pyrender_pdf_to_base64png。它干的事很直接,调 poppler 的 pdftoppm 子进程,把某一页渲染成 PNG。

非平凡的地方在 DPI 的算法

str(target_longest_image_dim * 72 / longest_dim)

那个 72 是「每点 1/72 英寸」的换算因子。它先量出这一页最长边有多少 PDF 点,再反推一个 DPI,保证输出图的最长边恰好等于 target_longest_image_dim。这个值默认是 1288,来自命令行参数 --target_longest_image_dim

这意味着什么?不管你的 PDF 是 US Letter、A4、还是奇怪的横版海报,喂给模型的图永远是一张最长边 1288 像素的 PNG。PDF 里原本干净的向量文本,到这里一律被栅格化成像素。

向量文本,丢掉。

坦白讲,第一次看到这个设计我有点愣。PDF 不是本来就带可提取的文本层吗,pdftotextpdfiumpypdf 都能直接抽文字,为什么要把信息量从向量降级成栅格?

其实吧,答案藏在 olmocr/prompts/anchor.py 里。这个文件实现了 get_anchor_text,支持五种抽取引擎,pdftotextpdfiumpypdfpdfreport,还有一个叫 topcoherency。最后这个很有意思,它把前三个引擎的结果都跑出来,然后塞进一个 135M 参数的小语言模型 SmolLM,算每个结果的平均 token 对数似然,谁连贯谁赢。

你看出门道了吗?AI2 早就试过把向量文本抽出来辅助模型,结果发现三个引擎抽出来的文字谁都不靠谱,得另开一个小模型去判断哪份最像人话。到了 v4 的提示词 build_no_anchoring_v4_yaml_prompt,他们干脆把锚点文本整个去掉了,模型只看图。

栅格化不是偷懒,是他们试了一圈发现向量文本信不过。

视觉模型的八次重试阶梯

第三段是真正的核心,视觉模型推理,入口是 process_page,跑的是 olmOCR-2-7B-1025-FP8。这一段最值得拆,因为它把一个 7B 的 VLM 当成了一个不稳定的算子,而不是一个神。

每个页面的默认重试上限是 8 次,来自 --max_page_retries 默认 8。重试分两条完全不同的路径。

第一条,旋转错误路径。模型在 front matter 里会吐一个 is_rotation_valid 字段,如果它说这页放歪了,还会顺带给出 rotation_correction,告诉你要转 90、180 还是 270。这条路径必须串行重试,因为下一次推理要把上一次的旋转角累加进去(cumulative_rotation),重新渲染图片再喂回去。你得等模型的反馈,才知道下一张图该怎么转。

第二条,非旋转错误路径,比如重复输出、超长截断、网络挂掉。这里有个聪明的小开关,每次失败后检查一下 vLLM 的队列,如果队列空了(vllm_queued_requests == 0),就把剩下几次重试一次性并发甩出去,用 asyncio.as_completed 谁先成功用谁。队列还有活儿就老老实实串行,别跟别的任务抢 GPU。

更细的是温度阶梯。TEMPERATURE_BY_ATTEMPT = [0.1, 0.1, 0.2, 0.3, 0.5, 0.8, 0.9, 1.0]。前两次几乎确定性,第三次开始升温,到最后一次直接拉到 1.0。这专门对付视觉模型的重复卡壳,温度高了,模型才肯从「the the the the」的死循环里跳出来。配合 olmocr/repeatdetect.py 里的 RepeatDetector,它从输出尾部反向数 n-gram 重复次数,一旦尾部某个 n-gram 重复飙上去,就判定这次生成废了。

8 次都救不回来的页面,也不是直接报错。make_fallback_result 会兜底,退化成纯 pdftotext 抽取,至少保证这一页有内容,不拖垮整批任务。

老实讲,这套设计里我最喜欢的细节是 HTTP 客户端。pipeline.py 没用 httpx 也没用 aiohttp,而是手写了一个 apost 函数,直接拿 asyncio.open_connection 拼 HTTP 报文。注释写得很直白,httpcore 的 session 池里有 4 把锁,跑到 1 亿请求量级会在各种奇怪的地方死锁,干脆自己写一个最薄的实现。

模型不是神,是会卡壳的算子。整个第三段都在围绕这个认知做工程。

让模型自己吐 front matter

第四段是文本后处理。olmocr 不让模型自由发挥,而是要求它先吐一段 YAML front matter,再吐正文。front matter 的 schema 在 PageResponse 里写死了,primary_languageis_rotation_validrotation_correctionis_tableis_diagramnatural_text 六个字段。

为了保证模型真的按格式输出,try_single_page 里挂了一个 guided_regex,用正则约束解码,让模型一开头的 front matter 必须匹配那个固定结构。这一手很关键,因为后面要用 FrontMatterParser 把 front matter 和正文拆开,格式乱了整条流水线就断。输出还要过两道关,total_tokens 超过 16384 的判废,finish_reason 不是 stop 的也判废。

我一直觉得这种「结构化输出加流式质量守门」的组合,比单纯堆模型大小有效得多。模型再大,也会偶发幻觉,工程上能做的就是把幻觉拦在交付之前。

评测基准的诚实与不诚实

最后一段是评测。olmocr 自带一个 olmOCR-Bench,7000 多条测试,覆盖 1400 份文档。它的设计哲学挺聪明,不用编辑距离这种软指标,而是把每条测试做成一个二选一的 pass/fail 断言,比如「某句话必须出现在这一页」「某个公式里 ∫ 必须出现在 x 左边」。这种 unit test 式的评测,比对着参考答案算字符差异要靠谱。

但说实话,这个基准有几个不太诚实的地方,我把源码翻了才发现。

第一,基准的 ground truth 是用 GPT 自己挖出来的。看 olmocr/bench/miners/ 目录,mine_tables_gpt.pymine_math.pymine_headers_footers.py 一堆脚本,全是调 GPT 去给文档生成测试断言。也就是说,用来评判 OCR 模型的标准,本身是另一个闭源大模型生成的。这里头有个不太舒服的循环,GPT 挖出来的 case 难免带它自己的偏差,哪些算「该识别」哪些算「可以漏」,是 GPT 说了算。

第二,README 的特性列表赫然写着「支持手写」,但 bench 里压根没有手写专项。最能体现复杂识别能力的 old_scans_math(老旧扫描件里的数学)这一列,所有参赛模型,Mistral、Marker、MinerU、DeepSeek-OCR、olmocr 自己,得分都卡在 33 到 83 之间,olmocr 是 82.3。看起来不错,但你要是拿一张潦草手写笔记去测,基准给不了你任何承诺。「支持手写」这句话在基准里是没被验证的。

第三,基准只跑单页 PDF。README 自己写得很坦诚,olmocr-bench operates on single page PDFs directly。可 olmocr 的卖点之一是「跨页的自然阅读顺序」。单页测不了跨页,这意味着它最吹的那个能力,基准根本没覆盖。

第四,那张成绩表里有几个带星号 * 的行,MinerU、PaddleOCR-VL、Infinity-Parser、Chandra OCR,意思是自报成绩。Infinity-Parser 那行甚至写着 82.5±?,不确定度是个问号。自家基准,对手自报,还不带误差,这个对比的可信度你自己掂量。

聪明的基准也会骗人,尤其是当它既当裁判又当运动员的时候。

栅格优先模式,和它的天花板

把 5 段流水线合起来看,olmocr 给出的其实是一个可命名的模式,我管它叫「栅格优先 OCR」。它的核心判断是,PDF 的向量文本层在复杂版式下信不过,与其费劲修各种文本抽取的 bug,不如统一栅格化,把版式理解的活儿整个甩给视觉模型。再用一套温度递增的重试阶梯、旋转反馈回路、重复检测、pdftotext 兜底,把模型的不稳定压到可接受。

这个模式的适用边界很清楚。

该用的场景,英文为主的学术 PDF、arXiv 论文、多栏排版的技术文档、带表格和公式的资料,批量转语料喂大模型训练。这正是 AI2 自己的用途,每百万页 200 美元的成本就是为这个量级算的。16 个贡献者里 jakep-allenai 一个人扛大头,bus factor 不算高,但项目迭代节奏稳定,从 v0.1.58 到 v0.4.27 一年多走了二十多个版本。

别碰的场景,中文为主的扫描件。那个写死英文的过滤器只是表象,更深层的是基准里中文缺位,模型在中文上的表现没有公开验证。手写笔记也别指望,README 说了支持,bench 没测。老旧模糊扫描件,所有模型都卡在 33 分上下,olmocr 也不例外。还有实时单页转换,它要起一个 vLLM 推理服务,冷启动和吞吐都是为批量优化的,你要的是 200 毫秒内出结果,这不是它的菜。

反正我觉得,olmocr 不是要取代 Tesseract。它瞄准的是「给大模型喂数据」这件事的工业化。栅格化看起来粗暴,但他们用一整条流水线的工程量,把粗暴换成了可预期。这年头,能把不稳定的视觉模型驯成一条无人值守的批量产线,本身就是一种本事。

评论互动

© 2026 王若风的技术博客 · Powered by Astro