實現yolo3模型訓練自己的資料集總結
經過兩天的努力,借鑑網上眾多部落格,在自己電腦上實現了使用yolo3模型訓練自己的資料集並進行測試圖片。本文主要是我根據下面參考文章一步步實施過程的總結,可能沒參考文章中那麼詳細,但是會包含一些參考文章中沒提及的容易掉坑的小細節,建議讀者結合參考文章一起看,一步步走即可。首先貼出本文主要參考的文章以及程式碼出處:
程式碼:https://github.com/qqwweee/keras-yolo3
參考文章:https://blog.csdn.net/patrick_Lxc/article/details/80615433
一.下載專案原始碼,進行快速測試
從上面程式碼連結處下載整個專案原始碼。下載好後,首先根據github中指引進行快速測試。
yolo web:https://pjreddie.com/darknet/yolo
對應操作如下(命令列操作):
1. wget https://pjreddie.com/media/files/yolov3.weights
註釋:這裡wget為linux命令,windows系統可以直接訪問後面連結來下載yolov3權重檔案,也可以訪問yolo web去下載。
2. python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5
註釋:執行convert.py檔案,此為將darknet的yolo轉換為可以用於keras的h5檔案,生成的h5被儲存在model_data下。命令中的convert.py和yolov3.vfg克隆下來後已經有了,不需要單獨下載。
3.用已經被訓練好的yolo.h5進行圖片識別測試。執行:python yolo.py
執行後會讓你輸入一張圖片的路徑,因為我準備的圖片(網上隨便找的)放在yolo.py同級目錄,所以直接輸入圖片名稱,沒有加路徑。
過程和結果如下圖所示:
以上結果表明快速開始專案成功,接下來我們進行搭建自己的資料集,進行模型的訓練以及訓練後模型用於測試圖片識別。
二.準備自己的資料集
可以按照上面參考文章裡面做法下載VOC資料集,然後清空裡面內容,保留檔案目錄結構。也可以直接手動建立如下目錄結構:
這裡面用到的資料夾是Annotation、ImageSets和JPEGImages。注意:需要在VOC2007再建立一個上級目錄VOCdevkit。
其中資料夾Annotation中主要存放xml檔案,每一個xml對應一張影象,並且每個xml中存放的是標記的各個目標的位置和類別資訊,命名通常與對應的原始影象一樣;而ImageSets我們只需要用到Main資料夾,這裡面存放的是一些文字檔案,通常為train.txt、test.txt等,該文字檔案裡面的內容是需要用來訓練或測試的影象的名字;JPEGImages資料夾中放我們已按統一規則命名好的原始影象。
原始圖片就不解釋了,而與原始圖片一 一對應的xml檔案,可以使用LabelImg工具,具體使用方法百度即可。工具可以從參考的部落格中的附帶地址下載,也可以自己網上找,很容易。
將自己的圖片以及xml按照要求放好後,在VOC2007的同級目錄下建立convert_to_txt.py檔案,拷貝下面的程式碼,然後執行該py檔案。該程式碼是讀取上面的xml檔案中圖片名稱,並儲存在ImageSets/Main目錄下的txt檔案中。注意:此處txt中僅有圖片名稱。
程式碼:
import os
import random
trainval_percent = 0.1
train_percent = 0.9
xmlfilepath = 'Annotations'
txtsavepath = 'ImageSets\Main'
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('ImageSets/Main/trainval.txt', 'w')
ftest = open('ImageSets/Main/test.txt', 'w')
ftrain = open('ImageSets/Main/train.txt', 'w')
fval = open('ImageSets/Main/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()
然後,回到從github上下載的原始碼所在目錄,執行其中的voc_annotation.py,會在當前目錄生成新的三個txt檔案,手動去掉名稱中2007_部分。當然,可以自己進入voc_annotation.py修改程式碼,使生成的txt檔名中不包含2007_。
注意:一開始VOC2007,也可以叫VOC2008之類,這樣此處的txt就會成為2008_xxx.txt。此外,有一個很關鍵的地方需要注意,必須修改,不然此處生成的三個新的txt檔案中僅僅比前面Main下的txt中多了圖片路徑而已,並不包含框box的資訊,這樣的話在後面的訓練步驟,由於沒有框的資訊,僅僅是圖片路徑和名稱資訊,是訓練不好的,即使可以得到訓練後的h5檔案,但是當用這樣的h5檔案去執行類似前面所說的測試圖片識別,效果就是將整幅圖框住,而不是框住你所要識別的部分。
故所要做的是:在執行voc_annotation.py之前,開啟它,進行修改。將其中最上面的sets改為你自己的,比如2012改為我得2007,要和前面的目錄年份保持一致。還需要將最上面的classes中的內容,改為你自己xml檔案中object屬性中name屬性的值。你有哪些name值,就改為哪些,不然其中讀取xml框資訊的程式碼就不會執行。
上面是我的xml中一個object截圖,這裡的name實際上為你用LableIma工具畫框時候給那個框的命名值。
至此,自己資料集的準備工作就完成了。
三.修改一些檔案,然後執行訓練
首先是修改model_data下的檔案,放入你的類別,coco,voc這兩個檔案都需要修改。這裡的命名會成為最終檢測圖片時候框的框上的名稱。
其次是yolov3.cfg檔案
這一步事後我和同學討論了下,得出的結論是,從0開始訓練自己的模型,則不需要下面的修改步驟,而如果想用遷移學習思想,用已經預訓練好的權重接著訓練,則需要下面的修改步驟。
IDE裡直接開啟cfg檔案,ctrl+f搜 yolo, 總共會搜出3個含有yolo的地方。
每個地方都要改3處,filters:3*(5+len(classes));
classes: len(classes) = 1,我只識別一種,所以為1
random:原來是1,視訊記憶體小改為0
如果要用預訓練的權重接著訓練,則需要執行以下程式碼:然後執行原train.py就可以了。原train.py中有載入預訓練權重的程式碼,並凍結部分層數,在此基礎上進行訓練。可以修改凍結層數。
python convert.py -w yolov3.cfg yolov3.weights model_data/yolo_weights.h5
這個在github和參考的文章中均提到。
如果不用預訓練的權重,上一步不用執行(執行也沒影響),但是下面的train.py需要修改,改為如下所示(程式碼出處為上文提到的參考部落格中):直接複製替換原train.py即可
"""
Retrain the YOLO model for your own dataset.
"""
import numpy as np
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.callbacks import TensorBoard, ModelCheckpoint, EarlyStopping
from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss
from yolo3.utils import get_random_data
def _main():
annotation_path = 'train.txt'
log_dir = 'logs/000/'
classes_path = 'model_data/voc_classes.txt'
anchors_path = 'model_data/yolo_anchors.txt'
class_names = get_classes(classes_path)
anchors = get_anchors(anchors_path)
input_shape = (416,416) # multiple of 32, hw
model = create_model(input_shape, anchors, len(class_names) )
train(model, annotation_path, input_shape, anchors, len(class_names), log_dir=log_dir)
def train(model, annotation_path, input_shape, anchors, num_classes, log_dir='logs/'):
model.compile(optimizer='adam', loss={
'yolo_loss': lambda y_true, y_pred: y_pred})
logging = TensorBoard(log_dir=log_dir)
checkpoint = ModelCheckpoint(log_dir + "ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5",
monitor='val_loss', save_weights_only=True, save_best_only=True, period=1)
batch_size = 10
val_split = 0.1
with open(annotation_path) as f:
lines = f.readlines()
np.random.shuffle(lines)
num_val = int(len(lines)*val_split)
num_train = len(lines) - num_val
print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
model.fit_generator(data_generator_wrap(lines[:num_train], batch_size, input_shape, anchors, num_classes),
steps_per_epoch=max(1, num_train//batch_size),
validation_data=data_generator_wrap(lines[num_train:], batch_size, input_shape, anchors, num_classes),
validation_steps=max(1, num_val//batch_size),
epochs=500,
initial_epoch=0)
model.save_weights(log_dir + 'trained_weights.h5')
def get_classes(classes_path):
with open(classes_path) as f:
class_names = f.readlines()
class_names = [c.strip() for c in class_names]
return class_names
def get_anchors(anchors_path):
with open(anchors_path) as f:
anchors = f.readline()
anchors = [float(x) for x in anchors.split(',')]
return np.array(anchors).reshape(-1, 2)
def create_model(input_shape, anchors, num_classes, load_pretrained=False, freeze_body=False,
weights_path='model_data/yolo_weights.h5'):
K.clear_session() # get a new session
image_input = Input(shape=(None, None, 3))
h, w = input_shape
num_anchors = len(anchors)
y_true = [Input(shape=(h//{0:32, 1:16, 2:8}[l], w//{0:32, 1:16, 2:8}[l], \
num_anchors//3, num_classes+5)) for l in range(3)]
model_body = yolo_body(image_input, num_anchors//3, num_classes)
print('Create YOLOv3 model with {} anchors and {} classes.'.format(num_anchors, num_classes))
if load_pretrained:
model_body.load_weights(weights_path, by_name=True, skip_mismatch=True)
print('Load weights {}.'.format(weights_path))
if freeze_body:
# Do not freeze 3 output layers.
num = len(model_body.layers)-3
for i in range(num): model_body.layers[i].trainable = False
print('Freeze the first {} layers of total {} layers.'.format(num, len(model_body.layers)))
model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',
arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})(
[*model_body.output, *y_true])
model = Model([model_body.input, *y_true], model_loss)
return model
def data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes):
n = len(annotation_lines)
np.random.shuffle(annotation_lines)
i = 0
while True:
image_data = []
box_data = []
for b in range(batch_size):
i %= n
image, box = get_random_data(annotation_lines[i], input_shape, random=True)
image_data.append(image)
box_data.append(box)
i += 1
image_data = np.array(image_data)
box_data = np.array(box_data)
y_true = preprocess_true_boxes(box_data, input_shape, anchors, num_classes)
yield [image_data, *y_true], np.zeros(batch_size)
def data_generator_wrap(annotation_lines, batch_size, input_shape, anchors, num_classes):
n = len(annotation_lines)
if n==0 or batch_size<=0: return None
return data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes)
if __name__ == '__main__':
_main()
我是在cpu版本tensorflow上跑的,故特別慢,有運算資源的就不說了,如果資源有限,建議少弄些圖片和xml檔案,這樣最後的txt檔案中資料就少,跑起來輕鬆點。其次可以修改train.py中的迭代次數epochs的值,該值原作者設定的為500;也可以修改batch_size = 10的大小。
注意:我第一次訓練時候,確實在目錄下自動生成了logs資料夾,並在其中生成000資料夾,然後裡面放的是自己訓練好的h5檔案。但是後來我除錯程式碼,刪除該目錄,再次訓練時,報如下錯誤:
此時只需要手動建立logs資料夾和其內的000資料夾即可。嫌名字不好,可以自己修改train.py檔案,改裡面的儲存目錄。
下面為成功測試截圖:
四.用自己訓練的h5檔案進行測試
先修改yolo.py檔案中的模型路徑,如下所示,改為自己訓練後生成的h5檔案路徑。
然後執行,測試過程和前面所講一樣,因為我得沒怎麼訓練,就不貼出很挫的測試效果圖了。在最初沒有框資訊的txt檔案訓練後,執行測試很慢,因為訓練時候根本不知道要框什麼,改為正常txt後,訓練後的模型進行測試,速度就會很快。