【Yolov系列】Yolov5学习(一)补充2:Focus模块详解

一、相关知识

Focus模块是一种用于特征提取的卷积神经网络层,用于将输入特征图中的信息进行压缩和组合,从而提取出更高层次的特征表示,它被用作网络中的第一个卷积层,用于对输入特征图进行下采样,以减少计算量和参数量。

1、下采样相关知识

1. 下采样

下采样就是一种缩小图像的手法,用来降低特征的维度并保留有效信息,一定程度上避免过拟合,都是以牺牲部分信息为代价,换取数据量的减少。下采样就是池化操作。但是池化的目的不仅如此,还需要考虑旋转、平移、伸缩不变形等。采样有最大值采样,平均值采样,随机区域采样等,对应池化:比如最大值池化,平均值池化,随机池化等。

卷积神经网络中,卷积是最基本的模块,在W*H*C的图像操作中,卷积就是输入图像区域和滤波器进行内积求和的过程,卷积就是一种下采样的方式。具体操作如下:

2. 常见下采样操作

(1)采用stride为2的池化层

  • 最大值池化(Max-pooling):对邻域内特征点取最大,类似锐化,突出滑窗内的细节点(特殊点)。
  • 平均值池化(Average-pooling):对邻域内特征点只求平均,有点像平滑滤波,根据滑窗的尺寸控制下采样的力度,尺寸越大,它的采样率越高,但边缘信息损失越大。

(2)采用stride为2的卷积层

  • 下采样的过程是一个信息损失的过程,而池化层是不可学习的,用stride为2的可学习卷积层来代替pooling可以得到更好的效果,当然同时也增加了一定的计算量。

3. 下采样的作用

下采样实际上就是缩小图像,主要目的是为了使得图像符合显示区域的大小,生成对应图像的缩略图。比如说在CNN中的池化层或卷积层就是下采样。不过卷积过程导致的图像变小是为了提取特征,而池化下采样是为了降低特征的维度。

下采样层有两个作用:

  1. 减少计算量,防止过拟合;
  2. 增大感受野,使得后面的卷积核能够学到更加全局的信息。

2、上采样相关知识

1. 上采样

在卷积神经网络中,由于输入图像通过卷积神经网络(CNN)提取特征后,输出的尺寸往往会变小,而有时我们需要将图像恢复到原来的尺寸以便进行进一步的计算(如图像的语义分割),这个使图像由小分辨率映射到大分辨率的操作,叫做上采样。

2. 常见上采样操作

常见的上采样操作有反卷积(Deconvolution,也称转置卷积)、上池化(UnPooling)方法、双线性插值(各种插值算法)。具体如下:

  1. 插值。一般使用的是双线性插值,因为效果最好,虽然计算上比其他插值方式复杂,但是相对于卷积计算可以说不值一提,其他插值方式还有最近邻插值、三线性插值等;
  2. 转置卷积又或是说反卷积(Transpose Conv)。通过对输入feature map间隔填充0,再进行标准的卷积计算,可以使得输出feature map的尺寸比输入更大;
  3. 上池化(UpPooling)。 最大值向上池化(Max Unpooling) 、均值向上池化( Avg Unpooling),在对称的max pooling位置记录最大值的索引位置,然后在unpooling阶段时将对应的值放置到原先最大值位置,其余位置补0;

unpooling的操作与unsampling类似,区别是unpooling记录了原来pooling是取样的位置,在unpooling的时候将输入feature map中的值填充到原来记录的位置上,而其他位置则以0来进行填充。

3. 上采样作用

上采样实际上就是放大图像,指的是任何可以让图像变成更高分辨率的技术。

3、Focus前身:Yolov2中的PassThrough层

  • Yolov2中的passthrough层的本质是特征重排,将相邻的特征堆积在不同的通道中,这样可以将大尺度特征图下采样后与小尺度特征图进行融合,进而增加了小目标检测的精确度。

  • 一般而言,两个特征层要从通道上进行拼接,需要保持空间大小一致。比如(26,26,512)的特征层要和(13,13,1024)的特征层在通道维度上进行拼接,那么需要将空间维度的26x26下采样成13*13,或者13*13上采样成26*26。最简单的做法就是通过池化下采样即可,但是Yolov2为了保留feature map的更多细节,在空间维度上进行拆分(见图中红绿部分所示),得到4个13*13*512的特征层,然后将这4个特征层在通道维度上拼接(concat)成一个13*13*2048的特征层。最后就可以将(13,13,1024)和(13,13,2048)两个特征层在通道维度上进行拼接就得到(13,13,3072)。
  • 看操作与Focus好像是一样的,改了个名字?

二、Focus层

