YOLOv3 アルゴリズムの原理とパドルの実装

YOLOv3 アルゴリズムの原理とパドルの実装

ゼロベースのパドル入門コースに沿ってまとめたYOLOv3学習ノート。

1。概要

古典的な R-CNN シリーズのアルゴリズムは 2 段階のターゲット検出アルゴリズムとも呼ばれ、この方法ではまず候補領域を生成し、次に候補領域の位置座標を分類して予測する必要があるため、アルゴリズムの速度が非常に遅くなります。これに対応するのが、YOLOアルゴリズムに代表される一段階検出アルゴリズムであり、1つのネットワークのみで候補領域の生成と物体のカテゴリと位置座標の予測を同時に行うことができます。

R-CNN シリーズのアルゴリズムとは異なり、YOLOv3 は単一のネットワーク構造を使用し、候補領域を生成しながらオブジェクトのカテゴリと位置を予測でき、検出タスクを完了するために 2 つの段階に分割する必要はありません。さらに、YOLOv3 アルゴリズムによって生成される予測フレームの数は、Faster R-CNN の予測フレーム数よりもはるかに少なくなります。Faster R-CNN の各実際のフレームは複数の陽性候補領域に対応する可能性がありますが、YOLOv3 の各実際のフレームは 1 つの陽性候補領域のみに対応します。これらの特性により、YOLOv3 アルゴリズムが高速になり、リアルタイム応答のレベルに達することができます。

Joseph Redmon らは、2015 年に YOLO (You Only Look Once、YOLO) アルゴリズム (通称 YOLOv1) を提案し、2016 年にアルゴリズムを改良して YOLOv2 バージョンを提案し、2018 年に YOLOv3 バージョンが開発されました。

2. YOLOv3 モデルの設計アイデア

  • トレーニング段階では次のことが行われます。
  1. 一定のルールに従って画像上に一連の候補領域を生成し、これらの候補領域と画像上のオブジェクトの実フレームとの位置関係に従って候補領域をマークします。グラウンド トゥルース ボックスに十分近い候補領域はポジティブ サンプルとしてマークされ、グラウンド トゥルース ボックスの位置がポジティブ サンプルの位置ターゲットとして使用されます。実際のフレームから逸脱する候補領域はネガティブ サンプルとしてマークされ、ネガティブ サンプルは位置やカテゴリを予測する必要がありません。
  2. 畳み込みニューラル ネットワークを使用して画像の特徴を抽出し、候補領域の位置とカテゴリを予測します。このように、各予測フレームはサンプルと見なすことができ、ラベル値は、その位置とカテゴリに関連して実際のフレームをマークし、ネットワーク モデルを通じてその位置とカテゴリを予測し、ネットワーク予測値とネットワーク予測値を比較することによって取得されます。ラベル値を指定すると、損失関数を構築できます。
  • 予測段階では、事前に定義されたアンカー フレームと抽出された画像特徴に従って予測フレームが計算され、マルチクラス非最大値抑制を使用して重複するフレームが削除され、最終結果が得られます。

YOLOv3のアルゴリズム フローを図 1 に示します。


図 1: ターゲット検出の設計スキーム

次に、YOLOv3 アルゴリズムをトレーニングと予測の 2 つの側面から深く理解します。

3. YOLOv3 モデルのトレーニング

YOLOv3 アルゴリズムのトレーニング プロセスは図 2 に示すように 2 つの部分に分割できます。

  • 一定のルールに従って画像上に一連の候補領域を生成し、これらの候補領域と画像上のオブジェクトの実フレームとの位置関係に従って候補領域をマークします。グラウンド トゥルース ボックスに十分近い候補領域はポジティブ サンプルとしてマークされ、グラウンド トゥルース ボックスの位置がポジティブ サンプルの位置ターゲットとして使用されます。実際のフレームから逸脱する候補領域はネガティブ サンプルとしてマークされ、ネガティブ サンプルは位置やカテゴリを予測する必要がありません。
  • 畳み込みニューラル ネットワークを使用して画像の特徴を抽出し、候補領域の位置とカテゴリを予測します。このように、各予測フレームはサンプルと見なすことができ、ラベル値は、その位置とカテゴリに関連して実際のフレームをマークし、ネットワーク モデルを通じてその位置とカテゴリを予測し、ネットワーク予測値とネットワーク予測値を比較することによって取得されます。ラベル値を指定すると、損失関数を構築できます。



図 2: YOLOv3 アルゴリズムのトレーニング フローチャート

  • 図 2 の左側は入力画像です。上部に示されているプロセスは、畳み込みニューラル ネットワークを使用して画像から特徴を抽出することです。ネットワークが前方に伝播し続けるにつれて、特徴マップのサイズはますます小さくなります、各ピクセルはより抽象的な画像を表します。出力特徴マップまでの特徴パターンは、そのサイズが元の画像の 1 に縮小されます32 \frac{1}{32}3 21
  • 図 2 の下部は、候補領域を生成するプロセスを示しています。まず、元の画像が複数の小さな正方形に分割され、各小さな正方形のサイズは32 × 32 32 \times 32 になります。3 2×3 2と入力し、それぞれの小さな正方形を中心とした一連のアンカー ボックスを生成すると、画像全体がアンカー ボックスで覆われます。各アンカーフレームに基づいて対応する予測フレームが生成され、これらの予測フレームは、画像上のアンカーフレームと予測フレームおよびオブジェクトの実フレームとの位置関係に従ってマークされる。
  • 上のブランチで出力された特徴マップは、下のブランチで生成された予測ボックス ラベルに関連付けられ、損失関数が作成され、エンドツーエンドのトレーニング プロセスが開始されます。

次に、プロセス内の各ノードのアルゴリズム原理を詳しく紹介します。

3.1 候補領域の生成

候補領域を生成する方法は、検出モデルの中核となる設計です。現在、畳み込みニューラル ネットワークに基づくほとんどのモデルは次の方法を採用しています。

  1. 一定の位置をもつ一連のアンカー ボックスが特定のルールに従って画像上に生成され、これらのアンカー ボックスが候補領域とみなされます。
  2. アンカーフレームに対象物体が含まれるかどうかを予測し、対象物体が含まれる場合には、含まれる対象物のカテゴリと、アンカーフレームの位置に対する予測フレームの調整の大きさも予測する必要がある。

1.アンカーボックスを生成する

YOLOv3 アルゴリズムでは、元の画像はm × nm\times nに分割されます。メートル×図 3 に示すように、 n 個の領域元の画像の高さH = 640 H=640H=6 4 0、幅W = 480 W=480W=4 8 0、小さな領域のサイズを32 × 32 32 \times 323 2×3 2、次にmmmnnn はそれぞれ次のとおりです。

m = 640 32 = 20 m = \frac{640}{32} = 20メートル=3 26 4 0=2 0

n = 480 32 = 15 n = \frac{480}{32} = 15n=3 24 8 0=1 5

つまり、元の画像を 20 行 15 列の小さな正方形の領域に分割しました。



