# YOLOv4-tiny 模型訓練教學
###### tags: `YOLO`
>本篇記錄如何使用自己的資料集,利用YOLO進行訓練
## Step 0: Environment setting
1. 下載 darknet
```bash=
git clone https://github.com/AlexeyAB/darknet.git
```
2. 編譯 Yolo (根據自身電腦環境,修改 Makefile)
參考 https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-make
## Step 1: Label data
安裝 labelImg (Windows 作業系統)
https://drive.google.com/drive/folders/13f6XFDPI-cguUk9UUH9SH5AqrDU1gkTp?usp=share_link
開啟 labelImg 軟體後:
Step 1. 選擇資料夾
選取欲訓練的照片資料夾,並選取儲存 Label 的資料夾。(儲存Label 的資料夾,建議與訓練的照片資料夾相同)
![](https://i.imgur.com/74bKhue.jpg)
Step 2. 選擇儲存成 Yolo 格式
![](https://i.imgur.com/rJ4H3gJ.jpg)
Step 3. 標記目標物件
點選 Create RectBox,框選目標物件,並輸入物件名稱
![](https://i.imgur.com/XvaEV3g.jpg)
Step 4. 存檔
![](https://i.imgur.com/Df0I0D5.jpg)
此時,會根據 xxx.jpg ,產生對應的 xxx.txt。
xxx.txt 為Yolo格式,作為訓練 Yolo 模型的檔案。
完成所有資料的標記後,將所有訓練的照片檔案(.jpg)以及標記檔案(.txt),放至同一份資料夾。
## Step 2: Data preprocessing (Option)
:::info
此步驟為在 Step 1. Label data,使用 VOC 作為 label 的儲存格式(.xml),而非 Yolo 格式(.txt),則需要將 xml 檔案轉 txt 檔案,以符合 Yolo 的訓練格式。
若 Step 1. Label data 有正確將 label 設為 Yolo 格式(.txt),則不必進行此步驟。
:::
定義getYoloFormat函數,功用如下
1. 把物件類別轉成數字
2. 把 images 裡的照片重新命名,並存入名為 "yolo" 的資料夾
3. 把 labels 裡的 xml 轉成 txt 並重新命名存入名為 "yolo" 的資料夾
4. xxx.jpg 和 xxx.tx 名稱會一一對應
程式碼如下,並將status_dic內改成對應的label
```python=
from bs4 import BeautifulSoup
import os
import shutil
from IPython.display import clear_output
status_dic = {'person':0} #用dictionary 記錄label的名稱
def getYoloFormat(filename, label_path, img_path, yolo_path, newname):
with open(label_path+filename, 'r') as f:
soup = BeautifulSoup(f.read(), 'xml')
imgname = soup.select_one('filename').text #讀取xml
image_w = soup.select_one('width').text
image_h = soup.select_one('height').text
ary = []
for obj in soup.select('object'): #取出xmin, xmax, ymin, ymax及name
xmin = int(obj.select_one('xmin').text) #並且用status_dictionary 來轉換name,good =>2
xmax = int(obj.select_one('xmax').text)
ymin = int(obj.select_one('ymin').text)
ymax = int(obj.select_one('ymax').text)
objclass = status_dic.get(obj.select_one('name').text)
x = (xmin + (xmax-xmin)/2) * 1.0 / float(image_w) #YOLO吃的參數檔有固定的格式
y = (ymin + (ymax-ymin)/2) * 1.0 / float(image_h) #先照YOLO的格式訂好x,y,w,h
w = (xmax-xmin) * 1.0 / float(image_w)
h = (ymax-ymin) * 1.0 / float(image_h)
ary.append(' '.join([str(objclass), str(x), str(y), str(w), str(h)]))
if os.path.exists(img_path+imgname+'.jpg'): # 圖片本來在image裡面,把圖片移到yolo資料夾下
shutil.copyfile(img_path+imgname+'.jpg', yolo_path+newname+'.jpg') #同時把yolo參數檔寫到yolo之下
with open(yolo_path+newname+'.txt', 'w') as f:
f.write('\n'.join(ary))
elif os.path.exists(img_path+imgname): #有的labelImg名稱已經自動加上.jpg
shutil.copyfile(img_path+imgname, yolo_path+newname+'.jpg')
with open(yolo_path+newname+'.txt', 'w') as f:
f.write('\n'.join(ary))
def update_progress(progress):
bar_length = 20
if isinstance(progress, int):
progress = float(progress)
if not isinstance(progress, float):
progress = 0
if progress < 0:
progress = 0
if progress >= 1:
progress = 1
block = int(round(bar_length * progress))
clear_output(wait = True)
text = "Progress: [{0}] {1:.1f}%".format( "#" * block + "-" * (bar_length - block), progress * 100)
print(text)
```
呼叫getYoloFormat
```python=
labelpath = 'darknet/labels' # label xml資料夾
imgpath = 'darknet/images' # image 資料夾
yolopath = 'darknet/yolo' # 存放轉換成yolo訓練格式的照片及txt檔案
total_progress = len(os.listdir(labelpath))
progress = 0
for idx, f in enumerate(os.listdir(labelpath)): #透過getYoloFormat將圖像和參數檔全部寫到YOLO下
progress += 1
try:
if f.split('.')[1] == 'xml':
getYoloFormat(f, labelpath, imgpath, yolopath, str(idx))
else:
print('Error, the file should be the xml')
except Exception as e:
print(e)
update_progress(progress/total_progress)
```
## Step 3: 切割訓練及測試資料集
範例程式碼
```python=
import os
import random
# 存放 yolo 要訓練的照片及txt檔案的資料夾
datasets = ['絕對路徑/yolo/'+ f for f in os.listdir('./yolo/') if f.endswith('.jpg')]
random.shuffle(datasets)
len_dataset = int(len(datasets) * 0.7)
random.shuffle(datasets)
print('All data length',len(datasets))
print(len_dataset)
print(len(datasets)-len_dataset)
with open('/root/notebooks/yolov4/darknet/cfg/train.txt', 'w') as f:
f.write('\n'.join(datasets[:len_dataset]))
with open('/root/notebooks/yolov4/darknet/cfg/test.txt', 'w') as f:
f.write('\n'.join(datasets[len_dataset:]))
```
## Step 4: 編輯訓練參數檔案
### 此步驟將建立以下三個檔案:
- obj.data
建立obj.data檔案
說明:
classes:總共的物件數量
train: 包含所有欲訓練的照片及標記檔案路徑
valid: 包含所有欲測試的照片及標記檔案路徑
names: obj.names 路徑位置
backup: 存放模型的訓練權重的資料夾位置
範例:
```
classes= 1
train = cfg/train.txt
valid = cfg/test.txt
names = cfg/obj.names
backup = backup
```
- obj.name
建立obj.name檔案,輸入所有的label種類。
注意: label 名稱須按照 Step 1. 的標記順序擺放。
例如:
```
person
```
- obj.cfg
以 Yolov4-tiny 為範例,`./darknet/cfg/` 底下,修改 yolov4-tiny-custom.cfg,複製一份新檔案,重新命名為 `obj.cfg`,並修改 `filter` 以及 `classes` :
修改方式:
:bulb: 尋找關鍵字:yolo,並參考下圖的相對位置做更改
:bulb: 修改規則為 [yolo] 內的classes以及 [yolo] 標籤前的filters,如下圖
![](https://i.imgur.com/jq7JpwR.png =50%x)
filter數量計算方式為: 3*(classes + 5)
參考資料: Yolo 官方網站
https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects
## Step 5: Train
- 下載 pre-trained model (以 Yolov4-tiny 為範例)
https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29
- 開始訓練
於 `darknet` 底下執行 (需改成對應的參數路徑)
```bash=
./darknet detector train cfg/obj.data cfg/obj.cfg yolov4-tiny.conv.29 -map -dont_show
```
## Step 6: Test
查看辨識結果,產生於darknet/prediction.jpg
```bash=
./darknet detector test cfg/obj.data cfg/obj.cfg cfg/your_weight.weights
```
(option) 設置 thresh ,可查看更低機率的class
```bash=
./darknet detector test cfg/obj.data cfg/obj.cfg cfg/your_weight.weights -thresh 0.1
```
:bulb:TroubleShooting
辨識不出東西,設 -thresh 0 ,發現預測機率都是 0% 時,將darknet/MakeFile中的cudnn設為0,並重新make clean 以及 make
## 番外 : 儲存log檔,並查看訓練過程的loss以及IOU
- 於darknet底下建立visulization資料夾,執行以下指令開始訓練
```bash=
./darknet detector train cfg/obj.data cfg/yolov3_PB.cfg cfg/darknet53.conv.74 2>1 | tee visualization/yolov3_PB.log
```
- 讀取檔案
```python=
import inspect
import os
import random
import sys
def extract_log(log_file, new_log_file, key_word):
with open(log_file, 'r') as f:
with open(new_log_file, 'w') as train_log:
for line in f:
# 去除多gpu的同步log
if 'Syncing' in line:
continue
# 去除除零错误的log
if 'nan' in line:
continue
if key_word in line:
train_log.write(line)
f.close()
train_log.close()
extract_log('yolov3_PH.log','train_log_loss.txt','images')
extract_log('yolov3_PH.log','train_log_iou.txt','IOU')
```
- avg loss
```python=
#!/usr/bin/python
#coding=utf-8
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 根据log_loss.txt的行数修改lines, 修改训练时的迭代起始次数(start_ite)和结束次数(end_ite)。
lines = 3000
start_ite = 0 # train_log_loss.txt里面的最小迭代次数
end_ite = 3000 # train_log_loss.txt里面的最大迭代次数
step = 10 # 跳行数,决定画图的稠密程度
igore = 100 # 当开始的loss较大时,忽略前igore次迭代,注意这里是迭代次数
y_ticks = [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3] # 纵坐标的值
data_path = 'train_log_loss.txt' # log_loss的路径。
result_path = 'avg_loss' # 保存结果的路径。
names = ['loss', 'avg', 'rate', 'seconds', 'images']
result = pd.read_csv(data_path, skiprows=[x for x in range(lines) if (x<lines*1.0/((end_ite - start_ite)*1.0)*igore or x%step!=9)], error_bad_lines=\
False, names=names)
result.head()
for name in names:
result[name] = result[name].str.split(' ').str.get(1)
result.head()
print(result.tail())
for name in names:
result[name] = pd.to_numeric(result[name])
result.dtypes
print('result',result['avg'].values)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
x_num = len(result['avg'].values)
tmp = (end_ite-start_ite - igore)/(x_num*1.0)
x = []
for i in range(x_num):
x.append(i*tmp + start_ite + igore)
#print(x)
print('total = %d\n' %x_num)
print('start = %d, end = %d\n' %(x[0], x[-1]))
ax.plot(x, result['avg'].values, label='avg_loss')
#ax.plot(result['loss'].values, label='loss')
plt.yticks(y_ticks)#如果不想自己设置纵坐标,可以注释掉。
plt.grid()
ax.legend(loc = 'best')
ax.set_title('The loss curves')
ax.set_xlabel('batches')
fig.savefig(result_path)
#fig.savefig('loss')
```
![](https://i.imgur.com/AC40388.png)
- IOU loss
```python=
#!/usr/bin/python
#coding=utf-8
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#根据log_iou修改行数
lines = 952812
step = 5000
start_ite = 0
end_ite = 50200
igore = 100
data_path = 'train_log_iou.txt' #train_log_iou的路径。
result_path = 'avg_iou' #保存结果的路径。
names = ['Region Avg IOU', 'Class', 'Obj', 'No Obj', '.5_Recall', '.7_Recall', 'count']
#result = pd.read_csv('log_iou.txt', skiprows=[x for x in range(lines) if (x%10==0 or x%10==9)]\
result = pd.read_csv(data_path, skiprows=[x for x in range(lines) if (x<lines*1.0/((end_ite - start_ite)*1.0)*igore or x%step!=0)]\
, error_bad_lines=False, names=names)
result.head()
for name in names:
result[name] = result[name].str.split(': ').str.get(1)
result.head()
result.tail()
for name in names:
result[name] = pd.to_numeric(result[name])
result.dtypes
x_num = len(result['Region Avg IOU'].values)
tmp = (end_ite-start_ite - igore)/(x_num*1.0)
x = []
for i in range(x_num):
x.append(i*tmp + start_ite + igore)
#print(x)
print('total = %d\n' %x_num)
print('start = %d, end = %d\n' %(x[0], x[-1]))
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(x, result['Region Avg IOU'].values, label='Region Avg IOU')
#ax.plot(result['Avg Recall'].values, label='Avg Recall')
plt.grid()
ax.legend(loc='best')
ax.set_title('The Region Avg IOU curves')
ax.set_xlabel('batches')
fig.savefig(result_path)
```
![](https://i.imgur.com/L9mXTJ1.png)