👁️

【脱OCR】文字起こしはもうやめだ。PDFを「見たまま」検索するColPali RAG実装のやり方

に公開

🚀 はじめに:RAGの「OCRの壁」を突破する

ルミナイR&Dチームの宮脇彰梧です。
現在はマルチモーダルAIの研究を行う大学院生として、
生成AIやAIエージェントの技術を実践的に探求しています。

「表組みが複雑な仕様書をRAGに入れたら、数字がグチャグチャになった」
「論文の図表にある重要な注釈が検索に引っかからない」

RAG(検索拡張生成)システムを開発するエンジニアなら、誰もがこの「OCR(光学文字認識)の限界」に直面したことがあるはずです。PDFをテキストに変換する時点で、レイアウト情報は失われ、意味の塊が破壊されてしまう——これが従来のRAGの構造的欠陥でした。

しかし、2024年後半、この常識を覆すモデルが登場しました。Hugging Faceなどで公開された「ColPali」です。

これは、PDFのページを「テキスト」ではなく「画像(Vision Embedding)」としてベクトル化し、検索可能にする技術です。つまり、人間がページを目で見て探すのと同じように、AIが「見た目」で情報を検索します。

今回は、この革新的なRetrieverであるColPaliと、視覚理解に優れたQwen2-VLを組み合わせ、「OCRを一切使わない完全マルチモーダルRAG」を実装・解説します。

さらに今回は、Google Colabの無料枠(T4 GPU)でも動作するように、メモリ管理と量子化を最適化したコードを紹介します。

🔬 ColPali とは何か?

実装に入る前に、ColPaliの凄さを1分で解説します。

従来のRAGとColPaliの最大の違いは、「情報の保持形式」です。

  • 従来 (OCR-based): PDF \rightarrow OCR \rightarrow テキストチャンク \rightarrow Embedding
    • \times レイアウト情報喪失、図表崩壊
  • ColPali (Vision-based): PDF \rightarrow 画像パッチ \rightarrow Vision Language Model (PaliGemma) \rightarrow Multi-Vector Embedding
    • \bigcirc 見たままを理解、図表もレイアウトも保持

ColPaliは「ColBERT(Late Interaction)」という強力な検索手法をVisionモデルに応用しています。これにより、クエリ(質問)とドキュメント(画像パッチ)の詳細なマッチングが可能になり、「右上のグラフの数値」のような位置情報や視覚情報に依存する質問にも強くなります。

🛠️ 実装:ColPali × Qwen2-VL RAG

それでは、実際にコードを書いていきましょう。
今回の最大の技術的課題は「VRAM容量」です。

ColPali(検索モデル)とQwen2-VL(生成モデル)という2つの巨人を、限られたGPUメモリ(T4の16GB)に同居させるため、「検索が終わったらメモリを掃除して、生成モデルを呼ぶ」というフローを構築します。

0. 環境構築

Google Colabでの実行を想定しています。
ColPaliを扱う byaldi や、PDF処理に必要な poppler-utils をインストールします。

!apt-get update
!apt-get install -y poppler-utils
!pip install -q pdf2image byaldi qwen-vl-utils bitsandbytes

1. PDFの準備と可視化

まずは対象となるPDFを用意します。
ここでは例として、論文のPDF(図表が含まれているもの)を読み込みます。

https://arxiv.org/abs/2512.23676

「AIに検索させる前に、人間が中身を確認する」のはデバッグの基本です。

import os
from pdf2image import convert_from_path
import matplotlib.pyplot as plt

# 任意のPDFパス(Colabにアップロードしてください)
pdf_path = "sample.pdf"

# ファイル確認
if not os.path.exists(pdf_path):
    print(f"⚠️ {pdf_path} が見つかりません。")
else:
    print(f"📄 PDFを読み込んでいます: {pdf_path}")

    # PDFの1ページ目を画像に変換して表示
    images = convert_from_path(pdf_path)
    sample_image = images[0]

    plt.figure(figsize=(10, 10))
    plt.imshow(sample_image)
    plt.axis('off')
    plt.title("Target Document Page 1")
    plt.show()

    print(f"全ページ:{len(images)}ページ")

2. ColPaliによるインデックス作成と検索 (Retriever)

次に、byaldi を使ってColPaliモデルをロードし、PDFをインデックス化(ベクトル化)します。

from byaldi import RAGMultiModalModel

# ColPaliモデルのロード
# verbose=0 でログ出力を抑制
RAG = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2", verbose=0)

# インデックスの作成
index_name = "my_pdf_index"
print("Indexing start...")

RAG.index(
    input_path=pdf_path,
    index_name=index_name,
    store_collection_with_index=True,
    overwrite=True
)
print("Indexing done.")

準備が整いました!検索を実行してみましょう。
ここでは「図の中身」について質問します。OCRでは絶対に答えられない質問です。

# 🔍 検索クエリ:図の内容を問う
query_text = "このドキュメントに記されている図は何を示していますか?"

# 検索実行 (k=1 で最も関連度の高いページを取得)
results = RAG.search(query_text, k=1)

# 検索結果のスコア表示
print(f"Query: {query_text}")
print(f"Top Result Score: {results[0].score:.4f}")
print(f"Retrieved Page ID: {results[0].doc_id}, Page Index: {results[0].page_num}")

