面壁 MiniCPM-OCR 的读判分离:跟着一页发票走完五站,看那个「不吐字的裁判」在干什么

举报
HugoLiao 发表于 2026/10/30 15:44:31 2026/10/30
【摘要】 面壁 10 月 28 日开源了两个模型:1.3B 的 MiniCPM-OCR 1.0,六项基准拿下四项 SOTA;2B 的 MiniCPM-OCR Jev,一个字都不生成。一个 OCR 新王,为什么要给自己配一个专职裁判?这件事的解释不在跑分表里,在数据流里。这篇不按技术点罗列,而是跟着一页发票走完五站,看每一站它在干什么、省了什么、卡在哪。 零、先说清楚那个反常之处先把两个模型的角色摆出来...

面壁 10 月 28 日开源了两个模型:1.3B 的 MiniCPM-OCR 1.0,六项基准拿下四项 SOTA;2B 的 MiniCPM-OCR Jev,一个字都不生成。

一个 OCR 新王,为什么要给自己配一个专职裁判?这件事的解释不在跑分表里,在数据流里。这篇不按技术点罗列,而是跟着一页发票走完五站,看每一站它在干什么、省了什么、卡在哪。


零、先说清楚那个反常之处

先把两个模型的角色摆出来:

MiniCPM-OCR 1.0 MiniCPM-OCR Jev
参数 1.3B 2B
干什么 读图,输出 Markdown / LaTeX / HTML 表格 只给候选结果打分,不输出任何文字
输出 结构化文本 + 坐标 一个 0–1 的分数
许可证 Apache 2.0 Apache 2.0
速度 1.217 页/秒(RTX 5090) 毫秒级(一次前向)

一个只判卷不答题的模型,为什么需要存在?

因为行业里有一个一直没被正面回答的问题。过去几年所有 OCR 厂商都在比"认得准",宣称 99%。但在财税、金融、核保这类场景里,99% 意味着:

100 张发票,错了 1 张,但你不知道是哪一张。你只能雇人把 100 张全部重看一遍。

于是出现了那个荒诞的场景:企业买了最前沿的 AI,转头又雇几十号人做流水线核对。不知道错在哪的 99 分,和 0 分一样,都要人类兜底。

Jev 要解决的就是这一件事:把"错在哪"标出来。 而它用的方法,是把"读"和"判"彻底拆成两个模型。

下面跟着数据走一遍,就明白为什么要拆。


第 0 站:版面切块——不是模型干的

这一站最容易被忽略,但它决定了后面所有环节的输入质量。

MiniCPM-OCR 的解析管线不是一个模型,是三部分:

PDF / PNG / JPG
    ↓
minicpmvocr SDK          读取文件,PDF 逐页栅格化成图
    ↓
PP-DocLayoutV3           检测文档区域 + 判定阅读顺序,切成 crop
    ↓
识别模型(1.3B)          逐个 crop 并发识别
    ↓
按阅读顺序拼装成 Markdown

两个部署上必须知道的细节:

细节一:SDK 和推理服务跑在两个不同的 Python 环境里,而且可以部署在不同机器上。 这不是设计洁癖,是实用考虑——SDK 侧要装 PDF 处理、图像栅格化的依赖(体积大、依赖冲突多),推理服务侧要装 GPU 推理栈。混在一个环境里,装依赖能折腾半天。分开之后,你可以在 A 机器跑 SDK、B 机器跑 GPU 推理。

细节二:区域类型是靠 prompt 指定的,不是自动判断的。

支持的五种:

Text Recognition:      正文识别
Table Recognition:     表格
Formula Recognition:   公式
Seal Recognition:      印章
Chart to Table:        图表转表格

这意味着什么?你必须知道自己要给模型看什么。 如果你的业务里有一批"发票右下角有个红章,需要单独读章上的税号",你得显式指定 Seal Recognition:。默认的正文识别不会主动去找章。

这是一个实用但常被忽略的能力——大多数 OCR 管线里,印章是干扰项(印泥糊住正文),但在这里它可以是一个独立的信息源。票面信息 + 章面信息两侧对照,核验真伪多了一道依据。


第 1 站:识别——1.3B 是怎么做到 1.2 页/秒的

这一站是唯一"正常"的一站:一个视觉语言模型看图,输出结构化文本。

架构拆开看:

