从0开始实现目标检测——实践篇

根据上一篇《从0开始实现目标检测——原理篇》的讲述,我们选择了YOLOv3作为模型,那么本篇文章将继续接着上篇的内容,自己动手基于YOLOv3实现模型训练和mAP的计算。 在自己动手的这个过程中,一边解决遇到的问题,一边体会YOLOv3的原理,让我们学习起来吧。

 

一. YOLOv3之初体验

YOLOv3使用参考官网教程:https://pjreddie.com/darknet/yolo/

1. 安装YOLOv3并体验VOC数据

(1). YOLOv3安装

首先就是下载YOLOv3项目并安装了,如下:

git clone https://github.com/pjreddie/darknet
cd darknet
make

接着就是下载YOLOv3已经提前训练好的一个模型体验下效果了:

wget https://pjreddie.com/media/files/yolov3.weights
./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg

这个条命令运行后,可以看到在项目的安装目录下多了一个predictions.jpg的文件,这就是检查结果的图片。结果如图:

从0开始实现目标检测——实践篇_第1张图片

命令也输出了各个网络层的输入和输出,以及最后识别出多少个物体分类和属于这个分类的概率。如下图所示:

从0开始实现目标检测——实践篇_第2张图片

上图显示了卷积网络各层的计算过程,下图显示了整张图片检测的耗时,以及图中的物品类别和对应的概率。结果如图所示:

从0开始实现目标检测——实践篇_第3张图片

对着输出和结果图片,可以看到识别的准确率还是很高的,但是也相当耗时,耗费了18.66秒。YOLOv3提供了一个层数只有13层的tiny模型,识别速度会更快,下载体验下:

wget https://pjreddie.com/media/files/yolov3-tiny.weights
./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg

识别结果如下:

从0开始实现目标检测——实践篇_第4张图片

对于同样的图片,可以看到识别耗时从18.66秒直接降到了0.6秒,时间居然能下降97%,当然准确度也下降了。结果图片如图所示:

从0开始实现目标检测——实践篇_第5张图片

从结果中tiny的模型准确度可能的确不高,从图中能看出来把一辆truck识别成了2个car和一个truck的组合。

(2). 下载VOC数据

在体验了几次YOLOv3的检测效果后,开始思考如何才能训练自己的模型呢?很自然的想法是先按照YOLOv3开放的数据集做训练,跑通流程后再利用自己的数据集训练。接着就开始我们的第一步,利用开放的数据集进行训练。

首先使用在公开的VOC数据集上进行下验证,下载数据集:

wget https://pjreddie.com/media/files/VOCtrainval_11-May-2012.tar
wget https://pjreddie.com/media/files/VOCtrainval_06-Nov-2007.tar
wget https://pjreddie.com/media/files/VOCtest_06-Nov-2007.tar
tar xf VOCtrainval_11-May-2012.tar
tar xf VOCtrainval_06-Nov-2007.tar
tar xf VOCtest_06-Nov-2007.tar

下载完毕数据集后,我们还需要对转化数据集的标记格式为YOLOv3的标记方式,YOLOv3采用.txt文件来保存标记,格式如下:

    

其中object-class就是目标的种类index,x, y, width, height分别是图中目标的起始坐标和宽高。也就是之前《从0开始实现目标检测——原理篇》提到的。

YOLOv3也提供一个脚本程序,将VOC数据转化并标记为YOLO格式的标记,我们需要下载转化程序并进行转化:

wget https://pjreddie.com/media/files/voc_label.py
python3 voc_label.py

转化后,可以看到在项目目录下多了个VOCdevkit的文件夹,包含VOC2007,VOC2012,VOC2022 三个子文件夹,每个子文件夹格式如下:

从0开始实现目标检测——实践篇_第6张图片

Annotations保存了每个图片对应的xml标记文件,可以打开看看,内容还是很好理解的,定义了图片的地址,宽、高、包含的物品类别以及各个类别的坐标和宽高。JPEGImages里是图片文件,labels文件夹下的存放着每个图片对应的标记数据,比如:

19 0.482 0.4053333333333333 0.8280000000000001 0.752

