DeepLearning
深層学習
CNTK

「Generative Adversarial Nets」論文要約と、CNTKによるGANの実装紹介

始めに

GAN(Generative Adversarial Nets)とは、2014年にIan Goodfellowによって考案された、
効率的に生成モデルを訓練させるためのモデリング手法です。
現在に至るまで多くの派生モデルが登場しており、Deep Learning分野の中でも大きな注目を集めています。
また、生成モデル自体が応用の利きやすい手法であることから、他分野でもGANのアイデアが使用されており1、今後も動向を見逃せない分野だと思っています。
今更な感が非常に強いですが先日にGenerative Adversarial Netsを読んだので、内容の簡単なまとめと、
CNTKによる実装紹介を行いたいと思います。

Generative Adversarial Nets

GANの基礎理論

GANの目的は、観測データ$\boldsymbol{x} \sim p_{data}(\boldsymbol{x})$から、$p_{data}(\boldsymbol{x})=p_{g}(\boldsymbol{z})$となるような確率分布$p_{g}(\boldsymbol{z})$を得ることです。
$p_{g}(\boldsymbol{z})$を推定すると何が嬉しいかというと、推定前に設定した事前分布$p_{\boldsymbol{z}}(\boldsymbol{z})$から潜在変数$\boldsymbol{z} \sim p_{\boldsymbol{z}}(\boldsymbol{z})$をサンプリングして$p_{g}(\boldsymbol{z})$に入力することで、観測データに似た性質を持つデータを生成できるようになります。

このような観測データを生成する確率分布を推定するモデルは、生成モデルと呼ばれています。
観測データの構造を明らかにする生成モデルは非常に有用ですが、学習困難性が問題になりやすい手法でした。
なぜなら、観測データの分布$p_{data}(\boldsymbol{x})$を表現するためには、生成モデルの確率分布$p_{g}(\boldsymbol{z})$も$p_{data}(\boldsymbol{x})$と同等の表現力を有している必要があり、分布の表現力を上げていくほど最適解を見つけ出すのが困難になるからです。

GANでは生成モデルと一緒に識別モデルを訓練させることで、生成モデルの学習困難性を回避させます。
1. $G(\boldsymbol{z};\theta_{g})$ もしくは $p_{g}(\boldsymbol{z})$ ... 生成モデル(Generator)
2. $D(\boldsymbol{x};\theta_{d})$ ... 識別モデル(Discriminator)

$G(\boldsymbol{z};\theta_{g})$は観測データ$\boldsymbol{x}$と同一の空間上で表現される生成的分布を表しており、
この分布から生成されたデータ$\boldsymbol{x} \sim G(\boldsymbol{z};\theta_{g})$が観測データ$\boldsymbol{x}$と似ていることが望まれます。

対して$D(\boldsymbol{x};\theta_{d})$は、入力データが観測データである確率を出力する識別器です。
よって、このモデルは、観測データ$\boldsymbol{x}$に対して大きな値を取り、
生成データ$\boldsymbol{x} \sim G(\boldsymbol{z};\theta_{g})$に対して小さな値を取ることが望まれます。

上記のように2つのモデルの理想像を考えると、以下のような訓練を行う必要があります。

  • $G(\boldsymbol{z};\theta_{g})$は、$D(G(\boldsymbol{z};\theta_{g});\theta_{d})$が大きな値を取るように訓練したい。
  • $D(\boldsymbol{x};\theta_{d})$は、観測データ$\boldsymbol{x}$に対して大きな値を取り、 $G(\boldsymbol{z};\theta_{g})$に対して小さな値を取るよう訓練したい。

これらは以下の価値関数のminmax問題で表現することができ、これからGANは
「生成モデルと識別モデルを敵対的に訓練させることで、互いに競い合って精度を向上させていく」手法2
であると読み取ることができます。

