TL;DR
| 項目 | 内容 |
|---|---|
| 入力 | 罫線・表組みを含む日本語帳票PDF |
| 出力 | 構造化データ(Excel / JSON) |
| GPU | RTX 3090 (24GB VRAM) 1枚 |
| 外部API | なし(すべてローカル) |
使用モデル
| Phase | モデル | VRAM | 役割 |
|---|---|---|---|
| Phase 0 | pdfplumber + OCRCorrector | CPU | テーブル構造抽出 + OCR誤変換補正 |
| Phase 0 (fallback) | fitz + PaddleOCR 2.7.3 | CPU | 画像PDFのテキスト抽出 |
| Phase 1 | Nemotron-Nano-9B-v2-Japanese (Q4_K_M) | ≈6GB | 軽量LLM 一次抽出 |
| Phase 2 | llm-jp-4-32b-A3B (Q4_K_M) | ≈21GB | 汎用LLM 詳細推論 |
| Phase 3 | Qwen-3-VL-Ricoh-8B (INT8) | ≈21GB | VLM 画像ベース抽出 |
PaddleOCRだけでは「建設改良費」が「建設改良荘」に、「償還金」が「憤還金」になる。本記事では、pdfplumber + OCR補正 → 軽量LLM一次抽出 → 汎用LLM詳細推論 → VLM画像抽出の多段パイプラインを、24GB VRAMの制約内で時分割実行する仕組みを解説する。すべてローカルで完結するため、機密文書を外部APIに送信する必要がない。
この記事が対象とする文書
本パイプラインは「罫線を含む日本語帳票PDF」全般を対象としている。以下はいずれもOCR単体では精度が出にくい文書の例である。
- 自治体の予算書・決算書 ── 款・項・目・節の4階層、千円単位の金額
- 企業の財務諸表・決算短信 ── 勘定科目、前期比較の複数列
- 医療機関の診療報酬明細 ── 点数・回数・金額の密な数値テーブル
- 不動産登記事項証明書 ── 枠線内に詰まった漢字・数字の混在
- 保険証券・約款 ── 細かい条件表、特約一覧
共通する問題は「OCRは文字を読めるが構造がわからない」「VLMは構造がわかるが文字を間違える」という相互補完的な弱点にある。
本記事の検証データは自治体の補正予算書(公会計PDF)を使用しているが、パイプラインの設計自体は上記いずれの文書にも適用可能である。
なぜ多段パイプラインが必要なのか
OCRだけでは精度が出ない
PaddleOCRは高い基本性能を持つが、帳票特有の問題がある。
- 罫線がテキスト領域の検出を妨害する
- 信頼度0.85前後の「微妙に間違っている」行が頻出する
- 「費」→「荘」、「償」→「憤」、「購入」→「職入」のような形態的に近い漢字への誤認識
これらは後処理のルールベース補正では対処しきれない。「建設改良荘」を「建設改良費」に直すには、「建設改良費」という語彙が存在することを知っている必要がある。
VLMだけでも精度が出ない
Qwen-3-VL-Ricoh-8Bにページ画像を直接投げて構造化する方法も試した。構造認識は優れているが、数値の転記ミスやハルシネーションが発生する。特にKVキャッシュが溜まった状態では、存在しない行が生成されることがある。
解決策:役割分担
テーブル抽出、OCR補正、LLMによるデータ抽出、VLMによる画像ベース補完をそれぞれ別のフェーズに分離し、各フェーズの弱点を次のフェーズが補う構成にしている。
アーキテクチャ
特徴的なのは、Phase 1でNemotron-Nano-9B(6GB)が先に処理し、そこで抽出できなかった項目だけをPhase 2のllm-jp-4-32b(21GB)にエスカレーションする点である。全データを大型LLMに投げるのではなく、「取りこぼし」だけを次に回す。
VRAMスケジューリング
RTX 3090の24GB VRAMを時分割で使い回す。これがこのシステムの核心部分である。
時間軸 →
Phase 0 [pdfplumber / PaddleOCR ─ CPU] ───── VRAM: 0GB
Phase 1 ......................... [Nemotron 9B] ──── unload VRAM: ≈6GB
Phase 2 ....................................... [llm-jp-4-32B] ── unload VRAM: ≈21GB
Phase 3 ......................................................... [Qwen-VL-8B] VRAM: ≈21GB
├──────────────────────────────────────────────────────────────────────┤
VRAM 0GB 24GB
Phase 2(21GB)とPhase 3(21GB)は同時にVRAMに載らない。フェーズ切り替え時には、現在のモデルをアンロードしてから次のモデルをロードする。
vLLM終了後のVRAM解放は即座には起こらない。プロセス終了後にnvidia-smiで使用量をポーリングし、解放を確認してから次のモデルをロードする。この待機処理を省略するとCUDA OOMで失敗する。
Phase 1のNemotron-Nano-9Bは6GBと軽量なため、Phase 0完了後すぐに起動できる。大半の定型項目はここで抽出が完了し、Phase 2のllm-jp-4-32b(21GB)をロードする頻度を減らせる。この「軽量モデルで先に大半を処理し、重量モデルのロード回数を最小化する」設計が、24GB制約下での実用的な処理速度を実現している。
Phase 0:テーブル抽出とOCR補正
pdfplumberによる構造抽出
pdfplumberでPDF内のテーブルを構造的に抽出する。罫線情報からセル境界を認識し、DataFrameとして取得する。
pdfplumberが有効でない場合、またはテーブルが検出されない場合(画像PDFなど)は、fitz(PyMuPDF)でテキストを取り出し、テキスト量が50文字未満ならPaddleOCR 2.7.3にフォールバックする。
OCRCorrector
Acrobat OCRやPaddleOCRが誤認識しやすいパターンをルールベースで一括補正するユーティリティである。主な補正内容は以下の通り。
- 全角数字・記号 → 半角 ── 「123」→「123」
- マイナス記号の正規化 ── 「△」「▲」はマイナス表記として保持、OCR誤読の「A」→「△」に補正
- 会計用語の誤認識修正 ── 「Go 腿」→「金額」、「補正類」→「補正額」
- 丸付き数字の保護 ── NFKC正規化で①が「1」に変換されるのを防ぐため、保護範囲(U+2460〜U+24FF等)を定義してプレースホルダに退避してから正規化を適用する
丸付き数字の保護は地味だが重要な知見である。NFKCを先に適用すると①②③が1, 2, 3に変換され、項目番号が本文の数値と区別できなくなる。正しい順序は「保護対象をプレースホルダに退避 → NFKC正規化 → プレースホルダを復元」である。
款・項・目・節の階層fill-down
予算書のテーブルでは、上位の款・項は最初の行にだけ記載され、以降の行は空セルになる。これをfill-down処理で補完し、各行が完全な階層情報を持つようにする。この処理がないと、LLMが科目名を正しくJSON化できない。
Phase 1〜2:LLMによるデータ抽出
Phase 0で取得したテキストを、款・項の境界で意味的にチャンク分割し、LLMに投げて構造化データ(JSON)を抽出する。
なぜ Nemotron 9B → llm-jp-4 32B の2段階なのか
- VRAMの節約 ── Nemotron 9B(6GB)で大半の項目を抽出できれば、llm-jp-4 32B(21GB)のロード回数を減らせる。21GBモデルのロードには数十秒かかるため、この差は大きい
- タスク特性の違い ── 定型的な項目(金額、年度、科目名)は9Bモデルで十分な精度が出る。文脈依存の推論が必要な項目だけを32Bモデルに回す
- 処理時間の短縮 ── 9Bモデルは高速なので、全チャンク×全項目を素早く走査できる
Phase 1で抽出できた項目はスキップされ、Phase 2では「取りこぼし」だけが処理される。実際の運用では、Phase 1で7〜8割の項目が抽出でき、Phase 2に到達するのは残りの2〜3割である。
プロンプト設計
LLMへのプロンプトには、帳票特有の指示を含める。
1. テキストから指定されたJSONスキーマの項目を抽出してください
2. 公会計書類の表はマトリクス構造です。「横の行(科目名)」と
「縦の列のヘッダー」を交差確認して数値を抽出してください
3. 「△」「▲」、OCR誤認識の「A」はすべてマイナスの数値として出力してください
4. 「Go 腿」→「金額」のようなOCRノイズは文脈から補正して解釈してください
特に重要なのは3と4である。予算書ではマイナス表記に「△」が使われるが、OCRが「A」に誤認識する。プロンプトでこれを明示しないと、LLMはAを英字として解釈してしまう。
重複検出とマージ
複数チャンクにまたがって同じ科目が出現する場合がある。duplicate_historyで重複を追跡し、同一キーに対する上書きをカウントする。これにより、後段の監査で「同じ科目が複数回抽出された」ことを検知できる。
Phase 3:VLMによる画像抽出
Phase 1〜2のテキストベース抽出でも取りこぼした項目がある場合にのみ、PDFのページ画像をQwen-3-VL-Ricoh-8B(INT8、21GB)に直接投げて抽出する。
Qwen-3-VL-Ricoh-8Bを選んだ理由は、Ricoh社が日本語文書認識に特化してファインチューニングしたモデルであり、罫線の多い帳票の構造認識に強いためである。
テキスト抽出とVLMの抽出を分離している理由は、VLMは画像全体を処理するためテキストベースより遅く、VRAMも21GBと大きく消費するからである。VLMは「最後の手段」として使い、テキストで取れるものはテキストで取る。
Phase 2(llm-jp-4-32b、21GB)とPhase 3(Qwen-VL-8B、21GB)は同じVRAMバジェットを使う。Phase 2をアンロードし、VRAM解放を確認してからPhase 3をロードする。
開発で踏んだ地雷
同種のパイプラインを構築する際の参考として、実際に遭遇した問題を記録しておく。
PaddleOCR 2.7.3固定
PaddleOCRはv3.x以降、paddlex経由でmodelscope依存が入る。modelscope→torchのDLL読み込みがWindows+WSL環境で競合し、インポート時にクラッシュする。v2.7.3では発生しない。EasyOCRは日本語の精度がPaddleOCRより明確に劣るため、代替にはならない。
vLLM終了後のVRAM残留
vLLMプロセスをkillした後、nvidia-smi上ではVRAMが即座に解放されない。CUDAコンテキストの解放にOSレベルの遅延があるためで、プロセス終了後にnvidia-smiをポーリングしてVRAM使用量が閾値以下になったことを確認してから次のモデルをロードする必要がある。これを省略するとCUDA OOMが頻発する。
NFKC正規化の順序
全角→半角の正規化にNFKCを使うが、これを丸数字(①②③)の除去より先に適用すると、①が「1」に変換されてしまい、項目番号の誤認識が発生する。OCRCorrectorでは保護範囲(U+2460〜U+24FF、U+3251〜U+325F等)を定義し、該当文字をプレースホルダに退避してからNFKCを適用している。
gemma-3とtorch.compile
gemma-3系モデルをvLLMで使う場合、torch.compileのウォームアップに約290秒かかる。--enforce-eagerで回避できるが、gemma-3アーキテクチャでは--enforce-eager自体が非互換で使えない場合がある。結果としてgemma-3系は採用を見送った。
△とAの混同
予算書のマイナス表記「△」はOCRで「A」に誤認識される。これをLLMプロンプトで明示的に指示しないと、LLMは「A179」をマイナス179千円ではなく意味不明な文字列として扱う。OCRCorrectorのルールベース補正とLLMプロンプトの両方で対処する二重防御が必要だった。
実際のOCR補正例
自治体の水道事業会計補正予算書での検証結果を示す。同様の誤認識パターンは企業の財務帳票にも共通して現れる。
LLMによる文脈ベース補正
| OCR結果 | 補正後 | 原因 |
|---|---|---|
| 建設改良荘 | 建設改良費 | 「費」→「荘」の誤認識 |
| 企業債憤還金 | 企業債償還金 | 「償」→「憤」の誤認識 |
| 受益青貞担金 | 受益者負担金 | 複数文字の連鎖誤認識 |
| 工事請負畳 | 工事請負費 | 「費」→「畳」の誤認識 |
| 3固定斉産購 | 3固定資産購入 | 全角数字 +「資」→「斉」 |
| 65-無形間定査産 | 65-無形固定資産 | 「固定」→「間定」の誤認識 |
| 企業情世還介 | 企業債償還金 | 多数文字の連鎖誤認識 |
これらはLLMが事前学習で「建設改良費」「固定資産」「企業債償還金」といった語彙を知っているからこそ可能な補正である。ルールベースでは「荘→費」のような変換辞書を文書ジャンルごとに用意する必要がある。
OCRCorrectorによるルールベース補正
| OCR結果 | 補正後 | ルール |
|---|---|---|
| Go 腿 | 金額 | 会計用語誤認識テーブル |
| 補正類 | 補正額 | 会計用語誤認識テーブル |
| A179 | △179 |
A + 数字 → △ + 数字 |
| 123 | 123 | 全角数字 → 半角 |
| ,000 | ,000 | 全角カンマ・数字 → 半角 |
これらはパターンが決まっているため、LLMに頼らずルールベースで処理する方が速く確実である。
ルールベースで処理できるものはルールベースで、文脈が必要なものはLLMで、という役割分担がこのパイプラインの設計原則である。
この設計が使えるケース、使えないケース
向いている文書
- 罫線・表組みを含む定型帳票
- 科目名・項目名など「LLMが知っている語彙」が多い文書
- 数値の正確性が求められる財務系文書
- 機密性の高い文書(外部APIに送信できないもの)
向いていない文書
- 手書き文書(PaddleOCRの認識精度が根本的に不足する)
- 写真中心の文書(テーブル抽出が意味をなさない)
- 多言語混在文書(本パイプラインは日本語に最適化している)
- 1ページあたりの情報量が極端に少ない文書(パイプラインのオーバーヘッドが見合わない)
まとめ
日本語の帳票PDFという「OCRが最も苦手とする文書ジャンル」に対して、テーブル抽出・ルールベース補正・LLM抽出・VLM画像抽出を多段で組み合わせることで、実用的な精度を達成した。
構築のポイントは4つ。
- pdfplumber + OCRCorrector ── テーブル構造はpdfplumberで、文字補正はルールベースで。LLMに到達する前にノイズを減らす
- Nemotron 9B → llm-jp-4 32B のエスカレーション ── 全データを21GBの大型LLMに投げるのではなく、6GBの軽量LLMで7〜8割を処理し、取りこぼしだけを大型LLMに回す。VRAMと処理時間の両方を節約する
- VRAMの時分割 ── 24GBの制約内でNemotron 9B(6GB)→ llm-jp-4 32B(21GB)→ Qwen-VL-8B(21GB)を切り替え、nvidia-smiポーリングでVRAM解放を確認してから次をロードする
- ルールベースとLLMの二重防御 ── OCRCorrectorで「A→△」「Go 腿→金額」を先に直し、LLMプロンプトでも同じ指示を重ねる。どちらか片方だけでは漏れる
すべてローカルで完結するため、機密文書を外部APIに送信する必要がない。RTX 3090一枚で、帳票PDFの読み取りとデータ抽出を自動化できる。
参考リンク
- PaddleOCR ── v2.7.3を使用
- pdfplumber ── PDFテーブル構造抽出
- Qwen-3-VL-Ricoh-8B ── Ricoh社による日本語文書特化VLM
- vLLM ── LLM/VLM推論エンジン
- llama.cpp ── GGUFモデル推論
- llm-jp-4 ── 日本語特化LLM
- PyMuPDF (fitz) ── PDF解析ライブラリ
関連記事
- RAGの次は「検索しないAI」かもしれない ― KRF(Knowledge Runtime Format)の実装と計測 — 本記事のPhase 0で抽出したデータの出力先となるKnowledge Unitフォーマット
- 日本語帳票PDFの構造抽出とOCR補正をCPUだけで実装する ― Knowledge Compilerの入口 — 本記事のPhase 0部分をコードと計測データで補完