画像タグ付けはもう不要:CLIPとQdrantでセマンティック検索エンジンを構築する

AI tutorial - IT technology blog
AI tutorial - IT technology blog

メタデータを超えて:マルチモーダル検索への移行

5万枚の画像データベースに手動でタグを付けるのは、気が遠くなるような作業です。さらに悪いことに、それはしばしば無意味です。「夕日」というタグを付け忘れると、たとえ素晴らしい画像であっても、その言葉で検索するユーザーは二度とその画像を見つけることができません。人間による入力が一貫性を欠いた瞬間に崩壊してしまう、キーワードベースの脆弱なシステムに私たちは皆苦しんできました。

マルチモーダル検索は、ニューラルネットワークを使用して実際の視覚的内容を理解することで、この問題を解決します。テキストと画像を「ベクトルエンベディング」と呼ばれる共有の数学的空間にマッピングすることで、意味に基づいた一致を見つけ出します。私は、手動のラベル付けが不可能だった非構造化データセットに対してこのパターンを導入しましたが、検索精度は一貫して従来のキーワードフィルターを上回っていました。

大きな議論:キーワード vs ベクトル

コードを1行も書く前に、BM25を使用したElasticsearchのような、これまで使用してきた可能性のある従来のスタックとどのように違うのかを見てみましょう。

1. キーワードベースの検索(従来の方法)

これは、ファイル名、代替テキスト(Alt-text)、またはSQLタグなどのメタデータに完全に依存しています。特定の製品IDを見つけるような完全一致には非常に高速です。しかし、「常識」が全くありません。ユーザーが「子犬」と検索しても、タグが「イヌ科」となっている場合、網羅的な類義語辞典を手動で構築していない限り、システムは結果をゼロとして返します。

2. マルチモーダルベクトル検索(モダンな方法)

OpenAIのCLIP(Contrastive Language-Image Pre-training)を使用すると、すべての画像とテキストクエリに対して512次元の数値ベクトルを生成します。検索エンジンは、これらの点と点の間の数学的な距離を計算します。ベクトルが近ければ、そのコンテンツは関連性があるということです。これにより、真のセマンティックな理解が可能になります。「霜の降りた朝」で検索すれば、データベース内に「霜」という言葉が一度も登場しなくても、午前6時の凍った野原の写真を返すことができます。

ベクトル検索の現実:トレードオフ

完璧なアーキテクチャは存在しません。これらのシステムを大規模に運用して気づいたことを以下にまとめます。

メリット

  • ゼロショット性能: モデルを再学習させる必要はありません。CLIPは「アールデコ建築」から「ゴールデンレトリバー」まで、一般的な概念をそのままの状態で理解します。
  • 複雑なクエリ: ユーザーは、どのタグが使われているか推測するのではなく、「公園のベンチに座っている赤い帽子をかぶった男」といった自然な文章を使用できます。
  • 労働力の節約: 手動でのデータ入力を排除できるため、人間によるラベリング時間を何百時間も節約できます。

課題

  • 計算コスト: 標準的なCPUで100万枚の画像を処理するには10時間以上かかる場合があります。高スループットのインデックス作成を効率的に行うには、GPU(NVIDIA T4など)が必要になります。
  • 解釈可能性: それは「ブラックボックス」です。なぜモデルがある夕日を別の夕日よりも高くランク付けしたのかを正確に説明することは、数学的に複雑です。
  • メモリ使用量: ベクトルデータベースはメモリを大量に消費します。ミリ秒以下の検索速度を求める場合、512次元のベクトル100万件につき、およそ2GBのRAMを割り当てる必要があります。

推奨される本番用スタック

ローカルなプロトタイプから一歩進める場合は、以下の組み合わせをお勧めします。

  • CLIP: 速度を重視する場合は ViT-B/32 を、精度を重視する場合は ViT-L/14 を使用します。OpenCLIPは、OpenAI以外の重みを好む場合の優れた代替案です。
  • Qdrant: Rustで書かれた高性能なベクトルデータベースです。高次元データをスマートに処理し、堅牢なPython SDKを提供しています。
  • FastAPI: 検索ロジックをクリーンで並行性の高いREST APIとして公開するために使用します。
  • Docker: 環境構築の煩わしさなしにQdrantエンジンを管理するために使用します。

