YOLO系列介绍(二)

2022-05-06 10:27:32 浏览数 (1)

接YOLO系列介绍

YOLOV4

上图是YOLOV4的网络结构。

通过上图,我们可以看到V4比V3无论在准确率上还是检测速度上都有了一个很大的提升,在准确率上提升了10%,在速度上提升了12%。

YOLOV4并不是YOLO系列原作者写的,是Alexey Bochkovskiy发表于2020年的CVPR中,并且被原作者认可。相比于YOLOV3,V4使用了CSPDarknet53的网络结构

CSP结构是在CSPDenseNet中被提出。它能够增强CNN学习的能力;移除了计算瓶颈;降低显存的使用。使用CSP结构后能够加快网络的推理速度。

在上图中可以看到,对于输入的特征层会分成两部分——Part1和Part2。对于Part2分支是通过一系列的Dense Block,再通过一个Transition与Part1融合,有关DenseNet的内容可以参考Tensorflow深度学习算法整理 中的DenseNet。在标准的CSPDenseNet中,Part1和Part2是按照通道来进行一个均分操作,但是在YOLOV4中并不是如此分割的。在YOLOV4中是将特征层经过两个1*1的卷积层,它们的通道数都是原特征层通道数的一半。其中的一个分支经过了一系列的Res Block后再通过一个1*1的卷积层,再与另一个分支进行拼接后再通过一个1*1的卷积层来最后的输出。

我们来看一下它的BackBone(主干网络)这一块的网络结构

这个DownSample1跟上面提到的CSP结构略有不同,就是在两个分支中,它们的通道数没有减半,而是保持了特征层相同的通道数。与YOLOV3不同,V4的每一个卷积层使用的激活函数不再是LeakRelu而是Mish激活函数。在DownSample1中的Res Block也不是一个标准的Res Block,而是一个类似的结构,先通过一个1*1的卷积层变成32通道,再经过一个3*3的卷积核变回64通道再做残差连接。

Mish激活函数的公示为

图像如下

从图中可以看出它在负值的时候并不是完全截断,而是允许比较小的负梯度流入,从而保证信息流动。

DownSample2到后面的DownSample5就跟之前所说的CSP的结构是一样的了。

图像特征经过了主干网络之后会经过一个SPP(Spatial Pyramid Pooling)的网络结构作为最后的输出到Neck中

对于输入的特征层,它会依次通过一个5*5、9*9、13*13的最大池化层,它们的步距都为1,通过不同的padding之后,它们输出的大小和输入的特征层大小是相同的,再将这三个池化层的输出以及原输入进行cancat拼接。通过SPP结构,它能在一定程度上解决多尺度的问题。

现在我们来看一下YOLOV4的Neck(使用BackBone提取的特征)部分

在上图的a的右半部分以及b的部分合称Neck,它使用的是PAN(Path Aggregation NetWork)的网络结构。在YOLOV3中没有b的部分,只有对主干网络从深层向浅层进行连接,分层提取3种形状的单元格的坐标、置信度以及分类信息,这一部分我们称为FPN。在YOLOV4中,b的部分刚好与之相反,从浅层向深层进行连接并提取。

这里需要注意的是,在原始的PAN网络中关于特征层与特征层融合的部分是采用相加的策略,但是在YOLOV4中是采用concat的策略,将两个特征层在深度方向进行拼接。

我们再来看一下YOLOV4的整体结构

