1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OCRだけでは読めない日本語帳票をローカルLLMで補正する ─ 24GB VRAMで動く多段パイプライン

1
Last updated at Posted at 2026-06-24

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段階なのか

  1. VRAMの節約 ── Nemotron 9B(6GB)で大半の項目を抽出できれば、llm-jp-4 32B(21GB)のロード回数を減らせる。21GBモデルのロードには数十秒かかるため、この差は大きい
  2. タスク特性の違い ── 定型的な項目(金額、年度、科目名)は9Bモデルで十分な精度が出る。文脈依存の推論が必要な項目だけを32Bモデルに回す
  3. 処理時間の短縮 ── 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つ。

  1. pdfplumber + OCRCorrector ── テーブル構造はpdfplumberで、文字補正はルールベースで。LLMに到達する前にノイズを減らす
  2. Nemotron 9B → llm-jp-4 32B のエスカレーション ── 全データを21GBの大型LLMに投げるのではなく、6GBの軽量LLMで7〜8割を処理し、取りこぼしだけを大型LLMに回す。VRAMと処理時間の両方を節約する
  3. VRAMの時分割 ── 24GBの制約内でNemotron 9B(6GB)→ llm-jp-4 32B(21GB)→ Qwen-VL-8B(21GB)を切り替え、nvidia-smiポーリングでVRAM解放を確認してから次をロードする
  4. ルールベースとLLMの二重防御 ── OCRCorrectorで「A→△」「Go 腿→金額」を先に直し、LLMプロンプトでも同じ指示を重ねる。どちらか片方だけでは漏れる

すべてローカルで完結するため、機密文書を外部APIに送信する必要がない。RTX 3090一枚で、帳票PDFの読み取りとデータ抽出を自動化できる。


参考リンク

関連記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?