1、Focus层原理

  • Focus层在YOLOv5中是图片进入Backbone前,对图片进行切片操作,原理与Yolov2的passthrough层类似,采用切片操作把高分辨率的图片(特征图)拆分成多个低分辨率的图片或特征图,即隔列采样+拼接
  • 具体操作是在一张图片中每隔一个像素拿到一个值,类似于邻近下采样,这样就拿到了四张图片,四张图片互补,长得差不多,但是没有信息丢失,这样一来,将W、H信息就集中到了通道空间,输入通道扩充了4倍,即拼接起来的图片相对于原先的RGB三通道模式变成了12个通道,最后将得到的新图片再经过卷积操作,最终得到了没有信息丢失情况下的二倍下采样特征图。
  • 以YOLOv5s为例,原始的640 × 640 × 3的图像输入Focus结构,采用切片操作,先变成320 × 320 × 12的特征图,拼接(Concat)后,再经过一次卷积(CBL,后改为SiLU(CBS))操作,最终变成320 × 320 × 32的特征图。

切片操作如下:

2、代码分析

Focus层及相关代码如下:

def autopad(k, p=None):  # kernel, padding自动填充的设计,更加灵活多变
    # Pad to 'same'
    if p is None:
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  
# auto-pad自动填充,通过自动设置填充数p
        #如果k是整数,p为k与2整除后向下取整;如果k是列表等,p对应的是列表中每个元素整除2。
    return p
 
class Conv(nn.Module):
    # 这里对应结构图部分的CBL,CBL = conv+BN+Leaky ReLU,后来改成了SiLU(CBS)
    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) 
#将其变为均值为0,方差为1的正态分布,通道数为c2
        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
#其中nn.Identity()是网络中的占位符,并没有实际操作,在增减网络过程中,可以使得整个网络层数据不变,便于迁移权重数据;nn.SiLU()一种激活函数(S形加权线性单元)。
 
    def forward(self, x):#正态分布型的前向传播
        return self.act(self.bn(self.conv(x)))
 
    def forward_fuse(self, x):#普通前向传播
        return self.act(self.conv(x))
 
class Focus(nn.Module):
    # Focus wh information into c-space
    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 = Conv(c1 * 4, c2, k, s, p, g, act)
        # self.contract = Contract(gain=2)
 
    def forward(self, x):  # x(b,c,w,h) -> y(b,4c,w/2,h/2)
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1)) 
        #图片被分为4块。x[..., ::2, ::2]即行按2叠加取,列也是,对应上面原理图的“1”块块), x[..., 1::2, ::2]对应“3”块块,x[..., ::2, 1::2]指“2”块块,x[..., 1::2, 1::2]指“4”块块。都是每隔一个采样(采奇数列)。用cat连接这些采样图,生成通道数为12的特征图
        # return self.conv(self.contract(x))
  1. 先采取切片操作(x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2] ),把图片分成1,3,2,4共4块(如上面的原理图);
  2. 然后进行一个连接(concat);
  3. 最后再来一次卷积,这里的卷积是自定义卷积:先进行一次卷积,然后变化成正态分布,最后来个SiLU激活,即CBS=Conv+BN+SiLU。

3、Focus层变化

在Yolov5较新版本源码(如Yolov5-6.2、Yolov5-7.0)中,代码中存在Focus层但是在Yolov5s、Yolov5l、Yolov5m、Yolov5x中均没有使用,以Yolov5s网络结构为例:

1. 旧版Yolov5s网络结构:

  • Focus层是3*3*32的结构,输入通道为3通道,输出通道数为32。
  • 作用增大感受野的同时降低运算量。如果没有初始的切片操作,直接使用一个3*3的卷积,那么感受野相对会小了一半;如果直接使用一个6*6的卷积,同样是输入3通道,输出32通道,那么运算量会是前面的4倍。
  • Focus层的具体结构如下:

以分辨率为640*640的三通道图像为例:

  • Focus切片操作把紫色的归第1组,绿色的归第2组,蓝色的归第3组,红色的归第4组,每组仍然是3个通道,然后把4组排在一起(按第1个维度合并(从第0维度开始算),每组都是一个(b,3,320,320)的张量,b就是批大小),就得到了12个通道的输入,即(b,12,320,320);

  • 使用一个输入通道12,输出通道32的3*3卷积运算(按yolov5s算),输出分辨率还是320乘320。

2. Yolov5-7.0的Yolov5s网络结构:

Focus层原本处于整个网络结构的第一层,现在已经被蓝框中的6*6卷积层替换掉了。

注意:这里Yolov5s的第一个卷积层的输出通道数并不是64,在s、l、m、x的yaml文件中写的均为64,但是实际的输出通道数还要乘以上面的width_multiple:0.50,这个值在几个yaml中是不一样的,它控制了不同规格的模型的通道数。所以在yolov5s中第一个卷积层的输出通道数是64*0.5=32。