在上图中,一个416*416*3的彩色图像经过CSPDarknet53的主干网络,首先会通过一个ConvSet1的网络结构,该结构是一个1*1、3*3、1*1的三层卷积核的网络结构,再通过一个SPP结构,再通过一个ConvSet2的网络结构,它的结构和ConvSet1是一样的。通过ConvSet2后就得到了一个13*13*512的feature map。此时我们接上一个1*1的卷积层,通过Upsample1上采样将feature map的高和宽翻倍,从13*13变成26*26。此时我们将DownSample4的输出通过一个1*1的卷积层调整它的通道,然后再和Upsample1的输出进行concat拼接。再通过ConvSSet3得到一个26*26*256的feature map。ConvSet3是5个卷积层堆叠得到的。这里需要注意的是在CSPDarknet53中的激活函数为Mish,但是在搭建PAN模块中所采用的激活函数都是LeakReLu。通过ConvSet3经过一个1*1的卷积层再通过Upsample2将feature map的尺寸再翻倍变成52*52.此时将DownSample3的输出通过一个1*1的卷积层调整通道数然后再与Upsample2的输出进行一个cancat拼接再接上一个ConvSet4,ConvSet4与ConvSet3的结构是一样的,得到一个52*52*128的feature map。将该feature map通过一个3*3的卷积层和1*1的卷积层得到一个最后的输出,没有bn以及激活函数,它的通道数为(4 1 numclass)*3。然后将该52*52*128的feature map通过一个3*3的卷积层,步长为2的卷积层进行下采样变回26*26*256的featue map再和ConvSet3产生的26*26*256的feature map进行cancat的拼接,再通过一个ConvSet5,再经过一个3*3、1*1的卷积层得到一个26*26*256尺寸的预测输出。再将ConvSet5的输出经过一个3*3,步长为2的卷积层进行下采样,将尺寸从26*26变成13*13,再与ConvSet2所产生的13*13*512的featue map进行一个cancat的拼接,经过ConvSet6,经过3*3、1*1的卷积层,得到一个预测输出。

YOLOV4和YOLOV3最大的不同就是V3是只有主干网络 FPN就进行三层的输出,没有V4的反卷积过程。V4的FPN会与反卷积的各层进行融合,然后再进行输出。当然最原始的YOLOV3也没有SPP网络,但是YOLOV3 SPP版本是有的。

优化策略

首先就是消除Grid网格的敏感程度。在YOLOV3中,我们都是通过1*1的卷积层来预测,每滑动到一个单元格中,就会预测当前的单元格中,它所对应的3种Anchor的一系列输出值。假设当这个1*1的卷积核滑动到上图的最中间的单元格的时候,它会针对每一个Anchor回归出相应的目标边界框回归参数、置信度以及类别的score分数。针对每个Anchor,它的回归参数有tx、ty、tw和th。上图中bx是预测目标边界框(上图中的蓝色框)的中心点的x坐标,它的计算方法就是将tx传入sigmoid函数

,再加上cx(当前单元格左上角的x坐标)。by就是目标边界框中心点的y坐标,它的计算方法就是将ty传入sigmoid函数再加上cy(当前单元格左上角的y坐标)。bw是目标边界框宽度,它的计算方式是将pw(Anchoor边框的宽度,上图中的虚线框)乘以e^tw;bn是目标边界框高度,它的计算方式是将ph(Anchoor边框的高度)乘以e^th。由于sigmoid函数是在0~1之间,所以目标边界框的中心点是被限制在当前的单元格内部,这样就会产生一个问题,比如groun truth边框的中心点落在当前的单元格的边界的时候,更极端一点是在当前单元格的左上角的时候,此时我们希望σ(tx)和σ(ty)都为0,但是对sigmoid函数来看,只有当x->-∞时才能取到0

对于这种极端的数值,我们的网络一般是无法达到的,为了解决这个问题,YOLOV4的作者引入了一个缩放因子scale,这样目标边界框中心点的坐标公式就变为

一般我们会把scale设成2,那么上面的式子就变成

这样我们可以对该函数和sigmoid函数进行一个函数图像上的对比

通过上图,我们可以看到,在相同的x缩放范围之内,包含scale的式子能取到y的范围更广,故y对x变的更加的敏感,并且值域由原来的0~1变成了-0.5~1.5之间。所以改变了目标边界框中心点的坐标公式之后,我们的预测值也就可以到达0和1这些点了,就解决了这个问题了。

Mosaic data augmentation,将四张不同的图片按照一定的规则给拼接到一起,拼接好之后得到一张新的图片,能够扩张训练的样本多样性。

IoU threshold(match posotive samples),匹配正样本所用的阈值。在YOLOV3中,对于每一个ground truth(浅蓝色块),去和每一个anchor(黄色块)进行一个匹配(match)。由于每一输出特征层有3种Anchor,我们会将同一个ground truth与这三种Anchor的左上角对齐去计算一个IoU。譬如上图中,ground truth与第一种Anchor的IoU为0.25,与第二种Anchor的IoU为0.7,与第三种Anchor的IoU为0.2。假设我们设置的IoU阈值为0.3,那么满足要求的就是第二种Anchor——AT2,那么我们就会采用Anchor 2作为正样本。首先我们会将ground truth给缩放到feature map上,上图中,我们可以看到该ground truth的中心点在正中间的单元格内,那么这个单元格的Anchor 2就会作为正样本。如果有多个Anchor的IoU都大于0.3,那么这多个Anchor都会作为正样本。当然在原始的YOLOV3中,作者只会给一个Anchor赋予正样本,那么这样就会导致正样本数量过少,不如前面的方法。

