TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

韩文文档 OCR 比较 2:安装时卡住的五个地方以及用 eval() 运行模型输出的代码

状态:实测记录(2026-10-28 至 10-30)。 症状和命令取自测量日志和执行脚本。对于日志中未留下、无法准确还原的错误信息,文中均已说明。截至 2026-10-30,这三个 PR 均处于开启状态(未合并),结果不作绝对定论。

目录

第 1 篇只展示了结果。在此之后,安装过程中遇到了不少麻烦。为了让走同一条路的人少走弯路,特此记录症状、原因以及修复方法。测试环境是一台配有一张 24GB GPU 的 Linux 服务器,测量是在使用 GPU 的一次性容器内进行的。

1. 一目了然

卡住的地方 症状 修复方法
获取模型 Hugging Face 下载在 2.88GB 处停止 HF_HUB_DISABLE_XET=1 禁用 Xet 后下载
两个框架 在 PyTorch 镜像中放入 Paddle 导致 torch 崩溃(NCCL 冲突) 将 Paddle 放入独立的虚拟环境中,将 Unlimited-OCR 依赖放入单独的文件夹中
OpenCV 与 libGL ImportError: libGL.so.1 系统中 libgl1·libglib2.0-0 安装
headless OpenCV 触发 PaddleX 依赖检查,反复切换导致 cv2 损坏 卸载所有四个版本,仅重新安装一个版本 --force-reinstall --no-deps
漫长的测量 远程连接断开,整个容器随之消失 分批运行,每部分结果在日志中打印一行
评分 由于坐标标记,CER 被放大至 16% 添加坐标正则表达式(第 1 篇第 3 节)
安全 远程代码通过 eval() save_results=False 测量模型输出,向主干仓库提交 ast.literal_eval PR

2. 模型下载在 2.88GB 处停止

首次下载 Unlimited-OCR-2 的权重时,下载进度在 2.88GB 处停滞不前。既没有报错,进度条也完全卡住。Hugging Face Hub 会使用一种名为 Xet 的新存储方式(将文件切片以实现无重复地传输)来分发大文件,而 huggingface_hub 如果安装了 hf_xet ,就会走这条路径。

设置环境变量 HF_HUB_DISABLE_XET=1 后,即可回退到传统的 HTTP 下载方式。修改后,两个权重文件在 1 分 50 秒内下载完毕,加载模型耗时 114.7 秒。我们没有单独去重现为什么会卡住(网络路径、 版本、服务器端情况)(未确认)。我们在启动测量容器的脚本中默认加入了这个变量。 hf_xet 另外值得一提的是,PaddleOCR 的模型也是从 Hugging Face 下载的。通过 PaddleX 的

指定来源,并将缓存文件夹( PADDLE_PDX_MODEL_SOURCE=huggingface )放在持久化卷上,这样每次启动新容器时就不需要重新下载了。PADDLE_PDX_CACHE_HOME3. 在一个镜像中放入两个框架时

起初,我们试图将 PaddlePaddle 放入包含 PyTorch 的同一个容器镜像中,以便在一个地方运行所有三个引擎。这里遇到了两个问题。

PyPI 上没有 Paddle wheel 所需的 NCCL 版本。

要求 NVIDIA 的集合通信库 NCCL 版本为 paddlepaddle-gpu==3.4.0 ,但默认索引中最高只有 2.27.3,导致安装失败。解决方法是优先读取 Paddle 维护的针对 CUDA 12.9 的索引,然后再附加 PyPI 进行安装。 nvidia-nccl-cu12==2.28.3 放入相同环境会导致 torch 崩溃。

pip install paddlepaddle-gpu==3.4.0 \
  -i https://www.paddlepaddle.org.cn/packages/stable/cu129/ \
  --extra-index-url https://pypi.org/simple

PyTorch 和 Paddle 各自会拉取针对自身构建的 CUDA 库依赖包。将 Paddle 放入相同的 nvidia-* 中会导致两边的 NCCL 冲突,从而在加载 torch 阶段引发崩溃(日志中未记录究竟哪个版本获胜以及具体的错误信息)。由于不会在同一个进程中同时使用这两个框架,因此 site-packages 拆分环境 才是正解。 通过

python -m venv --without-pip /models/ocr/venv-paddle
P=/models/ocr/venv-paddle/bin/python
pip --python $P install paddlepaddle-gpu==3.4.0 -i …/cu129/ --extra-index-url https://pypi.org/simple
pip --python $P install "paddleocr[doc-parser]==3.7.0"

--without-pip 创建一个空的虚拟环境,并使用外部 pip 的 --python 在其中进行安装,可以节省为每个虚拟环境重新安装 pip 的时间。Unlimited-OCR 部分也没有去修改镜像中的软件包。模型卡测试所需的 Transformers 4.52.1 及其对应版本的 tokenizers·huggingface-hub 等被放入 pip install --target 的独立文件夹中,并仅通过 PYTHONPATH 调用。这种方式保持了镜像自带的 torch 不变,只加载模型代码所需的版本。

4. OpenCV、libGL 以及损坏的 cv2 文件夹

构建 Paddle 环境并 import cv2 后便卡住了。巡查发现:

ImportError: libGL.so.1: cannot open shared object file: No such file or directory

PaddleOCR 所引入的 OpenCV(opencv-contrib-python)附带了屏幕输出功能,因此会检查系统的 libGL 。而服务器容器镜像中并不包含该库。

常见的解决方法是将其替换为剔除屏幕功能的 headless 版(opencv-contrib-python-headless)。在此过程中,我们又遇到了两次阻碍。

  1. PaddleX 的依赖检查会检查发行版名称。 PaddleX 并不是通过检查所需软件包是否已安装、而是通过已安装的发行版名称来判断是否 import 。虽然 headless 版提供相同的功能,但由于名称不同,它被判定为“缺少所需软件包”而中止。 cv2 在不同版本间反复切换时,
  2. 文件夹损坏了。 cv2 OpenCV 的四个版本( )都会向同一个opencv-python, -headless, -contrib, -contrib-headless文件夹中写入文件。卸载一个版本会连同其他版本写入的文件一并删掉,而在此基础上安装新版本则会留下一个残缺的文件夹。pip 无法阻止这种情况。 site-packages/cv2/ 最终确定的步骤如下:保留原版本不动,补齐缺失的系统库。

必须将这四种版本

V=$(pip --python $P show opencv-contrib-python | sed -n 's/^Version: //p')
pip --python $P uninstall -y opencv-contrib-python opencv-contrib-python-headless \
  opencv-python opencv-python-headless
pip --python $P install --force-reinstall --no-deps opencv-contrib-python==$V
apt-get install -y libgl1 libglib2.0-0
$P -c "import cv2; print(cv2.__version__)"

全部 卸载,然后 仅重新安装一种版本 --force-reinstall --no-deps ,才能保持文件夹干净。版本号沿用 PaddleOCR 最初选择的值。如果每次启动新容器时运行 apt-get 的开销太大,最好单独制作一个包含 libgl1 的镜像。

5. 长时间测试两次中断

在同时运行 PaddleOCR 两个引擎的测试中,远程执行连接中途断开,测试容器随之被删除。日志末尾仅留下 TLS 错误和“等待条件超时”。Unlimited-OCR-2 的 40 页组合包也在会话重启过程中于输出时中断(因此第 1 篇中只有第 10 页和第 20 页)。

两次能够恢复,完全是因为测试脚本在 每完成一页时 都会保存结果。

此后,我们改为以文本包(普通正文、模糊复印件、表格、长文档)为参数, 分批运行,并为每个文本包单独记录日志。在容易断开连接的环境中,避免单次运行持续数小时是最划算的保险。此外,每次运行的第一页作为预热被丢弃了。Unlimited-OCR-2 的第一页耗时 43.9 秒,比后续页面(约 30 秒左右)更长。

6. 评分陷阱

正如第 1 篇第 3 节所述,如果不删除 Unlimited-OCR-2 输出中的区块坐标 text[71, 96, 900, 160] ,普通正文的 CER 将显示为 16.3%,而删除后则为 0.80%。如果引擎输出格式发生变化,评分器会默默给出错误的数值。在接入新引擎时,我们必须先用肉眼阅读几页输出原文,将规范化后的字符与标准答案并排比对后,方可相信得分。

