前段时间做了华为云的垃圾检测分类比赛(垃圾分类检测),当时的方案是SSD+efficientdet+CiouLoss,很遗憾最终未能进入复赛(如果大家感兴趣,也可以去比赛官方页面下载来玩一玩)。这几天闲来无事玩了下darknet官方版的yolov4和ultralytics的yolov5。嗯,初步测试yolov4-tiny确实很不错,而且速度是真的快。本篇文章主要记录如何正确食用yolov4,下一次记录下yolov5的使用。主要分为darknet编译,数据准备,训练,验证…。其实官方文档写的是真的详细,这里提取重要信息。yolov4地址:
http://link.zhihu.com/?target=https%3A//github.com/AlexeyAB/darknet
编译darknet
darknet一般有两个含义,一个指的是类似resnet,vgg之类的yolo用的特征提取主干网络,还有一个就是用c语言编写的一个深度学习框架,虽然没有tf,torch功能这么多,但简单高效。官方的yolo实现就是这样实现,完全不依赖于其它任何框架,速度又快移植性又好。这里就当做正常的c语言项目来编译就好,以ubuntu系统为例:如果需要可视化图片,需要安装opencv,如果需要GPU加速训练,需要提安装cuda,cudnn等
git clone git@github.com:AlexeyAB/darknet.git
cd darknet
gedit Makefile
'''
GPU=1#是否需要gpu,如果没有卡就设置为0吧
CUDNN=1#同上
CUDNN_HALF=0
OPENCV=0#如果没有安装opencv,或者设置为1编译报错,这里设置为0
'''
make -j8#编译
这样如果编译成功,会在当前路径下生成可执行darknet文件.然后会有一个build目录(编译产生的各种结果都在里面,而且我后面的训练数据权重上面的都在build下面,因美味这样把源码隔开了,有好处。官方教程中也建议把制作好的数据集也放到里面),为了简单的测试一下,先下载好yolov4训练好的权重(看readme)。然找个目录放好。在终端执行:
./darknet detector test cfg/coco.data yolov4.cfg yolov4.weights -ext_output data/dog.jpg
【注】我遇到了一个问题,在编译检测的时候只能用CPU也就是将Makefile文件中的GPU和CUDNN均设置为0才可以完成检测,在训练的时候可以将这两项设置为1进行训练。这个问题我之前在配置yolov3的时候也遇到了,不知道是不是电脑的问题。
如果成功,会输出结果并且可视化图片会保存到当前路径下.
训练自己的数据
制作自己的配置文件(yolov4-tiny为例)
这个其实官方文档写得也很清楚,有的细节需要补充强调下.
看这上面写这么多,其实如果有稍微了解过yolo系列的就应该清楚是怎么一回事。yolo的各个版本的网络结构,参数配置都是写到cfg/下面的xxx.cfg里面的。主干网络的最后一层输出通道为(classes + 5)x3.譬如你打开原始的cfg文件(针对coco数据),那么看到的最后一层的输出肯定是255,因为coco有80类,(80+5)x3=255.这么来的,以我自己的垃圾数据为例,总共44类,因此(44+5)*3=147.以yolov4-tiny为例:更改cfg/yolov4-tiny.cfg
注意,其实这里的anchor最好也根据自己的数据集进行调整(就是聚类得到9个anchor),官方有实现,一行命令得到这个后面会讲到。为了方便,我把这个.cgf改名为了yolov4-tiny-garbage.cfg
制作数据集
这个是关键,我们拿到的目标检测数据往往是两种格式:coco或者voc,而yolo系列其实是有自己的专用组织格式。train.txt里面存储了所有的训练图片路径,每一行是一张。每一张图片的标注由一个.txt来存储,格式如下:
<object-class> <x_center> <y_center> <width> <height>
所以一般来讲需要自己写一个脚本将coco,voc或者其它格式的数据标注转换为yolo所需格式。关于如何转换数据,其实这个自己写一个简单的脚本并不复杂,如果嫌麻烦,网上搜索吧。我在这里也给出voc转yolo的代码。官方在这里建议在编译产生的build目录下存放训练数据build/darknet/x64/data
这里是我组织好的训练数据格式:图片和标注都放在garbage_images下面
voc_yolo.py
下面是我自己的voc格式数据转yolo格式的脚本,不同数据稍作修改可用
import xml.etree.ElementTree as ET
import os
import cv2
classes = ['一次性快餐盒','书籍纸张','充电宝',
'剩饭剩菜' ,'包','垃圾桶','塑料器皿','塑料玩具','塑料衣架',
'大骨头','干电池','快递纸袋','插头电线','旧衣服','易拉罐',
'枕头','果皮果肉','毛绒玩具','污损塑料','污损用纸','洗护用品',
'烟蒂','牙签','玻璃器皿','砧板','筷子','纸盒纸箱','花盆',
'茶叶渣','菜帮菜叶','蛋壳','调料瓶','软膏','过期药物',
'酒瓶','金属厨具','金属器皿','金属食品罐','锅','陶瓷器皿',
'鞋','食用油桶','饮料瓶','鱼骨']
def convert_annotation(image_id):
in_file = open('/home/admins/qyl/trash_detection/efficientnet_SSD/datasets/VOC2007/Annotations/%s.xml' % image_id)
if not os.path.exists('./data/custom/labels1/'):
os.makedirs('./data/custom/labels1/')
out_file_img = open('./data/custom/trainval.txt', 'a') # 生成txt格式文件
out_file_label = open('./data/custom/labels1/%s.txt' % image_id,'a') # 生成txt格式文件
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
voc_img_dir='/home/admins/qyl/trash_detection/efficientnet_SSD/datasets/VOC2007/JPEGImages/{}.jpg'.format(image_id)
out_file_img.write(voc_img_dir)
out_file_img.write("\n")
img=cv2.imread(voc_img_dir)
dh = 1. / img.shape[0]
dw = 1. / img.shape[1]
cnt=len(root.findall('object'))
if cnt==0:
print('nulll null null.....')
print(image_id)
cc=0
for obj in root.iter('object'):
cc+=1
cls = obj.find('name').text
if cls not in classes:
continue
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
if dw*float(xmlbox.find('xmin').text)<0. or dw*float(xmlbox.find('xmax').text)<0. or dh*float(xmlbox.find('ymin').text)<0. or dh*float(xmlbox.find('ymax').text)<0.:
print(image_id)
b = (dw*float(xmlbox.find('xmin').text), dw*float(xmlbox.find('xmax').text), dh*float(xmlbox.find('ymin').text),
dh*float(xmlbox.find('ymax').text))
out_file_label.write(str(cls_id)+ " " + str((b[0]+b[1])/2) + " " + str((b[2]+b[3])/2) + " " + str(b[1]-b[0]) + " " + str(b[3]-b[2]))
if cc<cnt:
out_file_label.write("\n")
out_file_label.close()
imgname_list = []
part_name = 'trainval.txt' # test.txt
with open(os.path.join('/home/admins/qyl/trash_detection/efficientnet_SSD/datasets/VOC2007/', 'ImageSets/Main/' + part_name)) as f:
all_lines = f.readlines()
for a_line in all_lines:
imgname_list.append(a_line.split()[0].strip())
print(len(imgname_list))
for image_id in imgname_list:
convert_annotation(image_id)
train.txt和val.txt这两个.txt是需要自己制作的,如果没有验证集,也可以直接把训练集当做验证集,的每一行代表一张图片的路径
制作.data和.name文件
我们的程序是通过.data里面给定的train和val路径去寻找训练和验证数据集的。比如我自己的垃圾检测.data就像下面这样:
.name像这样
存储每一类对应的真实名字.
这样就制作好了数据集,至于上面的各种路径,其实没有那么死板,只要你自己的程序可以找到就行,以我自己的组织方式为例:
我把可执行的darknet文件也复制了一份到build/darknet/x64/下面
训练
最好先下载v4-tiny的预训练权重yolov4-tiny.conv.29;参看readme有下载路径不需要梯子,迅雷可以加速下载
./darknet detector train data/garbage.data cfg/yolov4-tiny-garbage.cfg ../../../weights/yolov4-tiny.conv.29 -map
注意,上面的路径很灵活,只要注意一点就是相对于./darknet可执行程序要能找到,比如我把darknet可执行程序放到了build/darknet/x64/下面,只需要在build/darknet/x64/执行上面的命令即可训练;
cfg/yolov4-tiny-garbage.cfg是制作的垃圾检测的.cfg文件;
…/…/…/weights/yolov4-tiny.conv.29是我的预训练权重相对于当前目录的保存的位置
-map是采取边训练边验证map值的策略
还有训练的各种参数可以在.cfg文件里面设置
训练完的模型被保存在backup文件夹下
验证
如果训练的时候没有采取边训练边验证的方式。而是训练完了再验证,可以使用命令:
./darknet detector map data/garbage.data cfg/yolov4-tiny-garbage.cfg backup/yolov4-tiny-garbage_best.weights
建议这样验证一次,因为这样可以输出各个类别的详细map值
关于anchor聚类
下面说一下anchor的聚类问题,为了得到更好的训练效果,最好通过聚类得到自己数据集的anchor聚类中心。这里其实官方已经制作好了,只需要一个简单的命令就可以搞定。
./darknet detector calc_anchors data/garbage.data -num_of_clusters 9 -width 416 -height 416
输入图片尺寸为416x416,聚类数为9,通过garbage.data指定的路径去找到训练数据。
最后
关于darknet的使用其实还有很多,可查看官方readme;yolov4-tiny是真的名不虚传,真的快!而且也mAP也蛮不错