在YOLOV4中

我们不仅会把中间的单元格的Anchor 2作为正样本,还会把该单元格上面的单元格和左边的单元格的Anchor 2都作为正样本,之所以能够这么做是因为YOLOV4的目标边界框中心点的坐标公式发生了改变,它的取值范围从0~1扩展到了-0.5~1.5之间了。对于当前单元格上面的单元格,它左上角的坐标到该中心点从x轴上看是小于0.5的,从y轴上看是小于1.5的,所以该中心点满足-0.5到1.5的范围内。对于当前单元格左边的单元格,它左上角的坐标到该中心点在x轴上是小于1.5的,在y轴上是小于0.5的,所以这个ground truth中心到左边单元格的范围也是在-0.5~1.5之间。通过这样的策略,它又能扩充正样本的数量。

上图是更多的情况。在第一幅图中,当ground truth的中心点在当前单元格的左上角的话,我们会取上面的左边的单元格的Anchor 2作为正样本;当ground truth的中心点在当前单元格的右下角的话,我们会取下面的右边的单元格的Anchor 2作为正样本;当ground truth的中心点在当前单元格的右上角的话,我们会取上面的右边的单元格的Anchor 2作为正样本;当ground truth的中心点在当前单元格的左下角的话,我们会取下面的左边的单元格的Anchor 2作为正样本;当ground truth的中心点刚好落在当前单元格的中心点的时候,我们只会利用当前的单元格的Anchor 2作为正样本。

Optimizered Anchors,在YOLOV3中,针对不同尺度都提供了不同的Anchor尺寸,它这个尺寸是通过聚类得到的。在YOLOV4中,作者针对512*512的尺寸又重新优化了Anchor。

YOLOV5

YOLOV5的项目作者是Glenn Jocher,也不是YOLO的原作者,并且YOLOV5没有发表论文,只是一个工程实现。YOLOV5的网络结构如下

  • Backbone: New CSP-Darknet53
  • Neck: SPPFNew CSP-PAN
  • Head: YOLOv3 Head

总体来说,V5和V4差不多,YOLOv5针对不同大小(n, s, m, l, x)的网络整体架构都是一样的,只不过会在每个子模块中采用不同的深度和宽度,分别应对yaml文件中的depth_multiple和width_multiple参数。

yolov5s.yaml

代码语言:javascript复制
depth_multiple: 0.33  # model depth multiple
width_multiple: 0.50  # layer channel multiple

还需要注意一点,官方除了n, s, m, l, x版本外还有n6, s6, m6, l6, x6,区别在于后者是针对更大分辨率的图片比如1280x1280,当然结构上也有些差异,后者会下采样64倍,采用4个预测特征层,而前者只会下采样到32倍且采用3个预测特征层。YOLOV5的整体结构图如下

在YOLOV5的backbone卷积层中使用的是分组卷积,它的激活函数由V4的Mish改成了SiLU。

代码语言:javascript复制
class Conv(nn.Module):
    # Standard convolution
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
        super().__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())

    def forward(self, x):
        return self.act(self.bn(self.conv(x)))

    def forward_fuse(self, x):
        return self.act(self.conv(x))

分组卷积主要是减少参数降低计算量用,其计算逻辑大概为假设输入的通道数为12,输出通道数为6,卷积核为3*3,分组group=3,bias=Fasle。

传统卷积每个卷积核大小为3*3*12,总参数量为6*3*3*12;分组后每个通道的输入为12/3=4,每个group的输出通道为6/3=2,每个group里面卷积核的大小为3*3*4,总的参数量为6*3*3*4。可以看出分组后参数量为传统卷积的1/group。不过在最新的YOLOV5 6.0中没有做这个分组。当我们跑一些大型模型的时候,当我们的GPU比较多,可以把卷积层的不同通道放到不同的GPU上跑的时候,就可以采用这种分组卷积。另外就是当我们只有一个GPU,而且显存不太够的情况下,为了减少参数量,也可以使用这种分组卷积。

SiLu的激活函数跟Mish是很像的,公式为

图像为

另外YOLOV5的backbone依然是一个CSP结构的,这跟YOLOV4是一样的。

代码语言:javascript复制
class Bottleneck(nn.Module):
    # Standard bottleneck
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, shortcut, groups, expansion
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_, c2, 3, 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        return x   self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))


class BottleneckCSP(nn.Module):
    # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False)
        self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False)
        self.cv4 = Conv(2 * c_, c2, 1, 1)
        self.bn = nn.BatchNorm2d(2 * c_)  # applied to cat(cv2, cv3)
        self.act = nn.SiLU()
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))

    def forward(self, x):
        y1 = self.cv3(self.m(self.cv1(x)))
        y2 = self.cv2(x)
        return self.cv4(self.act(self.bn(torch.cat((y1, y2), 1))))


class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super().__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # optional act=FReLU(c2)
        self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
        # self.m = nn.Sequential(*(CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)))

    def forward(self, x):
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

但是这里需要注意的是在YOLOV5 6.0版本之前一直使用的是BottleneckCSP结构,但是到了6.0的时候改成了C3结构,这两个结构都是CSP结构,它们的不同之处就在于C3比BottleneckCSP少了一个卷积层。而在中间的部分也不再是V4的Res Block,改成了Bottleneck,而Bottleneck也是一种类似的Res Block,就是把一层卷积改成了两层卷积。

这个是BottleneckCSP的网络结构,而红色虚线框的卷积层就是比C3多出来的部分,C3的网络结构如下

SPPF

在YOLOV4的SPP中是将输入的特征层并行输入各个大小的最大池化层,然后再去融合。但是在YOLOV5中,是将特征层串行的依次通过5*5的最大池化层,然后再将它们的输出以及特征层本身的输入进行concat的拼接。将两个5*5的最大池化层串行等同于一个9*9的最大池化层;而将三个5*5的最大池化层串行等同于一个13*13的最大池化层。但是采用SPPF的结构效率要更高,因为特征层在依次串行通过5*5的最大池化层的过程中,它比直接通过一个9*9或者13*13的最大池化层的计算量要小.

代码语言:javascript复制
class SPPF(nn.Module):
    # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher
    def __init__(self, c1, c2, k=5):  # equivalent to SPP(k=(5, 9, 13))
        super().__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * 4, c2, 1, 1)
        self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2)

    def forward(self, x):
        x = self.cv1(x)
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')  # suppress torch 1.9.0 max_pool2d() warning
            y1 = self.m(x)
            y2 = self.m(y1)
            return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))

除了SPPF和SPP的不同外,在PAN网络中,总体结构V5和V4也是相同的,但是V4的卷积层只是普通的带LeakyReLu激活函数的普通卷积层,而在V5中都调整成了带CiLu激活函数的C3结构,而C3结构也是CSP结构的,这是跟V4比较大的区别。

数据增强

Mosaic

这个跟V4是一样的,将4张图片给拼成一张图片,增加数据的多样性。

Copy paste

将不同图像中的目标给复制粘贴一下,将一张图像的目标实例分割给扣下来再放到其他图像中去。要使用这种数据增强必须在数据中有目标分割的标签,否则无法使用该增强方式。

Random affine(随机仿射变换)

这里的仿射变换指的是旋转、缩放、平移和错切。源码中只启用了随机的缩放和平移。在配置文件hyp.scratch-high.yaml中

代码语言:javascript复制
degrees: 0.0  # image rotation ( /- deg)
translate: 0.1  # image translation ( /- fraction)
scale: 0.9  # image scale ( /- gain)
shear: 0.0  # image shear ( /- deg)

MixUp

将两张图片按一定透明程度混合成一张新的图片。这种数据增强的方式被启用的概率只有10%。

Augment HSV

随机调整色度、饱和度以及明亮。

Random horizontal flip

随机的水平翻转

rectangular

同个batch里做rectangle宽高等比变换,加快训练。在我们对图像进行训练的时候,由于每张图片的宽高都可能不相同,通常我们会把图片给reshape到一个固定的大小。但是有一些图片的宽很窄,高很小,由此会产生两边的黑边就会很大。这种大黑边就会影响我们训练的速度。在YOLOV5中改进了该方法,在同一个batch中会尽可能的拥有自己单独的shape,满足这个batch中所有的图片都能有更小的黑边。

K-Means聚类Anchors

有关K-Means聚类的内容请参考聚类 。

0 人点赞