Yolov5-7.0中使用一个6*6且步长为2的卷积替换了Focus层,具体过程参考issue: https://github.com/ultralytics/yolov5/issues/4825

具体流程为:

在Yolov5目录下新建一个py文件,命名为Focus_test.py(自己随意命名,无所谓),直接运行Focus_test.py文件(注意,这里的输入通道数是3,输出通道数写的是64)

  1. 使用代码实例化一个Focus层。
  2. 将Focus层的卷积层参数拷贝给一个6乘6步长2的卷积层
  3. 给Focus层和6*6步长为2的卷积层以相同的输入,比较它们的输出。使用用torch.allclose函数,精度设为10的负6次方,因为浮点数计算顺序不同,可能结果是有误差的,比较一下两者的性能
import torch
from models.common import Focus, Conv
from utils.torch_utils import profile

# 实例化一个Focus层。输入通道为3,输出通道为64,卷积核大小为3*3
focus = Focus(3, 64, k=3).eval()
# 实例化一个卷积层。输入通道为3,输出通道为64,卷积核大小为6*6,步长为2,padding为2
conv = Conv(3, 64, k=6, s=2, p=2).eval()

# Express focus layer as conv layer
# 将Focus层参数传递给卷积层
conv.bn = focus.conv.bn
conv.conv.weight.data[:, :, ::2, ::2] = focus.conv.conv.weight.data[:, :3]
conv.conv.weight.data[:, :, 1::2, ::2] = focus.conv.conv.weight.data[:, 3:6]
conv.conv.weight.data[:, :, ::2, 1::2] = focus.conv.conv.weight.data[:, 6:9]
conv.conv.weight.data[:, :, 1::2, 1::2] = focus.conv.conv.weight.data[:, 9:12]

# Compare
# 随机一个张量,批大小为16
x = torch.randn(16, 3, 640, 640)
with torch.no_grad():
    # Results are not perfectly identical, errors up to about 1e-7 occur (probably numerical)
    assert torch.allclose(focus(x), conv(x), atol=1e-6)

# Profile
# device选择所使用的设备,GPU还是CPU,我电脑只能用cpu测试
results = profile(input=torch.randn(16, 3, 640, 640), ops=[focus, conv, focus, conv], n=10, device='cpu')

相同的输入,输出结果肯定相同,但对于不同的显卡性能方面可能存在。比如Yolov5作者在V100上测试时分别统计了前向传播和反向传播的时间,4行记录分别是focus,conv,focus,conv。在batch-size为16和1的情况下均进行了测试。

下面是我在自己笔记本CPU上进行的测试:

(有条件还是用GPU测试吧,配置不行,不具有参考意义) 

结论:

  1. 计算结果来看,Focus层和6*6步长为2的卷积层等价;
  2. 从性能上来看,性能较好的设备上Conv层比Focus层快,尤其是反向传播上;较老的设备上,Conv层比Focus层慢一些。

3. 替换详解

3.1 前向传播是否等价

分析Focus层和Conv层等价原因,Focus参数拷贝给Conv层方法。

(1)打印Focus层和Conv层的参数尺寸

# Express focus layer as conv layer
# 将Focus层参数传递给卷积层
conv.bn = focus.conv.bn
conv.conv.weight.data[:, :, ::2, ::2] = focus.conv.conv.weight.data[:, :3]
conv.conv.weight.data[:, :, 1::2, ::2] = focus.conv.conv.weight.data[:, 3:6]
conv.conv.weight.data[:, :, ::2, 1::2] = focus.conv.conv.weight.data[:, 6:9]
conv.conv.weight.data[:, :, 1::2, 1::2] = focus.conv.conv.weight.data[:, 9:12]
print(focus.conv.conv.weight.data.shape)
print(conv.conv.weight.data.shape)

  • Focus层输入通道为12,输出通道为64,卷积核大小为3*3。由输入通道为12,那么需要有12个3*3的卷积核,输出通道为64,那么要有64*12个3*3的卷积核,所以卷积层参数的形状为(64,12,3,3)。
  • Conv层输入通道为3,输出通道为64,卷积核大小为6*6。由输入通道为3,那么需要有3个6*6的卷积核,输出通道为64,那么要有64*3个6*6的卷积核,所以卷积层参数的形状为(64,3,6,6)。
  • 两者的输出通道相同,参数数量相同,区别在于输入通道和卷积核大小。因此只需要关心64组卷积核中的一组如何传递即可,对于Focus层,一组就是12个3*3的卷积核;对于Conv层来说,一组就是3个6*6的卷积核。

(2)卷积过程对比

1. Focus的卷积层输入是12*320*320的图像,如图:

  • 12个3*3的卷积核如下,这12个卷积核与输入图像的12个通道是对应的,各卷各的,相互独立。即既紫色只跟紫色、绿色只跟绿色、蓝色只跟蓝色、红色只跟红色。(卷积核也按4组分了颜色,方便后续指明对应关系)

  • 将Focus层的12个卷积核合并成3个,将12个通道的输入图还原成3通道的输入图

2. Conv中6*6且步长为2的卷积输入是3*640*640的图像,如图:

  • 3个6*6的卷积核如下,步长为2,且3个卷积核跟图像中3个对应的通道进行卷积,各卷各的,相互独立。其次,由于步长为2,(对应通道)卷积核中紫色部分只会去跟图像中的紫色部分进行卷积,绝对不会跟绿色、蓝色、红色卷积。同理绿色只跟绿色、蓝色只跟蓝色、红色只跟红色。

3. 若1和2均只按照“紫色只跟紫色、绿色只跟绿色、蓝色只跟蓝色、红色只跟红色”的原则来进行卷积的话,那么计算过程就是完全等价的

  • 所以两者的卷积过程是等价的,这个卷积参数的拷贝代码跟下图右边的图示是对应的。
conv.bn = focus.conv.bn
conv.conv.weight.data[:, :, ::2, ::2] = focus.conv.conv.weight.data[:, :3]
conv.conv.weight.data[:, :, 1::2, ::2] = focus.conv.conv.weight.data[:, 3:6]
conv.conv.weight.data[:, :, ::2, 1::2] = focus.conv.conv.weight.data[:, 6:9]
conv.conv.weight.data[:, :, 1::2, 1::2] = focus.conv.conv.weight.data[:, 9:12]

3.2 反向传播是否等价

答案是等价的。因为卷积层的梯度就是由它的输入决定的,卷积就是乘法和加法运算,前向传播运算过程完全等价,那么梯度当然是一样的。用代码验证一下:

import torch
from models.common import Focus, Conv
from copy import deepcopy
 
focus = Focus(3, 64, k=3).train()
focus2 = deepcopy(focus).train()
conv = Conv(3, 64, k=6, s=2, p=2).train()
 
# Express focus layer as conv layer
conv.bn = deepcopy(focus.conv.bn)
conv.conv.weight.data[:, :, ::2, ::2] = deepcopy(focus.conv.conv.weight.data[:, :3])
conv.conv.weight.data[:, :, 1::2, ::2] = deepcopy(focus.conv.conv.weight.data[:, 3:6])
conv.conv.weight.data[:, :, ::2, 1::2] = deepcopy(focus.conv.conv.weight.data[:, 6:9])
conv.conv.weight.data[:, :, 1::2, 1::2] = deepcopy(focus.conv.conv.weight.data[:, 9:12])
 
# Compare
x = torch.randn(16, 3, 640, 640, requires_grad=False)
 
with torch.no_grad():
    # Results are not perfectly identical, errors up to about 1e-7 occur (probably numerical)
    assert torch.allclose(focus(x), conv(x), atol=1e-6)
 
label = torch.randn(16, 64, 320, 320, requires_grad=False)
 
optimizer1 = torch.optim.SGD(focus.parameters(), lr=0.001, momentum=0.9, nesterov=True)
# optimizer1 = smart_optimizer(focus, 'SGD')
optimizer1.zero_grad()
loss1 = torch.mean(focus(x) - label)  # 要想计算loss,得有个标量输出,所以这里mean了一下。注意,你要是用sum,就会导致误差变大,最终梯度就不等了哦
loss1.backward()
optimizer1.step()
 
 
optimizer2 = torch.optim.SGD(conv.parameters(), lr=0.001, momentum=0.9, nesterov=True)
optimizer2.zero_grad()
loss2 = torch.mean(conv(x) - label)  # 同上
loss2.backward()
optimizer2.step()
 
print(f'loss1: {loss1.item():.10f}, loss2: {loss2.item():.10f}')
 
equivalent_grad = torch.zeros(64, 3, 6, 6, dtype=torch.float32)
equivalent_grad[:, :, ::2, ::2] = deepcopy(focus.conv.conv.weight.grad[:, :3])
equivalent_grad[:, :, 1::2, ::2] = deepcopy(focus.conv.conv.weight.grad[:, 3:6])
equivalent_grad[:, :, ::2, 1::2] = deepcopy(focus.conv.conv.weight.grad[:, 6:9])
equivalent_grad[:, :, 1::2, 1::2] = deepcopy(focus.conv.conv.weight.grad[:, 9:12])
 
assert torch.allclose(equivalent_grad, conv.conv.weight.grad, atol=1e-6)
print('梯度等价')

三、参考文章

Yolov5中的Focus层

(五)目标检测yolov2

上采样、下采样到底是什么?

yolov5 Focus模块详解

yolov5源码解析(0)--focus层哪去了?

  • 21
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值