\mathop{min}_{G}\mathop{max}_{D}V(D,G)=\mathbb{E}_{\boldsymbol{x}\sim p_{data}(\boldsymbol{x})}
\bigl[ \log D(\boldsymbol{x}) \bigl] + \mathbb{E}_{\boldsymbol{z}\sim p_{z}(\boldsymbol{z})}
\bigl[ \log (1-D(G(\boldsymbol{z}))) \bigl]

訓練アルゴリズム

以下がGANの訓練アルゴリズムになります。(図はgoodfellow et al., 2014から引用)
無題.png
全体を概観すると、以下のフローをイタレーションしていることが分かります。
1. 識別モデルの訓練フェイズ($k$回イタレーションする)
2. 生成モデルの訓練フェイズ

識別モデルの訓練フェイズ

以下のフローを$k$ステップ分繰り返して、識別モデルを訓練します。
1. 生成モデルの事前分布3$p_{z}(\boldsymbol{z})$を使ってサンプリングを$m$回実行し、データ$\bigl\{ \boldsymbol{z}^{(1)},...,\boldsymbol{z}^{(m)} \bigl\}$を得る。
2. 訓練途中の生成モデルとデータ$\bigl\{ \boldsymbol{z}^{(1)},...,\boldsymbol{z}^{(m)} \bigl\}$から、生成データ$\bigl\{ G(\boldsymbol{z}^{(1)}),...,G(\boldsymbol{z}^{(m)}) \bigl\}$を得る。
3. 訓練データセットから、$m$個の観測データ$\bigl\{ \boldsymbol{x}^{(1)},...,\boldsymbol{x}^{(m)} \bigl\}$を抽出する。
4. 以下の勾配を計算し、パラメータ$\theta_{d}$を更新する。

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

生成モデルの訓練フェイズ

  1. 生成モデルの事前分布$p_{z}(\boldsymbol{z})$を使ってサンプリングを$m$回実行し、データ$\bigl\{ \boldsymbol{z}^{(1)},...,\boldsymbol{z}^{(m)} \bigl\}$を得る。
  2. 訓練途中の生成モデルとデータ$\bigl\{ \boldsymbol{z}^{(1)},...,\boldsymbol{z}^{(m)} \bigl\}$から、生成データ$\bigl\{ G(\boldsymbol{z}^{(1)}),...,G(\boldsymbol{z}^{(m)}) \bigl\}$を得る。
  3. 以下の勾配を計算し、パラメータ$\theta_{g}$を更新する。
\nabla_{\theta_{g}} \frac{1}{m}\sum^m_{i=1}\bigl[ \log (1-D(G(\boldsymbol{z}^{(i)}))) \bigl]

ただし、$\nabla_{\theta_{g}} \frac{1}{m}\sum^m_{i=1}\bigl[ \log (1-D(G(\boldsymbol{z}^{(i)}))) \bigl]$には、訓練開始時点において学習が進まなくなるという問題点があります。これは、訓練当初で明らかに偽者だと分かるデータを生成してしまうことで、$D(G(\boldsymbol{z}^{(i)}))$が微小値となり、勾配が小さくなるからです。
そのため、代わりに$\nabla_{\theta_{g}} \frac{1}{m}\sum^m_{i=1}\bigl[ \log (D(G(\boldsymbol{z}^{(i)}))) \bigl]$を使ってパラメータ更新を行うよう、推奨されています。

価値関数の最適解

GANの目的は$p_{data}(\boldsymbol{x})=p_{g}(\boldsymbol{z})$となるような生成モデルを作成することでした。
しかし、あくまで訓練は上記で紹介した価値関数の最大化・最小化で実行されているため、実際に$p_{data}(\boldsymbol{x})=p_{g}(\boldsymbol{z})$が価値関数の最適解となっているのか確認する必要があります。

まずは、任意の生成モデルに対する、最適な識別モデルを計算してみます。
生成モデルを固定した状態で、識別モデルに関する価値関数を変形させてみます。

