ColPaliとByaldiによるVision RAG構築ガイド:OCR不要で複雑な表やPDFレイアウトを読み取る

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

クイックスタート:5分で動かすVision RAG

TesseractやPyMuPDFを使って財務諸表などのPDFをスクレイピングするのは、まさに悪夢と言えます。数値列がズレたり、結合セルが崩壊したり、脚注が本文に混ざったりと課題が山積みです。

Vision RAGは、この課題をまったく異なるアプローチで解決します。生テキストを抽出する代わりに、PDFページを直接画像としてレンダリングします。そしてモデルが、文字情報と視覚的レイアウトの双方を同時にベクトル空間へ埋め込み(embed)ます。これにより、棒グラフや多層構造の複雑な表であっても、空間的なコンテキストを損なうことなく保持できます。

以下では、ColPaliモデルをラップし、インデックス作成(indexing)や検索(search)の処理を大幅に簡素化してくれるライブラリbyaldiを使ったクイックな実装手順を紹介します。

ステップ1:環境構築

Python 3.10以上と、最低8GBのVRAMを搭載したGPU(RTX 3060やGoogle ColabのT4など)が必要です:

# byaldiおよび画像処理関連の依存パッケージをインストール
pip install byaldi pdf2image torch torchvision --upgrade

# PDFを画像にレンダリングするためpopplerのインストールが必須(Ubuntu/Debian)
sudo apt-get update && sudo apt-get install -y poppler-utils

ステップ2:ドキュメントのインデックス作成と検索

quick_vision_rag.pyを作成し、任意の財務レポートファイルを使って動作を確認します:

from byaldi import RAGMultiModalModel

# ColPali v1.2のチェックポイント(PaliGemma-3Bベース)をロード
RAG = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")

# 1. PDFファイルを直接インデックス化
print("ドキュメントのインデックス作成を開始...")
RAG.index(
    input_path="bao_cao_tai_chinh_q3.pdf", # PDFファイルのパスまたは複数PDFを含むフォルダパス
    index_name="financial_reports",        # ディスク上に保存するインデックス名
    store_collection_with_index=True,      # プレビュー用に画像のコピーも併せて保存
    overwrite=True
)

# 2. 自然言語の質問で表形式データを検索
query = "クラウドサービス部門の売上高は前年同期比で何%成長しましたか?"
results = RAG.search(query, k=3)

# 3. 最も関連性の高いページを抽出
for idx, res in enumerate(results):
    print(f"Top {idx + 1} | ページ: {res.page_num} | 類似度スコア: {res.score:.4f}")

正規表現も不要、テキストのチャンク分割も不要です。システムが必要なデータテーブルを含むページをピンポイントで特定してくれます。

仕組みの解剖:ColPaliとByaldiはどのように動作するのか?

従来のRAGのパイプラインは通常、次のようなフローを辿ります:

PDF -> OCR/PyPDF -> Text Chunks -> Bi-Encoder Embedding -> Vector DB

破綻の原因は最初のステップにあります。生テキストを抽出する過程で、行の位置、フォントサイズ、色、罫線などの情報が完全に失われてしまいます。財務諸表や技術文書において、レイアウトの喪失は情報の喪失そのものを意味します。

ColPali:ドキュメント画像にLate Interaction機構を導入

ColPaliは、ColBERTアーキテクチャとVision-Language Model(VLM)であるPaliGemma-3Bを融合させたモデルです。ページ全体を単一のベクトル(Single-Vector)に圧縮するのではなく、ページ画像を細かいパッチのグリッドに分割して処理します:

  • 画像トークン化(Image tokenization):画像としてレンダリングされたドキュメントページ(448×448ピクセル)は、1024個のビジュアルトークン(visual tokens)に分割されます。各トークンはページ全体のコンテキストを含む局所的な画像領域を表現します。
  • クエリトークン化(Query tokenization):ユーザーの質問はテキストトークン(通常10〜30トークン程度)に分割されます。
  • 遅延相互作用(Late Interaction / MaxSim):単純なコサイン類似度を計算する代わりに、クエリの各トークンがページ内で最もマッチする画像トークンを探索します。これらの最適なペアの類似度スコアの合計が、最終的なマッチングスコアとなります。

このアプローチにより、微細な詳細情報の処理という課題が解決されます。数値がテーブルの右下隅のセルに隠れていたり、フッターに6ptの極小フォントで書かれた脚注であっても、クエリトークンがその位置を正確に捉えることができます。

Byaldi:研究成果をプロダクト実装へ橋渡しするライブラリ

