Skip to content

필사 모드: 生成AI完全ガイド: GAN・VAE・拡散モデルをマスターする

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

生成AIは今日のテクノロジーで最もホットな分野です。DALL-E 3は1行のテキストからリアルな画像を生成し、Stable Diffusionはアートワークを生成し、Soraは動画を生成します。これらすべての革新の背後には、数十年にわたって開発された生成モデルがあります。

このガイドでは、VAE(変分オートエンコーダ)、GAN(敵対的生成ネットワーク)、そして現代の拡散モデルを数学的直感と完全なPyTorchコード実装とともに網羅的に解説します。

1. 生成モデルの概要

1.1 生成モデルと識別モデル

ディープラーニングモデルは大きく2つに分類されます。

**識別モデル**: 入力データxが与えられたとき、条件付き確率P(y|x)を学習します。画像分類、物体検出などに使用されます。

**生成モデル**: 結合確率分布P(x)またはP(x, y)を学習します。学習後に新しいデータサンプルを生成できます。

生成モデルの核心的な問い: 「このデータはどのように生成されたのか?同じ分布から新しいサンプルを生成するにはどうすれば良いのか?」

1.2 潜在空間の概念

ほとんどの生成モデルは**潜在空間**(より低次元の表現空間)を活用します。

例えば、28x28のMNIST画像(784次元)を2〜100次元の潜在ベクトルに圧縮できます。この潜在空間では:

- 数字「3」と数字「8」は近くに配置されます

- 中間点のベクトルをデコードすると「3」と「8」の中間が生成されます

- 潜在空間を歩くこと(補間)で連続的な変換が可能になります

1.3 生成モデルの応用

- **画像生成**: リアルな顔、風景、アートワーク

- **画像変換**: 昼の写真から夜の写真、スケッチから写真

- **データ拡張**: 希少なトレーニングデータを補完

- **創薬**: 新しい分子構造を生成

- **テキスト生成**: GPTファミリーの言語モデル

- **音楽/音声合成**: 音楽作曲、TTSシステム

2. オートエンコーダ

VAEを理解するために必要な基盤として、オートエンコーダから始めます。

2.1 エンコーダ-デコーダアーキテクチャ

オートエンコーダは2つの部分から構成されます。

- **エンコーダ**: 高次元の入力x → 低次元の潜在ベクトルz

- **デコーダ**: 低次元の潜在ベクトルz → 再構成出力x'

目標: x'がxにできるだけ類似するよう学習します(再構成損失の最小化)。

from torchvision import datasets, transforms

from torch.utils.data import DataLoader

class Autoencoder(nn.Module):

"""MNIST用の基本的なオートエンコーダ"""

def __init__(self, input_dim=784, latent_dim=32):

super().__init__()

エンコーダ

self.encoder = nn.Sequential(

nn.Linear(input_dim, 256),

nn.ReLU(),

nn.Linear(256, 128),

nn.ReLU(),

nn.Linear(128, latent_dim),

nn.ReLU()

)

デコーダ

self.decoder = nn.Sequential(

nn.Linear(latent_dim, 128),

nn.ReLU(),

nn.Linear(128, 256),

nn.ReLU(),

nn.Linear(256, input_dim),

nn.Sigmoid() # ピクセル値を[0, 1]に制限

)

def forward(self, x):

x = x.view(x.size(0), -1)

z = self.encoder(x)

x_reconstructed = self.decoder(z)

return x_reconstructed, z

def encode(self, x):

x = x.view(x.size(0), -1)

return self.encoder(x)

def decode(self, z):

return self.decoder(z).view(-1, 1, 28, 28)

def train_autoencoder(epochs=20):

"""オートエンコーダの学習"""

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

transform = transforms.Compose([transforms.ToTensor()])

train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)

model = Autoencoder().to(device)

optimizer = optim.Adam(model.parameters(), lr=1e-3)

criterion = nn.BCELoss()

for epoch in range(epochs):

total_loss = 0

for data, _ in train_loader:

data = data.to(device)

optimizer.zero_grad()

reconstructed, z = model(data)

target = data.view(data.size(0), -1)

loss = criterion(reconstructed, target)

loss.backward()

optimizer.step()

total_loss += loss.item()

avg_loss = total_loss / len(train_loader)

print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")

return model

2.2 基本的なオートエンコーダの限界

基本的なオートエンコーダはデータをうまく圧縮しますが、**新しいサンプルの生成が難しい**という問題があります。潜在空間が正則化されていないため、ランダムな潜在ベクトルをデコードしても意味のない画像が生成されることが多いです。

これがまさにVAEが解決する問題です。

3. 変分オートエンコーダ(VAE)

VAEは2013年のKingmaとWellingの革新的な論文(arXiv:1312.6114)で提案されました。

3.1 VAEの核心アイデア

VAEの核心は潜在空間に**確率分布**を学習することです。

- 基本的なオートエンコーダ: z = encoder(x) (決定論的な点)

- VAE: z ~ N(μ, σ²) (ガウス分布からサンプリング)

エンコーダは潜在ベクトルzを直接出力する代わりに、**分布パラメータ(平均μ、分散σ²)**を出力します。

