EN Last sync: 2026-01-15

第4章: 訓練と評価

対照学習、事前学習、ハルシネーション、ベンチマーク

読了時間: 30-35分 コード例: 8 演習: 4

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 事前学習目標

現代のマルチモーダルモデルは複数の事前学習目標を組み合わせます。

graph TB subgraph Objectives["事前学習目標"] ITC[画像-テキスト対照] ITM[画像-テキストマッチング] LM[言語モデリング] MIM[マスク画像モデリング] end subgraph Purpose ITC --> P1[粗いアライメント] ITM --> P2[細かいアライメント] LM --> P3[生成能力] MIM --> P4[視覚理解] end style ITC fill:#e3f2fd style ITM fill:#fff3e0 style LM fill:#f3e5f5 style MIM fill:#e8f5e9

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 マルチモーダルハルシネーション

ハルシネーションは、モデルが視覚コンテンツと矛盾するテキストを生成する重要な課題です。

マルチモーダルハルシネーションの種類

根本原因

  1. 言語事前知識の優位性: モデルが学習した言語パターンに過度に依存
  2. 共起バイアス: 訓練データで一緒に見られることが多いオブジェクト
  3. 弱い視覚的グラウンディング: テキストと画像領域間の接続が不十分
  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章の重要ポイント

演習

演習1: 対照損失の実装

対称的な対照損失を実装し、マッチするペアとマッチしないペアに対して正しい勾配を生成することを確認してください。

演習2: ハルシネーション分析

異なるVLMを使って10枚の画像のキャプションを生成してください。ハルシネーションを手動でアノテーションし、モデルごとのハルシネーション率を計算してください。

演習3: ベンチマーク評価

VQAv2の検証セット(サブセット)でVLMを実行してください。異なる質問タイプ(yes/no、カウンティング、その他)での精度を比較してください。

演習4: LoRAファインチューニング

LoRAを使ってカスタムドメイン(例:医療画像、商品写真)でVLMをファインチューニングしてください。ファインチューニング前後の性能を測定してください。