关于训练YOLO v2模型过程中的经验总结。
数据集:
考虑到自己收集图片并标注,工作量较大,当前主要将包含行人的图片从已有的数据集PASCAL VOC中抽取出来,按照\VOCdevkit\VOC2007\ImageSets\Main中相应类别的txt文件,提取person_train.txt、person_val.txt、person_test.txt、person_trainval.txt中的正样本文件(包括JPEGImages中的图片和Annotations中的xml标注文件);
新建文件夹VOC_person,在其下建立文件夹Annotations、ImageSets、JPEGImages(如下图所示),将提取的图片和标注xml文件分别放置JPEGImages和Annotations中,在ImageSets中建立文件夹Main。
。
生成ImageSets\Main里的四个txt文件
通过xml名字(或图片名),生成四个txt文件,即:test.txt train.txt trainval.txt val.txt
txt文件中的内容为:
000002
000005
000028
即图片名字(无后缀),test.txt是测试集,train.txt是训练集,val.txt是验证集,
trainval.txt是训练和验证集.VOC2007中,trainval大概是整个数据集的50%,test也大概是整个数据集的50%;train大概是trainval的50%,val大概是trainval的50%,当数据集较小时,可减少val和test的数量;
生成四个txt文件文件的代码为:
%该代码根据已生成的xml,制作VOC2007数据集中的trainval.txt;train.txt;test.txt和val.txt
%trainval占总数据集的50%,test占总数据集的50%;train占trainval的50%,val占trainval的50%;
%上面所占百分比可根据自己的数据集修改,如果数据集比较少,test和val可少一些
%%
%注意修改下面四个值
xmlfilepath='E:\Image Sets\VOC_person\Annotaions';
txtsavepath='E:\Image Sets\VOC_person\ImageSets\Main\';
trainval_percent=0.8;%trainval占整个数据集的百分比,剩下部分就是test所占百分比
train_percent=0.8;%train占trainval的百分比,剩下部分就是val所占百分比
%%
xmlfile=dir(xmlfilepath);
numOfxml=length(xmlfile)-2;%减去.和.. 总的数据集大小
trainval=sort(randperm(numOfxml,floor(numOfxml*trainval_percent)));
test=sort(setdiff(1:numOfxml,trainval));
trainvalsize=length(trainval);%trainval的大小
train=sort(trainval(randperm(trainvalsize,floor(trainvalsize*train_percent))));
val=sort(setdiff(trainval,train));
ftrainval=fopen([txtsavepath 'trainval.txt'],'w');
ftest=fopen([txtsavepath 'test.txt'],'w');
ftrain=fopen([txtsavepath 'train.txt'],'w');
fval=fopen([txtsavepath 'val.txt'],'w');
for i=1:numOfxml
if ismember(i,trainval)
fprintf(ftrainval,'%s\n',xmlfile(i+2).name(1:end-4));
if ismember(i,train)
fprintf(ftrain,'%s\n',xmlfile(i+2).name(1:end-4));
else
fprintf(fval,'%s\n',xmlfile(i+2).name(1:end-4));
end
else
fprintf(ftest,'%s\n',xmlfile(i+2).name(1:end-4));
end
end
fclose(ftrainval);
fclose(ftrain);
fclose(fval);
fclose(ftest);
将数据集转换成YOLO训练所需标注格式,并生成训练、验证、测试图片路径文件:
(1)将数据集拷贝到darknet-master\data下:如VOC_person数据集
打开darknet-master\scripts下的voc_label.py文件,修改代码:
#coding:utf-8
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
#sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test')]
#classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
sets = ["train","val","test"]
classes = ["person"]#改成自己的类别
#voc_label.py自带的函数,未做修改
def convert(size, box):
dw = 1./(size[0])
dh = 1./(size[1])
x = (box[0] + box[1])/2.0 - 1
y = (box[2] + box[3])/2.0 - 1
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('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id))
in_file = open('/home/yi_miao/darknet-master/data/VOC_person/Annotaions/%s.xml'%(image_id))
out_file = open('/home/yi_miao/darknet-master/data/VOC_person/labels/%s.txt'%(image_id), 'w')
tree=ET.parse(in_file)
root = tree.getroot()
size = root.find('size') #访问size标签的数据
w = int(size.find('width').text) #读取size标签中宽度的数据
h = int(size.find('height').text) #读取size标签中高度的数据
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()
for image_set in sets:
if not os.path.exists('/home/yi_miao/darknet-master/data/VOC_person/labels/'):
os.makedirs('/home/yi_miao/darknet-master/data/VOC_person/labels/')
image_ids = open('/home/yi_miao/darknet-master/data/VOC_person/ImageSets/Main/%s.txt'%(image_set)).read().strip().split()
list_file = open('person_%s.txt'%(image_set), 'w')
for image_id in image_ids:
list_file.write('/home/yi_miao/darknet-master/data/VOC_person/JPEGImages/%s.jpg\n'%(image_id))
convert_annotation(image_id)
list_file.close()
os.system("cat person_train.txt person_val.txt > train.txt")
#os.system("cat 2007_train.txt 2007_val.txt 2007_test.txt 2012_train.txt 2012_val.txt > train.all.txt")
注意根据自己的实际情况修改数据集的路径名。
(2)终端进入darknet-master\svripts,执行
python voc_label.py
此后可以看到,VOC_person里多了一个labels文件夹(如下图所示),里面有每张图片的标注文件(文件内容形如0 0.488888888889 0.289256198347 0.977777777778 0.429752066116),其中第一个数字表示类别标签(0表示person,也即前面写的classes的顺序),后面四个数为包围框信息,做了转换;
darknet-master\scripts下多了person_train.txt、person_val.txt和person_test.txt,三个文件(如下),这三个文件是数据集中图片的路径。由于yolo训练只需要一个txt文件,文件中包含所有你想要训练的图片的路径,因此,我们可以用person_train.txt、person_val.txt包含的图片均用来训练,因此执行(voc_label.py中最后两行为相应代码):
cat person_train.txt person_val.txt > train.txt
现在,已经将数据集中的训练集和验证集全都放在一个txt文件中,这些图片用来作为YOLO的训练图片。 将上述txt文件移至VOC_person文件夹下。
修改训练代码:
(1)在darknet-master\data下新建文件personTrain.names文件,内容为:
person
(2)在darknet-master\cfg下新建 personTrain.data文件,内容为:
classes= 1
train = /home/yi_miao/darknet-master/data/VOC_person/train.txt
valid = /home/yi_miao/darknet-master/data/VOC_person/person_test.txt
names = data/personTrain.names
backup = /home/yi_miao/darknet-master/backup
(3)修改darknet-master\src\yolo.c文件:
- 检测目标类别改成自己的数据集类别:
char *voc_names[] = {"person"};
- Train_yolo函数:
char *train_images = "/home/yi_miao/darknet-master/data/VOC_person/train.txt";
char *backup_directory = "/home/yi_miao/darknet-master/backup/";
train_images应该指向我们刚得到的train.txt;backup_directory指向的路径是训练过程中生成的weights文件保存的路径(可以在darknet-master下新建文件夹backup然后指向它)。这两个路径按自己系统修改即可;
- validate_yolo函数:
char *base = "/home/yi_miao/darknet-master/results/comp4_det_test_";
list *plist = get_paths("/home/yi_miao/darknet-master/data/VOC_person/person_test.txt");
- validate_yolo_recall函数
char *base = "/home/yi_miao/darknet-master/results/comp4_det_test_";
list *plist = get_paths("/home/yi_miao/darknet-master/data/VOC_person/person_test.txt");
- test_yolo函数:
draw_detections(im, l.side*l.side*l.n, thresh, boxes, probs, voc_names, alphabet, 1);
类别数改为1类;
- run_yolo函数:
demo(cfg, weights, thresh, cam_index, filename, voc_names, 1, frame_skip, prefix, .5, 0,0,0,0);
类别数改为1类;
(4)修改darknet-master\src\detector.c文件:
- validate_detector_recall函数:
list *plist = get_paths("/home/yi_miao/darknet-master/data/VOC_person/person_test.txt");
- run_detector函数:
int classes = option_find_int(options, "classes", 1);
类别数改为1类;
(5)修改darknet-master\cfg\tiny-yolo.cfg文件:
修改tiny-yolo.cfg文件,其他模型修改类似。
【region】层中 classes 改成1;
【region】层上方第一个【convolution】层,其中的filters值要进行修改,改成(classes+ coords+ 1)* (NUM) ,我的情况中:(1+4+1)* 5=30,所以需要把filters 的值改成30;
[convolutional]
size=1
stride=1
pad=1
#filters=425
filters=30
activation=linear
[region]
anchors = 0.738768,0.874946, 2.42204,2.65704, 4.30971,7.04493, 10.246,4.59428, 12.6868,11.8741
bias_match=1
#classes=80
classes=1
coords=4
num=5
softmax=1
jitter=.2
rescore=1
(6)其他的一些参数可以按自己需求修改,比如学习率、max_batches等。
得到预训练模型文件:
根据不同的model,要对已有的weights进行转换,得到与.cfg文件相匹配的初始权重;
./darknet partial cfg/tiny-yolo.cfg tiny-yolo-voc.weights tiny-yolo.conv.14 14
注意,这里 ./darknet-master partial 转化网络 现有weights的路径 需要生成的weights的路径
YOLO v1和YOLO v2两个版本的weight不通用。
经过以上修改,重新make一下darknet
训练:
在darknet-master下执行:
./darknet detector train cfg/personTrain.data cfg/tiny-yolo.cfg tiny-yolo.conv.14
训练中断:
- 在保存的最新的权重文件的基础上继续训练,执行:
./darknet detector train cfg/personTrain.data cfg/tiny-yolo.cfg backup/tiny-yolo_8000.weights
- 也可以执行以下命令:
./darknet partial cfg/tiny-yolo.cfg backup/tiny-yolo_8000.weights tiny-yolo_8000.conv.14 14
./darknet detector train cfg/tiny-yolo.cfg tiny-yolo_8000.conv.14 2>1 | tee person_train_log.txt
保存训练中间参数结果person_train_log.txt,用于可视化调参。
测试和结果:
执行命令:
./darknet detector test cfg/tiny-yolo.cfg tiny-yolo_final.weights data/person.jpg
一般预训练模型都用图像分类的模型,而不是用检测模型训练的,最后没用预训练模型来训练网络,迭代了43000次后的效果如下所示(注:图片来自百度图片):