最近一直在嘗試用pytorch版本的Tiny yolo v3,來訓練自己的數據集。為記錄下整個過程,在原創博客:https://blog.csdn.net/sinat_27634939/article/details/89884011的基礎上,補充了一點東西。
主要流程分為六步:
一、數據集制作
1、首先,我們要對自己的數據進行標注,使用的工具是labelimg。Iabelimg可以直接網頁搜索下載exe,運行使用。也可以在python的環境下,輸入命令:pip install labelimg,在conda管理的python環境中安裝labelimg,運行方法就是直接在該conda環境下CMD輸入labelimg即可運行。labelimg打開后的效果如下圖所示。
之后,將你的數據集圖像所在文件夾設置為Open Dir,新建一個文件夾作為存在Annotation的文件夾,設置為Change Save Dir,標注得到的xml文件格式如下所示。
<annotation> <folder>Desktop</folder> <filename>BloodImage_00000.jpg</filename> <path>/Users/xxx/Desktop/BloodImage_00000.jpg</path> <source> <database>Unknown</database> </source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>cell</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>200</xmin> <ymin>337</ymin> <xmax>304</xmax> <ymax>446</ymax> </bndbox> </object> </annotation>
其中,最主要的部分就是bndbox內的,就是我們所標記的人工標注框。
注:在標記數據之前,最好先把圖片數據的文件名修改一下,這里放一個我的文件批量重命名代碼鏈接:https://gitee.com/alexbd/rename_file_all
雖然寫的文件名是視頻文件批量重命名,但是,只要對文件后綴進行修改,就可以對任何文件夾內的同一格式文件進行批量重命名。
二、訓練代碼
YOLO有官方的代碼,我們這里采用的是github上的鏈接:https://github.com/ultralytics/yolov3,git下來。之后建議創建一個專門用於yolo的conda環境,安裝pytorch等需要的包,詳細見requirements文件。
另外,為了更好的訓練,需要安裝apex。
安裝apex方法:
1、從該鏈接:https://github.com/NVIDIA/apex 鏈接上git到你的電腦上;
2、從里面的requirements文件中依次安裝需要的依賴包。
在你這個yolo的conda環境下,依次執行:
pip install cxxfilt pip install tqdm pip install numpy pip install PyYAML pip install pytest
3、完成后,在apex的根目錄下,python運行安裝apex命令,
python setup.py install
當看到如下圖示時,就說明安裝apex成功了。
三、數據預處理
為了能夠用clone下來的工程進行訓練和預測,我們需要對數據進行處理,以適應相應的接口。
1、將細胞數據Annotations和JPEGImages放入data目錄下,並新建文件ImageSets,labels,復制JPEGImages,重命名images,
2、在根目錄下新建makeTxt.py,將數據分成訓練集,測試集和驗證集,其中比例可以在代碼設置,代碼如下:
import os import random trainval_percent = 0.1 train_percent = 0.9 xmlfilepath = 'data/Annotations' txtsavepath = 'data/ImageSets' total_xml = os.listdir(xmlfilepath) num = len(total_xml) list = range(num) tv = int(num * trainval_percent) tr = int(tv * train_percent) trainval = random.sample(list, tv) train = random.sample(trainval, tr) ftrainval = open('data/ImageSets/trainval.txt', 'w') ftest = open('data/ImageSets/test.txt', 'w') ftrain = open('data/ImageSets/train.txt', 'w') fval = open('data/ImageSets/val.txt', 'w') for i in list: name = total_xml[i][:-4] + '\n' if i in trainval: ftrainval.write(name) if i in train: ftest.write(name) else: fval.write(name) else: ftrain.write(name) ftrainval.close() ftrain.close() fval.close() ftest.close()
在ImageSets得到四個文件,其中我們主要關注的是train.txt,test.txt,val.txt,文件里主要存儲圖片名稱。
3、運行根目錄下voc_label.py,得到labels的具體內容以及data目錄下的train.txt,test.txt,val.txt,這里的train.txt與之前的區別在於,不僅僅得到文件名,還有文件的具體路徑。voc_label.py的代碼如下
import xml.etree.ElementTree as ET import pickle import os from os import listdir, getcwd from os.path import join sets = ['train', 'test','val'] classes = ["RBC"]#我們只是檢測細胞,因此只有一個類別 def convert(size, box): dw = 1. / size[0] dh = 1. / size[1] x = (box[0] + box[1]) / 2.0 y = (box[2] + box[3]) / 2.0 w = box[1] - box[0] h = box[3] - box[2] x = x * dw w = w * dw y = y * dh h = h * dh return (x, y, w, h) def convert_annotation(image_id): in_file = open('data/Annotations/%s.xml' % (image_id)) out_file = open('data/labels/%s.txt' % (image_id), 'w') tree = ET.parse(in_file) root = tree.getroot() size = root.find('size') w = int(size.find('width').text) h = int(size.find('height').text) for obj in root.iter('object'): difficult = obj.find('difficult').text cls = obj.find('name').text if cls not in classes or int(difficult) == 1: continue cls_id = classes.index(cls) xmlbox = obj.find('bndbox') b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text), float(xmlbox.find('ymax').text)) bb = convert((w, h), b) out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n') wd = getcwd() print(wd) for image_set in sets: if not os.path.exists('data/labels/'): os.makedirs('data/labels/') image_ids = open('data/ImageSets/%s.txt' % (image_set)).read().strip().split() list_file = open('data/%s.txt' % (image_set), 'w') for image_id in image_ids: list_file.write('data/images/%s.jpg\n' % (image_id)) convert_annotation(image_id) list_file.close()
labels文件下的具體labels信息
data目錄下train.txt
四、配置文件
1.在data目錄下新建rbc.data,配置訓練的數據,內容如下
classes=1 train=data/train.txt valid=data/test.txt names=data/rbc.names backup=backup/ eval=coco
2.在data目錄下新建rbc.names,配置預測的類別,內容如下
3.網絡結構配置,在原工程下cfg目錄下有很多的yolov3網絡結構,我們本次采用的是yolov3-tiny.cfg
具體參數的意義可以參考博客YOLOV3實戰4:Darknet中cfg文件說明和理解和yolo配置文件的參數說明和reorg層的理解!
因為我們只是估計了一個類,所以需要對cfg文件進行修改,yolov3-tiny.cfg
[net] # Testing batch=1 subdivisions=1 # Training # batch=64 # subdivisions=2 width=416 height=416 channels=3 momentum=0.9 decay=0.0005 angle=0 saturation = 1.5 exposure = 1.5 hue=.1 learning_rate=0.001 burn_in=1000 max_batches = 500200 policy=steps steps=400000,450000 scales=.1,.1 [convolutional] batch_normalize=1 filters=16 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=2 [convolutional] batch_normalize=1 filters=32 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=2 [convolutional] batch_normalize=1 filters=64 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=2 [convolutional] batch_normalize=1 filters=128 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=2 [convolutional] batch_normalize=1 filters=256 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=2 [convolutional] batch_normalize=1 filters=512 size=3 stride=1 pad=1 activation=leaky [maxpool] size=2 stride=1 [convolutional] batch_normalize=1 filters=1024 size=3 stride=1 pad=1 activation=leaky ########### [convolutional] batch_normalize=1 filters=256 size=1 stride=1 pad=1 activation=leaky [convolutional] batch_normalize=1 filters=512 size=3 stride=1 pad=1 activation=leaky [convolutional] size=1 stride=1 pad=1 filters=18 activation=linear [yolo] mask = 3,4,5 anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319 classes=1 num=6 jitter=.3 ignore_thresh = .7 truth_thresh = 1 random=1 [route] layers = -4 [convolutional] batch_normalize=1 filters=128 size=1 stride=1 pad=1 activation=leaky [upsample] stride=2 [route] layers = -1, 8 [convolutional] batch_normalize=1 filters=256 size=3 stride=1 pad=1 activation=leaky [convolutional] size=1 stride=1 pad=1 filters=18 #3*(class + 4 + 1) activation=linear [yolo] mask = 0,1,2 anchors = 10,14, 23,27, 37,58, 81,82, 135,169, 344,319 classes=1 num=6 jitter=.3 ignore_thresh = .7 truth_thresh = 1 random=1
注:修改的地方主要是filter,因為我們每一個網格就預測3個anchor結果,所以filter =3*(1 + 5)=18
4.獲取官網已經訓練好的網絡參數yolov3-tiny.weights,下載鏈接https://pjreddie.com/media/files/yolov3-tiny.weights,導入weights目錄下,需要自己創建weights文件夾,由於需要進行fine-tune,所以需要對yolov3-tiny.weights進行改造,因而需要下載官網的代碼https://github.com/pjreddie/darknet,運行一下腳本,並將得到的yolov3-tiny.conv.15導入weights目錄下,腳本如下
./darknet partial cfg/yolov3-tiny.cfg yolov3-tiny.weights yolov3-tiny.conv.15 15
這里,直接提供yolov3-tiny.conv.15下載地址。
如果是其他結構的網絡,那么可以參考download_yolov3_weights.sh中的說明,里面有詳細的介紹。
五、訓練
一切准備妥當,我們就可以開始訓練了,訓練腳本如下
python train.py --data data/rbc.data --cfg cfg/yolov3-tiny.cfg --epochs 10 --weights weights/yolov3-tiny.weights
訓練時,可能會有報錯:SyntaxError:unexpected character after line continuation character。
報錯:SyntaxError: unexpected character after line continuation character的解決方法
得到訓練好的模型best.pt
訓練結果如下(這里只有10次迭代的結果)
六、預測
我們將得到的模型進行預測,這里代預測的圖片我們放在data/samples目錄下
運行以下腳本
python detect.py --name data/rbc.data --cfg cfg/yolov3-tiny.cfg --weights weights/best.pt
注:代碼中用的是pt后綴保存權重文件,用.pth也是可以的,只要代碼中所有地方都統一。
得到的結果可以在output目錄
可以看出來效果一般,主要我們的網絡結構較簡單,同時迭代的次數較少。
知識補充:關於 混淆矩陣、准確率(accuracy)、查准率(精度)(Precision)、查全率(召回率)(recall)、Roc、AUC、和MAP
混淆矩陣
對於二分類問題,可將樣例根據其真實類別與分類器預測類別的組合划分為:
真正例(true positive):將一個正例正確判斷為正例
假正例(false positive):將一個反例錯誤判斷為正例
真反例(true negative):將一個反例正確判斷為反例
假反例(false negative):將一個正例錯誤判斷為反例
令TP、FP、TN、FN分別表示對應的樣例數,這四個指標構成了分類結果的混淆矩陣:
分類結果混淆矩陣
正例(預測結果) | 反例(預測結果) | |
正例(真實情況) | TP(真正例) | FN(假反例) |
反例(真實情況) | FP(假正例) | TN(真反例) |
樣例總數 = TP + FP + TN + FN
准確率(accuracy)
accuracy = (TP+TN)/TP+FP+TN+FN
查准率 = 精度 = precision = TP/(TP+FP) : 模型預測為正類的樣本中,真正為正類的樣本所占的比例
查全率 = 召回率 = recall = TP/(TP+FN) : 模型正確預測為正類的樣本的數量,占總的正類樣本數量的比值
一般來說,查准率高時,查全率往往偏低;查全率高時,查准率往往偏低。
P-R曲線:查准率-查全率曲線:precision為縱軸,recall為橫軸
第一種: 若學習器的P-R曲線被另一個學習器完全“包住”,則后者的性能優於前者; 第二種: 若兩個學習器的P-R曲線發生了交叉,可以運用平衡點(Break-Even Point,BEP),即根據在“查准率=查全率”時的取值,判斷學習器性能的好壞。 第三種: 若兩個學習器的P-R曲線發生了交叉,亦可以使用F1/F_\beta度量,分別表示查准率和查全率的調和平均和加權調和平均。
其中,F2分數中,召回率的權重高於准確率,而F0.5分數中,准確率的權重高於召回率。
F_\beta的物理意義就是將准確率和召回率這兩個分值合並為一個分值,在合並的過程中,召回率的權重是准確率的\beta倍。
F1分數認為召回率和准確率同等重要,F2分數認為召回率的重要程度是准確率的2倍,而F0.5分數認為召回率的重要程度是准確率的一半。 第四種: 若兩個學習器的P-R曲線發生了交叉,亦可以使用AP\MAP:即計算P-R曲線下的面積
關於mAP轉自
鏈接:https://www.zhihu.com/question/53405779/answer/993913699
來源:知乎
評價指標 mAP
下面用一個例子說明 AP 和 mAP 的計算
先規定兩個公式,一個是 Precision,一個是 Recall,這兩個公式同上面的一樣,我們把它們擴展開來,用另外一種形式進行展示,其中 all detctions
代表所有預測框的數量, all ground truths
代表所有 GT 的數量。
AP 是計算某一類 P-R 曲線下的面積,mAP 則是計算所有類別 P-R 曲線下面積的平均值。
假設我們有 7 張圖片(Images1-Image7),這些圖片有 15 個目標(綠色的框,GT 的數量,上文提及的 all ground truths
)以及 24 個預測邊框(紅色的框,A-Y 編號表示,並且有一個置信度值)

