Yolov5总结文档(理论、代码、实验结果)

点击上方“3D视觉工坊”,选择“星标”

干货第一时间送达

ce0ed970e649aa69704d472a5faa71dc.png

作者丨Mr.Hang@知乎

来源丨https://zhuanlan.zhihu.com/p/449257337

编辑丨3D视觉工坊

本篇文章是对Yolo-v5的一个总结,全文一共分为四个部分。第一个部分主要介绍Yolo-v5的结构以及相对于之前版本的一些改进;第二部分是对Yolo-v5代码主要部分的解读,包括如何更换backbone的细节;第三部分给出了两次实验的结果和在测试集上的表现;最后一部分是我对Yolo-v5的简要总结和思考。

一、Yolo-v5结构

首先我用一张图来简单说明Yolo-v5的前向过程:

89261ff4d3cd8d83f6aaef9187b6c688.png

图1 Yolo-v5简要前向过程

Yolo-v5相较于Yolo-v4来说改动不是特别大,最主要的区别在于对于anchor的处理机制,这个机制我觉得也是让Yolo-v5收敛快的核心,另外Yolo-v5的loss与之前的Yolo系列也有些差别,接下来我从输入、Backbone、Neck、Loss四个部分来介绍Yolo-v5的结构。

1.输入

首先对于数据增强,Yolov5的输入端采用了和Yolov4一样的Mosaic数据增强的方式,Yolov3则没有采用这种数据增强,Mosaic的具体细节不在这里过多阐述,不过这种方式的数据增强对于小目标的检测有比较大的提升。不同于Yolov4的是,Yolov5在选定锚框比时采用了自适应锚框计算,此前的Yolov3和Yolov4都是先采用聚类算法在数据集中预先训练,选好9个anchor的宽高,但Yolov5中将此功能嵌入到代码中,每次训练时,自适应的计算不同训练集中的最佳锚框值。在train.py中的参数说明中,“noautoanchor”参数便是用来控制该操作,设置为ture则启用自适应anchor机制,设置为false则采用给定的anchor。

2.Backbone(以Yolov5s为例)

Backbone部分Yolov5与Yolov4并没有太大区别,主要的区别在于Yolov5在输入之后增加了一个Fcos,我对于Fcos的理解是它其实就和Yolov2中的PassThrough-Layer类似,为了方便我借鉴一张网上的图片来理解:

de4f7c82ae133bb30c21501dba61c13c.png

图2 Fcos的切片操作(图片借鉴于网络)

在一个channel上进行上图的操作,最终的channel数是原featuremap的四倍。相比于Yolov4,还有一个不同的地方是Yolov5设计了两种CSP结构,分别用在了Backbone和neck部分,CSP结构涉及到了depth_multiple和width_multiple两个参数,这两个参数使得整个Backbone的设计能够更加灵活,具体细节会在代码部分进行说明。

3.Neck

Yolov5的neck部分用到了上面提到的CSP结构,我觉得目的就是为了能够更好地与前面网络提取的特征进行融合,其余地部分就与Yolov4没有区别,主要用FPN+PAN来进行下采样和上采样,给出三个不同尺度的featuremap,用来进行预测。

4.Loss

Yolov5相较于之前的版本,最大的改动就在这个部分,而这个部分最大的改动就是对于正样本anchor区域的计算。在之前的Yolo系列中,对于每一个ground truth(后面简称为gt),都有一个唯一的anchor与其对应,而这个anchor选择的方式就是选与gt的IOU最大的那个anchor,不考虑一个gt对应多个anchor的情况。Yolov5采用的匹配规则是:计算bbox和当前层anchor的宽高比,若宽高比大于设定的阈值,则该anchor与bbox不匹配,丢弃该bbox,认为其为负样本。剩下的bbox,计算它落在哪个网格内并要寻找出相邻的两个网格,认为这三个网格都可能是来预测该bbox的,这就和之前的Yolo系列有很大不同,单从这里来看现在的正样本anchor数量比以前至少增加3倍之多。也正是因为如此,对于一个bbox,那么至少有3个anchor进行匹配。对于loss函数的计算,总体还是分为三部分:类别损失、置信度损失和定位损失,在类别损失和置信度损失上仍然采用BCEloss,这与Yolov3和Yolov4相同,但是对于定位损失,即w、h、x、y的loss,采用了GIoU-loss。整个loss函数的定义我整理为如下表达式:

