powered by TechFeed
表示モード
Deep Dive

OCRはテキストを返すが、ドキュメントを返さない — スキャンPDFをRAGに使うと何が壊れるか

6月19日、Towards Data Scienceが「Parse Scanned PDFs for RAG with EasyOCR: Free OCR Gives You Words, Not a Document」と題した記事を公開した。スキャンPDFをEasyOCRでテキスト抽出する際に生じる「構造情報の欠落」という問題と、RAGパイプラインへの影響について詳しく論じている。「OCRをかければ解決する」という認識が、なぜRAGパイプラインを静かに壊すのかが本記事の核心だ。

6月19日、Towards Data Scienceが「Parse Scanned PDFs for RAG with EasyOCR: Free OCR Gives You Words, Not a Document」と題した記事を公開した。スキャンPDFをEasyOCRでテキスト抽出する際に生じる「構造情報の欠落」という問題と、RAGパイプラインへの影響について詳しく論じている。「OCRをかければ解決する」という認識が、なぜRAGパイプラインを静かに壊すのかが本記事の核心だ。


OCRは「テキスト」を返す。「ドキュメント」を返すのではない

RAG(Retrieval-Augmented Generation)パイプラインにスキャンPDFを取り込む際、多くのエンジニアが「OCRをかければ解決する」と考えがちだ。だがこの記事が突きつけるのは、その認識の甘さである。

OCRはテキストを復元する。しかしRAGパイプラインが必要とするのは、テキストの「周辺にある構造」だ。 ページ境界、セクション見出し、表のセル構造、図のキャプション、読み順——これらはすべて、OCRエンジンが返さない情報である。

記事が取り上げるのはEasyOCR(JaidedAI製、Apache 2.0ライセンス)。テキスト検出+テキスト認識を行うライブラリで、モデルウェイトは初回ダウンロード時に約150MB、CPU動作対応、完全無料という手軽さが特徴だ。APIもシンプルで、Readerを構築してreadtextに画像を渡すだけである。

import easyocr
import fitz
import numpy as np

reader = easyocr.Reader(["en"], gpu=False)  # 初回は ~150MB ダウンロード

page = fitz.open("data/contracts/scanned_amendment.pdf")[0]
pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))  # 2倍ズーム ≒ 144 DPI
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(
    pix.height, pix.width, pix.n,
)

detections = reader.readtext(img)
for quad, text, conf in detections:
    print(round(conf, 2), text)

出力は(bbox, text, confidence)のフラットなリストだ。これがEasyOCR(そしてTesseract、PaddleOCRも同様)の全出力である。


RAGが壊れる5つの構造欠落

記事はEasyOCRが返せない構造情報を5項目に整理している。

  • TOC/セクション境界toc_dfが空になるため、「Section 3.2に答えがある」という質問のルーティングができない
  • 図の個別認識:スキャンページ全体を1枚の画像として扱うため、ページ内に埋め込まれた図が個別のオブジェクトとして取得できない
  • 多段組ページの読み順:2カラムのページでは、左右の行がy座標順に混在して返ってくる。生成モデルはジグザグに並んだテキストを読むことになる
  • 表のセル構造:表は切り離されたテキストボックスの羅列として返り、「この値はこのラベルに属する」という関係が失われる
  • フォント・太さ・サイズ情報:見出しや強調を示すタイポグラフィ情報が捨てられ、見出し分類ができない

特に3番目の読み順の崩壊は静かに深刻だ。2カラムページでは「左列1行目→右列1行目→左列2行目→右列2行目…」という順で返ってくる。RAGが引用する文章は意味不明なジグザグ文になる。


レイアウトモデルなしではボックスがy座標順に返るため、2カラムページはジグザグに混在する(画像:原著者)


EasyOCR vs Docling:同じOCRエンジン、違う出力

記事の核心は、実際の1974年スキャンPDF(karg74.pdf、米空軍のMULTICSセキュリティ評価レポート、パブリックドメイン)を使ったEasyOCRとDocling(MIT)の比較だ。

興味深いのは、DoclingのデフォルトOCRバックエンドはEasyOCR自身という点だ。同じ認識器が同じピクセルを読んでいる。差異はDoclingがその上に積み上げるものすべてにある。

以下の数値は元記事に記載されたものである(※編集部では独自に再現検証していない)。

EasyOCR Docling
処理時間 59.7秒 134.4秒
検出行数 346ボックス 105行/段落
抽出文字数 4,952文字 5,423文字
page_df 5行
toc_df 11エントリ
image_df 4行(図を個別検出)
confidenceカラム あり(平均0.81) なし

EasyOCRは速い(2.3倍速)。confidenceスコアも付いてくる。しかし構造は何もない。Doclingはレイアウトモデルを走らせることで、同じOCR出力から見出しをTOCエントリに、図をimage_dfの行に変換する。

文字レベルの誤り率は両者でほぼ同等だった(この1974年スキャンでは"Laboralory"、"und"など同様のノイズが発生)。OCR精度の問題ではなく、その後の処理の差が結果を分けている。


parse_pdf_easyocrの設計

記事ではparse_pdf_easyocrというラッパー関数も紹介されている。他のパーサー(PyMuPDFやDocling)と同じインターフェース(line_dfparsing_summaryなどのキーを持つdict)を返すよう設計されており、page_dftoc_dfなどは空のDataFrameとして返る(KeyErrorは起きない)。

主なパラメータは以下の通りだ。

  • **languages**:ISO-639-1コードのタプル(enfrzhなど)。多言語コーパスには言語セットごとにReaderを用意し、@lru_cacheでメモリ上に保持する
  • **render_scale**:ページのラスタライズ解像度を制御するスケール係数。scale=1.0はfitzのデフォルト解像度(通常72DPI相当)に対応するが、実際の出力解像度はPDF内部の座標系に依存するため厳密には異なる場合がある。本文テキストの標準値は2.0(約144DPI相当)で、小フォントには3.0まで上げることが推奨されている
  • gpu:デフォルトはCPU。CUDA有効時は3〜5倍の高速化が得られる
  • **confidence_threshold**:低信頼度の検出を除外する閾値。劣化スキャンでは0.3でノイズの大半を除去できる

どう使い分けるか

記事の結論はシンプルだ。EasyOCRはOCRの「床」である。テキストが埋め込まれていないスキャンPDFからテキストを取り出すだけなら機能する。しかしRAGパイプラインがセクション検索、表の抽出、図の参照を必要とするなら、レイアウトモデルを持つエンジン(Docling、Azure Document Intelligence、Vision LLMなど)が必要になる。

EasyOCRをRAGパイプラインのベースラインとして使い、その出力の限界を理解した上で上位エンジンと比較する——という使い方が、この記事の提案する立ち位置だ。

詳細はParse Scanned PDFs for RAG with EasyOCR: Free OCR Gives You Words, Not a Documentを参照していただきたい。