新しい画像を生成する際は、標準正規分布N(0, I)からzをサンプリングしてデコーダに渡します。

3.2 ELBO(証拠下界)

VAEの学習目標はデータの対数尤度 log P(x) の最大化です。直接最適化が難しいため、代わりにELBOを最大化します。

log P(x) >= E_q[log P(x|z)] - KL[q(z|x) || P(z)]

↑ ↑

再構成損失 KL発散

**ELBO = 再構成損失 + KL発散**

- **再構成損失**: デコードされた出力が元データにどれだけ類似しているか

- **KL発散**: 学習した潜在分布q(z|x)が事前分布P(z) = N(0, I)にどれだけ近いか

3.3 再パラメータ化トリック

z ~ N(μ, σ²) を直接サンプリングすると逆伝播がブロックされます。再パラメータ化トリックでこれを解決します。

z = μ + σ * ε, ε ~ N(0, I)

これにより確率性(ε)がネットワークパラメータから分離され、μとσに対する勾配計算が可能になります。

3.4 完全なVAE実装(MNIST)

from torchvision import datasets, transforms

from torch.utils.data import DataLoader

class VAE(nn.Module):

"""変分オートエンコーダ"""

def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):

super(VAE, self).__init__()

self.latent_dim = latent_dim

エンコーダ: 入力 -> μ, log(σ²)

self.fc1 = nn.Linear(input_dim, hidden_dim)

self.fc_mu = nn.Linear(hidden_dim, latent_dim) # 平均

self.fc_logvar = nn.Linear(hidden_dim, latent_dim) # 対数分散

デコーダ: 潜在ベクトル -> 再構成

self.fc3 = nn.Linear(latent_dim, hidden_dim)

self.fc4 = nn.Linear(hidden_dim, input_dim)

def encode(self, x):

"""エンコーダ: x -> (μ, log_var)"""

h = F.relu(self.fc1(x))

return self.fc_mu(h), self.fc_logvar(h)

def reparameterize(self, mu, logvar):

"""再パラメータ化トリック: z = μ + ε * σ"""

if self.training:

std = torch.exp(0.5 * logvar) # σ = exp(0.5 * log σ²)

eps = torch.randn_like(std) # ε ~ N(0, I)

return mu + eps * std

else:

return mu # 推論時は平均のみ使用

def decode(self, z):

"""デコーダ: z -> x'"""

h = F.relu(self.fc3(z))

return torch.sigmoid(self.fc4(h))

def forward(self, x):

x_flat = x.view(-1, 784)

mu, logvar = self.encode(x_flat)

z = self.reparameterize(mu, logvar)

x_recon = self.decode(z)

return x_recon, mu, logvar

def generate(self, num_samples, device):

"""標準正規分布からサンプリングして画像を生成"""

with torch.no_grad():

z = torch.randn(num_samples, self.latent_dim).to(device)

samples = self.decode(z)

return samples.view(num_samples, 1, 28, 28)

def vae_loss(x_recon, x, mu, logvar, beta=1.0):

"""

VAE損失 = 再構成損失 + β * KL発散

beta=1: 標準VAE

beta>1: β-VAE(よりディスエンタングルな表現)

"""

再構成損失(BCE)

recon_loss = F.binary_cross_entropy(

x_recon, x.view(-1, 784),

reduction='sum'

)

KL発散: KL[N(μ, σ²) || N(0, 1)]

= -0.5 * Σ(1 + log σ² - μ² - σ²)

kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())

return recon_loss + beta * kl_loss

def train_vae(epochs=50, latent_dim=20, beta=1.0):

"""VAEの学習"""

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

transform = transforms.Compose([transforms.ToTensor()])

train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)

model = VAE(latent_dim=latent_dim).to(device)

optimizer = optim.Adam(model.parameters(), lr=1e-3)

train_losses = []

for epoch in range(epochs):

model.train()

total_loss = 0

for data, _ in train_loader:

data = data.to(device)

optimizer.zero_grad()

x_recon, mu, logvar = model(data)

loss = vae_loss(x_recon, data, mu, logvar, beta)

loss.backward()

optimizer.step()

total_loss += loss.item()

avg_loss = total_loss / len(train_dataset)

train_losses.append(avg_loss)

if (epoch + 1) % 10 == 0:

print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")

return model, train_losses

def interpolate_latent_space(model, img1, img2, steps=10, device='cpu'):

"""潜在空間内の2枚の画像を補間"""

model.eval()

with torch.no_grad():

z1_flat = img1.view(-1, 784).to(device)

z2_flat = img2.view(-1, 784).to(device)

mu1, _ = model.encode(z1_flat)

mu2, _ = model.encode(z2_flat)

線形補間

interpolated_images = []

for alpha in np.linspace(0, 1, steps):

z_interp = (1 - alpha) * mu1 + alpha * mu2

img_recon = model.decode(z_interp)

interpolated_images.append(img_recon.view(1, 28, 28))

return interpolated_images

3.5 畳み込みVAE(CIFAR-10用)

class ConvVAE(nn.Module):

"""カラー画像用の畳み込みVAE"""

def __init__(self, latent_dim=128):

super().__init__()

self.latent_dim = latent_dim

畳み込みエンコーダ

self.encoder_conv = nn.Sequential(

nn.Conv2d(3, 32, 4, stride=2, padding=1), # 16x16

nn.ReLU(),

nn.Conv2d(32, 64, 4, stride=2, padding=1), # 8x8

nn.ReLU(),

nn.Conv2d(64, 128, 4, stride=2, padding=1), # 4x4

nn.ReLU(),

)

self.fc_mu = nn.Linear(128 * 4 * 4, latent_dim)

self.fc_logvar = nn.Linear(128 * 4 * 4, latent_dim)

転置畳み込みデコーダ

self.decoder_fc = nn.Linear(latent_dim, 128 * 4 * 4)

self.decoder_conv = nn.Sequential(

nn.ConvTranspose2d(128, 64, 4, stride=2, padding=1), # 8x8

nn.ReLU(),

nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1), # 16x16

nn.ReLU(),

nn.ConvTranspose2d(32, 3, 4, stride=2, padding=1), # 32x32

nn.Sigmoid()

)

def encode(self, x):

h = self.encoder_conv(x).view(x.size(0), -1)

return self.fc_mu(h), self.fc_logvar(h)

def reparameterize(self, mu, logvar):

std = torch.exp(0.5 * logvar)

eps = torch.randn_like(std)

return mu + eps * std

def decode(self, z):

h = F.relu(self.decoder_fc(z)).view(-1, 128, 4, 4)

return self.decoder_conv(h)

def forward(self, x):

mu, logvar = self.encode(x)

z = self.reparameterize(mu, logvar)

return self.decode(z), mu, logvar

4. 敵対的生成ネットワーク(GAN)

GANは2014年のIan Goodfellowの論文(arXiv:1406.2661)で提案された革命的なアイデアです。

4.1 ジェネレータとディスクリミネータのゲーム

GANは2つのニューラルネットワークが競い合って学習します。

**ジェネレータ(G)**: ランダムノイズzから偽のデータを作成します。

- 目標: ディスクリミネータを騙せるほどリアルなデータを生成する

**ディスクリミネータ(D)**: 本物のデータと生成された偽のデータを区別します。

- 目標: 本物のデータを1、偽のデータを0に分類する

このゲームで両方のネットワークはナッシュ均衡に達するまで競い合います。

4.2 ミニマックス損失

min_G max_D [E_x[log D(x)] + E_z[log(1 - D(G(z)))]]

- **Dが最大化**: 本物のデータでlog D(x)を最大化、偽のデータでlog(1 - D(G(z)))を最大化

- **Gが最小化**: G(z)がDを騙せるようlog(1 - D(G(z)))を最小化

実際には、ジェネレータ損失として `-log(D(G(z)))` を使用します(非飽和損失)。

4.3 基本的なGAN実装

from torchvision import datasets, transforms

class Generator(nn.Module):

"""GANジェネレータ"""

def __init__(self, noise_dim=100, output_dim=784):

super().__init__()

self.model = nn.Sequential(

nn.Linear(noise_dim, 256),

nn.LeakyReLU(0.2),

nn.BatchNorm1d(256),

nn.Linear(256, 512),

nn.LeakyReLU(0.2),

nn.BatchNorm1d(512),

nn.Linear(512, 1024),

nn.LeakyReLU(0.2),

nn.BatchNorm1d(1024),

nn.Linear(1024, output_dim),

nn.Tanh() # [-1, 1]の範囲

)

def forward(self, z):

return self.model(z).view(-1, 1, 28, 28)

class Discriminator(nn.Module):

"""GANディスクリミネータ"""

def __init__(self, input_dim=784):

super().__init__()

self.model = nn.Sequential(

nn.Linear(input_dim, 1024),

nn.LeakyReLU(0.2),

nn.Dropout(0.3),

nn.Linear(1024, 512),

nn.LeakyReLU(0.2),

nn.Dropout(0.3),

nn.Linear(512, 256),

nn.LeakyReLU(0.2),

nn.Dropout(0.3),

nn.Linear(256, 1),

nn.Sigmoid()

)

def forward(self, x):

return self.model(x.view(x.size(0), -1))

def train_gan(epochs=200, noise_dim=100, lr=2e-4):

"""GANの学習"""

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

transform = transforms.Compose([

transforms.ToTensor(),

transforms.Normalize([0.5], [0.5]) # [-1, 1]に正規化

])

train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)

dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)

G = Generator(noise_dim).to(device)

D = Discriminator().to(device)

optimizer_G = optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))

optimizer_D = optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))

criterion = nn.BCELoss()

for epoch in range(epochs):

for i, (real_imgs, _) in enumerate(dataloader):

batch_size = real_imgs.size(0)

real_imgs = real_imgs.to(device)

real_labels = torch.ones(batch_size, 1).to(device)

fake_labels = torch.zeros(batch_size, 1).to(device)

=== ディスクリミネータの更新 ===

optimizer_D.zero_grad()

d_real = D(real_imgs)

d_loss_real = criterion(d_real, real_labels)

z = torch.randn(batch_size, noise_dim).to(device)

fake_imgs = G(z).detach() # Gの勾配をデタッチ

d_fake = D(fake_imgs)

d_loss_fake = criterion(d_fake, fake_labels)

