Yolov5学习笔记6——v6.0源码剖析——Backbone部分3
Yolov5学习笔记6——v6.0源码剖析——Backbone部分3
Backbone概览及参数
源码如下
1 | # Parameters |
yolov5s的backbone部分如上,其网络结构使用yaml文件配置,通过./models/yolo.py解析文件加了一个输入构成的网络模块。与v3和v4所使用的config设置的网络不同,yaml文件中的网络组件不需要进行叠加,只需要在配置文件中设置number即可。
Parameters
1 | # Parameters |
nc: 80
代表数据集中的类别数目,例如MNIST中含有0-9共10个类.depth_multiple: 0.33
用来控制模型的深度,仅在number≠1时启用。 如第一个C3层(c3具体是什么后续介绍)的参数设置为[-1, 3, C3, [128]],其中number=3,表示在v5s中含有1个C3(3*0.33);同理,v5l中的C3个数就是3(v5l的depth_multiple参数为1)。width_multiple: 0.50
用来控制模型的宽度,主要作用于args中的ch_out。如第一个Conv层,ch_out=64,那么在v5s实际运算过程中,会将卷积过程中的卷积核设为64x0.5,所以会输出32通道的特征图。
backbone
1 | backbone: |
- from:-n表示从前n层获得的输入,如-1表示从前一层获得输入
- number:表示网络模块的数目,如[-1, 2, C3, [128] ]表示含有3个C3模块
- model:表示网络模块的名称,具体细节可以在/models/common.py中查看,如Conv、C3、SPPF都是已经在common中定义好的模块。
- args:表示向不同模块内传递的参数,即[ch_out, kernel, stride, padding, groups],这里连ch_in都省去了,因为输入都是上层的输出(初始ch_in为3)。为了修改过于麻烦,这里输入的获取是从./models/yolo.py的
def parse_model(md, ch)
函数中解析得到的。
示例
1 | [-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 |
input为:3×640×640
[ch_out,kernel,stride,padding]=[64, 6, 2, 2]
故新的通道数为64×0.5=32
根据特征图计算公式:Feature_new=(Feature_old-kernel+2xpadding)/stride+1可得:
新的特征图尺寸为:Feature_new=(640-6+2x2)/2+1=320
1 | [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 |
input为:32×320×320
[ch_out, kernel, stride]=[128, 3, 2]
同理可得:新的通道数为64,新的特征图尺寸为160
Backbone组成
v6.0版本的Backbone去除了Focus模块(便于模型导出部署),Backbone主要由CBL、BottleneckCSP/C3以及SPP/SPPF等组成,具体如下图所示:
CBS模块
CBS模块实际上就是Conv+BatchNorm+SiLU。
CBS模块框架图
CBS源码
1
2
3
4
5
6
7
8
9
10
11
12
13class 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))
这里配合CBS模块的源码,分析Conv()函数里的一些参数,作为pytorch中卷积操作的复习。
从源码可以看出,Conv()包含7个参数,这些参数也是二维卷积Conv2d()中的重要参数。ch_in, ch_out, kernel, stride这4个参数前文已经提到过,是用来计算特征图尺寸的。主要分析后三个参数。
padding
从目前主流卷积操作来看,大多数的研究者不会通过kernel来改变特征图的尺寸,如googlenet中3x3的kernel设定了padding=1,所以当kernel≠1时需要对输入特征图进行填充。当指定p值时按照p值进行填充,当p值为默认时则通过autopad函数进行填充:
1 | def autopad(k, p=None): # kernel, padding |
这里作者考虑到对不同的卷积操作使用不同大小的卷积核时padding也需要做出改变,所以这里在为p赋值时会首先检查k是否为int,如果k为列表则对列表中的每个元素整除。
groups
表示分组卷积,示意图如下:
groups – 从输入通道到输出的阻塞连接数
- groups=1 时,所有输入都卷积到所有输出。
- groups=2 时,该操作等效于并排具有两个凸层,每个凸层看到一半的输入通道,并产生一半的输出通道,随后两者都串联起来。
- groups= in_channels 时,每个输入通道都用自己的一组滤波器进行卷积,其大小为:⌊(out_channels)/(in_channels)⌋。
act参数
决定是否对特征图进行激活操作,SILU表示使用Sigmoid进行激活。
关于激活函数的内容在Yolov5学习笔记3中有提及
补充:dilation参数
在Conv2d()中有一个重要的参数——空洞卷积dilation
1 | dilation: _size_2_t = 1, |
通俗解释就是控制kernel点(卷积核点)间距的参数,通过改变卷积核间距实现特征图及特征信息的保留,在语义分割任务中空洞卷积比较有效。
CSP/C3
注:CSP即backbone中的C3,因为在backbone中C3存在shortcut,而在neck中C3不使用shortcut,所以backbone中的C3层使用CSP1_x表示,neck中的C3使用CSP2_x表示。
CSP结构
源码:
1
2
3
4
5
6
7
8
9
10
11
12
13class 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) # 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)), dim=1))
从源码中可以看出:输入特征图一条分支先经过.cv1,再经过.m,得到子特征图1;另一分支经过.cv2后得到子特征图2。最后将子特征图1和子特征图2拼接后输入.cv3得到C3层的输出,如下图所示。
这里的CV就是前面的Conv2d+BN+SiLU
结构图
.m操作是用nn.Sequential将多个Bottleneck(也就是上图中的Res_u)串接到网络中,
Bottleneck
在Resnet出现之前,人们的普遍为网络越深获取信息也越多,模型泛化效果越好。然而随后大量的研究表明,网络深度到达一定的程度后,模型的准确率反而大大降低。这并不是过拟合造成的,而是由于反向传播过程中的梯度爆炸和梯度消失。也就是说,网络越深,模型越难优化,而不是学习不到更多的特征。
为了能让深层次的网络模型达到更好的训练效果,残差网络中提出的残差映射替换了以往的基础映射。对于输入x,期望输出H(x),网络利用恒等映射将x作为初始结果,将原来的映射关系变成F(x)+x。与其让多层卷积去近似估计H(x) ,不如近似估计H(x)-x,即近似估计残差F(x)。因此,ResNet相当于将学习目标改变为目标值H(x)和x的差值,后面的训练目标就是要将残差结果逼近于0。
残差函数有什么好处呢?
- 梯度弥散方面。加入ResNet中的shortcut结构之后,在反传时,每两个block之间不仅传递了梯度,还加上了求导之前的梯度,这相当于把每一个block中向前传递的梯度人为加大了,也就会减小梯度弥散的可能性。
- 特征冗余方面。正向卷积时,对每一层做卷积其实只提取了图像的一部分信息,这样一来,越到深层,原始图像信息的丢失越严重,而仅仅是对原始图像中的一小部分特征做提取。这显然会发生类似欠拟合的现象。加入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减少了参数量,优化了计算,保持了原有的精度。
- Bottleneck的源码如下:
1 | class Bottleneck(nn.Module): |
结构图如下:
可以看到,CSP中的Bottleneck同resnet模块中的类似,先是1x1的卷积层(CBS),然后再是3x3的卷积层,最后通过shortcut与初始输入相加。但是这里与resnet的不通点在于:CSP将输入维度减半运算后并未再使用1x1卷积核进行升维,而是将原始输入x也降了维,采取concat的方法进行张量的拼接,得到与原始输入相同维度的输出。其实这里能区分一点就够了:resnet中的shortcut通过add实现,是特征图对应位置相加而通道数不变;而CSP中的shortcut通过concat实现,是通道数的增加。二者虽然都是信息融合的主要方式,但是对张量的具体操作又不相同.
SSPF模块
- 源码
1 | class SPPF(nn.Module): |
- 结构图
SSPF模块将经过CBS的x、一次池化后的y1、两次池化后的y2和3次池化后的self.m(y2)先进行拼接,然后再CBS提取特征。 仔细观察不难发现,虽然SSPF对特征图进行了多次池化,但是特征图尺寸并未发生变化,通道数更不会变化,所以后续的4个输出能够在channel维度进行融合。这一模块的主要作用是对高层特征进行提取并融合,在融合的过程中作者多次运用最大池化,尽可能多的去提取高层次的语义特征。
Yolov5s的Backbone总览
运行yolo.py,结合上述的分析,对输出的结果应该很容易理解了。