Optunaでハイパパラメータチューニング

今回はハイパパラメータチューニングを自動化するOptunaを触りながら紹介していきます。

Optuna

勾配ブースティング木やニューラルネットワークなどのハイパパラメータチューニングは職人芸みたいなところがあって、一発で最適に近い値をマークするなどほぼ不可能です。かと言って、グリッドサーチなどで探索しようにもパラメータ数が多いと計算時間が膨大になります。

こうした実質ブラックボックスのパラメータチューニングを、ベイズ最適化で行ってくれるPythonライブラリが Optuna です。

preferred.jp

これら↓の記事が大変参考になります。

tech.preferred.jp

www.slideshare.net

BayesianOptimizationのかゆいところ

同じくベイズ最適化を行ってくれるPythonライブラリとして、以前紹介1した BayesianOptimization がありますが、以下の点で使い勝手の悪さを感じていました。

他方Optunaはこれらの点で優位性があるかと思います。

Optunaを用いた実装

最初にpipでインストールしておきます。

$ pip install -U optuna

Python 3.7環境下でoptuna 1.5にて実装しています。

import sys
print(sys.version)
# 3.7.7 (default, Mar 26 2020, 10:32:53) 
# [Clang 4.0.1 (tags/RELEASE_401/final)]

import optuna
print(optuna.__version__)
# 1.5.0

1変数関数の最適化

簡単な例として、まずは以下の1変数関数の最適化をOptunaで行います。2つの極大値を持つ関数となってます。


f(x) = -(x^{4} - 20x^{2} + 10x) + 300

f:id:ohke:20200704112307p:plain

Optunaを用いたチューニングの実装は以下になります。大きく3ステップです。

  • ステップ1. 目的関数 (f) をラップするobjective関数を定義
    • 引数 (trial) はTrial型の値
      • 探索 (トライアル) の途中状態を持つ
      • 次に試すxを選ぶ (parameter suggestion) のインタフェースを持つ
  • ステップ2. Study型の変数 (study) を生成
    • storage、sampler、prunerのインスタンスを持ち、新しいトライアルを実行するクラス
      • storage: これまでのトライアルの結果を入出力する
      • sampler: 次のトライアルのパラメータを選択する
      • pruner: トライアルを途中で打ち切るかジャッジする
    • create_study関数で初期化する
      • direction="maximize"とすることで、最大値を求める (デフォルトは"minimize")
      • storage (デフォルトはInMemoryStorage)、sampler (デフォルトはTPE 2) 、pruner (デフォルトはMedianPruner 3) もこの関数の引数で指定
  • ステップ3. optimizeメソッドで最適化
    • 最終的な結果はStudyのアトリビュート (best_value, best_params) に格納
    • 履歴はget_trialsメソッド

20回の試行でx=-3.348のときに最大となっています。

import optuna

def f(x):
    return -(x**4 - 20 * x**2 + 10 * x) + 300

def objective(trial):
    x = trial.suggest_uniform("x", -5, 5)
    ret = f(x)
    return ret

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=20)

# 探索後の最良値
print(study.best_value)  # 432.0175613887767
print(study.best_params)  # {'x': -3.3480816839313774}

# 探索の履歴
for trial in study.get_trials():
    print(f"{trial.number}: {trial.value:.3f} ({trial.params['x']})")
# 0: 352.773 (-4.423048512320604)
# 1: 407.505 (-2.4294965219926326)
# 2: 305.123 (0.8356711564205996)
# 3: 393.128 (-2.1597035592672)
# 4: 376.969 (-4.25413619864094)
# 5: 379.965 (-1.9307217923302078)
# 6: 398.657 (-4.059011681641815)
# 7: 347.563 (2.0948937615830747)
# 8: 401.788 (-4.025156085303183)
# 9: 366.608 (3.2808351995664804)
# 10: 342.989 (-1.281990491946241)
# 11: 425.971 (-2.8826956711964233)
# 12: 412.053 (-2.5236953455516193)
# 13: 307.889 (-0.42721642437729157)
# 14: 428.742 (-2.988498516819845)
# 15: 430.659 (-3.087648006717103)
# 16: 300.537 (-0.04887834795409862)
# 17: 246.420 (-4.923547931892692)
# 18: 432.018 (-3.3480816839313774)
# 19: 331.979 (-1.0635461426020039)

分散最適化

storageにDBを指定することで、トライアル履歴をプロセス間で共有できるようになりますので、分散処理の実装も容易です。

