すぎゃーんメモ

2016-05-16

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

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

f:id:sugyan:20160516092946p:image

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


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


DCGANとは

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

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

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

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

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

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

原理としては、

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

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

f:id:sugyan:20160514234103p:image

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

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


TensorFlowでの実装

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

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


Generator

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

f:id:sugyan:20160515012346p:image

(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_scopereuse=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_rate0.0002beta10.5を使うとのことだったけれど、Qiitaでの先行事例ではlearning_rateはさらに半分の0.0001としていて 実際大きすぎると最初の段階で失敗してしまうことがあったので0.0001にしておいた。

あと効くかどうか分からないけど一応すべてのweights分類タスク で使っていたWeight Decayを入れておいた。


Generating Images

こうして学習のopsも定義できたらあとはそれを実行して繰り返していれば少しずつ「ランダムな出力」から「顔らしい画像」になっていく。はず。

で、その成果物を確かめたいのでやっぱり画像ファイルとして書き出したいわけで。Generatorからの出力を取得して変換かけて、scipypylabなどを使って画像として出力できるみたいだけど、そのあたりも実は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で生成された複数の画像出力を並べたものを一発で得ることができる。便利〜。

f:id:sugyan:20160516093024p:image


計算量を減らす(?)

今回は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ほど回してみたところ ちゃんとランダム出力から顔っぽいものに変わっていっているのが観測できたので これでやってみた。

f:id:sugyan:20160516092949g:image

少しずつ顔っぽいものが現れてきて、それぞれが個性ある顔に 鮮明に写るようになっていく変化がみてとれるかと。

こうして丸2日以上かけて、7000stepくらい回した結果 得たのが冒頭の画像になります。まだちゃんとした顔にならなかったりするのは、単に学習回数が足りていないのか パラメータが足りなすぎてこれ以上キレイにならないのか、はもうちょっと続けてみないと分からないけど 多分まだ回数が足りていないだけなんじゃないかな… もうちょっと続けてみます。

今回は「顔」っていうとても限定的な領域での生成だし、これくらいで大丈夫だろう、と かなり勘でパラメータ数を決めてしまっているので 本当はもっと理論的に適切な数を導き出したいところだけど…。

前回までの分類器だとsparsityというのを計測していたのでそれを元に削れるだろうな、と思っているのだけど、今回の場合すべてにBatch Normalizationが入っているのでそれも意味なくて、なんとも難しい気がする。それこそGPUで何度もぶん回して探っていくしかないのかなぁ。


任意の画像の生成…

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

2016-03-28

TensorFlowでのDeep Learningによるアイドルの顔識別 のためのデータ作成

続・TensorFlowでのDeep Learningによるアイドルの顔識別 - すぎゃーんメモ の続き、というかなんというか。

前回までは「ももいろクローバーZのメンバー5人の顔を識別する」というお題でやっていたけど、対象をより広範囲に拡大してみる。


様々なアイドル、応援アプリによる自撮り投稿

あまり知られていないかもしれないけど、世の中にはものすごい数のアイドルが存在しており、毎日どこかで誰かがステージに立ち 歌って踊って頑張っている。まだまだ知名度は低くても、年間何百という頻度でライブを中心に活動している、所謂「ライブアイドル」。俗に「地下アイドル」と言ったりする。

そういったアイドルさんたち 活動方針も様々だけど、大抵の子たちはブログTwitterを中心としてWebメディアも活用して積極的に情報や近況を発信していたりする。

そんな中、近年登場した「自撮り投稿サービス」。

概要としては、各アイドルさんが自撮りなどの写真を投稿し、ファンのユーザーが「応援」としてポイントを送ることでその数で順位がつく、というもの。ある期間で上位にランクインすると街頭広告雑誌インタビューに掲載されるといったイベントも開催されるため、アイドルさんは可愛い自撮りをたくさん投稿するしファンポイント貯めたり買ったりして頑張って応援する。有名なところで以下の2つのサービスが登場している。

それぞれのサービスに登録されているアイドルさんを 雑にスクレイピング して取得したところ、軽く1000件を超えた。それだけたくさんアイドルがいるわけで、最終目的としては入力した顔画像をそれら1000人以上のアイドルに分類すること、となる。

…のだけど、つまりそれを学習させるためにそれらのアイドルさんたちの「顔画像」と「(それが誰であるかを示す)ラベル」のセットが大量に必要となるわけで。


自撮り投稿から顔識別用データセットを作るには

多くのアイドルさんが利用しているこの自撮り投稿サービスから、投稿者と画像をセットで引っ張ってくれば「自撮り」なんだから自動でラベル付け済みの顔画像データセットを作れるんじゃないか、と考えたのだけど、まぁそんなに簡単な話でもなく。

まず「主に自撮りが投稿される」のであって必ず投稿した本人だけの顔が写っているとは限らない。別にそういうルールがあるわけではないので、共演した別のアイドルさんとの2ショットだったり 同じグループのメンバーと一緒の仲良しショットがあったりすることも珍しくない。そういった写真は顔検出は自動で出来ても、複数検出されたもののうちどの顔が投稿者本人なのかは分からない。

あと、そもそも上記の自撮り投稿サービスたちはデータ取得用のAPIが公開されていない。CHEERZの方はWeb版があるのでスクレイピングすれば可能そうではあるけど、DMM.yellはアプリ専用なのでそれなりにハックが必要になる。


Twitterから収集、管理するWebアプリ

…やはりラベル付けは人力でやるしかない、と覚悟を決めて、とにかくまずは収集してみることにした。

アプリには取得APIが無いが、投稿時にTwitterと連動する機能がついているようで、大抵のアイドルさんは自分のTwitterアカウントと紐付けてそちらにも画像付きTweetで流している。ので、連動投稿に付与されている「#CHEERZ」「#dmmyell」といったハッシュタグTwitter検索をかければ大抵の画像は取得できる。それらの投稿から各アイドルさんの個人アカウントも把握できるので、自撮りアプリ連携ではない普段の画像Tweetも収集対象になるように登録しておく。

そうやって定期ジョブTwitterからひたすら画像付きTweetを取得し、 自作の顔検出器 にかけて顔部分を抽出して保存していく。このあたりは 前回までももクロちゃんデータセットを作成したときと同様で、管理用のWebアプリを自作している。

以前はこれをHeroku運用していたのだけど、今回はあっという間に顔画像が10000件を超えて 無料枠で利用できる範囲をオーバーしてしまったので、以前から持っていたのにほぼ使っていなかったさくらVPSサーバに移行した。


ひたすら目視でラベル付け

こんな感じで、様々なアイドルさんの顔画像が取得できる。(かわいい)

f:id:sugyan:20160323195015p:image

が、これらをそれぞれ「誰であるか」のラベル付けをする必要があり。僕だって一応ドルヲタとして年間200〜300の現場に足を運び何百組というアイドルを見てきたし そのへんの人よりはアイドルに詳しいつもりだから 100人とか200人くらいなら顔を識別できる自信はあるけれど、1000人くらいの顔がごちゃ混ぜになっていると流石に無理がある。

とりあえずは知ってる顔はどんどん片付けていって、あとは知らない子でも普通に自撮りっぽい画像で1人で写っていればそれはまぁ本人に違いないだろう、と判断できるのでラベルを付けていく。本当に見たことのない知らない子たちの集合写真とかはまったく判別できないので後回し。

というのを考えて、顔が1つだけ検出されている画像を優先でランダムで選択しつつラベル付けしていったり。いちおう入力しやすいよう補完つけたりインタフェースを工夫したりはしている。

f:id:sugyan:20160327235241g:image

気の遠くなるような作業ではあるけれど、まぁ好きなので意外と飽きない。数千件は自力でラベル付けできた。


とりあえず学習させてみる

と、こんな作業を続けているうちに 何人かはある程度の枚数の顔画像が揃うので、集まっている限りのデータを使って 前回のもの と同じ、96x96 size, 3 channelの画像を入力とする4層の畳み込みと3層の全結合によるネットワークを使って学習させてみる。

1分類につき10枚程度だと心許ないが、30枚くらい学習させたらある程度の特徴を掴んでそれっぽいものは判別してくれるようになるのでは…?という目論見。

学習用と評価用とかデータセットを分ける余裕はないので、とりあえずはラベル付けしたものは全部学習用に使う。前回と同じTFRecordのファイルを入力に使うため、 ダウンロードできるように顔画像JPEGバイナリとラベル番号のセットを含むTFRecordバイナリを吐くエンドポイント実装 した。

ラベル付けして30枚以上集まっている顔画像セットに、それ以外のものとして 数枚しか集まっていないものやそもそも顔画像じゃないものも「分類対象外」のラベルとして3割ほど混ぜる。数千枚を50クラスくらいに分類するもの、となる。これは前回と同じモデル(むしろ畳み込み層のパラメータ数は減らしてる)で 1000〜2000 stepくらいでも十分にcross entropyが減少して 教師データに対してはほぼミスなく ちゃんと分類してくれるようになる。


推論結果を確認、修正

で、ある程度学習が済んだモデルが出来上がったら、未分類の顔画像に対してその分類器にかけて推論してもらう。

f:id:sugyan:20160328002101p:image

学習済みの顔に近いものは高いスコアで識別されるはずなので、これを確認することで、未分類の顔画像に対して「誰であるか」を考えるのではなく「○○と推測されているが、合っているか」だけを考えることになるので、人間の負担が軽減し作業が捗る。

そして当然ながらまだまだ学習データ数が少ないので、この推論はすごくよく間違う(上記の画像のは全部あってます)。

傾向としては、一人金髪の子を学習すると 髪の明るい人物は大抵その子と認識するようになる とか、画質がボヤけ気味のが多い子を学習しているとボヤけてる画像はだいたいその子と認識する、とか。髪の短い子や 頬にほくろのある子 とか 一応なんらかの特徴を掴んでいるようで 似たようなものはそれっぽく分類することは多いけど、やっぱりヒトが見たら全然ちがうだろって思うような間違いをしていたりはする。

それはそれで確認しながら正しくラベル付けしてやることでまた学習に使えるデータが増えるので、ある程度の答え合わせが済んだら増えたデータセットを元に再び学習してやる。そうすることで以前間違えたようなものはもう同じ間違いはしないし、さらに精度の高い推論をするよう進化する。

似たような入力に対し何度も学習プロセスを繰り返すのに 毎回まっさらな状態から始めるのは非効率だと思ったので、学習し直すときは前回の学習済みモデルのパラメータをロードしてそこから始めることにした。分類対象数が増えた場合でも、それは最後の全結合の 隠れ層→出力層 の部分だけしか構造は変わらないので、それ以外のパラメータはそのまま使っても 初期化状態から始めるよりは早い、はず。

def restore_or_initialize(sess):
    if os.path.exists(FLAGS.checkpoint_path):
        for v in tf.all_variables():
            print 'restore variables "%s"' % v.name
            try:
                restorer = tf.train.Saver([v])
                restorer.restore(sess, FLAGS.checkpoint_path)
            except Exception:
                print 'could not restore, initialize!'
                sess.run(tf.initialize_variables([v]))
    else:
        print 'initialize all variables'
        sess.run(tf.initialize_all_variables())

https://github.com/sugyan/tf-classifier/blob/master/models/v2/train.py#L30-L46


結論

ということで、学習用のデータを用意するのは大変だけど、

  • ある程度集まったらとりあえず学習させる
  • 学習させたモデルを使って推論させてみる
  • 推論結果を検証することで学習データを増やし、再び学習させることで精度が上がる

というサイクルを続けることで、なんだかんだで自力で13000点ほどの分類済みのアイドル顔データを作ることができている。



現時点での最新の学習済み分類器での結果はたとえば

f:id:sugyan:20160328004436p:image

というかんじで、100クラス以上の分類数である中で Luce Twinkle Wink☆ のメンバー5人中4人くらいは一応判別できるようになっている。


課題

30枚くらい集まれば学習対象になって 推論結果にも出るからさらにデータを増やすのに使えるけれど、そもそもその30枚くらいまで集めるのが大変なわけで。

似たような特徴を持つ顔をクラスタリングしてまとめてラベル付けできたら良いのかな…?と思って、学習済みモデルの隠れ層の出力パターンが似たものとかで分類できないかと調べてみたのだけど ちょっと有意な傾向は把めなそうだった…。

あとは推論結果に対する検証なんかはアイドルに詳しいドルヲタの方々に手伝ってもらう形で集合知を利用して実現できれば良いのだけど、なんとも良いインタフェースが思い浮かばない。


余談

分類作業しているうちに、知らなかった子の顔もけっこう覚えるようになる。人間のラーニング能力もすごいな、って。


今後の展望

だんだんアイドルの顔画像」が集まってきたので、今度はこれらを利用して"生成する"というのにもチャレンジしてみたいと思っている。

他にもなにか良いアイディアがあれば。現時点で作ったデータセットも何かに利用したい、という方がおりましたら提供の相談させていただきますのでお気軽に連絡ください。

2016-01-28

続・TensorFlowでのDeep Learningによるアイドルの顔識別

TensorFlowによるディープラーニングで、アイドルの顔を識別する - すぎゃーんメモ の続き。

f:id:sugyan:20160128085051p:image

前回は最も簡単に画像分類を試すために TensorFlow に同梱されているtensorflow.models.image.cifar10パッケージのモデルや学習機構を利用して約75%の識別正答率の分類器を作ったが、それよりも良い結果を出したいし色々ためしてみたい、ということで今回は色々と自前で実装したり改良を加えてみた。

結論だけ先に書くと、約90%の正答率のものを作ることができた。分類数も変えてしまっているので一概には前回のものと比較できないけど。


入力画像の変更

まずは入力の画像について。

前回はCIFAR-10のデータセットに合わせて、検出して切り出した顔画像を32x32サイズに縮小したものを利用していた。

32x32 → inside 96x96 of 112x112

f:id:sugyan:20160125212738p:image

流石に32x32では小さすぎて人間が見てもなかなか区別できなかったりしたし、もうすこしハッキリと分かるくらいの画像サイズを入力に使えるようにしよう ということで各辺3倍サイズの96x96画像を入力にすることにした(画素数で言うと9倍)。

そして、ある程度のスケーリング誤差も吸収できるようにと 顔画像収集時には検出された顔領域の1.2倍ほどの少し大きめの領域で切り出し112x112サイズで取得し、そこから96〜112の間でランダムに切り出してさらに収縮させて最終的に96x96サイズに収まるように、というのを後述のDistortionのところで行った。

6 → 5 Classification

切り出す領域を変更したので顔画像は収集し直してラベルも付け直した。収集方法は同じで、ももクロメンバー5人についてそれぞれ200点、計1000点を学習用のデータセットとして用いた。

前回は6番目のラベルとして「ももクロ以外」の人物の顔を学習・評価に使っていたが、どうにも種類が少なくて分類のラベルとして使うのに適しているとは思えなかったので除外することにした。

TFRecord file

CIFAR-10のバイナリデータの場合、各ピクセルについてのR, G, Bの値を1byteずつ使って表す形だったので1画像あたり32 * 32 * 3 = 3072byteだったが、これが各辺3倍にするとデータサイズが9倍になってしまう。1000点集めると96 * 96 * 3 * 1000 = 27648000byte(26.4MB)。

まぁ別にそれくらいならどうってことないのだけど、もう少し小さいサイズで済むならそれに越したことはない。

TensorFlowには"TFRecords"というバイナリデータ列も含めたシリアライズのファイル形式をサポートするReader & Writerがあり、固定長でない構造的なデータなども複数格納したりできる。

ので、ここに分類の正解ラベルの値とJPEG画像のバイナリデータ列をセットで入れてシリアライズして書き込むことで、112x112サイズでも1画像あたり3~5KB程度でデータセットを作成できる。

使うときtf.TFRecordReaderでTFRecord fileを読んでFeatureを取り出せばあとはJPEGバイナリデータからtf.image.decode_jpegで画像に復元できる。

(tf.parse_single_exampleあたりはtensorflow-0.6.0と最新コードでは引数などインタフェースが異なるので注意。最新masterのドキュメント読みながらコード書いてたら動かなくてハマった)

Distortion

読み込んでdecodeした画像を、学習データとしてさらにランダムに加工して使う。これはtensorflow.models.image.cifar10.distorted_inputsでも使われている手法。

TensorFlowにはtf.image.random_crop, tf.image.random_flip_left_right, tf.image.random_brightness, tf.image.random_contrastなどの画像加工系メソッドが用意されており、これらによる加工処理を入れることで明るさや色合いを変えたり反転・拡大縮小したりできるので、1つの顔画像からも異なる複数の画像を生成して学習に利用できる。

例えば

f:id:sugyan:20160123231218p:image

という具合に。

各random系メソッドでは加工の度合いの上限・下限を指定できたりするので、その幅を拡げてもっと極端にすると

f:id:sugyan:20160123231220p:image

のようになったりする。

どの程度までやるのが適切なのかは分からないけど 異常になりすぎない程度に抑えておいた。

Code

コードとしてはこんなかんじ。

def inputs(files, distort=False):
    fqueue = tf.train.string_input_producer(files)
    reader = tf.TFRecordReader()
    key, value = reader.read(fqueue)
    features = tf.parse_single_example(value, features={
        'label': tf.FixedLenFeature([], tf.int64),
        'image_raw': tf.FixedLenFeature([], tf.string),
    })
    image = tf.image.decode_jpeg(features['image_raw'], channels=3)
    image = tf.cast(image, tf.float32)
    image.set_shape([IMAGE_SIZE, IMAGE_SIZE, 3])

    if distort:
        cropsize = random.randint(INPUT_SIZE, INPUT_SIZE + (IMAGE_SIZE - INPUT_SIZE) / 2)
        framesize = INPUT_SIZE + (cropsize - INPUT_SIZE) * 2
        image = tf.image.resize_image_with_crop_or_pad(image, framesize, framesize)
        image = tf.image.random_crop(image, [cropsize, cropsize])
        image = tf.image.random_flip_left_right(image)
        image = tf.image.random_brightness(image, max_delta=0.4)
        image = tf.image.random_contrast(image, lower=0.6, upper=1.4)
        image = tf.image.random_hue(image, max_delta=0.04)
        image = tf.image.random_saturation(image, lower=0.6, upper=1.4)
    else:
        image = tf.image.resize_image_with_crop_or_pad(image, INPUT_SIZE, INPUT_SIZE)

    min_fraction_of_examples_in_queue = 0.4
    min_queue_examples = int(FLAGS.num_examples_per_epoch_for_train * min_fraction_of_examples_in_queue)
    images, labels = tf.train.shuffle_batch(
        [tf.image.per_image_whitening(image), tf.cast(features['label'], tf.int32)],
        batch_size=BATCH_SIZE,
        capacity=min_queue_examples + 3 * BATCH_SIZE,
        min_after_dequeue=min_queue_examples
    )
    images = tf.image.resize_images(images, INPUT_SIZE, INPUT_SIZE)
    tf.image_summary('images', images)
    return images, labels

Inference

分類推定のモデルは、 VGGNet と呼ばれる画像分類のための畳み込みネットワークを参考に、独自に定義して作った。

x2 conv layers

VGGNetは「3x3でのconvolutionと2x2でのmax pooling」の組み合わせを複数(5回?)繰り返した後に 3層の全結合で最終的な出力を得ている。これを真似して、cifar10のときには2回だった畳み込み&プーリングを4回行なうようにして

  1. 96 * 96 * 348 * 48 * 32の畳み込み&プーリング層
  2. 48 * 48 * 3224 * 24 * 64の畳み込み&プーリング層
  3. 24 * 24 * 6412 * 12 * 128の畳み込み&プーリング層
  4. 12 * 12 * 1286 * 6 * 256の畳み込み&プーリング層
  5. 9216(= 6 * 6 * 256) * 1024の全結合層
  6. 1024 * 256の全結合隠れ層
  7. 256 * 5の全結合出力層

とした。畳み込みのときの出力channel数や中間層の数など、どれくらいに設定するのが適切なのかはよく分からないので適当に。

パラメータ数としてはweightだけで計算すると

(3 * 3 * 3 * 32) + (3 * 3 * 32 * 64) + (3 * 3 * 64 * 128) + (3 * 3 * 128 * 256) + (9216 * 1024) + (1024 * 256) + (256 * 5) = 10088544

くらい。cifar10のものでは

(5 * 5 * 3 * 64) + (5 * 5 * 64 * 64) + (4096 * 384) + (384 * 192) + (192 * 6) = 1754944

だったので5.7倍くらいには増えている。

Code

コードとしてはこんなかんじ。

def inference(images):
    def _variable_with_weight_decay(name, shape, stddev, wd):
        var = tf.get_variable(name, shape=shape, initializer=tf.truncated_normal_initializer(stddev=stddev))
        if wd:
            weight_decay = tf.mul(tf.nn.l2_loss(var), wd, name='weight_loss')
            tf.add_to_collection('losses', weight_decay)
        return var

    def _activation_summary(x):
        tensor_name = x.op.name
        tf.scalar_summary(tensor_name + '/sparsity', tf.nn.zero_fraction(x))

    with tf.variable_scope('conv1') as scope:
        kernel = tf.get_variable('weights', shape=[3, 3, 3, 32], initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[32], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv1 = tf.nn.relu(bias, name=scope.name)
        _activation_summary(conv1)
    pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool1')

    with tf.variable_scope('conv2') as scope:
        kernel = tf.get_variable('weights', shape=[3, 3, 32, 64], initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[64], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(bias, name=scope.name)
        _activation_summary(conv2)
    pool2 = tf.nn.max_pool(conv2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool2')

    with tf.variable_scope('conv3') as scope:
        kernel = tf.get_variable('weights', shape=[3, 3, 64, 128], initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[128], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv3 = tf.nn.relu(bias, name=scope.name)
        _activation_summary(conv3)
    pool3 = tf.nn.max_pool(conv3, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool3')

    with tf.variable_scope('conv4') as scope:
        kernel = tf.get_variable('weights', shape=[3, 3, 128, 256], initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv = tf.nn.conv2d(pool3, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[256], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv4 = tf.nn.relu(bias, name=scope.name)
        _activation_summary(conv4)
    pool4 = tf.nn.max_pool(conv4, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool4')

    with tf.variable_scope('fc5') as scope:
        dim = 1
        for d in pool4.get_shape()[1:].as_list():
            dim *= d
        reshape = tf.reshape(pool4, [BATCH_SIZE, dim])
        weights = _variable_with_weight_decay('weights', shape=[dim, 1024], stddev=0.02, wd=0.005)
        biases = tf.get_variable('biases', shape=[1024], initializer=tf.constant_initializer(0.0))
        fc5 = tf.nn.relu(tf.nn.bias_add(tf.matmul(reshape, weights), biases), name=scope.name)
        _activation_summary(fc5)

    with tf.variable_scope('fc6') as scope:
        weights = _variable_with_weight_decay('weights', shape=[1024, 256], stddev=0.02, wd=0.005)
        biases = tf.get_variable('biases', shape=[256], initializer=tf.constant_initializer(0.0))
        fc6 = tf.nn.relu(tf.nn.bias_add(tf.matmul(fc5, weights), biases), name=scope.name)
        _activation_summary(fc6)

    with tf.variable_scope('fc7') as scope:
        weights = tf.get_variable('weights', shape=[256, NUM_CLASSES], initializer=tf.truncated_normal_initializer(stddev=0.02))
        biases = tf.get_variable('biases', shape=[NUM_CLASSES], initializer=tf.constant_initializer(0.0))
        fc7 = tf.nn.bias_add(tf.matmul(fc6, weights), biases, name=scope.name)
        _activation_summary(fc7)

    return fc7

Loss

損失関数はcifar10のものと同様で、入力画像に対する出力と正解ラベルとのクロスエントロピー、そこに汎化性能向上のための正則化手法として(?)全結合層の最初と中間層のパラメータに適当な割合でweight decayを入れて それらを合計したものを最小化対象のtotal lossとしている。

Code
def loss(logits, labels):
    sparse_labels = tf.reshape(labels, [BATCH_SIZE, 1])
    indices = tf.reshape(tf.range(BATCH_SIZE), [BATCH_SIZE, 1])
    concated = tf.concat(1, [indices, sparse_labels])
    dense_labels = tf.sparse_to_dense(concated, [BATCH_SIZE, NUM_CLASSES], 1.0, 0.0)

    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits, dense_labels)
    mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
    tf.add_to_collection('losses', mean)
    return tf.add_n(tf.get_collection('losses'), name='total_loss')

Train

AdamOptimizer

学習にはGradientDescentOptimizerではなくAdamOptimizerというのを使ってみた。

ちゃんと比較はしていないのだけど、GradientDescentOptimizerよりも格段に早く(少ないstepで)lossが減少するように学習が進んでいるのは観測した。Learning Rateを減衰させて調整する、といったこともここでは行っていない。

ただ1500stepくらいまで行くとそれ以降1stepあたりの計算時間が4〜5倍かかるようになったのだけど これはAdamOptimizerのせいなのかな…?よく分かってない

Code
def train(total_loss, global_step):
    loss_averages = tf.train.ExponentialMovingAverage(0.9, name='avg')
    losses = tf.get_collection('losses')
    loss_averages_op = loss_averages.apply(losses + [total_loss])

    for l in losses + [total_loss]:
        tf.scalar_summary(l.op.name + ' (raw)', l)

    # Apply gradients, and add histograms
    with tf.control_dependencies([loss_averages_op]):
        opt = tf.train.AdamOptimizer()
        grads = opt.compute_gradients(total_loss)
    apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
    for var in tf.trainable_variables():
        tf.histogram_summary(var.op.name, var)
    for grad, var in grads:
        if grad:
            tf.histogram_summary(var.op.name + '/gradients', grad)

    # Track the moving averages of all trainable variables
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())

    with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
        train_op = tf.no_op(name='train')
    return train_op

Graph

最終的に出来た全体像のグラフはこんなかんじ。

f:id:sugyan:20160122023635p:image

潰れてしまって全然読めないと思うけど、下の方からTFRecordsのファイルを読み込んで加工した画像とラベルのbatchを作って 真ん中の分岐で左側では畳み込みニューラルネットワークに通して出力を計算して 右側を通るラベルと結果を突き合わせてクロスエントロピーを出して損失を計算したりしている、っていう感じになってるはず。


Results

f:id:sugyan:20160125200224p:image

学習結果はこのように、数百〜千step程度でじゅうぶんに0に近い値までlossが減少した。

最初 いくら学習を繰り返しても全然cross entropyが減少しなくて、なんでだ!?と思ったら序盤の畳み込み層の初期値(の幅、具体的にはtf.truncated_normal_initializerに渡すstddev)が小さすぎて途中の出力がすべて同一の値になってしまっていたのが原因だったようで そこを適切に設定し直すことで上記のように上手く学習が進むようになった。

各段階で保存した変数を使って「学習に使っていない」データを使って評価した結果が以下。

f:id:sugyan:20160125200223p:image

1600stepくらいのところでようやく90%ラインに到達して、あとはそこから上がらず まぁ誤差の範囲内かな くらいに。


実際に試すWebアプリも少しアップデートして結果の表示方法をちょっと変えた。冒頭に載せたやつは上手くいった例。

https://momoclo-face-recognizer.herokuapp.com/

↓最新アルバムのジャケ写。百田さんが「有安」になってしまっている

f:id:sugyan:20160128085100p:image

↓前回にも使ったやつ。佐々木さんが「有安」に。

f:id:sugyan:20160128085109p:image

↓だいぶ昔のアー写。百田さんが「高城」に。

f:id:sugyan:20160128085035p:image

↓比較的最近のアー写。玉井さんが「有安」に。

f:id:sugyan:20160128085043p:image

…という具合に集合写真だとやっぱり5人中1人くらいは間違う感覚。


考察

問題設定を変えてしまったので比較しづらくなってしまったのだけど… 結局6クラス分類が75%に上がるのと5クラス分類が90%に上がるのはあまり変わらない気はする。。

けどまぁ少なくとも悪化はしていないはず。あとはやっぱり学習データ数かな、と。


なんとかもっとラクに大量の学習データを用意する方法は無いだろうか…


Repository

https://github.com/sugyan/tf-classifier/tree/v2

2016-01-12

TensorFlowによるディープラーニングで、アイドルの顔を識別する

以前は MNISTの例を使って画像識別を試してみた けど、次はカラー画像についての識別を試してみる。

f:id:sugyan:20160112012505p:image

アイドルなんてみんな同じ顔に見える」って 最近も言われてるのかどうか知らないけど、自分もつい5年前くらいまではそう思っていたわけで。その識別を機械学習でやってみよう という試み。

最近はほとんどライブに行かなくなってしまったけど大好きなももいろクローバーZちゃんを題材にしてみることに。

5人のメンバーの顔は機械学習によってどれくらい分類できるようになるのか??


CIFAR-10

CIFAR-10 という、32×32サイズのカラー画像を10種類のクラスに分類する識別課題があり、そのデータセットが公開されている。これを実際にTensorFlowで学習するための畳み込みニューラルネットワークのモデル関数などがtensorflow.models.image.cifar10パッケージに同梱されているので、これを利用して学習させてみることにした。


画像収集

まず課題となるのが訓練用のデータセットの用意。教師あり学習を行うため、「顔の画像」と「それがどの人物の顔であるか(どう分類されるのが正解か)、を示すラベル」のセットが必要で、CIFAR-10では各6000枚の画像とラベルのセットが用意され提供されている。ももクロの5人の顔識別においては現実的にどれくらいの量が必要か分からないけど、最低でも各100くらいは用意したいところ。

ももクロちゃんはずっとアメブロを続けてきているので、そこに自撮り画像などはある程度蓄積されている。それを利用することにして、

というのを出来るwebアプリをまずrailsで作った。画像加工はRMagickでだいたいできるので便利。

face-collector
https://github.com/sugyan/face-collector

そしてこれを使って自動抽出された顔画像たちを目視で確認しながらラベル付け。これだけは残念ながら人力で行うしかない。集合知を上手く利用できればこのへんもある程度は自動化できるのかもしれないけど…。

とりあえず5人の各メンバーについて200点ずつくらいはすぐにデータを作ることができ(有安さんは安定の自撮りが多くて集めやすい、玉井さんは自撮り少なくて苦労した…あと高城さんは変顔が多くて判断に迷うことが多かったw)、メンバー以外のスタッフや共演者さんの顔画像なども幾つかあったのでそれらも「ももクロメンバーではない」という6つ目のラベルとして混ぜて、訓練用と評価用でデータセットを作成した。

f:id:sugyan:20160112081832p:image

管理上はユニークなデータとしていても同じ顔の写った同じ写真を複数のメンバーがブログに載せていたりもするので、実際には完全にユニークではなく数組ほぼ同じものが混ざっていたりもするかもしれない。


学習

これらを使って、 Tutorial とほぼ同様に学習させていく。CIFAR-10と同形式でファイルを用意しておけば、cifar10.input()関数1つでファイルからのデータ読み込み、加工、キューイングまですべて済んだ入力データを得られるので便利。

実際に書いたコードはこれだけで、

cifar10.IMAGE_SIZE = 32
cifar10.NUM_CLASSES = 6
cifar10.NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = 1000
cifar10.INITIAL_LEARNING_RATE = 0.08

FLAGS = tf.app.flags.FLAGS

tf.app.flags.DEFINE_integer('max_steps', 10000,
                            """Number of batches to run.""")
tf.app.flags.DEFINE_string('train_dir', 'train',
                           """Directory where to write event logs """
                           """and checkpoint.""")

def train():
    # ops
    global_step = tf.Variable(0, trainable=False)
    images, labels = cifar10.distorted_inputs()
    logits = cifar10.inference(tf.image.resize_images(images, cifar10.IMAGE_SIZE, cifar10.IMAGE_SIZE))
    loss = cifar10.loss(logits, labels)
    train_op = cifar10.train(loss, global_step)
    summary_op = tf.merge_all_summaries()

    with tf.Session() as sess:
        saver = tf.train.Saver(tf.all_variables(), max_to_keep=21)
        summary_writer = tf.train.SummaryWriter(FLAGS.train_dir)

        # restore or initialize variables
        ckpt = tf.train.get_checkpoint_state(FLAGS.train_dir)
        if ckpt and ckpt.model_checkpoint_path:
            saver.restore(sess, ckpt.model_checkpoint_path)
        else:
            sess.run(tf.initialize_all_variables())

        # Start the queue runners.
        tf.train.start_queue_runners(sess=sess)

        start = sess.run(global_step)
        for step in xrange(start, FLAGS.max_steps):
            start_time = time.time()
            _, loss_value = sess.run([train_op, loss])
            duration = time.time() - start_time

            assert not np.isnan(loss_value), 'Model diverged with loss = NaN'

            print '%d: %f (%.3f sec/batch)' % (step, loss_value, duration)

            if step % 100 == 0:
                summary_str = sess.run(summary_op)
                summary_writer.add_summary(summary_str, step)
            if step % 500 == 0 or (step + 1) == FLAGS.max_steps:
                checkpoint_path = os.path.join(FLAGS.train_dir, 'model.ckpt')
                saver.save(sess, checkpoint_path, global_step=step)

cifar10.inferenceにバッチ入力を渡すことで畳み込みニューラルネットワークモデルとその出力が作られ、cifar10.lossにその出力と正解ラベルを渡すことで誤差を計算、それをcifar10.trainに渡すことで学習が行われる。分かりやすい。

いくつかの定数値は上書きして変更することができ、ここでは

  • distorted_inputsでは32x32サイズの画像をさらにランダムに24x24サイズで切り出して入力としていたが、今回は顔画像領域に既に切り出されているし不要と判断し32x32そのまま入力とする
  • 分類数はメンバー5人+それ以外、で6種類に
  • 何度か学習を試してみたが途中でlossが発散してしまうことがあったのでINITIAL_LEARNING_RATE0.1から0.08に少し下げた

など。

入力画像の種類が少ないこともあってか、数千stepでもう十分なくらいに学習が進む。VPS上でDocker立ち上げて回してみていたけど数時間で10000stepの学習が終了した。

f:id:sugyan:20160108201508p:image

で、学習に使っていない評価用テストデータを使って正答率を計測してみたところ

f:id:sugyan:20160108201509p:image

のようになり、75%くらいまで到達した後 それ以上はもう上がらないようだった。


学習結果を使ったWebアプリ

せっかくここまで作ったのなら、前回のように実際に誰でも試せるようにWebアプリにして公開してみよう、と。

f:id:sugyan:20160112085108g:image

TensorFlowでのMNIST学習結果を、実際に手書きして試す - すぎゃーんメモときと同様に、学習済みのデータを使って画像を受け取り判定結果を返すJSON APIを用意し、それを使って判定結果を描画する。

任意の画像をDrag and Dropで受け取るので、まずはそこから判定するための顔領域だけを切り出す必要があり、自作の顔検出器では遅すぎるので ここではLIMITED PREVIEW版の Cloud Vision API を使ってみている。

色んな画像を上げて試してみてください。

https://momoclo-face-recognizer.herokuapp.com/


考察

冒頭の画像では玉井さんが高城さんと誤判定されている以外は当たっている。テストデータでの評価は75%程度だしまぁこんなものかと。

実際色んな画像で試してみるともっと残念な結果になるものの方が多いかんじ。実用的なレベルにはまだまだ達しない。

  • 訓練用の画像1000枚ではまだまだ足りていないのかも?画質の良くない自撮りや変顔なんかも多いし、もうちょっとバリエーションがあった方が良さそう
  • 入力32x32では小さすぎる、というのもある?さすがにそのくらい縮小されると自分で目視しても分かりづらかったりするし誤判定されても仕方ない気はする
    • あと顔領域の切り出し方によっても結構判定結果が変わるようだったので、やはりランダムcropは使った方が良かったのかも
  • ニューラルネットワーク自体が単純すぎる?今回のtensorflow.models.image.cifar10パッケージのものだと2階層だけの畳み込み-プーリングでそこから全結合のものとなっている。もうちょっと深いものだとまた精度が変わったりするだろうか?

色々ためしてみたいところではある。他のアイドルさんの画像も集めて分類数も増やしていきたい。


Repository

2015-12-26

AOJはじめました

「AIZU ONLINE JUDGE」通称(AOJ)という、"提出されたプログラムの正しさ・効率の自動判定を行うオンラインジャッジシステム"がある。

いわゆる競技プログラミング・プログラミングコンテストの過去問題などが多数掲載されており、各問題に対してソースコードを提出すると その問題の入力に対する正しい出力が得られているか否かを自動で判定してくれる。


…ていうのを何となくは知っていたのだけど実際に触ったことはなくて。先日 チームラボVSドワンゴ!競技プログラミング勉強会@ドワンゴオフィス - connpass というイベントに参加したときにオンラインジャッジに関する解説などがあり 実際に数問やってみる、ということでユーザ登録して挑戦してみたので、その後も継続して挑戦してみることにした。


べつに競技プログラミングで強くなりたい、とかではなく 主に「思考力・実装力を鍛える」という目的で、特に早解きやコードゴルフ的なことは意識しないことにした。

方針としては

  • まずはC++で頑張って自力で解く。
  • グローバル変数はできるだけ使わず、関数の入出力で回答を生成できるように。
  • どうにも上手くいかないときは他人のコードを見たりググって調べたりしても良い。
  • でも最終的にはちゃんと自分でコードを書く。
  • 解けても、もっと良いやり方がありそうであればリファクタリングする。
  • ついでにRubyでも解いてみる。
  • コードコメントは書かないが、考え方をメモしてgithubに上げる

という感じでやってみている。ようやく10問くらいできたところ。

https://github.com/sugyan/aoj


問題は山ほどあるけど、1番目から順番に…というのもアレなので ランダムで問題を選択するスクリプト を適当に作って、それで出てきたものに挑戦する、ようにしている。

とりあえず問題だけ読んで、移動中の電車の中で実装を考えて ちょっと気分転換するタイミングで実際にコードを書いてみたり。それくらいの気軽さで。


どの問題も数十行くらいで解けるようなものなのだけど、実際にやってみるととにかく予想外に詰まることが多くて、自分の力の無さを痛感する。他の人の回答を見て目から鱗、なことも多い。あとC++で書いたものをRubyに移植してみると すごく短く簡潔に書けたり すごく処理時間が増大したりするのを実感できて面白い。

週に2〜3問くらいのペースかな、とりあえず出来るだけ続けていきたいと思ってるけど もうちょい継続するモチベーションが欲しい気もするw 身近で同じ問題に挑戦したりレビューしあえるような仲間がいると良いのかなぁ