d_loss = d_loss_real + d_loss_fake

d_loss.backward()

optimizer_D.step()

=== ジェネレータの更新 ===

optimizer_G.zero_grad()

z = torch.randn(batch_size, noise_dim).to(device)

fake_imgs = G(z)

g_pred = D(fake_imgs)

GはDが偽物を本物として分類するよう望む

g_loss = criterion(g_pred, real_labels)

g_loss.backward()

optimizer_G.step()

if (epoch + 1) % 20 == 0:

print(f"Epoch {epoch+1}/{epochs} | D Loss: {d_loss.item():.4f} | G Loss: {g_loss.item():.4f}")

return G, D

4.4 モードの崩壊

GANの最大の問題の一つ。ジェネレータが多様な画像の生成をやめ、ディスクリミネータを騙すことに成功した少数のパターンのみを繰り返し生成するようになります。

5. GANの進化

5.1 DCGAN(Deep Convolutional GAN)

2015年に提案されたDCGANは、GANに畳み込みニューラルネットワークを正常に適用しました。

class DCGANGenerator(nn.Module):

"""DCGANジェネレータ(64x64画像用)"""

def __init__(self, noise_dim=100, ngf=64, nc=3):

super().__init__()

self.main = nn.Sequential(

入力: noise_dim x 1 x 1

nn.ConvTranspose2d(noise_dim, ngf * 8, 4, 1, 0, bias=False),

nn.BatchNorm2d(ngf * 8),

nn.ReLU(True),

状態: (ngf*8) x 4 x 4

nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),

nn.BatchNorm2d(ngf * 4),

nn.ReLU(True),

状態: (ngf*4) x 8 x 8

nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),

nn.BatchNorm2d(ngf * 2),

nn.ReLU(True),

状態: (ngf*2) x 16 x 16

nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),

nn.BatchNorm2d(ngf),

nn.ReLU(True),

状態: (ngf) x 32 x 32

nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),

nn.Tanh()

出力: nc x 64 x 64

)

def forward(self, z):

z = z.view(z.size(0), -1, 1, 1)

return self.main(z)

class DCGANDiscriminator(nn.Module):

"""DCGANディスクリミネータ(64x64画像用)"""

def __init__(self, ndf=64, nc=3):

super().__init__()

self.main = nn.Sequential(

入力: nc x 64 x 64

nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),

nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),

nn.BatchNorm2d(ndf * 2),

nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),

nn.BatchNorm2d(ndf * 4),

nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),

nn.BatchNorm2d(ndf * 8),

nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),

nn.Sigmoid()

)

def forward(self, x):

return self.main(x).view(-1, 1)

5.2 WGAN(Wasserstein GAN)

WGAN(arXiv:1701.07875)はJensen-Shannon発散の代わりにWasserstein距離を使用し、学習の安定性を大幅に向上させました。

class WGANDiscriminator(nn.Module):

"""WGANクリティック: Sigmoid出力なし"""

def __init__(self, input_dim=784):

super().__init__()

self.model = nn.Sequential(

nn.Linear(input_dim, 512),

nn.LeakyReLU(0.2),

nn.Linear(512, 256),

nn.LeakyReLU(0.2),

nn.Linear(256, 1)

Sigmoidなし!Wasserstein距離計算に必要

)

def forward(self, x):

return self.model(x.view(x.size(0), -1))

def train_wgan(G, D, dataloader, device, epochs=100,

n_critic=5, clip_value=0.01, lr=5e-5):

"""WGANの学習"""

optimizer_G = optim.RMSprop(G.parameters(), lr=lr)

optimizer_D = optim.RMSprop(D.parameters(), lr=lr)

for epoch in range(epochs):

for i, (real_imgs, _) in enumerate(dataloader):

real_imgs = real_imgs.to(device)

batch_size = real_imgs.size(0)

クリティックをn_critic回更新

for _ in range(n_critic):

optimizer_D.zero_grad()

z = torch.randn(batch_size, 100).to(device)

fake_imgs = G(z).detach()

Wasserstein損失: E[D(real)] - E[D(fake)]

d_loss = -torch.mean(D(real_imgs)) + torch.mean(D(fake_imgs))

d_loss.backward()

optimizer_D.step()

ウェイトクリッピング(Lipschitz制約)

for p in D.parameters():

p.data.clamp_(-clip_value, clip_value)

ジェネレータの更新

optimizer_G.zero_grad()

z = torch.randn(batch_size, 100).to(device)

fake_imgs = G(z)

g_loss = -torch.mean(D(fake_imgs))

g_loss.backward()

optimizer_G.step()

5.3 WGAN-GP(勾配ペナルティ)

WGANのウェイトクリッピングの代わりに、勾配ペナルティがLipschitz制約をより効果的に適用します。

def gradient_penalty(D, real_imgs, fake_imgs, device):

"""勾配ペナルティの計算"""

batch_size = real_imgs.size(0)

本物と偽物の画像間のランダムな補間

alpha = torch.rand(batch_size, 1, 1, 1).to(device)

interpolated = alpha * real_imgs + (1 - alpha) * fake_imgs

interpolated.requires_grad_(True)

d_interpolated = D(interpolated)