7. 安全备忘:曾对模型输出执行 eval() 的代码

Unlimited-OCR-2 使用 trust_remote_code=True 从代码仓库拉取并运行模型代码。在阅读这些代码时,我们在结果保存路径中发现了这样一行(modeling_unlimitedocr.py,修订版本 5632329a).

cor_list = eval(ref_text[2])
모델 출력이 파이썬 코드가 되는 자리 원격 코드에서 save_results=True 일 때만 타는 길 ① 문서 이미지 누구나 만들 수 있는 입력 ② 출력에 <|det|> 표식이 있으면 예: <|det|>text[71, 96, 900, 160]<|/det|> ③ 정규식이 [ … ] 를 잘라 괄호 안이 ] 만 아니면 무엇이든 통과 ④ eval("[ … ]") 숫자가 아니라 식이면 그 식을 실행 우리가 쓴 길 save_results=False 출력 글자만 받아 채점 업스트림에 낸 수정 ast.literal_eval 숫자·목록만 읽고 식은 거절
图:模型输出变为 Python 代码的位置

infer(..., save_results=True) 调用后,远程代码会通过正则表达式在模型输出中查找 <|det|>이름[…]<|/det|> 格式的标记,并为了将方括号内的字符读取为坐标而传递给 eval() 。只要方括号内不包含 ] ,正则表达式就会放行任何内容。而模型输出是 随输入文档而变的字符。文档人人可造,因此从原理上讲,文档中的字符存在通过模型输出并以 Python 表达式方式执行的途径。

我们将已确认的事实分类整理如下。

我们在测试中使用了 save_results=False 并仅获取输出字符。如果需要绘制图像或裁剪保存方框,坐标可以通过 ast.literal_eval 读取。由于该方法仅接收数字、列表、字符串等字面量并通过 ValueError 拒绝表达式,因此对于正常的坐标,结果是相同的。

由于该函数延续自 DeepSeek-OCR 系列的示例代码,相同的代码行存在于多个代码仓库中。我们已向三处提交了包含相同修改的 PR。我们在 PR 正文中说明了以下验证内容:使用相同输入运行原函数和修改后的函数,确保正常坐标的结果一致,且包含表达式的输入不会被执行而是进入原有的错误路径。同时我们还附上了声明,表明该修改是在 AI 辅助下准备的。

代码仓库 PR 状态(2026-10-30)
deepseek-ai/DeepSeek-OCR-2 (vLLM 示例脚本) #61 开启
openvinotoolkit/openvino_notebooks (deepseek-ocr, deepseek-vl2 notebook) #3820 开启
basetenlabs/truss-examples (deepseek-ocr 示例) #594 打开

trust_remote_code=True 我们在使用模型时遵循的原则也一同记录在此。固定 revision(提交哈希)以确保获取的代码不发生变化,通读一遍固定版本的代码,并关闭未使用的存储与可视化路径。启动用于评测的 GPU 容器时不带集群 API 令牌(关闭自动挂载服务账户令牌)。

8. 如果重新来过,一开始就该做的事

  1. 在启动评测容器的脚本中加入 HF_HUB_DISABLE_XET=1 和模型缓存卷作为默认项。
  2. 按框架划分环境。在 PyTorch 镜像之上为 Paddle 单独配置虚拟环境,远程代码模型的依赖放在 --target 文件夹。
  3. libgl1·libglib2.0-0 使用固定的镜像。OpenCV 版本只用 PaddleOCR 选定的一款,若需更改则将四个版本全部清除后重来。
  4. 评测脚本在每一页输出一个文件和一行 JSON,分批运行,第一页作为预热丢弃。
  5. 评分前先阅读输出原文,将归一化后的文字与标准答案并排对照。
  6. trust_remote_code 对于模型,固定 revision,并在该版本的代码中首先查找 eval·exec·pickle·subprocess 。

TT 的一句话。 OCR 模型读取的文字完全按文档呈现。因此,模型的输出也应像顾客递过来的便条一样对待。没人会直接执行便条上写的内容吧?

参考

登录后即可点赞

评论

还没有评论。

登录后即可发表评论