見出し画像

リクルートからも日本語CLIPが来た! recruit-jp/japanese-clip-vit-b-32-roberta-base を使って、ローカルの画像を日本語で検索してみる

一昨日、Googleのmultiligual SigLIPを使って画像検索する記事を書いたところで、なんと、昨日、リクルートからも日本語対応のCLIPが出ました。しかも商用可能なCC-BY-4.0ライセンス!ヤバい。今年はローカルで動くマルチモーダルがアツい年になりそうです。

CLIPとはスーパー雑に言えば、画像とテキストを同じ空間のベクトルにできるモデルで、テキストと画像が「近いか」を判定したりできます。この性質を利用して、任意のテキストタグで画像を分類したりできます。

というわけで、一昨日の記事と同じように、リクルートの日本語対応CLIPを使って、画像検索をするスクリプトをシュッと作ってみました。

↑一昨日の記事

Google Drive → AI → recruit-clip → images に、画像がたくさん保存されているとします。私は自分のスマホで撮った写真を500枚ほど入れました。作業ディレクトリは /content/drive/MyDrive/AI/recruit-clip としています。Google Colabで動きます。

!pip install pillow requests transformers torch torchvision sentencepiece ftfy gradio typing
from google.colab import drive
drive.mount('/content/drive')
%cd /content/drive/MyDrive/AI/recruit-clip/
# imagesフォルダ内の画像をすべてベクトル化してJSONに保存する

import os
import glob
from tqdm import tqdm
import json
import io
import requests

import torch
import torchvision
from PIL import Image
from transformers import AutoTokenizer, AutoModel


model_name = "recruit-jp/japanese-clip-vit-b-32-roberta-base"
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name, trust_remote_code=True).to(device)


def _convert_to_rgb(image):
    return image.convert('RGB')


preprocess = torchvision.transforms.Compose([
    torchvision.transforms.Resize(size=224, interpolation=torchvision.transforms.InterpolationMode.BICUBIC, max_size=None),
    torchvision.transforms.CenterCrop(size=(224, 224)),
    _convert_to_rgb,
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711])
])


def tokenize(tokenizer, texts):
    texts = ["[CLS]" + text for text in texts]
    encodings = [
        # NOTE: the maximum token length that can be fed into this model is 77
        tokenizer(text, max_length=77, padding="max_length", truncation=True, add_special_tokens=False)["input_ids"]
        for text in texts
    ]
    return torch.LongTensor(encodings)

# imagesフォルダの中身の画像を全てembeddingに変換
image_features = []
for image in tqdm(glob.glob("./images/*")):
    # imageが画像ファイルでないならスキップ
    if not image.endswith(('.jpg', '.png', '.jpeg')):
        continue
    # imageのファイルネームを取得
    filename = os.path.basename(image)
    try:
        image = Image.open(image)
        image = preprocess(image).unsqueeze(0).to(device)
        with torch.no_grad():
            embedding = model.get_image_features(image)
            embedding = embedding / embedding.norm(dim=-1, keepdim=True)
            image_features.append({'embedding': embedding,
                                  'filename': filename})
    except:
        print('error: ' + filename)
        continue

# print(image_features[0])

# image_featuresを文字列に変換
image_features_json = []
for image_feat in image_features:
    image_features_json.append({'embedding': image_feat['embedding'].tolist(),
                                'filename': image_feat['filename']})
# image_featuresをJSONに保存

with open('recruit_images_features.json', 'w', encoding='utf-8') as f:
    json.dump(image_features_json, f, indent=4)
# 画像検索UIをGradioで起動

import re, torch, gradio as gr
from typing import Union, List

# image_featuresをJSONから読み込み
with open('recruit_images_features.json', 'r', encoding='utf-8') as f:
    image_features_json = json.load(f)

print(len(image_features_json))

# image_features_jsonをimage_featuresに変換
image_features = []
for image_feat in image_features_json:
    image_features.append({'embedding': torch.tensor(image_feat['embedding']).to(device),
                           'filename': image_feat['filename']})

def find_similar_image_and_display(query: str):

    text_tokens = tokenize(tokenizer, texts=[query]).to(device)
    
    with torch.inference_mode():
      text_features = model.get_text_features(input_ids=text_tokens)
      text_features /= text_features.norm(dim=-1, keepdim=True)

    # 画像の特徴量とテキストの特徴量の間のコサイン類似度を計算
    cos_similarities = torch.stack([torch.nn.functional.cosine_similarity(
        text_features[0], img_feat['embedding'], dim=-1
        ) for img_feat in image_features]).squeeze()

    # cos_similaritiesが空でないことを確認
    if len(cos_similarities) == 0:
        return []

    top_k = min(4, len(cos_similarities))
    try:
        top_sim_indices = cos_similarities.topk(top_k, largest=True).indices
        selected_images = [image_features[idx]['filename'] for idx in top_sim_indices]
    except RuntimeError as e:
        print("Runtime error occurred:", e)
        return []

    # 画像のファイルパスを返す
    return [f"images/{filename}" for filename in selected_images]

# Gradioインターフェースの出力を変更
iface = gr.Interface(
    fn=find_similar_image_and_display,
    inputs="text",
    outputs=gr.Gallery(label="Images")
)
iface.launch()

無事、検索することができました。

ソースコードを見ての通り、文字列は77トークンまでなので、あまり長文は入力できません。タグ付けには十分な長さだと思います。

また、今回のリクルートさんのモデルは、同時に評価用データセットが公開されたこともアツいと思います。

マルチモーダルをLLMに活用するには、評価データセットが不可欠だと思うので、こうした活動には本当に頭が下がる思いです。日本語圏で利用されている画像に適したマルチモーダルAIが発展することを期待しています。

ちなみに、CLIPは画像とテキストでしたが、同様に、音とテキストを結びつけるCLAPというモデルもLAIONから提案されています。

今後は、どこかの研究室か企業から、日本語対応CLAPも出てきそうな気がします。今年はローカルマルチモーダルがアツい!(多分)

この記事が気に入ったらサポートをしてみませんか?