gradients = torch.autograd.grad(

outputs=d_interpolated,

inputs=interpolated,

grad_outputs=torch.ones_like(d_interpolated),

create_graph=True,

retain_graph=True

)[0]

gradients = gradients.view(batch_size, -1)

gradient_norm = gradients.norm(2, dim=1)

penalty = ((gradient_norm - 1) ** 2).mean()

return penalty

def wgan_gp_d_loss(D, real_imgs, fake_imgs, device, lambda_gp=10):

"""WGAN-GPディスクリミネータ損失"""

d_real = D(real_imgs).mean()

d_fake = D(fake_imgs).mean()

gp = gradient_penalty(D, real_imgs, fake_imgs, device)

return -d_real + d_fake + lambda_gp * gp

5.4 条件付きGAN(cGAN)

ラベル条件を追加することで特定のクラスの画像を生成します。

class ConditionalGenerator(nn.Module):

"""条件付きGANジェネレータ"""

def __init__(self, noise_dim=100, num_classes=10, embed_dim=50):

super().__init__()

self.label_embedding = nn.Embedding(num_classes, embed_dim)

self.model = nn.Sequential(

nn.Linear(noise_dim + embed_dim, 256),

nn.LeakyReLU(0.2),

nn.Linear(256, 512),

nn.LeakyReLU(0.2),

nn.Linear(512, 784),

nn.Tanh()

)

def forward(self, z, labels):

label_embed = self.label_embedding(labels)

x = torch.cat([z, label_embed], dim=1)

return self.model(x).view(-1, 1, 28, 28)

6. 拡散モデル

拡散モデルは今日の画像生成の標準となっています。DDPM(Denoising Diffusion Probabilistic Models、Ho et al., 2020、arXiv:2006.11239)がこの分野を切り開きました。

6.1 拡散モデルの直感

核心的なアイデアは2つのプロセスです。

**順方向プロセス(ノイズの追加)**: 本物の画像にガウスノイズを徐々に追加して純粋なノイズになるまで繰り返します。T ステップ後、標準正規分布になります。

**逆方向プロセス(ノイズの除去)**: 純粋なノイズから始めて徐々にノイズを除去して元の画像を復元します。ニューラルネットワークがこの逆方向プロセスを学習します。

6.2 順方向プロセスの数学

各ステップでノイズが追加されます。

q(x_t | x_{t-1}) = N(x_t; sqrt(1-β_t) * x_{t-1}, β_t * I)

ここでβ_tはノイズスケジュールです。

**ステップtへ直接ジャンプ(重要な性質):**

q(x_t | x_0) = N(x_t; sqrt(ā_t) * x_0, (1-ā_t) * I)

ここでā_tはsから1からtまでの(1 - β_s)の積です。

これにより任意のタイムステップtでノイズの多い画像を直接計算できます。

class NoiseSchedule:

"""ノイズスケジュールを管理"""

def __init__(self, timesteps=1000, beta_start=1e-4, beta_end=0.02):

self.timesteps = timesteps

線形ノイズスケジュール

self.betas = torch.linspace(beta_start, beta_end, timesteps)

self.alphas = 1.0 - 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.0 - self.alphas_cumprod)

逆方向プロセスの係数

self.posterior_variance = (

self.betas * (1.0 - self.alphas_cumprod_prev) /

(1.0 - self.alphas_cumprod)

)

def q_sample(self, x_start, t, noise=None):

"""順方向プロセス: x_0にtステップのノイズを追加"""

if noise is None:

noise = torch.randn_like(x_start)

sqrt_alphas_cumprod_t = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)

sqrt_one_minus_alphas_cumprod_t = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)

return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise

6.3 U-Netアーキテクチャ

拡散モデルのノイズ予測ネットワークはU-Netアーキテクチャを使用し、正弦波埋め込みを通じてタイムステップtで条件付けられます。

class SinusoidalPositionEmbeddings(nn.Module):

"""タイムステップの正弦波位置埋め込み"""

def __init__(self, dim):

super().__init__()

self.dim = dim

def forward(self, time):

device = time.device

half_dim = self.dim // 2

embeddings = np.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 ResidualBlock(nn.Module):

"""タイムステップ条件付きの残差ブロック"""

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

super().__init__()

self.time_mlp = nn.Sequential(

nn.SiLU(),

nn.Linear(time_emb_dim, out_channels)

)

self.block1 = nn.Sequential(

nn.GroupNorm(8, in_channels),

nn.SiLU(),

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

)

self.block2 = nn.Sequential(

nn.GroupNorm(8, out_channels),

nn.SiLU(),

nn.Conv2d(out_channels, out_channels, 3, padding=1)

)

self.residual_conv = (

nn.Conv2d(in_channels, out_channels, 1)

if in_channels != out_channels else nn.Identity()

)

def forward(self, x, time_emb):

h = self.block1(x)

time_emb = self.time_mlp(time_emb)[:, :, None, None]

h = h + time_emb

h = self.block2(h)

return h + self.residual_conv(x)

class SimpleUNet(nn.Module):

"""DDPMのための簡略化したU-Net"""

def __init__(self, in_channels=1, model_channels=64, time_emb_dim=256):

super().__init__()

タイムステップ埋め込み