storageにSQLiteを使い、joblibで2プロセスの並列処理を実装する例を示します。ポイントは2点です。

  • ポイント1. create_studyでstorageとstudy_nameを指定
    • storageにはDBのURLを指定する (SQLiteの場合は指定のパスにファイルが作成される)
  • ポイント2. 各プロセスではload_study関数でトライアル履歴を取得してトライアルを実行する
    • create_studyと同じstudy_nameを指定
import optuna
import time
import random
import os
from joblib import Parallel, delayed

def f(x):
    # トライアルごとに5〜10秒かかるようにsleep
    time.sleep(random.uniform(5, 10))
    return -(x**4 - 20 * x**2 + 10 * x) + 300

def objective(trial):
    x = trial.suggest_uniform("x", -5, 5)
    ret = f(x)
    return ret

study = optuna.create_study(
    study_name="f_study",
    direction="maximize",
    storage="sqlite:///./f_study.db"
)

def run():
    study = optuna.load_study(
        study_name="f_study",
        storage="sqlite:///./f_study.db",
    )
    study.optimize(objective, n_trials=20)
    return os.getpid()
    
# joblibでプロセス並列化
process_ids = Parallel(n_jobs=2)([delayed(run)() for _ in range(2)])
print(process_ids)  # [14937, 14938]

# 結果はcreate_studyで作られたインスタンスからアクセス可能
print(study.best_value)
print(study.best_params)

寄り道になりますが、DBのテーブル構成をサクッと見ておきます。

import pandas as pd
import sqlite3

with sqlite3.connect("./f_study.db") as conn:
    print("tables")
    df = pd.read_sql("SELECT name FROM sqlite_master WHERE type ='table' AND name NOT LIKE 'sqlite_%';", conn)
    display(df)
    
    print("studies")
    df = pd.read_sql("SELECT * FROM studies;", conn)
    display(df)
    
    print("trials")
    df = pd.read_sql("SELECT * FROM trials;", conn)
    display(df)

study_nameとstudy_idがキーとなって、studiesとtrialsが紐づくようになっています。そのためstudy_nameさえ重複しなければ、DBを共用して、別の最適化を同時に走らせることも可能になります。4

f:id:ohke:20200704154153p:plain

scikit-learnの最適化

scikit-learnのSVMとRandomForestを同時に最適化して、最良の組み合わせを探索する例を、Irisデータセットで示します。

  • Optunaで最適化の再現性を担保するためには、seedを固定したsamplerをcreate_studyでセットします 5
  • カテゴリ変数はsuggest_categoricalメソッド、ログスケール変数はsuggest_loguniformメソッドを使います
    • カテゴリ変数で分離して、それぞれでパラメータをセットできます
import optuna
from sklearn import datasets, svm, ensemble, model_selection, metrics

# irisデータセットのロードとスプリット
iris = datasets.load_iris()

data = iris.data
target = iris.target
tr_x, va_x, tr_y, va_y = model_selection.train_test_split(data, target, random_state=0)

def objective(trial):
    classifier_category = trial.suggest_categorical("classifier", ["SVC", "RandomForest"])
    
    if classifier_category == "SVC":
        svc_c = trial.suggest_loguniform("SVC_C", 0.01, 100)
        svc_gamma = trial.suggest_loguniform("SVC_gamma", 0.01, 100)
        classifier = svm.SVC(
            C=svc_c, gamma=svc_gamma, random_state=0
        )
        
    elif classifier_category == "RandomForest":
        randomforest_n_estimators = trial.suggest_int("RandomForest_n_estimators", 1, 3)
        randomforest_max_depth = trial.suggest_int("RandomForest_max_depth", 1, 3)
        classifier = ensemble.RandomForestClassifier(
            n_estimators=randomforest_n_estimators, max_depth=randomforest_max_depth,
            random_state=0
        )
        
    classifier.fit(tr_x, tr_y)
    va_pred = classifier.predict(va_x).astype("uint8")
    acc = metrics.accuracy_score(va_y, va_pred)
        
    return acc

sampler = optuna.samplers.TPESampler(seed=0)
study = optuna.create_study(sampler=sampler, direction="maximize")
study.optimize(objective, n_trials=30)

ちなみにget_trialsメソッドでパラメータ (params) は辞書形式となっており、自由に値が設定できることがわかります。

[
    FrozenTrial(number=0, ..., params={'classifier': 'SVC', 'SVC_C': 2.351681338577186, 'SVC_gamma': 23.82665049363666}, ...),
    FrozenTrial(number=1, ..., params={'classifier': 'RandomForest', 'RandomForest_n_estimators': 2, 'RandomForest_max_depth': 2}, ...),
    ...
]

まとめ

今回はOptunaを使ったハイパパラメータチューニングについて紹介しました。

他のライブラリと比較しても高機能で使い勝手が良く、1.0以降もガンガンバージョンアップしていますので "まずはOptunaでチューニング" というのが主流になっていくのかなと思います。

画像の一部をマスクするオーグメンテーションのまとめ (Random Erasing, Cutout, Hide-and-Seek, GridMask)

画像の一部をマスクすることでオーグメンテーションする手法について代表的なものをまとめました。

そもそも: なぜ画像の一部をマスクするのか?

オーグメンテーション全体に言えることですが、モデルの汎化能力を向上させることが目的です。

他のオーグメンテーション手法と比較して、遮蔽やノイズに対してロバストにする点が特徴的です。

  • ランダムフリップと異なり、情報を損失する
  • ランダムクロップと異なり、対象の全体的な構造をキープ
  • ドロップアウトと異なり、連続した矩形領域の情報を落とす

また、これらオーグメンテーション手法と相補的で、組み合わせて使うこともできます。

Random erasing

大きさがランダムの矩形領域で画像をマスクするのが Random erasing1 です。矩形領域のRGBにはランダムな値 (0〜255となる一様分布で生成) で埋められます。

  • 必要なハイパパラメータは3つ
    • 画像全体に対するマスク領域の面積比の最小・最大
    • マスク領域の縦横比のレンジ
    • Random erasingを適用する確率
  • 固定値ではなくランダムな値で埋めることで、ノイズとして機能している
    • 固定値 (0や255) で埋めるよりも精度が高くなることをCIFAR-10で確認
    • 後述する Hide-and-Seek では学習データの平均値を使うことを推奨してます

[1] Figure 1 抜粋

物体検出タスクでは、画像全体をマスクする、物体ごとにマスクする、両方を組み合わせてマスクする、という3パターンの適用方法をオプションとして提案しています。

  • VOC2007 testでは3番目が一番高いmAPをマークしてます (面積比0.02〜0.2、縦横比0.3、確率0.5を用いた場合)

[1] Figure 2 抜粋

Cutout

Random erasingとほぼ時同じくして提案された Cutout2 はもっとシンプルで、画像中のランダムな位置を中心として正方形領域 (辺の長さは固定) を固定値0でマスクします。3

  • 位置によっては全ての正方形領域を含まない (画像外にはみ出す) ケースがある
    • 常に正方形領域が画像内に収まるように調整するよりも、パフォーマンスは良かった (画像の大部分を残すことが重要)
  • Cutoutの適用確率は50%

[2] Figure 1 抜粋

画像分類タスク (CIFAR-10, CIFAR-100など) において精度改善が見られる。正方形領域の辺の長さ (下図Patch Length) が重要で、同じ画像サイズ (32 x 32) でもより細かな情報が必要となるタスクの場合は短く設定するなどのチューニングが必要です。

[2] Figure 3 抜粋

中間層の活性化度合いの偏りを見たところ、Cutoutを導入することでより広範囲の特徴を利用するように均されていることがわかります。

[2] Figure 4 抜粋

Hide-and-Seek

Random erasingやCutoutでは、重要な情報を落としすぎたり、逆に全く意味のない情報を落としてしまったりといったことが発生してしまいます。

画像を小さな正方形で分割し、このグリッドの各マスを一定確率でマスクするアルゴリズムとして Hide-and-Seek4 が提案されています。グリッド分割することよって、対象物全体を削除したり、あるいは、全く関係のない部分を削除してしまうといった問題を低減しています。

[4] Figure 2 抜粋

GridMask

Hide-and-Seekでも、連続したグリッドがたまたまマスクされることで、重要な情報を落としてしまうといったことが起こりえます。

重要な情報を落としすぎない = 隠されていない部分でなんとか学習できるくらいの"ちょうど良い" 大きさでマスクし、かつ極めて簡単に実装できるアルゴリズムとして GridMask5 が提案されてます。以下のように四角形のマスクを縦横一定の間隔でかけることで、このちょうど良さを実現しようとします。

  • この逆に、正方形領域を残す (グリッド枠の部分をマスクする) などのバリエーションがあります

[5] Figure 1 抜粋

GridMaskのハイパパラメータ4つです。それぞれ指針が示されています。

  • マスクされない領域の辺の長さの割合 (r)
    • rが小さくなるほどマスク領域は大きくなる
    • 一定の割合でマスクされるように固定する
    • 複雑なデータセットほどrを大きくとることで細かな情報まで得られやすいようにする
  • 1グリッドの辺の長さ (d)
    • rが一定ならば、dが大きくなるほど1つのマスク領域が大きくなる
    • 最小値・最大値を定めてランダムにセットするのが効果的 (ただし小さすぎると畳み込み演算でかき消されてしまうため無意味になる)
  • マスクとなる長方形の縦横の長さ ( delta_y, delta_x)
    • dとrを満たすようにランダムにセット

[5] Figure 4 抜粋

まとめ

画像の一部をマスクするオーグメンテーションアルゴリズムを4つ紹介しました。


  1. [1708.04896] Random Erasing Data Augmentation

  2. [1708.04552] Improved Regularization of Convolutional Neural Networks with Cutout

  3. Cutoutの初期構想はもっと複雑で、フォワード後の特徴マップをアップサンプリングして活性化された領域 (= 重要な特徴となっている領域) を特定し、次のエポックでその領域をマスクするというものだったようです。これはこれで上手くいったのですが、固定サイズをランダムに除去した場合と大差はなかったとのこと。

  4. [1811.02545] Hide-and-Seek: A Data Augmentation Technique for Weakly-Supervised Localization and Beyond

  5. [2001.04086] GridMask Data Augmentation

物体検出で重なったバウンディングボックスを除去・集約するアルゴリズムのまとめ (NMS, Soft-NMS, NMW, WBF)

物体検出の分野では、検出した物体をバウンディングボックス (BBox) で囲んで、それぞれに信頼度 (スコア) を算出します。

このとき重複したBBoxを除去あるいは集約するアルゴリズムにはバリエーションがあります。物体検出モデルの後処理やコンペなどでよく使われる4つを紹介します。

  • NMS
  • Soft-NMS
  • NMW
  • WBF

最初におさらい: IoU (Intersection over Union)

2つのBBoxがどれくらい重複しているかを表す指標の1つで、1.0に近づくほど重複しています。

  • 分子が重なっている面積
  • 分母が2つのBBoxの総面積


IoU = \frac{Intersection}{Union}

実装

def iou(a: tuple, b: tuple) -> float:
    a_x1, a_y1, a_x2, a_y2 = a
    b_x1, b_y1, b_x2, b_y2 = b
    
    if a == b:
        return 1.0
    elif (
        (a_x1 <= b_x1 and a_x2 > b_x1) or (a_x1 >= b_x1 and b_x2 > a_x1)
    ) and (
        (a_y1 <= b_y1 and a_y2 > b_y1) or (a_y1 >= b_y1 and b_y2 > a_y1)
    ):
        intersection = (min(a_x2, b_x2) - max(a_x1, b_x1)) * (min(a_y2, b_y2) - max(a_y1, b_y1))
        union = (a_x2 - a_x1) * (a_y2 - a_y1) + (b_x2 - b_x1) * (b_y2 - b_y1) - intersection
        return intersection / union
    else:
        return 0.0
    
assert iou((10, 20, 20, 30), (10, 20, 20, 30)) == 1.0
assert iou((10, 20, 20, 30), (20, 20, 30, 30)) == 0.0
assert 0.142 < iou((10, 20, 20, 30), (15, 25, 25, 35)) < 0.143
assert 0.111 < iou((10, 10, 40, 40), (20, 20, 30, 30)) < 0.112

NMS (Non-Maximum Suppression)

IoUがある閾値を超えて重なっているBBoxの集合から、スコアが最大のBBoxを残して、それ以外を除去するのがNMS1です。

f:id:ohke:20200620094403p:plain

実装

def nms(bboxes: list, scores: list, iou_threshold: float) -> list:
    new_bboxes = []
    
    while len(bboxes) > 0:
        i = scores.index(max(scores))
        bbox = bboxes.pop(i)
        scores.pop(i)
        
        deletes = []
        for j, (bbox_j, score_j) in enumerate(zip(bboxes, scores)):
            if iou(bbox, bbox_j) > iou_threshold:
                deletes.append(j)
                
        for j in deletes[::-1]:
            bboxes.pop(j)
            scores.pop(j)
                
        new_bboxes.append(bbox)
        
    return new_bboxes
    
assert nms([(10, 20, 20, 30), (20, 20, 30, 30), (15, 25, 25, 35)], [0.8, 0.7, 0.9], 0.1) == [(15, 25, 25, 35)]

Soft-NMS

NMSではスコアが最大のBBox以外は除去されていましたが、例えば以下のように、同じラベルの物体が重なりあっている場合に誤って除去してしまう (再現率を落とす) ことが起こりえます。

f:id:ohke:20200620092744p:plain

こうしたケースに着目し、IoU閾値を超えたBBoxを残しつつ、代わりにスコアを割り引くのがSoft-NMS2です。

f:id:ohke:20200620133111p:plain

割引きの計算式は以下2つが提案されていますが、いずれもIoUが大きくなる (= より重複している) とより多く割り引くようになっています。

実装

先程のNMSの実装を少し変えるだけで実現できます。割り引き計算には上の1つ目の式を用いています。

def soft_nms(bboxes: list, scores: list, iou_threshold: float) -> (list, list):
    new_bboxes, new_scores = [], []
    
    while len(bboxes) > 0:
        i = scores.index(max(scores))
        bbox = bboxes.pop(i)
        score = scores.pop(i)
        
        for j, (bbox_j, score_j) in enumerate(zip(bboxes, scores)):
            iou_j = iou(bbox, bbox_j)
            if iou_j > iou_threshold:
                scores[j] = score_j * (1 - iou_j)
                
        new_bboxes.append(bbox)
        new_scores.append(score)
        
    return new_bboxes, new_scores
    
print(soft_nms([(10, 20, 20, 30), (20, 20, 30, 30), (15, 25, 25, 35)], [0.8, 0.7, 0.9], 0.1))
# ([(15, 25, 25, 35), (10, 20, 20, 30), (20, 20, 30, 30)], [0.9, 0.6857142857142858, 0.6])

Non-Maximum Weighted (NMW)

NMSとSoft-NMSは、いずれもBBoxの座標情報を変更していません。以下のように、いずれのBBoxでも正確に捉えきれていない場合、BBoxのどれかを残すのではなく、"良い感じに"ミックスして平均的なBBoxを作るほうが全体を上手に捉えることができそうな気がしてきます。

f:id:ohke:20200620231231p:plain

NMW3は、重なりあったBBoxをスコアとIoUで重み付けして足し合わせることで、1つの新たなBBoxを作り出すアルゴリズムです。

  •  b_{pre} が新たに作るBBoxです
  •  b_{argmax_{i} c_{i}}はスコアが最も高いBBoxです (つまりNMSなら残されるBBoxです)

実装

IoUが低いのでそれほど目立ってないですが、わずかにBBox (15, 25, 25, 35) が他の2つに近づいていることがわかります。

def nmw(bboxes: list, scores: list, iou_threshold: float) -> list:
    new_bboxes = []
    
    while len(bboxes) > 0:
        i = scores.index(max(scores))
        bbox = bboxes.pop(i)
        score = scores.pop(i)
        
        numerator = (bbox[0] * score, bbox[1] * score, bbox[2] * score, bbox[3] * score)
        denominator = score
        deletes = []
        for j, (bbox_j, score_j) in enumerate(zip(bboxes, scores)):
            iou_j = iou(bbox, bbox_j)
            if iou_j > iou_threshold:
                w = scores[j] * iou_j
                numerator = (
                    numerator[0] + bbox_j[0] * w, numerator[1] + bbox_j[1] * w,
                    numerator[2] + bbox_j[2] * w, numerator[3] + bbox_j[3] * w
                )
                denominator += w
                
                deletes.append(j)
                
        for j in deletes[::-1]:
            bboxes.pop(j)
            scores.pop(j)
          
        new_bboxes.append((
            numerator[0] / denominator, numerator[1] / denominator,
            numerator[2] / denominator, numerator[3] / denominator
        ))
        
    return new_bboxes
    
print(nmw([(10, 20, 20, 30), (20, 20, 30, 30), (15, 25, 25, 35)], [0.8, 0.7, 0.9], 0.1))
# [(14.935897435897434, 24.038461538461537, 24.935897435897434, 34.03846153846154)]

Weighted Boxes Fusion (WBF)

これまで触れてきたアルゴリズムは、重なり合わないBBoxについては何もせず残します。同じ画像に対して複数の推論を行う場合 (複数モデルでアンサンブルする、オーグメンテーションをかけるなど) 、これが誤検出につながるケースがあります。

3つのモデルで"dog"を検出する推論をかけ、以下のようにBBoxが出力されたとします。ある1つのモデルだけ左の人を検出してしまっています。NMS・Soft-NMS・NMWでは、この左の誤ったBBoxを除去したりスコアを下げたりできません。

WBF4は、複数モデルの推論結果を集約することを主目的としたアルゴリズムです。

複数モデルで推論させて得られたBBoxから、IoUを閾値として重複するBBoxを見つけ出します。このT個の重複したBBoxについて、スコア (C) と座標 (X1, X2, Y1, Y2) を以下の式で計算します。スコアで重み付けされた平均値となっており、この時点ではIoUを用いていません。

全てのBBoxについてスコアと座標の計算が終わった後、以下のいずれかの式でスコアをモデルの数 (N) で正規化します。これにより、検出されたモデルの数 (つまりT) が少ないBBoxほどスコアが下がるため、上のように1つのモデルだけで検出されたBBoxをスコアで足切りすることができるようになります。

実装

以下の通り、重なり合うBBoxは高いスコア (0.8) をキープしますが、重なっていないBBox (30, 30, 40, 40) は元のスコアの1/3されています。

def wbf(bboxes: list, scores: list, iou_threshold: float, n: int) -> (list, list):
    lists, fusions, confidences = [], [], []
    
    indexes = sorted(range(len(bboxes)), key=scores.__getitem__)[::-1]
    for i in indexes:
        new_fusion = True
        
        for j in range(len(fusions)):
            if iou(bboxes[i], fusions[j]) > iou_threshold:
                lists[j].append(bboxes[i])
                confidences[j].append(scores[i])
                fusions[j] = (
                    sum([l[0] * c for l, c in zip(lists[j], confidences[j])]) / sum(confidences[j]),
                    sum([l[1] * c for l, c in zip(lists[j], confidences[j])]) / sum(confidences[j]),
                    sum([l[2] * c for l, c in zip(lists[j], confidences[j])]) / sum(confidences[j]),
                    sum([l[3] * c for l, c in zip(lists[j], confidences[j])]) / sum(confidences[j]),
                )
                
                new_fusion = False
                break

        if new_fusion:
            lists.append([bboxes[i]])
            confidences.append([scores[i]])
            fusions.append(bboxes[i])
            
        print(lists, fusions, confidences)
            
    confidences = [(sum(c) / len(c)) * (min(n, len(c)) / n) for c in confidences]
    
    return fusions, confidences
    
print(wbf([(10, 10, 20, 20), (15, 10, 25, 20), (15, 15, 25, 25), (30, 30, 40, 40)], [0.8, 0.7, 0.9, 0.5], 0.1, 3))
# ([(13.333333333333332, 11.874999999999998, 23.33333333333333, 21.874999999999996), (30, 30, 40, 40)], [0.8000000000000002, 0.16666666666666666])

使い分け

ではこれらアルゴリズムをどう使い分けるべきかについてです。最終的にはメトリクスを見ながら適切なものを選ぶべきかと思いますが、大まかにはこういった使い分けになるかと思います。

  • 単一モデルの精度を高めたい ->
    • 同一物体に対する誤検出が多い -> NMS (あるいは、IoU閾値を下げる or スコア閾値を上げる)
    • 同一画像内にたくさんの物体が含まれており、重複による未検出が多い -> Soft-NMS (あるいは、IoU閾値を上げる or スコア閾値を下げる)
    • 変形や動きが大きい物体が含まれており、1つのBBoxで捉えきれずにIoUが低い -> NMW
  • 精度がある程度高いモデルを複数組み合わせて、より高い精度を達成したい -> WBF

まとめ

物体検出で重なったBBoxを除去・集約するアルゴリズムについて整理しました。


  1. [1311.2524] Rich feature hierarchies for accurate object detection and semantic segmentation (初出の論文は別かも…?)

  2. [1704.04503] Soft-NMS -- Improving Object Detection With One Line of Code

  3. Huajun Zhou, Zechao Li, Chengcheng Ning, and Jinhui Tang. Cad: Scale invariant framework for real-time object detection. In Proceedings of the IEEE International Conference on Computer Vision, pages 760–768, 2017.

  4. [1910.13302] Weighted Boxes Fusion: ensembling boxes for object detection models