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の問題点
- 追加のモデルが必要: ノイズ画像に対して動作するClassifierを別途学習する必要がある
- 計算コスト: 各サンプリングステップでClassifierの勾配計算が必要
- 柔軟性の欠如: 事前定義されたクラス以外の条件付けが困難
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を使うには、同一のモデルで
- 条件付き予測 $\bm{\epsilon}_\theta(\bm{x}_t, t, \bm{c})$
- 無条件予測 $\bm{\epsilon}_\theta(\bm{x}_t, t, \varnothing)$
の両方ができる必要があります。
これを実現するため、学習時に一定確率で条件を空(null)に置き換える「条件ドロップ」を行います。
条件ドロップの手順
- 各ミニバッチのサンプルに対して、確率 $p_{\text{uncond}}$(通常10%〜20%)で条件を空に置き換え
- 空の条件は、空文字列のテキスト埋め込みや、学習可能な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は、現代の画像生成モデルにおいて不可欠な技術であり、プロンプトに忠実な高品質画像生成を可能にしています。
次のステップとして、以下の記事も参考にしてください。