self.time_mlp = nn.Sequential(

SinusoidalPositionEmbeddings(model_channels),

nn.Linear(model_channels, time_emb_dim),

nn.SiLU(),

nn.Linear(time_emb_dim, time_emb_dim)

)

エンコーダ

self.down1 = ResidualBlock(in_channels, model_channels, time_emb_dim)

self.down2 = ResidualBlock(model_channels, model_channels * 2, time_emb_dim)

self.pool = nn.MaxPool2d(2)

ボトルネック

self.bottleneck = ResidualBlock(model_channels * 2, model_channels * 2, time_emb_dim)

デコーダ

self.up1 = ResidualBlock(model_channels * 4, model_channels, time_emb_dim)

self.up2 = ResidualBlock(model_channels * 2, model_channels, time_emb_dim)

self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)

出力

self.final = nn.Conv2d(model_channels, in_channels, 1)

def forward(self, x, t):

time_emb = self.time_mlp(t)

エンコーダパス

x1 = self.down1(x, time_emb)

x2 = self.down2(self.pool(x1), time_emb)

ボトルネック

x_mid = self.bottleneck(x2, time_emb)

デコーダパス(スキップ接続付き)

x = self.up1(torch.cat([self.upsample(x_mid), x2], dim=1), time_emb)

x = self.up2(torch.cat([self.upsample(x), x1], dim=1), time_emb)

return self.final(x)

6.4 DDPMの学習とサンプリング

class DDPM:

"""DDPMの学習とサンプリング"""

def __init__(self, model, noise_schedule, device):

self.model = model

self.schedule = noise_schedule

self.device = device

def get_loss(self, x_start, t):

"""学習損失: ノイズ予測誤差"""

noise = torch.randn_like(x_start)

x_noisy = self.schedule.q_sample(x_start, t, noise)

ニューラルネットワークが追加されたノイズを予測

predicted_noise = self.model(x_noisy, t)

return F.mse_loss(noise, predicted_noise)

@torch.no_grad()

def p_sample(self, x_t, t):

"""逆方向プロセス: x_tからx_{t-1}へ1ステップデノイズ"""

betas_t = self.schedule.betas[t].view(-1, 1, 1, 1).to(self.device)

sqrt_one_minus_alphas_cumprod_t = (

self.schedule.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1).to(self.device)

)

sqrt_recip_alphas_t = torch.sqrt(

1.0 / self.schedule.alphas[t]

).view(-1, 1, 1, 1).to(self.device)

predicted_noise = self.model(x_t, t)

model_mean = sqrt_recip_alphas_t * (

x_t - betas_t * predicted_noise / sqrt_one_minus_alphas_cumprod_t

)

if t[0] == 0:

return model_mean

else:

posterior_variance_t = (

self.schedule.posterior_variance[t].view(-1, 1, 1, 1).to(self.device)

)

noise = torch.randn_like(x_t)

return model_mean + torch.sqrt(posterior_variance_t) * noise

@torch.no_grad()

def sample(self, batch_size, image_shape):

"""純粋なノイズから始めて画像を生成"""

img = torch.randn(batch_size, *image_shape).to(self.device)

for i in reversed(range(self.schedule.timesteps)):

t = torch.full((batch_size,), i, dtype=torch.long, device=self.device)

img = self.p_sample(img, t)

return img

def train_ddpm(epochs=100):

"""DDPMの学習"""

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

transform = transforms.Compose([

transforms.ToTensor(),

transforms.Normalize([0.5], [0.5])

])

dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)

dataloader = DataLoader(dataset, batch_size=128, shuffle=True)

model = SimpleUNet(in_channels=1).to(device)

schedule = NoiseSchedule(timesteps=1000)

ddpm = DDPM(model, schedule, device)

optimizer = optim.Adam(model.parameters(), lr=2e-4)

for epoch in range(epochs):

total_loss = 0

for batch, (x, _) in enumerate(dataloader):

x = x.to(device)

ランダムなタイムステップサンプリング

t = torch.randint(0, schedule.timesteps, (x.size(0),), device=device)

loss = ddpm.get_loss(x, t)

optimizer.zero_grad()

loss.backward()

optimizer.step()

total_loss += loss.item()

if (epoch + 1) % 10 == 0:

avg_loss = total_loss / len(dataloader)

print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")

return model, ddpm

7. DDIM - 高速サンプリング

DDPMは1000ステップのサンプリングが必要で低速です。DDIM(Denoising Diffusion Implicit Models、Song et al., 2020)は高速で決定論的なサンプリングを提供します。

7.1 DDIMのアイデア

DDIMはサンプリングプロセスを非マルコフ過程として再定義します。重要なのは同じ学習済みモデルを使用しながらサンプリングステップを大幅に削減(1000→50ステップ)することです。

@torch.no_grad()

def ddim_sample(model, schedule, batch_size, image_shape,

ddim_timesteps=50, eta=0.0, device='cpu'):

"""

DDIMサンプリング

eta=0.0: 完全に決定論的

eta=1.0: DDPMと同等

"""

均等に間隔を置いたタイムステップを選択

c = schedule.timesteps // ddim_timesteps

timestep_seq = list(range(0, schedule.timesteps, c))[::-1]

