PyTorch 生成対抗ネットワーク (GAN)
生成対抗ネットワーク(Generative Adversarial Network、GAN)は深層学習の中でも最も創造的なモデルアーキテクチャの一つです。2つのニューラルネットワークを互いに対抗させ、相互に学習させることで、最終的には非常にリアルなデータを生成できます。GAN は画像生成、スタイル変換、データ拡張などのシーンで広く応用されています。
1. GAN の核心原理
GAN の核心的な考え方はゲーム理論における「ゼロサムゲーム」に由来します。GAN には互いに対抗する2つのネットワークが含まれます:
- 生成器(Generator):偽データの生成を学習し、判別器が生成データと本物のデータを区別できないようにすることを目指します
- 判別器(Discriminator):本物のデータと生成データを区別することを学習し、できるだけ正確に判断することを目指します
両者はトレーニングプロセスにおいて互いに対抗し、継続的に向上し、最終的にナッシュ均衡状態に達します。
1.1 GAN の目的関数
GAN のトレーニング目標は以下のミニマックスゲームとして表すことができます:
\[ \min_G \max_D \mathbb{E}_{x \sim p_{data}(x)}[\log D(x)] + \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))] \]
ここで:
- \(G\) は生成器ネットワークを表します
- \(D\) は判別器ネットワークを表します
- \(x\) は本物のデータを表します
- \(z\) はランダムノイズベクトルを表します(通常は標準正規分布に従います)
- \(G(z)\) は生成器がノイズに基づいて生成した偽データを表します
1.2 トレーニングプロセスの解説
GAN のトレーニングは2つの段階に分かれます:
第1段階:判別器のトレーニング
生成器を固定し、判別器の識別能力を高めます:
\[ \max_D \mathbb{E}_{x \sim p_{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]
第2段階:生成器のトレーニング
判別器を固定し、生成器の欺瞞能力を高めます:
\[ \min_G \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]
実際のトレーニングでは、通常、まず判別器を k ステップトレーニングし、次に生成器を 1 ステップトレーニングして、バランスを保ちます。
2. 基本 GAN の実装
以下では、2次元データポイントを生成するための最も単純な GAN を実装します。
2.1 生成器と判別器の定義
例
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
# ランダムシードを設定
torch.manual_seed(42)
# ── 生成器ネットワーク ──────────────────────────────────────
class Generator(nn.Module):
"""
生成器:ランダムノイズからデータを生成
入力:ノイズベクトル (batch_size, noise_dim)
出力:生成データ (batch_size, data_dim)
"""
def __init__(self, noise_dim, data_dim, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(noise_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, data_dim),
# 出力は活性化せず、GAN が適切な分布を学習します
)
def forward(self, x):
return self.net(x)
# ── 判別器ネットワーク ──────────────────────────────────────
class Discriminator(nn.Module):
"""
判別器:本物のデータと生成データを区別
入力:データポイント (batch_size, data_dim)
出力:本物のデータの確率 (batch_size, 1)
"""
def __init__(self, data_dim, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(data_dim, hidden_dim),
nn.LeakyReLU(0.2), # LeakyReLU で勾配消失を防止
nn.Linear(hidden_dim, hidden_dim),
nn.LeakyReLU(0.2),
nn.Linear(hidden_dim, 1),
nn.Sigmoid() # 出力確率
)
def forward(self, x):
return self.net(x)
# ハイパーパラメータ
NOISE_DIM = 16
DATA_DIM = 2
HIDDEN_DIM = 64
BATCH_SIZE = 128
# ネットワークを作成
generator = Generator(NOISE_DIM, DATA_DIM, HIDDEN_DIM)
discriminator = Discriminator(DATA_DIM, HIDDEN_DIM)
print(f"生成器のパラメータ数: {sum(p.numel() for p in generator.parameters()):,}")
print(f"判別器のパラメータ数: {sum(p.numel() for p in discriminator.parameters()):,}")
2.2 トレーニングループ
例
lr = 0.001
g_optimizer = optim.Adam(generator.parameters(), lr=lr)
d_optimizer = optim.Adam(discriminator.parameters(), lr=lr)
# 損失関数:二値分類クロスエントロピー
criterion = nn.BCELoss()
# ── トレーニングデータ:環状分布 ──────────────────────────
def generate_real_data(batch_size):
"""環状分布の本物のデータを生成"""
angles = torch.rand(batch_size) * 2 * torch.pi
radius = 1.0 + torch.randn(batch_size) * 0.1 # 半径は約 1
x = radius * torch.cos(angles)
y = radius * torch.sin(angles)
return torch.stack([x, y], dim=1)
# ── トレーニングループ ──────────────────────────────────────
NUM_EPOCHS = 1000
d_losses = []
g_losses = []
for epoch in range(NUM_EPOCHS):
# 1. 判別器をトレーニング
# 偽データを生成
noise = torch.randn(BATCH_SIZE, NOISE_DIM)
fake_data = generator(noise).detach() # detach で生成器の勾配計算を回避
# 本物のデータを生成
real_data = generate_real_data(BATCH_SIZE)
# 判別器の損失
real_pred = discriminator(real_data)
fake_pred = discriminator(fake_data)
d_loss = criterion(real_pred, torch.ones_like(real_pred)) + \
criterion(fake_pred, torch.zeros_like(fake_pred))
# 判別器を更新
d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()
# 2. 生成器をトレーニング
# 新しいバッチの偽データを生成
noise = torch.randn(BATCH_SIZE, NOISE_DIM)
fake_data = generator(noise)
# 生成器の損失:判別器に生成データが本物だと思わせる
fake_pred = discriminator(fake_data)
g_loss = criterion(fake_pred, torch.ones_like(fake_pred))
# 生成器を更新
g_optimizer.zero_grad()
g_loss.backward()
g_optimizer.step()
# 損失を記録
d_losses.append(d_loss.item())
g_losses.append(g_loss.item())
if (epoch + 1) % 100 == 0:
print(f"Epoch {epoch+1:4d} | D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")
print(トレーニング完了!)
2.3 生成結果の可視化
例
def visualize_results(generator, num_samples=1000):
noise = torch.randn(num_samples, NOISE_DIM)
generated_data = generator(noise).detach().numpy()
plt.figure(figsize=(6, 6))
plt.scatter(generated_data[:, 0], generated_data[:, 1],
alpha=0.5, s=10, c='blue', label='Generated')
plt.xlim(-2, 2)
plt.ylim(-2, 2)
plt.xlabel('x')
plt.ylabel('y')
plt.title('GAN Generated Data')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# 生成結果を確認
visualize_results(generator)
3. DCGAN - 深層畳み込み GAN
DCGAN は畳み込みニューラルネットワークを GAN に導入した古典的なアーキテクチャであり、画像生成の品質を大幅に向上させました。
3.1 DCGAN アーキテクチャの要点
- 転置畳み込み(Transposed Convolution)を使用してアップサンプリングし、画像を生成します
- ストライド付き畳み込みを使用してダウンサンプリングし、画像を判別します
- 生成器と判別器で BatchNorm を使用します(ただし、出力層と入力層では使用しません)
- 生成器は ReLU を使用し、判別器は LeakyReLU を使用します
3.2 DCGAN の実装
例
import torch.nn as nn
# ── DCGAN 生成器 ─────────────────────────────────
class DCGenerator(nn.Module):
"""
DCGAN 生成器:転置畳み込みでアップサンプリング
"""
def __init__(self, noise_dim=100, channels=3, features_g=64):
super().__init__()
self.noise_dim = noise_dim
# 入力: noise_dim x 1 x 1
self.net = nn.Sequential(
# 転置畳み込み: (batch, features_g*8, 4, 4)
nn.ConvTranspose2d(noise_dim, features_g * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(features_g * 8),
nn.ReLU(True),
# (batch, features_g*4, 8, 8)
nn.ConvTranspose2d(features_g * 8, features_g * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g * 4),
nn.ReLU(True),
# (batch, features_g*2, 16, 16)
nn.ConvTranspose2d(features_g * 4, features_g * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g * 2),
nn.ReLU(True),
# (batch, features_g, 32, 32)
nn.ConvTranspose2d(features_g * 2, features_g, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_g),
nn.ReLU(True),
# 出力: (batch, channels, 64, 64)
nn.ConvTranspose2d(features_g, channels, 4, 2, 1, bias=False),
nn.Tanh() # 出力範囲 [-1, 1]
)
def forward(self, x):
# x: (batch, noise_dim) -> (batch, noise_dim, 1, 1)
x = x.view(x.size(0), x.size(1), 1, 1)
return self.net(x)
# ── DCGAN 判別器 ─────────────────────────────────
class DCDiscriminator(nn.Module):
"""
DCGAN 判別器:畳み込みでダウンサンプリング
"""
def __init__(self, channels=3, features_d=64):
super().__init__()
# 入力: (batch, channels, 64, 64)
self.net = nn.Sequential(
# (batch, features_d, 32, 32)
nn.Conv2d(channels, features_d, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*2, 16, 16)
nn.Conv2d(features_d, features_d * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 2),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*4, 8, 8)
nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 4),
nn.LeakyReLU(0.2, inplace=True),
# (batch, features_d*8, 4, 4)
nn.Conv2d(features_d * 4, features_d * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(features_d * 8),
nn.LeakyReLU(0.2, inplace=True),
# 出力: (batch, 1, 1, 1)
nn.Conv2d(features_d * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).view(x.size(0), -1)
# ネットワークをテスト
noise_dim = 100
generator = DCGenerator(noise_dim=noise_dim, channels=3, features_g=64)
discriminator = DCDiscriminator(channels=3, features_d=64)
# フォワード伝播をテスト
noise = torch.randn(2, noise_dim)
fake_images = generator(noise)
print(f"生成画像の形状: {fake_images.shape}") # torch.Size([2, 3, 64, 64])
decision = discriminator(fake_images)
print(f"判別結果の形状: {decision.shape}") # torch.Size([2, 1])
3.3 完全な DCGAN トレーニングコード
例
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用デバイス: {device}")
NOISE_DIM = 100
LEARNING_RATE = 0.0002
BETA1 = 0.5 # Adam パラメータ
# ネットワークを作成
generator = DCGenerator(noise_dim=NOISE_DIM).to(device)
discriminator = DCDiscriminator().to(device)
# オプティマイザ
g_optimizer = optim.Adam(generator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
criterion = nn.BCELoss()
# ── トレーニングループ ─────────────────────────────────────
fixed_noise = torch.randn(64, NOISE_DIM, device=device) # 可視化用
def train_dcgan(generator, discriminator, g_optimizer, d_optimizer, criterion,
num_epochs, device, fixed_noise):
G_losses = []
D_losses = []
for epoch in range(num_epochs):
for batch_idx in range(100): # 各エポックに100バッチあると仮定する
# 判别器を訓練する
discriminator.zero_grad()
# 実画像(既にあると仮定)
# real_images = ...
# ここではランダムノイズで模擬する
real_images = torch.randn(32, 3, 64, 64).to(device)
batch_size = real_images.size(0)
labels_real = torch.ones(batch_size, 1).to(device)
labels_fake = torch.zeros(batch_size, 1).to(device)
# 実画像の損失
output = discriminator(real_images)
d_loss_real = criterion(output, labels_real)
# 生成画像の損失
noise = torch.randn(batch_size, NOISE_DIM).to(device)
fake_images = generator(noise)
output = discriminator(fake_images.detach())
d_loss_fake = criterion(output, labels_fake)
# 合計損失
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
d_optimizer.step()
# 生成器を訓練する
generator.zero_grad()
noise = torch.randn(batch_size, NOISE_DIM).to(device)
fake_images = generator(noise)
output = discriminator(fake_images)
g_loss = criterion(output, labels_real) # 生成画像が本物と判定されることを期待
g_loss.backward()
g_optimizer.step()
# 損失を記録
if batch_idx % 50 == 0:
G_losses.append(g_loss.item())
D_losses.append(d_loss.item())
print(f"[{epoch}/{num_epochs}][{batch_idx}/100] "
f"D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")
return G_losses, D_losses
# 訓練開始
# G_losses, D_losses = train_dcgan(generator, discriminator, g_optimizer,
# d_optimizer, criterion, 5, device, fixed_noise)
print("DCGANアーキテクチャの定義が完了しました。訓練を開始できます!")
4. GAN のトレーニングテクニック
4.1 よくある問題と解決策
| 問題 | 原因 | 解決策 |
|---|---|---|
| モード崩壊(Mode Collapse) | 生成器が限られた種類のサンプルしか生成しない | WGANを使用、ミニバッチ判別を追加、ラベル平滑化を使用 |
| 判别器が強すぎる | 生成器の勾配が消失し、学習できない | 生成器を複数回訓練、判别器の学習率を下げる、LeakyReLUを使用 |
| 訓練が不安定 | GANの目的関数は非凸で、収束が難しい | スペクトル正規化、勾配ペナルティ、学習率ウォームアップを使用 |
| 生成品質が低い | ネットワーク容量の不足または訓練不足 | ネットワークの深さを増やす、より多くの訓練データを使用、より長く訓練 |
4.2 損失関数の改善
元のGANはJSダイバージェンスを使用しており、勾配消失問題があります。WGANはWasserstein距離を使用してより安定しています:
例
def wgan_d_loss(real_pred, fake_pred):
"""判别器損失:実サンプルはスコアが高く、生成サンプルはスコアが低い"""
return -(torch.mean(real_pred) - torch.mean(fake_pred))
def wgan_g_loss(fake_pred):
"""生成器損失:生成サンプルのスコアを高くする"""
return -torch.mean(fake_pred)
# 勾配ペナルティ(Gradient Penalty)- WGAN-GP
def gradient_penalty(discriminator, real_images, fake_images, device):
"""WGAN-GP 勾配ペナルティ項"""
batch_size = real_images.size(0)
alpha = torch.rand(batch_size, 1, 1, 1).to(device)
# 実画像と生成画像の間で補間
interpolated = alpha * real_images + (1 - alpha) * fake_images
interpolated.requires_grad = True
# 補間画像の判别器出力を計算
pred = discriminator(interpolated)
# 勾配を計算
gradients = torch.autograd.grad(
outputs=pred,
inputs=interpolated,
grad_outputs=torch.ones_like(pred),
create_graph=True,
retain_graph=True,
only_inputs=True
)[0]
# 勾配ノルムを計算
gradients = gradients.view(batch_size, -1)
gradient_norm = gradients.norm(2, dim=1)
penalty = ((gradient_norm - 1) ** 2).mean()
return penalty
4.3 スペクトル正規化
スペクトル正規化はGANの訓練を安定させ、判别器のLipschitz定数を制御できます:
例
# スペクトル正規化を使用した判别器
class SNDiscriminator(nn.Module):
def __init__(self, channels=3, features_d=64):
super().__init__()
self.net = nn.Sequential(
spectral_norm(nn.Conv2d(channels, features_d, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d, features_d * 2, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1)),
nn.LeakyReLU(0.2, inplace=True),
spectral_norm(nn.Conv2d(features_d * 4, 1, 4, 1, 0)),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).view(x.size(0), -1)
5. 条件付き GAN (cGAN)
条件付きGANは、生成データのクラスラベルを指定でき、条件付き生成を実現します。
5.1 cGAN アーキテクチャ
例
import torch.nn as nn
class ConditionalGenerator(nn.Module):
"""条件付き生成器:ノイズとクラスラベルの両方を受け取る"""
def __init__(self, noise_dim, num_classes, embed_dim, img_channels, features_g=64):
super().__init__()
self.label_emb = nn.Embedding(num_classes, embed_dim)
# ノイズとラベル埋め込みを連結
self.net = nn.Sequential(
nn.Linear(noise_dim + embed_dim, features_g * 8 * 4 * 4),
nn.BatchNorm1d(features_g * 8 * 4 * 4),
nn.ReLU(),
# その後、転置畳み込みを接続(DCGANと同様)
# ...
)
def forward(self, noise, labels):
# クラスラベルをノイズと同じ次元に埋め込む
label_embedding = self.label_emb(labels)
# ノイズとラベル埋め込みを連結
x = torch.cat([noise, label_embedding], dim=1)
return self.net(x)
class ConditionalDiscriminator(nn.Module):
"""条件付き判别器:画像とクラスラベルの両方を受け取る"""
def __init__(self, img_channels, num_classes, embed_dim, features_d=64):
super().__init__()
self.label_emb = nn.Embedding(num_classes, embed_dim)
# 画像とラベル埋め込みを連結
self.net = nn.Sequential(
nn.Conv2d(img_channels + embed_dim, features_d, 4, 2, 1),
nn.LeakyReLU(0.2),
# ...
)
def forward(self, img, labels):
# ラベル埋め込みを画像と同じ空間サイズに調整
label_embedding = self.label_emb(labels)
# 連結のために形状を調整
label_embedding = label_embedding.unsqueeze(2).unsqueeze(3)
label_embedding = label_embedding.expand(-1, -1, img.size(2), img.size(3))
# 画像とラベルを連結
x = torch.cat([img, label_embedding], dim=1)
return self.net(x)
6. GAN評価指標
6.1 一般的な評価指標
| 指標 | 説明 | 利点 | 欠点 |
|---|---|---|---|
| Inception Score (IS) | Inception v3を使用して生成画像の品質と多様性を評価 | 計算が簡単で、人間の判断とある程度相関がある | 過適合を評価せず、モード崩壊を検出できない |
| Fréchet Inception Distance (FID) | 実画像と生成画像の特徴空間での距離を計算 | ノイズとモード崩壊により敏感 | 多くのサンプルが必要で、計算が遅い |
| 人間による評価 | 人間が生成画像の品質を判断 | 人間の知覚を最も正確に反映 | 主観的で、時間がかかる |
6.2 FID 計算の実装
例
from scipy import linalg
def calculate_fid(real_activations, fake_activations):
"""
Fréchet Inception Distanceを計算
real_activations: 実画像の特徴ベクトル (N, dim)
fake_activations: 生成画像の特徴ベクトル (N, dim)
"""
# 平均と共分散を計算
mu1, sigma1 = real_activations.mean(axis=0), np.cov(real_activations, rowvar=False)
mu2, sigma2 = fake_activations.mean(axis=0), np.cov(fake_activations, rowvar=False)
# FIDを計算
diff = mu1 - mu2
# 共分散行列の和を計算
covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)
# 複素数を避ける
if np.iscomplexobj(covmean):
covmean = covmean.real
fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2 * covmean)
return fid
# 簡略化した例:ランダムデータを使用
np.random.seed(42)
real_acts = np.random.randn(1000, 2048) # Inception v3 の出力次元
fake_acts = np.random.randn(1000, 2048)
fid_score = calculate_fid(real_acts, fake_acts)
print(f"FID Score: {fid_score:.2f}")
# FIDは低いほど良く、通常50未満で良好な生成品質を示す
7. 一般的な GAN の変種
GANは現在までに多くの変種を生み出し、さまざまな応用シーンに適しています:
| モデル | 正式名称 | 特徴 | 適用シーン |
|---|---|---|---|
| DCGAN | Deep Convolutional GAN | 畳み込みネットワークを使用し、高品質な画像を生成 | 画像生成 |
| WGAN | Wasserstein GAN | Wasserstein距離を使用し、訓練がより安定 | 安定した訓練 |
| WGAN-GP | WGAN with Gradient Penalty | 重みクリッピングの代わりに勾配ペナルティ | 安定した訓練 |
| CGAN | Conditional GAN | 条件情報を追加し、制御可能な生成 | 条件付き生成 |
| CycleGAN | Cycle-Consistent GAN | 教師なし画像変換 | スタイル変換 |
| StyleGAN | Style-Based GAN | スタイル制御、高品質な顔生成 | 顔生成 |
| BigGAN | Big GAN | 大規模で高品質な画像生成 | 高解像度画像 |
| ProGAN | Progressive Growing GAN | 段階的に解像度を向上 | 高解像度生成 |