组件 用的是什么
视觉编码器 SigLIP
语言主干 Qwen3.5 混合注意力
底座来源 基于 MiniCPM-V 4.6 训练
定位 MiniCPM-V 家族首个 OCR 专用模型
输出格式 Markdown 正文 + LaTeX 公式 + HTML 表格(从 OTSL 转换)+ 带坐标的阅读顺序

“OCR 专用"这个定语是关键。过去做文档解析,通用的多模态模型也能干,但它是"什么都会一点”;专用模型是在同一个底座上,把能力全部压到"读懂版面"这一件事上。参数降到 1.3B,能力反而更强,靠的就是这个收窄。

速度靠两块技术:

技术 作用
LLaVA-UHD v4 视觉分片 448 分辨率下做 4 倍视觉 token 压缩,最多 9 个切片。大图切块编码,把 token 数压到小模型能跑的量级
DSpark 投机采样解码 端到端处理效率提升 55%

这两块叠起来,才有这个结果——单张 RTX 5090,平均不到 1 秒解析一页。

同硬件、同测试条件下的吞吐对比:

模型 页/秒 相对值
MiniCPM-OCR 1.0 1.217 1.00x
PaddleOCR-VL-1.6 0.818 0.67x
MinerU2.5-Pro-Advanced 0.336 0.28x
TeleOCR 0.141 0.12x

单卡吞吐约为 TeleOCR 的 8.6 倍。

精度侧,六项基准拿四项 SOTA(均为厂商自测):

基准 得分 说明
Real5-OmniDocBench 93.68 专考手机随手拍、镜头畸变、反光翻拍
Wild-OmniDocBench 92.39 野生真实文档
OCRBench v2 54.7 第二名 47.0
CHAOS-Bench 29.78 考忠实度,第二名的 1.4 倍以上

CHAOS-Bench 这一项值得单独看。 它测的不是"认得多准",是"在强语义先验干扰下,能不能忠实于图片识别"。换句话说:图上是 5.576M,模型会不会因为"这里应该是个金额"就自己脑补成 $5.576M。

这个基准的存在本身就说明:OCR 领域最大的问题已经不是"认不出字",而是"认出了它以为该有的东西"。

而这一站,恰好解决不了这个问题。

五类真实文档的实测表现

跑分的说服力有限,看具体场景更有用。官方放出的实测覆盖了五类真实文档,每一类都对应一种典型失败模式:

文档 难点 结果 这一步验证了什么
发票(红章压字) 红色表格线 + 两枚压字的印章 + 密集数字 购买方/销售方/密码区左右分栏还原,合并单元格原样,18 位纳税人识别号、16 位银行账号逐位对上;红章被单独切出,没有糊进正文 版面分层能力:知道哪些笔画属于章、哪些属于正文
手写推导笔记 斜阳打光、半边泛橙;公式 + 手绘曲线 + 箭头批注 + 方框 整条推导链逐行还原成标准 LaTeX,带分数线的约分、上下标一步不乱;两张手绘示意图被单独切成图片保留 混合内容(文字/公式/图形)的区域判定
揉皱的英文教材复印页 纸张揉皱、满页矩阵和不等式 先自动摆正 2°,再逐行还原;分块矩阵 [A −I]、上标 x*、下标 x₁ 全部转成可渲染 LaTeX,4×4 矩阵一个数都没错 几何校正 + 数学结构识别
杂志涂鸦墙照片 几种字体大标题挤在一起,白字印在砖墙涂鸦上 背景极花的白色小字完整读出四个自然段,照片区域单独切图 复杂背景下的文字提取
整版中文日报 横排竖排标题混搭、窄栏正文、照片穿插、"下转第四版"跨版提示 按阅读顺序重排版面;连报头下的刊号、邮发代号、新闻热线、电子邮箱这些小字都没漏 阅读顺序判定(这是版面模型的核心价值)

最后一条值得单独留意:"阅读顺序"才是报纸这一类文档的真正难点。 横排竖排混排的版面上,认得每个字不难,知道"先读哪一栏、再读哪一栏"才难。而这个能力来自第 0 站的 PP-DocLayoutV3,不是识别模型。

这就引出一个部署上的关键判断:

如果你换掉了 PP-DocLayoutV3,识别模型还是那个模型,但报纸、多栏文档、复杂表格的阅读顺序会直接崩掉。

这个管线是三个组件绑定的能力,不是一个模型的独角戏。换零件之前,先搞清楚每一段在管什么。


第 2 站:重新渲染回图——这一步是整个设计的枢纽

数据从识别模型输出后,如果不做任何事直接入库,就回到了开头那个困境:99% 正确,不知道错在哪。

