GAN(Generative Adversarial Networks、敵対的生成ネットワーク)は、2014年にIan Goodfellowらによって提案された生成モデルです。2つのニューラルネットワークを「敵対的」に競わせることで、高品質なデータを生成できるようになるという画期的なアイデアで、深層生成モデルの研究に大きなブレイクスルーをもたらしました。
同じ生成モデルであるVAE(変分オートエンコーダ)が確率分布を明示的にモデル化する手法であるのに対し、GANはデータの分布を暗黙的に学習します。VAEでは変分下限(ELBO)を最大化するのに対して、GANでは生成器と識別器の間のミニマックスゲームを解くという、ゲーム理論的な枠組みに基づいています。
本記事の内容
- GANの基本構造(生成器と識別器)
- ミニマックスゲームの目的関数の定式化
- 最適な識別器と生成器の導出(JSダイバージェンスとの関係)
- 学習アルゴリズムと不安定性への対処
- PyTorchでの1次元GAN実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
GANの基本構造
GANは、生成器(Generator) $G$ と 識別器(Discriminator) $D$ という2つのニューラルネットワークで構成されます。
生成器 $G$ は、潜在空間(ノイズ空間)のベクトル $\bm{z}$ を入力として、データ空間にマッピングする関数です。
$$ G: \bm{z} \in \mathbb{R}^{d_z} \mapsto G(\bm{z}) \in \mathbb{R}^{d_x} $$
ここで、$\bm{z}$ はノイズ分布 $p_z(\bm{z})$(通常は標準正規分布やー様分布)からサンプリングされます。生成器が出力するデータの分布を $p_g$ と書きます。
識別器 $D$ は、データ空間のベクトル $\bm{x}$ を入力として、それが本物のデータである確率を出力する関数です。
$$ D: \bm{x} \in \mathbb{R}^{d_x} \mapsto D(\bm{x}) \in [0, 1] $$
$D(\bm{x})$ の値が $1$ に近ければ「本物」、$0$ に近ければ「偽物(生成器が作ったデータ)」と判定していることになります。
イメージとしては、生成器は「偽札を作る贋作者」、識別器は「偽札を見破る鑑定士」のような関係です。贋作者はより精巧な偽札を作ろうとし、鑑定士はより正確に偽札を見破ろうとします。この競争を通じて、最終的に贋作者は本物と区別がつかない偽札を作れるようになる、というのがGANの直感的な理解です。
ミニマックスゲームの定式化
GANの学習は、以下の ミニマックスゲーム の目的関数 $V(D, G)$ を解くことで行われます。
$$ \min_G \max_D V(D, G) = \mathbb{E}_{\bm{x} \sim p_{\text{data}}(\bm{x})}[\log D(\bm{x})] + \mathbb{E}_{\bm{z} \sim p_z(\bm{z})}[\log(1 – D(G(\bm{z})))] $$
この目的関数の各項の意味を整理しましょう。
第1項 $\mathbb{E}_{\bm{x} \sim p_{\text{data}}}[\log D(\bm{x})]$ は、本物のデータ $\bm{x}$ に対する識別器の出力の対数期待値です。識別器は本物のデータに対して $D(\bm{x}) \to 1$ を出力したいので、この項を大きくしたいと考えます。
第2項 $\mathbb{E}_{\bm{z} \sim p_z}[\log(1 – D(G(\bm{z})))]$ は、生成されたデータ $G(\bm{z})$ に対する識別器の出力に関する項です。識別器は偽物のデータに対して $D(G(\bm{z})) \to 0$ を出力したい、すなわち $\log(1 – D(G(\bm{z}))) \to \log 1 = 0$ に近づけたいので、この項も大きくしたいと考えます。
一方、生成器の立場からすると、$D(G(\bm{z})) \to 1$ となるように(つまり識別器を騙せるように)学習したいので、$\log(1 – D(G(\bm{z}))) \to \log 0 = -\infty$ にしたい、すなわち $V(D, G)$ を小さくしたいと考えます。
このように、識別器は $V(D, G)$ を最大化し、生成器は $V(D, G)$ を最小化するという、互いに相反する目的を持つゲームになっています。
最適な識別器の導出
まず、生成器 $G$ を固定した状態で、目的関数 $V(D, G)$ を最大化する最適な識別器 $D^*$ を求めましょう。
目的関数を、生成器が誘導する分布 $p_g$ を用いて書き直します。$\bm{x} = G(\bm{z})$ と置くと、$\bm{z} \sim p_z$ のもとで $\bm{x} \sim p_g$ となるので、
$$ V(D, G) = \mathbb{E}_{\bm{x} \sim p_{\text{data}}}[\log D(\bm{x})] + \mathbb{E}_{\bm{x} \sim p_g}[\log(1 – D(\bm{x}))] $$
と書けます。これを期待値の定義(積分形)で展開すると、
$$ V(D, G) = \int_{\bm{x}} \left[ p_{\text{data}}(\bm{x}) \log D(\bm{x}) + p_g(\bm{x}) \log(1 – D(\bm{x})) \right] d\bm{x} $$
となります。各点 $\bm{x}$ において、被積分関数を $D(\bm{x})$ について最大化します。$a = p_{\text{data}}(\bm{x})$、$b = p_g(\bm{x})$、$y = D(\bm{x})$ と置くと、最大化すべき関数は、
$$ f(y) = a \log y + b \log(1 – y) $$
です。$f(y)$ を $y$ で微分して $0$ と置くと、
$$ \begin{align} f'(y) &= \frac{a}{y} – \frac{b}{1 – y} = 0 \\ \frac{a}{y} &= \frac{b}{1 – y} \\ a(1 – y) &= by \\ a &= (a + b)y \\ y &= \frac{a}{a + b} \end{align} $$
ここで $a > 0$ かつ $b > 0$ のとき、$f”(y) = -\frac{a}{y^2} – \frac{b}{(1-y)^2} < 0$ なので、確かに極大(最大)であることが確認できます。
したがって、最適な識別器は、
$$ \begin{equation} D^*(\bm{x}) = \frac{p_{\text{data}}(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})} \end{equation} $$
となります。この結果は直感的にも納得できます。ある点 $\bm{x}$ における本物のデータの密度が高く、生成データの密度が低ければ $D^*(\bm{x}) \to 1$(本物と判定)、逆であれば $D^*(\bm{x}) \to 0$(偽物と判定)となります。
最適な識別器のもとでの生成器の目的関数
最適な識別器 $D^*$ を目的関数に代入することで、生成器が実質的に何を最小化しているのかを明らかにしましょう。
$D^*(\bm{x}) = \frac{p_{\text{data}}(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}$ を $V(D^*, G)$ に代入します。
$$ \begin{align} V(D^*, G) &= \mathbb{E}_{\bm{x} \sim p_{\text{data}}}\left[\log \frac{p_{\text{data}}(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}\right] + \mathbb{E}_{\bm{x} \sim p_g}\left[\log \frac{p_g(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}\right] \end{align} $$
ここで、Jensen-Shannon ダイバージェンス(JSダイバージェンス) の定義を思い出しましょう。2つの確率分布 $p$ と $q$ に対して、
$$ \text{JSD}(p \| q) = \frac{1}{2} D_{\text{KL}}\left(p \,\middle\|\, \frac{p + q}{2}\right) + \frac{1}{2} D_{\text{KL}}\left(q \,\middle\|\, \frac{p + q}{2}\right) $$
と定義されます。$D_{\text{KL}}$ はKLダイバージェンスです。
それでは、$V(D^*, G)$ をJSダイバージェンスの形に変形していきます。$m(\bm{x}) = \frac{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}{2}$ と置くと、
$$ \begin{align} V(D^*, G) &= \mathbb{E}_{\bm{x} \sim p_{\text{data}}}\left[\log \frac{p_{\text{data}}(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}\right] + \mathbb{E}_{\bm{x} \sim p_g}\left[\log \frac{p_g(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}\right] \\ &= \mathbb{E}_{\bm{x} \sim p_{\text{data}}}\left[\log \frac{p_{\text{data}}(\bm{x})}{2 m(\bm{x})}\right] + \mathbb{E}_{\bm{x} \sim p_g}\left[\log \frac{p_g(\bm{x})}{2 m(\bm{x})}\right] \\ &= \mathbb{E}_{\bm{x} \sim p_{\text{data}}}\left[\log \frac{p_{\text{data}}(\bm{x})}{m(\bm{x})} – \log 2\right] + \mathbb{E}_{\bm{x} \sim p_g}\left[\log \frac{p_g(\bm{x})}{m(\bm{x})} – \log 2\right] \\ &= \mathbb{E}_{\bm{x} \sim p_{\text{data}}}\left[\log \frac{p_{\text{data}}(\bm{x})}{m(\bm{x})}\right] + \mathbb{E}_{\bm{x} \sim p_g}\left[\log \frac{p_g(\bm{x})}{m(\bm{x})}\right] – 2 \log 2 \\ &= D_{\text{KL}}\left(p_{\text{data}} \,\middle\|\, m\right) + D_{\text{KL}}\left(p_g \,\middle\|\, m\right) – 2 \log 2 \\ &= 2 \cdot \text{JSD}(p_{\text{data}} \| p_g) – 2 \log 2 \end{align} $$
したがって、
$$ \begin{equation} V(D^*, G) = 2 \cdot \text{JSD}(p_{\text{data}} \| p_g) – 2 \log 2 \end{equation} $$
が得られます。JSダイバージェンスは常に非負であり、$p_{\text{data}} = p_g$ のとき、かつそのときに限り $0$ となります。よって、
$$ V(D^*, G) \geq -2 \log 2 $$
であり、等号は $p_g = p_{\text{data}}$ のときに達成されます。
これは非常に重要な結果です。最適な識別器のもとでは、生成器の目的関数を最小化することは、生成分布 $p_g$ と真のデータ分布 $p_{\text{data}}$ のJSダイバージェンスを最小化することと等価です。つまり、GANの学習が収束すると、生成器は真のデータ分布を再現する、ということを意味しています。
学習アルゴリズム
GANの学習は、識別器と生成器を交互に最適化する手続きで行われます。具体的なアルゴリズムは以下の通りです。
アルゴリズム: GANの学習
各エポックについて以下を繰り返します。
ステップ1: 識別器の更新($k$ 回繰り返す)
- ノイズ分布 $p_z(\bm{z})$ からミニバッチ $\{\bm{z}^{(1)}, \dots, \bm{z}^{(m)}\}$ をサンプリング
- 学習データ分布 $p_{\text{data}}(\bm{x})$ からミニバッチ $\{\bm{x}^{(1)}, \dots, \bm{x}^{(m)}\}$ をサンプリング
- 識別器のパラメータ $\theta_d$ を、以下の勾配で更新(勾配上昇法):
$$ \nabla_{\theta_d} \frac{1}{m} \sum_{i=1}^{m} \left[ \log D(\bm{x}^{(i)}) + \log(1 – D(G(\bm{z}^{(i)}))) \right] $$
ステップ2: 生成器の更新(1回)
- ノイズ分布 $p_z(\bm{z})$ からミニバッチ $\{\bm{z}^{(1)}, \dots, \bm{z}^{(m)}\}$ をサンプリング
- 生成器のパラメータ $\theta_g$ を、以下の勾配で更新(勾配降下法):
$$ \nabla_{\theta_g} \frac{1}{m} \sum_{i=1}^{m} \log(1 – D(G(\bm{z}^{(i)}))) $$
原論文では、識別器を $k$ 回更新してから生成器を1回更新する方法が提案されています。これは、識別器がある程度最適に近い状態でないと、生成器の更新方向が不正確になるためです。実際には $k = 1$ でうまくいく場合も多いです。
生成器の損失関数の実用的な工夫
理論上の目的関数では、生成器は $\log(1 – D(G(\bm{z})))$ を最小化します。しかし、学習の初期段階では生成器が出力するデータの質が低く、識別器は容易に偽物を見破れるため、$D(G(\bm{z})) \approx 0$ となります。このとき、
$$ \log(1 – D(G(\bm{z}))) \approx \log 1 = 0 $$
となり、勾配がほぼ $0$ になってしまいます。これでは生成器が学習できません。
そこで実用上は、$\log(1 – D(G(\bm{z})))$ を最小化する代わりに、$-\log D(G(\bm{z}))$ を最小化する(等価的に $\log D(G(\bm{z}))$ を最大化する)という代替的な目的関数が使われます。$D(G(\bm{z})) \approx 0$ のとき、$-\log D(G(\bm{z})) \to +\infty$ となり、十分な勾配が得られます。
学習の不安定性
GANの学習は理論的には美しいですが、実際には多くの困難が伴います。代表的な問題を紹介します。
モード崩壊(Mode Collapse)
真のデータ分布が多峰性(複数のモードを持つ)の場合、生成器がその一部のモードだけを学習し、他のモードを無視してしまう現象をモード崩壊と呼びます。
例えば、手書き数字の生成タスクで、生成器が「7」ばかり生成して他の数字を生成できなくなるような状況です。これは、生成器が「識別器を最も騙しやすいモード」に集中してしまうために起こります。
数学的には、生成器が $\arg\min_{\bm{x}} [-\log D(\bm{x})]$ に近い点に出力を集中させてしまい、$p_g$ がデルタ関数的になることに対応します。
勾配消失
識別器が強すぎる場合、本物と偽物を完璧に区別できるようになり、$D(G(\bm{z})) \to 0$ となります。このとき、元の目的関数 $\log(1 – D(G(\bm{z})))$ の勾配がほぼ $0$ になり、生成器の学習が停滞します。
逆に、代替的な目的関数 $-\log D(G(\bm{z}))$ を使う場合でも、識別器が過度に学習すると $D(\bm{x})$ が $0$ または $1$ に飽和し、勾配が消失する問題が生じます。
学習の振動
識別器と生成器の学習バランスが崩れると、一方が相手を大きく上回り、その後もう一方が追いつくということを繰り返し、学習が振動して収束しないことがあります。
実践的なテクニック
上記の問題に対して、多くの実践的な対処法が提案されています。
特徴マッチング(Feature Matching)
生成器の目的関数を、識別器の出力を直接使うのではなく、識別器の中間層の特徴量の統計量を一致させるように変更します。
$$ \| \mathbb{E}_{\bm{x} \sim p_{\text{data}}} [f(\bm{x})] – \mathbb{E}_{\bm{z} \sim p_z} [f(G(\bm{z}))] \|^2 $$
ここで $f(\bm{x})$ は識別器の中間層の出力です。これにより、識別器の出力に直接依存せず、より安定した学習が可能になります。
ミニバッチ識別(Minibatch Discrimination)
モード崩壊を防ぐために、識別器がミニバッチ内のサンプル間の類似度を考慮できるようにします。各サンプルの特徴量をミニバッチ内の他のサンプルと比較し、多様性の情報を識別器に与えます。生成器が同じようなサンプルばかり出力するとミニバッチ内の多様性が低下し、識別器に見破られやすくなるため、結果として多様なサンプルの生成が促されます。
スペクトル正規化(Spectral Normalization)
識別器の各層の重み行列をスペクトルノルム(最大特異値)で正規化することで、識別器のリプシッツ定数を制限します。
$$ \bar{W} = \frac{W}{\sigma(W)} $$
ここで $\sigma(W)$ は重み行列 $W$ の最大特異値です。これにより、識別器の学習が安定し、勾配消失や勾配爆発を抑制できます。
ラベル平滑化(Label Smoothing)
識別器のターゲットラベルを、本物 $= 1$、偽物 $= 0$ とする代わりに、本物 $= 0.9$、偽物 $= 0.1$ のように平滑化します。これにより、識別器が過度に自信を持つこと(出力の飽和)を防ぎ、学習が安定します。
PyTorchでの実装
ここでは、1次元のガウス混合分布を学習するシンプルなGANをPyTorchで実装します。2つの正規分布の混合分布からのサンプルを「本物のデータ」として、GANがこの分布を学習する過程を可視化します。
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
# 乱数シード固定
torch.manual_seed(42)
np.random.seed(42)
# --- 本物のデータ: ガウス混合分布 ---
def sample_real_data(n):
"""2つのガウス分布の混合からサンプリング"""
# 等確率で2つのモードから選択
mix = np.random.choice([0, 1], size=n)
samples = np.where(mix == 0,
np.random.normal(loc=-2.0, scale=0.5, size=n),
np.random.normal(loc=3.0, scale=0.5, size=n))
return torch.FloatTensor(samples).unsqueeze(1)
# --- 生成器 ---
class Generator(nn.Module):
def __init__(self, z_dim=8, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(z_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
)
def forward(self, z):
return self.net(z)
# --- 識別器 ---
class Discriminator(nn.Module):
def __init__(self, hidden_dim=64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, hidden_dim),
nn.LeakyReLU(0.2),
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)
# --- ハイパーパラメータ ---
z_dim = 8
lr = 2e-4
batch_size = 512
n_epochs = 5000
k_disc = 3 # 識別器の更新回数
# --- モデルとオプティマイザ ---
G = Generator(z_dim=z_dim)
D = Discriminator()
opt_G = optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))
opt_D = optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))
criterion = nn.BCELoss()
# --- 学習過程の記録 ---
snapshots = []
snapshot_epochs = [0, 500, 1000, 2000, 5000]
# --- 学習ループ ---
for epoch in range(1, n_epochs + 1):
# ステップ1: 識別器の更新(k回)
for _ in range(k_disc):
real_data = sample_real_data(batch_size)
z = torch.randn(batch_size, z_dim)
fake_data = G(z).detach()
# 本物と偽物に対する識別器の出力
d_real = D(real_data)
d_fake = D(fake_data)
# 識別器の損失
loss_D = criterion(d_real, torch.ones_like(d_real)) \
+ criterion(d_fake, torch.zeros_like(d_fake))
opt_D.zero_grad()
loss_D.backward()
opt_D.step()
# ステップ2: 生成器の更新(1回)
z = torch.randn(batch_size, z_dim)
fake_data = G(z)
d_fake = D(fake_data)
# 生成器の損失: -log(D(G(z))) を最小化
loss_G = criterion(d_fake, torch.ones_like(d_fake))
opt_G.zero_grad()
loss_G.backward()
opt_G.step()
# スナップショットを記録
if epoch in snapshot_epochs:
with torch.no_grad():
z_test = torch.randn(2000, z_dim)
generated = G(z_test).numpy().flatten()
snapshots.append((epoch, generated))
if epoch % 1000 == 0:
print(f"Epoch {epoch:5d} | D loss: {loss_D.item():.4f} | G loss: {loss_G.item():.4f}")
# --- 可視化: 学習過程のスナップショット ---
fig, axes = plt.subplots(1, len(snapshots), figsize=(18, 3.5))
x_range = np.linspace(-6, 7, 300)
# 本物のデータの分布(参考用)
from scipy.stats import norm
p_true = 0.5 * norm.pdf(x_range, -2.0, 0.5) + 0.5 * norm.pdf(x_range, 3.0, 0.5)
for ax, (ep, gen) in zip(axes, snapshots):
ax.hist(gen, bins=60, density=True, alpha=0.6, color="steelblue", label="Generated")
ax.plot(x_range, p_true, "r-", lw=2, label="True distribution")
ax.set_title(f"Epoch {ep}")
ax.set_xlim(-6, 7)
ax.set_ylim(0, 1.0)
ax.legend(fontsize=8)
fig.suptitle("GAN Training Progress: 1D Gaussian Mixture", fontsize=14)
plt.tight_layout()
plt.show()
このコードを実行すると、学習の初期段階では生成データの分布がデタラメですが、エポックが進むにつれて、2つのガウス分布の山($\mu = -2$ と $\mu = 3$)を正しく捉えていく過程が確認できます。
コードのポイントを整理します。
- 識別器はLeakyReLUを使用: 通常のReLUだと負の領域で勾配が完全に $0$ になるため、識別器にはLeakyReLUが推奨されます
- 識別器をk回、生成器を1回更新: 識別器がある程度正確でないと、生成器の学習方向が不正確になります
- Adamオプティマイザのbetas: GAN学習では $\beta_1 = 0.5$ が経験的によく使われます。デフォルト値($0.9$)だと学習が不安定になることがあります
- 生成器の損失にBCELossを使用:
criterion(d_fake, torch.ones_like(d_fake))は $-\log D(G(\bm{z}))$ の最小化に対応し、前述の実用的な工夫を反映しています
まとめ
本記事では、GAN(敵対的生成ネットワーク)の理論について解説しました。
- GANは生成器 $G$ と識別器 $D$ の2つのネットワークによるミニマックスゲームとして定式化される
- 最適な識別器は $D^*(\bm{x}) = \frac{p_{\text{data}}(\bm{x})}{p_{\text{data}}(\bm{x}) + p_g(\bm{x})}$ で与えられる
- 最適な識別器のもとでは、生成器の目的関数は $p_{\text{data}}$ と $p_g$ のJSダイバージェンスの最小化と等価になる
- 実際の学習ではモード崩壊や勾配消失といった不安定性があり、特徴マッチングやスペクトル正規化などの対処法が提案されている
- 実用上は $-\log D(G(\bm{z}))$ を最小化する代替的な目的関数が使われる
次のステップとして、以下のトピックも学んでみてください。
- DCGAN(Deep Convolutional GAN): 畳み込みニューラルネットワークをGANに適用し、画像生成に特化したアーキテクチャ
- 条件付きGAN(Conditional GAN): クラスラベルなどの条件情報を与えて、制御可能な生成を行う手法
- Wasserstein GAN(WGAN): JSダイバージェンスの代わりにWasserstein距離を使い、学習の安定性を大幅に改善した手法
- 拡散モデル(Diffusion Models): GANとは異なるアプローチで高品質な生成を実現する、近年注目されている生成モデル