TOKYO IDOL FESTIVAL 2016 のタイムテーブル画像化ツールを作った

日本最大規模のアイドルの祭典・"TOKYO IDOL FESTIVAL"(通称TIF)。

今年は明日からの8/5〜8/7の3日間の日程で、始まります。

TOKYO IDOL FESTIVAL 2016

で、こういったフェスってステージが複数あって観たいステージが幾つもあるなか、どの時間にどれを見るか とか決めるのが大変で、そういうのを解決するためのツールみたいなのを多くの人たちが作っていたりしますね。 今回のTIF 2016でも幾つかそういうのが有志によって公開されています。

今年はメインのタイテ情報がJSONで取得できるようになっていたのでこういったツールも作りやすい環境になっていたかと思います。

で、自分も何か作ろうかな…と思って

自分の場合、タイテ情報ってけっこう画像をそのままダウンロードしたりWebで載ってるものをスクリーンショットで保存して使うことが多くて。ネットワークの繋がらないところでもすぐに確認できるのが便利なので。

なので自分で組んだ自分だけのタイテを一枚絵の画像でダウンロードできるようなのがあればいいな、と思ったのでそういうのを作ってみた。

条件を指定して絞り込んで表示した一覧から自分が行きたいものだけを選択すると、それだけを抽出してこういった画像を生成する、というだけのもの。

↓例 f:id:sugyan:20160804230856p:plain

技術的にも難しいことは特にしてなくて

  • サーバサイド: RailsJSON APIを用意
    • 公式タイムテーブル情報は定期タスクでfetch & parseして整形しキャッシュに突っ込んでおいてそれを読むだけ
    • 選択したものを元にRMagickでテキスト描画しつつ画像を生成
  • クライアントサイド: ReactでUI生成
    • データは全件取得した上で絞り込みによる表示変更
    • 生成結果の画像を表示切り替えで雑にSPAっぽく

という感じ。

Repository : https://github.com/sugyan/tif2016-mytt

JSのビルドには最近webpackを使うようにしていて、開発時にはwebpack-dev-serverを使った。

などを参考にして、webpack-dev-server --inlineなどでソース変更時に再ビルドとリロードが自動で走るようにしつつ配信しておき、Rails側では

module ApplicationHelper
  def javascript_include_tag(*sources)
    if Rails.env.development?
      opts = {
        src: '//localhost:8080/javascripts/main.js'
      }
      return content_tag(:script, '', opts)
    end
    super(*sources)
  end
end

みたいな感じで開発時のみJSがそっちを向くように設定したら捗った。

Asset Piplineを使わずに/public以下に直接成果物を配置すればいいかな、と思ったけど本番の更新時にキャッシュとか制御できるか不安だったので結局一度/assets以下に吐いてAsset Piplineでfingerprint付きのpathで配信するようにした…。


と、こんな感じで作ったアプリをHerokuにdeployしてちょっとお知らせしてみたところ 色んな界隈のヲタクの方々から400RTくらい拡散していただいて。どれくらいPVあったかは分からないけど 使ってくれたヒトが作った画像を載せてくれて「それどうやって作ったの?」「ここから!」みたいにTwitter上でクチコミで広まったりして、思っていた以上に使ってもらえて、作った甲斐あったわー という感じでとても嬉しい。

「TensorFlowはじめました」を読んだ

著者の有山さんとは、TensorFlowでの独自の画像データセットの分類に取り組む同士(?)として勉強会などでお話する機会があり、そんな縁もありまして有り難いことに献本ということで読ませていただくことができました。

第1章の「TensorFlowの基礎」では最初にまずデータフローグラフの「構築」と「実行」で分かれているという概念について、丁寧に説明されていてとても良かったです。いきなり何も知らずに公式Tutorialだけ始めていた自分は、こういう概念について理解するのが遅かった…。

第2章ではCIFAR-10の学習モデルと評価。公式Tutorialの英語を問題なく読めて ある程度のCNNの知識があれば困らないかもしれないけど こうやって日本語で解説されているものがあるというのは(数ヶ月前の自分のように)新しく始める人にとってはとても有り難いだろうなと思います。

第3章ではもうちょっと踏み込んで、TensorFlowでのデータ保存・読込や可視化について。

第4章「CIFAR-10奮闘記」がとても面白かった。 モデル構成や学習データを変えつつ評価結果を確認し、と試行錯誤を繰り返しながら少しずつ正答率を上げる方法を模索していく。自分もアイドル顔識別でそういった試行錯誤を続けていたので とても共感できる内容でした。何をどうすれば正答率が上がる、ってなかなか感覚的な部分もあったりして 最良の道はそう簡単には見つけだせないんですよね…。


というわけで新たにTensorFlowを使って機械学習・画像分類とかをやってみたい、というヒトにはとてもオススメの1冊でした。本当にあと数ヶ月はやくこれが出ていれば僕も最初の躓きが少なかっただろうに…!

