Classifier-Free Guidanceの数学的導出と実装

Classifier-Free Guidance(CFG)は、2022年にHo & Salamansが提案した条件付き拡散モデルの生成品質を向上させる手法です。論文 “Classifier-Free Diffusion Guidance” で発表され、Stable Diffusion、DALL-E 2、Imagenなど、現代の画像生成モデルで広く採用されています。

CFGの核心的なアイデアは、条件付き生成と無条件生成の差分を増幅することで、条件(テキストプロンプトなど)により忠実な生成を実現することです。追加のClassifierネットワークを必要とせず、1つのモデルで実現できるため「Classifier-Free」と呼ばれます。

本記事の内容

  • Classifier Guidanceとの違い
  • Classifier-Free Guidanceの数式
  • 学習時の条件ドロップ
  • ガイダンススケールの影響
  • PyTorchでの実装

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

背景:条件付き拡散モデル

無条件生成と条件付き生成

無条件生成では、モデルはデータ分布 $p(\bm{x})$ からサンプリングします。学習データ全体の分布を学習し、その分布に従った画像を生成します。

条件付き生成では、条件 $\bm{c}$(テキストプロンプト、クラスラベル等)が与えられたときの条件付き分布 $p(\bm{x}|\bm{c})$ からサンプリングします。

拡散モデルでは、ノイズ予測ネットワーク $\bm{\epsilon}_\theta$ に条件 $\bm{c}$ を入力することで条件付き生成を実現します。

$$ \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}) $$

問題:条件への忠実さと多様性のトレードオフ

単純に条件を入力するだけでは、生成画像が条件に十分忠実にならないことがあります。特に、

  • 学習データに含まれる条件-画像の対応が曖昧な場合
  • 条件が複雑な場合(長いテキストプロンプトなど)

この問題を解決するのがGuidance手法です。

Classifier Guidance

Classifier-Free Guidanceの前身として、Classifier Guidanceという手法がありました。

アイデア

別途学習したClassifier $p_\phi(y|\bm{x}_t)$(ノイズ画像からクラスを予測)の勾配を使って、生成過程を特定のクラスに向けて誘導します。

スコア関数(対数確率密度の勾配)に対して、

$$ \nabla_{\bm{x}_t} \log p(\bm{x}_t | y) = \nabla_{\bm{x}_t} \log p(\bm{x}_t) + \nabla_{\bm{x}_t} \log p(y | \bm{x}_t) $$

これをガイダンススケール $w$ で増幅します。

$$ \tilde{\nabla}_{\bm{x}_t} \log p(\bm{x}_t | y) = \nabla_{\bm{x}_t} \log p(\bm{x}_t) + w \cdot \nabla_{\bm{x}_t} \log p(y | \bm{x}_t) $$

Classifier Guidanceの問題点

  1. 追加のモデルが必要: ノイズ画像に対して動作するClassifierを別途学習する必要がある
  2. 計算コスト: 各サンプリングステップでClassifierの勾配計算が必要
  3. 柔軟性の欠如: 事前定義されたクラス以外の条件付けが困難

Classifier-Free Guidance

核心的アイデア

Classifier-Free Guidanceは、条件付きモデルと無条件モデルを1つのネットワークで学習し、推論時に両方の予測を組み合わせます。

Classifierの勾配 $\nabla_{\bm{x}_t} \log p(y | \bm{x}_t)$ を、条件付き・無条件のノイズ予測の差で近似します。

数式の導出

ベイズの定理より、

$$ \log p(\bm{c} | \bm{x}_t) = \log p(\bm{x}_t | \bm{c}) – \log p(\bm{x}_t) + \text{const} $$

両辺の $\bm{x}_t$ に関する勾配を取ると、

$$ \nabla_{\bm{x}_t} \log p(\bm{c} | \bm{x}_t) = \nabla_{\bm{x}_t} \log p(\bm{x}_t | \bm{c}) – \nabla_{\bm{x}_t} \log p(\bm{x}_t) $$

これをClassifier Guidanceの式に代入すると、

$$ \begin{align} \tilde{\nabla}_{\bm{x}_t} \log p(\bm{x}_t | \bm{c}) &= \nabla_{\bm{x}_t} \log p(\bm{x}_t) + w \cdot \nabla_{\bm{x}_t} \log p(\bm{c} | \bm{x}_t) \\ &= \nabla_{\bm{x}_t} \log p(\bm{x}_t) + w \cdot \left( \nabla_{\bm{x}_t} \log p(\bm{x}_t | \bm{c}) – \nabla_{\bm{x}_t} \log p(\bm{x}_t) \right) \\ &= (1 – w) \nabla_{\bm{x}_t} \log p(\bm{x}_t) + w \cdot \nabla_{\bm{x}_t} \log p(\bm{x}_t | \bm{c}) \end{align} $$

ノイズ予測への変換

拡散モデルでは、スコア関数とノイズ予測に以下の関係があります。

$$ \nabla_{\bm{x}_t} \log p(\bm{x}_t) \propto -\bm{\epsilon}_\theta(\bm{x}_t, t) $$

したがって、CFGでのノイズ予測は以下のように計算されます。

