画像処理のうち一般物体認識と呼ばれている分野で主に取り組まれているものとして、分類(classification)、物体検知(object detection)、セグメンテーション(segmentation)の三つのテーマがあります。このうち物体検知は応用範囲が広く、現在でも盛んに研究と応用が行われているテーマです。最近だとM2Detと呼ばれる手法が話題になりました。

物体検知の精度性能を評価する指標には、mAP (mean Average Precision)がよく用いられます。手法同士の精度の優劣を比較するためには、学習用のデータと評価用のデータを揃えた上でmAPを測定しないと意味がありません。さらにややこやしいことに、mAPの定義はいくつかのバージョンがあるので、その点も留意が必要です。

一般によく用いられているのは、Pascal VOCとCOCOというオープンデータセットで、それぞれのmAPについても定義が決まっています。ここでは当面、Pascal VOC 2007および2012の16,551枚のtrainval画像を用いて学習し、Pascal VOC 2007の4,952枚のtest画像を用いて評価する手法を使っていくことにします。論文でも多く使われている評価法のひとつで、クラス数は20。物体検知としては比較的簡単な対象物が集められています。

#Pythonで実装されているPascal VOC用のmAP測定プログラムを使用します。公式実装ではありませんが作者はそれに非常に近いはずだと書いてるので信用。^_^

アカデミックな世界ではある評価指標での最高値を達成したときに、achieve state-of-the-art result (SOTA)みたいに言います。物体検知のPascal VOC 2007テストのSOTAの変移を見るには(公式サイトではないですが) https://paperswithcode.com/sota/object-detection-on-pascal-voc-2007 が参考になります。もちろん検証なしにスコアを鵜呑みできるわけではないのですが、コードと紐付けられているものも多く助かります。個人的には、mAP=0.70をひとつの目安とし、0.80を超えると性能の良さを感じます。(0.70以下だと使えないという意味。速度やモデルのサイズなど精度以外の指標もあるため高ければいいというものでもないからです)

さて、前置きが長くなりましたが先程触れたM2Detの学習と評価をやってみましょう。M2Detの論文を読むとこれまでに提案されたさまざまな手法を合わせ技で持ってきてる印象を持ちました。githubに公開されているコードを改変し、Pascal VOCの学習と評価のコードが動くようにします。さて結果やいかに!

AP for aeroplane = 0.5837
AP for bicycle = 0.6655
AP for bird = 0.4137
AP for boat = 0.3316
AP for bottle = 0.2773
AP for bus = 0.6471
AP for car = 0.7267
AP for cat = 0.7056
AP for chair = 0.2730
AP for cow = 0.3793
AP for diningtable = 0.5536
AP for dog = 0.5646
AP for horse = 0.7400
AP for motorbike = 0.6282
AP for person = 0.5832
AP for pottedplant = 0.3012
AP for sheep = 0.3861
AP for sofa = 0.5730
AP for train = 0.6107
AP for tvmonitor = 0.5549
Mean AP = 0.5250

あれれ。。。ちっとも精度が出ていません。mAP=0.52という値は、まったく学習ができていないときには絶対にでない値なので、なんらかの学習はできているものの、state-of-the-artとは程遠い値しか達成できませんでした。もちろん可能性が高いのは、学習にあたって私がなんらかの間違いをしてることでしょう。そう思いたい。ただM2Detの学習を実際にやってみた人の報告が見つけられなかったこともあり、腰を据えて解析しないといけなさそうです。なので、M2Detについてはいずれ戻ってくることにします。

そういうわけで、ここからはstate-of-the-artの物体検知を手元で実現するために、あらためて物体検知にじっくりと向き合ことにします。^_^

よく知られていることですが、Deep Learningを用いた物体検知には大きく分けて二つのアプローチがあります。ひとつはtwo stage型と呼ばれるもの。two stage型はR-FCNの流れを汲む手法で、対象オブジェクトの領域候補を作るstageとCNNを用いた認識を行うstageのふたつを持つものです。もうひとつのone stage型はYoloやSSDの流れを汲み、すべての学習をone stageでやってしまうものです。

もともとtwo stage型は精度がよいけど遅く、one stage型は速いけれど精度に劣っていたのですが、最近はone stage型の精度向上が進んでおり、Faster RCNのような代表的なtwo stage型の精度を凌ぐと主張する手法も増えています。one stage型を世に広めた立役者はなんと言ってもJoseph Redmon氏によるYoloなのですが、以降の様々な手法に大きく影響を与えたという意味では、SSDの功績も大変に大きいものがあります。