巻末の参考文献に私の「すぎゃーんメモ」も載せていただいていて、非常に光栄であります。 この先も続く挑戦の道のり、楽しみにしています!ありがとうございました!!

TensorFlowで顔識別モデルに最適化した入力画像を生成する

f:id:sugyan:20160710002948p:plain

動機

elix-tech.github.io

の記事を読んで、「可視化」の項が面白いなーと思って。 引用されている図によると、人間の目にはまったく出力クラスとは関係なさそうに見える画像でもCNNによる分類器は騙されてしまう、ということのようだ。

なるほど分類モデルの方を固定しておいて入力を変数として最適化していけば任意の出力に最適な入力を得ることができるのか、と。 自分でもやってみることにした。

分類モデル

TensorFlowによるDeep Learningでのアイドル顔識別モデルの性能評価と実験 - すぎゃーんメモ の記事で使ったモデルとデータセットで、ここではCross Validation用にデータを分けずに7,200件すべてを学習に使い20,000 step進めたものを用意した。

このモデルは学習したアイドルたちの顔画像に対してはかなりハッキリと分類できるようになっていて、試しに幾つかを入力して得た分類結果の上位3件をtf.nn.top_k(tf.nn.softmax(logits), k=3)で出力してみると

・例1: エルフロート・マアヤさん (label_index: 10)

f:id:sugyan:20160705200332j:plain

[0.9455398321151733, 0.016151299700140953, 0.013260050676763058] [10, 38, 7]
[0.9314587712287903, 0.02145007625222206, 0.0140310600399971] [10, 7, 38]
[0.9718993306159973, 0.0045845722779631615, 0.0037077299784868956] [10, 2, 17]
[0.9961466789245605, 0.001244293642230332, 0.0008690679096616805] [10, 7, 31]
[0.9985087513923645, 0.0003244238905608654, 0.0003135611186735332] [10, 30, 7]

・例2: フラップガールズスクール・横山未蘭さん (label_index: 13)

f:id:sugyan:20160705200355j:plain

[0.9963579773902893, 0.0019185648998245597, 0.0008565362659282982] [13, 20, 25]
[0.9986739158630371, 0.0006054828991182148, 0.00040348240872845054] [13, 19, 31]
[0.9996882677078247, 0.00011850777082145214, 6.301575194811448e-05] [13, 31, 20]
[0.9860101938247681, 0.006886496674269438, 0.0037682976108044386] [13, 19, 20]
[0.9992870688438416, 0.0002755637979134917, 0.00010664769797585905] [13, 19, 20]

・例3: じぇるの!・針谷早織さん (label_index: 24)

f:id:sugyan:20160705200406j:plain

[0.9933986663818359, 0.004436766263097525, 0.0004516197368502617] [24, 2, 36]
[0.9997298121452332, 6.973237032070756e-05, 5.891052205697633e-05] [24, 8, 2]
[0.9980282187461853, 0.000929205387365073, 0.000297865248285234] [24, 2, 36]
[0.9958142638206482, 0.0027367006987333298, 0.0004832764097955078] [24, 21, 20]
[0.991788923740387, 0.002572949742898345, 0.0013722123112529516] [24, 2, 26]

という具合に、正しいindexの番号の出力が0.9以上になるくらいのものとなっている。

このモデルを騙して誤識別させるような画像を生成する、というのが今回のテーマ。

inputs

今回は入力画像が変数となるので、そのサイズ(今回の場合は96 x 96 x 3)の変数を用意する。取り得る値の範囲は0.0 - 1.0とする。

import tensorflow as tf

with tf.variable_scope('input') as scope:
    v = tf.get_variable('input', shape=(96, 96, 3), initializer=tf.random_uniform_initializer(0.0, 1.0))

inference

学習済みのモデルにこの変数を入力として与え、分類結果を得る。 ただ、元々このモデルはJPEGなどの画像から復元した0 - 255の値をとるtf.uint8Tensortf.image.per_image_whiteningによって変換したものを入力として取るようにしていたので、それに従って同じように変換する。

image = tf.mul(tf.clip_by_value(v, 0.0, 1.0), 255.5)
input_image = tf.image.per_image_whitening(image)
# 以前までの記事で使っていた識別器。今回はbatch sizeを1とする
r = Recognizer(batch_size=1)
logits = r.inference(input_image)

loss

上記で得られた結果と、「理想とする出力」の差分が今回の損失の値になる。 単純に引き算なんかでも良いかもしれないけど、分類モデルの学習と同様にCross Entropyを使ってみることにする。

losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits, [FLAGS.target_class])

train

定義したlossを最小化する手続き。これも適当にAdamOptimizerを使っておく。 学習によって値を更新していくのは入力変数vのみ。

train_op = tf.train.AdamOptimizer().minimize(losses, var_list=[v])

replace tf.image.per_image_whitening

これだけで学習させていけるかなーと思ったのだけど、実際にSession作ってtrain_opを実行してみると

