RelativisticGANの論文を読んでPytorchで実装した その2

その1の続き

Standardな方のRSGANを実装してみる。
WGANまでTensorflowで実装してて今更Pytorchに変えたのはGeneratorとCriticのアーキテクチャの部分とか訓練の部分の定義がめんちいから。自分が効率悪い書き方してるだけの向上心がクズなだけです・・・

訓練のデータセットはhiragana73なるものを使ってみた。某開始5分村焼きソシャゲとデータセットの名前が似てたからそれだけ。
文字画像データセット(平仮名73文字版)を試験公開しました | NDLラボ

コードはここ

github.com

Pytorch初めて触ったけどかなり良さげだった。

書いてて感動したのはまず最適化の部分

GANではgeneratorとcriticで別々に更新するパラメータを指定しないといけない。
tensorflowのときはパラメータを指定するとき

self.cri_vars = [x for x in tf.trainable_variables() if "cri_" in x.name]
self.gen_vars = [x for x in tf.trainable_variables() if "gen_" in x.name]

こんな感じでパラメータのリスト用意してoptimizerに突っ込んだ。

pytorchだとこれで終わる

self.critic = Critic()
self.opt_critic = torch.optim.RMSprop(self.critic.parameters(), lr=0.00002)
        
self.generator = Generator()
self.opt_generator = torch.optim.RMSprop(self.generator.parameters(), lr=0.00002)

訓練中に更新ステップでself.opt_critic.step()とか呼び出せばいい。特に一部のパラメータは更新しないとかじゃなきゃ楽すぎる。嬉しい

あとDataLoaderなるものもよかった
今まで向上心がないので画像データを[-1,1]の範囲に正規化するのをミニバッチ(X_train)でデータ取得してから

(X_train, _) = train_generator.next()
X_train = (X_train - 127.5) / 127.5

こんな感じで取得してそこから直してモデルに渡してた。

pytorchだとこんなのでいけた

transform = torchvision.transforms.Compose([
                torchvision.transforms.Grayscale(),
                torchvision.transforms.ToTensor(),
                torchvision.transforms.Lambda(lambda x: (x*255. - 127.5)/127.5)
                 ])

train_dataset = torchvision.datasets.ImageFolder(
    root='./hiragana73/',
    transform=transform
)

それがtransform・・・僕の求めていた力・・・。RandomCropなりFlipなり色々あるけど特にtorchvision.transforms.Lambdaこれすき
kerasにもkeras.preprocessing.image.ImageDataGeneratorみたいのあるけどrescaleしかなくて[-1,1]の範囲に正規化するのめんどかった(探せば楽な方法あるんだろうけど)

地味にWGANのクリッピングの操作がこれだけで終わるのも嬉しすぎた

for p in self.critic.parameters():
    p.data = torch.clamp(p.data,-self.clip_value,self.clip_value)

元の定義されてたRSGANの数式

{ \displaystyle L^{RSGAN}_{D} = - \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } [log(sigmoid(C(x_r) - C_(x_f)))] }
{ \displaystyle L^{RSGAN}_{G} = - \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } [log(sigmoid(C(x_f) - C_(x_r)))] }


これに従ってRSGANの更新ステップは次のように書いた

generated_X = self.critic(self.generator(noise_z))
real_X = self.critic(X_train.detach())

cri_loss = -torch.mean(torch.log(torch.sigmoid(real_X - generated_X)))
total_cri_loss += cri_loss.item()
self.opt_critic.zero_grad()
cri_loss.backward()
self.opt_critic.step()

for p in self.critic.parameters():
  p.requires_grad = False

generated_X = self.critic(self.generator(noise_z))
real_X = self.critic(X_train.detach())

gen_loss = -torch.mean(torch.log(torch.sigmoid(generated_X - real_X)))
total_gen_loss += gen_loss.item()
self.opt_generator.zero_grad()
gen_loss.backward()
self.opt_generator.step()

話は戻って論文で学習時間も改善できると書いてあったし折角なのでWGANもpytorchで書き直して1エポックあたりにかかる学習時間を比較してみた

1エポック 48x48 のグレースケール画像を2万枚として環境は貧乏なのでGoogle Colaboratoryを頼る
WGANはcriticの回数を5に固定して1エポック100秒ほどかかったのに対してRSGANは1エポック35秒ほどだった。criticの回数1と思えば早いのはそれもそう

学習時間が早くても精度が悪かったら駄目じゃん。50エポックまでの精度を比較する。

WGANのGeneratorが生成したひらがなと思わしき画像はこんな感じ
f:id:Owatank:20180923224643p:plain

各エポックごとのCriticとGeneratorのロス(1ステップ100枚=200ステップの合計)についてはこんなん
f:id:Owatank:20180923224717p:plain

これに対してRSGANのGeneratorが生成したひらがなと思わしき画像たち
f:id:Owatank:20180923224831p:plain

RSGANのCriticとGeneratorのロス
f:id:Owatank:20180923225219p:plain ヤベ100エポックまで回してるのバレた

う、うーん・・どっちも魔界の王を決める戦いの魔本の文字みたいなのしか生成してない気がする。RSGANのがちょっとだけ良さそうに見えなくもない
早くてこの精度ならめっちゃいいじゃんRelativistic GAN

参考にしたもの

https://github.com/AlexiaJM/RelativisticGAN
実践Pytorch




論文ちょいちょい読んでいるけれど、自分の元の数学の知識が乏しいせいで大事でとても面白い部分をスルーして読んでいるのを毎回痛感する・・・。
高校のときに読んだ算数の小説に「ゼータ関数の自明でない零点の実数部は全て1/2である」といったのが載ってた。当時は全くわかんなくてそれでもすごい惹かれて大学入って数学の授業受ければわかるかなあとか思ってたし、Poincare embeddingという論文を読んで双曲幾何学をすげーと思ってせめて論文の述べてる仕組みわかるようになりたいとか、読んでいくたび論文に勉強足りなさすぎだハゲといった感じでボコボコにされつつ聞いたことのある数学の単語とかが出てきて、どうしてそれが出てきたのか意味が知りたかったり。わかればきっと楽しいはずで、多分・・・
まだまだ勉強足りないのに時間だけが迫ってくる。うーん。全く手をつけていない卒論頑張ろう

RelativisticGANの論文を読んでPytorchで実装した その1

生きてます
お世話になってる博士の人にRelativistic GANなる論文を教えてもらったので読んだ。相変わらず自分から良さげな論文を探すスキルが向上しない・・みんなどっから情報収集してるんだ

[1807.00734] The relativistic discriminator: a key element missing from standard GAN

点線から点線までは論文と直接関係ない

自分が最初に読んだGANはGenerator(生成器)Discriminator(識別器)の2つを持ってて、識別器は与えられたデータが本物か偽物(生成器が作ったもの)であるかを識別する役割だった。
一方でWGANと呼ばれるやつはGeneratorCriticの2つを持ってた。CriticはどちらかというとWasserstein距離というものを求める役割で上のDiscriminatorと役割は違う。

ところでWGANのようなものはIPM (Integral probability metrics) GANと呼ぶらしい。積分と確率、、うーん、WGAN読んだとき2つの確率分布が似ているかどうかの指標となる距離を求めるには期待値を計算しなくちゃいけなくて、その期待値の計算に積分が出てくるから関係あるのか。測度論について勉強していくとわかってくるのかな。そうでもない?そう・・・

話は戻って初期の頃のGANはGeneratorDiscriminatorの学習バランスが不安定なのが問題と自分が読んできたGANの論文で指摘されていた。

そういえば何で2つの学習バランスが悪いんだっけ。
GeneratorDiscriminatorに自身が作った偽物を渡して返ってくる結果が本物(確率として1に近い値)であるように自身のパラメータを修正していく。学習ステップが初期の頃にGeneratorが作るものが本物のデータとかけ離れていればDiscriminatorは容易に本物と偽物の識別ができるに違いない。

そうなるとGeneratorは返ってきた結果のほとんどが偽物と返ってくるため、本物のデータに近づくための手がかり(教師信号)が得られないんじゃなかろうか。1個でも本物と返ってきたならばその1個に近いサンプルを作れるようにパラメータを修正していって試行錯誤できるけど、色々作ったサンプルが全部偽物と返ってきたら自分ならどうすればいいか分かんなくて挫ける。
でもこれって判別する役割より作る役割のが大変だからなのかなあ。そんなこんなで訓練の改善策としてUnrolledGANなりWGANなり出てきた。

本題

Relativistic GANの改善点はどうやらWGANみたいに1ステップCriticをn回更新、Generatorを1回更新、といったことをせず両モデル1ステップ1回更新で学習するから400%の時間短縮できて、今流行りのWGAN-GPとかより高品質な生成物を作れるぜって書いてある。ス、スゲー!!!
この論文でのGANにおけるDiscriminatorはWGANみたいに距離の最小化の役割も持ってて、かつ偽物か本物かを識別する役割も持ったDiscriminatorと述べてる。

初期の頃に登場していたGANには、Generatorが作った偽物{ \displaystyle x_f}をDiscriminatorに渡したときの確率{ \displaystyle D(x_f)}が高まる(偽物渡してるのに本物と識別されてる)につれて、本物のデータ{ \displaystyle x_r}を渡した確率{ \displaystyle D(x_r)}は反対に下がるべきという特性が失われているらしい。
この論文で初めて2年も前に登場していたGANの重要な特性を知った。考えもせず無知なままなのは罪である。

どうもGeneratorの訓練がうまくいったとして、与えられた入力{ \displaystyle x}がどれだけ現実的(realistic)かを出力する関数{ \displaystyle C(x)}に偽物 { \displaystyle x_f} と本物 { \displaystyle x_r} を渡したら{ \displaystyle C(x_r) \approx C(x_f)}といった状況になってるらしい。どっちも本物っぽいぜ〜 1に近い値出すぜ〜ってことか

いや、別にその状況よくね?とも思ったんだけど、もし { \displaystyle C(x)} に渡す N個のうち半分が本物で半分が偽物と事前に知っていたなら、このような状況はおかしいというか不合理、良くて「うわ〜どれも本物っぽくて迷うな〜〜どうしよっかな〜〜〜わっかんねーからどっちつかずの 0.5 って返しとこ」あたりが自然だよねって述べてる、はず。

マークシートの試験で全8問のうち4問はAが正解と知っているのに全問Bで埋めてる奴はどう考えてもヤバイ・・・中忍試験のように白紙のがまだ理解できる。

じゃあGeneratorの学習が良い方向に進む、{ \displaystyle C(x_f)} が本物っぽくなる(値が大きくなる)につれて{ \displaystyle C(x_r)}は反対に下がるような大事な特性を与えてあげるのがRelativistic GANの特徴なのか。というよりは今まで読んできたGANはこの特性を与えるように工夫したGANだったんだな。

この大事な特性が失われている「どっちも本物っぽいから1に近い値出すぜ」な感じになってるGANの学習と「どっちも本物っぽくて迷うからどっちつかずの 0.5 って返しとこ」な感じのGANの学習ではどう違うのかの説明が論文に述べられてる。

最初に登場したGANの学習方法として論文にはこんなのが載ってた。

{ \displaystyle  \min_{G}\max_{D} V(D,G) = \mathbb{E}_{\boldsymbol{x} \sim p_{data(\boldsymbol{x})}} \ {[}logD(\boldsymbol{x}){]} +  \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}(\boldsymbol{z})}} \  {[}log(1-D(G(\boldsymbol{z}))){]} }

Relativistic GANの方の論文だとDiscriminatorの目的関数はJSダイバージェンスと等しく、そして目的関数を最大化するように学習していくと書いてある。数式だとこんな感じ。

{ \displaystyle  JSD(\mathbb{P} || \mathbb{Q}) = \frac{1}{2}( log(4) + \max_{D:X \to [0,1]} \mathbb{E}_{x_r \sim \mathbb{P}}[log(D(x_r))] + \mathbb{E}_{x_f \sim \mathbb{Q}}[ 1 - log(D(x_f))]  ) }

本物データ分布{ \displaystyle \mathbb{P} }とGeneratorの偽物のデータ分布{ \displaystyle \mathbb{Q}}が近いかを示す指標のJSダイバージェンスを最大化するって二つの分布が遠ざかってる、似てないってことだよな。
{ \displaystyle log(4)}は置いといて、上の式の第二項と第三項の{ \displaystyle D(x_r)}{ \displaystyle D(x_f)}どうなれば大きくなるか。識別器としては{ \displaystyle D(x_r) = 1}{ \displaystyle D(x_f) = 0 }と割り当てるのが役割なので、{ \displaystyle log(D(x_r)) = log(1) = 0}{ \displaystyle 1 - log(D(x_f)) = 1 - log(0) }となっちゃうから{ \displaystyle log(0) }によってJSダイバージェンスは無限大になっちゃうな。最大化だからいいのか

GeneratorはこのJSダイバージェンスが小さくなるように自身の偽物のデータ分布を本物データ分布に近づけるように学習していく。上の式の場合{ \displaystyle D(x_r) = D(x_f) = \frac{1}{2} }と識別器に割り当てさせるように学習させればJSダイバージェンスが最小になると書いてある。自分はならなかったんですけど(´;ω;`)
{ \displaystyle D(x_f) = 1}{ \displaystyle D(x_r) = 0}となってしまうと{ \displaystyle log(D(x_r)) = log(0)}となってしまうからそれもそっか

JSダイバージェンスの最小化としては{ \displaystyle D(x_r) = D(x_f) = \frac{1}{2} }、次のような感じで徐々に二つの分布が近づくようにしていきたい。(論文の図だとこんなのが載ってる)

f:id:Owatank:20180922181708p:plain


赤が本物のデータで青がGeneratorが作った偽物のデータ

GANの大事な特性が失われていると、{ \displaystyle D(x_f)}のみが増加する。(論文の図)

f:id:Owatank:20180922182244p:plain

どっちも本物っぽいって状況だ。これではJSダイバージェンスは最小にはなってないからGeneratorの学習としてはゴールじゃない
Discriminatorの学習において{ \displaystyle D(x_f)}の変動に対し{ \displaystyle D(x_r)}が変化してないというのはDiscriminatorは識別するための一体何がデータの本物らしさを意味しているのかということをGeneratorの偽物のみから(焦点をおいて)学習していることに繋がっているそうで。本物のデータも参考にして学習してくれよ・・・。そうなるとGeneratorもより本物らしく偽物を作るように学習ができず訓練自体が行き詰まるとか。

理想としては{ \displaystyle D(x_r) = D(x_f) = \frac{1}{2}}な状況。{ \displaystyle D(x_f)}の値が大きくなるのに対して{ \displaystyle D(x_r)}は下がるとDiscriminatorが本物のデータに対しても誤って「偽物」と割り当てる可能性も出てきて、偽物と本物の両方のデータを参考にして本物らしさを学習している感じかな(論文の図)

f:id:Owatank:20180922182949p:plain

Ideal が眩しいぜ

実際にどういう変更を加えるとそういった学習になってくれるのか。
初期のGANのDiscriminatorとGeneratorの各目的関数は次のように定義されてる

{ \displaystyle  \nabla_{\theta_{d}} \frac{1}{m} \sum_{i=1}^{m} {[}logD({\boldsymbol{x}}^{(i)}) + log(1-D(G({\boldsymbol{z}}^{(i)}))) {]}}

{ \displaystyle  \nabla_{\theta_{g}} \frac{1}{m} \sum_{i=1}^{m} log(1-D(G({\boldsymbol{z}}^{(i)}))) }

上で述べてるのはこの定義で訓練すると{ \displaystyle D(x_r)}が変化しない、Generatorの偽物しか参考にしてない悪い状況になるはずで。

Discriminatorの最後の出力層を{ \displaystyle C(x) }、それにシグモイド関数で活性化したものを{ \displaystyle D(x) = sigmoid(C(x)) }と定義しておく。

本物{ \displaystyle x_r } と偽物{ \displaystyle x_f } の両方 { \displaystyle \tilde{x} = (x_r,x_f) } を見比べるようなDiscriminatorは次のように定義すれば実現できるそうだ。

{ \displaystyle D(\tilde{x}) = sigmoid(C(x_r) - C(x_f)) }

思ったよりシンプル。{ \displaystyle C(x_r) - C(x_f) }の時点では確率にはなってなくて、この{ \displaystyle D(\tilde{x}) }は与えられた本物のデータ{ \displaystyle x_r }がGeneratorの偽物{ \displaystyle x_f }より本物らしいといった確率を推定していることを意味する(解釈できる)と書いてある。

逆にGeneratorの偽物の方が本物のデータよりも本物らしいといった確率はこうなる

{ \displaystyle D_{rev}(\tilde{x}) = sigmoid(C(x_f) - C(x_r)) }

このDiscriminatorの定義に従って学習していけばいいんだな。目的関数としては次のようにすればいいそうだ

{ \displaystyle L^{RSGAN}_{D} = - \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } [log(sigmoid(C(x_r) - C_(x_f)))] }
{ \displaystyle L^{RSGAN}_{G} = - \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } [log(sigmoid(C(x_f) - C_(x_r)))] }

Discriminatorの方で考えると{ \displaystyle sigmoid(C(x_r) - C(x_f)) }が 1 に近ければ嬉しいのでlogでくくると log1 = 0になってくはずだから最小化なのかな。

RSGANは Relativistic Standard GANの略でStandard GANについては論文だとこう述べてる。

In the original GAN by Goodfellow et al. [2014], which we refer to as Standard GAN (SGAN), D is a classifier, thus it is predicting the probability that the input data is real.

RelativisticなDiscriminatorは他のGANにも適用できないかと思ってしまうのは自然で、次に一般的な表現というか定義の説明があった。
多くのGANを次のように表現する

{ \displaystyle L^{GAN}_{D} = \mathbb{E}_{ x_{r} \sim \mathbb{P} } [ f_{1} (C(x_r))] + \mathbb{E}_{ x_{f} \sim \mathbb{Q} } [ f_{2} (C(x_f))] }
{ \displaystyle L^{GAN}_{G} = \mathbb{E}_{ x_{r} \sim \mathbb{P} } [ g_{1} (C(x_r))] + \mathbb{E}_{ x_{f} \sim \mathbb{Q} } [ g_{2} (C(x_f))] }

where f1, f2, g1, g2 are scalar-to-scalar functions.

これにRelativisticなDiscriminatorをソイヤ

{ \displaystyle L^{RGAN}_{D} = \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } \ [ f_{1}(C(x_r) - C_(x_f)) ] + \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } \ [ f_{2}(C(x_f) - C_(x_r))] }
{ \displaystyle L^{RGAN}_{G} = \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } \ [ g_{1}(C(x_r) - C_(x_f)) ] + \mathbb{E}_{ (x_{r}\ , \ x_{f}) \sim (\mathbb{P},\mathbb{Q}) } \ [ g_{2}(C(x_f) - C_(x_r))] }

これが・・・Relativistic GAN・・・!ほとんどのGANではGeneratorの{ \displaystyle g_{1}}は無視される、勾配がゼロと書いてある。本物のデータから本物らしさを学ばずに学習してしまう悪い状況じゃん。
RGANの方では非ゼロの勾配を持つそうで、本物データからもGeneratorは影響を受けて学習できる。いいじゃん!!!

もう一つRelativistic average GAN(RaGAN)というものの説明が載ってたんですけど犬の画像がとても可愛くて癒されたので見てほしい。
f:id:Owatank:20180923215626p:plain
食パンと類似した画像なら他にもあっただろうにあえて犬のケツを選ぶセンスすこだ・・・

まずはStandardなRSGAN試したろと思って長くなったので毎度のこと分ける

参考文献というか論文
[1807.00734] The relativistic discriminator: a key element missing from standard GAN
[1807.00734] The relativistic discriminator: a key element missing from standard GAN
[1807.00734] The relativistic discriminator: a key element missing from standard GAN

WGANの論文読んでTensorflowで実装する その2

WGANの論文読んでTensorflowで実装する その1 - 時給600円の続き

前回はEarth Mover DistanceもしくはWasserstein Distanceが他のJSダイバージェンスやTV距離と比べて優れてるというのをまとめた。

このEM距離をGANの目的関数として使いたいが、

{ \displaystyle W(\mathbb{P}_r\ ,\ \mathbb{P}_g) = \inf_{\gamma \in \Pi (\mathbb{P}_r\ ,\ \mathbb{P}_g)} \mathbb{E}_{(x,y) \sim \gamma} [ || x\ - \ y || ] }

このままでは使うことができないと書いてある。そもそも同時分布の集合を求めるのも大変だし、KLダイバージェンスと違って積分が閉じてないとかなんとか。

駄目じゃん。ってなるがその次のセクションに双対性というので以下の式を計算することでEM距離の値を求めることができると書いてある。双対問題はまだよくわかってない(´・ω・)
なんというかAの世界では掛け算で解くけど、Bの世界では足し算で解けるみたいな物事を別の世界で考えて計算を楽にする感じなんだろうか

{ \displaystyle W(\mathbb{P}_r\ ,\ \mathbb{P}_\theta) = \sup_{|| f ||_L \ \leq 1} \mathbb{E}_{x \sim \mathbb{P}_r} [ f(x) ] -  \mathbb{E}_{x \sim \mathbb{P}_\theta}[ f(x) ] \ \ \ (2) }

{ \displaystyle || f ||_L }という謎のものがある。まず関数{ \displaystyle f }リプシッツ関数( 1 - Lipshitz )であることが条件らしい。リプシッツ関数って何だよって思ったけど

f:id:Owatank:20180627153242p:plain

ある関数 { \displaystyle f }が存在して、その曲線が上の赤の三角形のように表せるならリプシッツ関数といえる認識でいいっぽい。間違ってるかもしれない。
要は増加量が線形というか緩やかなものがリプシッツ関数といえるのかな。そうなると{ \displaystyle x^{3} }とかはリプシッツ関数とはいえないはず(/・ω・)/
論文の端っこに、sigmoidtanhなどはリプシッツ関数の例であると説明があった。そういう認識で問題なさそう

問題は{ \displaystyle || f ||_L \leq 1}{ \displaystyle \leq 1}の部分。期待値の差分を取るから値はスカラーなはずで、その差が1以下のものって条件かな。ぬん・・・

GANなので識別器と生成器の2つがある。この式に2つをあてはめて目的関数として実用するには

{ \displaystyle \max_{w \in \mathcal{W} }  \mathbb{E}_{x \sim \mathbb{P}_r} [ f_w(x) ] -  \mathbb{E}_{z \sim p(z)}[ f_w(g_\theta(z) ) ]  }

とすればいいらしい。maxだから最大値を求めるのか。識別器はDiscriminatorだからよく d(x) みたいに表されるけど、なんで{ \displaystyle f_w(x)  }と書かれているのかというのは後にわかるから置いておいて、この式の条件下で最大値を取るとき、その時のパラメータ { \displaystyle \theta } で生成器{ \displaystyle  g_\theta(z) }は本物に近い生成データを得られる、つまり欲しい真の分布{ \displaystyle P_r }に近くなってるはず。前にEM距離は連続であると書かれていたから、微分ができて徐々に{ \displaystyle P_r }に近くなるように学習できるぜってことなんだな。すごい・・・すごくない・・・?

と思ったら次にこんな一文がある

Now comes the question of finding the function f that solves the maximization problem in equation (2).

うん?関数 { \displaystyle f} を見つける?
関数 { \displaystyle f} を表現するために使われる、集合 { \displaystyle \mathcal{W} }の中から取れるパラメータ { \displaystyle w } を先に最適化して式(2)が最大になるような関数 { \displaystyle f} もといパラメータ { \displaystyle w } 先に見つけないと駄目なのかな。
だからあくまでこの関数 { \displaystyle f}識別の役割とはいえないから Discriminator ではなく Critic という名前でこの論文では呼ばれている。

で、パラメータの更新によっては { \displaystyle w } の値が集合 { \displaystyle \mathcal{W} } にはない値を取る場合もある。これでは式(2)の条件を満たせないので、無理やり値を抑える(クリッピングと呼ばれてる)。式(2)の条件というより値がはみ出るとリプシッツ関数として成り立たないからとかだろうか。

値を抑える範囲としては論文では [ -0.01 , 0.01 ]が採用されている。この範囲が大きすぎると最適なパラメータ { \displaystyle w } を見つけるのに時間が掛かって、小さすぎると今度は勾配消失の問題が起きやすいと書かれている。難しい・・・

まとめると、先に Critic { \displaystyle f_w }のパラメータ { \displaystyle w } を式(2)

{ \displaystyle W(\mathbb{P}_r\ ,\ \mathbb{P}_\theta) = \sup_{|| f ||_L \ \leq 1} \mathbb{E}_{x \sim \mathbb{P}_r} [ f(x) ] -  \mathbb{E}_{x \sim \mathbb{P}_\theta}[ f(x) ] \ \ \ (2) }

が最大になるように訓練して、(パラメータの値がある範囲を超えたら抑える)

何回か訓練させた後、つまりEM距離になってくれているCritic { \displaystyle f_w }を使って、生成器 { \displaystyle  g_\theta(z) } のパラメータ { \displaystyle \theta } をより本物が作れるような方向に、つまり分布{ \displaystyle P_r }に近づくように更新していく。

生成器のパラメータの更新については数式ではこう表現されている。

{ \displaystyle \nabla_\theta W(\mathbb{P}_r\ ,\ \mathbb{P}_\theta) = -  \mathbb{E}_{z \sim p(z)} [ \nabla_\theta f(g_\theta (z)) ] }

GANやDCGANでいえば critic を discriminatorと見立てたとき、{ \displaystyle f_w(x)} の返す値は 1(本物)と見れるので、上記の式でも { \displaystyle f(g_\theta (z)) }が 1(本物)の値を返すように学習するという見方で大丈夫だろうか。

学習の方法の流れが優しく書いてあったので、WGANを実装してみる。UnrolledGANのときと同じくガウス分布のデータを使って実験する。

1回の学習ステップの流れとしてはこうする

f:id:Owatank:20180627170709j:plain

先に k回 critic を更新してから、k回更新したcritic、もといEM距離を使って1回だけ生成器を更新する。なんかUnrolledGANと似てるな。

前回コード載せたけどここにも置く

github.com

論文で再三リプシッツ関数だからなと述べているのに、出力にtanh関数やsigmoid関数を使ってしまって、最初学習がうまくいかなかった。
出力を critic、generatorどちらとも

### Generator 
#fc = tf.tanh(tf.nn.xw_plus_b(h2, self.gen_w3, self.gen_b3))
fc = tf.nn.xw_plus_b(h2, self.gen_w3, self.gen_b3)

### Critic
#fc = tf.nn.sigmoid(tf.nn.xw_plus_b(h2, self.cri_w3, self.cri_b3))
fc = tf.nn.xw_plus_b(h2, self.cri_w3, self.cri_b3)

に直したらうまくいった。アレ・・・?ただの恒等関数ってリプシッツ関数でいいのかな?)`Д゚).・;'∴

