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