...
tensorflow.python.framework.errors.InvalidArgumentError: We only handle up to Tensor::dims() up to 8, not 0
         [[Node: gradients/Relu_grad/ReluGrad = ReluGrad[T=DT_FLOAT, _device="/job:localhost/replica:0/task:0/cpu:0"](gradients/Sqrt_grad/mul_1, Relu)]]
Caused by op 'gradients/Relu_grad/ReluGrad', defined at:
...

みたいなエラーが出てしまう。 よく分からないのだけど、入力に変数を使っているときにReluがあると傾きを計算できないの…?TensorFlowのバグなんだろうか、、

tf.image.per_image_whiteningがやっているのは画像の各画素値から平均値とか分散とか求めて引いたり割ったりしているだけなので、そのあたりのソースを参考にtf.nn.reluを使っているところだけ除外して

# image = tf.image.per_image_whitening(image)
mean, variance = tf.nn.moments(image, [0, 1, 2])
pixels = tf.reduce_prod(tf.shape(image))
stddev = tf.sqrt(tf.maximum(variance, 0))
input_image = tf.sub(image, mean)
input_image = tf.div(input_image, tf.maximum(stddev, tf.inv(tf.sqrt(tf.cast(pixels, tf.float32)))))

と書き換えてみたら無事に学習できるようになった。

結果

これで学習を進めていくと、1000 step程度でlossは十分に減少し、softmaxの目標indexの出力は0.999を超えるくらいになる。

$ python optimal_inputs.py --target_class 10
0000 - loss: 4.388829 (0.012415)
0001 - loss: 4.170918 (0.015438)
0002 - loss: 3.950892 (0.019238)
0003 - loss: 3.728565 (0.024027)
0004 - loss: 3.509432 (0.029914)
0005 - loss: 3.291546 (0.037196)

...

0997 - loss: 0.000944 (0.999057)
0998 - loss: 0.000942 (0.999058)
0999 - loss: 0.000941 (0.999060)

で、この学習終了後の変数vを画像として出力してみると。

output_image = tf.image.convert_image_dtype(v, tf.uint8, saturate=True)
filename = 'target-%03d.png' % FLAGS.target_class
with open(filename, 'wb') as f:
    f.write(sess.run(tf.image.encode_png(output_image)))

(ちなみにこういったランダム要素の多い画像をjpeg出力するときはchroma_downsamplingオプションをFalseにしないとかなり情報が落ちてしまうようなので注意。結構ハマった)

f:id:sugyan:20160709224905p:plain

いちおう拡大してみると

f:id:sugyan:20160709225048p:plain

にわかには信じがたいけど、このデタラメ模様にしか見えないような画像が 今回の分類器に最適化された入力画像、となる。 実際にこの画像から分類器にかけてtf.nn.top_k(tf.nn.softmax(logits), k=3)を出力してみると、

with open('target-010.png', 'rb') as f:
    img = tf.image.decode_png(f.read())
img.set_shape([96, 96, 3])
inputs = tf.expand_dims(tf.image.per_image_whitening(img), 0)
logits = r.inference(inputs, FLAGS.num_classes)
softmax = tf.nn.softmax(logits)

...

with tf.Session() as sess:
    print(*[x.flatten().tolist() for x in sess.run(tf.nn.top_k(softmax, k=3))])

結果は

[0.9992352724075317, 0.00031209649750962853, 0.00014285057841334492] [10, 31, 18]

となり、確かにindex 10のものが0.999以上の値になっている。 つまり、この分類器からすると

f:id:sugyan:20160709235237j:plainf:id:sugyan:20160709224905p:plain

どちらも非常に高い確度で同じ人物だと認識する、ということになる。マジかよ。

こんなデタラメ模様のどこに特徴が隠れているんだ…。

実験

ちなみに一応、他のindexに最適化された画像もそれぞれ生成してみた。

f:id:sugyan:20160710001524p:plain

という感じで、どれもそれぞれ対応するindexに対し0.999くらいのsoftmax出力になる画像なのだけど、人間の目にはデタラメ模様にしか見えない。

ということは、ランダムな値ではなく別の画像を初期値として そこに人間の目には分からないような最適化を加えたものを作ることもできるわけで。 先ほどのマアヤさん (label_index: 10)の画像を変数の初期値として、異なるindexに最適化された画像を作ると

フラップガールズスクール・横山未蘭さん (label_index: 13)

f:id:sugyan:20160710002258p:plain

[0.9998733997344971, 4.3967520468868315e-05, 2.7816629881272092e-05] [13, 20, 31]

・じぇるの!・針谷早織さん (label_index: 24)

f:id:sugyan:20160710002305p:plain

[0.9999223947525024, 1.816232907003723e-05, 1.359641919407295e-05] [24, 4, 2]

というように、ちょっとしたノイズが載っているだけのように見えるけれど 分類器の結果は完全に別の人物としての高確度の識別をしてしまう。 不思議〜〜〜。