UnrolledGANのときは、ガウス分布の入力の組が [ -1 , 1 ] の範囲の値をとるからGeneratorの出力関数をtanh関数に設定したけど、そうしないでただの恒等関数でもちゃんと [ -1 , 1 ] の値を取ってきてくれるのだろうか。不思議だなあ・・・

あとはWGANでめんどいクリッピングの操作はゴリ押ししか今のとこできなかったので次のようにオペレーションを定義した

clip_value = 0.01

clip_list = [
    self.critic.cri_w1.assign(
        tf.clip_by_value(self.critic.cri_w1, -clip_value,
                         clip_value)),
    self.critic.cri_w2.assign(
        tf.clip_by_value(self.critic.cri_w2, -clip_value,
                         clip_value)),
    self.critic.cri_w3.assign(
        tf.clip_by_value(self.critic.cri_w3, -clip_value,
                         clip_value)),
    self.critic.cri_b1.assign(
        tf.clip_by_value(self.critic.cri_b1, -clip_value,
                         clip_value)),
    self.critic.cri_b2.assign(
        tf.clip_by_value(self.critic.cri_b2, -clip_value,
                         clip_value)),
    self.critic.cri_b3.assign(
        tf.clip_by_value(self.critic.cri_b3, -clip_value,
                         clip_value))
]

self.clip_op = tf.group(*clip_list)

今回はtf.group()というのを使ってみた。これを使うと訓練時にいちいち各パラメータのクリッピングのオペレーションを呼ばずに、

# Train Critic
# Critic step for Critic
for k in range(critic_step):
    # ノイズ事前分布からノイズをミニバッチ分取得
    noise_z = np.random.uniform(
        -1, 1, size=[batch_size, 100]).astype(np.float32)
    # 訓練データのミニバッチ取得
    cri_perm = np.random.permutation(datanum)
    X_batch = X_train[cri_perm][:batch_size]

    sess.run(
        self.opt_cri,
        feed_dict={
            self.input_X: X_batch,
            self.is_train: False,
            self.gen_z: noise_z
        })

    # Clip Critic Parameter
    sess.run(self.clip_op)

一行でクリッピングの操作を済ませることができる。かしこい

論文通りcritic_stepの回数を5回に設定して実験する。最適化の手法としてAdamのようなmomentum based optimizerはWGANの訓練を不安定にさせるからRMSPropのがいいよと書いてあったけど、なんかこのガウス分布のデータにおける実験ではAdamのがよかったからこっちを採用した。RMSPropはよく知らなかったけど、

We therefore switched to RMSProp [21] which is known to perform well even on very nonstationary problems [13].

と書いてあって、RMSPropおもろいなと思った。いい情報だ(∩ ^ω^ ∩)

実験結果としてはこんな感じ

f:id:Owatank:20180627172915p:plain

マジでただの恒等関数の出力で [ -1 , 1 ]の範囲の値を取ってやがる・・・。ほんまか・・・

前のUnrolledGANの時の結果は次の通りだった
f:id:Owatank:20180601115051p:plain

うーん・・・。UnrolledGANのが綺麗に生成できている気がする。同じネットワーク構造、ハイパーパラメータじゃないから比較は難しいんだけども
でも大体入力データを真似てくれているから実装としてはあまり間違えはなさそう

ポアンカレ埋め込みの論文と同じで距離についてワクワクさせてくれる素敵な論文だった。Appendixのところとかまだ完璧に理解できないのでまた成長したら読みたいな。
どっちも同じFacebookのリサーチャーの人が関わっているらしくてFacebookすげえ・・・

おまけでこの論文では feedforward neural network のことを

By a feedforward neural network we mean a function composed by affine transformations and pointwise nonlinearities which are smooth Lipschitz functions (such as the sigmoid, tanh, elu, softplus, etc).

と下に小さい箇所で述べていた。なんかめっちゃカッコいい・・・?カッコよくない・・・?

WGANの論文読んでTensorflowで実装する その1

前回、間違えてUnrolledGANの論文を読んでしまった。
このWGANというのが本当は読もうと思った論文。正直UnrolledGANを先に読んでなかったらWGANの理解が深まらなかったと思う。読んでてよかった

という訳で論文はここからどうぞ

[1701.07875] Wasserstein GAN

あとコードも先に置いておく

github.com

WGANという名前が付いてるから、このWが重要になってくる。WはWassersteinの略。わずさーなのかわっさーなのか英語がカスなので読み方がわからん・・・

Wassersteinというのは論文ではEarth Mover (EM) distanceとも呼ばれる。distanceだから距離を表すもの。この距離をGANに使ったらいい感じになったってことなのか?と読み始めに思った。

今まで自分が読んだGANはどれも GeneratorとDiscriminatorの学習速度のバランスが悪いから何とかする、というのだった。両モデルにバッチ正規化層を追加したり、学習方法を変更したりとか、、

WGANはざっくりいうと、バランスを調整するためのバッチ正規化といったトリックを使う必要がなく、単なるMLPを用いたGANでもEarth Mover (EM) distanceを使うことで学習が崩壊せずに上手く行くぜってことが書いてある。

ス、スゲー!!!わずか論文3ページ目でこのインパクト・・・。続きが知りたくなるやんけ

それで何でEarth Mover (EM) distanceが出てくるんだよってなる。ここで生成モデリングについて考える。

f:id:Owatank:20180620145145p:plain

{ \displaystyle P_{r} }を「知りたい目的のデータの分布」とする。

{ \displaystyle P_g }をこの{ \displaystyle P_r }に近づける、もとい形状が似るようにしたい。どうするか・・・

{ \displaystyle P_g }がパラメータ{ \displaystyle \boldsymbol{\theta_g} }で作られているとしたら、{ \displaystyle \boldsymbol{\theta_g} }の値を変えてやれば形状が変わったり、軸 { \displaystyle a } も別のところに移動できるはず

じゃあパラメータの値を上げたり下げたりするとして、どっちの操作をすればいいのか。パラメータが3つの値から成ってたら、一つ目の値は今より小さくして、二つ目の値は今より大きくして、、という決定をどうすればいいかわからない。

今パラメータ{ \displaystyle \boldsymbol{\theta_g} }が持つ値を全て今より大きくしたとき、{ \displaystyle P_g }{ \displaystyle P_r }近づいているのか、遠のいているのか、その情報が得られれば、「パラメータを大きくしたら{ \displaystyle P_r }に遠のいたから下げよう」といったことができそうだ

近いってどう数値として表すか・・・シンプルだと
f:id:Owatank:20180620151857p:plain

関数 { \displaystyle f } と関数 { \displaystyle g } の二つがあったとして、この二つが近いとしたら { \displaystyle f } がとる値の± { \displaystyle \varepsilon } した範囲(赤の部分 { \displaystyle V_\varepsilon }{ \displaystyle \varepsilon }は小さい正の数)に関数{ \displaystyle g }のとる値が入っていれば、{ \displaystyle \varepsilon }が小さい値であるほど赤の範囲は狭くなるからより二つの関数が「近い」といえるはず。

| { \displaystyle f(x) - g(x) } | { \displaystyle < \varepsilon \ (\varepsilon}は小さい正の数)

{ \displaystyle x }がとる区間を指定してその区間で上が成り立てばいい

| { \displaystyle f(x) - g(x) } |みたいに距離を使えば二つの分布が似ているかどうかがわかりそうだ(∩^ω^∩)

だから EM距離を使うぜ といった話になるんだな。多分・・・

話は戻って、論文では EM距離がすごいんだぜーというのを説明するためにEM距離含めて分布が近いかどうかを教えてくれる距離を4つ紹介している

せっかくなので一つずつ見ていく

まず前提として { \displaystyle \chi } をコンパクトな集合とする。有界閉集合ならコンパクトだけど、コンパクトは後なんか被覆が云々で言えたような・・・
論文だと 例として [0,1] の閉区間とかーって書いてある。うん、、
{ \displaystyle Prob(\chi) }{ \displaystyle \chi }上での確率測度(probability measure)とする。{ \displaystyle Prob(\chi) }が確率測度といえるための条件満たしてるぜってことでいいかな

1. Total Variation (TV) distance

{ \displaystyle \delta (\mathbb{P_r}\ ,\ \mathbb{P_g})\ = \ \sup_{A \in \Sigma} |\mathbb{P_r}(A) - \mathbb{P_g}(A)| }

{ \displaystyle \Sigma }{ \displaystyle \chi }でのボレル集合とする、と書いてある。 え何それは・・・
話を聞いたら、 { \displaystyle \chi }という集合の中で、{ \displaystyle Prob(\chi) }でその値を知りたいもの全部を含めた集合ってことでいいらしい。

その認識で考えれば、絶対値の差をとってるし、sup(上限)を選べってあるから

f:id:Owatank:20180620161837p:plain


こんな感じかな。(図ではボレル集合を一つの範囲でしか取らなかったけど)

確率の値での差を取るから、もし二つの分布が一致してたらTV距離は最大で0、一致してなかったら最大で1を取るから、 この距離は小さい方が近いってことを表してくれるのか。

2. The Kullback-Leibler (KL) divergence

{ \displaystyle KL (\mathbb{P_r}\ ||\ \mathbb{P_g})\ = \int log (\frac{P_r (x)}{P_g (x)})P_r (x) d \mu (x) }

KLダイバージェンスについては解説の記事が沢山あるから省くけど、{ \displaystyle P_r }{ \displaystyle P_g }が似てると、logの部分は log1 になってく、つまり 0に近づいていくので距離としては値が小さいほど二つが似ていることになる。分母の{ \displaystyle P_g }が 0 をとったらその・・・計算できないというか・・possibly infiniteと書いてあった。

ダイバージェンスという単語は電話レンジが云々のアニメで知ってた。
でも「距離」の紹介なのに「ダイバージェンス」っておかしくね?って思うんですが、距離 { \displaystyle d }

  1. { \displaystyle d(x,y) \geqq 0 }
  2. { \displaystyle d(x,y) = d(y,x) }
  3. { \displaystyle d(x,y) \leqq d(x,z) + d(z,y) }

この3つの条件を満たしていれば{ \displaystyle d }を距離と(正確には{ \displaystyle d(x,y)}をxとyの距離)いえる世の中のルールらしい
ダイバージェンスは二つ目を満たしてないから距離とは言えないとか
ところで距離の公理を初めて知った時、三角不等式が点列の収束を調べるときに使うものと知ったんですが、高校時代ナニコレ?と思ってたものがすげー役割を持ってたって思うと興奮した

3. Jensen-Shannon (JS) divergence

{ \displaystyle JS (\mathbb{P}_r\ ,\ \mathbb{P}_g)\ = KL (\mathbb{P}_r \ ||\ \mathbb{P}_m ) + KL (\mathbb{P}_g \ ||\ \mathbb{P}_m ) }
{ \displaystyle \mathbb{P}_m = \frac{\mathbb{P}_r + \mathbb{P}_g }{2} }

ジェンセ・・イェンセン・・論文でも他3つの距離と比べて説明少ないんだけど、{ \displaystyle P_m }の分母が2になってるから無限大になるのは回避できそう?

4. Earth-Mover (EM) distance or Wassterstein-1

{ \displaystyle W(\mathbb{P}_r\ ,\ \mathbb{P}_g) = \inf_{\gamma \in \Pi (\mathbb{P}_r\ ,\ \mathbb{P}_g)} \mathbb{E}_{(x,y) \sim \gamma} [ || x\ - \ y || ] }

これが今回の主役のEM距離だけど、式がむずそう
まず、 { \displaystyle \Pi ( \mathbb{P}_r \ ,\  \mathbb{P_g} ) }は同時分布{ \displaystyle \gamma(x,y) }の集合を表すそうだ

論文ではどうやらtranspoted、日本語だと輸送がうんぬんと言っている。これも話を聞いてみたら
f:id:Owatank:20180620173817p:plain
二つの分布をそれぞれ四角のブロックの集まりで表す。
{ \displaystyle P_g }がもつブロックを、{ \displaystyle P_r }の方に移すとする。{ \displaystyle P_r }と重なっていないブロックを移していけば、いつかは{ \displaystyle P_r }と同じ形になる

ここで移し方には、初期の分布の設定にもよるけど、沢山のパターンがあると思う。
ブロックを移すにも時間がかかる。EM距離は求めたい時の分布の位置状態で、ブロックの移す時間が一番少ない、最小コストの値を二つの分布の「近さ」として与えてくれる。

{ \displaystyle P_g }の右端にあるブロックを{ \displaystyle P_r }の左端の方に移したら、移すのにめっちゃ時間がかかってもったいない。

つまりブロックの輸送を開始する時点で、二つの分布が似ていれば、もともとブロックの移すコストも少なくなるはずだろってことか。発想がすげえ・・・

地味にこのEM距離はコンパクトな集合においてだけ使えるといったものじゃなくて、コンパクトじゃない集合においても使えるらしい。その時不思議な結果が出るとか何とか・・・

論文ではこの4つの距離を紹介したあと、この4つの距離を使ってシンプルな{ \displaystyle P_r } ([0,1]の一様分布 )を{ \displaystyle P_g }で近づけていく実験をしてた。
結果としては EM距離は以外は、二つの分布がぴったり重なる時と重ならない時で値が TV距離だと 1と0、 JSダイバージェンスだとlog2と0、といったように極端な結果を出していた。

論文の実験図だとこんなん

f:id:Owatank:20180620180633p:plain

左がEM距離の値、右がJSダイバージェンスの値
{ \displaystyle P_g }のパラメータ{ \displaystyle \theta }を0に近づけていくのがゴールで、EM距離はパラメータ{ \displaystyle \theta }が0に近づいていくほど、徐々に小さくなっていくのに対して、右のJSダイバージェンスでは、{ \displaystyle \theta }=0以外では { \displaystyle log2 }の値を取り続ける。

よくスイカ割りするじゃないですか。目隠しして歩いてスイカにたどり着くとして、1歩歩くごとにその地点からゴールまでの距離を教えてくれる人が

「ゴール以外は全部{ \displaystyle log2 }と教えてくれる人」と、「ゴールからあと{ \displaystyle K }メートル(ゴールに近くほどKが小さい)と教えてくれる人」だったらどっちのが嬉しいっていったら自分は後者。スイカ割りしたことないけど

きっと生成モデリングにおいても、{ \displaystyle \theta }がある「距離」が小さくなる方向に収束していくにつれて、{ \displaystyle \theta }から作られる{ \displaystyle P_g }が目的の分布{ \displaystyle P_r }に近づいていけるなら、それが嬉しいパラメータの修正の仕方なんだろう

だからEM距離はすごいんだぜー というのがわかったけど、どうやら上のEM距離の定義式のまんまGANの目的関数として使うのは厳しいらしい。ええ・・・
長くなったので分ける

この論文読む前に志賀浩二先生の「位相への30講」を読んでいなかったらほぼ即死だった・・・

でもこの論文で「測度」の公理なり、ちょいちょい見ていた「加法族」の意味なり知ることができてよかった(∩^ω^∩)
測度論はフーリエか何かの勉強で「オイラーの公式が成り立つことを詳しく知るには「測度論」について調べることをオススメする」みたいな文章があって単語だけ覚えてたんだけど、こんなとこで会うとは思わなんだ・・・
ちょいちょい意味が繋がるのは嬉しい

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

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

TensorflowでDCGANを実装した

前回でMLPでのGANの実装が大体できたので、次はDCGANを実装に挑戦する。
DCGANのDCは Deep Convolution のDCだから畳み込み層を追加してパワーアップした感じのGANなんだろかというのが論文を読む前のイメージだったりする。

コードはここ

github.com

いつも通り先に論文を読んだ

[1511.06434] Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks

前回読んだ論文で提案されたGANでは学習が不安定だったり、生成物にノイズがあったり、キメラみたいな意味的に、視覚的に理解が難しいものが生成されることがあると書いてあった。その後LAPGANというGANを拡張したものも出たがそれでもノイズなどは残ったらしい

DCGANではそのような学習の不安定さを低減するようにしたり、より精密なものを生成できるように改善されたGANのアーキテクチャと書いてある。すごいやん・・・。変更点としては生成モデルと識別モデルのネットワーク構造にCNNを用いるのと、3つのアプローチを採用するとある

  1. CNNといっても、Max Pooling とかを無くし、畳み込み層オンリーのネットワーク all convolution net というものを採用する。モデル自体が独自に空間ダウンサンプリング、アップサンプリングできるようにするためとか何とか。

  2. 分類問題においてのCNNでは畳み込み層のあと、それを平坦化(flatten)して全結合層に渡してクラス数に対応する出力を得るといったものが多い。Alexnetは何層もの畳み込みと最大プーリングのあと、全結合層が3層くらい続いている。DCGANでのCNNはこういった畳み込みの後の全結合層を生成器と識別器の両ネットワークにおいて排除する
    生成器では最後の畳み込み層で入力と同じ画像サイズ、チャンネルのものを返して、識別器では最後の畳み込み層の値を平均プーリングを適用して(バッチ数、クラス数)といった形の出力を返すようにする感じで合ってるはず

  3. 学習の不安定さの改善策として、両モデルの中間層にバッチ正規化(Batch Normalization)を適用する。それと生成器では出力層以外の層での活性化関数をRelu関数を、出力層はtanh関数([-1,1]の値を返す)を用いる。識別器には出力層にはシグモイド関数を、出力層以外にはLeakyRelu関数を用いる。まだLeakyReluについては知見はないけどこの関数のハイパーパラメータとしてalpha=0.2がいいらしい

変更点の次に実際のネットワーク構造は次のような図が論文に載ってあった
f:id:Owatank:20180514174053p:plain

相変わらず生成器の入力は100次元で一様分布から取ってくるっぽい。畳み込みの後の全結合層がダメだから、その前の全結合層はいいのかな。100次元の入力を全結合層に渡して 4*4*1024 の次元にする感じか。
よくチュートリアルとかで見る畳み込みは上の図で言えば右にいくにつれて四角が小さくなっていくけど、これは逆で四角が大きくなっていく。アップサンプリングってそういうことらしい。誤ってデコンボリューションと呼ばれると論文に書いてあった。

試したいデータセットがMNISTの手書き数字 28*28の1チャンネルの画像なので上の図のまんま適用ができない。困った。
上の図と似た次のようなネットワークを自分で考えて最初試すことにする。

f:id:Owatank:20180514175000j:plain

すごく・・・アナログです・・・

前回でのGANで5の手書き数字のみを訓練データとして渡して学習したところ、精一杯学習させてできたものは次のものが限界だった
f:id:Owatank:20180515123324p:plain

エポック数としては1エポック6000枚程度で3000エポックほど回した。2000エポックほどでは左のようにすごくぼやけた感じのものしかできていなかった。後心なしか生成できるものが同じ形状の5ばっかだった。

DCGANだし畳み込みを導入して前よりパワフルなはずだし、手書き数字5だけじゃなくて形状が似てる手書き数字3も訓練データとして渡してみよ〜^^と考えて入力のノイズを一様分布の [-1,1] の 100次元から取ってきて、約11000枚ほどのデータで100エポックほど回したところ

f:id:Owatank:20180515123739p:plain

こんなものしかできなかった。なんだこれは・・・
学習係数を変えてみたり、ネットワーク自体のパラメータを変更したりしてみたけど、大体上と同じ結果しか生成されなかった
背景が黒だから学習自体は似せるように学習しているとは思うんだけど

原因として考えられそうな仮説をいくつか考えて、試行錯誤しつつ検証していって何とか期待できる生成物が得られた。

f:id:Owatank:20180515131941g:plain

次のような変更をした

  • ネットワークは 4*4*256 -> 7*7*128 -> 14*14*64 -> 28*28*1 とユニット数を少なくした

  • 入力の次元数を手書き数字の5と3といった一部のみを渡すときには 100次元ではなく50次元と次元数を削減すること

  • 識別器のネットワークにはバッチ正規化層を適用しないこと、また最後の出力層に全結合層を用いること

  • 重み減衰を両モデルのパラメータに適用すること

2つめに関しては、MNISTの手書き数字0~9全体のおいて入力を100次元で [-1,1]の一様分布から取ってくるんだから、一部だったら次元数をその分少なくしたり、範囲を [-0.2,0.2]とか小さくした方が表現の幅として適切なのかなと考えた。結果的には範囲の制限より次元数の削減の方がかなり効いていた。

他もすごい効いた。ただ3つ目に関しては最初、識別器の出力を

h4 = tf.reduce_mean(
    tf.nn.avg_pool(
        h3, ksize=[1, 4, 4, 1], strides=[1, 1, 1, 1], padding='VALID'),
    axis=[3])

といった形で実装していたのだが、ミスってるのかな・・・と思い一旦全結合層で試してしまった。割とよかったのでそのまんまにした。

入力が次元数50で[-0.2,0.2]の範囲の一様分布から値をとってくるもので、重み減衰ありで手書き数字3と5を渡したときの結果

f:id:Owatank:20180515125310p:plain

希望が見えてきた

入力が次元数50で[-1,1]の範囲の一様分布から値をとってくるもので、重み減衰ありで手書き数字3と5を渡したときの結果

f:id:Owatank:20180515125355p:plain

こっちのが早く学習しているように見える。前回のGANと比べて生成物の表現の幅が広くて嬉しい

入力が次元数50で[-1,1]の範囲の一様分布から値で重み減衰なしだと
f:id:Owatank:20180515125924p:plain

50エポックまででこんな感じだった。有り無しで結構変わっている。

最後にMNISTの手書き数字の全訓練データ(約5万枚)を渡して学習させてみる
上と同じネットワークで入力が次元数50で[-1,1]の範囲の一様分布、重み減衰ありの結果
f:id:Owatank:20180515130735p:plain

上と同じネットワークで入力が次元数100で[-1,1]の範囲の一様分布、重み減衰ありの結果

f:id:Owatank:20180515131107p:plain

途中で学習が打ち切られてしまったのでエポック100まで回し損ねた
次元数100の方が次元数50の方より鮮明な感じがある。数字0〜9全体においては次元数は少し増やした方がいいのか

論文には一様分布から取ると書いてあったが、特に書いてなかったけど実際VAEみたいに正規分布から取ってくると精度が悪かったりするのか気になった

解決策を見つけた後にtensorflowのライブラリにtf.image.resize_imagesというメソッドがあることを知った。
これでMNISTの画像を 28*28から64*64にして論文通りネットワークを実装すればよかった・・・

GANの論文を読んだ自分なりの理解とTensorflowでのGANの実装メモ

タイトルのまんま

VAEの理解のために変分ベイズの方を優先したいが卒業がかかっているので先にGANの論文を読んだ
GANの論文って多いっぽいが以下のリンクのものを読み読みした

[1406.2661] Generative Adversarial Networks

これは自分の頭がお猿さんなせいもあると思うがハチャメチャ読みやすかった
Algorithm 1というパートのところは感動で涙が出た。な、なんてわかりやすい解説なんだ・・・・・

GAN(Generative Adversarial Nets)よろしく敵対学習は今自分が知りたいVAEと同じく生成モデリングのひとつ。VAEの論文を読んだときにもあったが既存の生成モデリングの手法は、MCMCといった計算時間がかなり掛かるものが依存関係として必要だったり、そもそも観測データの周辺分布の計算の積分が計算困難なことがあったりして困っていたっぽい(近似も困難だとか)
このGANはそういった計算の困難なところとかを回避した生成モデリングの手法。この手法ではモデルの学習中にMCMCが必要ないと書いてある。マジで?

そもなんでGenerative Adversarial Netsといった強そうな名前が付いたのか。論文での例を使うことにする。

本物に近い偽札を作りたい人がいたとする。偽札を作るモデルを生成モデル G (Generate)とする。
f:id:Owatank:20180417172250p:plain

その一方で、偽札の検出をしたい人がいる。本物か偽札かどうかを見分けるモデルを識別モデル D (Discriminative)とする

f:id:Owatank:20180417172641p:plain

GANではこの2つのモデルを同時に学習させて、生成モデルの作った偽札が識別モデルによる本物との識別が難しいくらいのものを作れるようにしていく。
偽札の例だと2つのモデルは作るものと見分けるもので反対のもの同士だからここら辺から敵対という名前が付いたといったことが書かれていた、、はず、、

どう学習するかというと生成モデルが本物と同じくらいの偽札を作るように、識別モデルが受け取った入力が本物か偽物か完璧に識別できるようにする、ある目的関数を設けてそれを最適化していく。具体的には確率勾配法を用いて最適化していく。

生成モデルと識別モデルの両方を多層パーセプトロンで構築すると、逆伝搬とドロップアウト、順伝搬から生成モデルによって得たサンプルの3つの要素を使うことでよりシンプルにそして同時に両モデルの学習が可能となると書いてある。天才か・・・

多層パーセプトロンを用いるのはわかったけどどういった枠組みなんじゃい。

生成モデル G は事前入力ノイズ変数 { \displaystyle P_{\boldsymbol{z}} (\boldsymbol{z}) } とパラメータ { \displaystyle \theta_g} をもつ。そして { \displaystyle  G(\boldsymbol{z} ; \theta_g) }はデータ空間へのマッピングを表すと書いてあったが要は入力変数zと生成モデル自身が持つパラメータ { \displaystyle \theta_g} を使って目的のデータと似たデータを作るってことかな。生成モデル G が多層パーセプトロンによって表され、微分可能な関数となっている。このGが観測データ{ \displaystyle \boldsymbol{x} } (上の例で言えば本物のお札) に対するジェネレータの分布{ \displaystyle p_g }を推定するために必要なのだ。

2つめの多層パーセプトロンとして { \displaystyle  D(\boldsymbol{x} ; \theta_d) } を定義する。これは入力ベクトル { \displaystyle  \boldsymbol{x} } と識別モデル自身が持つパラメータ { \displaystyle \theta_d } を用いて識別結果(偽物か本物か)を単一のスカラーを出力として返す。
関数 { \displaystyle  D(\boldsymbol{x}) }は入力{ \displaystyle \boldsymbol{x} }がジェネレータの分布{ \displaystyle p_g }から来ているのか、学習に用いているデータの分布から来ているのかを確率として表して、出力として返す。二値分類みたいなものの解釈でいいだろうか

識別モデル D は訓練データ(本物のお札データ)と生成モデル G からのサンプルの両データに対して正しいラベル(1は本物、0は偽物といったラベル)を割り当てる確率を最大にするように学習する。

そして生成モデル G は次の式が最小になるように学習する

{ \displaystyle  log(1-D(G(\boldsymbol{z})) ) }

偽札のラベルを 0 とすれば、上の式は log(1-0) で 0 が最小の値になるから、{ \displaystyle G(\boldsymbol{z}) }が作った偽物を{ \displaystyle D(G(\boldsymbol{z})) }がしっかり偽物と識別するように学習させろよってことだな

GとDを同時に学習させるのでこれらをまとめた値関数{ \displaystyle V(D,G) }を考えていく

{ \displaystyle  \min_{G}\max_{D} V(D,G) = \mathbb{E}_{\boldsymbol{x} \sim p_{data(\boldsymbol{x})}} \ {[}logD(\boldsymbol{x}){]} +  \mathbb{E}_{\boldsymbol{z} \sim p_{\boldsymbol{z}(\boldsymbol{z})}} \  {[}log(1-D(G(\boldsymbol{z}))){]} }

数式書くのクッソ疲れた。右辺の第1項は{ \displaystyle logD(\boldsymbol{x}) }だから本物のお札を入力として渡している。期待する値は{ \displaystyle logD(\boldsymbol{x}) }が最大になればいいのだから log(1) = 0 でその平均、期待値は0が望ましくて、第2項は上で述べたように { \displaystyle D(G(\boldsymbol{z})) = 0} が望ましい値であり結果としてその周りの期待値も 0 に近い方が良いで合ってるだろうか

お猿に優しい学習アルゴリズムの教育的な説明が書いてあった。次のように自分なりに理解した。
訓練データのサンプルから作られるデータ分布{ \displaystyle p_x } 、生成モデルGから作られるジェネレータ分布{ \displaystyle p_g } 、入力が本物か生成モデルから作られたものかを確率的に表す識別モデルDの3つが画像のようになっているとする。

f:id:Owatank:20180418103634p:plain

生成モデルGが本物に近いものを生成できるようにするということは上の図のデータ分布{ \displaystyle p_x }の形状とジェネレータ分布{ \displaystyle p_g }の形状が一致、もしくは限りなく近くなればいいのがわかる。そうするには上記の{ \displaystyle V(D,G) }を最適化しろと論文が述べている。

過適合の問題を避けるために、先に識別モデルDの更新を kステップ 行ってから、生成モデルGを更新後の識別モデルDを元に 1ステップ 更新すると書いてある。
Dはデータからサンプルを識別するように学習していく。具体的には次のように学習する。

{ \displaystyle  D^{\ast}(\boldsymbol{x}) = \frac{p_{data}(\boldsymbol{x})}{p_{data}(\boldsymbol{x})+p_{g}(\boldsymbol{x})} }

生成モデルGの更新が終わった後にまた識別モデルDの更新を行うが、そのときのDの勾配は入力が本物のデータとして分類されるような可能性が高い方に生成モデル{ \displaystyle G(\boldsymbol{z}) }を導くと書いてある。

識別モデルDを学習する(kステップ)
G の持つ事前入力ノイズ変数{ \displaystyle P_g(\boldsymbol{z}) }からm個のノイズをサンプリングする。これら{ { \displaystyle {\boldsymbol{z}}^{(1)} , ... , {\boldsymbol{z}}^{(m)}} }を元にm個の偽物を生成する。そして訓練データセット(データ分布)からm個の本物をサンプリングする。2つから次のようにDを確率勾配法を用いて更新する。

{ \displaystyle  \nabla_{\theta_{d}} \frac{1}{m} \sum_{i=1}^{m} {[}logD({\boldsymbol{x}}^{(i)}) + log(1-D(G({\boldsymbol{z}}^{(i)}))) {]}}

f:id:Owatank:20180418105655p:plain
{ \displaystyle p_x }{ \displaystyle p_g }が似ているところは識別が難しい=値が大きい(はっきりしている)ということ。真ん中あたりは形状が違うから値があやふやになっている感じ・・・なのか・・・?エントロピーみたいなもん?

このD(の勾配)を元に{ \displaystyle G(\boldsymbol{z}) }がデータ分布と類似するように学習させる。
上と同様にG の持つ事前入力ノイズ変数{ \displaystyle P_g(\boldsymbol{z}) }からm個のノイズをサンプリングする。これら{ { \displaystyle {\boldsymbol{z}}^{(1)} , ... , {\boldsymbol{z}}^{(m)}} }を元にm個の偽物を生成する。そしてその偽物たちから次のようにモデルGを確率勾配法より更新する。

{ \displaystyle  \nabla_{\theta_{g}} \frac{1}{m} \sum_{i=1}^{m} log(1-D(G({\boldsymbol{z}}^{(i)}))) }

f:id:Owatank:20180418110144p:plain

これで1エポックが終了か?これを何エポックか繰り返していくと
f:id:Owatank:20180418111411p:plain

{ \displaystyle p_x }={ \displaystyle p_g }となるくらいに類似することが可能なはず。このとき{ \displaystyle D(\boldsymbol{x})}は2つが似すぎていて入力がどちらのものか判断が難しい={ \displaystyle \frac{1}{2} }の値をとるということなんだな

2つの確率密度分布{ \displaystyle p_x }{ \displaystyle p_g }が類似していることを確かめる指標としてカルバックライブラダイバージェンスは使えないんだろうか?と思ったが、その後のアルゴリズムがちゃんと機能するかの裏付け?っぽい Theorem のパートで出てきていた。うーん証明難しい

損失関数の定義からアカン・・・めっちゃ自分でも作れそうなほどシンプルやんけ・・・と思ったのでTensorflowで実装してみた
ソースはここ

github.com

GeneratorとDiscrimitorの作成において、論文通りMLPで構築した。
ノイズ事前分布については正規分布のがいいのかと思ったが一様分布を用いている人が多かったのでそっちにした。
最適化の方法は Adam でいいかと思ったけど論文通りに Momentum で行った方が割とうまく行ったので論文のモデル設定に従った。

最初に損失関数を次のように定義していた

self.dis_loss_X = tf.log(self.discrimitor.run(self.input_X))
self.dis_loss_G = tf.log(self.label_t1 - self.discrimitor.run(self.generator.run(self.gen_z,self.is_train)))
self.dis_loss = -tf.reduce_mean(self.dis_loss_X + self.dis_loss_G) + dis_norm_term*dis_lambda_
self.gen_loss = tf.reduce_mean(tf.log(self.discrimitor.run(self.generator.run(self.gen_z,self.is_train))))

識別モデルの方は上で定義していた通りに実装したが、生成モデルは 1 - D(G(z)) じゃないやんと思うが、論文において

{ \displaystyle  log(D(G({\boldsymbol{z}}^{(i)}))) }


こっちのが収束がいいとか云々が少し書いてあった。
生成モデルで 1 - D(G(z)) を最小にするというのは生成したものが偽物とわかるようにパラメータを更新してねということになるから D(G(z)) のがいいよな・・・でもなぜかこれを最大化しろと書いてあって困ったがいう通りに従って-tf.reduce_mean()ではなくマイナスを取り去ったtf.reduce_mean()にした

そんなこんなでこれで学習を行ったところ損失がnanやinfになったりしてダメだった。そのせいか生成した画像は真っ暗なものしかできなかった。

損失関数は結局次の形で落ち着いた(後ろのnorm_termは重み減衰)

self.dis_entropy_X = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.label_t1, logits=input_X)  
self.dis_entropy_G = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.label_t0, logits=generated_X)   
self.dis_loss = tf.reduce_mean(self.dis_entropy_X + self.dis_entropy_G) + dis_norm_term*dis_lambda_
        
self.gen_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.label_t,logits=generated_X)
self.gen_loss = tf.reduce_mean(self.gen_entropy) + gen_norm_term*gen_lambda_

クロスエントロピーを使った。生成モデルに関しては、正解ラベルを本物 = 1 のラベルを与える。
こうすることで識別モデルでは生成モデルのものを偽物と判断するように学習していって、生成モデルは識別モデルを騙せるくらいの本物に近いものを作れるように学習していけるのだ・・・

次に詰まったのは最適化を行う際のパラメータの指定だった
最初はminimizeの部分でvar_listの指定をしていなかった。しかしこれを指定しないと、生成モデルの最適化を行う際に生成モデルのパラメータの更新と一緒に識別モデルのパラメータを偽物を本物と識別するような誤った方向に更新してしまう

参考にしたもの
TensorFlowで特定の変数を指定して学習させる方法
TensorFlowで必要な変数を選択してsave/restoreする

ようは識別モデルの更新には(生成モデルの推論結果を更新に用いるが)識別モデルのパラメータのみを、生成モデルの更新には(識別モデルの推論結果を更新に用いるが)生成モデルのパラメータのみを更新するようにしないと学習がうまくいかない(´・ω・)

両モデルのMLP構築におけるパラメータのユニット数や初期値にも苦労して何とか次のような結果が得られた
f:id:Owatank:20180420165454p:plain

これはMNISTのデータセットの中の手書き数字が 4 のみのデータで学習したもの
はっきりした形のものは得られなかったが、それっぽい形状が得られてるんじゃなかろうか
これはbatch_normをモデルの層に追加していないものでの学習結果だが、追加したものは収束がのんびりで、かなりのエポック数を回せばいい感じの形状が得られそうだと試して思った。

当たり前だがハイパーパラメータやユニットの初期値の設定がかなり大事何だろうか
GANのより良い学習方法については論文があるらしいので読んで改善したい

地味に損失の定義式で関数名と引数名が長すぎて途中で定義式をぶった切る形にしないといけなかったのがムズムズした

追記

世の中には Google Colaboratory という便利なものがあるそうでGPUを使って学習できるように改造した。
チェックポイントも作るようにした賢い

ソースはここ

10エポックほど回してみてCPUでの学習とどれくらい変わるのか計測して見た
f:id:Owatank:20180421170818p:plain

まあ・・・こんなもんすよ・・・