\begin{align}
V(D,G) &= \mathbb{E}_{\boldsymbol{x}\sim p_{data}(\boldsymbol{x})}
\bigl[ \log D(\boldsymbol{x}) \bigl] + \mathbb{E}_{\boldsymbol{z}\sim p_{z}(\boldsymbol{z})}
\bigl[ \log (1-D(G(\boldsymbol{z}))) \bigl] \\
&= \int_{\boldsymbol{x}} p_{data}(\boldsymbol{x}) \log D(\boldsymbol{x}) d\boldsymbol{x} + 
\int_{\boldsymbol{z}} p_{z}(\boldsymbol{z}) \log (1-D(G(\boldsymbol{z}))) d\boldsymbol{z} \\
&= \int_{\boldsymbol{x}} \bigl[ p_{data}(\boldsymbol{x}) \log D(\boldsymbol{x})+ 
p_{g}(\boldsymbol{x}) \log (1-D(\boldsymbol{x})) \bigl] d\boldsymbol{x} 
\end{align}

この価値関数を最大化するような理想的な識別モデル$D^{\ast}$は、$D$について解くことで得られます。

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

次に、識別モデルを$D^{\ast}$に固定した状態で、生成モデルに関する価値関数に変形させます。

\begin{align}
V(D^{\ast},G) &= \int_{\boldsymbol{x} \sim p_{data}} p_{data}(\boldsymbol{x}) \log D^{\ast}(\boldsymbol{x}) d\boldsymbol{x} + 
\int_{\boldsymbol{x} \sim p_{g}} p_{g}(\boldsymbol{x}) \log (1-D^{\ast}(\boldsymbol{x})) d\boldsymbol{x} \\
&= \Bigl( \int_{\boldsymbol{x} \sim p_{data}} p_{data}(\boldsymbol{x}) \log \frac{p_{data}(\boldsymbol{x})}{p_{data}(\boldsymbol{x})+p_{g}(\boldsymbol{x})} d\boldsymbol{x} +\log2 \Bigl) + 
\Bigl( \int_{\boldsymbol{x} \sim p_{g}} p_{g}(\boldsymbol{x}) \log \frac{p_{g}(\boldsymbol{x})}{p_{data}(\boldsymbol{x})+p_{g}(\boldsymbol{x})} d\boldsymbol{x}  +\log2 \Bigl) - \log4\\
&= KL \bigl( p_{data}||\frac{p_{data}+p_{g}}{2} \bigl) + KL \bigl( p_{g}||\frac{p_{data}+p_{g}}{2} \bigl) - \log4\\
&= 2 \cdot JSD(p_{data}||p_{g}) - \log4
\end{align}

$KL$はKLダイバージェンス、$JSD$はJensen-Shannonダイバージェンスを表しています。

最適な生成モデルは、上記の価値関数の最小化問題を解くことで得ることができます。
Jensen-Shannonダイバージェンスは非負の値を取り、$p_{data}(\boldsymbol{x})=p_{g}(\boldsymbol{z})$の時にのみ最小値$JSD(p_{data}||p_{g})=0$となることから、GANの目的である$p_{data} = p_{g}$が価値関数の大域的最適解であり、その時の価値関数が$- \log4$の最小値を取ることが確認できます。

GANの実装

それでは、MNISTデータを使用してGANを実装してみます。
今回の計算環境は以下になります。

  • Python 3.5
  • CNTK v2.3

モデルの設定

生成モデルと識別モデルの定義

生成モデルと識別モデルを、多層パーセプトロンの構成で定義します。

def Generator(z):
    with C.layers.default_options(init=C.xavier()):
        h1 = C.layers.Dense(G_HIDDEN_DIM, activation=C.relu)(z)
        return C.layers.Dense(G_OUTPUT_DIM, activation=C.tanh)(h1)

def Discriminator(x):
    with C.layers.default_options(init=C.xavier()):
        h1 = C.layers.Dense(D_HIDDEN_DIM, activation=C.relu)(x)
        return C.layers.Dense(D_OUTPUT_DIM, activation=C.sigmoid)(h1)
  • 生成モデル関数Generatorは、生成したランダムなノイズzを入力とし、MNISTに似せた画像を出力しようとする関数です。今回の実装では、出力値を$[-1,1]$と有界にするために、出力層の活性化関数にtanh関数を採用しています。(これに合わせて、MNISTデータの値も$[-1,1]$になるよう加工する必要があります)
  • 識別モデル関数Discriminatorは、入力データxが真のデータである確率を出力する関数です。そのため、出力値を$[0,1]$にするために、出力層の活性化関数にSigmoid関数を採用しています。4

計算グラフの構築

2つのネットワークを定義したので、次に計算グラフを構築します。
まずは、ネットワークの入出力データに対して、プレースホルダを作成します。

def build_graph():
    # 生成データのプレースホルダを作成
    input_dynamic_axes = [C.Axis.default_batch_axis()]
    z = C.input_variable(G_INPUT_DIM, dynamic_axes=input_dynamic_axes)
    x_fake = Generator(z)

    # 訓練データのプレースホルダを作成
    x_real = C.input_variable(D_INPUT_DIM, dynamic_axes=input_dynamic_axes)
    x_real_scaled = 2*(x_real/255.0) - 1.0

    # Discriminatorの出力プレースホルダの作成
    d_real = Discriminator(x_real_scaled)
    d_fake = d_real.clone(method="share",
                          substitutions={x_real_scaled.output:x_fake.output},
                          )
  • 変数zはランダムノイズのプレースホルダになります。
  • 変数zを生成モデルに入力し、生成データとなるネットワーク出力x_fakeを作成します。
  • 生成モデルの出力値と同様に、訓練データx_realも出力が$[-1,1]$になるよう加工しています。
  • 識別対象となる2種類のデータのプレースホルダを作成したので、別個に識別結果のプレースホルダを作成します。生成モデルの出力x_fakeに関する識別結果については、生成モデルの出力値x_fake.outputを作成済みのd_realプレースホルダに代入させることで対応します5

入出力のプレースホルダを作成した後は、損失関数と最適化手法の設定を行います。

def build_graph():
    ...

    # 損失関数の設定
    g_loss = -1.0 * C.log(d_fake)
    d_loss = -1.0 * (C.log(d_real) + C.log(1.0-d_fake))

    # 最適化手法の設定
    g_learner = C.learners.fsadagrad(
        parameters = x_fake.parameters,
        lr = C.learners.learning_parameter_schedule_per_sample(LEARNING_RATE),
        momentum = C.learners.momentum_schedule_per_sample(MOMENTUM),
        )
    d_learner = C.learners.fsadagrad(
        parameters = d_real.parameters,
        lr = C.learners.learning_parameter_schedule_per_sample(LEARNING_RATE),
        momentum = C.learners.momentum_schedule_per_sample(MOMENTUM),
        )

    # Trainerオブジェクトの作成
    g_trainer = C.Trainer(x_fake, g_loss, g_learner)
    d_trainer = C.Trainer(d_real, d_loss, d_learner)

    return x_real, x_fake, z, g_trainer, d_trainer
  • 損失関数には最小化すべき関数を設定する必要があるため、
    生成モデルの損失関数g_lossでは$L_{\theta_{g}}=- \frac{1}{m}\sum^m_{i=1}\bigl[ \log (D(G(\boldsymbol{z}^{(i)}))) \bigl]$、
    識別モデルの損失関数d_lossでは$L_{\theta_{d}}=-\frac{1}{m}\sum^m_{i=1}\bigl[ \log D(\boldsymbol{x}^{(i)}) + \log (1-D(G(\boldsymbol{z}^{(i)}))) \bigl]$と設定します。
  • モデル毎にlearnerオブジェクトを設定します。
    生成モデルのlearnerはg_learner、識別モデルのlearnerはd_learnerに当たります。

訓練の実行

モデル設定を全て完了したので、ミニバッチ毎にパラメータを更新させていきます。

# 訓練を実行する
for n_epoch in range(1, NUM_EPOCH+1):
    # 訓練データをシャッフルする
    sample_data = sample_data[np.random.permutation(len(sample_data))]

    for i in range(0, len(sample_data), BATCHSIZE):
        # 訓練データを抽出
        batch_data = sample_data[i:i+BATCHSIZE]

        # Discriminatorモデルのパラメータ更新
        z_data = noise_sample(BATCHSIZE)    
        d_input_map = {x_real:batch_data, z:z_data}
        d_trainer.train_minibatch(d_input_map)

        # Generatorモデルのパラメータ更新
        g_input_map = {z:z_data}
        g_trainer.train_minibatch(g_input_map)
  • noise_sample(n)は、(n, Generatorの入力サイズ)のshapeで一様乱数を出力する関数です。
  • 論文中では、識別モデルの訓練を$k$ステップ分繰り返すと記述されていますが、本実装では$k=1$としています。

訓練結果

以下は、訓練中に出力した生成画像になります。
訓練始めはノイズ画像にしか見えませんが、訓練を繰り返していくことで、
MNISTのような手書き画像を生成することができています。


(左から、1エポック後、50エポック後、90エポック後の生成画像)

以上

実装コード

import numpy as np
import cntk as C
from sklearn import datasets
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

### パラメータ
# Generator
G_INPUT_DIM = 100
G_HIDDEN_DIM = 256
G_OUTPUT_DIM =784

# Discriminator
D_INPUT_DIM = 784
D_HIDDEN_DIM = 256
D_OUTPUT_DIM = 1

# training config
NUM_EPOCH = 200
BATCHSIZE = 100
LEARNING_RATE = 0.0005
MOMENTUM = 0.996

np.random.seed(71)
def noise_sample(num_samples):
    return np.random.uniform(
        low = -1.0,
        high = 1.0,
        size = [num_samples, G_INPUT_DIM]
        ).astype(np.float32)

def Generator(z):
    with C.layers.default_options(init=C.xavier()):
        h1 = C.layers.Dense(G_HIDDEN_DIM, activation=C.relu)(z)
        return C.layers.Dense(G_OUTPUT_DIM, activation=C.tanh)(h1)

def Discriminator(x):
    with C.layers.default_options(init=C.xavier()):
        h1 = C.layers.Dense(D_HIDDEN_DIM, activation=C.relu)(x)
        return C.layers.Dense(D_OUTPUT_DIM, activation=C.sigmoid)(h1)

def build_graph():
    # 生成データのプレースホルダを作成
    input_dynamic_axes = [C.Axis.default_batch_axis()]
    z = C.input_variable(G_INPUT_DIM, dynamic_axes=input_dynamic_axes)
    x_fake = Generator(z)

    # 訓練データのプレースホルダを作成
    x_real = C.input_variable(D_INPUT_DIM, dynamic_axes=input_dynamic_axes)
    x_real_scaled = 2*(x_real/255.0) - 1.0

    # Discriminatorの出力プレースホルダの作成
    d_real = Discriminator(x_real_scaled)
    d_fake = d_real.clone(method="share",
                          substitutions={x_real_scaled.output:x_fake.output},
                          )

    # 損失関数の設定
    g_loss = -1.0 * C.log(d_fake)
    d_loss = -1.0 * (C.log(d_real) + C.log(1.0-d_fake))

    # 最適化手法の設定
    g_learner = C.learners.fsadagrad(
        parameters = x_fake.parameters,
        lr = C.learners.learning_parameter_schedule_per_sample(LEARNING_RATE),
        momentum = C.learners.momentum_schedule_per_sample(MOMENTUM),
        )
    d_learner = C.learners.fsadagrad(
        parameters = d_real.parameters,
        lr = C.learners.learning_parameter_schedule_per_sample(LEARNING_RATE),
        momentum = C.learners.momentum_schedule_per_sample(MOMENTUM),
        )

    # Trainerオブジェクトの作成
    g_trainer = C.Trainer(x_fake, g_loss, g_learner)
    d_trainer = C.Trainer(d_real, d_loss, d_learner)

    return x_real, x_fake, z, g_trainer, d_trainer

