4.1 マルチモーダルモデルのための対照学習
対照学習は現代のVision-Languageモデルの基盤です。マッチするペア間の類似度を最大化し、マッチしないペアとの類似度を最小化することで表現を学習します。
InfoNCE損失
対照損失の式
N個の画像-テキストペア \((v_i, t_i)\) のバッチに対して:
$$\mathcal{L} = -\frac{1}{2N}\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)} + \log\frac{\exp(\text{sim}(t_i, v_i)/\tau)}{\sum_{j=1}^{N}\exp(\text{sim}(t_i, v_j)/\tau)}\right]$$
ここで \(\tau\) は学習可能な温度パラメータ(通常0.07)です。
# 対照損失の実装
import torch
import torch.nn as nn
import torch.nn.functional as F
class ContrastiveLoss(nn.Module):
def __init__(self, temperature=0.07):
super().__init__()
self.temperature = nn.Parameter(torch.tensor(temperature))
def forward(self, image_embeds, text_embeds):
"""
image_embeds: (N, D) 正規化された画像埋め込み
text_embeds: (N, D) 正規化されたテキスト埋め込み
"""
# 類似度行列を計算
logits = image_embeds @ text_embeds.T / self.temperature.exp()
# ラベル: 対角要素がポジティブペア
labels = torch.arange(len(image_embeds), device=logits.device)
# 両方向でクロスエントロピー損失
loss_i2t = F.cross_entropy(logits, labels)
loss_t2i = F.cross_entropy(logits.T, labels)
return (loss_i2t + loss_t2i) / 2
# 訓練ループの例
def train_step(model, images, texts, optimizer, criterion):
optimizer.zero_grad()
# 正規化された埋め込みを取得
image_embeds = F.normalize(model.encode_image(images), dim=-1)
text_embeds = F.normalize(model.encode_text(texts), dim=-1)
# 損失を計算
loss = criterion(image_embeds, text_embeds)
loss.backward()
optimizer.step()
return loss.item()
大きなバッチサイズ: なぜ重要か
対照学習は大きなバッチサイズから大きな恩恵を受けます。
| バッチサイズ | ネガティブサンプル数 | 効果 |
|---|---|---|
| 256 | サンプルあたり255 | 弱い識別 |
| 4,096 | サンプルあたり4,095 | より良い表現 |
| 32,768 | サンプルあたり32,767 | CLIPレベルの品質 |
# 効果的な大バッチのための勾配累積
def train_with_accumulation(model, dataloader, optimizer, criterion,
accumulation_steps=8):
"""勾配累積で大バッチをシミュレート"""
model.train()
optimizer.zero_grad()
for i, (images, texts) in enumerate(dataloader):
# フォワードパス
image_embeds = F.normalize(model.encode_image(images), dim=-1)
text_embeds = F.normalize(model.encode_text(texts), dim=-1)
# 累積ステップで損失をスケーリング
loss = criterion(image_embeds, text_embeds) / accumulation_steps
loss.backward()
# 累積ステップごとに重みを更新
if (i + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad()
return loss.item() * accumulation_steps
4.2 事前学習目標
現代のマルチモーダルモデルは複数の事前学習目標を組み合わせます。
BLIPスタイルのマルチ目標訓練
# マルチ目標事前学習
class MultimodalPretraining(nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
self.itc_loss = ContrastiveLoss()
self.itm_head = nn.Linear(model.hidden_dim, 2) # 二値分類
self.lm_head = nn.Linear(model.hidden_dim, model.vocab_size)
def forward(self, images, texts, text_labels):
# 1. 画像-テキスト対照(ITC)
image_embeds = self.model.encode_image(images)
text_embeds = self.model.encode_text(texts)
itc_loss = self.itc_loss(
F.normalize(image_embeds, dim=-1),
F.normalize(text_embeds, dim=-1)
)
# 2. ハードネガティブを含む画像-テキストマッチング(ITM)
# シャッフルでネガティブペアを作成
neg_images = images[torch.randperm(len(images))]
pos_hidden = self.model.fuse(images, texts)
neg_hidden = self.model.fuse(neg_images, texts)
pos_logits = self.itm_head(pos_hidden[:, 0]) # CLSトークン
neg_logits = self.itm_head(neg_hidden[:, 0])
itm_labels = torch.cat([
torch.ones(len(images)),
torch.zeros(len(images))
]).long().to(images.device)
itm_loss = F.cross_entropy(
torch.cat([pos_logits, neg_logits]),
itm_labels
)
# 3. 言語モデリング(画像で条件付け)
lm_hidden = self.model.generate_hidden(images, texts[:, :-1])
lm_logits = self.lm_head(lm_hidden)
lm_loss = F.cross_entropy(
lm_logits.view(-1, self.model.vocab_size),
text_labels[:, 1:].contiguous().view(-1),
ignore_index=-100
)
# 損失を結合
total_loss = itc_loss + itm_loss + lm_loss
return total_loss, {"itc": itc_loss, "itm": itm_loss, "lm": lm_loss}
4.3 マルチモーダル事前学習のためのデータセット
| データセット | サイズ | ソース | 用途 |
|---|---|---|---|
| LAION-5B | 58.5億ペア | Webクロール | 大規模事前学習 |
| LAION-400M | 4億ペア | フィルタリングサブセット | 研究、ファインチューニング |
| CC3M/CC12M | 300万-1200万ペア | Conceptual Captions | 高品質訓練 |
| COCO Captions | 33万画像 | 人間によるアノテーション | 評価、ファインチューニング |
| Visual Genome | 10.8万画像 | 密なアノテーション | シーン理解 |
4.4 マルチモーダルハルシネーション
ハルシネーションは、モデルが視覚コンテンツと矛盾するテキストを生成する重要な課題です。
マルチモーダルハルシネーションの種類
- オブジェクトハルシネーション: 画像にないオブジェクトを記述
- 属性ハルシネーション: 間違った色、サイズ、プロパティ
- 関係ハルシネーション: 間違った空間的または意味的関係
- シーンハルシネーション: 全体的なコンテキストの誤解釈
根本原因
- 言語事前知識の優位性: モデルが学習した言語パターンに過度に依存
- 共起バイアス: 訓練データで一緒に見られることが多いオブジェクト
- 弱い視覚的グラウンディング: テキストと画像領域間の接続が不十分
- 訓練データのノイズ: Webスクレイピングデータでの画像-テキストのミスアライメント
検出と緩和
# CLIPを使ったハルシネーション検出
import torch
from transformers import CLIPProcessor, CLIPModel
class HallucinationDetector:
def __init__(self, threshold=0.3):
self.model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
self.threshold = threshold
def check_claim(self, image, claim):
"""
画像についての主張がハルシネーションかどうかを確認。
主張が画像によってサポートされればTrue、ハルシネーションの可能性があればFalseを返す。
"""
# 画像と主張をエンコード
inputs = self.processor(
text=[claim, f"not {claim}"],
images=image,
return_tensors="pt",
padding=True
)
with torch.no_grad():
outputs = self.model(**inputs)
probs = outputs.logits_per_image.softmax(dim=1)
# 主張の高い確率 = おそらく真
claim_prob = probs[0, 0].item()
return claim_prob > self.threshold, claim_prob
def analyze_caption(self, image, caption):
"""キャプションを分解して各主張を確認"""
# 単純な文分割(本番環境ではNLPライブラリを使用)
claims = caption.replace(",", ".").split(".")
claims = [c.strip() for c in claims if c.strip()]
results = []
for claim in claims:
is_valid, confidence = self.check_claim(image, claim)
results.append({
"claim": claim,
"valid": is_valid,
"confidence": confidence
})
return results
# 使用例
detector = HallucinationDetector()
results = detector.analyze_caption(image, generated_caption)
for r in results:
status = "有効" if r["valid"] else "ハルシネーション?"
print(f"{status} ({r['confidence']:.2f}): {r['claim']}")
緩和戦略
| 戦略 | 段階 | アプローチ |
|---|---|---|
| 対照デコーディング | 推論 | VLMロジットからLLMのみのロジットを減算 |
| ハルシネーションペナルティ付きRLHF | 訓練 | 報酬モデルがハルシネーションにペナルティ |
| 視覚的グラウンディング損失 | 訓練 | 明示的な領域-単語アライメント |
| Chain-of-Thought | 推論 | ステップバイステップの視覚的推論 |
4.5 評価ベンチマーク
理解ベンチマーク
| ベンチマーク | タスク | メトリクス | SOTA(2025) |
|---|---|---|---|
| VQAv2 | 視覚的QA | 精度 | 〜87% |
| GQA | 構成的QA | 精度 | 〜75% |
| MMMU | マルチモーダル推論 | 精度 | 〜84%(GPT-5.1) |
| TextVQA | 画像内テキスト | 精度 | 〜82% |
| POPE | ハルシネーション | F1スコア | 〜90% |
生成ベンチマーク
| ベンチマーク | タスク | メトリクス |
|---|---|---|
| GenEval | テキストから画像のセマンティクス | 精度 |
| DPG-Bench | プロンプトフォロー | アライメントスコア |
| FID | 画像品質 | Frechet Inception Distance |
| CLIPスコア | テキスト-画像アライメント | コサイン類似度 |
# 生成画像のCLIPスコアを計算
from torchmetrics.multimodal import CLIPScore
def evaluate_generation(generated_images, prompts):
"""テキストから画像生成のCLIPスコアを計算"""
metric = CLIPScore(model_name_or_path="openai/clip-vit-base-patch16")
scores = []
for image, prompt in zip(generated_images, prompts):
# 画像はuint8テンソル(B, C, H, W)である必要がある
if isinstance(image, Image.Image):
image = transforms.ToTensor()(image).unsqueeze(0) * 255
image = image.to(torch.uint8)
score = metric(image, prompt)
scores.append(score.item())
return {
"mean_clip_score": sum(scores) / len(scores),
"individual_scores": scores
}
4.6 マルチモーダルモデルのファインチューニング
# Vision-LanguageモデルのLoRAファインチューニング
from peft import LoraConfig, get_peft_model
from transformers import LlavaForConditionalGeneration
def setup_lora_finetuning(model_name, target_modules=None):
"""効率的なVLMファインチューニングのためのLoRAを設定"""
# ベースモデルの読み込み
model = LlavaForConditionalGeneration.from_pretrained(
model_name,
torch_dtype=torch.float16
)
# LLaVAのデフォルトターゲットモジュール
if target_modules is None:
target_modules = [
"q_proj", "v_proj", # アテンション
"mm_projector" # Vision-Languageコネクタ
]
# LoRA設定
lora_config = LoraConfig(
r=16, # ランク
lora_alpha=32, # スケーリング
target_modules=target_modules,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# LoRAを適用
model = get_peft_model(model, lora_config)
# 学習可能なパラメータを表示
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"学習可能: {trainable:,} / {total:,} ({100*trainable/total:.2f}%)")
return model
# 訓練設定
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./llava-finetuned",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
fp16=True,
logging_steps=10,
save_strategy="epoch"
)
4.7 まとめ
第4章の重要ポイント
- 対照学習とInfoNCE損失がアラインされた埋め込みを作成
- 大バッチサイズ(32K+)が品質の高い表現に重要
- マルチ目標訓練(ITC + ITM + LM)が多機能モデルを生む
- ハルシネーションは複数の緩和アプローチを持つ主要な課題
- ベンチマーク(MMMU、VQAv2、GenEval)が異なる能力を測定
- LoRAファインチューニングが最小のパラメータで効率的な適応を可能に
演習
演習1: 対照損失の実装
対称的な対照損失を実装し、マッチするペアとマッチしないペアに対して正しい勾配を生成することを確認してください。
演習2: ハルシネーション分析
異なるVLMを使って10枚の画像のキャプションを生成してください。ハルシネーションを手動でアノテーションし、モデルごとのハルシネーション率を計算してください。
演習3: ベンチマーク評価
VQAv2の検証セット(サブセット)でVLMを実行してください。異なる質問タイプ(yes/no、カウンティング、その他)での精度を比較してください。
演習4: LoRAファインチューニング
LoRAを使ってカスタムドメイン(例:医療画像、商品写真)でVLMをファインチューニングしてください。ファインチューニング前後の性能を測定してください。