実際、顔画像を収集している中でたまにOpenCVによる顔検出で誤検出された壁紙の模様とか「まったく人間の顔ではないもの」が たまに高確度であるアイドルさんとして識別されることがあったりして不思議に思っていたけど、こういう結果を見るとまぁ起こり得るんだろうなぁと納得できる。むしろこんな意味わからん誤認識をするくせに94%とかの精度が出る方が不思議だわ…。

Source code

github.com

EC2のGPU instanceで Ubuntu 16.04 + TensorFlow 0.9.0 の環境をつくる

memo.sugyan.com

の続き(?)。 この記事を書いたところ、「Ubuntu 16.04でもこうすれば簡単にCUDAインストールできるよ」とアドバイスをいただきました。ありがとうございます。

qiita.com

というわけで これを使ってやってみた。

g2.2xlargeで、Ubuntu 16.04 LTS (Xenial Xerus)のAMIを使ってインスタンスを立ち上げ、上記記事の通りに操作するだけでCUDA 7.5, cuDNN 4がインストールされる。

あとは(自分はPython3.5を使うので)

$ sudo yum install python3-pip
$ sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0-cp35-cp35m-linux_x86_64.whl

とやるだけで TensorFlow 0.9.0 がGPUで動いた。やっぱりソースから自分でビルドする必要はなさそうだ。

EC2のGPU instanceでTensorFlow動かすのにもうソースからのビルドは必要ないっぽい?

めも。

aws ec2, g2-2xlarge, ubuntu14.04にtensorflowを導入する。 - Qiita などを参考にしてg2.2xlargeでの環境を作っていたけど、どうやら 0.8.0 くらいからは 「Cuda compute capabilityうんぬんの問題があるので TF_UNOFFICIAL_SETTING=1 ./configure指定してソースからビルドして」というところは必要なさそう…? 実際にCuda Toolkit 7.5、cuDNN v4だけ入れている状態から公式に配布されているpipパッケージでインストールして動かしてみたところ、tensorflow 0.7.1だとcompute capabilityがサポート外ってことでGPUが有効にならなかったが、0.8.0以降だと普通に動かせるっぽい。

Releases · tensorflow/tensorflow · GitHubだけ見てもよく分からないけど、0.7.1〜0.8.0の間の変更でそのへん対応されていたのかな…?

このへん?

github.com

最新のEC2 GPU instanceでのTensorFlowセットアップ記事はこのへんのようだ。

ただrunfileからCUDA 7.5をinstallする場合?はdriverが古いとhangしてしまうなど問題があるっぽいのでそこは注意する必要がありそう。

0.9.0が正式リリースされたらまた真っさらなUbuntuから環境つくりなおしてみようっと。

EC2 Spot Instanceの価格変動をターミナルでモニタリングする

というものを作った。こんな感じの。

f:id:sugyan:20160623232900p:plain

Repository : https://github.com/sugyan/spot-price-watcher

背景

TensorFlowを使ったDeep Learningでアイドルの顔識別なんかをずっとやってきたけど、やっぱりCPUマシンだけでやっていくのは厳しいな、と思い、

ということでAmazon EC2にてGPU instanceを使って動かすことにした。

g2.2xlargeインスタンスでTensorFlowを動かせるようにするのは、下記記事なんかを参考にしながらやってみたら何とか出来た。 ソースからビルドする必要があるのは時間かかってつらかったけど…

qiita.com

まぁ1回環境を構築してAMIを作ってしまえばその後は苦労せずに同じ環境を再現できるので便利ですね。

で、g2.2xlargeインスタンスを普通に作ると一番安いUSリージョンでも $0.65 per Hour かかるのだけど、これを出来るだけ節約したい。 今までEC2まともに使ったことなくて今回はじめて知ったのだけど、Spot Instancesというのがある。

スポットインスタンスでは、スポットインスタンスリクエストに入札価格を指定して、支払うことができるインスタンス時間あたりの価格を選択します。入札価格がその時点のスポット価格以上であれば、リクエストが受理されてインスタンスを実行できるようになります。このインスタンスの実行は、お客様がインスタンスを終了した時点と、スポット価格が入札価格を上回った時点のいずれか早い方までとなります。スポット価格は Amazon EC2 によって設定され、スポットインスタンス容量に対する需要と供給に応じて定期的に変動します。

製品の詳細 - Amazon EC2 スポットインスタンス - Amazon EC2 | AWS

これを使って、スポット価格が安い時間帯に実行すれば通常インスタンスよりかなり安く済ませられる。 ここ数日の大雑把な感覚だと、US regionでのg2.2xlargeだと $0.1 - $0.2 per Hour のときがあり、最大7〜8割お得になる計算。

ただこのスポット価格は刻一刻と変化しており、いつどのAvailability Zoneがどんな価格に変化するか分からないし、予測できない(これをDeep Learningで予測する、とかも課題としては面白いかもしれないw けどどんな要素が絡んでいるかもよく分からないしなぁ…)。