表示第19类数据的坐标和宽高。但是这个第19类代表的是什么?后边的坐标信息和宽高信息和Annotations下的标记信息也不同啊,这是怎么回事?

答案就在voc_label.py文件里,打开这个文件,发现类别的定义如下:

classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]

那么第19类就是tvmonitor, 接着往下阅读此文件,发现文件是从Annotations中读入xml文件,通过函数convert把原始图片的坐标和宽高转化为了labels里的坐标和宽高数据。阅读函数,发现这个过程其实是一个归一化的过程,把原始数据映射到[0, 1]区间内,并且把坐标点移动到了图片的几何中心。

数据格式化和归一化完成后,我们还需把训练集合并成一个较大的训练集,从而获得较好的训练结果,合并命令如下:

cat 2007_train.txt 2007_val.txt 2012_*.txt > train.txt

接着把合并结果移动到data/voc目录下:

mv train.txt data/voc

(3). 训练VOC数据

在完成上述的数据准备后,再调整下训练模型的data文件,打开cfg/voc.data:

classes = 20
train = /train.txt
valid = /2007_test.txt
names = data/voc.names
backup = backup

替换为自己存储文件地址即可。

  • classes代表要分类的类数量。
  • train是train.txt文件保存位置。
  • valid是验证数据的存储地址位置。
  • names文件中存放了20个类别的名称。
  • backup是模型训练过程的权重文件保存目录,如果backup目录不存在,建立就行。

最后,调整下训练参数文件cfg/yolov3-voc.cfg:

[net]
# Testing
# batch=1
# subdivisions=1
# Training
batch=64
subdivisions=8

把训练的batch和subdivisions参数打开,关闭测试的batch和subdivisions参数。

batch表示是更新weights和bias的基本单位,可以这样理解每经过batch数量的样本训练后,更新一遍网络参数。

subdivisions表示网络中前向传播、反向传播的基本单位,也可理解为把整个batch分作几份训练,那么一次送入训练器的样本数量实际上是batch/subdivisions。

实际上网络是batch/subdivisions张图片进行训练(前向推理和反向传播),但是升级权值是在batch数目结束后进行的。这样在比较小的显存情况下实现大batch的训练。理论上batch越大,训练效果越好,但是batch太大内存可能吃不消。


在准备完备VOC训练数据后,在正式开始训练前还需要下载一个预训练文件:

wget https://pjreddie.com/media/files/darknet53.conv.74

在完成这些后,终于可以开始训练了,利用以下命令开始VOC数据的训练:

./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg darknet53.conv.74

从程序输出中可以看到,程序在完成了网络加载后开始进行了训练过程,输出如下:

从训练程序的输出分为3类信息:

  • Region 82 Avg IOU:82卷积层为最大的预测尺度,使用较大的mask,但是可以预测出较小的物体。
  • Region 94 Avg IOU:为中间的预测尺度,使用中等的mask。
  • Region 106 Avg IOU:为最小的预测尺度,使用较小的mask,可以预测出较大的物体。

一些输出信息解释如下:

  • Region Avg IOU: 0.900319:表示在当前subdivision内的图片的平均IOU,代表预测的矩形框和真实目标的交集与并集之比。
  • Class: 0.999576: 标注物体分类的正确率,期望该值趋近于1。
  • Obj: 0.991654: 越接近1越好。
  • No Obj: 0.000033: 期望该值越来越小,但不为零。
  • .5R:1.000000: 以IOU = 0.5为阈值时候的召回率。
  • .75R:1.000000:以IOU = 0.75为阈值时候的召回率。
  • count: 1:表示所有的当前subdivision图片(=batch/subdivisions)中包含正样本的图片的数量。

训练是按照yolov3-voc.cfg中的batch和subdivisions参数一组一组地读取图片训练,每组完成后输出下列信息:

其中内容说明如下:

  • 2001:当前训练的迭代次数。
  • 0.048537:总体的损失。
  • 0.048537 avg:是平均Loss, 一般低于0.060730就可以终止训练了。
  • 0.002000 rate:当前的学习率。
  • 3.904890 seconds:当前这批数据训练花费的总时间。
  • 2560128 images:到目前为止,参与训练的总图片数量。