所以 Jev 的输入不是"文本",是三元组:

① 原图 crop            这一块长什么样
② 候选解析文本          模型说它是什么
③ 由②重新渲染出的图     把模型说的话再"画"一遍

为什么要多此一举,把文本重新渲染成图?

因为这样比对才成立:

比对空间 比什么 能抓出什么错
视觉空间(① vs ③) 版式结构 错列、漏行、表格行列错位、合并格没填
跨模态空间(① vs ②) 字符语义 数字看错位、符号丢失、上下标退化

如果只给 Jev 文本,它能判断"这段话通不通顺",但判断不了"这段话和图对不对得上"。加上渲染回来的那张图,它就有了一把可以逐格对照的尺子。

这一步是整个"可信 OCR"概念的技术落点。它把"检查有没有认错"这个任务,从语义判断(容易脑补)转成了视觉比对(难以糊弄)。


第 3 站:判分——2B 模型,不生成一个字

Jev 的改造方式非常干脆:

从 Qwen3.5-2B 出发,保留视觉编码器和语言主干,把语言模型的输出头换成二分类头。

输出头一换,性质就变了:它不再是"生成模型",而是"判别模型"。一次前向,不生成任何文字,毫秒级给出 0–1 的分数。

这个"不生成"的设计,解决了两个问题:

问题一:速度。 如果让一个大模型做质检,它得字斟句酌生成一大段推理,延迟极高。你不可能为了质检让整条流水线等几秒。而一次前向是毫秒级的,可以卡在"识别"和"入库"之间当闸机。

问题二(更重要):自我评估的天然失效。

这是整篇文章我认为最值得记住的一段推理:

为什么不能让解析模型自己给自己打分?

因为自回归生成模型在逐字解码时有很强的语言先验。哪怕它把票据里的数量少看了一个零,或者把公式的上下标吞掉了,只要生成的文本在语法上通顺,模型内部的 token 预测概率依然很高。

它抓不出自己的事实错误,因为它的"自信"来自"这句话读起来对不对",不是"这张图上是这个吗"。

用一个类比:让大模型给自己打分,就像让考生自己判卷。它不是不认真,是它的大脑会自动脑补它以为该有的东西。 市面上一些 OCR 引擎附带的"置信度",本质上就是这种自己给自己的分。

而 Jev 的分数不一样——它是用大量真实人工标注数据做过统计校准的。不是模型"感觉对",是"在这类样本上,这个分数对应这个正确率"。

效果数据:

模型 参数 AUROC(判别能力)
MiniCPM-OCR Jev 2B 93.6
Qwen3.5 397B 93.8

2B 几乎打平 397B。 差了 0.2 分,参数差了 198 倍。

这个结果说明一件事:"判断一个 OCR 结果对不对"和"生成一个 OCR 结果"是两种不同的能力,前者不需要那么大的模型。 把两种能力塞进一个巨无霸里,是过去行业的默认做法,但它不是唯一解法——而且是最贵的那种。


第 4 站:重试决策——三轮实测,无人干预

这是全文最有说服力的一段,因为它抓住了人类肉眼极易放过的一种错。

测试样本:DeepSeek-V3 技术报告里那张训练成本表(就是"557.6 万美元"那个数字的出处)。

第一轮:

项目 结果
数字 全对——2664K、119K、5K,连合计 5.576M 都对
符号 四个美元符号全被读成了公式标记
合计呈现 变成 )5.576M
Jev 判分 约 0.000001

注意这个失败模式:所有数字都是对的。 人扫一眼,看到 2664K、119K、5.576M 都在,很容易放过——但那个 ) 说明 LaTeX 解析已经错位,整张表的语义坏了。

这就是"99% 正确"的典型形态:错的那 1% 藏在符号里,不在数字里。 而人对数字更敏感,正好漏掉符号。

第二轮: 程序按策略处理——原图放大 2 倍、四周留白。结果:美元符号还是不对,Jev 分数纹丝不动。

这一轮的意义在于:它证明了 Jev 的分数是稳定的,不是随机波动。放大图片没有改善,它就诚实地不给分。如果分数会随无关操作漂移,这个质检机制就不可信了。

第三轮: 程序按预设策略换了一套识别流程。四个 $ 全部回来了,Jev 复判给 0.9692,重试停止,表格存成 JSON。

最终结果:表头、各阶段 GPU 小时数、美元金额,逐项对回原图全部一致。三轮识别、两次返工,中间没有人手工补一个符号,也没有人守着点"再试一次"。

这一站在工程上的意义是:它把"要不要重试"变成了一个可以写进代码的判断。

读 → 判 → 分数低于阈值 → 换策略重读 → 再判 → 过线入库
                                    ↓
                              仍不过线 → 标记出来交人复核

人类审核的角色,从"全量巡检"变成"异常块复核"。


第 5 站:入库——三种用法和一个必须防的陷阱

Jev 不绑定 MiniCPM-OCR。 它是高度解耦的独立决策模型,可以作为"置信度外挂"架在任何 OCR 之后:PaddleOCR、TeleOCR、商业 API、本地私有化微调的模型,都不用改识别底座。

三种标准用法:

用法 做法 适合
阈值自动放行 每个区域打分,过阈值的直接入库,剩下的推人工 大批量、人工复核成本高的场景
区域内 best-of-N 多个解析器读同一个 crop,取最高分的那个 有多个现成解析器可调
跨块批选择 整份文档按阅读顺序从最佳候选中拼装 追求单份文档的最高质量

但这里有一个必须防的陷阱,它比分数本身更容易出事:

服务返回的是 verdict + score 两个值,只有 scored 才代表模型真的跑过。

Precheck、图像渲染、前向计算失败时,返回的是固定值——这些固定值不能被当成低分。

一个忽略 verdict 的流水线,会把"坏请求"静默当成"文档不合格"。

这是和前面提到的"静默失败"同一类问题,但更隐蔽:分数低,你会怀疑文档质量;verdict 异常,才该怀疑服务本身。 两者混在一起,你会花几天时间去查一个根本不存在的"识别精度问题",而真正的原因是一个渲染请求超时了。

入库前必须有的两行判断:

if verdict != "scored":
    # 服务侧问题,不是文档问题。重试或告警,不要走质量判断分支
    raise RuntimeError(f"Scoring did not run: {verdict}")
if score < THRESHOLD:
    # 真实的低置信,走重试/人工复核
    ...

边界:Jev 做不到的三件事

第一,Jev 只能在"已有的候选"里选。

它的输入里必须包含候选文本。如果所有解析器都漏了某个表格单元格,那个单元格根本不在候选集里,Jev 无从打分,也无从补上。

这是判别模型的结构性局限:它能判断"这个答得对不对",不能发现"这里还有一道题没答"。 漏检(false negative on existence)这一类错误,仍然要靠其他手段——比如行列数校验、小计对齐。

第二,阈值调高和人工复核量是一对跷跷板。

阈值高,自动通过率降、准确率高、人工量大;阈值低,反之。这两个指标必须一起看,只报其中一个没有意义。上线前应该在自己的样本上扫一遍阈值曲线,找到"自动通过率 × 通过样本准确率"的平衡点。

第三,它的判断标准来自训练数据分布。

Jev 的分数经过统计校准,但校准的前提是"你的文档和校准数据长得像"。如果你的场景是某个垂直行业的特殊票据(特殊的表格线、特殊的字段布局),校准可能失效。上线前必须用自己的样本跑一遍 AUROC,不能直接套别人的 93.6。


把五站串起来:流水线骨架

前面的机制讲完了,落成代码其实很短。下面是一个把五站串起来的最小骨架(伪代码级,参数名按官方文档核对后再用):

# ---------- 第 0 站:版面切块 ----------
def stage0_layout(pdf_or_image):
    """PDF 逐页栅格化 → 版面检测 → 切 crop + 读阅读顺序"""
    pages = sdk.load(pdf_or_image)              # PDF 自动栅格化
    regions = []
    for page in pages:
        detected = layout_model.detect(page)    # PP-DocLayoutV3
        for r in detected.regions:              # 已按阅读顺序排序
            regions.append({
                "crop": r.crop,                 # 图块
                "type": r.region_type,          # text / table / formula / seal / chart
                "order": r.reading_order,
            })
    return regions


# ---------- 第 1 站:识别 ----------
REGION_PROMPT = {
    "text":    "Text Recognition:",
    "table":   "Table Recognition:",
    "formula": "Formula Recognition:",
    "seal":    "Seal Recognition:",
    "chart":   "Chart to Table:",
}

def stage1_recognize(region, strategy=0):
    """区域类型由 prompt 决定,不是自动判断;strategy 决定预处理/解析路径"""
    prompt = REGION_PROMPT[region["type"]]
    return ocr_model.generate(region["crop"], prompt=prompt,
                              strategy=strategy)   # 输出 Markdown/LaTeX/HTML


# ---------- 第 2~4 站:判分 + 重试决策 ----------
def stage234_with_retry(region, max_attempts=3):
    for attempt in range(max_attempts):
        candidate = stage1_recognize(region, strategy=attempt)  # 每轮换策略

        verdict = jev.score(
            image=region["crop"],
            candidate_text=candidate,      # ② 候选文本
            # ③ 由候选文本重新渲染出的图由服务内部或 SDK 生成
        )

        # 关键:先看 verdict,再看 score
        if verdict.status != "scored":
            raise RuntimeError(f"Scoring did not run: {verdict.status}")

        if verdict.score >= ACCEPT_THRESHOLD:
            return {"text": candidate, "score": verdict.score, "attempts": attempt + 1}

    # 三次都不过线 → 标记出来,交人复核,不静默丢弃
    return {"text": candidate, "score": verdict.score,
            "attempts": max_attempts, "needs_human": True}


# ---------- 第 5 站:入库 ----------
def stage5_assemble(regions):
    results = [stage234_with_retry(r) for r in regions]
    human_queue = [r for r in results if r.get("needs_human")]

    markdown = "\n\n".join(
        r["text"] for r in results if not r.get("needs_human")
    )
    return markdown, human_queue

这段骨架里有四个点,每一个都是踩过坑才写得出来的:

位置 为什么这么写
REGION_PROMPT 显式映射 区域类型靠 prompt 指定,不指定就按正文处理,印章/公式会被当普通文字糟蹋
strategy=attempt 重试不是"再跑一遍同样的",而是换策略。原图放大、加留白、换解析路径,都是不同的 strategy
verdict.status != "scored" 先判断 这是那个陷阱。先看 verdict 再看 score,顺序错了会把服务故障当文档质量问题
needs_human 而不是丢弃 三次不过线不等于"这份文档不能用",是"这块要人看一眼"。静默丢弃是最坏的选项

建议加的监控指标(这几个不出现在任何文档里,但决定上线后能不能睡好觉):

指标 看什么 异常意味着
一次通过率 第 1 轮就过阈值的比例 掉下来说明文档形态变了,或模型/预处理被改动过
平均重试次数 1.0 是理想,>1.5 要查 识别侧的某种退化,可能是输入质量或 prompt 配置
verdict != scored 占比 应接近于 0 这是服务健康度指标,不是质量指标,>1% 要立刻查
人工队列占比 业务能承受的上限 决定阈值该怎么调,也决定 ROI 算不算得过来

最后一条最容易被忽略:人工队列占比是这条流水线唯一真正的业务指标。 前面所有分数、AUROC、吞吐,最终都是为了把这个比例压到一个业务能接受的水平。


结论:这次发布值得抄的不是分数,是那个拆分

第一,它把一个做不了的承诺,换成了一个能做的承诺。

"我们的 OCR 准确率 99%"这个承诺,从技术上就无法兑现——因为生成模型有语言先验,它自己都不知道那 1% 在哪。

而"我们能标出哪里可能认错"这个承诺是可以兑现的:它不要求识别模型变完美,只要求有一个独立的判别器能稳定地给出可信度。

从"永远不出错"到"错了我知道",这是设计目标的降级,但也是工程可行性的升级。 在真实业务里,后者比前者有用得多。

第二,读判分离的代价很小,收益很大。

代价:多一个 2B 模型的推理(毫秒级、一次前向、可跨机部署)。
收益:一个有统计校准的可信度分数,让流水线能自动决定"放行还是重打"。

用参数规模看这个交易有多划算:2B 的判别器拿到 93.6 的 AUROC,397B 的通用模型是 93.8。 为了这 0.2 分用 198 倍的参数,在任何成本模型里都不成立。

第三,真正的行业信号是"参数缩水曲线"。

过去一年,文档模型参数一路往下走:3B → 1.2B → 0.9B → 0.6B。"认字"正在从技术稀缺品变成算力日用品。

当人人都能认字的时候,稀缺的东西就变了。不是"谁认得准",而是"谁的结果敢直接进数据库"。

这也解释了为什么这次发布把「可信 OCR」单独立成一个品类——它不是营销概念,它是在回答一个参数缩水后必然出现的新问题: 当模型便宜到可以随手部署,你的瓶颈就不再是识别能力,而是你敢不敢让它的输出无人值守地进生产库。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。