7e4642415972cc2309253602d04bc052.png

二、修改Backbone

Yolov5的源码中,是将yolov5s的结构封装在“yolov5s.yaml”中,但是他没有单独写neck,将neck分开在了backbone和head里面。

第一部分是三个参数,第一个是数据集中的类别数,因为使用的是VOC2007的数据,所以类别是20,第二个用来调整网络的深度,第三个用来调整网络的宽度,具体怎么调整的结合后面的backbone代码解释。

# parameters
nc: 20  # number of classes
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50  # layer channel multiple

第二部分是backbone部分:

backbone:
 # [from, number, module, args]
 [[-1, 1, Focus, [64, 3]],  # 0-P1/2
 [-1, 1, Conv, [128, 3, 2]],  # 1-P2/4
 [-1, 3, C3, [128]],
   [-1, 1, Conv, [256, 3, 2]],  # 3-P3/8
 [-1, 9, C3, [256]],
   [-1, 1, Conv, [512, 3, 2]],  # 5-P4/16
 [-1, 9, C3, [512]],
   [-1, 1, Conv, [1024, 3, 2]],  # 7-P5/32
 [-1, 1, SPP, [1024, [5, 9, 13]]],
   [-1, 3, C3, [1024, False]],  # 9
 ]

这部分是backbone部分的具体网络结构,四个参数的意义分别是:

第一个参数:从哪一层获得输入,-1表示从上一层获得,-2表示从上两层获得;

第二个参数:表示有几个相同的模块,如果为9则表示有9个相同的模块;

第三个参数:模块的名称,这些模块写在common.py中;

第四个参数:这个参数就与第一部分的“width_multiple”参数有关了,上面把width_multiple设置为了0.5,那么第一个[64,3]就会被解析为[3,64*0.5=32,3],其中第一3为输入channel(因为输入),32为输出channel,第二个3为卷积核大小,第四个参数为步长,没有设置则默认为1。对于第二行的[128,3,2]也是同样的道理,它会被解析为[32,128*0.5=64,3,2],第一个为上一层的channel即32,第二个参数还是根据“width_multiple”参数计算.剩下部分均按照这一规律计算,head部分也相同。

“width_multiple”参数的作用已经介绍过了,那么“depth_multiple”又是什么作用呢?在yolo.py的247行有对它的定义,具体代码如下:

n = max(round(n * gd), 1) if n > 1 else n # depth gain

暂且将这段代码当作公式(1),其中gd就是depth_multiple的值,n的值就是backbone中列表的第二个参数:

838caceeba1dc664c12a0438e6251577.png

以gd=0.33为例,当n=1时计算得出公式(1)中的n=1,计算出的n就代表了该模块有几个残差结构,当n=9时可以算出公式(1)中的n=3,说明有3个残差结构。

yaml文件的内容会在yolo.py中进行调用,相当于可以灵活地修改网络的结构,只需要修改“width_multiple”和“depth_multiple”两个参数就可以。

如果要替换backbone的话其实就只用在common.py中将需要的banckbone所包含的结构写出来,然后重新建一个yaml配置文件就可以。因为要求采用的backbone为 MobileNetV3-Small,所以首先在common.py中增加如下代码:

class h_sigmoid(nn.Module):
    def __init__(self, inplace=True):
        super(h_sigmoid, self).__init__()
        self.relu = nn.ReLU6(inplace=inplace)

    def forward(self, x):
        return self.relu(x + 3) / 6

class h_swish(nn.Module):
    def __init__(self, inplace=True):
        super(h_swish, self).__init__()
        self.sigmoid = h_sigmoid(inplace=inplace)
    def forward(self, x):
        y = self.sigmoid(x)
        return x * y

class SELayer(nn.Module):
    def __init__(self, channel, reduction=4):
        super(SELayer, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc = nn.Sequential(
                nn.Linear(channel, channel // reduction),
                nn.ReLU(inplace=True),
                nn.Linear(channel // reduction, channel),
                h_sigmoid()
        )
    def forward(self, x):
        b, c, _, _ = x.size()
        y = self.avg_pool(x)
        y = y.view(b, c)
        y = self.fc(y).view(b, c, 1, 1)
        return x * y

class conv_bn_hswish(nn.Module):
    def __init__(self, c1, c2, stride):
        super(conv_bn_hswish, self).__init__()
        self.conv = nn.Conv2d(c1, c2, 3, stride, 1, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = h_swish()
    def forward(self, x):
        return self.act(self.bn(self.conv(x)))
    def fuseforward(self, x):
        return self.act(self.conv(x))
 
class MobileNetV3_InvertedResidual(nn.Module):
    def __init__(self, inp, oup, hidden_dim, kernel_size, stride, use_se, use_hs):
        super(MobileNetV3_InvertedResidual, self).__init__()
        assert stride in [1, 2]

        self.identity = stride == 1 and inp == oup

        if inp == hidden_dim:
            self.conv = nn.Sequential(
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # Squeeze-and-Excite
                SELayer(hidden_dim) if use_se else nn.Sequential(),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
        else:
            self.conv = nn.Sequential(
                # pw
                nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
                nn.BatchNorm2d(hidden_dim),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # dw
                nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim, bias=False),
                nn.BatchNorm2d(hidden_dim),
                # Squeeze-and-Excite
                SELayer(hidden_dim) if use_se else nn.Sequential(),
                h_swish() if use_hs else nn.ReLU(inplace=True),
                # pw-linear
                nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
                nn.BatchNorm2d(oup),
            )
    def forward(self, x):
        y = self.conv(x)
        if self.identity:
            return x + y
        else:
            return y

然后在同一文件夹下新建配置文件“yolov5-mobilenetv3small.yaml”,配置文件的内容如下:

nc: 20  # number of classes
depth_multiple: 0.33  
width_multiple: 0.50  
# anchors
anchors:
  - [10,13, 16,30, 33,23]  # P3/8
 - [30,61, 62,45, 59,119]  # P4/16
 - [116,90, 156,198, 373,326]  # P5/32
# custom backbone
backbone:
 # MobileNetV3-small
  # [from, number, module, args]
 [[-1, 1, conv_bn_hswish, [16, 2]],                             # 0-p1/2
 [-1, 1, MobileNetV3_InvertedResidual, [16,  16, 3, 2, 1, 0]],  # 1-p2/4
 [-1, 1, MobileNetV3_InvertedResidual, [24,  72, 3, 2, 0, 0]],  # 2-p3/8
 [-1, 1, MobileNetV3_InvertedResidual, [24,  88, 3, 1, 0, 0]],  # 3-p3/8
 [-1, 1, MobileNetV3_InvertedResidual, [40,  96, 5, 2, 1, 1]],  # 4-p4/16
 [-1, 1, MobileNetV3_InvertedResidual, [40, 240, 5, 1, 1, 1]],  # 5-p4/16
 [-1, 1, MobileNetV3_InvertedResidual, [40, 240, 5, 1, 1, 1]],  # 6-p4/16
 [-1, 1, MobileNetV3_InvertedResidual, [48, 120, 5, 1, 1, 1]],  # 7-p4/16
 [-1, 1, MobileNetV3_InvertedResidual, [48, 144, 5, 1, 1, 1]],  # 8-p4/16
 [-1, 1, MobileNetV3_InvertedResidual, [96, 288, 5, 2, 1, 1]],  # 9-p5/32
 [-1, 1, MobileNetV3_InvertedResidual, [96, 576, 5, 1, 1, 1]],  # 10-p5/32
 [-1, 1, MobileNetV3_InvertedResidual, [96, 576, 5, 1, 1, 1]],  # 11-p5/32
 ]
head:
  [[-1, 1, Conv, [256, 1, 1]],
   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
   [[-1, 8], 1, Concat, [1]],  # cat backbone P4
 [-1, 1, C3, [256, False]],  # 15
 [-1, 1, Conv, [128, 1, 1]],
   [-1, 1, nn.Upsample, [None, 2, 'nearest']],
   [[-1, 3], 1, Concat, [1]],  # cat backbone P3
 [-1, 1, C3, [128, False]],  # 19 (P3/8-small)
 [-1, 1, Conv, [128, 3, 2]],
   [[-1, 16], 1, Concat, [1]],  # cat head P4
 [-1, 1, C3, [256, False]],  # 22 (P4/16-medium)
 [-1, 1, Conv, [256, 3, 2]],
   [[-1, 12], 1, Concat, [1]],  # cat head P5
 [-1, 1, C3, [512, False]],  # 25 (P5/32-large)
 [[19, 22, 25], 1, Detect, [nc, anchors]],  # Detect(P3, P4, P5)
 ]

为了与之前实验对比,所以“width_multiple”和“depth_multiple”两个参数与之前保持一致。

三、实验结果

本次实验的结果均在NVIDIA GeForce RTX 2080 super上完成,每次训练都采用单卡训练,超参数均为源代码默认参数,epoch设置为300,batch-size设置为16。因为voc2007数据集本身是没有对数据集进行划分的,所以我自己用脚本划分了一下数据集,最终训练集有8467张图像,验证集有600张图像,测试集有896张图像。

Backbone:yolov5s

训练过程中类别损失、置信度损失、边框位置损失的曲线变化图如下:

cfc8afee0cdb1d88b4dd60c1940bed5b.png

图3 从左至右依次为训练过程中的类别损失、置信度损失、边框损失的曲线图

下图为训练过程中各类别的precision,recall和PR曲线图:

4d4ec2e83fb3f03faea11b3859043751.png

图4 从左至右依次为各类别训练过程中的precision、recall、PR曲线图

训练过程中总体的precision、recall、mAP@0.5、mAP@0.5:0.95的曲线图如下:

85ddc9e95d5a521d7e85b70893c247bb.png

图5 训练中各项指标的总体曲线变化图

训练完成后,该模型在测试集上的表现如下:

表1 Backbone使用Yolov5s在测试集上的结果

0fbcb664ac4522264ca7b7a03efdfaec.png

Backbone:MobileNetV3-Small

训练过程中类别损失、置信度损失、边框位置损失的曲线变化图如下:

ff90eb504008ba2126c7090ee91b4b30.png

图6 从左至右依次为类别损失、置信度损失、边框损失的曲线图

下图为训练过程中各类别的precision,recall和PR曲线图:

ab552a414bbac58c6b4dc496b6b7547d.png

图7 从左至右依次为各类别训练过程中的precision、recall、PR曲线图

训练过程中总体的precision、recall、mAP@0.5、mAP@0.5:0.95的曲线图如下:

f1492dd5921e14bb74a7a895482d15fc.png

图8 训练中各项指标的总体曲线变化图

我截取了几张训练过程中的图,具体如下:

0b04c40fb5a4510fa0f4383154474133.png 230cc4c4a38c0b80989eb0ab8ce1603d.png

图9 训练过程中截取的图像,因为采用了Mosaic数据增强,所以每张图都是由几张图拼接而成的

在测试集上的表现如下:

表2 Backbone使用MobileNetV3-Small在测试集上的结果

8165b1ddbdce5467dbd5f6c66b043e88.png

最后放上几张在测试集上的检测结果:

b5fd946848f5447e0f82ccf29835f17e.png 490a8240aa6ae1154103684fe31e7da5.png

图 10 测试集上的检测结果

这里再对两次实验做一个简单的总结:当Backbone使用MobileNetv3-small时,得益于其通道可分离卷积的设计,训练速度确实要快一些,在我的设备上使用yolov5s完成300个epoch的训练大概需要6个小时,而使用MobileNetv3-small只需要4.5小时左右,在网络参数上面MobileNetv3-small也占到了优势。但是从在测试集的表现来看,yolov5s在precision、recall、mAP@0.5、mAP@0.5:0.95四个指标上都超过了MobileNetv3-small,特别是在mAP@0.5这个指标上,高出了0.103。当然这只是两组实验,并没有进行太多调参等优化的工作,只能从宏观上大致认为两种Backbone的特点分别是快和准,具体使用哪个应该根据具体的使用场景来决定。

四、总结

Yolov5从发布到现在已经过去一年,它的代码也是不断在进行更新,总的来说它在Yolov4的基础上又增加了一些tricks,让模型能够更快地收敛,最终的各项指标也都有一定程度的提升。我觉得最大的两个改动是:考虑了邻域的正样本anchor匹配策略,增加了正样本;通过配置参数,可以得到不同复杂度的模型。对于前者单从实验效果上来看确实有效,但我觉得这样直接增加正样本数量应该也会对网络训练产生一些负面的影响,缺少一些理论的可解释性,也有可能是我理解还不够。对于后者通过参数配置来改变模型的复杂度,确实使得在训练中能够更加灵活地调整结构去适应不同的数据集,我理解为手动增强模型的泛化能力,不知道这样说是否准确。

本文仅做学术分享,如有侵权,请联系删文。

3D视觉精品课程推荐:

1.面向自动驾驶领域的多传感器数据融合技术

2.面向自动驾驶领域的3D点云目标检测全栈学习路线!(单模态+多模态/数据+代码)
3.彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进
4.国内首个面向工业级实战的点云处理课程
5.激光-视觉-IMU-GPS融合SLAM算法梳理和代码讲解
6.彻底搞懂视觉-惯性SLAM:基于VINS-Fusion正式开课啦
7.彻底搞懂基于LOAM框架的3D激光SLAM: 源码剖析到算法优化
8.彻底剖析室内、室外激光SLAM关键算法原理、代码和实战(cartographer+LOAM +LIO-SAM)

9.从零搭建一套结构光3D重建系统[理论+源码+实践]

10.单目深度估计方法:算法梳理与代码实现

11.自动驾驶中的深度学习模型部署实战

12.相机模型与标定(单目+双目+鱼眼)

13.重磅!四旋翼飞行器:算法与实战

重磅!3DCVer-学术论文写作投稿 交流群已成立

扫码添加小助手微信,可申请加入3D视觉工坊-学术论文写作与投稿 微信交流群,旨在交流顶会、顶刊、SCI、EI等写作与投稿事宜。

同时也可申请加入我们的细分方向交流群,目前主要有3D视觉CV&深度学习SLAM三维重建点云后处理自动驾驶、多传感器融合、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、学术交流、求职交流、ORB-SLAM系列源码交流、深度估计等微信群。

一定要备注:研究方向+学校/公司+昵称,例如:”3D视觉 + 上海交大 + 静静“。请按照格式备注,可快速被通过且邀请进群。原创投稿也请联系。

541c39c0a24b22c727f39b2c5ce6baf5.png

▲长按加微信群或投稿

ed7a1d46d1f2cfee14e89768272ec74f.png

▲长按关注公众号

3D视觉从入门到精通知识星球:针对3D视觉领域的视频课程(三维重建系列三维点云系列结构光系列手眼标定相机标定激光/视觉SLAM自动驾驶等)、知识点汇总、入门进阶学习路线、最新paper分享、疑问解答五个方面进行深耕,更有各类大厂的算法工程人员进行技术指导。与此同时,星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息,打造成集技术与就业为一体的铁杆粉丝聚集区,近4000星球成员为创造更好的AI世界共同进步,知识星球入口:

学习3D视觉核心技术,扫描查看介绍,3天内无条件退款

8bd22bd84ece5b17d319018ea10ec9fd.png

 圈里有高质量教程资料、答疑解惑、助你高效解决问题

觉得有用,麻烦给个赞和在看~  

  • 4
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值