けやみぃアーカイブ

CV勉強中の大学生のアウトプットです

MTCNNをPyTorchで試してみる

前回の最後にYOLOとかMTCNNとかも試すって書いたのでやります。YOLOの場合は「物体検出アルゴリズム」なのでネット上で入手できる訓練済みモデルは一般の物体検出を学習させたもので、顔検出にそのままでは転用できないものばかりです。なので今回はネット上で簡単に顔検出の訓練済みモデルが手に入るMTCNNをPyTorchでやってみたいと思います。

MTCNNって?

arXivのリンクはこちら。DeepL*1と共に読んでみます。間違いがあればコメントで教えていただけるとありがたいです。

MTCNNは3つのステージから成り、それぞれが別々のネットワークを持っています。Stage1ではP-Net*2、3層のConvolution層と1回のMaxPooling、1層の全層結合を持ちます。入力画像を複数のサイズにリサイズして(任意の顔の大きさに対応できる)ネットワークに入力し、正確性というよりは高速を目的に顔領域の候補を検出します。

Stage2ではR-Net*3、3層のConvolution層と2回のMaxPooling、2層の全層結合を持ちます。Stage1の出力を基に、顔でない領域候補を消していきます。

Stage3ではO-Net*4、4層のConvolution層と3回のMaxPooling、2層の全層結合を持ちます。Stage2の出力を基に、より正確に顔でない領域候補を消していきます。また、顔のランドマーク(右目、左目、鼻、右口角、左口角のx,y座標)を検出し、最終的な顔の領域を出力します。

また、各StageでNMS(Non-Maximum Suppression)を用いて、大きく重なってる領域候補を削除してます。NMSについてのわかりやすい説明はこちらです。中間層の活性化関数にはPReLU(Parametric Rectified Linear Unit)を使用しています。PReLUとは通常のReLUとは異なり、学習可能なチャネルごとのパラメータaを持ち、次の式で表されます。

  \text{PReLU}(x) = \max(0,x) + a * \min(0,x) 

(あんまり数式の書き方わからん…)
LeakyReLU*5の進化版みたいな感じですかね。

使用したモデル

MTCNNをPyTorchで実装、訓練していた方がGitHubで公開していたので、使わせていただくことに。 github.com 今回も必要な部分だけ写経しました。

検出してみる

前回が悲惨だったためにとても緊張します。前回と同じようにFDDBデータセットから画像をとってきます。結果はこちら。

f:id:kym384:20200502113150p:plainf:id:kym384:20200502113157p:plainf:id:kym384:20200502113200p:plain
検出結果1
おお、正確に検出できてる!顔領域は青四角、ランドマークは適当に色分けして丸をつけてます。顔領域はもちろん、ランドマークもほとんどぴったりです。(横向きの写真だと怪しいけど…。)2人いてもどっちもちゃんと検出できてる。

しかし、思ってた以上…。Deepって言ったって全体としてはMaxPooling含めて20層ちょっと、全層結合抜きのVGG16より少し多いくらいです。SSDでは特徴抽出のためだけにVGGを使っていたことを考えれば驚異的…。(まあクラスが顔だけというのもありますが。)

もっと複数人写ってる画像で試してみましょう。ハロプロ公式サイトの各グループのプロフィールページから写真を持ってきます。ランドマークまで描画するとぐちゃぐちゃするので顔領域だけ青四角で描画します。

まずモーニング娘。'20(14人)

f:id:kym384:20200502114303p:plainf:id:kym384:20200502114342p:plain
モーニング娘。'20
1枚目は検出されてない人が何人かいますが、2枚目は1人程度。あと、ほまちゃんが小さめに検出されてる感じです。ランドマークの位置が重視されてるんでしょうか。2枚目のようにごちゃごちゃしててもうまく検出できてます。

次、Juice=Juice(8人)

f:id:kym384:20200502114351p:plainf:id:kym384:20200502114356p:plain
Juice=Juice
1枚目は全員検出。2枚目は衣装が白くて背景と同化してるせいで精度が落ちた?

最後に、BEYOOOOONDS(12人)

f:id:kym384:20200502114404p:plain
BEYOOOOONDS
漏れなく全員検出できてる。すごい。

精度も高く、回転にも比較的寛容なようです。Deep Learning恐るべし、って感じです。ランドマークも検出できるので、GAN用に学習データの顔の位置を調整したりする(FFHQデータセットとかは目の位置が揃ってますね)のにも使えると思います。

次回はOpenCVとdlibあたりと速度とか比較したいと思います。

*1:DeepLってのはドイツの企業が開発した翻訳モデルで最近はGoogle翻訳よりも優れていると言われることも多いです。こちらから試せます。Windowsだとデスクトップ版アプリがあって、インストールすれば任意の文章をドラッグしてCtrl+Cを2回でアプリが起動して翻訳してくれるという優れものです。

*2:「Proposal」のPだそうです。

*3:「Refine」のRだそうです。

*4:「Output」のOだそうです。

*5:   \text{LeakyReLU}(x) = \max(0,x) + {\alpha} * \min(0,x)
 \alphaは定数。0.2がデフォルト? {\alpha} = 0 の時ReLUと同じになります。

物体検出アルゴリズムSSDで顔検出に挑戦(続き)

このQiitaの記事の続きです。 qiita.com Qiitaのアカウントを2つ登録してると思い込んで片方だけ退会しようとしたら、実際はアカウントは1つでこの記事を書いたアカウントも消えてしまったのでブログ開設してみました。

先に言ってしまうと今回もうまくいなかったけど…その後の試行錯誤について書きたいと思います。

何がダメだったのか

Qiitaの方にも載せましたが、最初に学習させたモデルの結果画像です。

f:id:kym384:20200501074701p:plainf:id:kym384:20200501074659p:plainf:id:kym384:20200501074656p:plain
顔検出結果1
画像はすべて学習データでもあるFDDBデータセットFDDB : Main)のものを使用しています。 他にもいろいろな画像で試してみた結果わかったことは…

  • 1枚目のように顔が大きめに写っている画像は大きな誤差はない
  • 2枚目のように複数人が写ってる画像は全員分検出されていない
  • 3枚目のように小さめの顔が写っている画像はバウンディングボックス(青四角)の大きさ自体は顔と一致しているものの、位置がかなりずれている

ここらへんです。ただ学習自体は損失も下記のグラフのように下がってることから全くの失敗というわけではないと思って、「SSDのネットワークの出力→バウンディングボックスの変換過程が間違っているのではないか?」と考えました。

f:id:kym384:20200501134441p:plain
最初の学習時の損失グラフ
というわけでコードを見直し。今回のコードは最近買ったこちらの書籍のを使わせていただいています。 www.amazon.co.jp 今回は顔検出なので検出する物体のクラス数(本書ではcatやpersonなど20種類)など一部改変する必要があったためコピペしたわけではなく、PyTorchの勉強も兼ねて写経しました。つまり、写す過程でどこか間違ったのだと思いました。

しかしどこにも間違っている箇所は見つからず…。

「わんち見逃してるだけなのか…?」とか思いましたが、ここであることに気づきます。今回僕が勝手に変えたクラス数などの定数に関してはすべて辞書型配列にひとまとめにして、その辞書型配列を全てネットワークの引数として渡して各パラメータを決定させていました。つまり、ここの値さえ変えれば著者の実装と同じものになり、著者が提供してくれている学習済みモデルが使えるはずです。

というわけで学習済みモデルをダウンロードし、中身がOrderedDictという辞書型配列の構造になってるのでキーの文字列を自分のモデルと同じように改変することで自分のモデルで読み込めるようにしました。そして著者がテスト用に使っている画像と同じ画像を入力してみたのがこちら。

f:id:kym384:20200501074707p:plain
馬と人
(面倒くさかったのでクラスによって色を変えるといった処理はしてません。)

バウンディングボックスの位置はしっかりあってます。従って、僕のモデルはSSDの出力がおかしくて、当初考えてたSSDの出力→バウンディングボックスの変換の過程は間違っていなかったことがわかりました。

再学習させる

以上のことからSSDの学習自体がうまくいってないことがわかったので再学習させます。学習に関して、Qiitaの方ではこう書きました。

最適化関数は本書では学習済みモデルを使って転移学習をさせていたためかSGDを使用していますが、ここでは一から学習させるためにAdam(lr=0.0001, beta1=0.9, beta2=0.999)を使用しました。

が、これは嘘っぱちで、本書の方では最初の特徴抽出用のVGG以外はしっかりとHeの初期値で重みを初期化したうえで、SGDを使って一から学習させていました。誤読して申し訳ないです。

本書に従って、再学習の方でも同じパラメータでSGDを使います。(学習データやネットワークの構造は変化させてません。)

あとは、記録用のlossの値について。Qiitaの方ではepochごとの和としたせいで最初の方が馬鹿でかくなっているため、今回はミニバッチごとの平均lossをとることにしました。200epoch回して、グラフはこんな感じ。

f:id:kym384:20200501081346p:plain
損失グラフ

結果

最終的にlossは0.05くらいまで下がり、まあまあいいのでは…?という期待を持ちながらいざ画像を入力…!

f:id:kym384:20200501081745p:plainf:id:kym384:20200501081748p:plainf:id:kym384:20200501081752p:plainf:id:kym384:20200501081755p:plain
顔検出結果2
失敗!!

相変わらず複数人の検出はできてないくせに、ドアップの場合はほぼ完璧です。1枚目とか最初の学習時とほぼ同じだし…。もしかして教師データの変換か損失関数の計算が間違ってる…?(これ以上やるのは面倒くさい…)

SSDって本当はもっと高性能なのに。まあ学習時間増やせばもっとマシにはなるんだろうけど、1日費やしてダメだったので諦めることにします。物体検出ならYOLOとか、顔検出ならMTCNNとかDeep Learningで顔検出する方法は他にもあるし、そのうちどっちも試したいと思います。PyTorchの文法の練習になっただけ良しとさせてください…。