训练过程很漫长,很漫长,很漫长(用天为单位计算)。

我发现有2个问题需要解决:

  1. 训练过程太慢了,有没有什么办法加速?
  2. 训练过程的数据能否有更好的显示呢? 比如类似dashboard的可视化方法?

第一个问题,如何加速训练过程?

当然是利用GPU了,YOLOv3的Makefile中可以修改gpu和cudnn的参数,有GPU的同学可以修改这两个参数后make出新的darknet程序来进行训练。强烈建议用GPU参与训练,CPU训练可能要等死人的。没GPU的话,现在很多云服务厂商都有GPU的云主机,临时租用一个也划算。

备注:我使用的是Ubuntu 20.04,如何在Ubuntu 20.04下安装gpu驱动和cuda编程组件是另一个需要解决的问题。

第二个问题,训练过程数据可以可视化吗?

肯定是可以可视化的,我们需要考虑如何可视化。简单的想法是把训练过程的输出重定向到一个日志文件,然后通过程序对日志文件中的数据进行提取,再把提取的数据可视化出来。如何完成可视化过程,我们本次不做尝试,等到我们把开始自己训练数据后再做训练过程的可视化。

到此,我们已经能够利用VOC的数据训练YOLOv3了。训练结果保存在backup目录下,可以看到有很多形如xxx_100.weights, xxx_200.weights, xxx_300.weights, ... , xxx_10000.weights的模型文件,和一个最终的yolov3-voc_final.weights的模型文件。

(4). 验证训练模型

训练模型已经完成,我们再把cfg/yolov3-voc.cfg的训练batch和subdvisions参数修改回去,用来验证下模型效果。

[net]
#Testing
batch=1
subdvisions=1
#Trainning
batch=64
subdvisions=16

运行命令进行测试:

./darknet detector test cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_final.weights test.png

如果结果显示了物品的分类,并且在项目下有predictions.jpg文件生成,那么说明模型成功了。

(5). 衡量模型的性能

根据前文的讨论,我们知道通过mAP这个数值来衡量模型的性能,现在的问题是我们有模型了(官方的模型),有数据集了(VOC数据集),就差计算mAP的过程了。可以通过如下命令验证模型:

./darknet detector valid cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_final.weights

整个过程花费了135秒,验证结束后会在results目录下生成每个类的验证数据文件。验证数据现在有了,剩下的就是基于验证数据计算mAP了,这个过程需要用到一个计算脚本,这个脚本名叫voc_eval.py,在另一个faster-rcnn的项目下,下载地址:

https://github.com/rbgirshick/py-faster-rcnn/tree/master/lib/datasets

计算mAP,主要是用到voc_eval里的一个函数voc_eval,用法如下:

rec, prec, ap = voc_eval('results/{}.txt', 'VOCdevkit/VOC2007/Annotations/{}.xml', 'VOCdevkit/VOC2007/ImageSets/Main/val.txt', 'person', '.')

第一个参数是刚才的验证模型结果文件地址,第二个参数是标记数据的地址,第三个参数是验证集地址,第四个参数是要计算准确度的类别名称。函数返回的第三个值就是对应类别的准确度。每次运行这个命令,都会在项目目录下生成一个annots.pkl的文件,如果更换验证集或者类别,需要删除这个文件重新计算。

有了准确度,mAP 就是所有类别的准确度相加,再除以所有类别的数量了。

但是在调用voc_eval的时候有些地方需要修改,voc_eval是根据python2的语法写的,里边用到了python2的cPickle和print的地方,我使用的时候python3,所以在调用的时候修改下几个关于cPickle的地方和print的地方:

import _pickle as cPickle
...
print("Reading annotation for {:d}/{:d}".format(i+ 1, let(imagenames)))
print("Saving cached annotations to {:s}".format(cachefile))
with open(cache file, 'wb') as f:
	  cPickle.dump(recs, f)
...
with open(cachefile, 'rb') as f:
		recs = cPickle.load(f)

一个类一个类的输入计算ap再去计算平均值有点费劲,可以写一个简单的脚本程序来完成这个过程,文件如下:

from voc_eval import voc_eval
import os

sub_files = os.listdir("results")
mAPs = []
for i in range(len(sub_files)):
		class_name = sub_files[i].split(".txt")[0]
		rec, prec, ap = voc_eval('results/{}.txt', 'VOCdevkit/VOC2007/Annotations/{}.xml', 'VOCdevkit/VOC2007/ImageSets/Main/val.txt', class_name, '.')
		print("{} :\t {}".format(class_name, ap))
		mAPs.append(ap)
mAP = tuple(mAPs)
print("mAP :\t {}".format(float(sum(mAP)/len(mAP))))

经过计算,yolov3.weights在VOC数据集上的mAP达到了0.82。

到此,基于VOC数据的准备、训练、验证、mAP计算就已经完成了。我们可以进入在自己的数据集上做这些流程的工作了。

二. 数据自己的训练,得到自己的模型

要开始用我们自己的数据训练了,这个过程有5步:

  1. 准备数据,包括训练集、验证集等数据。
  2. 修改训练参数,也就是之前的.cfg和.data 文件。
  3. 训练模型,得到训练结果.weights文件。
  4. 测试训练结果。
  5. 利用.weights文件,在验证集上计算mAP。

1. 准备数据

这个步骤就是生成VOC数据时的Animiations、ImagesSets、JPEGImages、labels的过程。在之前VOC数据训练的时候,我们已经知道了这几个文件夹的作用:

  • JPEGImages里存放的是图片原图,注意格式都是jpg,大小统一。
  • Animiations里都是描述对应图片物体信息的xml文件,属于图片标注信息。
  • labels里存放的是对应图片的供YOLOv3读取的标准信息。
  • ImagesSet里的Main文件夹下存放的是测试集,验证集等数据的文件名。

那么我们要怎么生成这些数据呢?之前用到的voc_label.py文件就是生成VOC数据的程序,打开参考了下,形成了思路:

  1. 原图肯定是有的,也就是JPEGImages里的数据是很容易产生的,可以通过一个脚本程序复制原生图片到JPEGImages下。
  2. Animiations需要产生,有个叫做ImageLabels的工具能够对图片进行区域标注,但是我拿到的数据其实是有标注信息的,只不过标注信息是json格式的,我需要写个程序把json的标注转化成xml的标注。
  3. labels里数据可以通过略微修改下voc_label.py的程序,通过读取Animations下的xml文件,利用其中的convert函数生成。
  4. ImagesSet下的Main文件夹里的数据,可以通过写一个脚本程序生成。

在这里我就不展示生成这些数据所用到的脚本程序了,总之就是生成和VOC数据格式一样的文件就行。注意voc_label.py里的classes分类是你要分类的命名,和后面的my.names里的文件中的命名一致。

2. 制作.cfg和.data文件

复制一份voc的.data文件,供修改用,这样也不至于破坏voc的文件。

cp cfg/voc.data cfg/my.data

打开my.data:

classes = 6 # 我要识别6种分类 
train = data/my_train.txt # 训练集保存位置 
valid = data/my_test.txt # 验证集保存位置 
names = data/my.names # 类别名称存储文件 
backup = backup # 模型文件输出地址

参考voc的训练文件和验证文件,发现其中内容就是训练图片和验证图片的保存位置。同样通过一个脚本根据我的数据生成这2个文件。接着建立my.names文件,保存类别名称,一行一个,注意要和Animations里标注的类名一致。这里我的my.names文件如下:

Pedestrian 
Cyclist 
Car 
Truck 
Tram 
Tricycle

好了,my.data文件制作到此结束,接着要制作.cfg了。同样复制一份voc的cfg:

cp cfg/yolov3-voc.cfg cfg/my.cfg

修改如下参数:

[net]
batch=64
subdivisions=16
max_batches=2000
...
[convolutional]
filters=33
[yolo]
classes=6
random=1
...
[convolutional]
filters=33
[yolo]
classes=6
random=1
...
[convolutional]
filters=33
[yolo]
classes=6
random=1

注意:[yolo]和[convolutional]的修改一共有3处,都需要修改。

batch和subdivisions之前说过含义了,就不再讲述了。

  • max_batches:训练的最大轮数,理论上越大越好,原始的是50200,但是这样会比较耗时,所以我先修改到2000,等完成训练后根据效果看看是否要增大。
  • classes:要识别的类别数量。
  • filters:filters = (classes + 5) * 3,这里就是(6 + 5) * 3 = 33。
  • random:如果显存不够大,可以设置为0。

其他cfg参数都没有修改,我们先跑通训练自己的数据,然后再回头来调整一些参数。

至此,.cfg文件也准备好了。我们可以开始训练自己的模型了,建议刚开始自己训练的时候不要准备过大的数据集数量,可以有个50张左右的图片先开始执行就行,先完整体验下后边的训练、测试等步骤,没有错误后,再修改训练集数据量,逐步放大这个过程。

3. 训练自己的模型

准备好以上文件后,通过命令开始训练:

./darknet detector train data/my.data cfg/my.cfg darknet53.conv.74

如果训练到一半终止了,想继续训练可以利用命令:

./darknet detector train data/my.data cfg/my.cfg darknet53.conv.74 backup/xxx.backup

程序输出样式和之前VOC数据训练的输出一样,说明正常开始了。要是没正常开始,会报一些错误,比如有的文件找不到之类的,检查文件名看看是否正确。修改报错,重新训练即可。

一个完备的数据集合训练非常耗时,我用max_batches=50200的配置,在Geforce RTX 2070的GPU下,1.7GB的训练数据,训练了4天才完成。

这个过程我自己在小规模数据集合和小的max_batches上尝试训练了2次,这个过程有助于调整一些配置文件的错误,如果文件有错,最后的.weights文件是不能测试出结果的。可能遇到的错误有:

  1. classes和filters的数量错误,filters = (classes + 5) * 3
  2. 数据集合里的文件名或者地址有错误。
  3. .names里的类别名和数据标注里的类别名不一致,比如大小写不一致造成的。

4. 测试自己的模型

跟VOC数据集上的验证步骤一样,我们需要先修改my.cfg的参数:

[net]
#Testing
batch=1
subdvisions=1
#Trainning
#batch=64
#subdvisions=16

然后使用命令进行测试:

./darknet detector test cfg/my.data cfg/my.cfg backup/my_final.weights data/test.jpg

同VOC数据集的效果一样,如果成功,会在项目目录下生成predictions.jpg文件,显示目标识别结果。如下图所示:

从0开始实现目标检测——实践篇_第7张图片

5. 计算自己模型的mAP

这一步跟刚才基于VOC数据的mAP计算过程一样,先通过命令计算验证集的数据,然后再通过脚本计算mAP。如下:

./darknet detector valid cfg/my.data cfg/my.cfg backup/my_final.weights

在调用计算mAP的脚本前,注意修改代码里的验证集、数据集的路径就行。

最后,自己模型的mAP达到了0.71。

三. 总结

本系列文章共两篇,总结记录了如何从0开始实现一个目标检测算法的过程。关于算法原理的部分可以参看这篇文章《从0开始实现目标检测——原理篇》。我们在这个过程中先学习了目标检测任务的主要原理,找到了衡量模型的指标mAP,接着在众多的模型中选取了YOLOv3作为项目的实现方式,然后在VOC数据集上体验了YOLOv3的训练、测试、验证、计算mAP的全过程,接着在自己的数据集上也造作了训练、测试、验证、计算mAP的全过程,最后得到的模型mAP是0.71。这个过程中的数据集准备是非常重要和关键的,然后我们也积累了一些用于数据集生成和结果验证的脚本。

接着有什么可以进一步提高的吗?在这里我也列出一个TODO List,后续有时间会相继完成,到时候再总结记录下来分享给大家。

  • 修改下验证集和测试集的划分,以及my.cfg中的其他一些参数,再次训练看看能否提高mAP。
  • 利用一些云端GPU的服务器,看看能否加速训练过程。
  • 采用YOLOv5再训练一遍自己的数据,看看mAP是否有所提升。
  • 采用faster-rcnn训练一遍自己的数据,看看mAP和识别时间究竟有多大差异。
  • 训练过程的可视化,能实时看到收敛曲线等。

你可能感兴趣的:(目标检测,人工智能,计算机视觉)