生のColPaliをそのまま動かすのは非常に煩雑です。PDFのレンダリング、数百万に及ぶ多次元ベクトルの保存、最適化されたMaxSimアルゴリズムの実装などをすべて自前で行わなければなりません。Sean MacAvaney氏によって開発されたByaldiは、このエンジニアリングレイヤーを完全に解決します。C++/CUDAで最適化されたマルチベクトル検索エンジンを内蔵しており、わずか2つの関数を呼び出すだけでインデックス作成と検索が可能になります。

応用編:ByaldiとVLMを組み合わせて完全なQ&Aパイプラインを構築する

該当ページを検索・特定することは、情報抽出の第一歩にすぎません。ユーザーの質問に完全に回答するには、検索されたページ画像をVLM(GPT-4o-miniやQwen2-VL-7Bなど)に渡し、具体的な数値を読み取らせます:

import base64
from pdf2image import convert_from_path
from byaldi import RAGMultiModalModel
from openai import OpenAI

# 1. ディスクに保存済みのインデックスをロード
RAG = RAGMultiModalModel.from_index(".byaldi/financial_reports")

query = "第3四半期の売上総利益率は第2四半期と比較してどのように推移しましたか?"
results = RAG.search(query, k=1)
top_hit = results[0]

# 2. ヒットしたページを画像ファイルにレンダリングしてLLMに渡す準備
pages = convert_from_path("bao_cao_tai_chinh_q3.pdf", first_page=top_hit.page_num, last_page=top_hit.page_num)
image_path = f"page_{top_hit.page_num}.png"
pages[0].save(image_path, "PNG")

def encode_image_base64(path):
    with open(path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

# 3. ページ画像をVLMに送信して解析
client = OpenAI()
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": f"画像内の表を読み取り、簡潔に回答してください: {query}"},
                {
                    "type": "image_url",
                    "image_url": {"url": f"data:image/png;base64,{encode_image_base64(image_path)}"}
                }
            ]
        }
    ]
)

print("\n--- 結果 ---")
print(response.choices[0].message.content)

本番環境(プロダクション)運用のための実践的最適化ノウハウ

ノートブック上でスムーズに動作したとしても、そのままサーバーにデプロイできるとは限りません。スケールさせる前に検討すべき3つの重要ポイントを解説します。

1. RAMとマルチベクトルストレージ容量の課題

PDFの1ページごとに1024個のベクトル(次元数128、float16)が生成され、1ページあたり約256 KBのベクトルデータを消費します。一見小さなサイズに思えますが、ページ数が増えると急速に膨らみます:

  • 10,000ページ規模:インデックスを保持するだけで約2.5 GBのRAMを消費。
  • 100,000ページ規模:25 GBのRAMを圧迫し、ハードウェアコストが急騰。
  • 解決策:Byaldiがネイティブ対応しているバイナリ量子化(Binary Quantization)やbfloat16などの圧縮フォーマットを活用します。ドキュメント規模が50,000ページを超える場合は、ローカルファイル保存から、VespaやQdrantなどLate Interactionをネイティブサポートするベクターデータベースへの移行を検討してください。

2. PDFレンダリング時のDPI標準化

PaliGemmaは入力サイズを448×448ピクセルにリサイズ・正規化します。しかし、初期レンダリング時のフォントの鮮明さが、モデルがコンテンツを正確に読み取れるかを左右します。

  • DPI < 100:フットノートの小さな文字が潰れて不鮮明になり、Recall@kが著しく低下します。
  • DPI > 200:精度向上にはほとんど寄与せず、popplerによるPDF変換時間が3倍近くかかり、CPUリソースを逼迫させます。
  • 推奨設定:dpi=150に固定することをおすすめします。これは視認性とI/O処理速度のバランスが最も優れた最適値です。

3. ハイブリッド構成:BM25 + Byaldiの併用

ColPaliは空間的な文脈理解や表形式データの解析に極めて長けています。しかし一方で、納税者番号、請求書のシリアル番号、契約番号といった規則性のない特定の文字列を見落とすことがあります。

実運用パイプラインでは、以下の並行アプローチが理想的です:

  • BM25 / Meilisearch:固有識別子(請求書番号、SKU、固有名詞など)の厳密一致検索を担当。
  • Byaldi(ColPali):全体のセマンティクス、フローチャート、表構造データの把握を担当。
  • Reranker:双方の検索結果から上位10件を取得し、Cross-Encoderを通して最適な2〜3ページに絞り込んでからVLM APIを呼び出します。これによりRecallを大幅に向上させつつ、毎月の画像トークンコストを大幅に節約できます。
Share: