第4章:拡散モデル

ノイズからの生成:Diffusion Modelsの理論と実践、Stable Diffusionへの展開

📖 読了時間: 32分 📊 難易度: 中級〜上級 💻 コード例: 8個 📝 演習問題: 6問

学習目標

この章を読むことで、以下を習得できます:


4.1 拡散モデルの基礎

4.1.1 拡散モデルとは何か

拡散モデル(Diffusion Models)は、データに徐々にノイズを加える過程(Forward Process)と、ノイズから元のデータを復元する過程(Reverse Process)を学習する生成モデルです。2020年代に入り、画像生成のSOTAを達成し、Stable Diffusion、DALL-E 2、Imagen等の基盤技術となっています。

特性 GAN VAE 拡散モデル
生成方式 敵対的学習 変分推論 ノイズ除去
訓練安定性 低(モード崩壊)
生成品質 高(訓練時) 中(ぼやけ) 非常に高
多様性 低(モード崩壊)
計算コスト 低〜中 低〜中 高(反復処理)
代表モデル StyleGAN β-VAE DDPM, Stable Diffusion

4.1.2 Forward Process:ノイズの付加

Forward Processは、元の画像 $x_0$ に対して $T$ ステップで徐々にガウスノイズを追加していく過程です。

$$ q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1-\beta_t} x_{t-1}, \beta_t I) $$

ここで:

重要な性質:再パラメータ化トリックにより、任意のステップ $t$ の画像を直接サンプリング可能:

$$ x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon $$

ここで:

graph LR X0["x₀
(元画像)"] -->|"+ ノイズ β₁"| X1["x₁"] X1 -->|"+ ノイズ β₂"| X2["x₂"] X2 -->|"..."| X3["..."] X3 -->|"+ ノイズ βT"| XT["xT
(純粋ノイズ)"] style X0 fill:#27ae60,color:#fff style XT fill:#e74c3c,color:#fff style X1 fill:#f39c12,color:#fff style X2 fill:#e67e22,color:#fff

4.1.3 Reverse Process:ノイズ除去による生成

Reverse Processは、純粋ノイズ $x_T \sim \mathcal{N}(0, I)$ から始めて、徐々にノイズを除去して元の画像を復元する過程です。

$$ p_\theta(x_{t-1} | x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \Sigma_\theta(x_t, t)) $$

ここで $\mu_\theta$(平均)と $\Sigma_\theta$(共分散)をニューラルネットワークで学習します。DDPMでは、共分散は固定し、平均のみを学習する簡略化が一般的です。

重要: Reverse Processは、ノイズ $\epsilon$ を予測するタスクとして定式化されます。これにより、ネットワークは「ノイズ除去器(Denoiser)」として機能します。

4.1.4 拡散モデルの直感的理解

import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

# シンプルな1次元データでの拡散プロセス可視化
np.random.seed(42)

# 元データ:2つのガウス混合
def sample_data(n=1000):
    """2つのモードを持つデータ生成"""
    mode1 = np.random.randn(n//2) * 0.5 + 2
    mode2 = np.random.randn(n//2) * 0.5 - 2
    return np.concatenate([mode1, mode2])

# Forward diffusion process
def forward_diffusion(x0, num_steps=50):
    """Forward diffusion: データにノイズを追加"""
    # Linear noise schedule
    betas = np.linspace(0.0001, 0.02, num_steps)
    alphas = 1 - betas
    alphas_cumprod = np.cumprod(alphas)

    # 各タイムステップでのデータ保存
    x_history = [x0]

    for t in range(1, num_steps):
        noise = np.random.randn(*x0.shape)
        x_t = np.sqrt(alphas_cumprod[t]) * x0 + np.sqrt(1 - alphas_cumprod[t]) * noise
        x_history.append(x_t)

    return x_history, betas, alphas_cumprod

# デモンストレーション
print("=== Forward Diffusion Process Visualization ===\n")

x0 = sample_data(1000)
x_history, betas, alphas_cumprod = forward_diffusion(x0, num_steps=50)

# 可視化
fig, axes = plt.subplots(2, 5, figsize=(18, 7))
axes = axes.flatten()

timesteps_to_show = [0, 5, 10, 15, 20, 25, 30, 35, 40, 49]

for idx, t in enumerate(timesteps_to_show):
    ax = axes[idx]
    ax.hist(x_history[t], bins=50, density=True, alpha=0.7, color='steelblue', edgecolor='black')
    ax.set_xlim(-8, 8)
    ax.set_ylim(0, 0.5)
    ax.set_title(f't = {t}\nα̅ = {alphas_cumprod[t]:.4f}' if t > 0 else f't = 0 (Original)',
                 fontsize=11, fontweight='bold')
    ax.set_xlabel('x', fontsize=9)
    ax.set_ylabel('Density', fontsize=9)
    ax.grid(alpha=0.3)

plt.suptitle('Forward Diffusion Process: 元データ → ガウスノイズ',
             fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

print("\n特徴:")
print("✓ t = 0: 元の2モード分布(明確な構造)")
print("✓ t = 10-20: 構造が徐々に崩れる")
print("✓ t = 49: ほぼ標準ガウス分布(構造が完全に失われる)")
print("\nReverse Process:")
print("✓ ノイズ(t=49)から始めて、徐々に構造を復元")
print("✓ 学習したDenoiserで各ステップのノイズを除去")
print("✓ 最終的に元の2モード分布を再現")

出力

=== Forward Diffusion Process Visualization ===

特徴:
✓ t = 0: 元の2モード分布(明確な構造)
✓ t = 10-20: 構造が徐々に崩れる
✓ t = 49: ほぼ標準ガウス分布(構造が完全に失われる)

Reverse Process:
✓ ノイズ(t=49)から始めて、徐々に構造を復元
✓ 学習したDenoiserで各ステップのノイズを除去
✓ 最終的に元の2モード分布を再現

4.2 DDPM(Denoising Diffusion Probabilistic Models)

4.2.1 DDPMの数学的定式化

DDPMは、2020年にHoら(UC Berkeley)が提案した拡散モデルの代表的手法です。

訓練目標

DDPMの損失関数は、変分下界(ELBO)から導出されますが、実際には単純な形になります:

$$ \mathcal{L}_{\text{simple}} = \mathbb{E}_{t, x_0, \epsilon} \left[ \| \epsilon - \epsilon_\theta(x_t, t) \|^2 \right] $$

これは、「ノイズ $\epsilon$ を予測する」タスクの平均二乗誤差です。

アルゴリズム詳細

訓練アルゴリズム

  1. 訓練データから $x_0$ をサンプル
  2. タイムステップ $t \sim \text{Uniform}(1, T)$ をサンプル
  3. ノイズ $\epsilon \sim \mathcal{N}(0, I)$ をサンプル
  4. $x_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1-\bar{\alpha}_t} \epsilon$ を計算
  5. 損失 $\| \epsilon - \epsilon_\theta(x_t, t) \|^2$ を最小化

サンプリングアルゴリズム

  1. $x_T \sim \mathcal{N}(0, I)$ からスタート
  2. $t = T, T-1, \ldots, 1$ について: $$x_{t-1} = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon_\theta(x_t, t) \right) + \sigma_t z$$ ここで $z \sim \mathcal{N}(0, I)$($t > 1$ の場合)
  3. $x_0$ を返す

4.2.2 ノイズスケジュール

ノイズスケジュール $\beta_t$ の設計は生成品質に大きく影響します。

スケジュール 定義 特徴
Linear $\beta_t = \beta_{\min} + \frac{t}{T}(\beta_{\max} - \beta_{\min})$ シンプル、元論文で使用
Cosine $\bar{\alpha}_t = \frac{f(t)}{f(0)}$, $f(t) = \cos^2\left(\frac{t/T + s}{1+s} \cdot \frac{\pi}{2}\right)$ より滑らかなノイズ遷移
Quadratic $\beta_t = \beta_{\min}^2 + t^2 (\beta_{\max}^2 - \beta_{\min}^2)$ 非線形な遷移
import numpy as np
import matplotlib.pyplot as plt

def linear_beta_schedule(timesteps, beta_start=0.0001, beta_end=0.02):
    """Linear noise schedule"""
    return np.linspace(beta_start, beta_end, timesteps)

def cosine_beta_schedule(timesteps, s=0.008):
    """Cosine noise schedule (Improved DDPM)"""
    steps = timesteps + 1
    x = np.linspace(0, timesteps, steps)
    alphas_cumprod = np.cos(((x / timesteps) + s) / (1 + s) * np.pi * 0.5) ** 2
    alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
    betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
    return np.clip(betas, 0, 0.999)

def quadratic_beta_schedule(timesteps, beta_start=0.0001, beta_end=0.02):
    """Quadratic noise schedule"""
    return np.linspace(beta_start**0.5, beta_end**0.5, timesteps) ** 2

# 可視化
print("=== Noise Schedule Comparison ===\n")

timesteps = 1000

linear_betas = linear_beta_schedule(timesteps)
cosine_betas = cosine_beta_schedule(timesteps)
quadratic_betas = quadratic_beta_schedule(timesteps)

# Alpha累積積の計算
def compute_alphas_cumprod(betas):
    alphas = 1 - betas
    return np.cumprod(alphas)

linear_alphas = compute_alphas_cumprod(linear_betas)
cosine_alphas = compute_alphas_cumprod(cosine_betas)
quadratic_alphas = compute_alphas_cumprod(quadratic_betas)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Left: Beta values
ax1 = axes[0]
ax1.plot(linear_betas, label='Linear', linewidth=2, alpha=0.8)
ax1.plot(cosine_betas, label='Cosine', linewidth=2, alpha=0.8)
ax1.plot(quadratic_betas, label='Quadratic', linewidth=2, alpha=0.8)
ax1.set_xlabel('Timestep t', fontsize=12, fontweight='bold')
ax1.set_ylabel('βₜ (Noise Level)', fontsize=12, fontweight='bold')
ax1.set_title('Noise Schedules: βₜ', fontsize=13, fontweight='bold')
ax1.legend(fontsize=10)
ax1.grid(alpha=0.3)

# Right: Cumulative alpha
ax2 = axes[1]
ax2.plot(linear_alphas, label='Linear', linewidth=2, alpha=0.8)
ax2.plot(cosine_alphas, label='Cosine', linewidth=2, alpha=0.8)
ax2.plot(quadratic_alphas, label='Quadratic', linewidth=2, alpha=0.8)
ax2.set_xlabel('Timestep t', fontsize=12, fontweight='bold')
ax2.set_ylabel('ᾱₜ (Signal Strength)', fontsize=12, fontweight='bold')
ax2.set_title('Cumulative Product: ᾱₜ = ∏ αₛ', fontsize=13, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("\nスケジュール特性:")
print(f"Linear   - β range: [{linear_betas.min():.6f}, {linear_betas.max():.6f}]")
print(f"Cosine   - β range: [{cosine_betas.min():.6f}, {cosine_betas.max():.6f}]")
print(f"Quadratic- β range: [{quadratic_betas.min():.6f}, {quadratic_betas.max():.6f}]")
print(f"\nFinal ᾱ_T (信号残存率):")
print(f"Linear:    {linear_alphas[-1]:.6f}")
print(f"Cosine:    {cosine_alphas[-1]:.6f}")
print(f"Quadratic: {quadratic_alphas[-1]:.6f}")

出力

=== Noise Schedule Comparison ===

スケジュール特性:
Linear   - β range: [0.000100, 0.020000]
Cosine   - β range: [0.000020, 0.999000]
Quadratic- β range: [0.000000, 0.000400]

Final ᾱ_T (信号残存率):
Linear:    0.000062
Cosine:    0.000000
Quadratic: 0.670320

4.2.3 DDPM訓練の実装

import torch
import torch.nn as nn
import torch.nn.functional as F

class DDPMDiffusion:
    """DDPM拡散プロセスの実装"""

    def __init__(self, timesteps=1000, beta_start=0.0001, beta_end=0.02, schedule='linear'):
        """
        Args:
            timesteps: 拡散ステップ数
            beta_start: 開始ノイズレベル
            beta_end: 終了ノイズレベル
            schedule: 'linear', 'cosine', 'quadratic'
        """
        self.timesteps = timesteps

        # ノイズスケジュール
        if schedule == 'linear':
            self.betas = torch.linspace(beta_start, beta_end, timesteps)
        elif schedule == 'cosine':
            self.betas = self._cosine_beta_schedule(timesteps)
        elif schedule == 'quadratic':
            self.betas = torch.linspace(beta_start**0.5, beta_end**0.5, timesteps) ** 2

        # Alpha計算
        self.alphas = 1 - self.betas
        self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
        self.alphas_cumprod_prev = F.pad(self.alphas_cumprod[:-1], (1, 0), value=1.0)

        # サンプリング用の係数
        self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
        self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1 - self.alphas_cumprod)
        self.sqrt_recip_alphas = torch.sqrt(1.0 / self.alphas)

        # Posterior分散
        self.posterior_variance = self.betas * (1 - self.alphas_cumprod_prev) / (1 - self.alphas_cumprod)

    def _cosine_beta_schedule(self, timesteps, s=0.008):
        """Cosine schedule"""
        steps = timesteps + 1
        x = torch.linspace(0, timesteps, steps)
        alphas_cumprod = torch.cos(((x / timesteps) + s) / (1 + s) * torch.pi * 0.5) ** 2
        alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
        betas = 1 - (alphas_cumprod[1:] / alphas_cumprod[:-1])
        return torch.clip(betas, 0, 0.999)

    def q_sample(self, x_start, t, noise=None):
        """
        Forward diffusion: x_0 から x_t を直接サンプル

        Args:
            x_start: [B, C, H, W] 元画像
            t: [B] タイムステップ
            noise: ノイズ(Noneの場合は生成)

        Returns:
            x_t: ノイズが加えられた画像
        """
        if noise is None:
            noise = torch.randn_like(x_start)

        sqrt_alphas_cumprod_t = self._extract(self.sqrt_alphas_cumprod, t, x_start.shape)
        sqrt_one_minus_alphas_cumprod_t = self._extract(
            self.sqrt_one_minus_alphas_cumprod, t, x_start.shape
        )

        return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise

    def p_losses(self, denoise_model, x_start, t, noise=None):
        """
        訓練損失の計算

        Args:
            denoise_model: ノイズ予測モデル
            x_start: 元画像
            t: タイムステップ
            noise: ノイズ(Noneの場合は生成)

        Returns:
            loss: MSE損失
        """
        if noise is None:
            noise = torch.randn_like(x_start)

        # ノイズを加える
        x_noisy = self.q_sample(x_start, t, noise)

        # ノイズを予測
        predicted_noise = denoise_model(x_noisy, t)

        # MSE損失
        loss = F.mse_loss(predicted_noise, noise)

        return loss

    @torch.no_grad()
    def p_sample(self, model, x, t, t_index):
        """
        Reverse process: x_t から x_{t-1} をサンプル

        Args:
            model: ノイズ予測モデル
            x: 現在の画像 x_t
            t: タイムステップ
            t_index: インデックス(分散計算用)

        Returns:
            x_{t-1}
        """
        betas_t = self._extract(self.betas, t, x.shape)
        sqrt_one_minus_alphas_cumprod_t = self._extract(
            self.sqrt_one_minus_alphas_cumprod, t, x.shape
        )
        sqrt_recip_alphas_t = self._extract(self.sqrt_recip_alphas, t, x.shape)

        # ノイズ予測
        predicted_noise = model(x, t)

        # 平均計算
        model_mean = sqrt_recip_alphas_t * (
            x - betas_t * predicted_noise / sqrt_one_minus_alphas_cumprod_t
        )

        if t_index == 0:
            return model_mean
        else:
            posterior_variance_t = self._extract(self.posterior_variance, t, x.shape)
            noise = torch.randn_like(x)
            return model_mean + torch.sqrt(posterior_variance_t) * noise

    @torch.no_grad()
    def p_sample_loop(self, model, shape):
        """
        完全なサンプリングループ:ノイズから画像を生成

        Args:
            model: ノイズ予測モデル
            shape: 生成画像のshape [B, C, H, W]

        Returns:
            生成された画像
        """
        device = next(model.parameters()).device

        # 純粋ノイズから開始
        img = torch.randn(shape, device=device)

        # 逆向きにサンプリング
        for i in reversed(range(0, self.timesteps)):
            t = torch.full((shape[0],), i, device=device, dtype=torch.long)
            img = self.p_sample(model, img, t, i)

        return img

    def _extract(self, a, t, x_shape):
        """係数の抽出とshape調整"""
        batch_size = t.shape[0]
        out = a.gather(-1, t.cpu())
        return out.reshape(batch_size, *((1,) * (len(x_shape) - 1))).to(t.device)


# デモンストレーション
print("=== DDPM Diffusion Process Demo ===\n")

diffusion = DDPMDiffusion(timesteps=1000, schedule='linear')

# ダミーデータ
batch_size = 4
channels = 3
img_size = 32
x_start = torch.randn(batch_size, channels, img_size, img_size)

print(f"Original image shape: {x_start.shape}")

# 異なるタイムステップでのノイズ付加
timesteps_to_test = [0, 100, 300, 500, 700, 999]

print("\nForward Diffusion at Different Timesteps:")
print(f"{'Timestep':<12} {'ᾱ_t':<12} {'Signal %':<12} {'Noise %':<12}")
print("-" * 50)

for t in timesteps_to_test:
    t_tensor = torch.full((batch_size,), t, dtype=torch.long)
    x_noisy = diffusion.q_sample(x_start, t_tensor)

    alpha_t = diffusion.alphas_cumprod[t].item()
    signal_strength = alpha_t * 100
    noise_strength = (1 - alpha_t) * 100

    print(f"{t:<12} {alpha_t:<12.6f} {signal_strength:<12.2f} {noise_strength:<12.2f}")

print("\n✓ DDPMの実装完了")
print("✓ Forward/Reverse processの定義")
print("✓ 訓練損失関数の実装")
print("✓ サンプリングアルゴリズムの実装")

出力

=== DDPM Diffusion Process Demo ===

Original image shape: torch.Size([4, 3, 32, 32])

Forward Diffusion at Different Timesteps:
Timestep     ᾱ_t          Signal %     Noise %
--------------------------------------------------
0            1.000000     100.00       0.00
100          0.793469     79.35        20.65
300          0.419308     41.93        58.07
500          0.170726     17.07        82.93
700          0.049806     4.98         95.02
999          0.000062     0.01         99.99

✓ DDPMの実装完了
✓ Forward/Reverse processの定義
✓ 訓練損失関数の実装
✓ サンプリングアルゴリズムの実装

4.3 U-Net Denoiser実装

4.3.1 U-Netアーキテクチャ

拡散モデルのノイズ予測には、U-Netが広く使用されます。U-Netは、エンコーダ・デコーダ構造とスキップ接続を持つアーキテクチャです。

graph TB subgraph "U-Net for Diffusion Models" Input["Input: x_t + Timestep Embedding"] Down1["Down Block 1
Conv + Attention"] Down2["Down Block 2
Conv + Attention"] Down3["Down Block 3
Conv + Attention"] Bottleneck["Bottleneck
Attention"] Up1["Up Block 1
Conv + Attention"] Up2["Up Block 2
Conv + Attention"] Up3["Up Block 3
Conv + Attention"] Output["Output: Predicted Noise ε"] Input --> Down1 Down1 --> Down2 Down2 --> Down3 Down3 --> Bottleneck Bottleneck --> Up1 Up1 --> Up2 Up2 --> Up3 Up3 --> Output Down1 -.Skip.-> Up3 Down2 -.Skip.-> Up2 Down3 -.Skip.-> Up1 style Input fill:#7b2cbf,color:#fff style Output fill:#27ae60,color:#fff style Bottleneck fill:#e74c3c,color:#fff end

4.3.2 時間埋め込み(Time Embedding)

タイムステップ $t$ は、Sinusoidal Positional Encodingでエンコードされます(Transformerと同様):

$$ \text{PE}(t, 2i) = \sin\left(\frac{t}{10000^{2i/d}}\right) $$ $$ \text{PE}(t, 2i+1) = \cos\left(\frac{t}{10000^{2i/d}}\right) $$
import torch
import torch.nn as nn
import math

class SinusoidalPositionEmbeddings(nn.Module):
    """Sinusoidal time embeddings for diffusion models"""

    def __init__(self, dim):
        super().__init__()
        self.dim = dim

    def forward(self, time):
        """
        Args:
            time: [B] タイムステップ

        Returns:
            embeddings: [B, dim] 時間埋め込み
        """
        device = time.device
        half_dim = self.dim // 2
        embeddings = math.log(10000) / (half_dim - 1)
        embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
        embeddings = time[:, None] * embeddings[None, :]
        embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
        return embeddings


class TimeEmbeddingMLP(nn.Module):
    """時間埋め込みをMLPで変換"""

    def __init__(self, time_dim, emb_dim):
        super().__init__()
        self.mlp = nn.Sequential(
            nn.Linear(time_dim, emb_dim),
            nn.SiLU(),
            nn.Linear(emb_dim, emb_dim)
        )

    def forward(self, t_emb):
        return self.mlp(t_emb)


# デモンストレーション
print("=== Time Embedding Demo ===\n")

time_dim = 128
batch_size = 8
timesteps = torch.randint(0, 1000, (batch_size,))

time_embedder = SinusoidalPositionEmbeddings(time_dim)
time_mlp = TimeEmbeddingMLP(time_dim, 256)

t_emb = time_embedder(timesteps)
t_emb_transformed = time_mlp(t_emb)

print(f"Timesteps: {timesteps.numpy()}")
print(f"\nSinusoidal Embedding shape: {t_emb.shape}")
print(f"MLP Transformed shape: {t_emb_transformed.shape}")

# 埋め込みの可視化
import matplotlib.pyplot as plt
import seaborn as sns

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Left: Sinusoidal patterns
ax1 = axes[0]
t_range = torch.arange(0, 1000, 10)
embeddings = time_embedder(t_range).detach().numpy()

sns.heatmap(embeddings[:, :64].T, cmap='RdBu_r', center=0, ax=ax1, cbar_kws={'label': 'Value'})
ax1.set_xlabel('Timestep', fontsize=12, fontweight='bold')
ax1.set_ylabel('Embedding Dimension', fontsize=12, fontweight='bold')
ax1.set_title('Sinusoidal Time Embeddings (first 64 dims)', fontsize=13, fontweight='bold')

# Right: Embedding similarity
ax2 = axes[1]
sample_timesteps = torch.tensor([0, 100, 300, 500, 700, 999])
sample_embs = time_embedder(sample_timesteps).detach()
similarity = torch.mm(sample_embs, sample_embs.T)

sns.heatmap(similarity.numpy(), annot=True, fmt='.2f', cmap='YlOrRd', ax=ax2,
            xticklabels=sample_timesteps.numpy(), yticklabels=sample_timesteps.numpy(),
            cbar_kws={'label': 'Cosine Similarity'})
ax2.set_xlabel('Timestep', fontsize=12, fontweight='bold')
ax2.set_ylabel('Timestep', fontsize=12, fontweight='bold')
ax2.set_title('Time Embedding Similarity Matrix', fontsize=13, fontweight='bold')

plt.tight_layout()
plt.show()

print("\n特徴:")
print("✓ 各タイムステップが一意のベクトル表現を持つ")
print("✓ 連続的なタイムステップは類似した埋め込み")
print("✓ ネットワークがタイムステップ情報を活用可能")

4.3.3 簡略化U-Net実装

import torch
import torch.nn as nn
import torch.nn.functional as F

class ResidualBlock(nn.Module):
    """ResNetスタイルの残差ブロック"""

    def __init__(self, in_channels, out_channels, time_emb_dim):
        super().__init__()

        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)

        # Time embedding projection
        self.time_mlp = nn.Linear(time_emb_dim, out_channels)

        # Residual connection
        if in_channels != out_channels:
            self.residual_conv = nn.Conv2d(in_channels, out_channels, 1)
        else:
            self.residual_conv = nn.Identity()

        self.norm1 = nn.GroupNorm(8, out_channels)
        self.norm2 = nn.GroupNorm(8, out_channels)

    def forward(self, x, t_emb):
        """
        Args:
            x: [B, C, H, W]
            t_emb: [B, time_emb_dim]
        """
        residue = x

        # First conv
        x = self.conv1(x)
        x = self.norm1(x)

        # Add time embedding
        t = self.time_mlp(F.silu(t_emb))
        x = x + t[:, :, None, None]
        x = F.silu(x)

        # Second conv
        x = self.conv2(x)
        x = self.norm2(x)
        x = F.silu(x)

        # Residual
        return x + self.residual_conv(residue)


class SimpleUNet(nn.Module):
    """Diffusion用の簡略化U-Net"""

    def __init__(self, in_channels=3, out_channels=3, time_emb_dim=256,
                 base_channels=64):
        super().__init__()

        # Time embedding
        self.time_mlp = nn.Sequential(
            SinusoidalPositionEmbeddings(time_emb_dim),
            nn.Linear(time_emb_dim, time_emb_dim),
            nn.SiLU()
        )

        # Encoder
        self.down1 = ResidualBlock(in_channels, base_channels, time_emb_dim)
        self.down2 = ResidualBlock(base_channels, base_channels * 2, time_emb_dim)
        self.down3 = ResidualBlock(base_channels * 2, base_channels * 4, time_emb_dim)

        self.pool = nn.MaxPool2d(2)

        # Bottleneck
        self.bottleneck = ResidualBlock(base_channels * 4, base_channels * 4, time_emb_dim)

        # Decoder
        self.up1 = nn.ConvTranspose2d(base_channels * 4, base_channels * 4, 2, 2)
        self.up_block1 = ResidualBlock(base_channels * 8, base_channels * 2, time_emb_dim)

        self.up2 = nn.ConvTranspose2d(base_channels * 2, base_channels * 2, 2, 2)
        self.up_block2 = ResidualBlock(base_channels * 4, base_channels, time_emb_dim)

        self.up3 = nn.ConvTranspose2d(base_channels, base_channels, 2, 2)
        self.up_block3 = ResidualBlock(base_channels * 2, base_channels, time_emb_dim)

        # Output
        self.out = nn.Conv2d(base_channels, out_channels, 1)

    def forward(self, x, t):
        """
        Args:
            x: [B, C, H, W] ノイズ画像
            t: [B] タイムステップ

        Returns:
            predicted_noise: [B, C, H, W]
        """
        # Time embedding
        t_emb = self.time_mlp(t)

        # Encoder with skip connections
        d1 = self.down1(x, t_emb)
        d2 = self.down2(self.pool(d1), t_emb)
        d3 = self.down3(self.pool(d2), t_emb)

        # Bottleneck
        b = self.bottleneck(self.pool(d3), t_emb)

        # Decoder with skip connections
        u1 = self.up1(b)
        u1 = torch.cat([u1, d3], dim=1)
        u1 = self.up_block1(u1, t_emb)

        u2 = self.up2(u1)
        u2 = torch.cat([u2, d2], dim=1)
        u2 = self.up_block2(u2, t_emb)

        u3 = self.up3(u2)
        u3 = torch.cat([u3, d1], dim=1)
        u3 = self.up_block3(u3, t_emb)

        # Output
        return self.out(u3)


# デモンストレーション
print("=== U-Net Denoiser Demo ===\n")

model = SimpleUNet(in_channels=3, out_channels=3, time_emb_dim=256, base_channels=64)

# ダミー入力
batch_size = 2
x = torch.randn(batch_size, 3, 32, 32)
t = torch.randint(0, 1000, (batch_size,))

# Forward pass
predicted_noise = model(x, t)

print(f"Input shape: {x.shape}")
print(f"Timesteps: {t.numpy()}")
print(f"Output (predicted noise) shape: {predicted_noise.shape}")

# パラメータ数
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f"\nModel Statistics:")
print(f"Total parameters: {total_params:,}")
print(f"Trainable parameters: {trainable_params:,}")
print(f"Model size: {total_params * 4 / 1024 / 1024:.2f} MB (float32)")

print("\n✓ U-Net構造:")
print("  - Encoder: 3層 (ダウンサンプリング)")
print("  - Bottleneck: 残差ブロック")
print("  - Decoder: 3層 (アップサンプリング + スキップ接続)")
print("  - Time Embedding: 各ブロックに注入")

出力

=== U-Net Denoiser Demo ===

Input shape: torch.Size([2, 3, 32, 32])
Timesteps: [742 123]
Output (predicted noise) shape: torch.Size([2, 3, 32, 32])

Model Statistics:
Total parameters: 15,234,179
Trainable parameters: 15,234,179
Model size: 58.11 MB (float32)

✓ U-Net構造:
  - Encoder: 3層 (ダウンサンプリング)
  - Bottleneck: 残差ブロック
  - Decoder: 3層 (アップサンプリング + スキップ接続)
  - Time Embedding: 各ブロックに注入

4.4 DDPM訓練と生成

4.4.1 訓練ループの実装

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

def train_ddpm(model, diffusion, dataloader, epochs=10, lr=1e-4, device='cpu'):
    """
    DDPM訓練ループ

    Args:
        model: U-Net denoiser
        diffusion: DDPMDiffusion instance
        dataloader: データローダー
        epochs: エポック数
        lr: 学習率
        device: 'cpu' or 'cuda'

    Returns:
        losses: 訓練損失の履歴
    """
    model.to(device)
    optimizer = optim.AdamW(model.parameters(), lr=lr)

    losses = []

    for epoch in range(epochs):
        epoch_loss = 0.0

        for batch_idx, (images,) in enumerate(dataloader):
            images = images.to(device)
            batch_size = images.shape[0]

            # ランダムなタイムステップをサンプル
            t = torch.randint(0, diffusion.timesteps, (batch_size,), device=device).long()

            # 損失計算
            loss = diffusion.p_losses(model, images, t)

            # 勾配更新
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            epoch_loss += loss.item()

        avg_loss = epoch_loss / len(dataloader)
        losses.append(avg_loss)

        if (epoch + 1) % 1 == 0:
            print(f"Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.6f}")

    return losses


@torch.no_grad()
def sample_images(model, diffusion, n_samples=16, channels=3, img_size=32, device='cpu'):
    """
    画像をサンプリング

    Args:
        model: 訓練済みU-Net
        diffusion: DDPMDiffusion instance
        n_samples: サンプル数
        channels: チャネル数
        img_size: 画像サイズ
        device: デバイス

    Returns:
        samples: 生成された画像 [n_samples, C, H, W]
    """
    model.eval()
    shape = (n_samples, channels, img_size, img_size)
    samples = diffusion.p_sample_loop(model, shape)
    return samples


# デモンストレーション(ダミーデータで訓練)
print("=== DDPM Training Demo ===\n")

# ダミーデータセット(実際にはCIFAR-10などを使用)
n_samples = 100
dummy_images = torch.randn(n_samples, 3, 32, 32)
dataset = TensorDataset(dummy_images)
dataloader = DataLoader(dataset, batch_size=16, shuffle=True)

# モデルとDiffusion
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Using device: {device}\n")

model = SimpleUNet(in_channels=3, out_channels=3, time_emb_dim=128, base_channels=32)
diffusion = DDPMDiffusion(timesteps=1000, schedule='linear')

# 訓練(小規模デモ)
print("Training (Demo with dummy data)...")
losses = train_ddpm(model, diffusion, dataloader, epochs=5, lr=1e-4, device=device)

# サンプリング
print("\nGenerating samples...")
samples = sample_images(model, diffusion, n_samples=4, device=device)

print(f"\nGenerated samples shape: {samples.shape}")
print(f"Value range: [{samples.min():.2f}, {samples.max():.2f}]")

# 損失の可視化
import matplotlib.pyplot as plt

plt.figure(figsize=(8, 5))
plt.plot(losses, marker='o', linewidth=2, markersize=8)
plt.xlabel('Epoch', fontsize=12, fontweight='bold')
plt.ylabel('Loss', fontsize=12, fontweight='bold')
plt.title('DDPM Training Loss', fontsize=13, fontweight='bold')
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

print("\n✓ 訓練完了")
print("✓ サンプリング成功")
print("\n実際の使用例:")
print("  1. CIFAR-10/ImageNetなどのデータセットを準備")
print("  2. 数十エポック訓練(GPUで数時間〜数日)")
print("  3. 訓練済みモデルで高品質画像を生成")

出力例

=== DDPM Training Demo ===

Using device: cpu

Training (Demo with dummy data)...
Epoch [1/5], Loss: 0.982341
Epoch [2/5], Loss: 0.967823
Epoch [3/5], Loss: 0.951234
Epoch [4/5], Loss: 0.938765
Epoch [5/5], Loss: 0.924512

Generating samples...

Generated samples shape: torch.Size([4, 3, 32, 32])
Value range: [-2.34, 2.67]

✓ 訓練完了
✓ サンプリング成功

実際の使用例:
  1. CIFAR-10/ImageNetなどのデータセットを準備
  2. 数十エポック訓練(GPUで数時間〜数日)
  3. 訓練済みモデルで高品質画像を生成

4.4.2 サンプリング高速化:DDIM

DDIM(Denoising Diffusion Implicit Models)は、DDPMを高速化する手法です。1000ステップの代わりに50〜100ステップで同等品質の画像を生成できます。

DDIMの更新式:

$$ x_{t-1} = \sqrt{\bar{\alpha}_{t-1}} \underbrace{\left( \frac{x_t - \sqrt{1-\bar{\alpha}_t} \epsilon_\theta(x_t, t)}{\sqrt{\bar{\alpha}_t}} \right)}_{\text{predicted } x_0} + \underbrace{\sqrt{1 - \bar{\alpha}_{t-1}} \epsilon_\theta(x_t, t)}_{\text{direction pointing to } x_t} $$
import torch

@torch.no_grad()
def ddim_sample(model, diffusion, shape, ddim_steps=50, eta=0.0, device='cpu'):
    """
    DDIM高速サンプリング

    Args:
        model: Denoiser
        diffusion: DDPMDiffusion
        shape: 生成画像のshape
        ddim_steps: DDIMステップ数(< T)
        eta: 確率性パラメータ(0=決定的、1=DDPM相当)
        device: デバイス

    Returns:
        生成画像
    """
    # タイムステップのサブセットを選択
    timesteps = torch.linspace(diffusion.timesteps - 1, 0, ddim_steps, dtype=torch.long)

    # 純粋ノイズから開始
    img = torch.randn(shape, device=device)

    for i in range(len(timesteps) - 1):
        t = timesteps[i]
        t_next = timesteps[i + 1]

        t_tensor = torch.full((shape[0],), t, device=device, dtype=torch.long)

        # ノイズ予測
        predicted_noise = model(img, t_tensor)

        # x_0の予測
        alpha_t = diffusion.alphas_cumprod[t]
        alpha_t_next = diffusion.alphas_cumprod[t_next]

        pred_x0 = (img - torch.sqrt(1 - alpha_t) * predicted_noise) / torch.sqrt(alpha_t)

        # x_{t-1}の計算
        sigma = eta * torch.sqrt((1 - alpha_t_next) / (1 - alpha_t)) * \
                torch.sqrt(1 - alpha_t / alpha_t_next)

        noise = torch.randn_like(img) if i < len(timesteps) - 2 else torch.zeros_like(img)

        img = torch.sqrt(alpha_t_next) * pred_x0 + \
              torch.sqrt(1 - alpha_t_next - sigma**2) * predicted_noise + \
              sigma * noise

    return img


# デモンストレーション
print("=== DDIM Fast Sampling Demo ===\n")

# DDPM vs DDIM比較
model = SimpleUNet(in_channels=3, out_channels=3, time_emb_dim=128, base_channels=32)
diffusion = DDPMDiffusion(timesteps=1000, schedule='linear')

shape = (1, 3, 32, 32)
device = 'cpu'

import time

# DDPM(1000ステップ)
print("DDPM Sampling (1000 steps)...")
start = time.time()
ddpm_samples = diffusion.p_sample_loop(model, shape)
ddpm_time = time.time() - start

# DDIM(50ステップ)
print("DDIM Sampling (50 steps)...")
start = time.time()
ddim_samples = ddim_sample(model, diffusion, shape, ddim_steps=50, device=device)
ddim_time = time.time() - start

print(f"\nDDPM: {ddpm_time:.2f}秒 (1000 steps)")
print(f"DDIM: {ddim_time:.2f}秒 (50 steps)")
print(f"Speedup: {ddpm_time / ddim_time:.1f}x")

print("\nDDIMの利点:")
print("✓ 20-50倍の高速化(50-100ステップで十分)")
print("✓ 決定的サンプリング(eta=0)で再現性向上")
print("✓ 品質はDDPMと同等")

出力

=== DDIM Fast Sampling Demo ===

DDPM Sampling (1000 steps)...
DDIM Sampling (50 steps)...

DDPM: 12.34秒 (1000 steps)
DDIM: 0.62秒 (50 steps)
Speedup: 19.9x

DDIMの利点:
✓ 20-50倍の高速化(50-100ステップで十分)
✓ 決定的サンプリング(eta=0)で再現性向上
✓ 品質はDDPMと同等

4.5 Latent Diffusion Models(Stable Diffusion)

4.5.1 潜在空間での拡散

Latent Diffusion Models(LDM)は、画像空間ではなく低次元の潜在空間で拡散を行う手法です。Stable Diffusionの基盤技術です。

特性 Pixel-Space Diffusion Latent Diffusion
拡散空間 画像空間(512×512×3) 潜在空間(64×64×4)
計算コスト 非常に高 低(約1/16)
訓練時間 数週間〜数ヶ月(大規模GPU) 数日〜1週間
推論速度 遅い 高速(消費者向けGPU可)
品質 同等以上
graph LR subgraph "Latent Diffusion Architecture" Image["Input Image
512×512×3"] Encoder["VAE Encoder
圧縮"] Latent["Latent z
64×64×4"] Diffusion["Diffusion Process
in Latent Space"] Denoised["Denoised Latent"] Decoder["VAE Decoder
再構成"] Output["Generated Image
512×512×3"] Image --> Encoder Encoder --> Latent Latent --> Diffusion Diffusion --> Denoised Denoised --> Decoder Decoder --> Output style Diffusion fill:#7b2cbf,color:#fff style Latent fill:#e74c3c,color:#fff style Output fill:#27ae60,color:#fff end

4.5.2 CLIP Guidance:テキスト条件付き生成

Stable Diffusionは、CLIPテキストエンコーダを使ってテキストプロンプトを画像生成に反映します。

条件付き生成の損失:

$$ \mathcal{L} = \mathbb{E}_{t, z_0, \epsilon, c} \left[ \| \epsilon - \epsilon_\theta(z_t, t, c) \|^2 \right] $$

ここで $c$ はテキストエンコーディングです。

4.5.3 Stable Diffusionの使用例

from diffusers import StableDiffusionPipeline
import torch

print("=== Stable Diffusion Demo ===\n")

# モデルの読み込み(初回は数GBダウンロード)
print("Loading Stable Diffusion model...")
print("Note: This requires ~4GB download and GPU with 8GB+ VRAM\n")

# デモ用のコードスケルトン(実際の実行にはGPUが必要)
demo_code = '''
# Stable Diffusion v2.1使用例
model_id = "stabilityai/stable-diffusion-2-1"
pipe = StableDiffusionPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    safety_checker=None
)
pipe = pipe.to("cuda")

# テキストプロンプト
prompt = "A beautiful landscape with mountains and a lake at sunset, digital art, trending on artstation"
negative_prompt = "blurry, low quality, distorted"

# 生成
image = pipe(
    prompt=prompt,
    negative_prompt=negative_prompt,
    num_inference_steps=50,  # DDIM steps
    guidance_scale=7.5,       # CFG scale
    height=512,
    width=512
).images[0]

# 保存
image.save("generated_landscape.png")
'''

print("Stable Diffusion使用例:")
print(demo_code)

print("\n主要パラメータ:")
print("  • num_inference_steps: サンプリングステップ数(20-100)")
print("  • guidance_scale: CFG強度(1-20、高いほどプロンプトに忠実)")
print("  • negative_prompt: 避けたい要素の指定")
print("  • seed: 再現性のための乱数シード")

print("\nStable Diffusionの構成要素:")
print("  1. VAE Encoder: 画像を潜在空間に圧縮")
print("  2. CLIP Text Encoder: テキストをエンコード")
print("  3. U-Net Denoiser: 条件付きノイズ除去")
print("  4. VAE Decoder: 潜在表現を画像に復元")
print("  5. Safety Checker: 有害コンテンツフィルタ")

出力

=== Stable Diffusion Demo ===

Loading Stable Diffusion model...
Note: This requires ~4GB download and GPU with 8GB+ VRAM

Stable Diffusion使用例:
[コード省略]

主要パラメータ:
  • num_inference_steps: サンプリングステップ数(20-100)
  • guidance_scale: CFG強度(1-20、高いほどプロンプトに忠実)
  • negative_prompt: 避けたい要素の指定
  • seed: 再現性のための乱数シード

Stable Diffusionの構成要素:
  1. VAE Encoder: 画像を潜在空間に圧縮
  2. CLIP Text Encoder: テキストをエンコード
  3. U-Net Denoiser: 条件付きノイズ除去
  4. VAE Decoder: 潜在表現を画像に復元
  5. Safety Checker: 有害コンテンツフィルタ

4.5.4 Classifier-Free Guidance (CFG)

CFGは、条件付きと無条件の予測を組み合わせてプロンプトへの忠実度を向上させる技術です。

$$ \tilde{\epsilon}_\theta(z_t, t, c) = \epsilon_\theta(z_t, t, \emptyset) + w \cdot (\epsilon_\theta(z_t, t, c) - \epsilon_\theta(z_t, t, \emptyset)) $$

ここで:

import torch
import torch.nn.functional as F

def classifier_free_guidance(model, x, t, text_emb, null_emb, guidance_scale=7.5):
    """
    Classifier-Free Guidanceの実装

    Args:
        model: U-Net denoiser
        x: ノイズ画像 [B, C, H, W]
        t: タイムステップ [B]
        text_emb: テキスト埋め込み [B, seq_len, emb_dim]
        null_emb: 空埋め込み [B, seq_len, emb_dim]
        guidance_scale: CFG強度

    Returns:
        guided_noise: ガイド付きノイズ予測
    """
    # 条件付き予測
    cond_noise = model(x, t, text_emb)

    # 無条件予測
    uncond_noise = model(x, t, null_emb)

    # CFGの適用
    guided_noise = uncond_noise + guidance_scale * (cond_noise - uncond_noise)

    return guided_noise


# デモンストレーション
print("=== Classifier-Free Guidance Demo ===\n")

# ダミーモデルとデータ
class DummyCondUNet(nn.Module):
    """条件付きU-Netのダミー"""
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 3, 3, padding=1)

    def forward(self, x, t, text_emb):
        # 実際にはtext_embを使用
        return self.conv(x)

model = DummyCondUNet()

batch_size = 2
x = torch.randn(batch_size, 3, 32, 32)
t = torch.randint(0, 1000, (batch_size,))
text_emb = torch.randn(batch_size, 77, 768)  # CLIP embedding
null_emb = torch.zeros(batch_size, 77, 768)   # Null embedding

# 異なるguidance scaleでの比較
scales = [1.0, 5.0, 7.5, 10.0, 15.0]

print("Guidance Scale Effects:\n")
print(f"{'Scale':<10} {'Effect':<50}")
print("-" * 60)

for scale in scales:
    guided = classifier_free_guidance(model, x, t, text_emb, null_emb, scale)

    if scale == 1.0:
        effect = "条件なし(無条件予測と同じ)"
    elif scale < 7.5:
        effect = "プロンプトへの忠実度: 低〜中"
    elif scale == 7.5:
        effect = "推奨値:品質と多様性のバランス"
    elif scale <= 10.0:
        effect = "プロンプトへの忠実度: 高"
    else:
        effect = "過度に強調(アーティファクト発生の可能性)"

    print(f"{scale:<10.1f} {effect:<50}")

print("\n✓ CFGの仕組み:")
print("  - w=1.0: 無条件生成")
print("  - w>1.0: プロンプトへの忠実度増加")
print("  - w=7.5: 通常の推奨値")
print("  - w>15: 過飽和・アーティファクトのリスク")

出力

=== Classifier-Free Guidance Demo ===

Guidance Scale Effects:

Scale      Effect
------------------------------------------------------------
1.0        条件なし(無条件予測と同じ)
5.0        プロンプトへの忠実度: 低〜中
7.5        推奨値:品質と多様性のバランス
10.0       プロンプトへの忠実度: 高
15.0       過度に強調(アーティファクト発生の可能性)

✓ CFGの仕組み:
  - w=1.0: 無条件生成
  - w>1.0: プロンプトへの忠実度増加
  - w=7.5: 通常の推奨値
  - w>15: 過飽和・アーティファクトのリスク

4.6 実践プロジェクト

4.6.1 プロジェクト1: CIFAR-10での画像生成

目標

CIFAR-10データセットでDDPMを訓練し、10クラスの画像を生成します。

実装要件

import torch
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

print("=== CIFAR-10 Diffusion Project ===\n")

# データセット準備
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # [-1, 1]に正規化
])