そこでまずはSSDでのPascal VOCを用いた学習と評価をきっちりやり、そこからイロイロと手を入れてみることにします。以前ご紹介したMax deGroot氏のPyTorch版SSD実装をベースに、少し改造を加えます。

オリジナルの実装はiterationベースなのですが、これをepochベースに変更し、最初のepochはwarmupとしてlearning rateを0からスタート値にリニアに増加させることにします。これはYoloでburninとして実装されているものと同じような効果を狙っており学習初期の不安定な動作を抑制させることができます。

# iterationの数 = mini-batchを回した数。mini-batchのサイズはGPUメモリに収めるためにモデルの大きさに応じて変更する必要があり、そうするとiterationの数が学習量(枚数)の指標として統一的に考えにくくなります。一方epochはデータセットを一周学習した回数なので、同じデータセットを使う限りはbatch sizeが変わっても学習量(枚数)の指標としては統一的に用いることができます。

# yoloで実装されているburninはlearning rateのスタート値に指数関数的に近づく実装になっていますが、リニアに近づけても十分に効果がありました。

改造済みのコードはGitHubに置いてありますのでご参考に。basenetのpretrain weightのvgg16_reducedfc.pthはググって落とし、weights以下に置いてください。Pascal VOCのデータがもちろん必要ですがこれも落としてきて、できればSSDに置いてあげてください。(置いた場所を引数で指定します)

学習と評価を行ってみます。多めに250epoch回してみました。オプティマイザはMomentum SGD。learning rateはwarmup後に0.001からはじめて、150epoch目と200epoch目にx0.1しています。最後のepochの値が最良とは限りませんが、そこは割愛します。

# training
$ CUDA_VISIBLE_DEVICES=0 python train.py --dataset=VOC --dataset_root=/mnt/ssd/VOCdevkit/  --weight_prefix='VOC300_'
# evaluation
$ CUDA_VISIBLE_DEVICES=0 python eval.py --voc_root=/mnt/ssd/VOCdevkit/ --trained_model weights/VOC300_249.pth
~snip~
AP for aeroplane = 0.8262
AP for bicycle = 0.8413
AP for bird = 0.7645
AP for boat = 0.6910
AP for bottle = 0.5185
AP for bus = 0.8503
AP for car = 0.8632
AP for cat = 0.8936
AP for chair = 0.6163
AP for cow = 0.8230
AP for diningtable = 0.7632
AP for dog = 0.8527
AP for horse = 0.8704
AP for motorbike = 0.8367
AP for person = 0.7887
AP for pottedplant = 0.5002
AP for sheep = 0.7611
AP for sofa = 0.8044
AP for train = 0.8540
AP for tvmonitor = 0.7671
Mean AP = 0.7743


モデルへの入力画素数を増加させると、情報が大きくなって精度の改善が見込めます。Pascal VOCのイメージデータは長辺が500あるので、入力サイズを512x512とするモデルを作ってみます。オリジナルの実装は入力サイズを300x300しかサポートしていませんが、いくつか変更を加えると512x512とすることができます。

アンカーボックスを設置する特徴マップの数を6から7に変更し、以下のように入力をダウンサンプリングしながら、以下のように(0)~(6)の部分に設置します。

512x512 -> 256x256 -> 128x128 -> 64x64(0) -> 32x32(1) -> 16x16(2) -> 8x8(3) -> 4x4(4) -> 2x2(5) -> 1x1(6)
train.pyとeval.pyの上の方を以下のように変更してください。

# from ssd import build_ssd
# cfg = voc

from ssd512 import build_ssd
cfg = voc512

学習と精度の測定を行ってみます。batch_sizeはお使いのGPUのメモリに収まるように調整ください。GTX1080TiとかRTX2080Tiとかだと、batch_size=16くらいが適当です。

# training
$ python train.py --dataset=VOC --dataset_root=/mnt/ssd/VOCdevkit/ --loss_type='cross_entropy' --weight_prefix='VOC512_' --batch_size=16
# evaluation
$ python eval.py --voc_root=/mnt/ssd/VOCdevkit/ --trained_model weights/VOC512_249.pth
~snip~
AP for aeroplane = 0.8577
AP for bicycle = 0.8705
AP for bird = 0.8107
AP for boat = 0.7505
AP for bottle = 0.6079
AP for bus = 0.8841
AP for car = 0.8842
AP for cat = 0.8807
AP for chair = 0.6537
AP for cow = 0.8654
AP for diningtable = 0.7685
AP for dog = 0.8652
AP for horse = 0.8908
AP for motorbike = 0.8540
AP for person = 0.8291
AP for pottedplant = 0.5624
AP for sheep = 0.8177
AP for sofa = 0.7923
AP for train = 0.8662
AP for tvmonitor = 0.8008
Mean AP = 0.8056

論文で報告されているmAPは0.772(SSD300)および0.798(SSD512)なので、それよりもいい値が出てしまいました。それぞれ複数回の学習と評価を行って同じくらいの値がでることを確認しています。正直いい値が出過ぎてしまった感あり。ともかくここからは、SSD512をベースラインに改善を試みます。

SSDは特徴マップに設置したすべてのアンカーボックスに対して、クラス推定を行います。また、与えられた教師データとのIoUが0.5を超えたものをPositive Matchとして、教師データとアンカーボックスとの差分を学んでいきます。SSDに設置されるアンカーボックスの数は標準設定のSSD300で

(38x38x4)+(19x19x6)+(10x10x6)+(5x5x6)+(3x3x4)+(1x1x4) = 8732

標準設定のSSD512で

(64x64x4)+(32x32x6)+(16x16x6)+(8x8x6)+(4x4x6)+(2x2x4)+(1x1x4) = 24564

となります。Positive Matchしなかったアンカーボックスはbackgroundクラスとして学習されることになりますが、これがSSDの課題のひとつとして認識されている部分で、Positive Matchせずにbackgroundクラスにアサインされたアンカーボックスの数が、大変に多くなるのです。

ディープラーニングの学習はLossを少なくなるように行うのはご存知の通りですが、極端にクラスの数に差がある(class imbalance)と学習がうまくいかないことがあります。(Lossの割当が均一であれば常にbackgroundと推測することがLossを減らす近道となる)

オリジナルのSSDはこの問題に対処するために、ハードネガティブマイニングという手法を採用しています。これはbackgroundクラスの教師データの数を、ロスの大きいものから一定の数に制限するというものです。

一方でclass imbalanceに対する解決策としてFacebook AIが提案したのがFocal Lossです。クラスtの推論確率をptとして、Focal Lossは以下のように定義します。

FL(pt) = −αt(1 − pt)^γ * log(pt).

ハードネガティブマイニングがロスの小さなサンプルを除外するアプローチを取ったのに対し、Focal Lossはロスの小さなサンプルのロスをγ乗することで急激に小さくして、数が多く累計されてもclass imbalanceの弊害が起きにくくするものです。

論文では、αとγの値を適切に設定することでハードネガティブマイニングを行わずにより良いmAPを達成したと報告しています。早速実装して評価してみましょう。

# training
$ python train.py --dataset=VOC --dataset_root=/mnt/ssd/VOCdevkit/ --loss_type='focal' --weight_prefix='VOC512_FL_' --batch_size=16
# evaluation
$ python eval.py --voc_root=/mnt/ssd/VOCdevkit/ --trained_model weights/VOC512_FL_249.pth
~snip~
AP for aeroplane = 0.8512
AP for bicycle = 0.8692
AP for bird = 0.7823
AP for boat = 0.7356
AP for bottle = 0.5707
AP for bus = 0.8761
AP for car = 0.8817
AP for cat = 0.8794
AP for chair = 0.6343
AP for cow = 0.8555
AP for diningtable = 0.7579
AP for dog = 0.8432
AP for horse = 0.8778
AP for motorbike = 0.8399
AP for person = 0.8191
AP for pottedplant = 0.5238
AP for sheep = 0.8272
AP for sofa = 0.7836
AP for train = 0.8675
AP for tvmonitor = 0.7843
Mean AP = 0.7930

残念ながら、ハードネガティブマイニングを用いたSSD512に比べて約1ポイント低い結果になりました。Focal Lossのオリジナル論文は、FPN (Feature Pyramid Network)を同時に組み込んでいることと、評価対象がCOCOデータセットなので、今回の結果と比較できません。0.7930という値自体は悪くないと思いますが、なにせベースラインを超えれなかったので残念。

もちろん私の実装が間違ってるってこともありえますが。。。今後役に立つかもしれないし。

今回はここまでとします。