目录
YOLOv4 网络架构
论文原址:https://arxiv.org/pdf/2004.10934
YOLOv4 相比于 YOLOv3 在多个方面进行了改进和优化以提升检测精度和速度,以下是一些主要的改变:
- CSP结构:YOLOv4 采用了 CSP 结构来减少计算量,同时保持模型的准确率。CSP 结构通过在网络的不同阶段共享计算量来减少参数数量和计算复杂度。
- PAN结构:YOLOv4 引入了 PAN 结构,这是一种自底向上的特征融合方式,它结合了 FPN(特征金字塔网络)的结构,增强了模型对不同尺度目标的检测能力。
- SPP模块:YOLOv4 使用了 SPP 模块来增大感受野,这有助于提取不同尺度的特征,从而提高对小目标的检测性能。
- 激活函数:YOLOv4 在其主干网络中使用了 Mish 激活函数,而 YOLOv3 使用的是 Leaky ReLU 激活函数。Mish 激活函数提供了平滑的非单调激活,有助于提高模型性能。
- 数据增强:YOLOv4 引入了如 Mosaic 数据增强等新的数据增强技术,这有助于提高模型的泛化能力。
- 损失函数:YOLOv4 在训练时使用了 CIOU_Loss 作为边界框回归的损失函数,而 YOLOv3 使用的是传统的 Smooth L1 Loss 或 IoU Loss。CIOU_Loss 考虑了边界框的重叠面积、中心点距离以及宽高比,有助于提高边界框预测的精度。
- 非极大值抑制(NMS):YOLOv4 对 NMS 进行了改进,使用了 DIOU NMS,它在传统的 NMS 基础上考虑了边界框中心点的距离,有助于提高遮挡情况下目标的检测性能。
YOLOv4 网络的模型架构如下:
组成的各模块作用如下:
- CBM:Yolov4网络结构中的最小组件,由Conv+Bn+Mish激活函数三者组成。
- CBL:由Conv+Bn+Leaky_relu激活函数三者组成。
- Res unit:借鉴Resnet网络中的残差结构,让网络可以构建的更深。
- CSPX:借鉴CSPNet网络结构,由卷积层和X个Res unint模块Concate组成。
- SPP:采用1×1,5×5,9×9,13×13的最大池化的方式,进行多尺度融合。
- Concat:张量拼接,维度会扩充,和Yolov3中的解释一样,对应于cfg文件中的route操作。
- add:张量相加,不会扩充维度,对应于cfg文件中的shortcut操作。
YOLOv4 创新点
YOLOv4 的创新主要分为4个方面:输入端、Backbone、Neck、Prediction。
- 输入端:主要包括Mosaic数据增强、cmBN、SAT自对抗训练。
- Backbone主干网络:将各种新的方式结合起来,包括:CSPDarknet53、Mish激活函数、Dropblock。
- Neck:目标检测网络在BackBone和最后的输出层之间往往会插入一些层,如Yolov4中的SPP模块、FPN+PAN结构。
- Prediction:输出层的锚框机制和Yolov3相同,主要改进的是训练时的损失函数CIOU_Loss,以及预测框筛选的NMS变为DIOU_nms。
下面对每一个创新点做详细介绍。
输入端
Mosaic数据增强
Mosaic数据增强是在CutMix的基础上改进得到,CutMix只使用了2张图片进行拼接,而Mosaic数据增强则采用了4张图片,随机缩放、随机裁剪、随机排布的方式进行拼接,从而生成新的训练样本,使模型在更小的范围内识别目标。
Mosaic数据增强的主要步骤为︰
- 随机选取图片拼接基准点坐标(xc, yc),另随机选取四张图片。
- 四张图片根据基准点,分别经过尺寸调整和比例缩放后,放置在指定尺寸的大图的左上、右上、左下、右下位置。
- 根据每张图片的尺寸变换方式,将映射关系对应到图片标签上。
- 依据指定的横纵坐标,对大图进行拼接。处理超过边界的检测框坐标。
CmBN
CmBN是一种改进的批量归一化技术,旨在解决传统批量归一化(BN)在小批量尺寸下性能下降的问题。通过在单个批次内的小批次之间收集统计信息来进行归一化,而不是仅在一个mini-batch内部进行。这种方法使得网络能够在较小的mini-batch尺寸下也能获得准确的归一化统计数据,从而提高了模型的泛化能力和性能。
SAT自对抗训练
SAT 是一种数据增强技术。首先,它对训练样本进行前向传播。在传统的反向传播算法中,通过调整模型权重来改进检测器的性能。在这里,它朝着相反的方向改变图像来最大化降低检测器的性能。也就是说,尽管新图片在视觉上看起来是一样的,但它会对当前模型产生对抗性攻击。接下来,使用原始边界框和类标签对模型进行训练,这有助于模型泛化并减少过拟合。
Backbone主干网络
CSPDarknet53
CSPDarknet53融合了Yolov3主干网络Darknet53和CSPNet后产生的Backbone结构,其包含了5个CSP模块。每个CSP模块前面的卷积核的大小都是3*3,stride=2,可以起到下采样的作用。由于Backbone有5个CSP模块,输入图像是608*608,所以特征图变化的规律是:608->304->152->76->38->19。即608*608图像经过5次CSP模块后得到19*19大小的特征图。
CSPNet的引入是为了减少反向传播时的梯度的重复(梯度组合更加丰富同时还能减少计算量)。CSP模块先将基础层的特征映射划分为两部分,然后通过跨阶段层次结构将它们合并,在减少了计算量的同时可以保证准确率。
CSPDarknet53网络结构,使得YOLOv4具有以下优点:增强CNN的学习能力,使得在轻量化的同时保持准确性、降低计算瓶颈、降低内存成本。
上图是DenseNet的示意图以及CSPDenseNet的改进,改进点在于CSPNet将浅层特征映射为两个部分,一部分经过Dense模块(图中的Partial Dense Block),另一部分直接与Partial Dense Block输出进行concat。
Mish激活函数
Mish计算公式:
Mish优点:
- 无上界有下界:无上界是任何激活函数都需要的特征,因为它避免了导致训练速度急剧下降的梯度饱和,因此加快训练过程。无下界有助于实现强正则化效果(适当的拟合模型)。(这个性质类似于ReLU和Swish的性质,其范围是[~0.31,])
- 非单调函数:这种性质有助于保持小的负值,从而稳定网络梯度流。大多数常用的激活函数,如ReLU [ f(x)= max(0, x) ],Leaky ReLU[ f(x) = max (0,x),1 ],由于其差分为0,不能保持负值,因此大多数神经元没有得到更新。
- 无穷阶连续性和光滑性:Mish函数是光滑函数,具有较好的泛化能力和结果的有效优化能力,可以提高结果的质量。
YOLOv4只在Backbone中采用了Mish激活函数,网络后面仍然采用Leaky_relu激活函数。
Dropblock
YOLOv4在预防过拟合这一措施上并未使用传统的Dropout正则化方式,而是使用与其相似的Dropblock。传统的Dropout是随机删除减少神经元的数量,网络对某个神经元的权重变化更不敏感。
Dropblock和Dropout的实现方式相似,如下图:
图中:(a)是原始图像;(b)Dropout;(c)Dropblock;
可以看出,普通的DropOut只是随机屏蔽掉一部分特征,而DropBlock是随机屏蔽掉一部分连续区域。图像是一个2D结构,像素或者特征点之间在空间上存在依赖关系,这样普通的DropOut在屏蔽语义就不够有效,但是DropBlock这样屏蔽连续区域块就能有效移除某些语义信息,比如狗的头,从而起到有效的正则化作用。
Neck:
SPP
在SPP模块中,使用k={1*1,5*5,9*9,13*13}的最大池化的方式,再将不同尺度的特征图进行Concat操作。这里最大池化采用padding操作,移动的步长为1,比如13×13的输入特征图,使用5×5大小的池化核池化,padding=2,因此池化后的特征图仍然是13×13大小。
SPP优点:SPP 可以产生固定大小的输出;SPP 对于特定的CNN网络设计和结构是独立的。(也就是说,只要把SPP放在最后一层卷积层后面,对网络的结构没有影响, 它只是替换了原来的池化层)
FPN+PAN
FPN层自顶向下传达强语义特征,而特征金字塔则自底向上传达强定位特征,两两联手,从不同的主干层对不同的检测层进行参数聚合。FPN 高维度向低维度传递语义信息(大目标更明确);PAN 低维度向高维度再传递一次语义信息(小目标也更明确)
Prediction:
CIOU_loss
CIOU损失函数是在IOU的基础加以改进得到,引入修正因子,以更准确地评估目标框的质量。
IOU由两个边界框的交集区域的面积与两个边界框的并集区域的面积相比得到。IOU的值在0和1之间,0表示没有重叠,1表示完全重叠。
CIoU考虑了重叠区域、中心点距离以及宽高比这三个几何因素,以提高目标检测的性能。CIOU计算公式如下:
其中:
- IoU 表示预测框与真实框之间的交集与并集的比值。
- b和分别表示预测框和真实框的中心点坐标。
- 表示两个中心点之间的欧氏距离。
- c 表示两个框最小外接矩形的对角线长度。
- v 用于度量宽高比的一致性,其计算方式涉及到预测框和真实框的宽高比。
- α 是一个权重函数,用于平衡宽高比的影响。
CIoU损失函数的优点在于:
- 形状不变性: CIOU 损失函数在设计上考虑了目标框的形状信息,通过引入修正因子,使得损失对于不同形状的目标框更具鲁棒性。这使得模型更容易捕捉目标的准确形状。
- 对定位精度的敏感性: CIOU 损失函数对目标框的位置预测更为敏感,因为它考虑了目标框的对角线距离。这有助于提高目标检测模型在定位目标时的精度。
- 全面性: CIOU 损失函数综合考虑了位置、形状和方向等多个因素,使得模型更全面地学习目标框的特征。这有助于提高模型在复杂场景中的性能。
DIOU_nms
由于CIOU_loss添加了影响因子,包含groundtruth标注框的信息,但在测试过程中,并没有groundtruth的信息,不用考虑影响因子,因此直接用DIOU_nms。
当两个不同物体挨得很近时,由于IOU值比较大,往往经过NMS处理后,只剩下一个检测框,这样导致漏检的错误情况发生。DIOU_NMS不仅仅考虑IOU,还考虑两个框中心点之间的距离。如果两个框之间IOU比较大,但是两个框的距离比较大时,可能会认为这是两个物体的框而不会被过滤掉。
YOLOv4 网络搭建
Backbone网络搭建:CSPDarknet
import math
from collections import OrderedDict
import torch
import torch.nn as nn
import torch.nn.functional as F
#-------------------------------------------------#
# MISH激活函数
#-------------------------------------------------#
class Mish(nn.Module):
def __init__(self):
super(Mish, self).__init__()
def forward(self, x):
return x * torch.tanh(F.softplus(x))
#---------------------------------------------------#
# 卷积块 -> 卷积 + 标准化 + 激活函数
# Conv2d + BatchNormalization + Mish
#---------------------------------------------------#
class BasicConv(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride=1):
super(BasicConv, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, kernel_size//2, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.activation = Mish()
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.activation(x)
return x
#---------------------------------------------------#
# CSPdarknet的结构块的组成部分
# 内部堆叠的残差块
#---------------------------------------------------#
class Resblock(nn.Module):
def __init__(self, channels, hidden_channels=None):
super(Resblock, self).__init__()
if hidden_channels is None:
hidden_channels = channels
self.block = nn.Sequential(
BasicConv(channels, hidden_channels, 1),
BasicConv(hidden_channels, channels, 3)
)
def forward(self, x):
return x + self.block(x)
#--------------------------------------------------------------------#
# CSPdarknet的结构块
# 首先利用ZeroPadding2D和一个步长为2x2的卷积块进行高和宽的压缩
# 然后建立一个大的残差边shortconv、这个大残差边绕过了很多的残差结构
# 主干部分会对num_blocks进行循环,循环内部是残差结构。
# 对于整个CSPdarknet的结构块,就是一个大残差块+内部多个小残差块
#--------------------------------------------------------------------#
class Resblock_body(nn.Module):
def __init__(self, in_channels, out_channels, num_blocks, first):
super(Resblock_body, self).__init__()
#----------------------------------------------------------------#
# 利用一个步长为2x2的卷积块进行高和宽的压缩
#----------------------------------------------------------------#
self.downsample_conv = BasicConv(in_channels, out_channels, 3, stride=2)
if first:
#--------------------------------------------------------------------------#
# 然后建立一个大的残差边self.split_conv0、这个大残差边绕过了很多的残差结构
#--------------------------------------------------------------------------#
self.split_conv0 = BasicConv(out_channels, out_channels, 1)
#----------------------------------------------------------------#
# 主干部分会对num_blocks进行循环,循环内部是残差结构。
#----------------------------------------------------------------#
self.split_conv1 = BasicConv(out_channels, out_channels, 1)
self.blocks_conv = nn.Sequential(
Resblock(channels=out_channels, hidden_channels=out_channels//2),
BasicConv(out_channels, out_channels, 1)
)
self.concat_conv = BasicConv(out_channels*2, out_channels, 1)
else:
#--------------------------------------------------------------------------#
# 然后建立一个大的残差边self.split_conv0、这个大残差边绕过了很多的残差结构
#--------------------------------------------------------------------------#
self.split_conv0 = BasicConv(out_channels, out_channels//2, 1)
#----------------------------------------------------------------#
# 主干部分会对num_blocks进行循环,循环内部是残差结构。
#----------------------------------------------------------------#
self.split_conv1 = BasicConv(out_channels, out_channels//2, 1)
self.blocks_conv = nn.Sequential(
*[Resblock(out_channels//2) for _ in range(num_blocks)],
BasicConv(out_channels//2, out_channels//2, 1)
)
self.concat_conv = BasicConv(out_channels, out_channels, 1)
def forward(self, x):
x = self.downsample_conv(x)
x0 = self.split_conv0(x)
x1 = self.split_conv1(x)
x1 = self.blocks_conv(x1)
#------------------------------------#
# 将大残差边再堆叠回来
#------------------------------------#
x = torch.cat([x1, x0], dim=1)
#------------------------------------#
# 最后对通道数进行整合
#------------------------------------#
x = self.concat_conv(x)
return x
#---------------------------------------------------#
# CSPdarknet53 的主体部分
# 输入为一张416x416x3的图片
# 输出为三个有效特征层
#---------------------------------------------------#
class CSPDarkNet(nn.Module):
def __init__(self, layers):
super(CSPDarkNet, self).__init__()
self.inplanes = 32
# 416,416,3 -> 416,416,32
self.conv1 = BasicConv(3, self.inplanes, kernel_size=3, stride=1)
self.feature_channels = [64, 128, 256, 512, 1024]
self.stages = nn.ModuleList([
# 416,416,32 -> 208,208,64
Resblock_body(self.inplanes, self.feature_channels[0], layers[0], first=True),
# 208,208,64 -> 104,104,128
Resblock_body(self.feature_channels[0], self.feature_channels[1], layers[1], first=False),
# 104,104,128 -> 52,52,256
Resblock_body(self.feature_channels[1], self.feature_channels[2], layers[2], first=False),
# 52,52,256 -> 26,26,512
Resblock_body(self.feature_channels[2], self.feature_channels[3], layers[3], first=False),
# 26,26,512 -> 13,13,1024
Resblock_body(self.feature_channels[3], self.feature_channels[4], layers[4], first=False)
])
self.num_features = 1
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def forward(self, x):
x = self.conv1(x)
x = self.stages[0](x)
x = self.stages[1](x)
out3 = self.stages[2](x)
out4 = self.stages[3](out3)
out5 = self.stages[4](out4)
return out3, out4, out5
def darknet53(pretrained):
model = CSPDarkNet([1, 2, 8, 8, 4])
if pretrained:
model.load_state_dict(torch.load("model_data/CSPdarknet53_backbone_weights.pth"))
return model
YOLOv4 完整网络实现
from collections import OrderedDict
import torch
import torch.nn as nn
from nets.CSPdarknet import darknet53
def conv2d(filter_in, filter_out, kernel_size, stride=1):
pad = (kernel_size - 1) // 2 if kernel_size else 0
return nn.Sequential(OrderedDict([
("conv", nn.Conv2d(filter_in, filter_out, kernel_size=kernel_size, stride=stride, padding=pad, bias=False)),
("bn", nn.BatchNorm2d(filter_out)),
("relu", nn.LeakyReLU(0.1)),
]))
#---------------------------------------------------#
# SPP结构,利用不同大小的池化核进行池化
# 池化后堆叠
#---------------------------------------------------#
class SpatialPyramidPooling(nn.Module):
def __init__(self, pool_sizes=[5, 9, 13]):
super(SpatialPyramidPooling, self).__init__()
self.maxpools = nn.ModuleList([nn.MaxPool2d(pool_size, 1, pool_size//2) for pool_size in pool_sizes])
def forward(self, x):
features = [maxpool(x) for maxpool in self.maxpools[::-1]]
features = torch.cat(features + [x], dim=1)
return features
#---------------------------------------------------#
# 卷积 + 上采样
#---------------------------------------------------#
class Upsample(nn.Module):
def __init__(self, in_channels, out_channels):
super(Upsample, self).__init__()
self.upsample = nn.Sequential(
conv2d(in_channels, out_channels, 1),
nn.Upsample(scale_factor=2, mode='nearest')
)
def forward(self, x,):
x = self.upsample(x)
return x
#---------------------------------------------------#
# 三次卷积块
#---------------------------------------------------#
def make_three_conv(filters_list, in_filters):
m = nn.Sequential(
conv2d(in_filters, filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
conv2d(filters_list[1], filters_list[0], 1),
)
return m
#---------------------------------------------------#
# 五次卷积块
#---------------------------------------------------#
def make_five_conv(filters_list, in_filters):
m = nn.Sequential(
conv2d(in_filters, filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
conv2d(filters_list[1], filters_list[0], 1),
conv2d(filters_list[0], filters_list[1], 3),
conv2d(filters_list[1], filters_list[0], 1),
)
return m
#---------------------------------------------------#
# 最后获得yolov4的输出
#---------------------------------------------------#
def yolo_head(filters_list, in_filters):
m = nn.Sequential(
conv2d(in_filters, filters_list[0], 3),
nn.Conv2d(filters_list[0], filters_list[1], 1),
)
return m
#---------------------------------------------------#
# yolo_body
#---------------------------------------------------#
class YoloBody(nn.Module):
def __init__(self, anchors_mask, num_classes, pretrained = False):
super(YoloBody, self).__init__()
#---------------------------------------------------#
# 生成CSPdarknet53的主干模型
# 获得三个有效特征层,他们的shape分别是:
# 52,52,256
# 26,26,512
# 13,13,1024
#---------------------------------------------------#
self.backbone = darknet53(pretrained)
self.conv1 = make_three_conv([512,1024],1024)
self.SPP = SpatialPyramidPooling()
self.conv2 = make_three_conv([512,1024],2048)
self.upsample1 = Upsample(512,256)
self.conv_for_P4 = conv2d(512,256,1)
self.make_five_conv1 = make_five_conv([256, 512],512)
self.upsample2 = Upsample(256,128)
self.conv_for_P3 = conv2d(256,128,1)
self.make_five_conv2 = make_five_conv([128, 256],256)
# 3*(5+num_classes) = 3*(5+20) = 3*(4+1+20)=75
self.yolo_head3 = yolo_head([256, len(anchors_mask[0]) * (5 + num_classes)],128)
self.down_sample1 = conv2d(128,256,3,stride=2)
self.make_five_conv3 = make_five_conv([256, 512],512)
# 3*(5+num_classes) = 3*(5+20) = 3*(4+1+20)=75
self.yolo_head2 = yolo_head([512, len(anchors_mask[1]) * (5 + num_classes)],256)
self.down_sample2 = conv2d(256,512,3,stride=2)
self.make_five_conv4 = make_five_conv([512, 1024],1024)
# 3*(5+num_classes)=3*(5+20)=3*(4+1+20)=75
self.yolo_head1 = yolo_head([1024, len(anchors_mask[2]) * (5 + num_classes)],512)
def forward(self, x):
# backbone
x2, x1, x0 = self.backbone(x)
# 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,2048
P5 = self.conv1(x0)
P5 = self.SPP(P5)
# 13,13,2048 -> 13,13,512 -> 13,13,1024 -> 13,13,512
P5 = self.conv2(P5)
# 13,13,512 -> 13,13,256 -> 26,26,256
P5_upsample = self.upsample1(P5)
# 26,26,512 -> 26,26,256
P4 = self.conv_for_P4(x1)
# 26,26,256 + 26,26,256 -> 26,26,512
P4 = torch.cat([P4,P5_upsample],axis=1)
# 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
P4 = self.make_five_conv1(P4)
# 26,26,256 -> 26,26,128 -> 52,52,128
P4_upsample = self.upsample2(P4)
# 52,52,256 -> 52,52,128
P3 = self.conv_for_P3(x2)
# 52,52,128 + 52,52,128 -> 52,52,256
P3 = torch.cat([P3,P4_upsample],axis=1)
# 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128
P3 = self.make_five_conv2(P3)
# 52,52,128 -> 26,26,256
P3_downsample = self.down_sample1(P3)
# 26,26,256 + 26,26,256 -> 26,26,512
P4 = torch.cat([P3_downsample,P4],axis=1)
# 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
P4 = self.make_five_conv3(P4)
# 26,26,256 -> 13,13,512
P4_downsample = self.down_sample2(P4)
# 13,13,512 + 13,13,512 -> 13,13,1024
P5 = torch.cat([P4_downsample,P5],axis=1)
# 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512
P5 = self.make_five_conv4(P5)
#---------------------------------------------------#
# 第三个特征层
# y3=(batch_size,75,52,52)
#---------------------------------------------------#
out2 = self.yolo_head3(P3)
#---------------------------------------------------#
# 第二个特征层
# y2=(batch_size,75,26,26)
#---------------------------------------------------#
out1 = self.yolo_head2(P4)
#---------------------------------------------------#
# 第一个特征层
# y1=(batch_size,75,13,13)
#---------------------------------------------------#
out0 = self.yolo_head1(P5)
return out0, out1, out2