img = torch.randn(batch_size, *image_shape).to(device)

for i, t in enumerate(timestep_seq):

t_tensor = torch.full((batch_size,), t, dtype=torch.long, device=device)

t_prev = timestep_seq[i + 1] if i + 1 < len(timestep_seq) else -1

alpha_bar = schedule.alphas_cumprod[t].to(device)

alpha_bar_prev = (

schedule.alphas_cumprod[t_prev].to(device) if t_prev >= 0

else torch.tensor(1.0, device=device)

)

ノイズを予測

pred_noise = model(img, t_tensor)

x_0を予測

pred_x0 = (img - torch.sqrt(1 - alpha_bar) * pred_noise) / torch.sqrt(alpha_bar)

pred_x0 = torch.clamp(pred_x0, -1, 1)

シグマを計算

sigma = eta * torch.sqrt(

(1 - alpha_bar_prev) / (1 - alpha_bar) * (1 - alpha_bar / alpha_bar_prev)

)

x_tへの方向

pred_dir = torch.sqrt(1 - alpha_bar_prev - sigma**2) * pred_noise

次のステップの画像

noise = torch.randn_like(img) if t_prev >= 0 else 0

img = torch.sqrt(alpha_bar_prev) * pred_x0 + pred_dir + sigma * noise

return img

8. Stable Diffusion解析

Stable Diffusion(arXiv:2112.10752)はピクセル空間ではなく**潜在空間**で拡散を行うという革新的なアプローチです。

8.1 潜在拡散モデル(LDM)

高解像度画像(512x512)に直接拡散を適用するのは計算コストが高いです。LDMは:

1. VAEエンコーダで画像を潜在空間に圧縮(512x512x3 → 64x64x4)

2. 潜在空間で拡散を実行

3. VAEデコーダで元の解像度を復元

これにより計算コストを8倍以上削減できます。

8.2 Stable Diffusionのコンポーネント

テキストプロンプト → CLIPテキストエンコーダ → テキスト埋め込み

純粋ノイズ z_T → [U-Net + クロスアテンション] → 潜在 z_0

VAEデコーダ → 最終画像

**CLIPテキストエンコーダ**: テキストを意味のあるベクトル表現に変換します。

**クロスアテンション付きU-Net**: クロスアテンションを通じてテキスト埋め込みを条件として使用します。

**Classifier-Free Guidance(CFG)**: 条件付きと無条件の予測を組み合わせてテキストの忠実度を向上させます。

guided_noise = uncond_noise + guidance_scale * (cond_noise - uncond_noise)

8.3 diffusersでStable Diffusionを使用

from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler

model_id = "stabilityai/stable-diffusion-2-1"

pipe = StableDiffusionPipeline.from_pretrained(

model_id,

torch_dtype=torch.float16,

use_safetensors=True,

)

高速スケジューラを使用

pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)

pipe = pipe.to("cuda")

画像を生成

prompt = "a photorealistic landscape of mountains at sunset, 8k, highly detailed"

negative_prompt = "blurry, low quality, distorted"

image = pipe(

prompt,

negative_prompt=negative_prompt,

num_inference_steps=25, # 1000 DDPMステップ → 25ステップ

guidance_scale=7.5, # CFGスケール

height=512,

width=512,

generator=torch.Generator("cuda").manual_seed(42)

).images[0]

image.save("generated_image.png")

画像から画像への変換

from diffusers import StableDiffusionImg2ImgPipeline

from PIL import Image

img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(

model_id,

torch_dtype=torch.float16

).to("cuda")

init_image = Image.open("input.jpg").resize((512, 512))

result = img2img_pipe(

prompt="a painting in Van Gogh style",

image=init_image,

strength=0.75, # 変換の度合い(0-1)

guidance_scale=7.5,

num_inference_steps=50

).images[0]

8.4 インペインティング

from diffusers import StableDiffusionInpaintPipeline

inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(

"runwayml/stable-diffusion-inpainting",

torch_dtype=torch.float16

).to("cuda")

image = Image.open("photo.jpg").resize((512, 512))

mask = Image.open("mask.jpg").resize((512, 512)) # 白=インペイント領域

result = inpaint_pipe(

prompt="a beautiful garden with flowers",

image=image,

mask_image=mask,

num_inference_steps=50

).images[0]

9. ControlNet - 細かい画像制御

9.1 ControlNetアーキテクチャ

ControlNet(Zhang et al., 2023)はStable Diffusionに追加の制御信号(Cannyエッジ、深度マップ、ポーズなど)を条件として追加できます。

元のU-Netの重みは凍結され、別の制御ネットワークが追加されます。SDエンコーダのコピーとして約3億6千万個の追加パラメータを学習します。

from diffusers import StableDiffusionControlNetPipeline, ControlNetModel

from diffusers.utils import load_image

Canny ControlNetを読み込み

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.jpg")

image_array = np.array(image)

canny = cv2.Canny(image_array, 100, 200)

canny_image = Image.fromarray(canny)

ControlNetで生成

result = pipe(

prompt="a beautiful oil painting",

image=canny_image, # 制御信号としてのCannyエッジ

controlnet_conditioning_scale=1.0,

num_inference_steps=50,

guidance_scale=7.5

).images[0]

