今更ながらのYOLOv3

世間ではYOLOv10が発表されている中、今更YOLOv3なんて需要がないという話はありますが調べたことをまとめておきます。


## YOLOv3とは？
### **Q: YOLOv3ってなんぞや？**

**A: コレ↓**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/16bd1c6f-1b17-884b-b5db-7ccd7a24c75f.png)

ネットワーク的な工夫としては以下が挙げられる。

- Residual Block
- 1x1 Convolution
- UpSampling
- 3つのScaleのYOLO Layer

### **Q: いやいや、YOLOv3ってそもそも何するもの？**

**A: 物体検知(Object Detection)を行う深層学習(Deep Learning)モデル**

由来はYou only look onceというイケイケなもの

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/073d1e3c-8b73-504b-7c04-0242024092ae.png)

- **歴史:**
  - YOLO (v1): 2016年
  - YOLOv2: 2017年
  - YOLOv3: 2018年

現在は、YOLOv4、YOLOv5、YOLOXも存在します。v3まではJoseph Redmon氏がリードオーサーでしたが、v4からは引退しリードオーサーが変わりました。
最新はYOLOv10ですね。

### YOLOv3の予測内容
YOLOv3は以下の2つを同時に予測する

1. **Bounding Boxの座標 + 物体である確信度**
   - 中心座標: x, y
   - 矩形の幅と高さ: w, h
   - 確信度 (0〜1)
2. **クラス分類の条件付き確率**
   - 各クラスに対して独立に予測 (0〜1)

※1、2は上の値が直接予測されるわけではなく
　予測結果を変換する必要がある(詳細は後ほど)

![yolo1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/d1823e67-c8cd-eae9-e255-abecdc1fb3cc.png)


## YOLOのAnchorとグリッドって何？
YOLOを調べているとよく出てくるAnchorとグリッドについてまとめる。

### YOLOv3の予測のための準備
* YOLOv3は事前に使用するデータセットの正解ラベル情報から、Bounding Boxの大きさ（幅と高さ）に当たりを付けている
    * ※具体的には正解ラベルの幅と高さをk-meansによって9個のクラスタに分け、各クラスタの重心を代表値として使用している？らしい

* 9個の代表値(によってできるBox)のことをPrior、Anchorと言ったりする

* 実際の予測はAnchorからどれだけずれているかを補正する形で行っている

* 要するにベースとなる矩型の形を9個事前に持っており、それを微調整する形で予測するということ

### モデルが予測するもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/d31add93-e182-e660-2637-1d79bfeb3b6a.png)

事前に用意した9個のAnchorを各Scaleの予測に3個ずつ割り当てる
→各Scaleの1pixel(グリッド)で3個の予測を行う

各Scaleの各pixel(グリッド)で「(4+1+クラス数)x3」次元の予測をする
* 4：Bounding Boxの予測
* 1：物体があるかの確信度の予測
* クラス数(図の80)：物体である時に各クラスに該当する条件付き確率
* 3：Anchorの数

**【Point】**
1. 入力画像をグリッドに分割する
2. YOLO layer(Detection Result)が3つあり、
    * Scale1 → 小さな物体
    * Scale2 → 中くらいの物体
    * Scale3 → 大きな物体
    
    を検出すること(複数スケールの利用)


### Scale結果(feature map)と入力画像(グリッド分割)との対応
![image.png](https://blog.paperspace.com/content/images/2018/04/yolo-5.png)

入力画像をScaleごとにサイズに合わせたグリッドに分割している
例）
Scale3のサイズ(13x13)　
→入力画像を縦横に13グリッドへの分割を意味する
　　　　　　　　　　　　　↓
Scaleでの1pixelと入力画像の1グリッドが対応づけられる
（両者ともに13x13に分割されているということ）
　　　　　　　　　　　　　↓
正解ラベルの情報から正解領域の中心座標を求める
→中心座標が入っているグリッドに注目
→その座標に対応するScale内のpixelにて結果の予想をする精神
(→学習時は、この対応するpixelからLossを計算する)

※推論時は逆の流れ：図の下から上へと流れる流れで推論
　Prediction Feature Map → Image Grid

### 複数スケールを使用する理由 (かなりざっくり)
![image.png](https://blog.paperspace.com/content/images/2018/04/yolo_Scales-1.png)

・大きなグリッド → 大きな物体を検出
・小さなグリッド → 小さな物体を検出しやすい

ということらしい…。

一応割り当てているAnchorのサイズは
13x13 → 大きいもの
52x52 → 小さいもの
となっていそうではある。

### 複数スケールを使用する理由 (YOLOv3のネットワーク向上から)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/c699c046-f21d-d57e-c3f0-7d71383746ac.png)

YOLOv3では(d)のFeature Pyramid Networkを採用している
(一段深い特徴量をUpsamplingしてConcat)

CNNとしては特徴マップの深さが
・浅→エッジ等の大雑把な特徴
・深→犬等の意味的な特徴
を捉える(これは一般的に言われていること)。

小さなサイズで捉えた意味的な特徴を大きなサイズへと反映させることで精度が上がる

らしい…。


## モデルの出力の変換

### Bounding Box編

#### 準備：YOLOで使われる座標系 (3種類)
![yolo2.PNG](https://pystyle.info/wp/wp-content/uploads/2021/07/pytorch-yolov3-explain-yolo-layer_05.svg)

1. 正規化された座標系：<font color="red">YOLO形式の正解ラベルは①の形で持っている</font>
    画像の左上を (0, 0)(0,0)、右下を (1, 1)(1,1) とした座標系です。
    →画像全体に対しどの辺の位置(割合)にいるかを表す
    
2. グリッド座標系：<font color="red">Bounding Boxへの変換はグリッド座標系上で行われる</font>
    $𝐺_𝑤$, $𝐺_ℎ$を横、縦のグリッド数としたとき、特徴マップの左上を (0, 0)、右下を($𝐺_𝑤$, $𝐺_ℎ$)とした座標系です。
    →座標が、グリッド何個分の位置にあるかを表す
3. 画像座標系：<font color="red">結果を描画するときには②→③の変換をする必要がある、SSDとかのPascal VOC形式は3.ベース</font>
    $𝐼_𝑤$, $𝐼_ℎ$を入力画像の幅及び高さとしたとき、画像の左上を(0, 0)、右下を($𝐼_𝑤$, $𝐼_ℎ$)とした座標系です。
    →一般的な画像処理で使われる座標

**【計算例】**
𝐺_𝑤=𝐺_ℎ=13、𝐼_𝑤=𝐼_ℎ=416の時

1.の中心座標(0.5, 0.5)
　　↓×13(=𝐺_𝑤)
2.(6.5, 6.5)
　　↓×32(=𝐼_𝑤/ 𝐺_𝑤)
3.(213, 213)

#### Bounding Boxへの変換
![yolo3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/f85331e3-46ad-b6ae-658c-7e1426f2c1dd.png)

変換後は以下のようになります。
![image.png](https://pystyle.info/wp/wp-content/uploads/2021/07/pytorch-yolov3-explain-yolo-layer_06.svg)

この変換はグリッド座標系上での話になります。
以下4つが最終的な変換後の形：
$𝑏_𝑥=𝑔_𝑖+𝜎(𝑡_𝑥 )$
$𝑏_𝑦=𝑔_𝑗+𝜎(𝑡_𝑦 )$
$𝑏_w=𝑒^{𝑡_𝑤} 𝑎_𝑤$
$𝑏_h=𝑒^{𝑡_ℎ} 𝑎_ℎ$

𝜎(⋅)はシグモイド関数、$𝑎_𝑤$, $𝑎_ℎ$はAnchorの幅と高さ
$𝑔_𝑖$, $𝑔_𝑗$ グリッドの位置(左から3番目、上から2番目的なやつ)を表す

$𝑡_𝑥$, $𝑡_𝑦$にシグモイドをかますことで、値が0〜1になり、
中心座標がグリッド内のどこにあるかが表現できる

$𝑡_𝑤$, $𝑡_ℎ$に指数関数をかますことで、値が正となるので、
基準となるAnchorのサイズを倍率調整することで領域の
幅と高さが表現できる


### クラス確率編

#### Objectness Scoreの変換
![yolo4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/307c8cfa-1d34-09c3-021c-fe7583728166.png)

$𝑝_𝑜$は物体であるかどうかを表す確率にするために
$$𝑝_𝑜 ← 𝜎(𝑝_𝑜)$$ 
とシグモイド関数をかます。

ここはこれだけ

#### Class Scoresの変換

![yolo5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/5e45b4c2-2b2b-4d5f-c3fa-37bfe65a9859.png)

多クラス分類はよくソフトマックス関数が使われる
(各値が0~1で総和が1となるため確率として扱える)

しかし、YOLOv3ではソフトマックス関数ではなく、全てのクラスの出力をシグモイド関数に通す
$$𝑝_𝑘←𝜎(𝑝_𝑘 )　(𝑘はクラスのインデックス)$$

変換後の値が、物体が存在するときに各クラスに該当する条件付き確率となる。

**【シグモイド関数を使う理由】**
ソフトマックス関数では複数クラスに該当する場合を扱えないが、シグモイド関数であれば可能なため

例）
ある物体がWomanかつPersonとマルチラベルの時
ソフトマックス：
　Woman=0.8, Person=0.15, … (合計1)
シグモイド
　Woman=0.8, Person=0.7, … (それぞれが0~1)
のように、シグモイドでは複数ラベルで検出可能とのこと。

※閾値が0.2であれば、ソフトマックスでのPersonは検出されない。



#### 最終的な確率への変換

これまでで、対応グリッドに物体が存在する確率 ($p_O $) と物体であったときに該当クラスの物体である条件付確率 ($p_c$) を求めることができた。 
![yolo6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/6eb434dd-719a-0218-fd2f-d44cfab556ed.png)


これをもとに、条件付確率 $𝑃(𝐴∩𝐵)=𝑃_𝐴 (𝐵)×𝑃(𝐴)$ を使用して「物体であってかつ該当クラスである確率」を求めることができる。

A＝物体である、B＝犬である という事象の時
・物体であって犬である確率$𝑃(𝐴∩𝐵)$
・物体である確率$𝑃(𝐴)$
・物体であった時に犬である確率$𝑃_𝐴(𝐵)$
であり、

<font coloR=red>物体であって犬である確率 ＝ 物体である確率 x 物体であった時に犬である確率</font>
($𝑃(𝐴∩𝐵)=𝑃_𝐴 (𝐵)×𝑃(𝐴)$)
となる。

よって閾値を適当に決め、「物体であって犬である確率>閾値」となる場合にそのグリッドのAnchorでは犬が検出されることになる。
(犬以外のクラスも同様に考える)

**【モデルの出力からの確率との対応】**
シグモイドで確率に変換した値は
Objectness Score：
　物体である確率
Class Scores：
　物体であった時そのクラスである確率
　(条件付き確率)
なので、2つの確率をかけることで
「物体であってかつそのクラスである確率」
を求めることができる！


## 予測の流れのまとめ
![image.png](https://camo.qiitausercontent.com/0b56a309478a496a47326c28e9d6d2ac055e85f2/68747470733a2f2f63646e2d616b2e662e73742d686174656e612e636f6d2f696d616765732f666f746f6c6966652f6d2f6d697368696d616e617473756b692f32303138303631342f32303138303631343134343030372e706e67)

①入力画像をグリッドに分割
　13x13、26x26、52x52　の3種類

②各グリッドにて以下2つを行う
　・Bounding Box予測＋物体である確率の予測
　　YOLOv3全体では (13x13+26x26+52x52)x3 個の
　　Bounding Box予測を行う
　・物体だった場合の(条件付き)クラス確率の予測

③条件付きクラス確率 x 物体である確率 > 閾値
　$(P(𝑂𝑏𝑗∩𝐶𝑙𝑠)=𝑃(𝐶𝑙𝑠│𝑂𝑏𝑗)×𝑃(𝑂𝑏𝑗)>𝑡ℎ𝑟𝑒𝑠ℎ𝑜𝑙𝑑)$
　→閾値を超えたら、そのクラスの物体が
　　存在すると判定する　


## Loss関数ってどうなっているの？

以下のようにLoss関数は3つの要素からなる：
![yolo7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594896/f0ff5d47-1f66-b589-e7a5-9860091b2c5e.png)


**【各種変数詳細】**
* G：グリッド数、B：グリッドのアンカー数(=3)、C：YOLOの検出クラス数(=論文では80)
* $𝜆_{𝑏𝑏𝑜𝑥}, 𝜆_{𝑜𝑏𝑗}, 𝜆_{𝑐𝑙𝑠}$：各要素のバランスのための重み(ハイパーパラメータ)
* $𝑡_𝑥, 𝑡_𝑦, 𝑡_𝑤, 𝑡_ℎ$：モデルの予測結果(Bounding Box部分)→グリッド座標系に変換したもの
* $\hat{𝑡_𝑥}, \hat{t_y}, \hat{𝑡_𝑤}, \hat{𝑡_ℎ}$：正解ラベルのBounding Box情報(正規化座標系)→グリッド座標系に変換したもの
* $𝑖𝑑_{𝑖,𝑗}^{𝑜𝑏𝑗} [⋅]$：注目しているグリッド内に物体があれば(ラベル情報から判定)1、なければ0を返す関数　($𝑖𝑑_{𝑖,𝑗}^{𝑛𝑜𝑜𝑏𝑗}[⋅]$は0/1が逆のもの)
* $𝑝_𝑜$, $𝑝_𝑘$：モデルの予測結果(Objectness Score, Class Scores部分)※シグモイド関数に通す前
* 𝜎(⋅)：シグモイド関数
* $\hat{𝑦_𝑘}$：クラスに関する正解ラベルの1-hotラベル

### IoUとその改良版 (GIoU)
![iou1](https://cvml-expertguide.net/wp-content/uploads/2022/02/iou-768x382.png)
IoUとは物体検出の評価指標で、正解データ$B^{gt}$と推論結果$B^{pred}$の範囲が「どの程度重なっているか」を示すもの。

**【Point】**
値は0~1をとる！
・ピッタリ重なっていれば 1
・全然重なっていなければ 0

**【補足】**
基本的なIoUはこの通りだが、改良版もある
(GIoU、DIoU、CIoU …)


### 【参考】Non-Maximum Suppression (NMS)
![NMS1](https://miro.medium.com/v2/resize:fit:1100/format:webp/1*6d_D0ySg-kOvfrzIRwHIiA.png)

同じ物体に対し、Bounding Boxがいくつも検出されるのを抑制する

最終的な確率(Class Confidence)が最大のBoxを残し、IoUが閾値(例えば0.5)よりも大きくなる他のBoxを出力結果から外すもの

上の画像はうまくいく例であるが、以下のような画像だと、真ん中の白い馬のみ検出され、両端の馬が検出されなくなる課題もある。
![NMS2](https://miro.medium.com/v2/resize:fit:1100/format:webp/1*cNHc0xK78TFvtnzbMiOKJQ.jpeg)

→対応策としてソフトNMSというのがあるらしい

日本語では非最大抑制というらしい。

### Binary Cross Entropy
背景は結果にベルヌーイ分布やカテゴリカル分布を仮定した時の対数尤度であるが割愛する。

**単純な2クラス分類**：𝑥をモデルの出力結果(確率)、𝑦を正解ラベル(0 or 1)
→ $𝐵𝐶𝐸(𝑥, 𝑦) = −(𝑦 \log⁡(𝑥)+(1−𝑦)\log⁡(1−𝑥))$
　・$y=1$　⇨　前半が使われ、確率$x$が大きい時BCEは小さくなる
　・$y=0$　⇨　後半が使われ、確率$x$が小さい時BCEは小さくなる

**多クラス分類**：上の拡張、𝑥をモデルの出力結果(確率)のベクトル、𝑦を正解ラベル(0 or 1)のベクトル
→ $𝐵𝐶𝐸(𝑥, 𝑦) = −\sum_{𝑘=0}^𝐶 (𝑦_𝑘 \log⁡(𝑥_𝑘)+(1−𝑦_𝑘)\log⁡(1−𝑥_𝑘))$
　※Cは分類したいクラス数(=ベクトルの次元数)、kはクラスのインデックス
　2クラスの時と同様に以下のようになる。クラスごとに計算を行い和or平均をとる
　・$𝑦_𝑘 = 1$　⇨　前半が使われ、確率𝑥_𝑘が大きい時BCEは小さくなる
　・$𝑦_𝑘 = 0$　⇨　後半が使われ、確率𝑥_𝑘が小さい時BCEは小さくなる

## 参考リンク集

- [YOLOv3論文](https://arxiv.org/abs/1804.02767)
- [Darknet](https://github.com/AlexeyAB/darknet)
- [Pytorch実装](https://github.com/ultralytics/yolov3)
- [YOLO layerの解説](https://pystyle.info/pytorch-yolov3-explain-yolo-layer/)
- [GIoU](https://python-ai-learn.com/2021/02/06/giou/)
- [BCEのざっくりとした背景](https://tips-memo.com/cross-entropy)
- [YOLOv3の工夫](https://qiita.com/cyberailab/items/850806c08af08853bac8)
- [NMSについて](https://towardsdatascience.com/non-maximum-suppression-nms-93ce178e177c)

---