Edited at

最新の画像生成技術に衝撃を受けたので、その基礎技術をTensorFlowで実装してみる

More than 3 years have passed since last update.


ここがすごい、DCGAN


概要

写真と見間違えるこの画像、

dcgan_bedroom.png

引用元:dcgan_code

実は機械学習により生成したものらしいです。

DCGAN(Deep Convolutional Generative Adversarial Network)というプロジェクトにて公開されている技術です。


概念の「演算」もできる

どういうことかと言うと・・・

dcgan_face.png

引用元:dcgan_code

このように概念を組み合わせて新たな画像を生成することもできるらしいです。

この記事では、なぜこのようなことが実現できるのか理解するために、TensorFlowでDCGANに使われている基礎的な技術を実装してみました。


DCGANの技術を分解

DCGANの頭文字を分解すると・・・


  • Deep : 今流行りのDeep Learning

  • Convolutional : 画像認識の分野で大きな成果を上げている畳み込みネットワーク

  • Generative Adversarial Nets : 参考文献[1]で提案された、データ生成のためのネットワーク

DeepやConvolutionalに関する資料は沢山あるので、この記事ではGenerative Adversarial Netsに着目しました。


Generative Adversarial Network

機械学習でデータ生成をする上で以下の2点が重要になります。


  • 学習データに似ているデータを生成すること

  • 学習データそのものを生成しないこと

Generative Adversarial Networkでは、ノイズ$z$からデータ空間へのデータ生成写像$G(z)$を考え、そのニューラルネットワークを訓練します(GはGenerativeと頭文字)。

同時に、入力データ$x$が学習データである確率を与える写像$D(x)$もニューラルネットワークとして訓練します(DはDiscriminativeの頭文字)。

二つのニューラルネットワーク$D, G$は以下のminimaxを最適化するように訓練します。

\min_G \max_D E_{x \sim P_{\mathrm{data}}(x)} \left[ \ln D(x) \right] + E_{z \sim P_z(z)} \left[ \ln ( 1 - D(G(z)) ) \right]

この式によって学習データに似ているが、学習データそのものは生成しないという条件を表現していることになります。

参考文献[2]によると、概念の演算は、ノイズベクトル$z$の演算として獲得できるとのこと。二つのネットワークを訓練していくと、特定のノイズが特定の概念に対応するようになり、しかも意味のある演算が成立するとはなかなか興味深いです。


TensorFlowでの実装


問題設定

直感的に理解しやすい入力空間にするために、一次元の入力を考えます。

学習データは、$\mathrm U (0.25, 0.75)$の一様分布から生成されると考えます。

Generative Adversarial Networkにより、入力データに「それなりに近いが、近すぎない」データが生成されることを期待します。


Discriminative Network

Discriminative Networkは確率を出力とするので、出力層の活性化関数はsigmoidです。

Discriminative NetworkのInferenceは、


  • Discriminative Networkの学習で使うときは訓練する

  • Generative Networkの学習で使うときは訓練しない

という区別をする必要があるため、2種類用意しておきます。

LossではGenerative Networkの出力(output_from_noise)を使います。

Generative Networkの学習パラメータは使い回す必要があるので、TensorFlowSharing Variablesの機能を使います。


discriminative.py

import tensorflow as tf

import numpy

INPUT_SIZE = 1
HIDDEN_UNIT_SIZE = 64
TRAIN_DATA_SIZE = 100

input = 0.25 + numpy.random.random(TRAIN_DATA_SIZE).reshape([TRAIN_DATA_SIZE, INPUT_SIZE]) * 0.5
# ~U(0.25, 0.75)

def inference(input, hidden1_weight, hidden1_bias, output_weight, output_bias):
hidden1_output = tf.nn.relu(tf.matmul(input, hidden1_weight) + hidden1_bias)
output = tf.sigmoid(tf.matmul(hidden1_output, output_weight) + output_bias)
return output

def trainable_inference(input):
hidden1_weight = tf.get_variable(
"d_hidden1_weight",
[INPUT_SIZE, HIDDEN_UNIT_SIZE],
initializer = tf.random_normal_initializer(0, 0.1)
)
hidden1_bias = tf.get_variable(
"d_hidden1_bias",
[HIDDEN_UNIT_SIZE],
initializer = tf.constant_initializer(0.1)
)
output_weight = tf.get_variable(
"d_output_weight",
[HIDDEN_UNIT_SIZE, 1],
initializer = tf.random_normal_initializer(0, 0.1)
)
output_bias = tf.get_variable(
"d_output_bias",
[1],
initializer = tf.constant_initializer(0.1)
)
return inference(input, hidden1_weight, hidden1_bias, output_weight, output_bias)

def get_train_params():
hidden1_weight = tf.get_variable("d_hidden1_weight", [INPUT_SIZE, HIDDEN_UNIT_SIZE])
hidden1_bias = tf.get_variable("d_hidden1_bias", [HIDDEN_UNIT_SIZE])
output_weight = tf.get_variable("d_output_weight", [HIDDEN_UNIT_SIZE, 1])
output_bias = tf.get_variable("d_output_bias", [1])
return [hidden1_weight, hidden1_bias, output_weight, output_bias]

def loss(output_from_given_data, output_from_noise):
with tf.name_scope('d_loss') as scope:
loss_1 = tf.reduce_sum(tf.log(output_from_given_data))
loss_2 = tf.reduce_sum(tf.log(1 - output_from_noise))
return [loss_1, loss_2]



Generative Network

Generative NetworkのLossはGenerative NetworkのInferenceを使う必要があるため、ここでは定義しません。

入力データの分布は適当ですが、初期値を入力したときにネットワークがデータ空間を十分にカバーする点に注意します。


generative.py

import tensorflow as tf

import numpy

INPUT_SIZE = 1
HIDDEN_UNIT_SIZE = 64
TRAIN_DATA_SIZE = 100
OUTPUT_SIZE = 1

input = -3 + numpy.random.random(TRAIN_DATA_SIZE * INPUT_SIZE).reshape([TRAIN_DATA_SIZE, INPUT_SIZE]) * 6

