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

以前に試した、アイドル顔識別の性能評価。

memo.sugyan.com

それから半年以上も経ってデータ数も増えたし ちょっと確かめたいこともあったので、再び試してみた。

新データセット

前回は 40人×180件 で 計7,200件 を用意したけど、今回はもう少し多めにデータが集まっていたので(卒業などでもうアイドルではなくなってしまった子も居るけど…)、今回は 120人×200件 で 計24,000件 を抽出してデータセットを作成した。

実際にラベル付けしたデータから抽出してみると、元が同じ画像なのに加工や顔検出器のブレなどで別の顔画像として登録されてしまっているもの、明らかに同じ日・同じ場所で連写していて「ほぼ同じ顔画像」と思われるもの などの重複が結構あることに気付いて、頑張って出来る限り排除した。

前回もある程度は人力でチェックしていたけど、今回は学習済みモデルに食わせた中間層出力の分布距離の近いもの、などを調べて機械的に検出した。それでも完全には排除できていないかもしれないけど…。

中身をザッと眺めるとこんな感じになる。

f:id:sugyan:20170220233000j:plain

120人。ほぼ自分でラベル付けしたものなので僕はだいたい見分けがつくけど、普通の人にはちょっと難しいかもしれないですね。

果たして自作の分類モデルはどれだけ見分けられるようになるのか。

Cross Validation で改めて評価

今回は用意したデータセットを5つに分割し、120人それぞれの160件ずつを学習に用い 40件ずつを評価に用いる、という分け方で前回同様に複数パターンで試してみた。 ちょっと時間と金がかかるので使ったのは5パターン全部ではなく3パターンだけ…

前回と同じ構造・同じパラメータ数のモデルを使って学習させ、評価データにおける正答率の変化を調べた。

f:id:sugyan:20170221205722p:plain

右上の方を拡大すると

f:id:sugyan:20170221205727p:plain

40,000 stepくらいで93%くらいには到達するものの、そこから先は続けても94%には到達せず…というところ。 前回のときより僅かに下がったか…?

lossに使っているcross entropyはある程度まで確実に減少している。

f:id:sugyan:20170221205732p:plain

まぁ重複精査したのもあるし、1人あたりの学習件数はそんなに変わらないのに3倍の分類数になっても同程度の精度は出ている、ってことで。

学習時の入力画像distortion再考

今のモデルは、CIFAR-10用のモデルを参考に 入力画像は96x96のサイズだがデータセットは一回り大きく112x112のサイズで用意しており、そこからtf.random_cropを使ってランダムに96x96の領域で切り取って使う、という形式 (評価時は常に中央部分を切り取って使う)。 なので、データセットに使う顔画像収集・管理ツールでも 検出された顔領域が96x96に収まるサイズに拡大・縮小した上で、そこを中心として周辺を含む112x112で切り取ってデータセット用に保存している。

しかし CIFAR-10のような一般分類タスクと違って、この顔識別タスクでは一応分類すべき領域が既に分かっているわけで (一応、というのは 自作顔検出器の精度がそれほど高いわけではなく実際にはそれほど正確に中央の96x96領域に顔がすっぽり収まっているデータばかりではない、というのがある)。 このtf.random_cropを利用せずに学習時も常に中央の領域だけを使う、としたときにどれだけ精度が落ちるか(もしくは意外と落ちないのか)も検証してみた方が良かったかな…(今回はしていない)。

で、違う方法としてtf.image.sample_distorted_bounding_boxを利用できると思ったのでやってみた。 これは物体位置特定タスク(object localization)などの学習なんかに使うためのもののようで、「与えられた画像サイズ(shape)」に対し「指定した矩形領域(bounding boxes)」の領域を一定以上の割合で含む新しい領域をランダムで決定し、それを切り取るための情報を出力してくれる。

つまり[height, width, channels][112, 112, 3]の画像に対し [y_min, x_min, y_max, x_max][8.0/112.0, 8.0/112.0, 104.0/112.0, 104.0/112.0]顔部分があるはずの矩形領域を指定することで、その領域を必ず指定した割合以上含むランダムな領域を得られる。 割合は1.0を指定すれば必ずその指定した矩形以上の大きさの領域が得られるが、それでは全体的に大きく切り取り過ぎてしまうためtf.random_cropのときに最も外れて切り取られた場合と同等の(80.0*80.0)/(96.0*96.0)の値を指定することにした。

縦横比もレンジを指定して制限することができ、デフォルトでは3:4-4:3となっているが 今回の顔画像データセットにおいては縦横の歪みは殆ど無いはずなので9:10-10:9と指定した。 最終的にはこうして得られたランダム領域で切り取った僅かに歪んだ画像を96x96にresizeして学習時の入力とする。

    bounding_boxes = tf.div(tf.constant([[[8, 8, 104, 104]]], dtype=tf.float32), 112.0)
    begin, size, _ = tf.image.sample_distorted_bounding_box(
        tf.shape(image), bounding_boxes,
        min_object_covered=(80.0*80.0)/(96.0*96.0),
        aspect_ratio_range=[9.0/10.0, 10.0/9.0])
    image = tf.slice(image, begin, size)
    image = tf.image.resize_images(image, [96, 96])

例として

f:id:sugyan:20170221211750p:plain

こんな112x112の画像を使ってdistortionをかけてみる。濃い色の部分が顔部分が入っているべき中央の96x96の領域となる。

従来のtf.random_cropを使った場合は

f:id:sugyan:20170221211757j:plain

という感じで、tf.image.sample_distorted_bounding_boxを使った場合は

f:id:sugyan:20170221211803j:plain

という感じになる。

同じ縮尺で場所だけ変えて切り取る従来のものより、様々なサイズのブレを吸収して学習できそうに思える。

結果

従来のtf.random_cropを適用させていた部分をtf.image.sample_distorted_bounding_boxを適用するよう書き換えて 同様にそれぞれ3パターンのデータセットで学習させ、評価データにおける正答率の変化を調べた。 結果は…

f:id:sugyan:20170221205739p:plain

んん…?

3パターンそれぞれの平均値を取って元のものと比較して…

f:id:sugyan:20170221205747p:plain

!!!特に大きな変化なし!!!

むしろ92〜93%くらいに到達するまでが遅くなってしまっている(計算時間は大きな違いは無さそうだった)。

うーん、distortionの方式はあんまり関係なかったか… もうちょっと明らかな改善になることを期待していたのだけど…

誤答の詳細を調べる

では93%程度の精度が出ている場合、残りの7%はどんな入力に対して誤答をしてしまっているの?

あるデータセットで誤答した画像を幾つか抽出してみた。

f:id:sugyan:20170222014440j:plain

簡単な傾向としては、

  • 眼鏡や手指などで隠れてしまっている部分があるもの
  • あまり正面向きではないもの、傾いてしまっているもの
  • 目を瞑っているもの

なんかはやはり間違いやすいのかな…。

実験: 正答評価方法の変更

前述のように顔検出器の精度がそれほど良くないために単純に変な大きさで切り取られているっぽいものも幾つかあり、それらは評価の微調整で解決できそうな気もした。

評価時はデータセット112x112の画像からtf.image.resize_image_with_crop_or_padを使って中央部分96x96を切り取って分類器への入力としていたけど、同時に88x88, 104x104の、「一回り小さく切り取ったもの」「一回り大きく切り取ったもの」も用意し、それぞれ96x96にリサイズして3種類の画像を分類器に入力する。 その3種類の入力に対する出力で「最大の値を得たもの」を分類結果として使うとどうだろうか。

image = tf.image.decode_jpeg(...)
image1 = tf.image.resize_image_with_crop_or_pad(image, 88, 88)
image1 = tf.image.resize_images(image1, [96, 96])
image2 = tf.image.resize_image_with_crop_or_pad(image, 96, 96)
image2 = tf.image.resize_images(image2, [96, 96]) # これはリサイズする必要ないのだけど
image3 = tf.image.resize_image_with_crop_or_pad(image, 104, 104)
image3 = tf.image.resize_images(image3, [96, 96])
inputs = tf.stack([
    tf.image.per_image_standardization(image1),
    tf.image.per_image_standardization(image2),
    tf.image.per_image_standardization(image3)
])
logits = tf.nn.softmax(model.inference(inputs))
values, indices = tf.nn.top_k(logits)

という形で3つの画像をまとめて分類器に入力し、その結果のtf.nn.top_kを取ると それぞれの分類結果の値とインデックスが得られる。 通常はこのindicesの真ん中のもの、すなわち96x96を入力した場合の出力のインデックスが分類結果として 正解ラベルと合っているか否か、を見るのだけど、 もしかすると他のサイズで切り取ったものの方が 違う結果としてより高いスコアを出しているかもしれない。

_, top_value_indices = tf.nn.top_k(tf.transpose(values))
answer = tf.gather(indices, tf.squeeze(top_value_indices))

ということでtf.nn.top_kから得られたvaluesを転置させて最大の値を出力しているのが何番目かを求め、indicesからそれを抽出する。 これを分類結果のラベルとして使って正解と照らし合わせる。 勿論96x96のもので最大の出力が得られていればそのまま使われるし、それが微妙な値で誤答していても他のサイズで高い値の正解を導く可能性がある。

という形で評価してみたところ、あるデータセットで学習したモデルで評価用データに対し

4527/4800 (94.312 %)

から

4573/4800 (95.271 %)

と、正解率が僅かに上昇した。さらに細かく92x92100x100も加えて5種類に入力を増やすと4584/4800 (95.500 %)に。

…しかしまぁ確かにある程度は正解が増えたけど 分類に余計な計算の手間がかかるだけだし、どちらかというと顔検出の精度を上げて出来るだけ同じサイズで学習・評価をできるようにした方が良さそうな気がする。

今のOpenCVを使った簡易顔検出ではまだまだ誤検出も多いし、ここを改善するのが先決かな、と。

まとめ・考察

  • 改めてデータセットを作成し、やはり93〜94%程度の精度であることを確認した
  • 学習データの加工は変えても結局あまり効果は無かった
  • どんな画像に対し誤答するかの傾向を少し調べた
  • 評価データの顔サイズのバラつきは評価方法を工夫することで少しは吸収できる見込み

眼鏡や指などで顔の一部が隠れているものなんかは、もっとデータを増やすことで誤答を減らせそうな気はする。 目を瞑っていたり極度の変顔とか、あまりに普段の表情と違うものはそれでも難しいかもしれないけれど…。

出来るだけ同じサイズの顔画像になるようデータセットを作っていたつもりではあったけど、まだバラつきがあってその影響を受けているらしいことが分かった。 もっと精度を上げるにはより高精度に顔領域を検出してデータセットも改善していく必要がありそうだ。

しかし、誤答例を見てるとまだ「これは普通に正しい大きさで分かりやすく写ってるし 正答してくれても良さそうなのに…」と思うものもある。 もう少し誤答の傾向は調べてみる価値がありそうな気もする。

その他

今回使ったデータセットも公開はしないですが 欲しい方がいらっしゃったら個別に連絡いただければと思います。