---
# System prepended metadata

title: PDS
tags: [郁翔, yolo]

---

# PDS
###### tags: `郁翔`, `yolo`

## 環境建置

目前環境建置會遇到兩個問題，第一，TensorFlow可支援的CUDA版本問題，又CUDA版本還有與cuDNN和顯卡驅動程式相容的問題，安裝和版本對應可參考官方[說明](https://www.tensorflow.org/install/gpu)。第二，為了配合較新版的TensorFlow，有些依賴的Package要採用特定版本，被淘汰的函式也需要調整寫法。

### Anaconda
關於GPU運算系統面的設置可參考子琦提供的[資料](https://ai-edu-for-everyone.medium.com/win10-%E7%92%B0%E5%A2%83-%E4%BD%BF%E7%94%A8-anaconda-%E5%BB%BA%E7%BD%AE-rtx-3000-%E7%B3%BB%E5%88%97%E7%9A%84-tensorflow-%E6%B7%B1%E5%BA%A6%E5%AD%B8%E7%BF%92%E7%92%B0%E5%A2%83-9929e2a7c313)，訓練時的Anaconda環境建置和版本建議如下：

```
conda create --name yolo python=3.8
conda env list

conda activate yolo
conda list

## Modeling
pip install tensorflow==2.4.0 # pip install tf-nightly-gpu
pip install opencv-python
pip install pillow==8.1.0 #conda install pillow
pip install matplotlib==3.3.2 #conda install matplotlib

## Cloud Deployment
pip install flask
pip install fastapi
pip install uvicorn
pip install gunicorn==20.0.4
pip install google-cloud-storage==1.35.0

conda deactivate

conda env remove --name yolo
```

### Tensorflow
Tensorflow 2.X後整合Keras到自己的package裡，較新版的Keras也大多直接調用Tensorflow，可以從解包Keras觀察到很多module內部已沒有code，而是直接透過`from tensorflow.keras.* import *`從Tensorflow調用，所以關於yolo中的`from keras.* import *`都可改成`from tensorflow.keras.* import *`。

此外有部分module或function不確定是整合不完全，還是已被汰，目前針對yolo會出問題的部分做出以下調整，有些是針對訓練、有些是針對測試做調整，調整完應該可以直接在兩環境切換運行：
* 在pd-yolo目錄下的`./yolo.py`程式碼中，加入`tf.compat.v1.disable_eager_execution()`，並將`K.get_session`改成`tf.compat.v1.keras.backend.get_session`。
* 在pd-yolo目錄下的`./yolo3/model.py`程式碼中，將`tf.Print`改成`tf.compat.v1.Print`、將`K.control_flow_ops.while_loop`改成`tf.while_loop`。

## 前置處理
### 資料清理
建議根據以下要點提升資料品質
* 提升照片解析度和清晰度
* 固定拍攝角度和環境（在資料量不大的情形下可考慮根據不同場景建立不同模型）
* 建立照片標記SOP
  * 各類別定義（含程度）
  * 完整性
  * 場景相關資料欄位

### 資料分割
一般來說，會將資料分成訓練集和測試集，應用交叉驗證（cross validation）在訓練集上選擇較理想的參數組合，再使用測試集評估模型。但面對資料量較大或模型較複雜的情況，交叉驗證會十分耗時，此時可將資料分成三部分，使用訓練集訓練模型、驗證集檢視模型目前的泛化（generalization）能力、測試集評估模型。此處將資料按8:1:1分為
* PDS_train.txt(訓練集)
* PDS_valid.txt(驗證集)
* PDS_test.txt(測試集)

因為標記的結果並不平衡(unbalance)，所以希望分割後每個類別在各資料集比都是8:1:1；又一張照片可能有多個標記結果，但測試上以照片為單位較為方便，所以希望抽樣的方式是以照片為單位，而不是以標記的結果為單位。

為此，必須設計個比較tricky的抽樣方法，將每張照片按其標記的類別組成給予註記，以目前資料為例，若一張照片被註記為 [10000] 表示其含有1個第一類物件、若一張照片被註記為 [02010] 表示其含有2個第二類物件和1個第四類物件。根據照片的註記分層抽樣，也就是將相同註記的照片按比例分割，如此就能以照片為單位進行抽樣，又能讓每個類別也按比例分割。

## 模型訓練
### 重新訓練 
##### Model_Weights_Path : ==./logs/004/ep023-loss15.632-val_loss17.558.h5==
#### 訓練建議
為了確認目前模型極限，先在未調整模型的架構下重新訓練，以便後續觀察，順便提供一些訓練時的建議。遷移學習（transfer learning）很適合應用在目前情境，為了讓模型比較穩健（robustness）經過測試建議可按以下流程進行，每一步以其前一步所得結果作為預訓練權重，fine tune模型的一部分；又越後面fine tune的參數越多、模型越接近收斂，初始的學習率要選擇越小：

1. Use yolo_weights.h5 as pretrained weight and freeze all but 3 output layers.
2. Use the stable result of step 1 as pretrained weight and freeze darknet53 body.
3. Use the stable result of step 2 as pretrained weight and unfreeze all of the layers.

![training flow](https://hackmd.io/_uploads/Hkj6Q-pyO.png)

除了步驟一，其餘步驟在選擇前一步驟的結果作為預訓練和最後的模型選擇，建議可依據下述標準。訓練的過程我們可以逐步紀錄每一epoch的訓練損失（training loss）和驗證損失（validation loss），理論上，訓練損失在沒有正則處理的情況會收斂到0、有正則處理的情況會收斂到某個值，**觀察訓練損失的收斂情況可以判斷模型還有沒有優化空間**，來決定是否要增加epoch。驗證集是訓練過程保留但不用來訓練的部分，**驗證損失作為模型目前泛化能力的檢視依據**，當訓練損失還在下降或收斂時，驗證損失卻不減反增，表示模型可能有overfitting的現象，就可以將開始不減反增前的那一個epoch視為較合適的結果。

![pretrain selection](https://hackmd.io/_uploads/Sku_1JQTv.png)

#### 模型評估
大部分模型在訓練的過程都會受到隨機初始設定的影響，建議可多實驗幾次找出評估後最好的結果。此次建模採用上述步驟實驗多次，記錄其中兩次結果分別記為Rebuild_1和Rebuild_2，並將思漢之前實驗結果記為Original。設定輸出門檻為(Score_threshold, IOU_threshold) = (.3, .45)，並在相信predicted bounding box和ground truth的IoU > .5為true positive下，比較mAP（mean average precision）表現，得到重訓練的過程至少能得到與原模型相當的表現，但有機會得到更好的結果。

![result compare](https://hackmd.io/_uploads/HyKlpeX6D.png)

同樣設定輸出門檻為(Score_threshold, IOU_threshold) = (.3, .45)，進一步比較以不同IoU為true positive下的mAP表現，平均來看，重訓練的過程至少能得到與原模型相當的結果，但當IoU的門檻提高時，重訓練的得到的結果相比原模型有較好的表現，可以推論重訓練在物件框的預測比較穩健。

| IOU       |  .45  |  .50  |  .55  |  .60  |  .65  |  .70  |  avg  |
| --------- |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| Original  | 45.55 | 44.39 | 42.37 | 37.83 | 31.18 | 22.77 | 37.35 |
| Rebuild_1 | 46.22 | 45.28 | 43.67 | 40.41 | 34.49 | 27.43 | 39.58 | 
| Rebuild_2 | 45.45 | 44.30 | 41.88 | 37.23 | 32.50 | 26.17 | 37.92 |

附註：實務上，我們可以根據呈現的切角和模型對各個物件偵測的能力量身訂製Score_threshold和IOU_threshold，不同模型在不同門檻下可能有相同表現，難以比較，透過mAP能得到一個與門檻無關的綜合衡量標準。

關於mAP的介紹可參考[此篇](https://jonathan-hui.medium.com/map-mean-average-precision-for-object-detection-45c121a31173)，簡單來說，某類別物件在固定IOU_threshold並在不同Score_threshold設定下，可得到不同的precision-recall組合，計算precision-recall curve下的面積得到AP（average precision），一個與Score_threshold決定無關的衡量準則，借助Original和Rebuild_1比較物件Cover的AP表現，可觀察AP越高，通常表示越有機會找到更好的precision-recall組合。mAP就是平均各物件偵測的表現，以不同IoU為true positive下的mAP更可以衡量模型在物件框預測的平均表現。

<center>

![cover AP](https://hackmd.io/_uploads/r1GMPQuTv.png =600x600)

</center>

### 模型調整
#### Grid Sensitive
##### Model_Weights_Path : ==./logs/010/ep050-loss14.132-val_loss17.121.h5==
yolo v3對物件中心點$(x, y)$的預測公式如下，模型並不是直接預測中心點相對整張圖的座標，而是與某Grid為基準點的偏移量，這種做法在訓練過程會相對穩定

\begin{split}
x = s \cdot ( g_x + \sigma(t_x)) \\
y = s \cdot ( g_y + \sigma(t_y))
\end{split}

其中$\sigma$為sigmoid激活函式，顯然，$x$不能完全等於$s \cdot g_x$或$s \cdot ( g_x + 1 )$，且逼近$s \cdot g_x$或$s \cdot ( g_x + 1 )$時需要$t_x$是一個極大的負值或正值，導致中心點落在Grid邊緣的預測成效不佳，為此調整公式如下，將sigmoid加上一個縮放或偏移便能有效改善此問題，其中$\alpha$可選擇略大於1的數就好

\begin{split}
x = s \cdot ( g_x + \alpha \cdot \sigma(t_x) - (\alpha - 1) / 2) \\
y = s \cdot ( g_y + \alpha \cdot \sigma(t_y) - (\alpha - 1) / 2)
\end{split}

本模型選擇$\alpha = 1.05$代入上述調整式，以前段最理想的Rebuild_1結果作為預訓練權重，繼續訓練得到結果記為Rebuild_3，同樣設定輸出門檻為(Score_threshold, IOU_threshold) = (.3, .45)，並在相信predicted bounding box和ground truth的IoU > .5為true positive下比較mAP表現。

| IOU       |  45   |  50   |  55   |  60   |  65   |  70   |  avg  |
| --------- |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| Rebuild_1 | 46.22 | 45.28 | 43.67 | 40.41 | 34.49 | 27.43 | 39.58 | 
| Rebuild_3 | 50.17 | 48.58 | 45.20 | 40.72 | 36.31 | 28.17 | 41.53 |

#### IoU Loss
##### Model_Weights_Path : ==./logs/012/ep044-loss13.916-val_loss17.827.h5==
IoU常作為物件偵測任務中對物件框的評價指標，即predicted bounding box和ground truth間的IoU越大效果越好。yolo v3對預測物件框的損失是採用Smooth L1 Loss，不能真實反應IoU擬合程度，為此可考慮在整個損失函數結構中，針對幾何結構的損失改為或加入IoU Loss

$$
\text{Adjusted Loss function} = \text{Original Loss function} + \text{IoU Loss}
$$

IoU Loss發展到現在已有許多變種，此處本模型採用最基本的IoU Loss，即計算predicted bounding box和ground truth的IoU並取負log，以前段最理想的Rebuild_3結果作為預訓練權重，繼續訓練得到的結果記為Rebuild_4，同樣設定輸出門檻為(Score_threshold, IOU_threshold) = (.3, .45)，並在相信predicted bounding box和ground truth的IoU > .5為true positive下比較mAP表現。

| IOU       |  45   |  50   |  55   |  60   |  65   |  70   |  avg  |
| --------- |:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
| Rebuild_3 | 50.17 | 48.58 | 45.20 | 40.72 | 36.31 | 28.17 | 41.53 |
| Rebuild_4 | 51.47 | 48.88 | 46.93 | 39.88 | 35.59 | 28.66 | 41.90 |

#### 門檻決策
mAP是個綜合評估模型好壞的依據，並不能幫助我們決定合適的precision-recall組合，實務上，還是得在不同的Score_threshold和IOU_threshold下描繪precision-recall curve得以幫助決策。

Score_threshold是物件框的可信度門檻值，小於此門檻值的物件框會被删除；IOU_threshold是同類別物件框的IoU門檻值，大於此門檻值的同類別物件會視為重複而被删除。調低Score_threshold或調高IOU_threshold會得到較多的物件框，反之，會得到較少的物件框。

得到的物件框太多，可能因物件的重複辨識或辨識率不佳導致precision太低；得到的物件框太少，可能因很多物件沒辨識出來導致recall太低，我們可以調整Score_threshold和IOU_threshold讓最後輸出結果依需求導向較高的precision或recall。

實際操作，可以在輸出門檻分別設定為(Score_threshold, IOU_threshold) = (.01, .45), (.01, .50), (.01, .55), (.01, .60)下產出結果，並描繪不同類別在不同IOU_threshold下的precision-recall curve，給定precision-recall期許的最低標準，藉由滿足標準的precision-recall組合反推Score_threshold和IOU_threshold的設定。

以Rebuild_4的模型為例，我們希望precision-recall分別至少要0.8-0.5，透過輔助線得知右上角區塊有無符合標準的precision-recall組合，若有可從中選取最理想的組合，若無則要考慮降低標準。

<center>

![threshold_decision](https://hackmd.io/_uploads/S1rytFyJt.png =650x650)

</center>

接著，若符合標準的precision-recall組合不只一組，如何決定最理想組合？極大化precision或極大化recall是相對容易的需求，取而代之，F1-Scoreu定義為precision和recall的調和平均數，可以得到一個概略的綜合指標

$$
\text{F}_1 \text{- Score} = \frac{2}{\text{precision}^{-1} + \text{recall}^{-1}} = 2 \cdot \frac{\text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}}
$$

回到Rebuild_4模型，藉由F1-Score選擇最理想的precision-recall組合，找到其對應的Score_threshold和IOU_threshold設定，比較Original的結果，得到precision提升3(5)個百分點、recall提升4(5)個百分點、accuracy提升4(3)個百分點。

**Original**
| Class       |Score | IOU  | TP |Precision |Recall|
| ----------- |:----:|:----:|:--:|:--------:|:----:|
| Cover       | 0.30 | 0.45 | 49 |   0.78   | 0.50 |
| Alligator   | 0.30 | 0.45 | 64 |   0.78   | 0.63 |
| Patch       | 0.30 | 0.45 | 119|   0.82   | 0.53 |
| Potholes    | 0.30 | 0.45 | 33 |   0.75   | 0.55 |
| LT-Crack    | 0.30 | 0.45 | 37 |   0.61   | 0.13 |
|              | | | |**0.75(0.78)**|**0.47(0.55)**|

**Rebuild_4**
| Class       |Score | IOU  | TP |Precision |Recall|
| ----------- |:----:|:----:|:--:|:--------:|:----:|
| Cover       | 0.31 | 0.45 | 60 |   0.82   | 0.61 |
| Alligator   | 0.30 | 0.45 | 69 |   0.81   | 0.68 |
| Patch       | 0.34 | 0.45 | 120|   0.84   | 0.54 |
| Potholes    | 0.49 | 0.45 | 34 |   0.85   | 0.57 |
| LT-Crack    | 0.45 | 0.45 | 46 |   0.60   | 0.16 |
|              | | | |**0.78(0.83)**|**0.51(0.60)**|

#### 衡量指標
$$
\text{Precision} =  \frac{\text{實際為此類別的樣本}}{\text{所有被預測為此類別的樣本}}
$$
$$
\text{Recall} =  \frac{\text{實際被預測為此類別的樣本}}{\text{所有為此類別的樣本}}
$$

Precision是所有被預測為此類別的樣本中實際為此類別的比例，檢查模型有沒有誤判，Recall是所有此類別的樣本中被正確預測出來的比例，檢查模型有沒有缺漏。以人孔蓋為例，所有被預測為人孔蓋的樣本中實際為人孔蓋的比例有0.82；所有人孔蓋的樣本被正確預測出來比例有0.61。

通常模型”最後”會需要決策某些門檻，這決策影響的是我們所能承受的風險，同時影響著我們重視或想呈現的切角，在門檻決策上，Precision和Recall會互相牽制，當你希望Precision越高Recall就會越低、Precision越低Recall就會越高。因此要同時提升Precision和Recall不是從最後決策時下手，而是一開始建模就要考量，一般來說有兩個切角，一是模型本質，可以考慮替換模型或調整模型參數；二是訓練的樣本，可以考慮增加訓練的樣本數或提升樣本標記的質量。

我們的目的是預測”破壞”，我們希望預測出來的要盡量正確，寧願某些破壞沒有被預測出來（這個描述對應Recall），也不希望不應該是破壞的被我們當作是破壞（這個描述對應Precision），根據上述的風險考量，在門檻決策時會偏好高Precision。


### 
| ClassID |      Class      | Class_c |
|:-------:|:----------------|:--------|
|        1|AlligatorCrack   |鱷魚狀裂縫|
|        2|LongitudinalCrack|縱向裂縫|
|        3|TransverseCrack  |橫向裂縫|
|        4|Patch            |補綻|
|        5|PipePatch        |管溝|
|        6|CoverEven        |人手孔(平)|
|        7|CoverBump        |人手孔(凸)|
|        8|CoverRecess      |人手孔(凹)|
|        9|CoverPatch       |人手孔(含補綻)|
|       10|DitchCover       |水溝蓋|
|       11|Pothole          |坑洞|
|       12|ExpansionJoints  |伸縮縫|