# ヒットしたページの画像を取得
# byaldiのpage_numは1始まりなので、list index用に-1します
page_index = results[0].page_num - 1
retrieved_image_content = images[page_index]

# 検索されたページを可視化(これがAIが見る情報です!)
plt.figure(figsize=(10, 10))
plt.imshow(retrieved_image_content)
plt.axis('off')
plt.title(f"Page for Query: {query_text}")
plt.show()

3. 【重要】メモリの大掃除

ここがT4 GPUで動かすための最大のポイントです。
検索が終わった今、ColPaliモデルは不要です。VRAMを圧迫しているため、次のQwen2-VLをロードする前に強制的にメモリから削除します。

import gc
import torch

# 🧹 メモリの掃除
print("Cleaning up VRAM...")

# Retrieverモデルと検索結果を削除
del RAG
del results

# ガベージコレクションとGPUキャッシュクリア
gc.collect()
torch.cuda.empty_cache()

print("✨ VRAM Cleaned! Ready for Generator.")

4. Qwen2-VL による回答生成 (Generator)

メモリが空いたので、視覚理解LLMである Qwen2-VL-7B-Instruct をロードします。
さらにメモリを節約するため、bitsandbytes を使って4bit量子化で読み込みます。

from transformers import Qwen2VLForConditionalGeneration, AutoProcessor, BitsAndBytesConfig
from qwen_vl_utils import process_vision_info

# 💾 メモリ不足対策:4bit量子化の設定
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16,
)

# Generator model
gen_model_name = "Qwen/Qwen2-VL-7B-Instruct"

print(f"Loading generator: {gen_model_name}")

# モデルのロード(量子化設定を適用)
model = Qwen2VLForConditionalGeneration.from_pretrained(
    gen_model_name,
    quantization_config=quantization_config,
    device_map="auto",
    torch_dtype=torch.float16
)

# Processorのロード
processor = AutoProcessor.from_pretrained(gen_model_name)

いよいよ生成です。検索された「画像」と「質問」をQwenに投げます。

# プロンプトの構築
messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "image",
                "image": retrieved_image_content, # 検索でヒットした画像
            },
            {"type": "text", "text": f"与えられた画像を元に、次の質問を詳しく答えてください。: {query_text}"},
        ],
    }
]

# 推論の準備
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
image_inputs, video_inputs = process_vision_info(messages)
inputs = processor(
    text=[text],
    images=image_inputs,
    videos=video_inputs,
    padding=True,
    return_tensors="pt",
).to("cuda")

# 生成実行
print("Generating...")
generated_ids = model.generate(inputs, max_new_tokens=512)
generate_ids_trimmed = [
    out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = processor.batch_decode(
    generate_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
)

print("-" * 30)
print("[AI Answer]")
print(output_text[0])
print("-" * 30)

📊 出力結果

実際にWeb World Modelsの論文PDFを入力した結果がこちらです。

以上のページが検索されました。
以下が生成された文章です。

Generating...
------------------------------
[AI Answer]
このドキュメントに記されている図は、「Web World Models」を示しています。この図は、異なるWeb World Modelsの例を示しています。これらのモデルは、異なるドメインをカバーしており、それぞれのモデルがどのように機能するかを示しています。具体的には、「Bookshelf」、「AI Spire」、「Cosmic Voyager」、「Galaxy Travel」、「World Travel」、「AI Alchemy」、「WWMPedia」などがあります。
------------------------------

完璧です。
テキスト抽出では単なる文字列の羅列になってしまう図中のラベル("Bookshelf", "AI Spire" など)を、AIが画像の配置から正確に読み取り、意味を理解して回答しています。

🧐 筆者の考察:実務での有用性

この「Vision RAG」を実装して感じたポイントは以下の3点です。

  1. 図表への強さは本物
    今回の実験のように、構造的な概念図やグラフの読み取りにおいて、OCRベースの手法を圧倒します。製造業の図面検索や、金融機関のチャート分析など、これまでRAGが苦手としていた領域での活用が期待できます。
  2. T4 GPUでも実用可能
    「VRAMが足りない」と諦めがちな最新モデルですが、「RetrieverとGeneratorを直列に実行し、都度メモリを掃除する」という工夫で、無料枠のColabでも十分に動作検証が可能です。
  3. 「OCRレス」という開発体験
    PDFパーサー(PyPDF2やUnstructured)の微調整から解放されるのは、開発者にとって大きなメリットです。「人間が見ているまま」をインデックス化するため、前処理が劇的にシンプルになります。

🔮 まとめ

今回は Hugging Face の ColPaliQwen2-VL を組み合わせ、PDFを画像のまま検索・理解する次世代RAGを実装しました。

  • ColPali:ページを画像としてEmbeddingし、視覚的特徴を含めて検索する。
  • Qwen2-VL:検索結果の画像を見て、高度な推論・回答生成を行う。
  • 工夫点:メモリ解放処理を挟むことで、一般のGPU環境でも動作可能にする。

OCRの精度に悩まされているエンジニアの皆さん、ぜひ「視覚で検索するRAG」を試してみてください。世界が変わって見えます。


https://github.com/LoNebula/Lluminai/tree/main/21_2025_12_30_rag_huggingface

執筆:宮脇 彰梧(ルミナイ株式会社 / Lluminai)


【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion