読者です 読者をやめる 読者になる 読者になる

Wasserstein GAN(WGAN)でいらすとや画像を生成してみる

このWGANを使って以下のようないらすとや画像を生成したというのが本記事の主旨。 f:id:yusuke_ujitoko:20170520145613g:plain

本論文のcontribution

  • 確率分布間の距離を計算するための,従来使われてきた距離指標とEarth Mover(EM) distanceとの比較を行った
  • EM distanceの近似関数を最小化するWasserstein-GANを定義した
  • WGANが既存のGANにおける問題を解決することを示した
    • WGANではdiscriminatorとgeneratorの学習バランスを気にしなくて良い
    • ネットワーク構造も気にしなくて良い
    • mode dropping phenomenonも殆ど無い
    • discriminatorを学習させることでEM distanceを連続的に推定できる
    • learning curveが生成画像の質の指標とみなせる.

既存のGANにおける問題

既存のGANでは,訓練データの分布{P_{r}(x)}と生成データの分布{P_{g}(x)}のf-divergenceを最小化することを目的としている.
f-divergenceは様々提案されているが、Jensen–Shannon divergenceに近いものが多い.

f-divergenceは密度比{\frac{P_{r}(x)}{P_{g}(x)}}の関数になっているが、 問題が起きる場面がある。
例えば問題が起きる場面としてでも2つの分布のsupportがoverlapしていない状況を考える.
overlapしていないところでは,この密度比は無限に大きくなるか,ゼロになってしまう.
また,supportがdisjointな場合,密度比が定数なので,f-divergenceは定数となる.

簡単な例を下に示す.

  • 訓練データ群が{(0, z)}の点にあるとする
    このとき{z \sim U(0,1)}である.
    つまり訓練データは{x=0}かつ{y=0からy=1}までの線分上にある.
  • モデルはパラメータ{\theta}を持っており,点{(\theta, z)}を生成する.
  • このとき分布は完全に一致するか,まったくoverlapしないかのどちらかとなる.

指標として

  • Total Variation (TV) distance
  • Kullback-Leibler (KL) divergence
  • Jensen-Shannon (JS) divergence
  • Earth-Mover (EM) distance or Wasserstein-1

の4つが論文で挙げられている.
そのうちEM distance(左)とJS divergence(右)を下に示す.

f:id:yusuke_ujitoko:20170518203218p:plain

右のJS divergenceは,多くの場所で平らになっている.
これは多くの場所で勾配が0となることを示している.
もしGANでDiscriminatorがJS divergenceを近似するほど学習していた場合には,Generatorは勾配情報をほとんど受け取れないことに(ほぼゼロ)となってしまう.
これはGANにおける勾配消失問題の一例.

そこで今度は左のEM distanceを見てみる.
EM distanceはf-divergenceと違い,密度比の関数ではない.
EM distanceは直観的には, 分布{\mathbb{P_{r}}}を分布{\mathbb{P_{g}}}に変化させるために,地点{x}から{y}へ移動させるべき質量を示している. つまり最適輸送問題におけるコストに相当するもの.

分布のsupportがoverlapしない場合には,EM distanceはどのくらい離れているかを表現できる.

上の左図を見てみると,EM distanceでは最適なパラメータへの勾配を得ることができるのがわかる.
{\theta = 0}に収束できる)

EM distanceの定義は以下のようになっている. {} $$ W(P_{r}, P_{g}) = inf_{\gamma \in \Pi (P_{r}, P_{g})} \mathbb{E}_{(x,y) \sim \gamma} \mid \mid x - y \mid \mid $$ (infはminimumと考える)
2つの分布における全ての二点間のセットを考え,そのセットにおける二点間の距離の平均を取って,一番小さいセットにおける平均距離がEM distanceとなる. ただし、このEM distanceはintractableで直接計算できない.

そこで次のような双対問題があるので上の定義の代わりに利用する. {} $$K \cdot W(P_{r},P_{g}) = \sup_{||f||_{L} \le K} (\mathbb{E}_{x \sim P_{r}}[f(x)] - \mathbb{E}_{x \sim P_{g}}[f(x)])$$

これはK-リプシッツ写像である.
これは直観的には,訓練データの平均と生成データの平均間の最大のマージンを得る関数となっている.

WGANの効果

  • 学習が安定する
  • 色んなアーキテクチャで上手くいく.例えば多層パーセプトロンでもOK
  • $$ \mathbb{E}_{x \sim P_{r}}[f(x)] - \mathbb{E}_{x \sim P_{g}}[f(x)] $$ のプロットが収束するので,いつGANの学習を終えればいいか判断できる.
  • mode collapse問題を避けられる

Wasserstein GANの実装

論文に記載のアルゴリズムは以下のようなものになっている f:id:yusuke_ujitoko:20170518194322p:plain

上のアルゴリズムを見ると分かるように、オリジナルのGANと極めて似ている。
実装ではGANのコードに以下変更を加えるだけで済む。

  • 誤差関数からlogをなくす。
  • Discriminatorの出力は確率ではなくなるので、Discriminatorの出力に活性化関数を施さない。
  • (K-リプシッツ連続性を保つためにDiscriminatorの重みをクリップする
  • GeneratorよりDiscriminatorをよく学習させる
  • OptimizerとしてADAMではなくRMSPropを使う
  • 学習率は小さい値を使う。論文では0.00005を使っている。

いらすとや画像の生成

ネットワーク構造は割と何でもよいようなので、 DCGANを試した際の構造をそのまま使った。
その時はkerasで実装していたので、tensorflowに移植した上で修正を施した。

またデータセットについては、 DCGANUnrolledGANのときはいらすとやから取得できる全画像を使っていたが,今回は人間の顔画像のみをデータセットとして用いる. 具体的には64x64x3のサイズの画像1814点を用いた.

ミニバッチサイズは64.
GTX780Tiで7時間ほどかけて13万epoch分計算した。

0 epoch

f:id:yusuke_ujitoko:20170520142634p:plain

500 epoch

f:id:yusuke_ujitoko:20170520142643p:plain

1000 epoch

f:id:yusuke_ujitoko:20170520142658p:plain

2000 epoch

f:id:yusuke_ujitoko:20170520142702p:plain

13000 epoch

f:id:yusuke_ujitoko:20170520142713p:plain

WGANによる生成画像アニメーション

0〜7000 epochまでの生成画像のgifアニメがこちら。
下のDCGANの生成画像と比較すると

  • 振動が抑えられている
  • ぼやけている

のがわかる。

f:id:yusuke_ujitoko:20170520145613g:plain

(比較用)DCGANによる生成画像アニメーション

f:id:yusuke_ujitoko:20170520142834g:plain

UnrolledGANでいらすとや画像を生成してみる

これまで実装してきたGANDCGANでは学習の際にDiscriminatorが早く学習しすぎてしまうという問題があった. これに対するアプローチとして,Generatorに先取り学習をさせて,学習を促進させようという方法がある.

そのアプローチを採用しているのがUnrolled GANというネットワークだ。 今回はこの論文を読んで、tensorflowで実装してみる。 これまではkerasを使ってきたが、細かいところをいじるならtensorflowの方が便利。

GANのおさらい

GANの紹介
  • 生成モデルでは通常,尤度を最大化するよう学習するが,GANは尤度計算をexplicitに行わず,GeneratorとDiscriminatorのminimax gameを解く
  • GANは様々な成功を収めてきた
    • 画像生成
    • 画像高解像度化
    • etc.
GANの問題

GANには多くの問題もある

  • Generatorがたった1つの画像や類似した画像しか生成しなくなること(mode collapse)
  • GeneratorとDiscriminatorの学習が収束するのではなく振動すること
  • GeneratorとDiscriminatorのうちどちらかの学習が早く進みすぎると,もう一方のモデルへの伝達される勾配が小さくなり学習が進まなくなること
  • 収束したとしても,Generatorが訓練データの全体分布を生成しないこと(KL divergenceなどを目的関数としているのにも関わらず)
  • 生成画像の評価が難しいこと(Parzen windowを使う方法は,欠点が指摘されている)

本論文では,学習の不安定性とmode collapseに対応するためにDiscriminatorをunrolling optimizationする.

GANの仕組み

Unrolled GANの説明のために,通常のGANの仕組みの説明を導入する。 {} $$ \begin{align} \theta_{G}^{\ast} &= argmin_{\theta_{G}} ~ max_{\theta_{D}} ~ f(\theta_{G}, \theta_{D}) \\ &= argmin_{\theta_{G}} ~~ f({\theta_{G}}, \theta_{D}^{\ast}, \theta_{G}) \\ \theta_{D}^{\ast}(\theta_{D}) &= argmax_{\theta_{D}} ~~ f(\theta_{G}, \theta_{D}) \end{align} $$

ここで{f}{} $$ f(\theta_{G}, \theta_{D}) = \mathbb{E}_{x \sim p_{data}} log(D(x;\theta_{D})) + \mathbb{E}_{z \sim N(0, 1)} log(1-D(G(z;\theta_{G});\theta_{D})) $$

{p_{data}} は訓練データの分布)
Discriminatorの最適値 {D^{\ast}} は,Generatorの生成したデータの確率分布 {p_{G}(x)}を使って,次のように書ける. {} $$ D^{\ast}(x) = \frac{p_{data}(x)}{p_{data}(x) + p_{G}(x)} $$

反復的に パラメータ{\theta^{\ast} = (\theta_{G}^{\ast}, \theta_{D}^{\ast})} を最適解に近づけたいが,上の{f(\theta_{G}, \theta_{D})}{\theta_{G}}のconvexに遠く,{\theta_{D}}のconcaveに遠いため,学習は上手く進まない.

本論文ではこの問題に対処するため,Generatorの訓練を効率的に行うため, Generatorの訓練時に利用したい真の目的関数である{f(\theta_{G}, \theta_{D}^{\ast} (\theta_{G} ))} により近い,{f_{K} (\theta_{G}, \theta_{D}) }を使うことを提案している.

Unrolled GAN

Generatorの学習より,Discriminatorの学習が早く進んでしまうという問題に対し,UnrolledGANでは,Generatorの学習の際にKステップ学習した後のDIscriminatorを利用して勾配計算させている.

前半頑張りすぎて少し力尽きてきたので,ざっくり説明すると以下のようになる.

  • DiscriminatorをKステップ分学習させる.
    このとき,1ステップ学習後のDiscriminatorの勾配を覚えておく.
  • Kステップ分学習させたDiscriminatorを組み込んだモデルでGeneratorの勾配計算をする.
  • Discriminator,Generatorのパラメータを更新する

このようにしてGeneratorにだけ,Kステップ分先取り学習させることにより, GeneratorとDiscriminatorの学習のバランスをとろうというものである.

生成画像

このUnrolledGANでいらすとや画像を訓練データとして、画像生成してみる。 論文に書いてあるとおりK=5の設定でUnrolling optimizationする。 結論から言ってしまうと、論文に記載のネットワーク構造、パラメータ(を画像サイズに合わせて若干修正を加えたもの)で試してみたところ、うまく学習できなかった。

生成画像パターン1

f:id:yusuke_ujitoko:20170517202553p:plain f:id:yusuke_ujitoko:20170517203208g:plain

生成画像パターン2

f:id:yusuke_ujitoko:20170517203007p:plain f:id:yusuke_ujitoko:20170517203332g:plain

DCGANのときよりも微妙かも。
パラメータチューニング不足の感はある。
しかし手元の環境(GPU1個)だと時間がかかるためそこまでパラメータ調整できない。

さて,…次はどのGANを試してみようか…WGANかな.

リプシッツ連続

リプシッツ連続についてのメモ。

定義:リプシッツ連続

関数{f(x)}が任意の実数{x, y}に対し、 {} $$ \mid \, f(x) - f(y) \mid \leq k \mid x - y \, \mid $$ を満たす0以上の{k}がとれるとき、関数{f(x)}はリプシッツ連続であるといい、{k}をリプシッツ定数という。

{x = y}のとき、任意の実数について上式は成り立つので、 「関数{f(x)}がリプシッツ連続」であることは、「{x \neq y}となる任意の実数{x, y}に対して {} $$ \left| \frac{f(x) - f(y)}{x - y} \right| \leq k $$ を満たす0以上の定数{k}がとれることと同値である。 つまり関数{f(x)}がリプシッツ連続であるとは、関数{y = f(x)}のグラフ上の任意の異なる2点{(a, f(a)), (b, f(b))}を通る直線の傾きが、{-k}以上{k}以下である、すなわち、関数{f(x)}の変化率の絶対値は{k}を超えないということである。

定理:リプシッツ連続な関数は連続

{x_{n} \rightarrow x}とすると、{\mid \, f(x_{n}) - f(x) \mid \leq k \mid x_{n} - x \mid \rightarrow 0}より、{f(x_{n}) \rightarrow f(x)}

となるため、リプシッツ連続な関数は連続となる。

CS224dのTensorflow Tutorialを読んでみる

Tensoflowの公式のtutorialは眺めたことはあるが, 今一度確認のため,
CS224dのTensorflow tutorialを読んでみる.

f:id:yusuke_ujitoko:20170510232436p:plain

このメモは単なる写経です.
CS224dが気になる人は上のリンクを直接たどって見てみると良いと思います.

Tensorflowはnumpyと文法がそっくり.

例えば以下は同じ処理をTensorflowとnumpyで記述したもの.

import numpy as np

a = np.zeros((2,2))
b = np.ones((2,2))

print(np.sum(b, axis=1))
print(a.shape)

print(np.reshape(a, (1,4)))
import tensorflow as tf
tf.InteractiveSession()

a = tf.zeros((2,2))
b = tf.ones((2,2))

tf.reduce_sum(b, reduction_indices=1).eval()

a.get_shape()

tf.reshape(a, (1, 4)).eval()

ただしTensorflowは,式を明示的に評価する必要がある.

a = np.zeros((2,2))
ta = tf.zeros((2,2))

print(a)
# [[ 0.  0.]
#  [ 0.  0.]]

print(ta)
# Tensor("zeros_2:0", shape=(2, 2), dtype=float32)

print(ta.eval())
# [[ 0.  0.]
#  [ 0.  0.]]

Session Object

“A Session object encapsulates the environment in which Tensor objects are evaluated”

a = tf.constant(5.0)
b = tf.constant(6.0)
c = a * b
with tf.Session() as sess:
        print(sess.run(c))
        print(c.eval())

ただしtf.InteractiveSession()を使うと,jupyter上でデフォルトのsessionを保持してくれる.

Tensor Variables

Variablesは初期化しなければならない.

W1 = tf.ones((2,2))
W2 = tf.Variable(tf.zeros((2,2)), name="weights")

with tf.Session() as sess:
    print(sess.run(W1))
    sess.run(tf.global_variables_initializer())
    print(sess.run(W2))

Variableの更新

state = tf.Variable(0, name="counter")

new_value = tf.add(state, tf.constant(1)) # new_value = state + 1
update = tf.assign(state, new_value) # state = new_value

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print(sess.run(state))

    for _ in range(3):
        sess.run(update)
        print(sess.run(state))

複数の変数を同時にfetchすることもできる

input1 = tf.constant(3.0)
input2 = tf.constant(2.0)
input3 = tf.constant(5.0)

intermed = tf.add(input2, input3)
mul = tf.multiply(input1, intermed)

with tf.Session() as sess:
    result = sess.run([mul, intermed])
    print(result)

Inputting data

これまではtf.でTensorflow内で主導で変数を定義していた.
外部からデータをTensorflowに取り込むにはどうするか.

numpy arrayを取り込むとき

a = np.zeros((3,3))
ta = tf.convert_to_tensor(a)

with tf.Session() as sess:
    print(sess.run(ta))
# [[ 0.  0.  0.]
#  [ 0.  0.  0.]
#  [ 0.  0.  0.]]

ただ,convert_to_tensorは便利だけど,スケールしない.
そのため実際にはtf.placeholderを使う.
tf.placeholderはdummy nodeであり,計算グラフにデータを入れるentry pointsを提供するもの.

input1 = tf.placeholder(tf.float32)
input2 = tf.placeholder(tf.float32)
output = tf.multiply(input1, input2)
with tf.Session() as sess:
    print(sess.run([output], feed_dict={input1:[7.], input2:[2.]}))
# [array([ 14.], dtype=float32)]

TensorFLowの基本演算

定数

import tensorflow as tf

# 定数
a = tf.constant(2)
b = tf.constant(3)

with tf.Session() as sess:
    print("a: %i" % sess.run(a), "b: %i" % sess.run(b))
    print("Addition with constants: %i" % sess.run(a+b))
    print("Multiplication with constants: %i" % sess.run(a*b))

# a: 2 b: 3
# Addition with constants: 5
# Multiplication with constants: 6

変数

a = tf.placeholder(tf.int16)
b = tf.placeholder(tf.int16)

# 演算を定義
add = tf.add(a, b)
mul = tf.multiply(a, b)

with tf.Session() as sess:
    # Run every operation with variable input
    print("Addition with variables: %i" % sess.run(add, feed_dict={a: 2, b: 3}))
    print("Multiplication with variables: %i" % sess.run(mul, feed_dict={a: 2, b: 3}))

# Addition with variables: 5
# Multiplication with variables: 6

行列演算

# 1x2 行列
matrix1 = tf.constant([[3., 3.]])

# 2x1行列
matrix2 = tf.constant([[2.],[2.]])

# 行列積
product = tf.matmul(matrix1, matrix2)

with tf.Session() as sess:
    result = sess.run(product)
    print(result)

# [[ 12.]]