図 3: 画像を複数の 32x32 の小さな正方形に分割します。

YOLOv3 アルゴリズムは、各領域の中心に一連のアンカー ボックスを生成します。表示の便宜上図 4 に示すように、生成されたアンカー ボックスを図の 10 行目、4 列目の小さな正方形の位置の近くにのみ描画します。



図 4: 行 10、列 4 の小さな正方形領域に 3 つのアンカー ボックスを生成します。


例証します:

ここでは、プログラム内の番号と対応させるため、一番上の行番号を0行、一番左の列番号を0列目とします。


2. 予測ボックスの生成

アンカーフレームの位置は固定されており、オブジェクトのバウンディングボックスと一致させることは不可能であることは以前から指摘されていますが、予測を生成するにはアンカーフレームを基準に位置を微調整する必要がありますフレーム。予測フレームはアンカーフレームとは中心位置やサイズが異なりますが、予測フレームはどのように取得すればよいのでしょうか? まずその中心位置座標を生成する方法を考えてみましょう。

たとえば、緑色の点線のボックスに示すように、上図の 10 行 4 列目の小さな正方形領域の中央にアンカー ボックスが生成されます。小さな正方形の幅を単位長さとして、

この小さな正方形領域の左上隅の位置座標は次のとおりです。
cx = 4 c_x = 4c×=4
cy = 10 c_y = 10cはい=1 0

このアンカー ボックスのエリア中心座標は次のとおりです。
center_x = cx + 0.5 = 4.5 center\_x = c_x + 0.5 = 4.5センター_x _ _ _ _ _ _=c×+0 5=5
中心_y = cy + 0.5 = 10.5 中心\_y = c_y + 0.5 = 10.5センター_y _ _ _ _ _ _=cはい+0 5=1 0 5

予測ボックスの中心座標は次の方法で生成できます:
bx = cx + σ ( tx ) b_x = c_x + \sigma(t_x)b×=c×+s ( t×)
by = cy + σ ( ty ) b_y = c_y + \sigma(t_y)bはい=cはい+s ( tはい)

ここでtx t_xt×ty t_ytはいは実数、σ ( x ) \sigma(x)σ ( x )は前に学習したシグモイド関数であり、次のように定義されます。

σ ( x ) = 1 1 + exp ( − x ) \sigma(x) = \frac{1}{1 + exp(-x)}σ ( x )=1+e x p ( x )1

シグモイドの関数値は0 〜 1 0なので\thicksim 101なので、上式で計算される予測ボックスの中心点は常に 10 行 4 列の小領域内に収まります。

現在tx = ty = 0 t_x=t_y=0t×=tはい=0bx = cx + 0.5 b_x = c_x + 0.5b×=c×+0 5by = cy + 0.5 b_y = c_y + 0.5bはい=cはい+0.5 の場合、予測フレームの中心とアンカー フレームの中心は一致し、どちらも小領域の中心になります

アンカー ボックスのサイズは事前に設定されており、モデル内のハイパーパラメーターとみなすことができます。下の図に描かれているアンカー ボックスのサイズは次のとおりです。

ph = 350 p_h = 350p=3 5 0
pw = 250 p_w = 250p=2 5 0

この時点で、予測フレームのサイズは次の式で生成できます。

bh = フェス b_h = p_h e^{t_h}b=pet
bw = pwetw b_w = p_w e^{t_w}b=pet

結果tx = ty = 0、th = tw = 0 t_x=t_y=0、t_h=t_w=0t×=tはい=0 t=t=0 の場合、予測ボックスはアンカー ボックスと一致します。

tx 、 ty 、 th 、 tw が与えられた場合、 t_x、 t_y、 t_h、 t_wt×tはいttランダムな割り当ては次のとおりです。

tx = 0.2、ty = 0.3、tw = 0.1、th = − 0.12 t_x = 0.2、t_y = 0.3、t_w = 0.1、t_h = -0.12t×=0 2 tはい=0 3 t=0 1 t=0 1 2

すると、図 5 の青枠に示すように、予測フレームの座標は (154.98, 357.44, 276.29, 310.42) として取得できます。



図 5: 予測ボックスの生成


説明:
ここの座標はxywh xywhですx y w h形式。


ここで尋ねます: tx 、 ty 、 tw 、 th t_x、 t_y、 t_w、 t_hのときt×tはいtt値を設定すると、予測フレームは実際のフレームと一致しますか? 質問に答えるには、上記の予測ボックスの座標にbx 、 by 、 bh 、 bw b_x、 b_y、 b_h、 b_w を入力するだけで済みます。b×bはいbbそれを実際のボックスの位置として設定すると、ttを解くことができます。tの値。

令:
σ ( tx ∗ ) + cx = gtx \sigma(t^*_x) + c_x = gt_xs ( tバツ)+c×=gt _×
σ ( ty ∗ ) + cy = gty \sigma(t^*_y) + c_y = gt_ys ( ty)+cはい=gt _はい
pwetw ∗ = gth p_w e^{t^*_w} = gt_hpetw=gt _
pheth ∗ = gtw p_h e^{t^*_h} = gt_wpeth=gt _

求解出可能:( tx ∗ , ty ∗ , tw ∗ , th ∗ ) (t^*_x, t^*_y, t^*_w, t^*_h)( tバツtytwth)

だったらtはネットワークによって予測された出力値、t ∗ t^*tを目標値、それらの間のギャップを損失関数として、ネットワーク パラメータを学習することで回帰問題を確立できますt はt ∗ t^*に十分近いt∗ を計算することで、予測フレームの位置座標とサイズを求めることができます。

予測フレームはアンカー フレームに基づく微調整とみなすことができます。各アンカー フレームには対応する予測フレームがあります。tx 、 ty 、 tw 、 th t_x 、 t_y 、 t_w 、 t_h を決定する必要がありますt×tはいtt、アンカー ボックスに対応する予測ボックスの位置と形状を計算します。

3. 候補領域にラベルを付けます

YOLOv3 では、各領域は 3 つの異なる形状のアンカー ボックスを生成し、各アンカー ボックスは候補領域となります。これらの候補領域については、次のことを理解する必要があります。

  • アンカー ボックスにオブジェクトが含まれているかどうかは、ラベルのオブジェクト性によって表される 2 つのカテゴリの問題とみなすことができます。アンカー フレームにオブジェクトが含まれる場合、objectness=1 となり、予測フレームがポジティブ クラスに属することを示します。アンカー フレームにオブジェクトが含まれない場合、objectness=0 に設定され、アンカー フレームがネガティブ クラスに属することを示します。別のケースとして、いくつかの予測フレームと実際のフレーム間の IoU は非常に大きいですが、最大のものではないため、そのオブジェクト性ラベルを負のサンプルとして 0 に直接設定することは適切ではない可能性があります。この状況を回避するには、 YOLOv3 アルゴリズムは、ボックスのオブジェクト性が 1 ではないことを予測するときに IoU しきい値 iou_threshold を設定しますが、実際のボックスの IoU が iou_threshold より大きい場合、そのオブジェクト性ラベルは -1 に設定され、損失の計算には関与しません。関数。

  • アンカー フレームにオブジェクトが含まれている場合は、対応する予測フレームの中心位置とサイズ、または上記のtx、ty、tw、th t_x、t_y、t_w、t_h を計算する必要があります。t×tはいttいくらにするべきですか。

  • アンカー ボックスにオブジェクトが含まれている場合、特定のカテゴリが何であるかを計算する必要がありますが、ここでは変数 label を使用して、それが属するカテゴリのラベルを表します。

ラベルアンカーボックスにオブジェクトが含まれているかどうか

図 6 に示すように、合計 3 つのターゲットがあり、一番左のポートレートを例に取ると、その実際のフレームは( 133.96 , 328.42 , 186.06 , 374.63 ) (133.96, 328.42, 186.06, 374.63) となります。( 1 3 3 . 9 6 3 2 8 4 2 1 8 6 0 6 3 7 4 6 3 )


図 6: グラウンド トゥルース ボックスの中心と同じ領域にあるアンカー ボックスを選択します

グラウンド トゥルース ボックスの中心点の座標は次のとおりです。

中心 _ x = 133.96 中心\_x = 133.96センター_x _ _ _ _ _ _=1 3 3 9 6

中心 _ y = 328.42 中心\_y = 328.42センター_y _ _ _ _ _ _=3 2 8 4 2

i = 133.96 / 32 = 4.18625 i = 133.96 / 32 = 4.18625=1 3 3 9 6 / 3 2=1 8 6 2 5

d = 328.42 / 32 = 10.263125 d = 328.42 / 32 = 10.263125j=3 2 8 4 2 / 3 2=1 0 2 6 3 1 2 5

図 13 に示すように、行 10、列 4 の小さな四角形に該当します。この小さな正方形の領域では、形状の異なる 3 つのアンカー ボックスを生成できます。図上のアンカー ボックスの数とサイズはA 1 ( 116 , 90 ) 、 A 2 ( 156 , 198 ) 、 A 3 ( 373 , 326 ) A_1(116 , 90 ) です。 )、A_2(156、198)、A_3(373、326)1( 1 1 6 ,9 0 ) 2( 1 5 6 1 9 8 ) 3( 3 7 3 3 2 6 )

これら 3 つの異なる形状のアンカー ボックスと実際のボックスを使用して IoU を計算し、最大の IoU を持つアンカー ボックスを選択します。ここでは計算を簡略化するため、アンカーボックスの形状のみを考慮し、実際のボックスの中心とのオフセットは考慮しませんでした。具体的な計算結果を図 7 に示します



図 7: 選択されたグラウンド トゥルース ボックスとアンカー ボックスの IoU

このうち、実フレームでIoUが最も大きいのはアンカーフレームA3A_3です。3、形状は( 373 , 326 ) (373, 326)です。( 3 7 3 3 2 6 )では、対応する予測フレームのオブジェクトネス ラベルを 1 に設定し、それに含まれるオブジェクト カテゴリは実際のフレーム内のオブジェクトのカテゴリになります。

次に、他の実際のボックスに対応する最大の IoU を持つアンカー ボックスを見つけて、その予測されたボックスのオブジェクト性ラベルを 1 に設定します。合計は20 × 15 × 3 = 900 20 \times 15 \times 3 = 9002 0×1 5×3=9 0 0アンカー ボックスの場合、3 つの予測ボックスのみがポジティブとしてマークされます。

各実際のフレームは、正のオブジェクト性ラベルを持つ 1 つの予測フレームのみに対応するため、いくつかの予測フレームと実際のフレーム間の IoU が大きいが最大のものではない場合、そのオブジェクト性ラベルを負のサンプルとして直接 0 に設定する可能性があります。適切ではありません。この状況を回避するために、YOLOv3 アルゴリズムは IoU しきい値 iou_threshold を設定します。予測されたフレームのオブジェクト性が 1 ではないが、実際のフレームとの IoU が iou_threshold より大きい場合、そのオブジェクト性ラベルは -1 に設定され、損失に関与する 関数の計算。

他のすべての予測ボックスのオブジェクト性ラベルは 0 に設定され、負のクラスを示します。

objectness=1 の予測フレームの場合は、その位置と含まれるオブジェクトの特定の分類ラベルをさらに決定する必要がありますが、objectness=0 または -1 の予測フレームの場合は、その位置とカテゴリは考慮されません。

ラベル予測ボックスの位置サイズ

アンカーボックスのオブジェクト性=1の場合、その微調整に対する予測ボックスの位置の大きさ、すなわちアンカーボックスの位置ラベルを決定する必要がある。

以前にこのような質問をしたことがあります: when tx , ty , tw , th t_x, t_y, t_w, t_ht×tはいtt値を設定すると、予測フレームは実際のフレームと一致しますか? この方法では、予測フレーム座標でbx 、 by 、 bh 、 bw b_x、 b_y、 b_h、 b_w を使用します。b×bはいbb実際のフレームの座標として設定すると、ttを解くことができますtの値。

令:
σ ( tx ∗ ) + cx = gtx \sigma(t^*_x) + c_x = gt_xs ( tバツ)+c×=gt _×
σ ( ty ∗ ) + cy = gty \sigma(t^*_y) + c_y = gt_ys ( ty)+cはい=gt _はい
pwetw ∗ = gtw p_w e^{t^*_w} = gt_wpetw=gt _
pheth ∗ = gth p_h e^{t^*_h} = gt_hpeth=gt _

txの場合∗ t_x^*tバツty∗t_y^*ty、シグモイドの逆関数は計算が簡単ではないため、σ ( tx ∗ ) \sigma(t^*_x) を直接使用します。s ( tバツ)σ ( ty ∗ ) \sigma(t^*_y)s ( ty)を回帰ターゲットとして使用します。

dx ∗ = σ ( tx ∗ ) = gtx − cx d_x^* = \sigma(t^*_x) = gt_x - c_xdバツ=s ( tバツ)=gt _×c×

dy ∗ = σ ( ty ∗ ) = gty − cy d_y^* = \sigma(t^*_y) = gt_y - c_ydy=s ( ty)=gt _はいcはい

tw ∗ = log ( gtwpw ) t^*_w = log(\frac{gt_w}{p_w})tw=l o g (pgt _)

th ∗ = log ( gthph ) t^*_h = log(\frac{gt_h}{p_h})th=l o g (pgt _)

如果( tx , ty , th , tw ) (t_x, t_y, t_h, t_w)( t×tはいtt)はネットワークによって予測された出力値です。( dx ∗ , dy ∗ , tw ∗ , th ∗ ) (d_x^*, d_y^*, t_w^*, t_h^*)( dバツdytwth)として( σ ( tx ) , σ ( ty ) , th , tw ) (\sigma(t_x), \sigma(t_y), t_h, t_w)( s ( t×s ( tはいtt)目標値、それらの間のギャップを損失関数として使用すると、回帰問題を確立でき、ネットワーク パラメーターを学習することで、ttt はt ∗ t^*に十分近いt、これにより、予測ボックスの位置を解決できます。

アンカーフレーム内のオブジェクトカテゴリに注釈を付ける

objectness=1 のアンカー ボックスの場合、その特定のカテゴリを決定する必要があります。前述したように、オブジェクトネスが 1 とマークされたアンカー フレームには、それに対応する実フレームがあり、アンカー フレームが属するオブジェクト カテゴリは、対応する実フレームに含まれるオブジェクト カテゴリです。ここでは、カテゴリ ラベル label を表すためにワンホット ベクトルが使用されています。たとえば、合計 10 個のカテゴリがあり、実フレームに含まれるオブジェクト カテゴリが 2 番目のカテゴリである場合、ラベルは( 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) ( 0,1,0 ,0,0,0,0,0,0,0)( 0 ,1 0 0 0 0 0 0 0 0 )

まとめると図 8 のようになります。



図 8: ラベル付けプロセスの概略図

上記の導入により、YOLOv3 での候補領域のラベル付け方法を予備的に理解し、実際の予測フレームのラベルを取得できます。Paddle では、これらの操作はpaddle.vision.ops.yolo_loss APIにカプセル化されており、損失を計算する際には、この API を呼び出して上記の処理を実装するだけで済みます。次に、YOLOv3 のネットワーク構造を見て、ネットワークが対応する予測値をどのように計算するかを見てみましょう。

3.2 YOLOv3 ネットワーク構造

1.バックボーン

YOLOv3 アルゴリズムで使用されるバックボーン ネットワークは Darknet53 です。Darknet53ネットワークを図 9 に示します。このネットワークは、ImageNet 画像分類タスクで良好な結果を達成しています。検出タスクでは、図の C0 の背後にある平均プーリング、完全接続層、およびソフトマックスが削除され、入力から C0 までのネットワーク構造が検出モデルの基本ネットワーク構造 (バックボーン ネットワークとも呼ばれます) として保持されます。YOLOv3 モデルでは、バックボーン ネットワークに基づいて検出関連のネットワーク モジュールが追加されます。



図 9: Darknet53 のネットワーク構造

以下のプログラムは Darknet53 バックボーンネットワークの実装コードですが、上図の C0、C1、C2 で表される出力データを取り出し、それぞれの形状を確認します C 0 [ 1 , 1024 , 20 , 20 ] C0 [ 1 , 1024, 20, 20]C0 [ 1 _1 0 2 4 2 0 2 0 ]C1 [ 1 , 512 , 40 , 40 ] C1 [1, 512, 40, 40]C1 [ 1 _5 1 2 4 0 4 0 ]C 2 [ 1 , 256 , 80 , 80 ] C2 [1, 256, 80, 80]C2 [ 1 _2 5 6 8 0 8 0 ]

# coding=utf-8
# 导入环境
import os
import random
import xml.etree.ElementTree as ET
import numpy as np
import matplotlib.pyplot as plt
# 在notebook中使用matplotlib.pyplot绘图时,需要添加该命令进行显示
%matplotlib inline
from matplotlib.image import imread
import matplotlib.patches as patches
import cv2
from PIL import Image, ImageEnhance
import paddle
import paddle.nn.functional as F


# 将卷积和批归一化封装为ConvBNLayer,方便后续复用
class ConvBNLayer(paddle.nn.Layer):
    def __init__(self, ch_in, ch_out,  kernel_size=3, stride=1, groups=1, padding=0, act="leaky"):
        # 初始化函数
        super(ConvBNLayer, self).__init__()
        # 创建卷积层
        self.conv = paddle.nn.Conv2D(in_channels=ch_in, out_channels=ch_out, kernel_size=kernel_size, stride=stride, padding=padding, 
            groups=groups, weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Normal(0., 0.02)), bias_attr=False)
        # 创建批归一化层
        self.batch_norm = paddle.nn.BatchNorm2D(num_features=ch_out,
            weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Normal(0., 0.02), regularizer=paddle.regularizer.L2Decay(0.)),
            bias_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(0.0), regularizer=paddle.regularizer.L2Decay(0.)))
        self.act = act

    def forward(self, inputs):
        # 前向计算
        out = self.conv(inputs)
        out = self.batch_norm(out)
        if self.act == 'leaky':
            out = F.leaky_relu(x=out, negative_slope=0.1)
        return out
# 定义下采样模块,使图片尺寸减半
class DownSample(paddle.nn.Layer):
    def __init__(self, ch_in, ch_out, kernel_size=3, stride=2, padding=1):
        # 初始化函数
        super(DownSample, self).__init__()
        # 使用 stride=2 的卷积,可以使图片尺寸减半
        self.conv_bn_layer = ConvBNLayer(ch_in=ch_in, ch_out=ch_out, kernel_size=kernel_size, stride=stride, padding=padding)
        self.ch_out = ch_out
        
    def forward(self, inputs):
        # 前向计算
        out = self.conv_bn_layer(inputs)
        return out
# 定义残差块
class BasicBlock(paddle.nn.Layer):
    def __init__(self, ch_in, ch_out):
        # 初始化函数
        super(BasicBlock, self).__init__()
        # 定义两个卷积层
        self.conv1 = ConvBNLayer(ch_in=ch_in, ch_out=ch_out, kernel_size=1, stride=1, padding=0)
        self.conv2 = ConvBNLayer(ch_in=ch_out, ch_out=ch_out*2, kernel_size=3, stride=1, padding=1)
        
    def forward(self, inputs):
        # 前向计算
        conv1 = self.conv1(inputs)
        conv2 = self.conv2(conv1)
        # 将第二个卷积层的输出和最初的输入值相加
        out = paddle.add(x=inputs, y=conv2)
        return out
# 将多个残差块封装为一个层级,方便后续复用
class LayerWarp(paddle.nn.Layer):
    def __init__(self, ch_in, ch_out, count, is_test=True):
        # 初始化函数
        super(LayerWarp,self).__init__()
        self.basicblock0 = BasicBlock(ch_in, ch_out)
        self.res_out_list = []
        for i in range(1, count):
            # 使用add_sublayer添加子层
            res_out = self.add_sublayer("basic_block_%d" % (i), BasicBlock(ch_out*2, ch_out))
            self.res_out_list.append(res_out)

    def forward(self,inputs):
        # 前向计算
        y = self.basicblock0(inputs)
        for basic_block_i in self.res_out_list:
            y = basic_block_i(y)
        return y
# DarkNet 每组残差块的个数,来自DarkNet的网络结构图
DarkNet_cfg = {
    
    53: ([1, 2, 8, 8, 4])}
