Skip to content
Published on

生成AI&拡散モデル完全ガイド: Stable DiffusionからControlNet・動画生成まで

Authors

はじめに

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構造
import torch
import torch.nn as nn

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はエンコーダが潜在空間の分布を学習し、デコーダがその分布からサンプリングして画像を復元します。

損失関数: L=E[logp(xz)]DKL(q(zx)p(z))\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)detf1xp(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)の順方向過程は、元のデータ x0x_0 にTステップかけてガウスノイズを追加します。

q(xtxt1)=N(xt;1βtxt1,βtI)q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t} x_{t-1}, \beta_t I)

これを累積すると、任意のタイムステップtで直接サンプリングできます:

q(xtx0)=N(xt;αˉtx0,(1αˉt)I)q(x_t | x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t} x_0, (1-\bar{\alpha}_t) I)

ここで αˉt=s=1t(1βs)\bar{\alpha}_t = \prod_{s=1}^{t}(1-\beta_s) です。

import torch
import torch.nn.functional as F

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θ(xt1xt)=N(xt1;μθ(xt,t),σt2I)p_\theta(x_{t-1} | x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \sigma_t^2 I)

学習目標は追加されたノイズと予測されたノイズ間のMSE:

L=Et,x0,ϵ[ϵϵθ(αˉtx0+1αˉtϵ,t)2]\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θ(x)=xlogpθ(x)s_\theta(x) = \nabla_x \log p_\theta(x)

拡散モデルのノイズ予測は、スコア関数を学習することと同値です:

ϵθ(xt,t)1αˉtxtlogq(xt)\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)dWdx = f(x,t)dt + g(t)dW

逆方向SDE: dx=[f(x,t)g(t)2xlogpt(x)]dt+g(t)dWˉ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
import torch

# 基本パイプラインの使用
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
import torchvision.transforms as T

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が高いほどプロンプトに忠実になり、低いほど多様性が増します。

ϵguided=ϵuncond+w(ϵcondϵuncond)\epsilon_{guided} = \epsilon_{uncond} + w \cdot (\epsilon_{cond} - \epsilon_{uncond})


4. LoRAとDreamBooth ファインチューニング

4.1 LoRAの原理

全体の重み行列 WRd×kW \in \mathbb{R}^{d \times k} を直接更新する代わりに、変化量を2つの低ランク行列の積で表します:

W=W+ΔW=W+BAW' = W + \Delta W = W + BA

ここで BRd×rB \in \mathbb{R}^{d \times r}ARr×kA \in \mathbb{R}^{r \times k}rmin(d,k)r \ll \min(d, k) です。

一般的にr=4〜16で、全パラメータ対比0.1〜1%のみを学習します。

from diffusers import StableDiffusionPipeline
from peft import LoraConfig, get_peft_model
import torch

# 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
import torch

# 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
import torch
import cv2
import numpy as np

# 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
import torch
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
import torch

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
import torchaudio

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
import torch

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連携

import json
import urllib.request

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・拡散モデルの理解確認

Q1. DDPMの順方向過程でガウスノイズを使用する数学的な理由は?

答え: 中心極限定理とガウス分布の再生性

解説: ガウスノイズを使用する理由は3つあります。第一に、ガウス分布は加法に対して閉じています(2つのガウスの和もガウス)。第二に、reparameterization trickが可能で、任意のタイムステップtで直接サンプリングできます: xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon。第三に、中心極限定理によりTが無限大に近づくと、どんな分布も標準ガウスに収束するため、前方過程の終点が明確な事前分布になります。

Q2. Stable DiffusionのU-Netが潜在空間で動作する理由は?(ピクセル空間との比較)

答え: 計算効率性と意味的圧縮

解説: ピクセル空間(512x512x3)でDiffusionを行うと計算量が爆発します。VAEを通じて64x64x4の潜在空間に圧縮すると、空間次元が約48分の1になります。また、VAEの潜在空間はピクセルレベルのノイズではなく意味的な特徴を含むため、より少ないステップで高品質な画像を生成できます。

Q3. LoRAが全体の重みのファインチューニングより効率的な理由は?

答え: 低ランク分解による更新パラメータの最小化

解説: 全体の重み行列 WRd×kW \in \mathbb{R}^{d \times k} を更新すると d×kd \times k 個のパラメータが必要です。LoRAは ΔW=BA\Delta W = BA (BRd×rB \in \mathbb{R}^{d \times r}ARr×kA \in \mathbb{R}^{r \times k}rd,kr \ll d, k)と分解して (d+k)×r(d+k) \times r 個のみを学習します。r=16、d=k=768の場合、約98%のパラメータ削減が可能です。また元の重みは固定されるため、複数のLoRAを交換しながらスタイルの切り替えができます。

Q4. ControlNetのアーキテクチャ: 深度マップやエッジマップなどの追加コンディショニングをどのように受け取るか?

答え: エンコーダーの複製 + ゼロ畳み込み

解説: ControlNetはSD U-Netのエンコーダーブロックを複製して別の制御ネットワークを作ります。核心はゼロ畳み込み(初期重みが0の1x1畳み込み)で、学習初期には制御シグナルの影響が0になり、元のSDの品質を保護します。学習が進むにつれてゼロ畳み込みの重みが大きくなり制御効果が強まります。深度マップ、エッジマップなどのコンディショニング画像は、制御ネットワークに入る前に別の小さなエンコーダーで処理されます。

Q5. Consistency ModelsがDDPMよりサンプリングステップを減らせる原理は?

答え: 任意のタイムステップから元データへ直接マッピングする一貫性関数の学習

解説: DDPMはT=1000ステップを逆方向にすべて通る必要があります(DDIMで減らしても20-50ステップ)。Consistency Modelsは一貫性関数 fθ(xt,t)x0f_\theta(x_t, t) \approx x_0 を学習します。この関数は同じ「軌跡(trajectory)」上のどの点 xtx_t からも同じ x0x_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でカスタムワークフローを構築