はじめに
2022年にStable Diffusionが公開されると、AI画像生成は大衆化の時代を迎えました。しかし「なぜノイズから画像が生まれるのか?」という問いに本当に答えられる人は多くありません。
このガイドでは**GANからConsistency Modelsまで**の生成モデルの系譜を整理し、DDPMの数学、Stable Diffusionの内部構造、ControlNet、LoRAファインチューニング、そしてSoraのような動画生成モデルまで完全に解剖します。
1. 生成モデルの系譜: GAN → VAE → Flow → Diffusion → Consistency
1.1 GAN (Generative Adversarial Network, 2014)
Ian Goodfellowが提案したGANは、**生成器(Generator)**と**識別器(Discriminator)**の敵対的ゲームで学習します。
- **長所**: 高品質な画像生成、高速サンプリング
- **短所**: 学習不安定(モード崩壊)、多様性の欠如
基本的なGAN構造
class Generator(nn.Module):
def __init__(self, latent_dim=100, img_size=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, img_size * img_size * 3),
nn.Tanh()
)
def forward(self, z):
return self.net(z).view(-1, 3, 64, 64)
1.2 VAE (Variational Autoencoder, 2013)
VAEはエンコーダが潜在空間の**分布**を学習し、デコーダがその分布からサンプリングして画像を復元します。
損失関数: $\mathcal{L} = \mathbb{E}[\log p(x|z)] - D_{KL}(q(z|x) \| p(z))$
- **長所**: 潜在空間が解釈可能、学習が安定
- **短所**: GANと比較してサンプル品質がぼやける
1.3 Normalizing Flow (2015~)
Flowモデルは**可逆変換(invertible transformation)**を積み重ねて単純な分布を複雑な分布に変換します。
$p(x) = p(z) \left|\det \frac{\partial f^{-1}}{\partial x}\right|$
- **長所**: 正確なlikelihood計算が可能
- **短所**: ネットワーク構造の制約(可逆性)、メモリ非効率
1.4 拡散モデル (Diffusion Models, 2020~)
拡散モデルはデータに**段階的にノイズを追加**し、その逆過程を学習します。Score matchingとSDE理論が結合した現在最高水準の生成モデルです。
1.5 Consistency Models (2023)
Consistency Modelsは拡散モデルの遅いサンプリング問題を解決します。任意のノイズレベルから**直接元のデータへマッピング**する一貫性関数を学習します。
2. 拡散モデルの数学: DDPM、Score Matching、SDE
2.1 DDPMの順方向過程
DDPM(Denoising Diffusion Probabilistic Models)の順方向過程は、元のデータ $x_0$ にTステップかけてガウスノイズを追加します。
$q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t} x_{t-1}, \beta_t I)$
これを累積すると、任意のタイムステップtで直接サンプリングできます:
$q(x_t | x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t} x_0, (1-\bar{\alpha}_t) I)$
ここで $\bar{\alpha}_t = \prod_{s=1}^{t}(1-\beta_s)$ です。
class DDPMScheduler:
def __init__(self, num_timesteps=1000, beta_start=1e-4, beta_end=0.02):
self.T = num_timesteps
線形ノイズスケジュール
self.betas = torch.linspace(beta_start, beta_end, num_timesteps)
self.alphas = 1.0 - self.betas
self.alpha_bar = torch.cumprod(self.alphas, dim=0)
def add_noise(self, x0, noise, t):
"""reparameterization trickでtステップのノイズをx0に追加"""
sqrt_alpha_bar = self.alpha_bar[t] ** 0.5
sqrt_one_minus = (1 - self.alpha_bar[t]) ** 0.5
ブロードキャスト用のshape調整
sqrt_alpha_bar = sqrt_alpha_bar.view(-1, 1, 1, 1)
sqrt_one_minus = sqrt_one_minus.view(-1, 1, 1, 1)
return sqrt_alpha_bar * x0 + sqrt_one_minus * noise
2.2 DDPMの逆方向過程
逆方向過程ではニューラルネットワークが各ステップのノイズを予測します:
$p_\theta(x_{t-1} | x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \sigma_t^2 I)$
学習目標は追加されたノイズと予測されたノイズ間のMSE:
$\mathcal{L} = \mathbb{E}_{t, x_0, \epsilon}\left[\|\epsilon - \epsilon_\theta(\sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t}\epsilon, t)\|^2\right]$
def ddpm_training_step(model, scheduler, x0, optimizer):
batch_size = x0.shape[0]
ランダムタイムステップのサンプリング
t = torch.randint(0, scheduler.T, (batch_size,))
ガウスノイズのサンプリング
noise = torch.randn_like(x0)
ノイズ追加(順方向過程)
xt = scheduler.add_noise(x0, noise, t)
ノイズ予測
predicted_noise = model(xt, t)
MSE損失
loss = F.mse_loss(predicted_noise, noise)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
2.3 Score Matchingの視点
スコア関数はデータ分布の**対数確率の勾配**です:
$s_\theta(x) = \nabla_x \log p_\theta(x)$
拡散モデルのノイズ予測は、スコア関数を学習することと同値です:
$\epsilon_\theta(x_t, t) \approx -\sqrt{1-\bar{\alpha}_t} \cdot \nabla_{x_t} \log q(x_t)$
2.4 SDEの視点 (Stochastic Differential Equation)
Song YangのSDEフレームワークは拡散を連続時間に一般化します。
順方向SDE: $dx = f(x,t)dt + g(t)dW$
逆方向SDE: $dx = [f(x,t) - g(t)^2 \nabla_x \log p_t(x)]dt + g(t)d\bar{W}$
このフレームワークでDDPM、SMLD(NCSN)、ODEサンプラーを統一された視点で理解できます。
3. Stable Diffusionの内部構造
3.1 全体アーキテクチャ
Stable Diffusionは3つの核心コンポーネントで構成されます:
1. **VAE (Variational Autoencoder)**: ピクセル空間 ↔ 潜在空間の変換
2. **U-Net**: 潜在空間でのノイズ予測
3. **CLIPテキストエンコーダー**: テキストプロンプトを埋め込みに変換
from diffusers import StableDiffusionPipeline
基本パイプラインの使用
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
).to("cuda")
画像生成
image = pipe(
prompt="a serene mountain landscape at sunset, photorealistic",
negative_prompt="blurry, low quality, distorted",
num_inference_steps=30,
guidance_scale=7.5,
width=512,
height=512
).images[0]
image.save("output.png")
3.2 なぜ潜在空間(Latent Space)なのか?
ピクセル空間で直接Diffusionを行うと512x512x3 = **786,432次元**を扱う必要があります。SDのVAEはこれを64x64x4 = **16,384次元**に圧縮します。
- 計算コスト: 約48分の1に削減
- 品質損失: VAEのperceptual lossにより最小化
VAE潜在空間の可視化
from diffusers import AutoencoderKL
from PIL import Image
vae = AutoencoderKL.from_pretrained("stabilityai/sd-vae-ft-mse")
vae = vae.to("cuda").eval()
transform = T.Compose([T.Resize((512, 512)), T.ToTensor(),
T.Normalize([0.5], [0.5])])
img = transform(Image.open("input.png")).unsqueeze(0).to("cuda")
with torch.no_grad():
ピクセル → 潜在(エンコード)
latent = vae.encode(img).latent_dist.sample()
latent = latent * vae.config.scaling_factor
print(f"潜在空間のサイズ: {latent.shape}") # [1, 4, 64, 64]
3.3 CLIPテキストエンコーダー
CLIPは画像-テキストペアで学習されたモデルです。SDではテキストエンコーダーのみを使用し、プロンプトを77トークン × 768次元の埋め込みに変換します。
from transformers import CLIPTextModel, CLIPTokenizer
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
text_encoder = CLIPTextModel.from_pretrained("openai/clip-vit-large-patch14")
prompt = "a fantasy castle in the clouds"
tokens = tokenizer(prompt, padding="max_length", max_length=77,
return_tensors="pt")
with torch.no_grad():
text_emb = text_encoder(tokens.input_ids)[0]
print(f"テキスト埋め込みサイズ: {text_emb.shape}") # [1, 77, 768]
3.4 CFG (Classifier-Free Guidance)
CFGは条件付き生成の強度を調整します。guidance_scaleが高いほどプロンプトに忠実になり、低いほど多様性が増します。
$\epsilon_{guided} = \epsilon_{uncond} + w \cdot (\epsilon_{cond} - \epsilon_{uncond})$
4. LoRAとDreamBooth ファインチューニング
4.1 LoRAの原理
全体の重み行列 $W \in \mathbb{R}^{d \times k}$ を直接更新する代わりに、変化量を2つの低ランク行列の積で表します:
$W' = W + \Delta W = W + BA$
ここで $B \in \mathbb{R}^{d \times r}$、$A \in \mathbb{R}^{r \times k}$、$r \ll \min(d, k)$ です。
一般的にr=4〜16で、全パラメータ対比**0.1〜1%のみを学習**します。
from diffusers import StableDiffusionPipeline
from peft import LoraConfig, get_peft_model
LoRA設定
lora_config = LoraConfig(
r=16, # ランク
lora_alpha=32, # スケーリングパラメータ
target_modules=["to_q", "to_v", "to_k", "to_out.0"],
lora_dropout=0.05,
bias="none",
)
モデルにLoRAを適用
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5"
)
unet_lora = get_peft_model(pipe.unet, lora_config)
unet_lora.print_trainable_parameters()
学習可能パラメータ: 約3M / 全体: 約860M (約0.3%)
4.2 DreamBooth ファインチューニング
DreamBoothは3〜10枚の画像で特定のオブジェクトを学習します。レアトークン(例: "sks")をオブジェクトの識別子として使用します。
from diffusers import DiffusionPipeline
DreamBooth学習済みモデルのロード
pipe = DiffusionPipeline.from_pretrained(
"./dreambooth-sks-dog", # 学習済みチェックポイント
torch_dtype=torch.float16
).to("cuda")
"sks dog"で特定の犬を生成
images = pipe(
"a photo of sks dog in front of the Eiffel Tower",
num_inference_steps=50,
guidance_scale=7.5
).images
5. ControlNetとIP-Adapter
5.1 ControlNetのアーキテクチャ
ControlNetはU-Netのエンコーダー部分をコピーして別の制御ネットワークを作り、**ゼロ畳み込み(zero convolution)**でSDの元の重みを保護します。
サポートされているコンディショニング:
- **深度マップ(Depth map)**: 空間的な深度情報
- **Cannyエッジ**: 輪郭線の保持
- **OpenPose**: 人体姿勢の制御
- **スクリブル(Scribble)**: ラフスケッチから詳細な画像へ
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
from diffusers.utils import load_image
ControlNetモデルのロード(Cannyエッジ)
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny",
torch_dtype=torch.float16
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16
).to("cuda")
Cannyエッジの抽出
image = load_image("input.png")
image_np = np.array(image)
low_threshold, high_threshold = 100, 200
canny_image = cv2.Canny(image_np, low_threshold, high_threshold)
canny_image = canny_image[:, :, None]
canny_image = np.concatenate([canny_image] * 3, axis=2)
ControlNet推論
result = pipe(
prompt="a beautiful landscape, detailed, 8k",
image=canny_image,
num_inference_steps=30,
controlnet_conditioning_scale=1.0,
).images[0]
5.2 IP-AdapterとInstantID
IP-Adapterは参照画像のスタイルや内容をテキストプロンプトと共に条件として使用します。
InstantIDは1枚の顔写真から一貫したIDを維持しながら多様なスタイルを生成します。ControlNet(姿勢制御)とIP-Adapter(顔の特徴)を組み合わせた構造です。
6. 高度な画像編集: InstructPix2Pix
InstructPix2Pixはテキスト命令で画像を編集します。「馬をシマウマに変えて」のような命令を理解します。
from diffusers import StableDiffusionInstructPix2PixPipeline
from diffusers.utils import load_image
pipe = StableDiffusionInstructPix2PixPipeline.from_pretrained(
"timbrooks/instruct-pix2pix",
torch_dtype=torch.float16,
safety_checker=None
).to("cuda")
image = load_image("horse.png")
result = pipe(
"turn the horse into a zebra",
image=image,
num_inference_steps=30,
image_guidance_scale=1.5, # 元の画像への忠実度
guidance_scale=7.5 # テキスト指示の強度
).images[0]
7. 動画生成: Sora、CogVideoX
7.1 Soraの技術的革新
OpenAIのSoraは**Video Diffusion Transformer**構造で、動画を「時空間パッチ(spacetime patches)」のシーケンスとして処理します。主要な革新:
1. **空間-時間アテンション**: 空間と時間次元の同時アテンション
2. **可変解像度学習**: 多様な解像度とフレームレートでの学習
3. **再キャプション(Recaptioning)**: 動画キャプション品質の向上
7.2 時間的一貫性の維持
動画生成の最大の課題は**時間的一貫性(temporal consistency)**です。
- **モーション事前分布(Motion prior)**: 自然な動きの分布の学習
- **クロスフレームアテンション**: フレーム間での特徴の共有
- **オプティカルフロー誘導**: 光学的フローによる動きの制御
from diffusers import CogVideoXPipeline
pipe = CogVideoXPipeline.from_pretrained(
"THUDM/CogVideoX-5b",
torch_dtype=torch.bfloat16
).to("cuda")
video = pipe(
prompt="A serene lake with rippling water, birds flying overhead",
num_videos_per_prompt=1,
num_inference_steps=50,
num_frames=49,
guidance_scale=6,
).frames[0]
8. 音楽・オーディオ生成
8.1 MusicGen (Meta)
MusicGenはテキストから音楽を生成する言語モデルベースのシステムです。
from audiocraft.models import MusicGen
model = MusicGen.get_pretrained("facebook/musicgen-large")
model.set_generation_params(duration=30) # 30秒生成
descriptions = ["happy jazz piano with upbeat rhythm"]
wav = model.generate(descriptions)
torchaudio.save("music.wav", wav[0].cpu(), sample_rate=32000)
8.2 AudioLMのアーキテクチャ
GoogleのAudioLMは階層的なトークン化を使用します:
- **セマンティックトークン** (w2v-BERT): 意味情報
- **粗い音響トークン** (SoundStream): 大まかな音響
- **細かい音響トークン** (SoundStream): 詳細な音響
8.3 VALL-E 音声合成
MicrosoftのVALL-Eは3秒の音声サンプルだけで話者の声を複製します。言語モデルのようにトークンを自己回帰的に生成します。
9. プロダクション展開
9.1 diffusersライブラリの最適化
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
)
メモリ最適化
pipe.enable_attention_slicing() # アテンションスライシング
pipe.enable_vae_slicing() # VAEスライシング
pipe.enable_model_cpu_offload() # CPUオフロード
xformersアクセラレーション(インストール済みの場合)
try:
pipe.enable_xformers_memory_efficient_attention()
print("xformers有効")
except:
print("xformersなし、デフォルトアテンション使用")
9.2 ComfyUI API連携
def queue_prompt(prompt_workflow, server_address="127.0.0.1:8188"):
"""ComfyUI APIでワークフローを実行"""
p = {"prompt": prompt_workflow}
data = json.dumps(p).encode("utf-8")
req = urllib.request.Request(
f"http://{server_address}/prompt",
data=data,
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req) as response:
return json.loads(response.read())
ComfyUIワークフロー(JSON形式)
workflow = {
"1": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "v1-5-pruned-emaonly.ckpt"}
},
"2": {
"class_type": "CLIPTextEncode",
"inputs": {
"text": "a beautiful sunset over mountains",
"clip": ["1", 1]
}
},
"3": {
"class_type": "KSampler",
"inputs": {
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["4", 0],
"latent_image": ["5", 0],
"seed": 42,
"steps": 30,
"cfg": 7.5,
"sampler_name": "euler",
"scheduler": "karras",
"denoise": 1.0
}
}
}
result = queue_prompt(workflow)
print(f"プロンプトID: {result['prompt_id']}")
9.3 ONNX/TensorRT最適化
from diffusers import OnnxStableDiffusionPipeline
ONNXランタイムで推論(CPU/GPUどちらも可能)
pipe = OnnxStableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
revision="onnx",
provider="CUDAExecutionProvider",
)
image = pipe("a mountain lake at dawn").images[0]
10. クイズ: 生成AI・拡散モデルの理解確認
**答え**: 中心極限定理とガウス分布の再生性
**解説**: ガウスノイズを使用する理由は3つあります。第一に、ガウス分布は加法に対して閉じています(2つのガウスの和もガウス)。第二に、reparameterization trickが可能で、任意のタイムステップtで直接サンプリングできます: $x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon$。第三に、中心極限定理によりTが無限大に近づくと、どんな分布も標準ガウスに収束するため、前方過程の終点が明確な事前分布になります。
**答え**: 計算効率性と意味的圧縮
**解説**: ピクセル空間(512x512x3)でDiffusionを行うと計算量が爆発します。VAEを通じて64x64x4の潜在空間に圧縮すると、空間次元が約48分の1になります。また、VAEの潜在空間はピクセルレベルのノイズではなく**意味的な特徴**を含むため、より少ないステップで高品質な画像を生成できます。
**答え**: 低ランク分解による更新パラメータの最小化
**解説**: 全体の重み行列 $W \in \mathbb{R}^{d \times k}$ を更新すると $d \times k$ 個のパラメータが必要です。LoRAは $\Delta W = BA$ ($B \in \mathbb{R}^{d \times r}$、$A \in \mathbb{R}^{r \times k}$、$r \ll d, k$)と分解して $(d+k) \times r$ 個のみを学習します。r=16、d=k=768の場合、約**98%のパラメータ削減**が可能です。また元の重みは固定されるため、複数のLoRAを交換しながらスタイルの切り替えができます。
**答え**: エンコーダーの複製 + ゼロ畳み込み
**解説**: ControlNetはSD U-Netのエンコーダーブロックを**複製**して別の制御ネットワークを作ります。核心は**ゼロ畳み込み**(初期重みが0の1x1畳み込み)で、学習初期には制御シグナルの影響が0になり、元のSDの品質を保護します。学習が進むにつれてゼロ畳み込みの重みが大きくなり制御効果が強まります。深度マップ、エッジマップなどのコンディショニング画像は、制御ネットワークに入る前に別の小さなエンコーダーで処理されます。
**答え**: 任意のタイムステップから元データへ直接マッピングする一貫性関数の学習
**解説**: DDPMはT=1000ステップを逆方向にすべて通る必要があります(DDIMで減らしても20-50ステップ)。Consistency Modelsは**一貫性関数** $f_\theta(x_t, t) \approx x_0$ を学習します。この関数は同じ「軌跡(trajectory)」上のどの点 $x_t$ からも同じ $x_0$ を出力しなければなりません(一貫性条件)。これにより**1〜2ステップ**だけで高品質なサンプリングが可能になり、DDPMの1000ステップと比べて100〜500倍高速になります。
おわりに
拡散モデルは数学的な優雅さと実用的な性能を同時に持つ、現在の生成AIの核心です。DDPMのガウス数学からStable Diffusionの潜在空間、ControlNetの制御メカニズム、LoRAの効率的な学習、そしてSoraの動画生成まで — これらすべての技術が一つの美しい数学的フレームワークの上に立っています。
次のステップとして推奨する学習経路:
1. DDPM論文 (Ho et al., 2020) の完全精読
2. HuggingFace diffusersチュートリアルの実践
3. ControlNet、LoRAファインチューニングを実際に実行
4. ComfyUIでカスタムワークフローを構築
현재 단락 (1/298)
2022年にStable Diffusionが公開されると、AI画像生成は大衆化の時代を迎えました。しかし「なぜノイズから画像が生まれるのか?」という問いに本当に答えられる人は多くありません。