赞
踩
Focus模块是一种用于特征提取的卷积神经网络层,用于将输入特征图中的信息进行压缩和组合,从而提取出更高层次的特征表示,它被用作网络中的第一个卷积层,用于对输入特征图进行下采样,以减少计算量和参数量。
下采样就是一种缩小图像的手法,用来降低特征的维度并保留有效信息,一定程度上避免过拟合,都是以牺牲部分信息为代价,换取数据量的减少。下采样就是池化操作。但是池化的目的不仅如此,还需要考虑旋转、平移、伸缩不变形等。采样有最大值采样,平均值采样,随机区域采样等,对应池化:比如最大值池化,平均值池化,随机池化等。
卷积神经网络中,卷积是最基本的模块,在W*H*C的图像操作中,卷积就是输入图像区域和滤波器进行内积求和的过程,卷积就是一种下采样的方式。具体操作如下:
(1)采用stride为2的池化层
(2)采用stride为2的卷积层
下采样实际上就是缩小图像,主要目的是为了使得图像符合显示区域的大小,生成对应图像的缩略图。比如说在CNN中的池化层或卷积层就是下采样。不过卷积过程导致的图像变小是为了提取特征,而池化下采样是为了降低特征的维度。
下采样层有两个作用:
在卷积神经网络中,由于输入图像通过卷积神经网络(CNN)提取特征后,输出的尺寸往往会变小,而有时我们需要将图像恢复到原来的尺寸以便进行进一步的计算(如图像的语义分割),这个使图像由小分辨率映射到大分辨率的操作,叫做上采样。
常见的上采样操作有反卷积(Deconvolution,也称转置卷积)、上池化(UnPooling)方法、双线性插值(各种插值算法)。具体如下:
unpooling的操作与unsampling类似,区别是unpooling记录了原来pooling是取样的位置,在unpooling的时候将输入feature map中的值填充到原来记录的位置上,而其他位置则以0来进行填充。
上采样实际上就是放大图像,指的是任何可以让图像变成更高分辨率的技术。
切片操作如下:
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))
在Yolov5较新版本源码(如Yolov5-6.2、Yolov5-7.0)中,代码中存在Focus层但是在Yolov5s、Yolov5l、Yolov5m、Yolov5x中均没有使用,以Yolov5s网络结构为例:
以分辨率为640*640的三通道图像为例:
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)
- 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测试吧,配置不行,不具有参考意义)
结论:
分析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)
(2)卷积过程对比
1. Focus的卷积层输入是12*320*320的图像,如图:
2. Conv中6*6且步长为2的卷积输入是3*640*640的图像,如图:
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]
答案是等价的。因为卷积层的梯度就是由它的输入决定的,卷积就是乘法和加法运算,前向传播运算过程完全等价,那么梯度当然是一样的。用代码验证一下:
- 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('梯度等价')
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。