生成器と識別器を騙し合わせてみたら、損失は教科書通りの値に収束したのに、形はきれいに揃わなかった。GAN(敵対的生成ネットワーク)を一から実装した
約9分で読めます

生成器と識別器を騙し合わせてみたら、損失は教科書通りの値に収束したのに、形はきれいに揃わなかった。GAN(敵対的生成ネットワーク)を一から実装した


前回、拡散モデルをnumpyで一から実装して、ノイズから本物に近い形を復元できることを確認した。画像生成AIにはもう一つの大きな源流がある。GAN(Generative Adversarial Networks、敵対的生成ネットワーク)だ。「偽物を作る生成器」と「本物か偽物かを見抜く識別器」を互いに競わせながら学習させる、という仕組みは知識としては知っていたが、実際に自分の手で2つのネットワークを騙し合わせたことはなかった。

そこで今回は、前回と全く同じtwo moonsデータをターゲットに、GANをnumpyだけで一から実装した。同じデータ・同じ実装規模(隠れ層1つのMLP)で拡散モデルと直接比較できるようにしたのが今回のポイントだ。

生成器と識別器、それぞれの役割

生成器 GG はランダムなノイズ zz を受け取って、データっぽい点を作ろうとする。識別器 DD は、渡された点が本物か GG が作った偽物かを判定しようとする。2つは相反する目的で同時に学習する。

minGmaxD  Expreal[logD(x)]+Ez[log(1D(G(z)))]\min_G \max_D \; \mathbb{E}_{x \sim p_{\text{real}}}[\log D(x)] + \mathbb{E}_{z}[\log(1 - D(G(z)))]
# D の学習: 本物には高い確率、偽物には低い確率を出すように更新
prob_real, cache_real = D_forward(real)
prob_fake, cache_fake = D_forward(fake)
d_loss = -np.mean(np.log(prob_real)) - np.mean(np.log(1 - prob_fake))

# G の学習: D の目を欺けるように更新(Dのパラメータ自体は固定)
prob_fake2, cache_fake2 = D_forward(fake2)
g_loss = -np.mean(np.log(prob_fake2))
*_, dx = D_backward(cache_fake2, d_prob)   # Dを通り抜けて、fakeデータへの勾配だけを取り出す
G_grads = G_backward(g_cache, dx)          # その勾配でGだけを更新する

Dの逆伝播を最後まで計算しつつ、Dのパラメータ自体は更新せず、入力(=Gが生成した点)への勾配だけを取り出してGの更新に使う、というのが実装していて一番面白かった部分だ。前回と同じくAdamでパラメータを更新する。

損失は教科書通りの値に落ち着いた

8000イテレーション学習させ、DとGそれぞれの損失を記録した。

識別器(青)と生成器(赤)の損失の推移を示す折れ線グラフ。学習序盤は大きく振動しながら下がっていくが、2000イテレーション付近で理論上の均衡値(D=ln4≈1.386、G=ln2≈0.693)の破線に収束する。ただし4000〜5000、7000〜7600イテレーション付近では収束後にもかかわらず再び激しい振動が発生している

理論上、生成器が本物と見分けがつかない分布を完璧に再現できたとき、識別器はもう何を見ても「五分五分」としか言えなくなり、DDの損失は ln41.386\ln 4 \approx 1.386GGの損失は ln20.693\ln 2 \approx 0.693 に収束するはずだ。実際、2000イテレーションを過ぎたあたりでほぼその値にぴったり張り付いた。ただし面白いのはそこで安定しなかった点で、4000〜5000イテレーション、7000〜7600イテレーション付近で、収束していたはずの損失が再び激しく振動している。 GANの学習が不安定になりやすいとよく言われるのを、数字の上で実際に目撃した格好だ。

識別器と生成器の「鬼ごっこ」を見る

損失の値だけでは何が起きているか分かりにくいので、識別器が「この座標は本物っぽい」と判定する確率をヒートマップにし、その上に生成器が実際に出力した点を重ねて、学習の推移をアニメーションにした。

識別器の「本物らしさ」の確率マップ(赤=本物寄り、青=偽物寄り)を背景に、生成器が生成した点(緑)を重ねたアニメーション。学習序盤はヒートマップがtwo moonsの三日月の形にくっきり沿って赤く色づいているが、学習が進むにつれてヒートマップ全体が白っぽく(確率0.5に近く)均されていき、生成器の点は三日月の周辺に広がってはいるものの、輪郭にぴったり沿った形にはならない

学習の一番最初、識別器はいとも簡単に本物データの形を見抜き、two moonsの三日月の輪郭にぴったり沿って「本物らしさ」の高い領域(赤色)を作る。ここではまだ緑の点(生成器の出力)は完全にランダムだ。学習が進むにつれて、識別器の地図はだんだん**真っ白(確率0.5、つまりお手上げ)**に均されていく。これは識別器が「もう本物と偽物の違いが分からない」と白旗を上げた状態で、損失の理論値に収束したのとちょうど対応する。

ただし、ここが今回一番興味深かった点だ。識別器が完全に騙された状態になっても、生成器の点はtwo moonsの輪郭にぴったり沿ってはいない。 三日月のだいたいの領域には広がっているが、輪郭のシャープさは本物には及ばない。「識別器を騙せた」ことと「本物の分布を正確に再現できた」ことは、必ずしもイコールではないというのが、実際に動かして見えてきた実感だった。

拡散モデルとの比較

最終的な生成結果を、本物のデータと並べてみる。

本物のtwo moonsデータ(左)と、GANが生成したサンプル(右)を並べた散布図。GAN側は右上から左下にかけて大まかにtwo moonsの領域には分布しているが、2つの三日月それぞれの輪郭のシャープさは本物ほどではなく、全体的に滲んだような分布になっている

前回の拡散モデルの生成結果と比べると、違いは明らかだった。拡散モデルは同程度の学習規模(隠れ層1つのMLP)でも、two moonsの2つの弧という構造をかなりはっきり再現できていた。今回のGANは、大まかな領域は捉えているものの、輪郭のシャープさでは一歩譲る結果になった。拡散モデルは「今のノイズ量に対する回帰問題を繰り返し解く」という、比較的素直な教師あり学習に近い仕組みなのに対し、GANは2つのネットワークがお互いの出方を見ながら追いかけっこをする仕組みなので、学習が不安定になりやすい、という一般的に言われる違いを、同じデータ・同じ実装規模でこの目で確認できたのは収穫だった。

識別器の”地形”を3Dで見る

学習が始まったばかりの時点で、識別器が学んだ「本物らしさ」の地形を3Dの曲面として可視化した。

驚くほどはっきりと、two moonsの三日月の稜線がそのまま山になって浮かび上がっている。識別器はまだ学習の初期段階で、こんなに簡単に本物データの形を丸暗記できてしまう。生成器はこの険しい山を崩しにかかるところから学習が始まる、と考えると、序盤の攻防がどれだけ非対称かがよく分かる。

手を動かして意外だったこと

一番印象に残ったのは、「相手を出し抜けなくなった」という状態が、必ずしも「正解にたどり着いた」ことを意味しないという点だった。識別器が完全に混乱し、損失が理論値にぴったり収束しても、生成器が作る分布は本物の輪郭ほどシャープにはならなかった。これは競争関係全般に言えることかもしれない。ライバル同士がお互いに完璧に対応し合って、どちらも相手を出し抜けない均衡状態に達したとしても、それは「二人がお互いにとって最適な状態」であるだけで、「本来目指すべきゴール」に届いているとは限らない。

拡散モデルの一歩ずつの回帰と比べると、GANの敵対的な学習は、相手の出方に応じて自分の戦略を変え続けるという、より複雑で不安定な力学を持っている。刺激し合うことで互いに成長する側面もあれば、意識が相手への対応だけに向いて、本来の目的(この場合は本物のデータ分布)から意識が逸れてしまう危うさもある。損失のグラフに何度も現れた振動は、そういう「相手ばかり見ていることの代償」を、数字の形で見せてくれた気がする。

まとめ

  • GAN(生成器Gと識別器Dを交互に学習させる敵対的生成ネットワーク)を、前回の拡散モデルと同じtwo moonsデータ・同程度の実装規模(隠れ層1つのMLP)でnumpyから実装した
  • 8000イテレーションの学習で、DとGの損失はどちらも理論上のナッシュ均衡値(D: ln41.386\ln4\approx1.386、G: ln20.693\ln2\approx0.693)にほぼ収束したが、収束後も4000〜5000・7000〜7600イテレーション付近で激しい振動が再発した
  • 識別器の「本物らしさ」ヒートマップは、学習序盤はtwo moonsの輪郭にくっきり沿うが、学習が進むと確率0.5に近い真っ白な状態に均一化されていく一方、生成器の出力点は輪郭にぴったり沿うところまでは至らなかった
  • 同じ実装規模での比較では、拡散モデルの方がtwo moonsの2つの弧という構造をよりはっきり再現できており、GANの敵対的学習は一般に言われる通り不安定さが目立つ結果になった
  • 「識別器を完全に騙せた(損失が理論値に収束した)」ことは「生成器が本物の分布を正確に再現できた」ことと必ずしもイコールではない、という点が、数値と可視化の両方から確認できた