2.1 Vision-Languageモデル入門
Vision-Languageモデル(VLM)は、画像とテキストの両方を理解し推論できるマルチモーダルAIシステムです。画像キャプションから視覚的質問応答まで、AIの重要な進歩を代表しています。
VLMの主要な能力
- 画像-テキスト検索: テキスト説明から画像を見つける、またはその逆
- 視覚的質問応答(VQA): 画像内容に関する質問に回答
- 画像キャプション生成: 画像の自然言語による説明を生成
- 視覚的推論: 視覚情報とテキスト情報を組み合わせた論理的推論
2.2 CLIP: 対照的言語画像事前学習
CLIPは2021年にOpenAIがリリースし、大規模な対照学習が強力なクロスモーダル表現を作成できることを実証し、Vision-Languageモデルに革命をもたらしました。
CLIPアーキテクチャ
ViT/ResNet] TE[テキストエンコーダー
Transformer] end subgraph Embeddings IE[画像埋め込み
512/768次元] TXE[テキスト埋め込み
512/768次元] end I --> VE T --> TE VE --> IE TE --> TXE IE --> CS[コサイン類似度] TXE --> CS CS --> CL[対照学習損失] style VE fill:#e3f2fd style TE fill:#fff3e0 style CS fill:#9b59b6,color:white
対照学習の目的
CLIPは、マッチする画像-テキストペア間の類似度を最大化し、マッチしないペアとの類似度を最小化することで学習します。
InfoNCE損失
$$\mathcal{L}_{\text{CLIP}} = -\frac{1}{N}\sum_{i=1}^{N}\left[\log\frac{\exp(\text{sim}(v_i, t_i)/\tau)}{\sum_{j=1}^{N}\exp(\text{sim}(v_i, t_j)/\tau)}\right]$$
ここで \(\text{sim}(v, t)\) はコサイン類似度、\(\tau\) は温度パラメータです。
CLIPの実装
# CLIPによるゼロショット画像分類
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import requests
import torch
# CLIPモデルの読み込み
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# 画像の読み込み
url = "https://images.unsplash.com/photo-1560807707-8cc77767d783?w=400"
image = Image.open(requests.get(url, stream=True).raw)
# 候補ラベルの定義
labels = ["犬の写真", "猫の写真", "鳥の写真"]
# 入力の処理
inputs = processor(
text=labels,
images=image,
return_tensors="pt",
padding=True
)
# 類似度スコアの取得
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image
probs = logits_per_image.softmax(dim=1)
# 結果の表示
for label, prob in zip(labels, probs[0]):
print(f"{label}: {prob.item():.2%}")
# 出力: "犬の写真: 98.5%"
CLIPによる画像-テキスト検索
# CLIPを使った画像-テキスト検索
import torch
from transformers import CLIPProcessor, CLIPModel
class CLIPRetriever:
def __init__(self, model_name="openai/clip-vit-base-patch32"):
self.model = CLIPModel.from_pretrained(model_name)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.image_embeddings = None
self.images = []
def index_images(self, images):
"""画像データベースの埋め込みを事前計算"""
self.images = images
inputs = self.processor(images=images, return_tensors="pt", padding=True)
with torch.no_grad():
self.image_embeddings = self.model.get_image_features(**inputs)
self.image_embeddings = self.image_embeddings / self.image_embeddings.norm(dim=-1, keepdim=True)
def search(self, query_text, top_k=5):
"""テキストクエリに最も類似した画像を検索"""
inputs = self.processor(text=[query_text], return_tensors="pt", padding=True)
with torch.no_grad():
text_embedding = self.model.get_text_features(**inputs)
text_embedding = text_embedding / text_embedding.norm(dim=-1, keepdim=True)
# 類似度を計算
similarities = (text_embedding @ self.image_embeddings.T).squeeze()
top_indices = similarities.argsort(descending=True)[:top_k]
return [(self.images[i], similarities[i].item()) for i in top_indices]
# 使用例
retriever = CLIPRetriever()
retriever.index_images(image_database)
results = retriever.search("海に沈む夕日")
2.3 BLIPとBLIP-2: ブートストラップVision-Language
BLIP(Bootstrapping Language-Image Pre-training)は、対照学習と生成目標を組み合わせることでCLIPを改良しています。
BLIP-2アーキテクチャ: Q-Former
ViT-G/14] LLM[大規模言語モデル
OPT/FlanT5] end subgraph Trainable["学習可能なQ-Former"] QT[学習クエリ
32トークン] SA[セルフアテンション] CA[クロスアテンション
視覚特徴へ] end I2[画像] --> VE2 VE2 --> VF[視覚特徴] VF --> CA QT --> SA SA --> CA CA --> QO[クエリ出力] QO --> FC[線形射影] FC --> LLM T2[テキストプロンプト] --> LLM LLM --> OUT[生成テキスト] style QT fill:#9b59b6,color:white style SA fill:#9b59b6,color:white style CA fill:#9b59b6,color:white
Q-Former: 主要なイノベーション
Q-Former(Querying Transformer)は凍結された画像エンコーダーとLLMを橋渡しします。
- 学習クエリ: 視覚情報を抽出する32個の学習可能なクエリ埋め込み
- クロスアテンション: クエリが凍結ViTからの視覚特徴にアテンド
- セルフアテンション: クエリ同士およびテキストトークンと相互作用
- 効率性: Q-Formerのみを学習し、ビジョンエンコーダーとLLMは凍結
# BLIP-2による画像キャプション生成
from transformers import Blip2Processor, Blip2ForConditionalGeneration
from PIL import Image
import torch
# BLIP-2モデルの読み込み
processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b")
model = Blip2ForConditionalGeneration.from_pretrained(
"Salesforce/blip2-opt-2.7b",
torch_dtype=torch.float16
)
model.to("cuda")
# 画像の処理
image = Image.open("example.jpg")
inputs = processor(image, return_tensors="pt").to("cuda", torch.float16)
# キャプション生成
generated_ids = model.generate(**inputs, max_new_tokens=50)
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"キャプション: {caption}")
BLIP-2による視覚的質問応答
# BLIP-2による視覚的QA
def ask_about_image(image_path, question):
image = Image.open(image_path)
# QA用のプロンプトフォーマット
prompt = f"Question: {question} Answer:"
inputs = processor(image, prompt, return_tensors="pt").to("cuda", torch.float16)
generated_ids = model.generate(
**inputs,
max_new_tokens=30,
num_beams=5,
early_stopping=True
)
answer = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
return answer.strip()
# 使用例
answer = ask_about_image("street_scene.jpg", "この画像には何人いますか?")
print(f"回答: {answer}") # "この画像には5人います"
2.4 LLaVA: 視覚的指示チューニング
LLaVA(Large Language and Vision Assistant)は、シンプルなアーキテクチャでも視覚的指示チューニングにより印象的な結果を達成できることを実証しています。
LLaVAアーキテクチャ
576トークン] end subgraph Projection["射影層"] VT --> MLP[2層MLP
GELU活性化] MLP --> PV[射影された視覚
LLM次元] end subgraph LLM["言語モデル"] PV --> Vicuna[Vicuna/LLaMA] TXT[テキストトークン] --> Vicuna Vicuna --> OUT2[応答] end style MLP fill:#9b59b6,color:white
LLaVAの学習戦略
| 段階 | データ | 学習対象 | 目的 |
|---|---|---|---|
| 事前学習 | 558K画像-キャプションペア | 射影層のみ | 視覚特徴をLLM空間にアライン |
| 指示チューニング | 158K視覚指示 | 全モデル | 視覚的指示に従うことを学習 |
LLaVAの実装
# LLaVAによる視覚的会話
from transformers import LlavaProcessor, LlavaForConditionalGeneration
from PIL import Image
import torch
# LLaVAモデルの読み込み
model_id = "llava-hf/llava-1.5-7b-hf"
processor = LlavaProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto"
)
def chat_with_image(image_path, user_message):
image = Image.open(image_path)
# LLaVA会話フォーマット
conversation = [
{
"role": "user",
"content": [
{"type": "image"},
{"type": "text", "text": user_message}
]
}
]
prompt = processor.apply_chat_template(conversation, add_generation_prompt=True)
inputs = processor(prompt, image, return_tensors="pt").to("cuda")
output = model.generate(**inputs, max_new_tokens=200)
response = processor.decode(output[0], skip_special_tokens=True)
return response
# マルチターン会話
response1 = chat_with_image("kitchen.jpg", "この画像に何が見えますか?")
print(response1)
response2 = chat_with_image("kitchen.jpg", "見える材料で何が作れますか?")
print(response2)
2.5 視覚的トークン化戦略
画像がトークンに変換される方法は、VLMの性能に大きく影響します。
パッチベースのトークン化(ViTスタイル)
# パッチベースの視覚的トークン化
import torch
import torch.nn as nn
class PatchEmbedding(nn.Module):
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
super().__init__()
self.img_size = img_size
self.patch_size = patch_size
self.n_patches = (img_size // patch_size) ** 2 # 224/16で196
# パッチを埋め込み次元に射影
self.proj = nn.Conv2d(
in_channels,
embed_dim,
kernel_size=patch_size,
stride=patch_size
)
# 学習可能な位置埋め込み
self.pos_embed = nn.Parameter(torch.randn(1, self.n_patches + 1, embed_dim))
self.cls_token = nn.Parameter(torch.randn(1, 1, embed_dim))
def forward(self, x):
B = x.shape[0]
# パッチ埋め込みを作成: (B, C, H, W) -> (B, embed_dim, H/P, W/P)
x = self.proj(x)
# フラット化: (B, embed_dim, n_patches^0.5, n_patches^0.5) -> (B, n_patches, embed_dim)
x = x.flatten(2).transpose(1, 2)
# CLSトークンを追加
cls_tokens = self.cls_token.expand(B, -1, -1)
x = torch.cat([cls_tokens, x], dim=1)
# 位置埋め込みを追加
x = x + self.pos_embed
return x # (B, n_patches + 1, embed_dim)
# 例: 224x224画像 -> 197トークン(196パッチ + 1 CLS)
patch_embed = PatchEmbedding()
image = torch.randn(1, 3, 224, 224)
tokens = patch_embed(image)
print(f"視覚トークンの形状: {tokens.shape}") # (1, 197, 768)
トークン化アプローチの比較
| 手法 | トークン数/画像 | 解像度 | 使用モデル |
|---|---|---|---|
| ViT-B/32パッチ | 49 + 1 CLS | 224x224 | CLIP |
| ViT-L/14パッチ | 256 + 1 CLS | 224x224 | LLaVA |
| Q-Formerクエリ | 32(固定) | 任意 | BLIP-2 |
| 高解像度ViT | 576+ | 336x336+ | LLaVA-1.5 |
2.6 VLMにおけるクロスアテンションメカニズム
クロスアテンションにより、一方のモダリティが他方から情報を照会できます。
# クロスアテンションの実装
class CrossAttention(nn.Module):
def __init__(self, dim, num_heads=8):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.scale = self.head_dim ** -0.5
# 一方のモダリティからクエリ、他方からキー/バリュー
self.q_proj = nn.Linear(dim, dim)
self.k_proj = nn.Linear(dim, dim)
self.v_proj = nn.Linear(dim, dim)
self.out_proj = nn.Linear(dim, dim)
def forward(self, query_tokens, kv_tokens):
"""
query_tokens: (B, N_q, D) - 例: テキストトークン
kv_tokens: (B, N_kv, D) - 例: 画像トークン
"""
B, N_q, D = query_tokens.shape
N_kv = kv_tokens.shape[1]
# Q, K, Vに射影
Q = self.q_proj(query_tokens).view(B, N_q, self.num_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(kv_tokens).view(B, N_kv, self.num_heads, self.head_dim).transpose(1, 2)
V = self.v_proj(kv_tokens).view(B, N_kv, self.num_heads, self.head_dim).transpose(1, 2)
# アテンション: テキストクエリが画像キー/バリューにアテンド
attn = (Q @ K.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1)
# バリューを集約
out = (attn @ V).transpose(1, 2).reshape(B, N_q, D)
return self.out_proj(out)
# 例: テキストトークン(10)が画像トークン(196)にアテンド
cross_attn = CrossAttention(dim=768)
text_tokens = torch.randn(1, 10, 768)
image_tokens = torch.randn(1, 196, 768)
attended = cross_attn(text_tokens, image_tokens)
print(f"出力形状: {attended.shape}") # (1, 10, 768)
2.7 統一埋め込み空間
VLMは画像とテキストを直接比較できる共有埋め込み空間を作成します。
# CLIP統一埋め込み空間の可視化
import numpy as np
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
def visualize_clip_space(images, texts, model, processor):
"""画像とテキストがCLIP空間でどのようにクラスタリングされるかを可視化"""
# 画像埋め込みを取得
image_inputs = processor(images=images, return_tensors="pt", padding=True)
with torch.no_grad():
image_embeds = model.get_image_features(**image_inputs)
image_embeds = image_embeds / image_embeds.norm(dim=-1, keepdim=True)
# テキスト埋め込みを取得
text_inputs = processor(text=texts, return_tensors="pt", padding=True)
with torch.no_grad():
text_embeds = model.get_text_features(**text_inputs)
text_embeds = text_embeds / text_embeds.norm(dim=-1, keepdim=True)
# 結合して次元削減
all_embeds = torch.cat([image_embeds, text_embeds], dim=0).numpy()
tsne = TSNE(n_components=2, perplexity=min(30, len(all_embeds)-1))
reduced = tsne.fit_transform(all_embeds)
# プロット
n_images = len(images)
plt.figure(figsize=(10, 8))
plt.scatter(reduced[:n_images, 0], reduced[:n_images, 1], c='blue', label='画像', s=100)
plt.scatter(reduced[n_images:, 0], reduced[n_images:, 1], c='red', label='テキスト', s=100)
# マッチするペア間に線を引く
for i in range(min(n_images, len(texts))):
plt.plot([reduced[i, 0], reduced[n_images+i, 0]],
[reduced[i, 1], reduced[n_images+i, 1]], 'g--', alpha=0.5)
plt.legend()
plt.title("CLIP統一埋め込み空間")
plt.show()
2.8 まとめ
第2章の重要ポイント
- CLIPは大規模なVision-Languageアライメントのための対照学習を開拓
- BLIP-2のQ-Formerは凍結ビジョンエンコーダーとLLMを効率的に橋渡し
- LLaVAはシンプルな射影 + 指示チューニングで強力な結果を達成
- 視覚的トークン化(パッチ、クエリ)が情報フローを決定
- クロスアテンションがモダリティ間の柔軟な情報交換を可能に
- 統一埋め込み空間により画像-テキストの直接比較が可能
演習
演習1: ゼロショット分類
CLIPを使用してカスタムカテゴリ(例:「プロの写真」、「アマチュアの写真」、「AI生成画像」)に画像を分類してください。異なるテキストプロンプトの定式化で結果を比較してください。
演習2: Q-Formerの分析
なぜBLIP-2は画像解像度に関係なく固定数のクエリトークン(32)を使用するのでしょうか?この設計選択のトレードオフを説明してください。
演習3: LLaVA会話
画像についてフォローアップの質問をするマルチターンの視覚的会話を実装してください。以前のターンからのコンテキストが回答にどう影響するでしょうか?
演習4: 埋め込み空間の可視化
異なるカテゴリの画像に対するCLIP埋め込みがどのようにクラスタリングされるかを示す可視化を作成してください。意味的に類似した画像は一緒にクラスタリングされますか?