10. 生成モデルの現在のトレンド

10.1 DiT(拡散トランスフォーマー)

PeeblesとXie(2022年)が提案したDiTは、U-Netの代わりにTransformerアーキテクチャを拡散モデルのバックボーンとして使用します。Sora、Flux、SD3などの現代的なモデルはDiTベースです。

class DiTBlock(nn.Module):

"""拡散トランスフォーマーブロック"""

def __init__(self, hidden_dim, num_heads, mlp_ratio=4.0):

super().__init__()

self.norm1 = nn.LayerNorm(hidden_dim)

self.attn = nn.MultiheadAttention(hidden_dim, num_heads, batch_first=True)

self.norm2 = nn.LayerNorm(hidden_dim)

mlp_dim = int(hidden_dim * mlp_ratio)

self.mlp = nn.Sequential(

nn.Linear(hidden_dim, mlp_dim),

nn.GELU(),

nn.Linear(mlp_dim, hidden_dim)

)

AdaLN(アダプティブ層正規化): タイムステップ条件付け

self.adaLN_modulation = nn.Sequential(

nn.SiLU(),

nn.Linear(hidden_dim, 6 * hidden_dim)

)

def forward(self, x, c):

タイムステップ/条件埋め込みから変調パラメータを計算

shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (

self.adaLN_modulation(c).chunk(6, dim=-1)

)

変調された正規化

x_norm = (1 + scale_msa.unsqueeze(1)) * self.norm1(x) + shift_msa.unsqueeze(1)

attn_out, _ = self.attn(x_norm, x_norm, x_norm)

x = x + gate_msa.unsqueeze(1) * attn_out

x_norm = (1 + scale_mlp.unsqueeze(1)) * self.norm2(x) + shift_mlp.unsqueeze(1)

x = x + gate_mlp.unsqueeze(1) * self.mlp(x_norm)

return x

10.2 SDXL(Stable Diffusion XL)

高解像度(1024x1024)画像を生成するStable Diffusionのアップグレード版です。

from diffusers import StableDiffusionXLPipeline

pipe = StableDiffusionXLPipeline.from_pretrained(

"stabilityai/stable-diffusion-xl-base-1.0",

torch_dtype=torch.float16,

use_safetensors=True,

variant="fp16"

).to("cuda")

image = pipe(

prompt="a majestic lion in photorealistic style, 4k",

negative_prompt="cartoon, blurry",

height=1024,

width=1024,

num_inference_steps=50,

guidance_scale=5.0

).images[0]

10.3 生成モデルの比較

| モデル | 年 | 核心アイデア | 強み | 弱み |

| ------ | ---- | ----------------------- | ------------------------ | ------------------------ |

| VAE | 2013 | 潜在分布を学習 | 安定、解釈可能な潜在空間 | ぼやけた画像 |

| GAN | 2014 | 敵対的学習 | シャープな画像 | 不安定な学習、モード崩壊 |

| DDPM | 2020 | 反復的デノイズ | 高品質、多様性 | 遅いサンプリング |

| LDM | 2022 | 潜在空間拡散 | 効率的、テキスト条件付け | 複雑なアーキテクチャ |

| DiT | 2022 | Transformerバックボーン | スケーリング効率 | 高い計算コスト |

まとめ

生成AIの旅を完全に探求しました。VAEの潜在空間理論から、GANの敵対的ゲーム、DDPMの反復的デノイズ、そして最終的にStable Diffusionの潜在空間拡散まで。

この分野は非常に速く進化します。今日の最先端は明日のベースラインになります。基礎を深く理解しながら、継続的に新しい研究論文をフォローすることが不可欠です。

継続学習のための推奨リソース:

- Hugging Face Diffusers: https://huggingface.co/docs/diffusers/

- VAE論文: arXiv:1312.6114

- GAN論文: arXiv:1406.2661

- DDPM論文: arXiv:2006.11239

- 潜在拡散 / Stable Diffusion: arXiv:2112.10752

参考文献

- Kingma, D. P., & Welling, M. (2013). Auto-Encoding Variational Bayes. arXiv:1312.6114

- Goodfellow, I., et al. (2014). Generative Adversarial Networks. arXiv:1406.2661

- Ho, J., et al. (2020). Denoising Diffusion Probabilistic Models. arXiv:2006.11239

- Rombach, R., et al. (2022). High-Resolution Image Synthesis with Latent Diffusion Models. arXiv:2112.10752

- Arjovsky, M., et al. (2017). Wasserstein GAN. arXiv:1701.07875

- Peebles, W., & Xie, S. (2022). Scalable Diffusion Models with Transformers. arXiv:2212.09748

- Zhang, L., et al. (2023). Adding Conditional Control to Text-to-Image Diffusion Models. arXiv:2302.05543

- Hugging Face Diffusers: https://huggingface.co/docs/diffusers/

현재 단락 (1/795)

生成AIは今日のテクノロジーで最もホットな分野です。DALL-E 3は1行のテキストからリアルな画像を生成し、Stable Diffusionはアートワークを生成し、Soraは動画を生成します。これら...

작성 글자: 0원문 글자: 27,306작성 단락: 0/795