TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

韓国語ドキュメントOCR比較 2:インストールでつまずいた5つのポイントとモデル出力をeval()していたコード

状態:実測記録(2026-10-28から10-30)。 症状とコマンドは測定ログおよび実行スクリプトから取得しました。ログに残っておらず正確なエラー文面を記載できないものについてはその旨を記載しています。3つのPRは2026-10-30時点でいずれもオープン(マージ前)であり、結果は断定的なものではありません。

目次

第1篇では結果のみを示しました。その裏では、インストールでつまずくことが少なからずありました。同じ道を歩む方が迷わないよう、症状と原因、修正方法を記載します。環境は24GBのGPUを1基搭載したLinuxサーバーであり、測定はGPUを使用する使い捨てコンテナ内で行いました。

1. 概要

つまずいた箇所 症状 修正方法
モデルの取得 Hugging Faceのダウンロードが2.88GBで停止 HF_HUB_DISABLE_XET=1 でXetを無効化して取得
2つのフレームワーク PyTorchイメージにPaddleを導入するとtorchが破損(NCCLの競合) Paddleは別個の仮想環境に、Unlimited-OCRの依存関係は別フォルダに
OpenCVとlibGL ImportError: libGL.so.1 システムに libgl1·libglib2.0-0 をインストール
headless OpenCV PaddleXの依存関係チェックにひっかかり、入れ替えるうちに cv2 が破損 4つの版をすべて削除し、1つの版のみ --force-reinstall --no-deps
長時間の測定 リモート接続が切断され、測定がコンテナごと消失 バッチを分割して実行し、ページごとに結果をログに1行ずつ出力
採点 座標マーカーのため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ダウンロードに戻ります。そのように変更したところ、2つの重みファイルのダウンロードが1分50秒で完了し、モデルのロードまでに114.7秒かかりました。なぜ停止したのか(ネットワーク経路、 hf_xet のバージョン、サーバー側の事情)については別途再現を行っていません(未確認)。私たちは測定用コンテナを起動するスクリプトにこの変数をデフォルトで設定しました。

1つ付け加えると、PaddleOCRのモデルもHugging Faceから取得しました。PaddleXの PADDLE_PDX_MODEL_SOURCE=huggingface でソースを指定し、キャッシュフォルダ(PADDLE_PDX_CACHE_HOME)を削除されないボリュームに配置しておけば、コンテナを新規に起動するたびに再取得する必要がなくなります。

3. 1つのイメージに2つのフレームワークを同居させると

当初はPyTorchが入ったコンテナイメージ1つにPaddlePaddleまで導入し、3つのエンジンを1箇所で動かそうとしました。そこで2つの問題に直面しました。

Paddleのホイール(wheel)が要求するNCCLのバージョンがPyPIに存在しませんでした。 paddlepaddle-gpu==3.4.0 はNVIDIAの集合通信ライブラリNCCLを nvidia-nccl-cu12==2.28.3 として要求しますが、デフォルトのインデックスには2.27.3までしか存在しないためインストールに失敗しました。Paddleが提供するCUDA 12.9用のインデックスを最初に参照し、その後にPyPIを追加する形式で取得すれば解決します。

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

同一の環境に導入するとtorchが破損しました。 PyTorchとPaddleは、それぞれに合わせてビルドされた nvidia-* CUDAライブラリ群を持ち込みます。Paddleを同じ site-packages に導入したところ、双方のNCCLが競合し、torchのロード段階で破損しました(どちらのバージョンが優勢だったのか、また正確なエラー文面はログに残りませんでした)。1つのプロセスで2つのフレームワークを同時に使用することはないため、 環境を分離する ことが正解でした。

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-2側についてもイメージ内のパッケージには手を加えませんでした。モデルカードがテストした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)に変更することです。ここでさらに2つの壁にぶつかりました。

  1. PaddleXの依存関係チェックがディストリビューション名を検証します。 PaddleXは必要なパッケージがインストールされているかを import が存在するかではなく、インストールされているディストリビューションの名前で確認します。headless版は同一の cv2 を提供しますが名前が異なるため、「必要なパッケージがない」と判断して停止しました。
  2. 両者を入れ替えるうちに cv2 フォルダが破損しました。 OpenCVの複数の版(opencv-python, -headless, -contrib, -contrib-headless)はすべて同一の site-packages/cv2/ フォルダにファイルを書き込みます。一方を削除すると他方の版が書き込んだファイルまで一緒に削除され、その上から新しい版をインストールすると中途半端に残ったフォルダとなります。pipはこの挙動を防いでいません。

最終的に落ち着いた手順は以下の通りです。元の版をそのままにし、不足しているシステムライブラリを追加します。

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__)"

4つの版を すべて 削除してから1つの版のみを --force-reinstall --no-deps で再インストールして初めてフォルダがクリーンになります。バージョン番号はPaddleOCRが最初に選択した値をそのまま使用します。コンテナを新しく起動するたびに apt-get が実行されるのを避けたければ、 libgl1 が含まれたイメージを別途作成しておく方が賢明です。

5. 長時間の測定が2回途切れた

PaddleOCRの2つのエンジンを同時に動かす測定は、リモート実行の接続が途中で切断され、測定用のコンテナごと削除されました。ログの末尾にはTLSエラーと「条件待ちがタイムアウトしました」のみが残りました。Unlimited-OCR-2の40ページ分のバッチもセッションが再起動して出力中に中断されました(そのため第1篇には10ページと20ページ分しかありません)。

両方のケースで復旧が可能だったのは、測定スクリプトが 1ページ完了するたびに 結果を出力していたためです。

その後は、バッチ(通常の本文、かすれたコピー、表、長文ドキュメント)を引数として受け取って 分割して実行し、バッチごとにログを個別に残しました。一度に何時間もかかる実行を行わないことが、接続が切れやすい環境においては最も安価な保険となります。さらに、毎回の実行の最初のページはウォームアップとして破棄しました。Unlimited-OCR-2の最初のページは43.9秒とかかり、それに続くページ(30秒前後)よりも長かったためです。

6. 採点の落とし穴の1つ

第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系のサンプルコードから引き継がれてきたものであるため、同様の行が複数のリポジトリに存在します。同じ修正を3つの箇所にPRとして提出しました。各PRの本文には、元の関数と修正した関数を同じ入力に対して実行し、正常な座標の結果が一致すること、および式が含まれる入力は実行されずに従来のエラーパスに分岐することを確認した内容を記載しました。AIの支援を受けて準備した旨の注記も追加しています。

リポジトリ PR 状態(2026-10-30)
deepseek-ai/DeepSeek-OCR-2(vLLMサンプルスクリプト) #61 オープン
openvinotoolkit/openvino_notebooks(deepseek-ocr、deepseek-vl2ノートブック) #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つのバージョンすべてを削除してやり直す。
  4. 測定スクリプトはページごとに出力ファイルとJSONの1行を残し、バッチに分けて実行し、最初のページはウォームアップとして破棄する。
  5. 採点前に出力の原文を読み、正規化された文字を正解と並べて確認する。
  6. trust_remote_code モデルはrevisionを固定し、そのバージョンのコードから eval·exec·pickle·subprocess を真っ先に探す。

Titiの一言。 OCRモデルが読み取った文字は、ドキュメントが指示した通りに出力されます。そのため、モデルの出力も顧客から渡されたメモのように扱うべきです。メモに書かれた内容をそのまま実行する人はいませんよね?

参考

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます