この記事の目的
物体検出でEfficientDetを用いた時に気づいたメモ。
EfficientDetのメリットや使用した根拠、実際に使用するためGitに公開されているコードからどの部分を変更する必要があったのかなどの備忘録として記載している。
EfficientDetとは
2020年に開発された物体検出フレームワーク。速度や精度でスケールするD0〜D7のモデルが存在する。Gitを見る限り2021年までは更新されているっぽい。詳しい精度などはGitを見てもらうのが早いが、とりあえずD0が高速/低精度、D7が低速/高精度である。
なぜEfficientDetを採用したか
- 複数のモデルで比較しやすい
- D0〜D7までのモデルを簡単な設定のみで変更できるため、速度や精度と相談しながら適している条件を見つけやすい。
- ライセンス問題
- Apache 2.0ライセンスなため、クローズドのまま商用利用出来る。 (YOLO系は大体GPL, AGPLでコピーレフトライセンス。非公開にしたい場合は商用ライセンスを購入する必要があったりするらしい。)
Detectron2やMMDetectionも同じApache 2.0で精度的にはEfficeintDetより高い。しかし、速度や精度の基準が定まっていない状態で学習・推論を行うのであれば、簡単にモデルを切り替えられるEfficientDetが良いと判断した。
変更箇所について
前提
EfficientDetは前処理時に画像のアスペクト比を維持してリサイズしてくれない。
アスペクト比の維持をしない場合、モデル精度の低下の恐れがある。
数万件以上の多様性がある画像のデータセットである場合、アスペクト比を維持しないリサイズであっても歪みに強く問題になりづらい。例えばクローリングした大量の画像やCOCOデータセットなど数万件〜数十万件では問題にならない。
独自に学習したい画像を準備し1000件ほどの画像である場合アスペクト比を維持したリサイズにコードを修正した方が精度が高そうだったので、試してみた。
変更点
dataloader.pyの中に前処理、データ拡張などの処理が全て入っているので、そのままAIに投げて聞くのがいいかなと。(一応以下に該当メソッドを置いておきます。)以下二つのメソッドで画像のリサイズ加工とBBoxの座標調整を行っているのでそこを修正するだけで大丈夫そう。
結果としては、リサイズのアスペクト比を維持するように変更した事で、1000~3000件ほどの画像での精度評価では、その他を同じ条件にした上でF値は0.1〜0.15ほど向上した。やはりデータ量が足りない場合には歪みがないリサイズにした方が良さそう。
def resize_and_crop_image(self, method=tf.image.ResizeMethod.BILINEAR):
target_height, target_width = self._output_size
orig_shape = tf.shape(self._image)
orig_height = tf.cast(orig_shape[0], tf.float32)
orig_width = tf.cast(orig_shape[1], tf.float32)
# 1. スケール比を計算
scale_y = tf.cast(target_height, tf.float32) / orig_height
scale_x = tf.cast(target_width, tf.float32) / orig_width
scale = tf.minimum(scale_y, scale_x)
new_height = tf.cast(orig_height * scale, tf.int32)
new_width = tf.cast(orig_width * scale, tf.int32)
# 2. リサイズ
resized_image = tf.image.resize(self._image, [new_height, new_width], method=method)
# 3. パディング(中央寄せ)
padded_image = tf.image.resize_with_pad(resized_image, target_height, target_width)
self._image = padded_image
# 4. 各スケールを個別に記録
self._scale_y = tf.cast(new_height, tf.float32) / self._orig_height
self._scale_x = tf.cast(new_width, tf.float32) / self._orig_width
self._scaled_height = new_height
self._scaled_width = new_width
self._crop_offset_y = (target_height - new_height) // 2
self._crop_offset_x = (target_width - new_width) // 2
return self._image
def resize_and_crop_boxes(self):
# 1. 正規化 → 実寸
boxes = self._boxes * tf.stack([
self._orig_height, self._orig_width,
self._orig_height, self._orig_width
])
# 2. 軸ごとのスケーリング
boxes = boxes * tf.stack([
self._scale_y, self._scale_x,
self._scale_y, self._scale_x
])
# 3. 中央寄せパディングの補正
offset = tf.cast(tf.stack([
self._crop_offset_y,
self._crop_offset_x,
self._crop_offset_y,
self._crop_offset_x
]), tf.float32)
boxes = boxes + tf.reshape(offset, [1, 4])
# 4. 範囲外座標のクリップ
boxes = self.clip_boxes(boxes)
# 5. 面積0のBox除外
valid = tf.where(
tf.not_equal((boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]), 0)
)
boxes = tf.gather_nd(boxes, valid)
classes = tf.gather_nd(self._classes, valid)
return boxes, classes
最後に
とりあえず動いてるし。とそのまま使っていて内部をあまり理解していない状態になっている事が多いと思う。実際、他のフレームワークが大体デフォルトのリサイズでアスペクト比を維持するので、同じだろうと確認していなかった訳で。きっとこんなもんだな、と思う前に一回全部AIに投げて知らない機能や実装がないか確かめるのも大事だと思います。