CLIPを使ってローカル画像の自然言語検索を低コストで実現する
X-tech推進本部 小林通常、ローカルにある画像素材を探す際は、ファイル名やフォルダ名、画像に付けられたタグなどを手掛かりにすることが多いと思います。しかし、あらかじめ丁寧に情報を整理しておかないと、検索が難しくなってしまうことがあります。
以前の記事で、過去に作成したコードを自然言語で検索する方法を紹介しましたが、このような自然言語検索は、テキストだけでなく画像にも応用できます。
今回は、画像と文章の意味的な近さを比較できるCLIPというモデルを使い、ローカルフォルダ内の画像を自然言語で検索するPythonツールを作成します。RAGやLLM、外部APIは使用せず、学習済みモデルをCPU環境でローカル実行するシンプルな構成で画像検索を試します。
CLIPとは
CLIPは、画像と文章をそれぞれベクトルに変換し、両者の意味的な近さを比較できるモデルです。画像と文章の内容が近い場合、それぞれから生成されたベクトルも近くなります。
機械学習を用いた画像検索というと、カテゴリごとに教師画像を用意したり、モデルを学習させたりといった事前準備が大変なイメージがありますが、CLIPでは学習済みモデルに任意の検索文を与えられます。検索対象となる画像をあらかじめベクトル化しておき、検索文のベクトルとの類似度を計算すれば、文章の意味に近い画像を順番に取得できます。
今回は、OpenAIが公開したCLIPの代表的なモデルの一つであるopenai/clip-vit-base-patch32を使用しました。実装例や情報が多く、Hugging Face Transformersから簡単に利用できます。また、比較的軽量で、CPU環境でも試しやすいというメリットがあります。
ただし、このモデルは日本語向けに学習されたものではありません。そのため、今回の検索文はすべて英語としました。
今回作るもの
今回は、Pexelsからダウンロードした100枚の画像を検索対象としました。画像には、オフィス、会議、工場、倉庫、IT機器、自然、風力発電、都市、建築、人物、料理といった内容が含まれています。
これらの画像を検索できるようにするために、ローカルフォルダ内の画像をベクトル化してファイルに保存するプログラムencode_images.pyと、入力された検索文をベクトル化して画像ベクトルとの類似度が高い画像名を表示するプログラムsearch_images.pyを作成します。また、今回のコードはCPU実行を想定しています。
実装
フォルダ構成は次のようになっています。
clip-image-search/
├─ images/
├─ src/
│ ├─ encode_images.py
│ └─ search_images.py
└─ data/
├─ image_features.pt
└─ image_names.txt
encode_images.py
encode_images.pyでは、imagesフォルダにある画像(今回はJPEG)すべてを参照して意味ベクトルを作成してimage_features.ptとして保存します。
# encode_images.py
from pathlib import Path
import torch
from PIL import Image
from transformers import CLIPModel, CLIPProcessor
MODEL_NAME = "openai/clip-vit-base-patch32"
PROJECT_DIR = Path(__file__).resolve().parents[1]
IMAGES_DIR = PROJECT_DIR / "images"
FEATURES_PATH = PROJECT_DIR / "data" / "image_features.pt"
NAMES_PATH = PROJECT_DIR / "data" / "image_names.txt"
image_paths = sorted(IMAGES_DIR.glob("*.jpg"))
images = [Image.open(path).convert("RGB") for path in image_paths]
model = CLIPModel.from_pretrained(MODEL_NAME) # CLIPモデル読み込み
processor = CLIPProcessor.from_pretrained(MODEL_NAME) # CLIP用の前処理読み込み
model.eval() # モデルを推論用に切り替え
inputs = processor(images=images, return_tensors="pt") # 画像をCLIPが読み取れる形式に変換
with torch.inference_mode():
features = model.get_image_features(**inputs) # 画像を意味ベクトルに変換
features = features / features.norm(dim=1, keepdim=True) # ベクトルを正規化
FEATURES_PATH.parent.mkdir(exist_ok=True)
torch.save(features, FEATURES_PATH) # 画像の意味ベクトルを保存
NAMES_PATH.write_text( # 画像ファイル名一覧を保存
"\n".join(path.name for path in image_paths),
encoding="utf-8",
)
print(f"{len(image_paths)}枚の画像ベクトルを保存しました。")
search_images.py
search_images.pyでは、ユーザーが入力した検索文から意味ベクトルを作成し、保存済みの各画像の意味ベクトルとの類似度を計算することで検索文にマッチする画像を見つけます。
# search_images.py
# encode_images.pyと共通するimport文および定数は省略
image_features = torch.load(FEATURES_PATH, weights_only=True) # 画像の意味ベクトルを読み込み
image_names = NAMES_PATH.read_text(encoding="utf-8").splitlines() # 画像名一覧読み込み
model = CLIPModel.from_pretrained(MODEL_NAME)
processor = CLIPProcessor.from_pretrained(MODEL_NAME)
model.eval()
query = input("検索文(英語): ")
inputs = processor(text=[query], return_tensors="pt") # 検索文をCLIPが読み取れる形式に変換
with torch.inference_mode():
text_features = model.get_text_features(**inputs) # 検索文を意味ベクトルに変換
text_features = text_features / text_features.norm(dim=1, keepdim=True)
similarities = image_features @ text_features.T # 類似度(正規化済みベクトルの内積)を計算
top_indices = similarities[:, 0].argsort(descending=True)[:5] # 上位5件を取得
for rank, index in enumerate(top_indices, start=1):
print(f"{rank}. {similarities[index, 0]:.3f} {image_names[index]}") # 上位の類似度と画像名を表示
実行
実行に必要なライブラリをインストールします。
pip install torch transformers pillow
encode_images.pyを実行して画像の意味ベクトルを作成します。初回実行ではモデルをダウンロードするため時間がかかります。また、一度実行した後は対象画像に変更がない限り再実行は不要です。
python src/encode_images.py
search_images.pyを実行して検索を実行できます。検索文を入力すると、類似度の高い順に5件の画像名が出力されます。
python src/search_images.py
検索文(英語): people having a business meeting
実行結果
検索結果のうち、特徴的だったものをいくつか紹介します。
人物とシチュエーション
検索文:
people having a business meeting(仕事の打ち合わせをする人たち)