def generate_fake_images(n_epoch, x_fake):
    def plot_images(images, subplot_shape):
        plt.style.use('ggplot')
        fig, axes = plt.subplots(*subplot_shape)
        for image, ax in zip(images, axes.flatten()):
            ax.imshow(image.reshape(28, 28), vmin = 0, vmax = 1.0, cmap = 'gray')
            ax.axis('off')
            plt.savefig("./log/fake_images_%sepoch.png" % n_epoch)

    num_image = 9
    fake_image = x_fake.eval({z:noise_sample(num_image)})
    plot_images(fake_image, subplot_shape=[3,3])    

if __name__ == '__main__':
    # MNISTデータの読み込み
    mnist = datasets.fetch_mldata('MNIST original', data_home=".")
    sample_data = mnist.data

    # MNISTデータの加工
    sample_data = sample_data.reshape(len(sample_data), -1)
    sample_data = sample_data.astype(np.float32)

    # モデルの設定
    x_real, x_fake, z, g_trainer, d_trainer = build_graph()

    # 訓練を実行する
    for n_epoch in range(1, NUM_EPOCH+1):
        # 訓練データをシャッフルする
        sample_data = sample_data[np.random.permutation(len(sample_data))]

        for i in range(0, len(sample_data), BATCHSIZE):
            # 訓練データを抽出
            batch_data = sample_data[i:i+BATCHSIZE]

            # Discriminatorモデルのパラメータ更新
            z_data = noise_sample(BATCHSIZE)    
            d_input_map = {x_real:batch_data, z:z_data}
            d_trainer.train_minibatch(d_input_map)

            # Generatorモデルのパラメータ更新
            g_input_map = {z:z_data}
            g_trainer.train_minibatch(g_input_map)

        print("epoch: %s,   Generator_Loss: %.4f,   Discriminator_Loss: %.4f,"
            % (n_epoch, g_trainer.previous_minibatch_loss_average, d_trainer.previous_minibatch_loss_average))

        # 10エポック毎にフェイクイメージを出力
        if n_epoch % 10 == 0:
            generate_fake_images(n_epoch, x_fake)

脚注


  1. 例えば、強化学習の内部報酬生成アルゴリズム 

  2. 論文中では、このような生成モデルと識別モデルの関係を偽造通貨の製造者と警察に例えています。偽造通貨の製造者(生成モデル)は警察が見破れないような偽造通貨を作成しようと訓練し、警察(識別モデル)は偽造通貨を見破ろうと訓練します。このような敵対的関係を使って訓練させていくことで、最終的には偽造通貨の製造者は本物と区別がつかないレベルの偽造通貨を作成できるようになります。 

  3. 論文中では、一様事前分布を採用しています。 

  4. 出力層の活性化関数を指定しない場合、損失関数をSoftplus関数を使った形に変形することができます。今回の実装では、活性化関数にSigmoid関数を採用していますが、本来は、計算都合上Softplusを計算する形に直した方が良いです。参考ページ 

  5. これを行うと、スクリプトを回した時に以下のwarningが出ますが、cntkの仕様のようです。参考ページ 

      [Note:] Trainer ctor: 4 of the model parameters are not covered by any of the specified Learners; these parameters will not be learned
      [Note:] Trainer ctor: 4 of the model parameters are not covered by any of the specified Learners; these parameters will not be learned