スポット価格が自分の入札価格を上回ってしまうとその時点でインスタンスは強制中断されてしまうので 中断されるのがイヤな場合はできるだけ高額の入札価格にしておけば良いのだけど、勿論そのスポット価格の分は課金されることになるわけで。

自分の場合は個人の趣味レベルでやってることだし アイドル顔認識モデルの学習なんかは数千step実行すれば十分で、g2.2xlargeインスタンスを使うとGPUの力で1stepあたり0.6秒弱、だから1時間あれば6000stepは進むのでそれくらいの間だけ動いてくれれば十分だったりする(CPUだと一晩以上かかっていた)。 たとえ最後まで終了する前に強制中断させられても「うーん残念。仕方ない」で割り切って またタイミングを見計らってやり直せばいいし、途中経過はちょいちょい書き出しているので実行中に様子を見つつそのcheckpointファイルを吸い出しておけば無駄になることは少ない。

なので、とにかく何も考えず「一番安くなっているもの」を使ってギリギリの入札価格で使いたい。 AMIさえコピーして転送しておけば使えるのでRegionもどこでも構わない。

で、EC2のWebコンソールからは「価格設定履歴」というのを開いて各Availability Zoneの価格の遷移を見ることができるのだけど、

f:id:sugyan:20160623234609p:plain

これだと

  • 選択しているRegionの中のAvailability Zoneでしか一覧できない
  • 高騰しているものも含めてすべて描画されて縮尺を変更できない
    • 最低価格付近だけを見たいのに…
  • 直近24時間より短い範囲で見られない

とか ちょっと不満があった。 何より、いちいちブラウザ開いてポチポチしないとチェックできないのは面倒。

  • ターミナルからコマンド一発で
  • 複数Regionで横断的に
  • 最低価格のあたりの直近の変動・値だけを見たい

と思ったので、そういうものを作った次第。

実装

まずコマンドラインからスポット価格の履歴を取得するのはAWS CLIで出来るし、

AWS コマンドラインインターフェイス | AWS

各言語から使うSDKも様々なものが公式から提供されている。

AWS のツール | AWS

どれを使うか迷ったけど、

  1. 各Regionから取得するのは並列で処理したい
  2. せっかくだからターミナル上にグラフを描画したい

1.はGoかNodeかな、という感じで 2.でbleesed-contribっていうのが以前話題になったのを思い出し、

https://github.com/yaronn/blessed-contrib

これを使ってみたかったというのと Goでも近いものでtermuiというものが実装されていたようだったけど こちらは複数データのLine Chartが未実装ということだったのでNodeを使うことにした。

最近ぜんぜんマトモにNode触っていなかったのでPromiseとかも初めて使ってみたくらいだったけど、勉強しながら書いてみました。 こういうのも公開されていて本当にありがたいです。

JavaScript Promiseの本

複数Regionから並列で各Availability Zoneの価格設定履歴を取得できれば、あとはそのデータを使って描画するだけ。 blessed-contribのバグも見事に踏んでしまったのでp-r送ったりもした。

せっかくなのでキー操作で縮尺・表示範囲を変えたり 最新データに更新できるようにしたりした。

f:id:sugyan:20160623232910g:plain

とりあえずこんなんで最新の価格と細かい値動きはターミナルから一覧できるようになって、便利。

もうちょっと汎用的に使えるように出来たかもしれないけど 多分こんなの自分しか使わないだろうし、ってことで対象RegionはUSの3つのみにしている(EU, APにもGPUインスタンスはあるけど常時高騰している)し 調べるのはg2.2xlargeの価格だけにしているし、AWS CLIのためのCredentialも既にaws configureとかで設定している前提で作っている。 ちょっと雑。

npmへのpublishは自重しておく。

TensorFlowによるDeep Learningでのアイドル顔識別モデルの性能評価と実験

以前から書いているDeep Learningによるアイドル顔識別の話の続き。

コツコツと顔画像収集とラベル付けを続けて、そこそこにデータが集まってきたので ここらでちゃんと性能評価をしてみよう、と。

データセットの作成

今回は、現時点で重複なく180件以上の顔画像が集まっている40人のアイドルを分類対象とした。

これらのアイドルに分類のラベルindexを振り(推してる順とかじゃなくてランダムにね)、それぞれから無作為に抽出した180件の顔画像をそれぞれラベルとセットでレコードを作り、シャッフルして30件ずつ6つのデータセットに分けて保存。

data-00.tfrecords
data-01.tfrecords
data-02.tfrecords
data-03.tfrecords
data-04.tfrecords
data-05.tfrecords

レコードは、以前の記事に書いた TFRecord 形式。 それぞれのファイルには 40人 × 30件 = 1,200 のデータが含まれることになる。画像サイズは112 x 112

中身を並べてみるとこんな感じ。