実装ガイド

このシステムの機能的なバージョンを構築しましょう。ロジックにはPythonを使用し、ストレージエンジンにはQdrantを使用します。

ステップ1:DockerでQdrantを起動する

Qdrantは軽量で管理が簡単です。次のコマンドを使用して、永続ストレージを備えたエンジンをローカルで実行します。

docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

ステップ2:環境のセットアップ

Qdrantクライアント、Sentence-Transformersライブラリ(CLIPの使用を効率化します)、そして画像処理用のPillowが必要です。

pip install qdrant-client sentence-transformers pillow

ステップ3:モデルの初期化

以下のスクリプトは、Qdrantに接続し、CLIPモデルをメモリにロードします。初回実行時には、数百メガバイトのモデルの重みがダウンロードされることに注意してください。

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
from sentence_transformers import SentenceTransformer
from PIL import Image
import os

# ローカルのQdrantインスタンスに接続
client = QdrantClient("localhost", port=6333)

# CLIPモデルをロード(速度と精度のバランスを考慮)
model = SentenceTransformer('clip-ViT-B-32')

# コレクションを初期化
COLLECTION_NAME = "image_catalog"
client.recreate_collection(
    collection_name=COLLECTION_NAME,
    vectors_config=VectorParams(size=512, distance=Distance.COSINE),
)

ステップ4:データセットのインデックス作成

画像をベクトルに変換し、Qdrantに「アップサート」する必要があります。1,000枚を超えるデータセットの場合は、メモリオーバーフローを避けるために必ずバッチ処理を行ってください。

def index_images(image_folder):
    images = []
    metadata = []
    
    for filename in os.listdir(image_folder):
        if filename.lower().endswith((".jpg", ".png", ".jpeg")):
            img_path = os.path.join(image_folder, filename)
            images.append(Image.open(img_path))
            metadata.append({"filename": filename, "path": img_path})

    print(f"{len(images)} 枚の画像をエンコード中...")
    # パフォーマンス向上のため、バッチ処理は必須です
    embeddings = model.encode(images, batch_size=32, show_progress_bar=True)

    client.upload_collection(
        collection_name=COLLECTION_NAME,
        vectors=embeddings,
        payload=metadata
    )
    print("インデックス作成が完了しました。")

index_images("./my_photos")

ステップ5:自然言語クエリのテスト

ここで数学が機能へと変わります。テキスト文字列を同じベクトル空間に変換し、Qdrantに最も近い視覚的な一致を問い合わせます。

def search_images(query_text, limit=3):
    query_vector = model.encode([query_text])[0]

    results = client.search(
        collection_name=COLLECTION_NAME,
        query_vector=query_vector,
        limit=limit
    )

    for res in results:
        print(f"一致スコア: {res.score:.4f} | ファイル: {res.payload['filename']}")

# 検索例
search_images("ノートパソコンのキーボードの上で眠る猫")

本番環境へのスケーリング

スクリプトから本番環境での信頼性の高いサービスに移行するには、3つの領域に焦点を当てる必要があります。まず、すべてをバッチ化することです。画像を1枚ずつインデックス化すると、スループットが極端に低下します。GPUを常にフル稼働させるには、batch_sizeパラメータを使用してください。

2つ目は、メモリの最適化です。RAMが限られている場合、Qdrantはベクトルのオンディスクストレージ(mmap)をサポートしています。このトレードオフによりレイテンシはわずかに増加しますが、控えめなハードウェアでも数百万のベクトルを処理できるようになります。

最後に、入力の標準化です。CLIPは特定の解像度(通常は224×224ピクセル)を想定しています。ライブラリが自動的に処理することも多いですが、ネットワークに到達する前に画像を事前にリサイズしておくことで、I/Oのボトルネックと処理時間を大幅に削減できます。

最後に

実際に「見る」ことができる検索エンジンを構築するには、以前は博士号と莫大な研究予算が必要でした。今日では、一人のエンジニアが午後の時間を使ってセマンティック画像検索をデプロイできます。このパターンは写真に限定されません。ビデオフレーム、音声クリップ、または医療画像にも同じベクトルの原理を適用できます。もし、まだ手動のタグに頼っているのなら、スタックをアップグレードする時が来ています。

Share: