TensorFlowによるDCGANでアイドルの顔画像生成

アイドル顔識別のためのデータ収集 をコツコツ続けて それなりに集まってきたし、これを使って別のことも…ということでDCGANを使ったDeep Learningによるアイドルの顔画像の「生成」をやってみた。


まだだいぶ歪んでいたりで あまりキレイじゃないけど…。顔画像を多く収集できているアイドル90人の顔画像からそれぞれ120件を抽出した合計10800件をもとに学習させて生成させたもの。


分類タスクとは逆方向の変換、複数のモデル定義などがあってなかなか理解が難しい部分もあったけど、作ってみるとそこまで難しくはなく、出来上がっていく過程を見るのが楽しいし とても面白い。

DCGANとは

"Deep Convolutional Generative Adversarial Networks"、略してDCGAN。こちらの論文で有名になった、のかな?

あとは応用事例として日本語の記事では以下のものがとても詳しいのでそれを読めば十分かな、と。

一応あらためて書いておくと。

顔識別のような分類タスクは

  • 入力は画像: 縦×横×チャネル数(RGBカラーなら3)で各ピクセルの値
  • 出力は<分類クラス数>次元ベクトル: 最も高い値を出力しているクラスが推定結果となる

といった分類器を作って学習させるだけだが、DCGANではそういった分類器を"Discriminator"として使い、それと別に"Generator"というモデルを構築し使用する。Generatorの役割は

  • 入力は乱数: -1.0〜1.0 の値をとるn次元のベクトル
  • 出力は画像

というものであり、この出力が最終的な「機械学習による画像生成」の成果物となる。
原理としては、

  • Discriminatorに「Generatorによって乱数ベクトルから生成された画像」と「学習用データ画像(生成させたい画像のお手本となるもの)」の両方を食わせ、それぞれの画像が「Generatorによって生成されたものであるか否か」の判定をさせる
  • Discriminatorは正しく判定できるよう学習させ、GeneratorはDiscriminatorを欺いて誤判定させる画像を生成するよう学習する

これを繰り返してお互いに精度を上げることで、ランダム入力から学習データそっくりの画像を生成できるようになる、というもの。


(http://qiita.com/mattya/items/e5bfe5e04b9d2f0bbd47 より引用)

言葉にしてみるとまぁなるほど、とは思うけどそんな上手く双方を学習できるのか、という感じではある。そのへんをBatch Normalizationを入れたりLeaky ReLUを使ったりして上手くいくようになったよ、というのが上記の論文のお話のようだ。

TensorFlowでの実装

先行のDCGAN実装例は既に結構ある。

TensorFlowによる実装も既にあったので、それを参考にしつつも自分で書いてみた。

Generator

乱数ベクトルから画像を生成するモデルは下図のようになる。

(arXiv:1511.06434より引用)

分類器などで使っている畳み込みの逆方向の操作で、最初は小さな多数のfeature mapにreshapeして、これを徐々に小数の大きなものにしていく。"deconvolution"と呼んだり呼ばなかったり、なのかな。TensorFlowではこの操作はtf.nn.conv2d_transposeという関数で実現するようだ。

各層間の変換でW(weights)を掛けてB(biases)を加え、このWとBの学習により最終的な出力画像を変化させていくことになる。あと論文にある通りReLUにかける前にBatch Normalizationという処理をする。これはTensorFlow 0.8.0からtf.nn.batch_normalizationが登場しているのかな?ここにtf.nn.momentsで得るmeanとvarianceを渡してあげれば良さそう。

ということでこんな感じのコードで作った。

def model():
    depths = [1024, 512, 256, 128, 3]
    i_depth = depths[0:4]
    o_depth = depths[1:5]
    with tf.variable_scope('g'):
        inputs = tf.random_uniform([self.batch_size, self.z_dim], minval=-1.0, maxval=1.0)
        # reshape from inputs
        with tf.variable_scope('reshape'):
            w0 = tf.get_variable('weights', [self.z_dim, i_depth[0] * self.f_size * self.f_size], tf.float32, tf.truncated_normal_initializer(stddev=0.02))
            b0 = tf.get_variable('biases', [i_depth[0]], tf.float32, tf.zeros_initializer)
            dc0 = tf.nn.bias_add(tf.reshape(tf.matmul(inputs, w0), [-1, self.f_size, self.f_size, i_depth[0]]), b0)
            mean0, variance0 = tf.nn.moments(dc0, [0, 1, 2])
            bn0 = tf.nn.batch_normalization(dc0, mean0, variance0, None, None, 1e-5)
            out = tf.nn.relu(bn0)
        # deconvolution layers
        for i in range(4):
            with tf.variable_scope('conv%d' % (i + 1)):
                w = tf.get_variable('weights', [5, 5, o_depth[i], i_depth[i]], tf.float32, tf.truncated_normal_initializer(stddev=0.02))
                b = tf.get_variable('biases', [o_depth[i]], tf.float32, tf.zeros_initializer)
                dc = tf.nn.conv2d_transpose(out, w, [self.batch_size, self.f_size * 2 ** (i + 1), self.f_size * 2 ** (i + 1), o_depth[i]], [1, 2, 2, 1])
                out = tf.nn.bias_add(dc, b)
                if i < 3:
                    mean, variance = tf.nn.moments(out, [0, 1, 2])
                    out = tf.nn.relu(tf.nn.batch_normalization(out, mean, variance, None, None, 1e-5))
    return tf.nn.tanh(out)

入力は乱数なのでtf.random_uniformを使えば毎回ランダムな入力から作ってくれる。逆畳み込みはchannel数が変わるだけなのでfor loopで繰り返すだけで定義できる。最後の出力にはBatch Normalizationをかけずにtf.nn.tanhで -1.0〜1.0 の範囲の出力にする。

Discriminator

こちらは以前までやっていた分類器とほぼ同じで、画像入力から畳み込みを繰り返して小さなfeature mapに落とし込んでいく。最後は全結合するけど、隠れ層は要らないらしい。出力は、既存のTensorFlow実装などでは1次元にしてsigmoidの出力を使うことで「0に近いか 1に近いか」を判定にしていたようだけど、自分はsigmoidを通さない2次元の出力にして、「0番目が大きな出力になるか 1番目が大きくなるか」で分類するようにした(誤差関数については後述)。
また各層の出力にはLeaky ReLUを使うとのことで、これに該当する関数はTensorFlowには無いようだったけど、tf.maximum(alpha * x, x)がそれに該当するということで それを使った。

また、Discriminatorは「学習用データ」と「Generatorによって生成されたもの」の2つの入力を通すことになるのでフローが2回繰り返されることになる。けどこれは同じモデルに対して入出力を行う、つまり同じ変数を使い回す必要がある。こういうときはtf.variable_scopeでreuse=Trueを指定すると2回目以降で同じ変数が重複定義されないようになるらしい。いちおう、初回の呼び出しか否かを使う側が意識する必要がないようPython3のnonlocalを使ってクロージャ的な感じで書いてみた。

ということでこんなコード。

def __discriminator(self, depth1=64, depth2=128, depth3=256, depth4=512):
    reuse = False
    def model(inputs):
        nonlocal reuse
        depths = [3, depth1, depth2, depth3, depth4]
        i_depth = depths[0:4]
        o_depth = depths[1:5]
        with tf.variable_scope('d', reuse=reuse):
            outputs = inputs
            # convolution layer
            for i in range(4):
                with tf.variable_scope('conv%d' % i):
                    w = tf.get_variable('weights', [5, 5, i_depth[i], o_depth[i]], tf.float32, tf.truncated_normal_initializer(stddev=0.02))
                    b = tf.get_variable('biases', [o_depth[i]], tf.float32, tf.zeros_initializer)
                    c = tf.nn.bias_add(tf.nn.conv2d(outputs, w, [1, 2, 2, 1], padding='SAME'), b)
                    mean, variance = tf.nn.moments(c, [0, 1, 2])
                    bn = tf.nn.batch_normalization(c, mean, variance, None, None, 1e-5)
                    outputs = tf.maximum(0.2 * bn, bn)
            # reshepe and fully connect to 2 classes
            with tf.variable_scope('classify'):
                dim = 1
                for d in outputs.get_shape()[1:].as_list():
                    dim *= d
                w = tf.get_variable('weights', [dim, 2], tf.float32, tf.truncated_normal_initializer(stddev=0.02))
                b = tf.get_variable('biases', [2], tf.float32, tf.zeros_initializer)
        reuse = True
        return tf.nn.bias_add(tf.matmul(tf.reshape(outputs, [-1, dim]), w), b)
    return model
Input Images

Discriminatorには[batch size, height, width, channel]の入力を与える前提で作っていて、学習用の画像データはその形のmini batchが作れれば良い。以前から 顔画像データはTFRecordsのファイル形式で作っていて それを読み取ってBatchにする処理は書いているので、それをほぼそのまま利用できる。

def inputs(batch_size, f_size):
    files = [os.path.join(FLAGS.data_dir, f) for f in os.listdir(FLAGS.data_dir) if f.endswith('.tfrecords')]
    fqueue = tf.train.string_input_producer(files)
    reader = tf.TFRecordReader()
    _, value = reader.read(fqueue)
    features = tf.parse_single_example(value, features={'image_raw': tf.FixedLenFeature([], tf.string)})
    image = tf.cast(tf.image.decode_jpeg(features['image_raw'], channels=3), tf.float32)
    image.set_shape([INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, 3])
    image = tf.image.random_flip_left_right(image)

    min_queue_examples = FLAGS.num_examples_per_epoch_for_train
    images = tf.train.shuffle_batch(
        [image],
        batch_size=batch_size,
        capacity=min_queue_examples + 3 * batch_size,
        min_after_dequeue=min_queue_examples)
    return tf.sub(tf.div(tf.image.resize_images(images, f_size * 2 ** 4, f_size * 2 ** 4), 127.5), 1.0)

元々は分類タスクの教師データなのでlabel_idとセットになっているデータセットだけど、ここではJPEGバイナリ部分だけ取り出して使うことになる。distort系の処理はとりあえずほぼ無しで、random_flip_left_right(ランダム左右反転)だけ入れている。あと分類タスクでは最後にtf.image.per_image_whiteningを入れていたけど、これをやると元の画像に戻せなくなってしまうと思ったので 単純に 0〜255 の値を -1.0〜1.0 の値になるよう割って引くだけにしている。

Training

で、GeneratorとDiscriminatorが出来たらあとは学習の手続き。それぞれに対して最小化すべき誤差(loss)を定義して、Optimizerに渡す。前述した「Discriminatorは正しく判定できるよう学習させ、GeneratorはDiscriminatorを欺いて誤判定させる画像を生成するよう学習する」というのをコードに落とし込む。
Discriminatorによる分類を「0なら画像はGeneratorによるもの、1なら学習データのもの」と判定する関数D(x)と定義し、Generatorから生成した画像をG()、学習データの画像をIとすると

  • Generatorは、D(G())がすべて1になるのが理想
  • Discriminatorは、D(G())をすべて0にし D(I)をすべて1にするのが理想

なので、そのギャップをlossとして定義することになる。Discriminatorのような排他的な唯一の分類クラスを決める場合の誤差にはtf.nn.sparse_softmax_cross_entropy_with_logitsを使うのが良いらしい。
ということでこんな感じのコード。

def train(self, input_images):
    logits_from_g = self.d(self.g())
    logits_from_i = self.d(input_images)
    tf.add_to_collection('g_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_g, tf.ones([self.batch_size], dtype=tf.int64))))
    tf.add_to_collection('d_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_i, tf.ones([self.batch_size], dtype=tf.int64))))
    tf.add_to_collection('d_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_g, tf.zeros([self.batch_size], dtype=tf.int64))))
    g_loss = tf.add_n(tf.get_collection('g_losses'), name='total_g_loss')
    d_loss = tf.add_n(tf.get_collection('d_losses'), name='total_d_loss')
    g_vars = [v for v in tf.trainable_variables() if v.name.startswith('g')]
    d_vars = [v for v in tf.trainable_variables() if v.name.startswith('d')]
    g_optimizer = tf.train.AdamOptimizer(learning_rate=0.0001, beta1=0.5).minimize(g_loss, var_list=g_vars)
    d_optimizer = tf.train.AdamOptimizer(learning_rate=0.0001, beta1=0.5).minimize(d_loss, var_list=d_vars)
    with tf.control_dependencies([g_optimizer, d_optimizer]):
        train_op = tf.no_op(name='train')
    return train_op, g_loss, d_loss

論文によるとAdamOptimizerのパラメータはデフォルト値ではなくlearning_rateは0.0002、beta1は0.5を使うとのことだったけれど、Qiitaでの先行事例ではlearning_rateはさらに半分の0.0001としていて 実際大きすぎると最初の段階で失敗してしまうことがあったので0.0001にしておいた。
あと効くかどうか分からないけど一応すべてのweightsに 分類タスク で使っていたWeight Decayを入れておいた。

Generating Images

こうして学習のopsも定義できたらあとはそれを実行して繰り返していれば少しずつ「ランダムな出力」から「顔らしい画像」になっていく。はず。
で、その成果物を確かめたいのでやっぱり画像ファイルとして書き出したいわけで。Generatorからの出力を取得して変換かけて、scipyやpylabなどを使って画像として出力できるみたいだけど、そのあたりも実はTensorFlowだけで出来るんですね。
Generatorからの出力は[batch size, height, width, channel]の、 -1.0〜1.0 の値をとるTensorなので、まずはそれらをすべて 0〜255 の整数値に変換する。
で、それをbatch sizeにsplitしてやると、それぞれ[height, width, channel]な画像データになるわけで。これらはtf.image.encode_pngとかにかければPNGのバイナリが得られる。
せっかくなので複数出力された画像をタイル状に並べて1つの画像として出力させたいじゃん、って思ったらtf.concatで縦に繋げたり横に繋げたりを事前に入れておくことでそれも実現できる。

def generate_images(self, row=8, col=8):
    images = tf.cast(tf.mul(tf.add(self.g(), 1.0), 127.5), tf.uint8)
    images = [tf.squeeze(image, [0]) for image in tf.split(0, self.batch_size, images)]
    rows = []
    for i in range(row):
        rows.append(tf.concat(1, images[col * i + 0:col * i + col]))
    image = tf.concat(0, rows)
    return tf.image.encode_png(image)

これで、得られたopsã‚’evalして得たバイナリをファイルに書き出すだけで1つのbatchで生成された複数の画像出力を並べたものを一発で得ることができる。便利〜。

計算量を減らす(?)

今回は64x64でなく96x96の画像を生成させようとしていて(学習データが112x112で収集しているし 折角ならそれなりに大きく作りたい!)、元の論文では各層のchannel数が 1024, 512, 256, 128 になっていて(Qiitaの記事ではすべて半分にしていた)、そのパラメータ数で手元のCPUマシンで計算させる(僕はケチなのでGPUマシンとか持ってない…)と、 1step に 50sec とかとんでもなく時間がかかってしまい ちょっと絶望的だわ…と思い 少しでも計算量が減るよう 250, 150, 90, 54 という数字に変えた(50sec -> 18sec)。そしてbatch sizeも 128 から、半分の 64 だと流石に無理そうだったので 96 に(18sec -> 13sec)。一応このパラメータ数で200stepほど回してみたところ ちゃんとランダム出力から顔っぽいものに変わっていっているのが観測できたので これでやってみた。


少しずつ顔っぽいものが現れてきて、それぞれが個性ある顔に 鮮明に写るようになっていく変化がみてとれるかと。
こうして丸2日以上かけて、7000stepくらい回した結果 得たのが冒頭の画像になります。まだちゃんとした顔にならなかったりするのは、単に学習回数が足りていないのか パラメータが足りなすぎてこれ以上キレイにならないのか、はもうちょっと続けてみないと分からないけど 多分まだ回数が足りていないだけなんじゃないかな… もうちょっと続けてみます。

今回は「顔」っていうとても限定的な領域での生成だし、これくらいで大丈夫だろう、と かなり勘でパラメータ数を決めてしまっているので 本当はもっと理論的に適切な数を導き出したいところだけど…。
前回までの分類器だとsparsityというのを計測していたのでそれを元に削れるだろうな、と思っているのだけど、今回の場合すべてにBatch Normalizationが入っているのでそれも意味なくて、なんとも難しい気がする。それこそGPUで何度もぶん回して探っていくしかないのかなぁ。

任意の画像の生成…

DCGANによる生成ができると、今度は入力の乱数ベクトルを操作することで任意の特徴をもつ画像をある程度狙って生成できるようになる、とのことなのでそれも試してみたいと思ったけど まだ出来ていないのと 長くなってしまったので続きは次回。