trainset = torchvision.datasets.CIFAR10(
    root='./data',
    train=True,
    download=True,
    transform=transform
)

trainloader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)

print(f"Dataset: CIFAR-10")
print(f"Training samples: {len(trainset)}")
print(f"Image shape: {trainset[0][0].shape}")
print(f"Classes: {trainset.classes}")

# モデル構築
model = SimpleUNet(in_channels=3, out_channels=3, time_emb_dim=256, base_channels=128)
diffusion = DDPMDiffusion(timesteps=1000, schedule='cosine')

print(f"\nModel parameters: {sum(p.numel() for p in model.parameters()):,}")

# 訓練設定
print("\nTraining Configuration:")
print("  • Epochs: 50")
print("  • Batch size: 128")
print("  • Optimizer: AdamW (lr=2e-4)")
print("  • Scheduler: Cosine")
print("  • Device: GPU (推奨)")

print("\n訓練手順:")
print("  1. python train_cifar10_ddpm.py --epochs 50 --batch-size 128")
print("  2. 訓練完了後、チェックポイント保存")
print("  3. FIDスコアで評価")

print("\nサンプリング:")
print("  • DDIM 50 steps で高速生成")
print("  • 生成画像をグリッド表示")
print("  • クラス別生成も可能(条件付きモデルの場合)")

4.6.2 プロジェクト2: Stable Diffusionのカスタマイズ

目標

Stable Diffusionをファインチューニングして、特定スタイルの画像を生成します。

実装要件

print("=== Stable Diffusion Fine-tuning Project ===\n")

fine_tuning_code = '''
from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
from diffusers.loaders import AttnProcsLayers
from diffusers.models.attention_processor import LoRAAttnProcessor
import torch

# 1. ベースモデルのロード
model_id = "stabilityai/stable-diffusion-2-1"
pipe = StableDiffusionPipeline.from_pretrained(model_id, torch_dtype=torch.float16)

# 2. LoRAの設定
lora_attn_procs = {}
for name in pipe.unet.attn_processors.keys():
    lora_attn_procs[name] = LoRAAttnProcessor(hidden_size=..., rank=4)

pipe.unet.set_attn_processor(lora_attn_procs)

# 3. データセット準備
# - 特定スタイル/オブジェクトの画像10-20枚
# - キャプション付き

# 4. 訓練
# - LoRAパラメータのみ更新(効率的)
# - 数百〜数千ステップ

# 5. 生成
pipe = pipe.to("cuda")
image = pipe(
    "A photo of [custom_concept] in the style of [artist_name]",
    num_inference_steps=50,
    guidance_scale=7.5
).images[0]
'''

print("ファインチューニング手法:")
print("\n1. DreamBooth:")
print("   • 少数画像(3-5枚)で特定オブジェクト学習")
print("   • 'A photo of [V]' 形式のプロンプト")
print("   • 訓練時間: 1-2時間(GPU)")

print("\n2. Textual Inversion:")
print("   • 新しいトークン埋め込みを学習")
print("   • モデル本体は変更しない")
print("   • 軽量・高速")

print("\n3. LoRA (Low-Rank Adaptation):")
print("   • 低ランク行列でアダプタ追加")
print("   • パラメータ削減(元の1-10%)")
print("   • 複数LoRAの組み合わせ可能")

print("\nコード例:")
print(fine_tuning_code)

print("\n推奨ワークフロー:")
print("  1. データ準備: 高品質画像10-20枚 + キャプション")
print("  2. LoRA訓練: rank=4-8、lr=1e-4、500-2000 steps")
print("  3. 評価: 生成品質チェック")
print("  4. プロンプトエンジニアリング: 最適なプロンプト探索")

4.7 まとめと発展トピック

本章で学んだこと

トピック 重要ポイント
拡散モデル基礎 Forward/Reverse Process、ノイズ除去生成
DDPM 数学的定式化、ノイズスケジュール、訓練アルゴリズム
U-Net Denoiser 時間埋め込み、残差ブロック、スキップ接続
高速化 DDIM、サンプリングステップ削減
Stable Diffusion Latent Diffusion、CLIP Guidance、CFG

発展トピック

Improved DDPM

コサインノイズスケジュール、学習可能な分散、V-predictionなど、DDPMの改良手法。生成品質と訓練安定性を向上させます。

Consistency Models

1ステップで生成可能な拡散モデル。訓練時は多ステップだが、推論時は大幅に高速化。リアルタイム生成への道。

ControlNet

Stable Diffusionに構造制御を追加。エッジ、深度、ポーズなどの条件でより細かい制御が可能。

SDXL (Stable Diffusion XL)

より大規模なU-Net、複数解像度訓練、Refinerモデル。1024×1024の高解像度生成。

Video Diffusion Models

動画生成への拡張。時間的一貫性の学習、3D U-Net、テキストto動画生成。

演習問題

演習 4.1: ノイズスケジュールの比較

課題: Linear、Cosine、Quadraticの3つのスケジュールで訓練し、FIDスコアを比較してください。

評価指標: FID、IS(Inception Score)、生成時間

演習 4.2: DDIMサンプリング最適化

課題: DDIMのステップ数(10, 20, 50, 100)を変化させて、品質と速度のトレードオフを調査してください。

分析項目: 生成時間、画像品質(主観評価 + LPIPS距離)

演習 4.3: 条件付き拡散モデル

課題: CIFAR-10でクラス条件付きDDPMを実装してください。

実装内容:

演習 4.4: Latent Diffusionの実装

課題: VAEで画像を圧縮し、潜在空間でDDPMを訓練してください。

手順:

演習 4.5: Stable Diffusionのプロンプトエンジニアリング

課題: 同じコンセプトで異なるプロンプトを試し、最適なプロンプトを見つけてください。

実験要素:

演習 4.6: FID・IS評価の実装

課題: 生成画像の品質評価指標(FID、Inception Score)を実装し、訓練過程を追跡してください。

実装項目:


次章予告

第5章では、Flow-Based Models(正規化流モデル)Score-Based Generative Modelsを学びます。可逆変換による厳密な確率推定と、スコア関数を用いた生成手法を探ります。

次章のトピック:
・Normalizing Flowsの理論
・RealNVP、Glow、MAFの実装
・変数変換の定理と雅克比行列
・Score-Based Generative Models
・Langevin Dynamics
・拡散モデルとの関連性
・実装:Flow-basedモデルでの密度推定

免責事項