f:id:sugyan:20160611200418j:plain

まぁ40人、教室1クラスぶんの人数くらいだし、見慣れてる人が見れば簡単に見分けられますね。

作成したデータセットGitHubにも上げておきました。

Cross Validation での評価

学習データに対していくら正確に分類できても、過学習していて学習データ以外のものに対しては正確な分類ができない可能性もある。また学習データに偏りがあっては汎化性能は測れない。 そういったことが起きていないか確認し、精度を検証するための手法として"Cross Validation (交差検証)"というものがあるそうで。 ここでは K-fold Cross Validation と呼ぶようなやり方を試してみる。

上記のデータセットは合計7,200件の「顔画像・ラベル」のセットがあるが、各1,200件の6つのファイルに分けているので、これらを学習用:評価用で5:1に分割し

  • Case #0: data-01,02,03,04,05で学習し、data-00で性能評価
  • Case #1: data-00,02,03,04,05で学習し、data-01で性能評価
  • Case #2: data-00,01,03,04,05で学習し、data-02で性能評価
  • Case #3: data-00,01,02,04,05で学習し、data-03で性能評価
  • Case #4: data-00,01,02,03,05で学習し、data-04で性能評価
  • Case #5: data-00,01,02,03,04で学習し、data-05で性能評価

の6つの学習・評価パターンを作って それぞれ場合の結果を調べてみる。 どの場合も学習用データ6,000件/評価用データ1,200件(40人それぞれ学習用に150件、評価用に30件ずつ含まれることになる)となり、評価用データは学習データには無いものたちなので汎化性能が測れるし、もしデータセットに偏りがあればどこかのケースで異常な結果になったりするはず。

評価結果

評価対象の分類モデルは、以前の記事で作った、4層の畳み込み&プーリング層と3層の全結合層からなるモデル。 入力は 96 x 96 x 3 のカラー画像で、以前と同様にランダムで切り出したり色味を変えたりして128サイズのmini batchを作り、cross entropyにweight decayを加えたものをAdamOptimizerでminimizeするよう学習する。 これをそれぞれのCaseで8,000 stepまで進めた。

各stepでの、評価データに対する正答率は以下の通りとなった。

step Case #0 Case #1 Case #2 Case #3 Case #4 Case #5 AVERAGE
0 3.667 5.083 2.250 2.500 2.500 4.750 3.458
1000 87.417 84.583 84.667 80.000 83.750 84.167 84.097
2000 91.833 90.583 90.417 89.000 91.500 90.917 90.708
3000 93.000 90.750 92.667 89.833 93.083 90.833 91.694
4000 93.083 94.000 93.417 92.583 92.583 94.500 93.361
5000 95.167 94.417 94.583 91.250 94.417 95.333 94.195
6000 96.083 93.667 94.500 93.583 94.667 94.500 94.500
7000 95.667 94.250 94.667 92.750 94.500 94.833 94.445
8000 95.333 94.750 94.667 93.583 95.583 96.333 95.042

グラフにするとこんな感じ。

f:id:sugyan:20160611235633p:plain

100%付近を大きめにすると

f:id:sugyan:20160611235639p:plain

まぁ95%よりちょっと下くらいに落ち着いているかな、と…。まだもう少し続ければ上昇しそうな傾向ではあるけど、こんなところで(CPUマシンだとめっちゃ時間かかるしキツい…)。

考察

どのケースの場合も同様の正答率に落ち着いているし、特にデータセットに依存するものではなく汎化能力として94%程度の正答率が得られる、と言える、はず。 Weight Decayのおかげなのか、少なくとも8,000 step程度では特に過学習となって評価用データに対する正答率が落ちたりはしないようだ。

この「約94%」という正答率を十分高いと捉えるか、まだ低いと捉えるか。 もうちょっと入力データを弄ったり(random distortの範囲を広げる、とか)モデルの構造を改良したり(畳み込みカーネルサイズを変える、とか 畳み込み層をもう1段深くする、とか)で改善する余地はあるかもしれない。 どういうデータに対して上手く分類できて、どういうものに対し失敗しているのか、などの傾向を調べてみると何か分かるかもしれない。

けど、とりあえず「1分類あたり150件の学習データ」で94%くらいの汎化性能があるならまずは十分かな、と。 出来ることならもっと多くのデータと分類数で試したかったけど現時点で用意できたのがこれだけだったので、もっとデータ数が増えればまた結果も少し変わってくるだろうし。 その上でもっと正答率を高めたい、となったときにまた改善方法を検討したい。

実験

現状のモデルがこれくらいの精度だということが分かったので、ここで入力やモデルを色々と変えて正答率がどう変化するか、などを試してみる。 上述のCross Validationでデータセットの偏りは無い、と判断したので(資源の都合もあり)ここから先はCase #0のみで試した。

1. パラメータを減らす

以前の記事で作った分類モデルは、その後ちょっとパラメータ数を変えて

  1. 96 * 96 * 348 * 48 * 32 の畳み込み&プーリング層
  2. 48 * 48 * 3224 * 24 * 48 の畳み込み&プーリング層
  3. 24 * 24 * 4812 * 12 * 72 の畳み込み&プーリング層
  4. 12 * 12 * 726 * 6 * 108 の畳み込み&プーリング層
  5. 3888 (= 6 * 6 * 108) * 1024 の全結合層
  6. 1024 * 256 の全結合隠れ層
  7. 256 * <number of classes> の全結合出力層

としていた。が、本当にこれだけのパラメータが必要なのか?もっと減らしても大丈夫なのでは?という疑問があったので 試してみることにした。

1.1 全結合層のユニット数

今使っている分類モデルはTensorFlow Tutorial の CIFAR-10 Modelを参考にして作ったのだけど、そこでは"sparsity"という値をsummaryで出力している。

  tf.scalar_summary(tensor_name + '/sparsity', tf.nn.zero_fraction(x))

これはtf.nn.zero_fractionつまり出力Tensorのうち値が0となっているものの割合を示している。 全結合層においては出力0のユニットは次の層への入力に影響を及ぼさない(次の層との接続の重みの値が幾らであっても0 * w0になるだけ)ので、存在しなくても結果は変わらない、はず。 このsparsityは当然 学習の過程で上下していくのだけど、(5.)最初の全結合層と次の(6.)隠れ層での値の遷移をみてみると

f:id:sugyan:20160613173237p:plain

f:id:sugyan:20160613173244p:plain

となっていて、特に最初の全結合層では徐々に上昇して最終的に0.96くらいにまで上がる。 これはつまり学習が進むにつれてこの層で出力が必要なユニット数が減っていっていて ある程度まで進んだ段階では多くのユニットが0しか出力しなくなっている、ということ。 (5.)の全結合層では1024のユニットを用意していたけど、結局次の層の入力で使われるのは1024 * (1 - 0.96)41個程度、ということ。 こんなにたくさん用意する必要ないのでは…。てことで、(一応余裕を持たせて)96まで減らしてみる。

同様に(6.)の隠れ層も、256ユニット用意していたけれど そのうち0.75くらいは出力が0になる、ってことなのでこれも半分くらいまで減らしても問題なさそう、ということで128に減らす。

1.2 畳み込み&プーリング層のchannel数

前半の畳み込み&プーリング層の方も同様に無駄な部分あれば減らしたいところだけど、構造が全結合とは違うので単純にsparsityだけで判断するのは良くないかな、と。 こちらは2次元の出力ではなく12 * 12 * 726 * 6 * 108のように3次元から3次元への変換となるので、各層でそれぞれのchannelごとに出力を見てみることにした。

学習済みモデルにおける、ある入力画像に対しての各層での各channelの出力を適当に可視化してみた結果。

1層目

f:id:sugyan:20160613175908p:plain

2層目

f:id:sugyan:20160613175918p:plain

3層目

f:id:sugyan:20160613175929p:plain

4層目

f:id:sugyan:20160613175937p:plain

ちょっと分かりづらいけど、2, 3層目では幾つかの四角が真っ黒になっていて完全に出力が0になっている。 小さくて全然分からないけど4層目は特に、半分くらいのものが黒くなっていて108も要らなそうだったので前段の層と同じchannel数72に減らしてみた。

ということで

  1. 96 * 96 * 348 * 48 * 32 の畳み込み&プーリング層
  2. 48 * 48 * 3224 * 24 * 48 の畳み込み&プーリング層
  3. 24 * 24 * 4812 * 12 * 72 の畳み込み&プーリング層
  4. 12 * 12 * 726 * 6 * 108 の畳み込み&プーリング層
    • 6 * 6 * 72に削減
  5. 3888 (= 6 * 6 * 108) * 1024 の全結合層
    • 2592 (= 6 * 6 * 72) * 96に削減
  6. 1024 * 256 の全結合隠れ層
    • 96 * 128に削減
  7. 256 * <number of classes> の全結合出力層
    • 128 * <number of classes>に削減

という変更をしてみる。

結果

正答率の変化を 前述のCross Varidationの全ケース平均と比較すると

f:id:sugyan:20160614154655p:plain

となり、ほぼ同様の変化を示して最終的に94%程度まで上がった。sparsityの変化は

f:id:sugyan:20160614154709p:plain f:id:sugyan:20160614154721p:plain

となり、当然ながら分母が減っているので より小さな値で落ち着くようになった。

効果としては、別に精度が上がるわけではないけれど より少ないパラメータ数で同程度の分類性能を出せるようになる、ということ。 CPUマシンで試していた限りでは計算速度にはほとんど差異は感じられなかった(計算グラフには変化が無いから?)が、パラメータを保存・復元するcheckpointファイルのサイズは元々が67MBほどだったものが5.5MBと、1/10以下まで軽くなった。これはちょっと嬉しいかもしれない。