def inference(input_placeholder):
with tf.name_scope('g_hidden1') as scope:
hidden1_weight = tf.Variable(tf.truncated_normal([INPUT_SIZE, HIDDEN_UNIT_SIZE], stddev=1.0), name="
hidden1_bias = tf.Variable(tf.constant(0.01, shape=[HIDDEN_UNIT_SIZE]), name="g_hidden1_bias")
hidden1_output = tf.nn.relu(tf.matmul(input_placeholder, hidden1_weight) + hidden1_bias)
with tf.name_scope('g_output') as scope:
output_weight = tf.Variable(tf.truncated_normal([HIDDEN_UNIT_SIZE, OUTPUT_SIZE], stddev=1.5), name="
output_bias = tf.Variable(tf.constant(0.01, shape=[OUTPUT_SIZE]), name="g_output_bias")
output = tf.nn.sigmoid(tf.matmul(hidden1_output, output_weight) + output_bias)
return output



二つのネットワークを訓練する

先述した通り、一方のネットワークを訓練するときは他方のネットワークの学習パラメータを定数として(placeholderとして)扱う点に注意して実装します。


main.py

import tensorflow as tf

import numpy
import discriminative
import generative

def training(loss, learning_rate):
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)
return train_step

with tf.Graph().as_default():
# placeholders
d_given_data_placeholder = tf.placeholder("float", [None, discriminative.INPUT_SIZE], name="g_given_data_placeholder")
g_output_placeholder = tf.placeholder("float", [None, discriminative.INPUT_SIZE], name="g_output_placeholder")

d_input_placeholder = tf.placeholder("float", [None, discriminative.INPUT_SIZE], name="d_input_placeholder")
g_input_placeholder = tf.placeholder("float", [None, generative.INPUT_SIZE], name="g_input_placeholder")

d_hidden1_weight_placeholder = tf.placeholder("float", [discriminative.INPUT_SIZE, discriminative.HIDDEN_UNIT_SIZE])
d_hidden1_bias_placeholder = tf.placeholder("float", [discriminative.HIDDEN_UNIT_SIZE])
d_output_weight_placeholder = tf.placeholder("float", [discriminative.HIDDEN_UNIT_SIZE, 1])
d_output_bias_placeholder = tf.placeholder("float", [1])

# inference output
g_output = generative.inference(g_input_placeholder)

with tf.variable_scope('d_params') as scope:
d_output_from_given_data = discriminative.trainable_inference(d_given_data_placeholder)
scope.reuse_variables()
d_output_from_noise_for_dtrain = discriminative.trainable_inference(g_output_placeholder)

d_output_from_noise_for_gtrain = discriminative.inference(
g_output,
d_hidden1_weight_placeholder,
d_hidden1_bias_placeholder,
d_output_weight_placeholder,
d_output_bias_placeholder
)

# loss
d_loss_1, d_loss_2 = discriminative.loss(d_output_from_given_data, d_output_from_noise_for_dtrain)
g_loss = tf.reduce_sum(tf.log(1 - d_output_from_noise_for_gtrain))

# training
d_train_op = training(-(d_loss_1 + d_loss_2), 0.01)
g_train_op = training(g_loss, 0.001)

# misc
with tf.variable_scope('d_params', reuse = True) as scope:
d_params = discriminative.get_train_params()
summary_op = tf.merge_all_summaries()
init = tf.initialize_all_variables()

with tf.Session() as sess:
summary_writer = tf.train.SummaryWriter('data', graph_def=sess.graph_def)
sess.run(init)

num_steps = 512
for step in range(num_steps):
g_output_eval = sess.run(g_output, feed_dict = {g_input_placeholder: generative.input})
sess.run(d_train_op, feed_dict = {
g_output_placeholder: g_output_eval,
d_given_data_placeholder: discriminative.input
})

d_train_params_eval = sess.run(d_params)
sess.run(g_train_op, feed_dict = {
g_input_placeholder: generative.input,
d_hidden1_weight_placeholder: d_train_params_eval[0],
d_hidden1_bias_placeholder: d_train_params_eval[1],
d_output_weight_placeholder: d_train_params_eval[2],
d_output_bias_placeholder: d_train_params_eval[3]
})

if step % (num_steps / 8) == 0:
print 'loss:'
print sess.run([d_loss_1, d_loss_2], feed_dict = {
g_output_placeholder: g_output_eval,
g_input_placeholder: generative.input,
d_given_data_placeholder: discriminative.input
})

test_input = numpy.linspace(0.0, 1.0, 51).reshape([51, 1])
numpy.savetxt(
'distribution.csv',
sess.run(d_output_from_given_data, feed_dict={d_given_data_placeholder: test_input})
)
numpy.savetxt(
'generated.csv',
sess.run(g_output, feed_dict={g_input_placeholder: generative.input})
)



実行結果

初期値によって結果が不安定になることがありましたが、直感的に理解しやすい出力を示した結果の一例を載せます。


Discriminative Network

Discriminative Networkの入力データ$x$に対する応答を見てみます。

distribution.png

予想通り、入力データの分布$\mathrm U (0.25, 0.75)$を再現する分布が得られました。このネットワークによって生成されたデータが入力データらしいかどうかを判定することができます。


Generative Network

次に、Generative Networkが入力データに対してどのように応答しているかを見ます。

generated.png

1付近はSigmoidの上限値なので無視するとして、下限は0.3付近、つまり学習データの一様分布の下限付近となっています。

Generative Networkは学習データにほどほどに近いが、近すぎないデータを生成する傾向を示していると考えられます。


所感

1次元のこれだけ簡単な問題でも数値的に安定させるためのチューニングが結構大変だったので、素人がノウハウなしにいきなりこれをDeep Networkでやるのは辛そうだなと思いました。

理論だけわかっていても、実際に実装してみると「じゃあここはどうすればいいのだろうか?」という点が沢山出てきました。

最後に、違ったアプローチのConvolutional Networkを使った画像生成技術として面白かった論文を参考文献[3]として紹介しておきます。


参考文献


  1. Generative Adversarial Nets: Goodfellow, Ian et al, Advances in Neural Information Processing Systems 27, 2014

  2. Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks: Alec Radford

  3. A Neural Algorithm of Artistic Style: Leon A. Gatys et al