# 创建DarkNet53骨干网络
class DarkNet53_conv_body(paddle.nn.Layer):
    def __init__(self):
        # 初始化函数
        super(DarkNet53_conv_body, self).__init__()
        self.stages = DarkNet_cfg[53]
        self.stages = self.stages[0:5]

        # 第一层卷积
        self.conv0 = ConvBNLayer(ch_in=3, ch_out=32, kernel_size=3, stride=1, padding=1)

        # 下采样,使用stride=2的卷积来实现
        self.downsample0 = DownSample(ch_in=32, ch_out=32 * 2)

        # 添加各个层级的实现
        self.darknet53_conv_block_list = []
        self.downsample_list = []
        for i, stage in enumerate(self.stages):
            conv_block = self.add_sublayer("stage_%d" % (i), LayerWarp(32*(2**(i+1)), 32*(2**i), stage))
            self.darknet53_conv_block_list.append(conv_block)
        # 两个层级之间使用DownSample将尺寸减半
        for i in range(len(self.stages) - 1):
            downsample = self.add_sublayer("stage_%d_downsample" % i, DownSample(ch_in=32*(2**(i+1)), ch_out=32*(2**(i+2))))
            self.downsample_list.append(downsample)

    def forward(self,inputs):
        # 前向计算
        out = self.conv0(inputs)
        out = self.downsample0(out)
        blocks = []
        # 依次将各个层级作用在输入上面
        for i, conv_block_i in enumerate(self.darknet53_conv_block_list): 
            out = conv_block_i(out)
            blocks.append(out)
            if i < len(self.stages) - 1:
                out = self.downsample_list[i](out)
        # 将C0, C1, C2作为返回值
        return blocks[-1:-4:-1] 

2. 予測モジュール

上記で、YOLOv3 アルゴリズムでは、ネットワークが 3 セットの結果を出力する必要があることを学びました。

  • 予測ボックスにオブジェクトが含まれているかどうか。これは、objectness=1 の確率としても理解できます。ここでは、ネットワークに実数xxを出力させることができます。xの場合は、Sigmoid ( x ) Sigmoid(x)S i g mo i d ( x ) は、オブジェクト性が正である確率を示しますP obj P_{obj}Pオブジェ_ _

  • オブジェクトの位置と形状を予測します。ネットワークを使用して、オブジェクトの位置と形状を表す 4 つの実数tx 、 ty 、 tw 、 th t_x、 t_y、 t_w、 t_h を出力できます。t×tはいtt

  • オブジェクト クラスを予測します。画像内のオブジェクトの特定のカテゴリ、またはそれが各カテゴリに属する​​確率を予測します。カテゴリの総数は C で、各カテゴリに属する​​オブジェクトの確率を予測する必要があります( P 1 , P 2 , ... , PC ) (P_1, P_2, ..., P_C)( P1P2PC)、ネットワークを使用して C 実数( x 1 , x 2 , . . , x C ) (x_1, x_2, ..., x_C) を( ×1バツ2バツC)、各実数のシグモイド関数を見つけます。P i = Sigmoid ( xi ) P_i = Sigmoid(x_i)P私は=Sigmoid ( x _ _ _ _ _ _私は)とすれば、物体が各カテゴリに属する​​確率を表すことができます。

したがって、予測されたボックスの場合、ネットワークは( 5 + C ) (5 + C)を出力する必要があります。( 5+C )オブジェクトが含まれているかどうか、位置と形状の寸法、および各カテゴリに属する​​確率を特徴付ける実数。

それぞれの小さな正方形領域に 3 つの予測ボックスを生成したため、すべての予測ボックスに対してネットワークによって出力される必要がある予測値の合計数は次のようになります。

[ 3 × ( 5 + C ) ] × m × n [3 \times (5 + C)] \times m \times n[ 3×( 5+C ) ]×メートル×n

もう 1 つのより重要な点は、ネットワーク出力は小さな正方形領域の位置を区別できなければならず、特徴マップを[ 3 × ( 5 + C ) ] × m × n [3]の出力サイズに直接接続できないことです。 \times (5 + C)] \times m \times n[ 3×( 5+C ) ]×メートル×nの全結合層

ここでも上の図を引き続き使用し、複数の畳み込みカーネル プーリング後の特徴マップを観察します。ストライド = 32、640 × 480 640 \times 4806 4 0×4 8 0サイズの入力画像は20 × 15 20\times152 0×1 5特徴マップ; 小さな正方形領域の数はちょうど20 × 15 20\times152 0×これは、特徴マップ上の各ピクセルが元の画像上の小さな正方形の領域に対応できることを意味しますこれが、最初に小さな正方形領域のサイズを 32 に設定する理由です。これにより、小さな正方形領域と特徴マップ上のピクセルをうまく一致させ、空間位置間の対応を解決できます。



図 10: 特徴マップ C0 と小さな正方形領域の形状の比較

以下にもピクセルポイント( i , j ) (i,j)が必要です。(j )は、行 i、列 j の小さな正方形領域によって必要とされる予測値に関連付けられており、各小さな正方形領域は 3 つの予測フレームを生成し、各予測フレームには ( 5 + C ) (5 + C ) が必要です( 5+C )実際の予測値の場合、各ピクセルは3 × ( 5 + C ) 3 \times (5 + C)3×( 5+C )実数。この問題を解決するには、特徴マップ上で複数の畳み込みが実行され、最終的な出力チャネル数は3 × ( 5 + C ) 3 \times (5 + C)3×( 5+C )、生成された特徴マップは、各予測フレームに必要な予測値と微妙に一致することができます。畳み込み後、保証される出力特徴マップ サイズは[ 1 , 75 , 20 , 15 ] [1, 75, 20, 15][ 1 7 5 2 0 1 5 ]各小さな正方形領域によって生成されるアンカー ボックスまたは予測ボックスの数は 3、オブジェクト カテゴリの数は 20、各領域に必要な予測値の数は 3 × ( 5 + 20 ) = 75 3 \times (5 + 20) = 753×( 5+2 0 )=7 5、出力チャンネルの数とまったく同じです。

この時点で、出力特徴マップが候補領域に関連付けられる様子図 11 に示します。

しょうP 0 [ t , 0 : 25 , i , j ] P0[t, 0:25, i, j]P0 [ t , _0:2 5 j ]と入力 t 番目のピクチャ(i, j)(j )最初の予測フレームに必要な 25 個の予測値に対応、P 0 [ t , 25 : 50 , i , j ] P0[t , 25:50 , i , j]P0 [ t , _2 5:5 0 j ]と入力 t 番目のピクチャ(i, j)(j ) 2 番目の予測フレームに必要な 25 個の予測値に対応、P 0 [t, 50:75, i, j] P0[t, 50:75, i, j]P0 [ t , _5 0:7 5 j ]と入力 t 番目のピクチャ(i, j)(j ) 3 番目の予測フレームで必要な 25 個の予測値に対応します。

P 0 [ t , 0 : 4 , i , j ] P0[t , 0:4 , i , j]P0 [ t , _0:4 j ]と入力 t 番目のピクチャ(i, j)(j )最初の予測フレームの位置に対応しますP 0 [ t , 4 , i , j ] P0[t , 4 , i , j]P0 [ t , _4 j ]と入力 t 番目のピクチャ(i, j)(j )最初の予測フレームに対応するオブジェクトネスP 0 [ t , 5 : 25 , i , j ] P0[t , 5:25 , i , j]P0 [ t , _5:2 5 j ]と入力 t 番目のピクチャ(i, j)(j )最初の予測フレームのカテゴリー対応。

このようにして、ネットワーク出力特徴マップを、それぞれの小さな正方形領域によって生成された予測フレームと微妙に一致させることができます。



図 11: 特徴マップ P0 と候補領域の関連付け

バックボーン ネットワークの出力特徴マップは C0 であり、次のプログラムは C0 に対して複数の畳み込みを実行して、予測フレームに関連する特徴マップ P0 を取得します。

class YoloDetectionBlock(paddle.nn.Layer):
    # define YOLOv3 detection head
    # 使用多层卷积和BN提取特征
    def __init__(self,ch_in,ch_out,is_test=True):
        super(YoloDetectionBlock, self).__init__()

        assert ch_out % 2 == 0, \
            "channel {} cannot be divided by 2".format(ch_out)

        self.conv0 = ConvBNLayer(ch_in=ch_in, ch_out=ch_out, kernel_size=1, stride=1, padding=0)
        self.conv1 = ConvBNLayer(ch_in=ch_out, ch_out=ch_out*2, kernel_size=3, stride=1, padding=1)
        self.conv2 = ConvBNLayer(ch_in=ch_out*2, ch_out=ch_out, kernel_size=1, stride=1, padding=0)
        self.conv3 = ConvBNLayer(ch_in=ch_out, ch_out=ch_out*2, kernel_size=3, stride=1, padding=1)
        self.route = ConvBNLayer(ch_in=ch_out*2, ch_out=ch_out, kernel_size=1, stride=1, padding=0)
        self.tip = ConvBNLayer(ch_in=ch_out, ch_out=ch_out*2, kernel_size=3, stride=1, padding=1)
        
    def forward(self, inputs):
        out = self.conv0(inputs)
        out = self.conv1(out)
        out = self.conv2(out)
        out = self.conv3(out)
        route = self.route(out)
        tip = self.tip(route)
        return route, tip

3. マルチスケール検出

上で説明した計算プロセスは、特徴マップ P0 とそのストライド = 32 に基づいています。特徴マップのサイズは比較的小さく、ピクセル数は比較的少なく、各ピクセルの受容野は大きく、非常に豊富な高レベルの意味情報が含まれているため、より大きなターゲットの検出が容易になる可能性があります。より小さいサイズのオブジェクトを検出できるようにするには、より大きいサイズの特徴マップの上に予測出力を構築する必要があります。C2 や C1 レベルの特徴マップ上で直接予測出力を生成すると、十分な特徴抽出が行われておらず、ピクセルに含まれる意味情報が十分に豊富ではないため、予測出力が困難になる可能性があります。効果的な特徴パターンを抽出します。ターゲット検出では、この問題を解決する方法は、高レベルの特徴マップのサイズを拡大し、それを低レベルの特徴マップと融合することです。新しい特徴マップには、豊富なセマンティック情報を含めることができ、より多くのピクセルを含めることができます。より細かい構造を説明します。

具体的なネットワーク実装方法を図 12 に示します。



図 12: マルチレベル出力特徴マップ P0、P1、P2 の生成

YOLOv3 は各領域の中央に 3 つのアンカー ボックスを生成します。3 つのレベルの特徴マップ上に生成されるアンカー ボックスのサイズは P2 [(10×13),(16×30),(33×23)]、 P1 [(30×61),(62×45),(59×119)], P0[(116 × 90), (156 × 198), (373 × 326]. 後の特徴マップで使用されます。特徴マップ上のアンカー フレームのサイズが大きいほど、大きなサイズのターゲットの情報をキャプチャでき、特徴マップ上のアンカー フレームのサイズが小さいほど、小さなサイズのターゲットの情報をキャプチャできます。

したがって、最終的な損失関数の計算とモデルの予測は、これら 3 つのレベルで実行されます。次に、完全なネットワーク構造の定義を実行できます。


例証します:

YOLOv3 では、損失関数は主に次の 3 つの部分で構成されます。

  • 対象物体が含まれるか否かを表す損失関数は、二値クロスエントロピー損失関数を用いて計算される。

  • オブジェクトの位置を表す損失関数。tx 、 ty t_x、 t_yt×tはいバイナリクロスエントロピー損失関数tw 、 th t_w 、 t_hを使用して計算されます。ttL1損失を計算に使用します。

  • オブジェクト カテゴリを表す損失関数。バイナリ クロスエントロピー損失関数を使用して計算されます。


3.3 エンドツーエンドのトレーニング

YOLOv3のトレーニング プロセスを図 13 に示します。入力画像は、3 つのレベルの出力特徴マップ P0 (ストライド = 32)、P1 (ストライド = 16)、および P2 (ストライド = 8) を取得するために特徴抽出の対象となり、対応して使用されます。異なるサイズ 対応するアンカー ボックスと予測ボックスを生成し、これらのアンカー ボックスをマークするための小さな正方形の領域。

  • P0 レベルの特徴マップ、 32 × 32 32\times32の使用に対応3 2×3各領域の中央にサイズ2の小さな正方形が[116, 90] [116, 90][ 1 1 6 9 0 ][ 156 、 198 ] [156、198][ 1 5 6 1 9 8 ][ 373 、 326 ] [373、 326][ 3 7 3 3 2 6 ]の 3 種類のアンカー ボックス。

  • P1 レベルの特徴マップ、 16 × 16 16\times16の使用に対応1 6×1各領域の中央に、[ 30 、 61 ] [ 30 、 61 ]の6つのサイズの小さな正方形が生成されます。[ 3 0 6 1 ][ 62 、 45 ] [62 、 45 ][ 6 2 4 5 ][ 59 、 119 ] [59 、 119 ][ 5 9 1 1 9 の3種類のアンカーボックス。

  • P2 レベルの特徴マップ、 8 × 8 8\times8の使用に対応8×各領域の中央に、[ 10 , 13 ] [10, 13]のサイズの8 つのサイズの小さな正方形が生成されます。[ 1 0 ,1 3 ][ 16 、 30 ] [16 、 30 ][ 1 6 3 0 ][ 33 、 23 ] [33、 23][ 3 3 2 3 ]の 3 種類のアンカー ボックス。

3 つのレベルの特徴マップは、対応するアンカー ボックス間のラベルに関連付けられ、損失関数が確立され、合計損失関数は 3 つのレベルの損失関数の合計に等しくなります。損失関数を最小限に抑えることで、エンドツーエンドのトレーニング プロセスを開始できます。



図 13: エンドツーエンドのトレーニング プロセス

完全なネットワークを定義するときは、 paddle.vision.ops.yolo_loss APIを使用して損失関数を計算する必要があります。この API は、上記の候補領域のラベル付けとマルチスケール損失関数の計算を均一にカプセル化します。

paddle.vision.ops.yolo_loss(x、gt_box、gt_label、アンカー、anchor_mask、class_num、ignore_thresh、downsample_ratio、gt_score=None、use_label_smooth=True、name=None、scale_x_y=1.0)

主要なパラメータは次のように説明されます。

  • x: 出力特徴マップ。
  • gt_box: グラウンド トゥルース ボックス。
  • gt_label: グラウンド トゥルース ボックスのラベル。
  • ignore_thresh は、予測フレームと実際のフレームの IoU しきい値がignore_thresh を超える場合、ネガティブ サンプルとして使用されず、YOLOv3 モデルでは 0.7 に設定されます。
  • downsample_ratio、特徴マップ P0 のダウンサンプリング比。Darknet53 バックボーン ネットワークを使用する場合は 32 です。
  • gt_score、実際のボックスの信頼度。ミックスアップ手法を使用するときに使用されます。
  • use_label_smooth、トレーニング手法、使用しない場合は False に設定します。
  • name、「yolov3_loss」などのレイヤーの名前。デフォルト値はNoneで、通常は設定する必要はありません。

4. YOLOv3 モデルの予測

予測プロセスのフローチャート14 は次のとおりです。



図 14: 予測プロセス

予測プロセスは 2 つのステップに分けることができます。

  1. 予測されたボックスの位置とカテゴリのスコアは、ネットワーク出力を通じて計算されます。
  2. 非最大抑制を使用して、重なりが大きい予測ボックスを排除します。

4.1 予測ボックスの計算

paddle.vision.ops.yolo_boxを使用して、3 つのレベルの特徴マップに対応する予測ボックスとスコアを計算できます。

paddle.vision.ops.yolo_box(x、img_size、アンカー、class_num、conf_thresh、downsample_ratio、clip_bbox=True、name=None、scale_x_y=1.0)

主要なパラメータには次の意味があります。

  • x、ネットワーク出力機能マップ(前述の P0 または P1、P2 など)。
  • img_size、入力画像サイズ。
  • アンカー、使用されるアンカーのサイズ ([10、13、16、30、33、23、30、61、62、45、59、119、116、90、156、198、373、326] など)
  • Anchor_mask: 各レベルで使用されるアンカーのマスク [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
  • class_num、オブジェクト カテゴリの数。
  • conf_thresh、信頼度のしきい値。このしきい値よりスコアが低い予測ボックスの位置の値は、計算せずに直接 0.0 に設定されます。
  • downsample_ratio、特徴マップのダウンサンプリング率。たとえば、P0 は 32、P1 は 16、P2 は 8 です。
  • name=None の場合、通常、「yolo_box」などの名前を設定する必要はなく、デフォルト値は None です。

戻り値にはボックスとスコアの 2 つの項目が含まれます。ボックスはすべての予測ボックスの座標値、スコアはすべての予測ボックスのスコアです。

予測フレーム スコアの定義は、カテゴリの確率に、予測フレームに対象物体が含まれるかどうかの物体確率を乗算したものです。

スコア = P obj ⋅ P 分類 スコア = P_{obj} \cdot P_{分類}スコア_ _ _ _=Pオブジェ_ _Pクラス分け_ _ _ _ _ _ _ _ _ _ _ _

paddle.vision.ops.yolo_boxP0、P1、P2の3レベルの特徴マップに対応する予測フレームとスコアを呼び出してつなぎ合わせることで、各カテゴリに属する​​全ての予測フレームとそのスコアが得られる

この時点で、完全な YOLOv3 ネットワークを定義できます。完全なコードは次のとおりです。

# 定义上采样模块
class Upsample(paddle.nn.Layer):
    def __init__(self, scale=2):
        # 初始化函数
        super(Upsample,self).__init__()
        self.scale = scale

    def forward(self, inputs):
        # 前向计算
        # 获得动态的上采样输出形状
        shape_nchw = paddle.shape(inputs)
        shape_hw = paddle.slice(shape_nchw, axes=[0], starts=[2], ends=[4])
        shape_hw.stop_gradient = True
        in_shape = paddle.cast(shape_hw, dtype='int32')
        out_shape = in_shape * self.scale
        out_shape.stop_gradient = True

        # 上采样计算
        out = paddle.nn.functional.interpolate(x=inputs, scale_factor=self.scale, mode="NEAREST")
        return out
# 定义完整的YOLOv3模型
class YOLOv3(paddle.nn.Layer):
    def __init__(self, num_classes=20):
        # 初始化函数
        super(YOLOv3,self).__init__()

        self.num_classes = num_classes
        # 提取图像特征的骨干代码
        self.block = DarkNet53_conv_body()
        self.block_outputs = []
        self.yolo_blocks = []
        self.route_blocks_2 = []
        # 生成3个层级的特征图P0, P1, P2
        for i in range(3):
            # 添加从ci生成ri和ti的模块
            yolo_block = self.add_sublayer("yolo_detecton_block_%d" % (i),
                YoloDetectionBlock(ch_in=512//(2**i)*2 if i==0 else 512//(2**i)*2 + 512//(2**i), ch_out = 512//(2**i)))
            self.yolo_blocks.append(yolo_block)

            num_filters = 3 * (self.num_classes + 5)

            # 添加从ti生成pi的模块,这是一个Conv2D操作,输出通道数为3 * (num_classes + 5)
            block_out = self.add_sublayer("block_out_%d" % (i),
                paddle.nn.Conv2D(in_channels=512//(2**i)*2, out_channels=num_filters, kernel_size=1, stride=1, padding=0,
                       weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Normal(0., 0.02)),
                       bias_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(0.0), regularizer=paddle.regularizer.L2Decay(0.))))
            self.block_outputs.append(block_out)
            if i < 2:
                # 对ri进行卷积
                route = self.add_sublayer("route2_%d"%i, ConvBNLayer(ch_in=512//(2**i), ch_out=256//(2**i), kernel_size=1, stride=1, padding=0))
                self.route_blocks_2.append(route)
            # 将ri放大以便跟c_{i+1}保持同样的尺寸
            self.upsample = Upsample()

    def forward(self, inputs):
        # 前向运算
        outputs = []
        blocks = self.block(inputs)
        for i, block in enumerate(blocks):
            if i > 0:
                # 将r_{i-1}经过卷积和上采样之后得到特征图,与这一级的ci进行拼接
                block = paddle.concat([route, block], axis=1)
            # 从ci生成ti和ri
            route, tip = self.yolo_blocks[i](block)
            # 从ti生成pi
            block_out = self.block_outputs[i](tip)
            # 将pi放入列表
            outputs.append(block_out)

            if i < 2:
                # 对ri进行卷积调整通道数
                route = self.route_blocks_2[i](route)
                # 对ri进行放大,使其尺寸和c_{i+1}保持一致
                route = self.upsample(route)

        return outputs

    def get_loss(self, outputs, gtbox, gtlabel, gtscore=None, anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
                 anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]], ignore_thresh=0.7, use_label_smooth=False):
        # 损失计算函数
        self.losses = []
        downsample = 32
        # 对三个层级分别求损失函数
        for i, out in enumerate(outputs): 
            anchor_mask_i = anchor_masks[i]
            # 使用paddle.vision.ops.yolo_loss 直接计算损失函数
            loss = paddle.vision.ops.yolo_loss(x=out, gt_box=gtbox, gt_label=gtlabel, gt_score=gtscore, anchors=anchors, anchor_mask=anchor_mask_i, 
                    class_num=self.num_classes, ignore_thresh=ignore_thresh, downsample_ratio=downsample, use_label_smooth=False)
            self.losses.append(paddle.mean(loss)) 
            # 下一级特征图的缩放倍数会减半
            downsample = downsample // 2 
        # 对所有层级求和
        return sum(self.losses) 

    def get_pred(self, outputs, im_shape=None, anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326],
                 anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]], valid_thresh = 0.01):
        # 预测函数
        downsample = 32
        total_boxes = []
        total_scores = []
        for i, out in enumerate(outputs):
            anchor_mask = anchor_masks[i]
            anchors_this_level = []
            for m in anchor_mask:
                anchors_this_level.append(anchors[2 * m])
                anchors_this_level.append(anchors[2 * m + 1])
            # 使用paddle.vision.ops.yolo_box 直接计算损失函数
            boxes, scores = paddle.vision.ops.yolo_box(x=out, img_size=im_shape, anchors=anchors_this_level, class_num=self.num_classes,
                   conf_thresh=valid_thresh, downsample_ratio=downsample, name="yolo_box" + str(i))
            total_boxes.append(boxes)
            total_scores.append(paddle.transpose( scores, perm=[0, 2, 1]))
            downsample = downsample // 2
        # 将三个层级的结果进行拼接
        yolo_boxes = paddle.concat(total_boxes, axis=1)
        yolo_scores = paddle.concat(total_scores, axis=2)
        return yolo_boxes, yolo_scores

4.2 マルチカテゴリー最大抑制

以前の計算プロセスで、ネットワークは同じターゲットに対して複数の検出を実行する可能性があります。これにより、同じオブジェクトに対して複数の予測フレームが作成されることになります。したがって、モデル出力を取得した後、非最大抑制 (nms) を使用して冗長なフレームを削除する必要があります。基本的な考え方は、同じオブジェクトに対応する予測フレームが複数ある場合、最も高いスコアを持つ予測フレームのみが選択され、残りの予測フレームは破棄されるというものです。

2つの予測ボックスが同一の物体に相当すると判断し、その基準をどう設定するか?

2 つの予測ボックスのカテゴリが同じで、それらの位置の重複が比較的大きい場合、それらは同じターゲットを予測していると考えることができます。非最大値抑制の方法は、特定のカテゴリの最高スコアを持つ予測フレームを選択し、どの予測フレームとその IoU がしきい値より大きいかを確認し、これらの予測フレームを破棄することです。ここでのIoUの閾値はハイパーパラメータであり、事前に設定する必要があり、YOLOv3モデルでは0.5に設定されています。

IOU を計算するコードを以下に示します。

# 计算IoU,其中边界框的坐标形式为xyxy
def box_iou_xyxy(box1, box2):
    # 获取box1左上角和右下角的坐标
    x1min, y1min, x1max, y1max = box1[0], box1[1], box1[2], box1[3]
    # 计算box1的面积
    s1 = (y1max - y1min + 1.) * (x1max - x1min + 1.)
    # 获取box2左上角和右下角的坐标
    x2min, y2min, x2max, y2max = box2[0], box2[1], box2[2], box2[3]
    # 计算box2的面积
    s2 = (y2max - y2min + 1.) * (x2max - x2min + 1.)
    
    # 计算相交矩形框的坐标
    xmin = np.maximum(x1min, x2min)
    ymin = np.maximum(y1min, y2min)
    xmax = np.minimum(x1max, x2max)
    ymax = np.minimum(y1max, y2max)
    # 计算相交矩形行的高度、宽度、面积
    inter_h = np.maximum(ymax - ymin + 1., 0.)
    inter_w = np.maximum(xmax - xmin + 1., 0.)
    intersection = inter_h * inter_w
    # 计算相并面积
    union = s1 + s2 - intersection
    # 计算交并比
    iou = intersection / union
    return iou

非最大値抑制の具体的な実装コードは以下のnms関数で定義されていますが、説明が必要なのは、データセットには複数のカテゴリのオブジェクトが含まれているため、ここでは複数カテゴリの非最大値抑制を行う必要があるということです。実装原理は非最大値抑制と同じですが、カテゴリごとに非最大値抑制が必要な点が異なり、実装コードを以下に示しますmulticlass_nms

# 非极大值抑制
def nms(bboxes, scores, score_thresh, nms_thresh):
    # 对预测框得分进行排序
    inds = np.argsort(scores)
    inds = inds[::-1]
    keep_inds = []
    # 循环遍历预测框
    while(len(inds) > 0):
        cur_ind = inds[0]
        cur_score = scores[cur_ind]
        # 如果预测框得分低于阈值,则退出循环
        if cur_score < score_thresh:
            break
        # 计算当前预测框与保留列表中的预测框IOU,如果小于阈值则保留该预测框,否则丢弃该预测框
        keep = True
        for ind in keep_inds:
            current_box = bboxes[cur_ind]
            remain_box = bboxes[ind]
            # 计算当前预测框与保留列表中的预测框IOU
            iou = box_iou_xyxy(current_box, remain_box)
            if iou > nms_thresh:
                keep = False
                break
        if keep:
            keep_inds.append(cur_ind)
        inds = inds[1:]
    return np.array(keep_inds)

# 多分类非极大值抑制
def multiclass_nms(bboxes, scores, score_thresh=0.01, nms_thresh=0.45, pos_nms_topk=100):
    batch_size = bboxes.shape[0]
    class_num = scores.shape[1]
    rets = []
    for i in range(batch_size):
        bboxes_i = bboxes[i]
        scores_i = scores[i]
        ret = []
        # 遍历所有类别进行单分类非极大值抑制
        for c in range(class_num):
            scores_i_c = scores_i[c]
            # 单分类非极大值抑制
            keep_inds = nms(bboxes_i, scores_i_c, score_thresh, nms_thresh)
            if len(keep_inds) < 1:
                continue
            keep_bboxes = bboxes_i[keep_inds]
            keep_scores = scores_i_c[keep_inds]
            keep_results = np.zeros([keep_scores.shape[0], 6])
            keep_results[:, 0] = c
            keep_results[:, 1] = keep_scores[:]
            keep_results[:, 2:6] = keep_bboxes[:, :]
            ret.append(keep_results)
        if len(ret) < 1:
            rets.append(ret)
            continue
        ret_i = np.concatenate(ret, axis=0)
        scores_i = ret_i[:, 1]
        # 如果保留的预测框超过100个,只保留得分最高的100个
        if len(scores_i) > pos_nms_topk:
            inds = np.argsort(scores_i)[::-1]
            inds = inds[:pos_nms_topk]
            ret_i = ret_i[inds]
        rets.append(ret_i)
    return rets

おすすめ

転載: blog.csdn.net/weixin_43273742/article/details/122929070