$$ \begin{equation} \tilde{\bm{\epsilon}}_\theta(\bm{x}_t, t, \bm{c}) = \bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing) + w \cdot \left( \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}) – \bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing) \right) \end{equation} $$

ここで $\varnothing$ は「条件なし」を表す特別なトークン(null条件)です。

式の解釈

$$ \tilde{\bm{\epsilon}} = \underbrace{\bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing)}_{\text{無条件予測}} + w \cdot \underbrace{\left( \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}) – \bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing) \right)}_{\text{条件による差分}} $$

  • 無条件予測: 条件を無視した場合のノイズ予測
  • 条件による差分: 条件があることで生じるノイズ予測の変化
  • ガイダンススケール $w$: 差分の増幅率

$w = 1$ のとき、純粋な条件付き生成(CFGなし)と同じです。

$w > 1$ のとき、条件への忠実さが増しますが、多様性は減少します。

別の表現

式を整理すると、以下のようにも書けます。

$$ \tilde{\bm{\epsilon}}_\theta = (1 – w) \cdot \bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing) + w \cdot \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}) $$

これは、無条件予測と条件付き予測の重み付き線形補間(ただし $w > 1$ の場合は外挿)です。

学習時の条件ドロップ

なぜ条件ドロップが必要か

CFGを使うには、同一のモデルで

  1. 条件付き予測 $\bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c})$
  2. 無条件予測 $\bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing)$

の両方ができる必要があります。

これを実現するため、学習時に一定確率で条件を空(null)に置き換える「条件ドロップ」を行います。

条件ドロップの手順

  1. 各ミニバッチのサンプルに対して、確率 $p_{\text{uncond}}$(通常10%〜20%)で条件を空に置き換え
  2. 空の条件は、空文字列のテキスト埋め込みや、学習可能なnullトークンで表現
# 学習時の条件ドロップ(擬似コード)
def prepare_condition(condition, p_uncond=0.1):
    if random.random() < p_uncond:
        return null_condition  # 空の条件
    else:
        return condition

これにより、モデルは同一のアーキテクチャで条件付き・無条件の両方を学習します。

ガイダンススケールの影響

$w$ の値と生成結果

$w$ の値 効果
$w = 0$ 純粋な無条件生成(条件を完全無視)
$w = 1$ 純粋な条件付き生成(CFGなし)
$w = 3 \sim 7$ バランスの取れた設定(一般的)
$w = 7 \sim 15$ 条件に非常に忠実、多様性低下
$w > 15$ 過度に条件に忠実、アーティファクト発生

品質と多様性のトレードオフ

  • $w$ が大きい: プロンプトに忠実だが、似たような画像ばかり生成される
  • $w$ が小さい: 多様な画像が生成されるが、プロンプトから外れることがある

Stable Diffusionではデフォルトで $w = 7.5$ 程度が使用されることが多いです。

彩度の問題

$w$ を大きくしすぎると、色が過飽和になる問題が知られています。これは、条件付き予測と無条件予測の差を過度に増幅することで、極端な値が生じるためです。

この問題に対処するため、動的しきい値処理(Dynamic Thresholding)などの手法が提案されています。

PyTorchでの実装

CFGを適用したサンプリング

import torch
import torch.nn.functional as F


def sample_with_cfg(
    model,
    scheduler,
    text_embeddings,
    null_embeddings,
    guidance_scale=7.5,
    num_steps=50,
    latent_shape=(1, 4, 64, 64),
    device='cuda',
):
    """
    Classifier-Free Guidanceを使ったサンプリング

    Args:
        model: U-Netモデル
        scheduler: ノイズスケジューラ
        text_embeddings: テキスト条件の埋め込み (B, seq_len, dim)
        null_embeddings: 空条件の埋め込み (B, seq_len, dim)
        guidance_scale: ガイダンススケール w
        num_steps: サンプリングステップ数
        latent_shape: 出力潜在表現の形状
        device: デバイス
    Returns:
        生成された潜在表現
    """
    batch_size = text_embeddings.shape[0]

    # ランダムノイズから開始
    latents = torch.randn(latent_shape, device=device)

    # タイムステップを設定
    scheduler.set_timesteps(num_steps)

    for t in scheduler.timesteps:
        # 条件付きと無条件の両方を計算するため、バッチを2倍に
        latent_input = torch.cat([latents, latents], dim=0)
        t_input = torch.cat([t.unsqueeze(0)] * 2 * batch_size, dim=0)

        # 条件埋め込みも結合(無条件, 条件付き)
        context = torch.cat([null_embeddings, text_embeddings], dim=0)

        # ノイズ予測
        with torch.no_grad():
            noise_pred = model(latent_input, t_input, context)

        # 予測を分割
        noise_uncond, noise_cond = noise_pred.chunk(2)

        # Classifier-Free Guidance
        noise_pred_cfg = noise_uncond + guidance_scale * (noise_cond - noise_uncond)

        # 1ステップのデノイジング
        latents = scheduler.step(noise_pred_cfg, t, latents)

    return latents

効率的な実装(バッチ結合版)

上記の実装では、各ステップで2回のモデル呼び出しが必要です。バッチを結合することで、1回の呼び出しで済みます。

def sample_with_cfg_efficient(
    model,
    scheduler,
    prompts,
    text_encoder,
    guidance_scale=7.5,
    num_steps=50,
):
    """効率的なCFGサンプリング"""
    batch_size = len(prompts)

    # テキストエンコード
    text_embeddings = text_encoder(prompts)

    # 空のプロンプトをエンコード
    null_prompts = [""] * batch_size
    null_embeddings = text_encoder(null_prompts)

    # 埋め込みを結合(推論時に1回のforward passで計算)
    # 順序: [null_1, null_2, ..., cond_1, cond_2, ...]
    combined_embeddings = torch.cat([null_embeddings, text_embeddings], dim=0)

    # 潜在表現を初期化
    latents = torch.randn(batch_size, 4, 64, 64)

    scheduler.set_timesteps(num_steps)

    for t in scheduler.timesteps:
        # バッチを2倍にして結合
        latent_doubled = torch.cat([latents, latents], dim=0)
        t_batch = t.expand(batch_size * 2)

        # 1回のforward pass
        noise_pred_combined = model(latent_doubled, t_batch, combined_embeddings)

        # 分割
        noise_uncond, noise_cond = noise_pred_combined.chunk(2)

        # CFG適用
        noise_pred = noise_uncond + guidance_scale * (noise_cond - noise_uncond)

        # デノイジング
        latents = scheduler.step(noise_pred, t, latents)

    return latents

ガイダンススケールの動的調整

より高度な実装では、サンプリングの進行に応じてガイダンススケールを変化させることもあります。

def get_dynamic_guidance_scale(t, t_max, min_scale=1.0, max_scale=7.5):
    """
    タイムステップに応じてガイダンススケールを調整

    初期ステップ(ノイズが多い)では低く、
    後半ステップ(詳細を決める)では高く
    """
    progress = 1.0 - (t / t_max)  # 0 -> 1
    return min_scale + (max_scale - min_scale) * progress

Negative Prompting

CFGの自然な拡張として、Negative Prompt(ネガティブプロンプト)があります。

アイデア

無条件予測 $\bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing)$ の代わりに、「避けたい要素」を記述したネガティブプロンプトの予測を使用します。

$$ \tilde{\bm{\epsilon}} = \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}_{\text{neg}}) + w \cdot \left( \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}_{\text{pos}}) – \bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c}_{\text{neg}}) \right) $$

これにより、「blurry, low quality」などを避け、「high quality, detailed」に近づけることができます。

実装

def sample_with_negative_prompt(
    model,
    scheduler,
    positive_embeddings,
    negative_embeddings,
    guidance_scale=7.5,
    num_steps=50,
    latent_shape=(1, 4, 64, 64),
):
    """ネガティブプロンプトを使ったサンプリング"""
    latents = torch.randn(latent_shape)

    scheduler.set_timesteps(num_steps)

    for t in scheduler.timesteps:
        latent_doubled = torch.cat([latents, latents], dim=0)
        t_batch = t.expand(2)

        # ネガティブとポジティブの埋め込み
        context = torch.cat([negative_embeddings, positive_embeddings], dim=0)

        noise_pred = model(latent_doubled, t_batch, context)
        noise_neg, noise_pos = noise_pred.chunk(2)

        # CFG(ネガティブ方向から離れ、ポジティブ方向へ)
        noise_pred_cfg = noise_neg + guidance_scale * (noise_pos - noise_neg)

        latents = scheduler.step(noise_pred_cfg, t, latents)

    return latents

CFGの数学的解釈

分布のシャープ化

CFGは実質的に、条件付き分布を「シャープ化」(尖らせる)操作を行っています。

$w > 1$ のとき、生成分布は以下のように解釈できます。

$$ \tilde{p}(\bm{x} | \bm{c}) \propto \frac{p(\bm{x} | \bm{c})^w}{p(\bm{x})^{w-1}} $$

これは温度スケーリングに似た効果を持ち、条件付き分布のモードをより強調します。

条件付き生成の強化

直感的には、CFGは「条件があるときと無いときの違い」を誇張することで、条件の影響を強めています。

まとめ

本記事では、Classifier-Free Guidance(CFG)の理論を解説しました。

  • Classifier Guidanceとの違い: 追加のClassifierなしで、1つのモデルで実現
  • 核心的アイデア: 条件付き予測と無条件予測の差分を増幅
  • 数式: $\tilde{\bm{\epsilon}} = \bm{\epsilon}_\theta(\varnothing) + w \cdot (\bm{\epsilon}_\theta(\bm{c}) – \bm{\epsilon}_\theta(\varnothing))$
  • 条件ドロップ: 学習時に一定確率で条件を空にすることで、両方の予測能力を獲得
  • ガイダンススケール: $w$ が大きいほど条件に忠実、小さいほど多様
  • Negative Prompt: 空条件の代わりに「避けたい要素」を指定

CFGは、現代の画像生成モデルにおいて不可欠な技術であり、プロンプトに忠実な高品質画像生成を可能にしています。

次のステップとして、以下の記事も参考にしてください。