2. 色情報を落とす

入力にはRGBカラー画像を使っていたけど、これがもしグレースケールの画像だったらどうなるだろうか。

f:id:sugyan:20160614102323j:plain

自分で目視するぶんには、グレースケールでも輪郭や髪型、表情などの情報は十分に得られるし、顔としての識別はできる。 機械学習だとそのあたりはどうなるだろうか?

データセットから入力mini batchを作る際に、tf.image.decode_jpegJPEGバイナリから画像を復元するが、その際にchannelsオプションを1に指定することでグレースケール画像として読み込むことができる。

あとはdistort系の処理でhue, saturationを変化させるものはグレースケールには適用できないので省くことになるが、これだけの変更で入力を[batch_size, height, width, 1]Tensorに変更できる。

             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.image.decode_jpeg(features['image_raw'], channels=1)
             image = tf.cast(image, tf.float32)
-            image.set_shape([Recognizer.IMAGE_SIZE, Recognizer.IMAGE_SIZE, 3])
+            image.set_shape([Recognizer.IMAGE_SIZE, Recognizer.IMAGE_SIZE, 1])

             # distort
-            image = tf.random_crop(image, [Recognizer.INPUT_SIZE, Recognizer.INPUT_SIZE, 3])
+            image = tf.random_crop(image, [Recognizer.INPUT_SIZE, Recognizer.INPUT_SIZE, 1])
             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)
+            # image = tf.image.random_hue(image, max_delta=0.04)
+            # image = tf.image.random_saturation(image, lower=0.6, upper=1.4)

分類モデル側は最初の畳み込み層のweightが変わるだけ。

         with tf.variable_scope('conv1') as scope:
-            kernel = tf.get_variable('weights', shape=[3, 3, 1, 32], initializer=tf.truncated_normal_initializer(stddev=0.08))
+            kernel = tf.get_variable('weights', shape=[3, 3, 3, 32], initializer=tf.truncated_normal_initializer(stddev=0.08))
             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)

これで学習させてみる。

結果

正答率の変化を 前述のCross Varidationの全ケース平均と比較すると

f:id:sugyan:20160614154738p:plain

と。 カラー画像の場合に対しそれほど大きく劣ることなく正答率が出せているようだ。もう少し追試すると有意差がハッキリ出るかもしれないけど…。

まぁ既にカラー画像のデータが用意できているのならわざわざ色情報を落とす必要は無いだろうな、とは思うけれども。 例えば学習データを集める段階からデータサイズを小さく済ませるよう設計したい、とかの場合に これくらいの顔識別タスクに対しては必ずしもカラー画像で用意しなくても それなりの精度は出る、と言えるかもしれない。

最初の畳み込み層へのchannel数が減っているのとdistortの処理が少し減っている影響か、計算時間はカラー画像の場合よりは少し短縮されていたようだった。これはこれで利点かもしれない。

3. 画像サイズを縮小する

ここまではすべて96 x 96サイズに切り出したものを使ってきたけれど、もっと縮小されると どれくらい精度に影響が出るだろうか?

80 x 80

f:id:sugyan:20160614161201j:plain

64 x 64

f:id:sugyan:20160614161210j:plain

48 x 48

f:id:sugyan:20160614161218j:plain

32 x 32

f:id:sugyan:20160614161225j:plain

感覚的には、64 x 64サイズくらいなら96 x 96とそんなに変わらない感じで見分けられそうで、48 x 48だとちょっと小さくて分かりづらいなー、32 x 32はなかなか厳しそうだ…という感じ。

画像のリサイズはtf.image.resize_imagesでmini batch丸ごと変更できるので、それぞれの場合に1行加えるだけで試すことができる。

モデル定義の方はまったく変更する必要がない。

結果

各入力サイズでの正答率の変化を 前述のCross Varidation全ケース平均と比較すると

f:id:sugyan:20160614163537p:plain

となった。 最も小さい32 x 32でも一応85%弱くらいまでは上がる。48 x 48以上だと90%超えるくらいまでは上がるようだ。 当然ながら縮小しない96 x 96のものが最も精度が高いが、64 x 64くらいに縮小しても致命的なほど精度が下がるわけではない、と言える ような気がする。

サイズ縮小は計算時間には結構大きく影響していて、32 x 32なんかだとCPUマシンでもサクサクと計算が進んだので、どうしても計算量を減らして早く結果を得たい!というときにはこういう縮小もアリかもしれない。

まとめ

  • アイドル顔識別に使えるデータセットを作成した
  • 自作した分類モデルを評価して、94%程度の正答率を得た
  • パラメータを減らしても同程度の精度のものが出来ることを確認した
  • 他にも色々と試して遊んでみた

顔画像データが蓄積されて より大きなデータセットが作れたら再び評価してみたいし、他に何か試したいことを思い付いたら挑戦してみようと思います。

Repository : https://github.com/sugyan/tf-face-recognizer/tree/experiment