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
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
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距離を使用してより安定しています:

例

# WGAN損失関数(BCEの代替)
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定数を制御できます:

例

import torch.nn.utils.spectral_norm as spectral_norm

# スペクトル正規化を使用した判别器
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
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 計算の実装

例

import numpy as np
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 段階的に解像度を向上 高解像度生成
その他の拡張