# 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.

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

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

同樣設定輸出門檻為(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>

</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>

</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 |伸縮縫|