# Parameters
nc: 80 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Conv, [64, 6, 2, 2]], # 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, 6, 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, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
yolov5 的 Backbone 部分的网络结构使用 yaml 文件配置,通过 yolo.py 解析文件加了一个输入构成的网络模块。
Backbone:主干网络,是用来提取特征的网络,其作用就是提取图片中的信息,以供后面的网络使用。
这些网络如 ResNet、VGG,并非是自己设计的,因为这些网络已经证明,在分类等问题上的特征提取能力是很强的。在用这些网络作为 Backbone 的时候,直接加载官方训练好的模型参数,后面接着自己的网络。在训练过程中,对其微调,使得更适合于我们的任务。
nc: 80 # 数据集上的类别个数
depth_multiple: 0.33
width_multiple: 0.5
"""
以下两个参数为缩放因子,通过这两个参数就能够实现不同复杂度的模型设计
depth_multiple: 0.33
用来控制模型的深度(BottleneckCSP数), 在number≠1时启用;
第一个C3层的参数设置为[-1, 3, C3, [128]], number=3表示含有1个C3(3*0.33)
width_multiple: 0.5
用来控制模型的宽度, 作用于args中的ch_out;
第一个Conv层, ch_out=64, 运算中会将卷积核设为64x0.5, 输出32通道的特征图
"""
backbone:
"""
[from, number, module, args]
from列参数: 当前模块输入来源, -1表示从上一层获得取
number列参数:模块重复的次数, 1表示只有一个, 3表示有三个相同模块
"""
[[-1, 1, Conv, [64, 6, 2, 2]], # 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, 6, 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, 3, C3, [1024]],
[-1, 1, SPPF, [1024, 5]], # 9
]
"""
[-1, 1, Conv, [64, 6, 2, 2]
input:3x640x640
[ch_out, kernel, stride, padding]=[64, 6, 2, 2]
新通道数为64x0.5=32
特征图计算公式: Feature_new = (Feature_old - kernel + 2 * padding) / stride + 1
新特征图尺寸为: Feature_new = (640 - 6 + 2 * 2) / 2 + 1 = 320
"""
YOLOv6.0 版本的 Backbone 去除了 Focus 模块(便于模型导出部署),Backbone主要由CBL、BottleneckCSP/C3以及SPP/SPPF等组成,具体如下图所示:
Input(3×640×640) -> CBS(32×320×320) -> CBS(64×160×160) -> CSP1_1(64×160×160) -> CBS(128×80×80) -> CSP1_2(128×80×80) -> CBS(256×40×40) -> CSP1_3(256×40×40) -> CBS(512×20×20) -> CSP1_1(512×20×20) -> SSPF(512×20×20)
class Conv(nn.Module):
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # 初始化构造函数
"""
Standard convolution: Conv + BN + SiLU
:params c1 : 输入的channel值
:params c2 : 输出的channel值
:params k : 卷积的kernel_size
:params s : 卷积的stride
:params p : 卷积的padding, 一般是None, 可通过autopad自行计算需要的padding数
:params g : 卷积的groups数, =1是普通的卷积, >1是深度可分离卷积
:params act: 激活函数类型, False就是不使用激活函数, 类型是nn.Module, 使用传进的激活函数类型
"""
super().__init__()
# 不使用bias偏置,Conv2d和BatchNorm2d结合使用以后,会进行融合操作,融合时卷积的bias值会被消掉
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
self.bn = nn.BatchNorm2d(c2)
# 激活函数改进,近年较火且效果较好的激活函数
# nn.Identity()是网络中的占位符, 无实际操作, 在增减网络过程中, 可使整个网络层数据不变, 便于迁移权重数据;
# self.act = nn.Tanh() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = nn.Sigmoid() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = nn.ReLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = nn.LeakyReLU(0.1) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = nn.Hardswish() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = Mish() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = FReLU(c2) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = AconC(c2) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = MetaAconC(c2) if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = DyReLUA(c2, conv_type='2d') if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
# self.act = DyReLUB(c2, conv_type='2d') if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x): # 网络的执行顺序根据forward函数来决定的
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
"""
用于Model类的fuse函数
没有bn,减少推理时间,一般用于测试/验证阶段
"""
return self.act(self.conv(x))
def autopad(k, p=None): # kernel, padding
"""
根据卷积核大小k自动计算卷积核padding数(0填充),让特征图大小不变
v5有两种卷积:
1、下采样卷积: conv3x3 s=2 p=k//2=1
2、feature size不变的卷积: conv1x1 s=1 p=k//2=1
:params k: 卷积核的kernel_size
:return p: 自动计算需要的pad值(0填充)
"""
if p is None:
# 如果k是整数, p为k与2整除后向下取整; 如果k是列表等, p对应的是列表中每个元素整除2
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # 自动计算pad数
return p
分组卷积
当groups = 1时, 所有输入都卷积到所有输出当groups = 2时, 该操作等效于两个并排的卷积层, 每个层看到一半的输入通道, 产生一半的输出通道, 并且两者随后连接在一起。
在groups= in_channels时(深度可分离卷积), 每个输入通道都使用自己的滤波器进行卷积, 其大小为: out_channels/in_channels
决定是否对特征图进行激活操作, SiLU表示使用Sigmoid进行激活
CSP 即 Backbone 中的 C3,因为在 Backbone 中 C3 存在 shortcut,而在 Neck 中 C3 不使用 shortcut,所以 backbone 中的 C3 层使用 CSP1_x 表示,Neck 中的 C3 使用 CSP2_x 表示。
class C3(nn.Module):
# 简化版的BottleneckCSP, 除Bottleneck只有3个卷积,所以取名C3, 它更简单、更快、更轻,具有相似的性能
def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):
"""
在 C3TR 模块和 yolo.py 的 parse_model 模块调用
:params c1: 整个BottleneckCSP的输入channel
:params c2: 整个BottleneckCSP的输出channel
:params n : 有n个Bottleneck
:params shortcut: bool Bottleneck中是否有shortcut,默认True
:params g : Bottleneck中的3x3卷积类型 =1普通卷积 >1深度可分离卷积
:params e : expansion ratio c2*e=中间其他所有层的卷积核个数/中间所有层的输入输出channel数
"""
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)))
# 实验性 CrossConv
# 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)), dim=1))
从源码中得:输入特征图一条分支先经过 cv1,再经过 m,得到子特征图1;另一分支经过 cv2 后得到子特征图2。最后将子特征图1和子特征图2拼接后输入 cv3 得到C3层的输出。CV操作即前面的 Conv2d + BN + SiLU,关键是 m 操作。
m操作:使用 nn.Sequential 将多个 Bottleneck( Resx )串接到网络中,for 循环中的n 即网络配置文件 args 中的 number,即将 number × depth_multiple 个 Bottleneck 串接到网络中。
在 Resnet 出现之前,人们认为网络越深获取信息也越多,模型泛化效果越好。
然而网络深度到达一定程度后,模型的准确率反而降低。这并不是过拟合造成的,而是由于反向传播过程中的梯度爆炸和梯度消失。即网络越深,模型越难优化,而不是学习不到更多的特征。
为了让深层次的网络模型达到更好的训练效果,残差网络提出了残差映射替换以往的基础映射。
对于输入 x,期望输出 H(x),网络利用恒等映射将 x 作为初始结果,将原来的映射关系变成 F(x)+x。与其让多层卷积去近似估计 H(x) ,不如近似估计 H(x)-x,即近似估计残差 F(x)。
因此,ResNet 相当于将学习目标改变为目标值 H(x) 和 x 的差值,后面的训练目标就是要将残差结果逼近于 0。
目前要求解的映射为:H(x),将这个问题转换为求解网络的残差映射函数,即F(x),其中F(x) = H(x)-x。残差:观测值与估计值之间的差。H(x)是观测值,x就是估计值(即上一层ResNet输出的特征映射)。
残差模块优点
1、梯度弥散方面。加入ResNet中的shortcut结构在反传时,每两个block之间不仅传递梯度,还加上了求导之前的梯度,相当于把每一个block中向前传递的梯度人为加大,就会减小梯度弥散的可能性。
2、特征冗余方面。正向卷积时,对每一层做卷积其实只提取了图像的一部分信息,越到深层,原始图像信息的丢失越严重,仅仅是对原始图像中的一小部分特征做提取。这会发生类似欠拟合的现象,加入shortcut结构,相当于在每个block中又加入了上一层图像的全部信息,一定程度上保留了更多的原始信息。
在resnet中,人们使用带有shortcut的残差模块搭建几百层甚至上千层的网络,浅层的残差模块被命名为Basicblock(18、34),深层网络使用的的残差模块,被命名为Bottleneck(50+)。
Bottleneck 与 Basicblock 最大的区别是卷积核的组成。
Basicblock由两个3x3的卷积层组成,Bottleneck由两个1x1卷积层夹一个3x3卷积层组成:其中1x1卷积层降维后再恢复维数,让3x3卷积在计算过程中的参数量更少、速度更快。
第一个1x1的卷积把256维channel降到64维,然后在最后通过1x1卷积恢复,整体上用的参数数目:1x1x256x64 + 3x3x64x64 + 1x1x64x256 = 69632,而不使用bottleneck的话就是两个3x3x256的卷积,参数数目: 3x3x256x256x2 = 1179648,差了16.94倍。
Bottleneck减少了参数量,优化了计算,保持了原有的精度。
# Standard bottleneck(True/False) Conv + Conv + shortcut(True/False)
class Bottleneck(nn.Module):
def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):
"""
在BottleneckCSP和yolo.py的parse_model中调用
:params c1: 第一个卷积的输入channel
:params c2: 第二个卷积的输出channel
:params shortcut: bool 是否有shortcut连接 默认是True
:params g : 卷积分组的个数 =1就是普通卷积
:params e : expansion ratio c2*e=第一个卷积的输出channel=第二个卷积的输入channel
"""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) # 1x1
self.cv2 = Conv(c_, c2, 3, 1, g=g) # 3x3
self.add = shortcut and c1 == c2 # shortcut=True and c1 == c2 才能做shortcut
def forward(self, x):
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
CSP中的Bottleneck与ResNet类似,先是1x1的卷积层(CBS),然后再是3x3的卷积层,最后通过shortcut与初始输入相加。
与ResNet的不同在于:CSP将输入维度减半运算后并,未再使用1x1卷积核进行升维,而是将原始输入x也降了维,采取concat的方法进行张量的拼接,得到与原始输入相同维度的输出。
ResNet中的shortcut通过add实现,是特征图对应位置相加而通道数不变;
CSP中的shortcut通过concat实现,是通道数的增加。
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))
"""
:params c1: 第一个卷积的输入channel
:params c2: 第二个卷积的输出channel
:params k : 初始化的kernel size
"""
super().__init__()
c_ = c1 // 2 # hidden channels
self.cv1 = Conv(c1, c_, 1, 1) # 对应第一个CBS
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) # 先通过CBS进行通道数减半
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))
SSPF模块将经过CBS的x、一次池化后的y1、两次池化后的y2和三次池化后的self.m(y2)先进行拼接,然后再CBS提取特征。
虽然SSPF对特征图进行了多次池化,但特征图尺寸并未变化,通道数更不会变化,所以后续的4个输出能在channel维度进行融合。
这一模块的主要作用是对高层特征进行提取并融合,在融合的过程中作者多次运用最大池化,尽可能多的去提取高层次的语义特征。
YOLOv5 Backbone详解
分组卷积(Group Convolution)与深度可分离卷积(Depthwise Separable Convolution)
YOLOV5-5.x 源码讲解
ResNet详解——通俗易懂版
YOLOv5中的SPP/SPPF结构详解