UnrolledGANをTensorFlowで実装した

別のGANの論文を読むつもりが間違えてUnrolledGANと呼ばれるものを印刷して読んでしまった
途中でこれ違う論文やんけと気づいたけど紙がもったいないのでちゃんと読んでUnrolledGANを実装した。

まちがえて読んだ論文はこれ

[1611.02163] Unrolled Generative Adversarial Networks

GANのまとめはこっち

GANとDCGANでは学習の不安定さや生成される画像の多様性がしょぼい、ノイズがかなり含まれる、などが問題として挙げられてた
この論文で学習の不安定さの原因として識別器の方が生成器よりも学習が早いというのが書かれていた
先に識別器の方がめちゃ強くなってしまう。ある学習途中のまだ弱い生成器にとって、強力な識別器の学習信号は学習信号として役立たなくなるとも書いてある。まじかよ・・・

リアルで考えれば、AとBの二人がお互い助け合って数学の勉強をしているとして、Aの方が成長が早く、AがBに教えようとするとBはまだAと同じレベルにいないからAの言っていることが時々わからずBの成長が悪くなってしまう感じだろうか。GANに限るかわからないが二人三脚な感じで学習していくのがベストってことなのかな

こういったことが起こると途中で学習が収束(悪い意味で崩壊)してそのエポック以降は同じもの、またはそれに類似したものしか生成しなくなってしまうんだとか。
これには思い当たりがある。実際前に作ったDCGANでも途中で識別器と生成器の損失が変わらず、次のような画像しか生成されないことが何回かあった

f:id:Owatank:20180515123739p:plain

識別器が強力になることが原因だったとは知らなんだ。DCGANで識別器にのみバッチ正規化を適用しなかったり、重み減衰を適用してうまくいったのはつまりそういうことだったのか

ここまで論文を読んで、じゃあ識別器の学習をのろまにさせる手法がUnrolledなのかなと思ったけどそうじゃなかった。逆に生成器の学習スピードが識別器に追いつけるようにする方法が書かれてあった。識別器を悪くさせる必要ないもんな・・・

今までのGANは次のように両モデルのパラメータを更新していった

{ \displaystyle  \theta_{G} \leftarrow \theta_{G} - \eta \frac{df(\theta_{G},\theta_{D})}{d\theta_{G}} }

{ \displaystyle  \theta_{D} \leftarrow \theta_{D} + \eta \frac{df(\theta_{G},\theta_{D})}{d\theta_{D}} }

{ \displaystyle \eta }は学習係数で、 { \displaystyle f (\theta_{G},\theta_D) } は目的関数

Unrolled GANではまず次のように定義して

{ \displaystyle  \theta_D^0 = \theta_D }

{ \displaystyle  \theta_D^{k+1} = \theta_D^k + \eta^k \frac{df(\theta_{G},\theta_D^k)}{d\theta_D^k}}

{ \displaystyle f_k (\theta_G,\theta_D) = f (\theta_G, \theta_D^k (\theta_G, \theta_D))}

以下のようにパラメータを更新する

{ \displaystyle  \theta_{G} \leftarrow \theta_{G} - \eta \frac{df_{\color{red}{k}}(\theta_{G},\theta_{D})}{d\theta_{G}} }

{ \displaystyle  \theta_{D} \leftarrow \theta_{D} + \eta \frac{df(\theta_{G},\theta_{D})}{d\theta_{D}} }

GANと変わったところは赤で書いた。数式眺めてすぐに理解できるほど賢くないけど、生成器のパラメータ更新は k回パラメータを更新した識別器を用いて更新するということなんじゃなかろうか

もしk = 0なら1回だけ識別器を更新して、次に生成器を1回更新する今までのGANと同じと書いてあるからkが1以上の自然数で与えればいいんだな

こうすることで生成器は識別器がどのように反応するかを考慮してより本物を作れるように学習ができるという理由が定義式の後に書いてあった。
まとめると識別器の更新パラメータは最初の1回の更新パラメータを採用する。生成器の更新パラメータはk回更新したときの識別器のパラメータを用いて更新したものを採用するということ。
論文ではこの生成器の更新の説明で 'see into the future' とかっこよく書かれていた。この一文を見たとき「才気煥発の極みじゃん」とかくだらないこと思ってしまった。

GANとの変更点は上記のパラメータ更新あたりしかない。それだけで改善するんかほんま・・・と思いつつ論文の実験の部分を読んで次のような実験結果が載っていた
f:id:Owatank:20180601113711p:plain

右端のTargetが二次元のガウス分布の入力データで、上の段がUnrolledGAN、下の段が通常のGAN(k=0)の学習の途中経過

UnrolledGANのがうまく多様性も含めて学習してるのがわかる。下の段でTargetの一部分しか学習できていない。それ以上にめちゃくちゃこの画像かっこいい。

かっこいいと思ったので同じような結果が得られるのか自分でUnrolledGANを実装して観察することにする

コードはここ

github.com

Unrolledな実装部分は流れとしてはこんな感じで作ることにする
f:id:Owatank:20180601115449j:plain

めんどくさいのがUnrolledStepに入る前の識別器の重みの保存。生成器のパラメータ更新が終わった後に識別器のパラメータを { \displaystyle \theta_{D}^1 } のものに復元しないといけない。

グラフをUnrolledする前にフリーズさせて終わった後にフリーズさせたグラフから { \displaystyle \theta_{D}^1 } のパラメータ取れるかなとか思ったけど、Variableノードのassign使えば雑だけどできそうだったのでこっちでやった

TensorFlow: Mutating variables and control flow – metaflow-ai

困ったのがメモリーリークだった
最初次のように識別器の重みをforループ内でコピーした

copy_dis_w1 = sess.run(self.discrimitor.dis_w1)

そして復元については次のようにしてた

sess.run(tf.assign(self.discrimitor.copy_dis_w1,copy_dis_w1))

何がまずいってforループ内でこんなことすると学習するたびに1Epochの終わる時間が4秒、5秒、7秒、...と増えていった

さすがにおかしいと思って調べたら復元のところでtf.assign()の引数でcopy_dis_w1Tensorじゃないため、呼び出すたびにcopy_dis_w1を格納するtf.constant()もどきの変数を作っていたらしい。いかんでしょ

それに加えてtf.assignのオペレーションのノード自体も呼び出すたびに作られていた。アホやんけ・・・

めんどくさがって学習のループ内でオペレーションを呼び出すんじゃなかった。学習に入る前に事前にオペレーションを定義しておいた。
いちいちtf.constantで退避させた重みを入れるのも嫌なのでtf.placeholderを作っておいてそれに格納してメモリーリークの問題を解決した。

以下結果

入力データを分散が0.01くらいの正規分布から作って8方向にシフトさせる
f:id:Owatank:20180601114736p:plain

Unrolledする回数として論文のコードを参考に k=5と設定した。また入力のノイズ変数の次元は100次元じゃなくて50次元に削減した。
自分で作ったUnrolledGANの生成結果としてはこんな感じ(Epoch : 1000)

f:id:Owatank:20180601114425g:plain


途中経過
f:id:Owatank:20180601115051p:plain  

同じモデル構造で、k=0のGANだと次のような学習結果が得られた
f:id:Owatank:20180601115143p:plain

論文と同じような結果が大体得られた。重み減衰入れなくてもめっちゃ安定している。

おまけでノイズ変数を20次元にしてk=3で学習させてみたら芸術的なものができた

f:id:Owatank:20180601121933g:plain

ところでこの論文、某検索エンジンインターンシップで提出されたらしい。向こうの学生はすごいなあ