根據上圖以及說明,我們可以列出以下表格,其中 Images 代表圖片的編號,Detections 代表預測邊框的編號,Confidences 代表預測邊框的置信度,TP or FP 代表預測的邊框是標記為 TP 還是 FP(認為預測邊框與 GT 的 IOU 值大於等於 0.3 就標記為 TP;若一個 GT 有多個預測邊框,則認為 IOU 最大且大於等於 0.3 的預測框標記為 TP,其他的標記為 FP,即一個 GT 只能有一個預測框標記為 TP),這里的 0.3 是隨機取的一個值。

通過上表,我們可以繪制出 P-R 曲線(因為 AP 就是 P-R 曲線下面的面積),但是在此之前我們需要計算出 P-R 曲線上各個點的坐標,根據置信度從大到小排序所有的預測框,然后就可以計算 Precision 和 Recall 的值,見下表。(需要記住一個叫累加的概念,就是下圖的 ACC TP 和 ACC FP)

- 標號為 1 的 Precision 和 Recall 的計算方式:Precision=TP/(TP+FP)=1/(1+0)=1,Recall=TP/(TP+FN)=TP/(
all ground truths
)=1/15=0.0666 (all ground truths 上面有定義過了
) - 標號 2:Precision=TP/(TP+FP)=1/(1+1)=0.5,Recall=TP/(TP+FN)=TP/(
all ground truths
)=1/15=0.0666 - 標號 3:Precision=TP/(TP+FP)=2/(2+1)=0.6666,Recall=TP/(TP+FN)=TP/(
all ground truths
)=2/15=0.1333 - 其他的依次類推
然后就可以繪制出 P-R 曲線

得到 P-R 曲線就可以計算 AP(P-R 曲線下的面積),要計算 P-R 下方的面積,一般使用的是插值的方法,取 11 個點 [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] 的插值所得

得到一個類別的 AP 結果如下:
要計算 mAP,就把所有類別的 AP 計算出來,然后求取平均即可。