複数の人物がテーブルを囲み、打ち合わせをしている画像が最もマッチする画像として選ばれました。単に人物が複数写っている画像ではなく、オフィス内で話し合っている様子の画像が選ばれており、「people」「business」「meeting」という検索文の意味をおおむね捉えられているように見えます。
検索文:
a warehouse worker using a tablet(タブレットを使う倉庫作業員)

倉庫内でタブレット端末を持って歩いている人物の画像がマッチしました。「warehouse」「worker」「tablet」という検索対象には条件によく合う画像が含まれていたため、かなり意図に近い結果となりました。
抽象的な検索
検索文:
a sustainable future(持続可能な未来)

風力発電の風車の画像がマッチしました。「持続可能な未来」は、人物やタブレットのように明確な形を持つ物体ではありません。そのため「正解した」と断定することはできないのですが、「検索文から連想される画像(再生可能エネルギーである風力発電)が上位に表示された」と評価できそうです。
複雑な視覚的判断
しかし一方で、検索条件が複雑になると思った通りに検索できないこともありました。1つ目に紹介したpeople having a business meetingの画像では4人がミーティングをしていますが、他にも今回用意した画像の中には2~3人でミーティングをしている画像が複数あります。試しにtwo people having a meetingやthree people having a meetingという文言で検索してみましたが、people having a business meetingと同じ画像が選ばれました。
また、この4人が会議している画像は次のように表現することもできそうです。
a man standing apart from people seated at a table(テーブルに着席した人たちから離れて立つ男性)
しかしこの文で検索したところ、次の画像がマッチしました。写っているのは1人だけで、検索にマッチする画像とは言えません。

今回実行した結果では、人数を指定した検索では、会議というシチュエーションが強く反映され、人数の違いは結果に反映されませんでした。また、複数の人物・物体・行動を含む検索文について、すべての条件を満たす画像が上位に表示されるとは限りませんでした。OpenAIの公式情報にも、CLIPは物体数を数えるような体系的な課題や、距離を判断するような複雑な視覚課題を苦手とすることが示されています。
画像や環境によっても結果は異なるため、実務に用いる際はさらに多くの検証や工夫が必要ですが、これらの結果を踏まえると簡単な検索文なら役立つ場面は多いと言えます。
今回はOpenAI版CLIPを使用しましたが、使えるマシンスペックや必要とする性能に応じてOpenCLIP、SigLIP2、MobileCLIPなどの後継・派生モデルも試してみると良いかもしれません。また、今回は総当たり検索を行いましたが、画像数が多く検索時間が問題になる場合は、Faissなどのベクトル類似検索ライブラリや、ベクトルデータベースの導入を検討しても良いでしょう。
まとめ
今回は、CLIPを使い、ローカルフォルダに保存した画像を自然言語で検索するツールを作成しました。画像と検索文をベクトル化し、類似度を計算することで、検索文の意味に近い画像を取得できます。
また、CLIPによる検索はローカル環境で処理できるため、セキュリティ面(画像を外部APIへ送信しない)、コスト面(API利用等の従量課金がない)のメリットがあります。
もちろんCLIPには先述した限界があり、既存の画像管理をすべて置き換えるものではありませんが、ファイル名やタグだけでは探しにくい画像を見つけるための補助としては十分に有用だと考えられます。例えば社内画像素材をベクトル化しておけば、企業サイトなどで使用したい画像をemployees working in an officeといった文で検索できます。RAGやLLMを導入せず簡単にできる自然言語画像検索として、試してみてはいかがでしょうか。