Prhub

#25364 Add Accuracy Benchmark for OCR models

原始 PR 作者 ckvermaAI 合并时间 2026-07-06 16:18 文件变更 9 提交数 2 评论 19 代码增减 +1915 / -0

执行摘要

为 DeepSeek-OCR-2 新增 olmOCR-bench 准确度基准测试与 HTML 报告

提供一个可复现的、标准化的 OCR 模型准确度评估工具,使用 DeepSeek-OCR-2 官方引用的 olmOCR-bench 数据集。在开发该基准测试过程中,发现并修复了 bench_serving 中未能正确加载 DeepSeek-OCR-2 处理器(DeepseekOCRProcessor)的 bug,该 bug 导致服务器在 token 计数时因传入 None 而崩溃。

建议合并。该 PR 提供了有价值的基准测试框架和必要的 bug 修复,设计上考虑了可扩展性(from_cli_args 模式、异步并发、灵活报告生成)。review 中的改进已全部落实。

讨论亮点

依赖位置争议:reviewer mickqian 建议将 pymupdf 移出核心依赖,作者将其移至 test 组。

命令和路径调整:polisettyvarma 要求移除 HF_HOME 硬编码,并使用 hf download 而非 huggingface-cli;yanbing-j 建议使用 from_cli_args 模式减少维护开销。

文档增强:yanbing-j 建议在 README 中增加 generate_report.py 用法说明。

代码重复:yanbing-j 指出 generate_report.py 中存在重复代码。

实现拆解

  1. 新增基准测试框架:添加 benchmark/ocr/bench_sglang.py,定义 BenchArgs 数据类,支持 CLI 参数,使用 aiohttp 异步并发向 sglang 服务器发送请求,渲染 PDF 页面为 base64 PNG 后发送给模型,并收集原始输出。
  2. 实现评估逻辑:添加 benchmark/ocr/eval_utils.py,实现 olmOCR-bench 的 5 种测试类型:text_presence(精确和模糊匹配)、text_absence、natural_reading_order(词级别顺序匹配)、table_accuracy(支持 Markdown 和 HTML 表格)、math_formula_accuracy(基于 LaTeX 关键令牌重叠的简化评估)。还提供 normalized_edit_distance 用于额外文本质量测量。
  3. 生成 HTML 报告:添加 benchmark/ocr/generate_report.py,从原始输出构建自包含的 HTML 报告,支持 MathJax 渲染 LaTeX 公式,可交互展开/折叠样本。
  4. 修复 bench_serving 处理器加载:修改 benchmark/utils.py 中的 get_processor,调用 hf_transformers_utils.get_processor 以使用模型特定的处理器检测,而非直接 AutoProcessor.from_pretrained。此变更对所有模型安全,仅对 DeepSeek-OCR 系列生效,其他模型回退到原行为。
  5. 配置更新:在 pyproject.toml 及平台变体(pyproject_cpu.toml、pyproject_npu.toml、pyproject_other.toml、pyproject_xpu.toml)的 test 依赖组中添加 pymupdf,用于 PDF 渲染。
  6. 文档编写:添加 benchmark/ocr/README.md,包含完整设置、使用示例、参数说明和参考分数。
文件 模块 状态 重要度
benchmark/ocr/bench_sglang.py OCR 基准 added 9.08
benchmark/ocr/eval_utils.py 评估逻辑 added 8.89
benchmark/ocr/generate_report.py 报告生成 added 8.98
benchmark/ocr/README.md 文档 added 4.11
python/pyproject.toml 项目配置 modified 2.36

关键符号

BenchArgs.add_cli_args BenchArgs.from_cli_args run_ocr_request normalize_text fuzzy_contains eval_text_presence eval_text_absence eval_reading_order generate_report _ocr_to_html

关键源码片段

benchmark/ocr/bench_sglang.py core-logic

新增 OCR 基准测试入口,定义 BenchArgs 和异步请求核心逻辑。

# benchmark/ocr/bench_sglang.py@dataclass
class BenchArgs:
    # 配置参数:服务器地址、模型名称、并发数、输出目录等
    port: int = 30000
    host: str = "127.0.0.1"
    model: str = "deepseek-ai/DeepSeek-OCR-2"
    split: str = "all"
    concurrency: int = 8
    output_dir: str = "./ocr_bench_results"
    max_samples: int = -1
    prompt_mode: str = "markdown"
    bench_dir: str = "./olmOCR-bench/bench_data"
    request_timeout: int = 300
    save_raw_outputs: bool = False
    render_dpi: int = 150
    debug: bool = False
    debug_accuracy: bool = False
​
    @staticmethod
    def add_cli_args(parser: argparse.ArgumentParser) -> None:
        # 将所有字段注册为 CLI 参数
        parser.add_argument("--port", type=int, default=BenchArgs.port)
        parser.add_argument("--host", type=str, default=BenchArgs.host)
        parser.add_argument("--model", type=str, default=BenchArgs.model)
        parser.add_argument("--split", type=str, default=BenchArgs.split,
                            choices=OLMOCR_BENCH_SPLITS + ["all"])
        parser.add_argument("--concurrency", type=int, default=BenchArgs.concurrency)
        parser.add_argument("--output-dir", type=str, default=BenchArgs.output_dir)
        parser.add_argument("--max-samples", type=int, default=BenchArgs.max_samples)
        parser.add_argument("--prompt-mode", default="markdown", choices=["markdown", "free_ocr"])
        parser.add_argument("--bench-dir", type=str, default=BenchArgs.bench_dir)
        parser.add_argument("--request-timeout", type=int, default=BenchArgs.request_timeout)
        parser.add_argument("--save-raw-outputs", action="store_true")
        parser.add_argument("--render-dpi", type=int, default=BenchArgs.render_dpi)
        parser.add_argument("--debug", action="store_true")
        parser.add_argument("--debug-accuracy", action="store_true")
​
    @classmethod
    def from_cli_args(cls) -> "BenchArgs":
        # 从 CLI 解析并返回实例(简化维护,新增参数只需在类中声明并注册 CLI)
        parser = argparse.ArgumentParser(description="OCR Benchmark (olmOCR-bench)")
        cls.add_cli_args(parser)
        args = parser.parse_args()
        return cls(**{k: v for k, v in vars(args).items() if k in cls.__dataclass_fields__})async def run_ocr_request(
    session: aiohttp.ClientSession,
    base_url: str,
    base64_image: str,
    prompt: str,
    timeout: int,
) -> str:
    # 向 sglang 服务器发送单次 OCR 请求(使用 chat/completions 接口)
    payload = {
        "model": "default",
        "messages": [
            {
                "role": "user",
                "content": [
                    {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}},
                    {"type": "text", "text": prompt},
                ],
            }
        ],
    }
    async with session.post(
        f"{base_url}/v1/chat/completions",
        json=payload,
        timeout=aiohttp.ClientTimeout(total=timeout),
    ) as resp:
        resp.raise_for_status()
        data = await resp.json()
        return data["choices"][0]["message"]["content"]
benchmark/ocr/eval_utils.py core-logic

实现 olmOCR-bench 所有测试类型的评估逻辑,包括文本存在性、阅读顺序、表格和数学公式准确度。

# benchmark/ocr/eval_utils.pyimport re
import unicodedata
from difflib import SequenceMatcher# 正则:统一不同 Unicode 字形的连字符、双引号、单引号
_HYPHEN_RE = re.compile(r"[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]")
_DQUOTE_RE = re.compile(r"[\u00AB\u00BB\u201C-\u201F\u2033\u2036\u276E\u276F\u3003\uFF02]")
_SQUOTE_RE = re.compile(r"[\u2018-\u201B\u2032\u2035\u2039\u203A\u2C8D\uFF07]")
_MARKDOWN_RE = re.compile(r"(\*{1,3}|_{1,3}|`{1,3}|~~|#{1,6}\s?)")def normalize_text(text: str) -> str:
    # 应用 NFC 归一化并统一常见标点符号
    text = unicodedata.normalize("NFC", text)
    text = _HYPHEN_RE.sub("-", text)
    text = _DQUOTE_RE.sub('"', text)
    text = _SQUOTE_RE.sub("'", text)
    return textdef strip_markdown(text: str) -> str:
    # 移除 Markdown 标记以便进行软匹配
    return _MARKDOWN_RE.sub("", text)def fuzzy_contains(needle: str, haystack: str, threshold: float = 0.85) -> bool:
    # 先尝试精确不区分大小写匹配,若失败则使用滑动窗口 + SequenceMatcher
    needle = normalize_text(strip_markdown(needle).strip())
    haystack = normalize_text(strip_markdown(haystack))
​
    if needle.lower() in haystack.lower():
        return True
​
    n = len(needle)
    if n == 0:
        return True
​
    step = max(1, n // 4)
    # 步长为 1/4 长度的滑动窗口,平衡速度与精度
    for i in range(0, max(1, len(haystack) - n + 1), step):
        window = haystack[i : i + n]
        ratio = SequenceMatcher(None, needle.lower(), window.lower()).ratio()
        if ratio >= threshold:
            return True
    return Falsedef eval_text_presence(test: dict, ocr_output: str) -> bool:
    # 评估目标文本是否出现在 OCR 输出中,支持精确 / 模糊匹配及切片
    needle = test.get("text", "")
    max_diffs = test.get("max_diffs", 0)
    case_sensitive = test.get("case_sensitive", True)
    first_n = test.get("first_n")
    last_n = test.get("last_n")
​
    # 限制 OCR 输出的前 / 后 N 个词(若测试要求)
    haystack = _get_words_slice(ocr_output, first_n, last_n)
​
    if max_diffs == 0:
        return exact_contains(needle, haystack, case_sensitive=case_sensitive)
    # 模糊:从 max_diffs 计算相似度阈值
    n = max(1, len(needle))
    threshold = max(0.6, (n - max_diffs) / n)
    return fuzzy_contains(needle, haystack, threshold=threshold)

评论区精华

pymupdf 依赖位置 设计

reviewer mickqian 建议将 pymupdf 移出核心 dependencies,因其仅为 benchmark 使用。

结论:作者将其移至 pyproject.toml 的 test 组下。 · 已解决

移除 HF_HOME 硬编码 style

reviewer polisettyvarma 要求移除 docstring 中的 HF_HOME 硬编码路径。

结论:作者已按要求移除。 · 已解决

使用 hf download 替代 huggingface-cli style

reviewer polisettyvarma 建议使用更现代的命令 hf download。

结论:作者已更新文档。 · 已解决

采用 from_cli_args 模式 设计

reviewer yanbing-j 建议参考 offline_throughput.py,使用 from_cli_args 减少参数维护开销。

结论:作者采纳,BenchArgs 实现了 from_cli_args 方法。 · 已解决

文档中增加 generate_report.py 说明 documentation

reviewer yanbing-j 建议在 README 中补充 generate_report.py 的用法。

结论:作者已补充。 · 已解决

generate_report.py 代码重复 style

reviewer yanbing-j 指出文件中有重复代码。

结论:作者已修复。 · 已解决

风险与影响

回归风险 (bench_serving):benchmark/utils.py 中 get_processor 的修改可能影响其他模型的处理器加载,但由于其回退到 AutoProcessor.from_pretrained,风险较低;需关注 _CUSTOMIZED_MM_PROCESSOR 检查的覆盖范围。

依赖风险:新增 pymupdf 仅在 test 组,对生产环境无影响。

基准测试代码安全:新增代码不进入核心服务路径,仅作为离线基准工具使用。

用户:获得 OCR 模型的标准评估工具,可量化改进。

开发者:便于在 CI 中集成 OCR 准确度回归测试,减少手动验证。

系统:新增的文件独立,不影响现有服务逻辑。

bench_serving 回归风险较低 新增依赖仅 test 组 核心路径未受影响

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论