Pytorch常用的函数(三)深度学习中常见的卷积操作
1、标准卷积(Standard Convolution)
1.1 标准卷积的理解
我们直接来看二维卷积,这在实际应用中是最常见的。
上图中Conv 2D其实就是卷积核,也叫做滤波器。滤波器的值决定了输出的情况,模型训练的过程就是调整这些值,使网络的输出更加准确。我们看看卷积算法如何将滤波器和输入结合在一起。
从概念上将,我们每次处理一个滤波器,要计算一个输出值,我们要看一下位于滤波器窗口下的输入区域。
在这个例子中,我们看一个3x3的像素区域,用这个区域和滤波器来计算输出。可以把滤波器看成包含某种模式,而输出值就是输入与这种模式的匹配程度。在网络中,比如一个浅层可能在寻找某种颜色,或者一个边缘;而深层可能在寻找一只狗。
计算完这个区域的值后,我们就移动到下一个区域,再次进行同样的计算来得到下一个输出值。
我们持续这个过程,计算出这一行中的其他输出。然后,我们在输入和输出中都换到新的一行,然后对这新的一行重复我们之前的操作。
我们按行地继续这个过程,直到整个输入的空间范围都被覆盖到。
换到下一个滤波器。用这个新的滤波器重复整个过程,形成输出的下一个特性。我们对每个滤波器都这样做,以形成输出的所有特性。
1.2 Pytorch中的API
torch.nn.Conv2d(
in_channels, # 输入通道数,即卷积核通道数
out_channels, # 输出通道数,即卷积核个数
kernel_size, # 核大小
stride=1, # 步幅
padding=0, # 填充
dilation=1, # 控制kernel点之间的空间距离
groups=1, # 分组卷积
bias=True,
padding_mode='zeros', # 图像四周默认填充值为0
device=None,
dtype=None
)
-
in_channels :代表输入特征矩阵的深度即channel,比如输入一张RGB彩色图像,那in_channels=3
-
out_channels:代表卷积核的个数,使用n个卷积核输出的特征矩阵深度,即channel就是n
-
kernel size:代表卷积核的尺寸,输入可以是int类型,如3代表卷积核的height=width=3,也可以是tuple类型如(3,5)代表卷积核的height=3,width=5
-
stride:代表卷积核的步距默认为1,和kernel size一样输入可以是int类型,也可以是tuple类型
-
padding:代表在输入特征矩阵四周补零的情况默认为0,同样输入可以为int型如1代表上下方向各补一行0元素、左右方向各补一列0像素(即补一圈0) ,如果输入为tuple型如(2,1)代表在上方补两行,下方补两行,左边补一列,右边补一列。
-
bias参数表示是否使用偏置 (默认使用)
-
dilation、groups是高阶用法
-
CNN的卷积核通道数 = 卷积输入层的通道数
-
CNN的卷积输出层通道数(深度) = 卷积核的个数
标准卷积的参数及计算量的计算
经卷积后的矩阵尺寸大小计算公式为 :
N = (W - K + 2P) / S + 1
例如:输入的矩阵 H=W=5,卷积核的K=2,S=2,Padding=1。
N = (5 - 2 + 2✖1) / 2 + 1 = 3.5
此时在Pytorch中是如何处理呢?
结论: 在卷积过程中会直接将最后一行以及最后一列给忽略掉,以保证N为整数
,此时N= (5 - 2 + 2 * 1 - 1) / 2 + 1 = 3 [即向下取整
]
注意:卷积核中的in_channels与需要进行卷积操作的数据x的channels一致
1.3 案例
举个例子:
输入一个12×12×3的一个输入特征图,经过一个5×5×3的卷积核卷积,得到一个8×8×1的输出特征图。如果此时我们有256个卷积核,我们将会得到一个8×8×256的输出特征图。
import torch
import torch.nn as nn
x = torch.rand(size=(1, 3, 12, 12))
model = nn.Sequential(
nn.Conv2d(
in_channels=3, # 卷积核中的in_channels与需要进行卷积操作的数据x的channels一致
out_channels=1, # 输出通道数,即卷积核个数
kernel_size=(5,5)
)
)
# torch.Size([1, 1, 8, 8])
# N = (W - K + 2P) / S + 1 = (12 - 5 + 2 * 0 ) / 1 + 1 = 8
print(model(x).shape)
model = nn.Sequential(
nn.Conv2d(
in_channels=3, # 输入通道数,即卷积核通道数
out_channels=256, # 输出通道数,即卷积核个数
kernel_size=(5,5) # 卷积核大小
)
)
# torch.Size([1, 256, 8, 8])
# N = (W - K + 2P) / S + 1 = (12 - 5 + 2 * 0 ) / 1 + 1 = 8
print(model(x).shape)
2、分组卷积(Group Convolution)
2.1 分组卷积的理解
对于需要区分各种视觉场景的大型的深度网络,我们需要大量的特征,尤其是在更深的层次,这就暴漏了卷积的性能扩展问题。
如下图,在更深的层次,每层的输入和输出特征数量都在增加。
如下图,增加输入特征的channels,会让滤波器更深(数量更多),增加输出特征的channels,就会有更多的滤波器,因此特征数量的倍增会让计算量增加4倍。
原来
增加输入特征的channels、增加输出特征的channels
想一想,每个滤波器真的需要查看输入的每个特征吗?肯定不是
因此,我们可以把输入特征分为两组,每个滤波器只需要查看其中一组就行。滤波器的前半部分会查看第一组输入,后半部分会查看另一组。
我们开始从第一组输入特征出发,使用对应的滤波器。注意每个滤波器的深度只和组的深度相同,而不是和整个输入的深度相同。这就是我们想要的性能提升。
当我们用完了一半的滤波器,就转向下一组特征,继续使用剩下的滤波器。这就和把输入和滤波器分开,执行单独的卷积,然后把结果拼接起来没有什么区别【设置groups=2】。
2.2 Pytorch中的API
对输入feature map进行分组,然后每组分别卷积
。这种分组只是在深度上【channels】进行划分,即某几个通道编为一组,这个具体的数量由 (C1/g) 决定。例如,输入的feature map的通道数C1=20,我们分为g=5组,那么每一组有4个卷积核。
torch.nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1, # 分组卷积,默认为1组
bias=True,
padding_mode='zeros' # 图像四周默认填充值为0
)
2.3 案例
x = torch.rand(size=(1, 256, 12, 12))
model = nn.Sequential(
nn.Conv2d(
in_channels=256,
out_channels=32, # 输出通道数,即卷积核的个数为256/8=32
kernel_size=(3,3),
padding=1,
groups=8 # 分为8组
)
)
# torch.Size([1, 32, 12, 12])
print(model(x).shape)
3、逐点卷积(PW Convolution)
3.1 逐点卷积的理解
Pointwise Convolution
的运算与常规卷积运算非常相似,它的券积核的尺寸为1X1XM
,M为上一层的通道数。
所以这里的卷积运算会将上一步的map在深度方向上进行加权组合,目的是: 生成新的Feature map
。
3.2 Pytorch中的API及案例
torch.nn.Conv2d(
in_channels,
out_channels,
kernel_size, # 逐点卷积,将卷积核的大小设置为1
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros'
)
假设我们得到了8×8×3的特征图,我们用256个1×1×3的卷积核对输入特征图进行卷积操作,输出的特征图变为8×8×256了
x = torch.rand(size=(1, 3, 8, 8))
model = nn.Sequential(
nn.Conv2d(
in_channels=3,
out_channels=256, # 输出通道数,即卷积核的个数,256个1×1×3的卷积核
kernel_size=(1,1) # 逐点卷积,卷积核的大小为1
)
)
# torch.Size([1, 256, 8, 8])
print(model(x).shape)
4、深度卷积(DW Convolution)
4.1 深度卷积的理解
想一想,如果我们把所有的组都分出来,会有什么问题【这就是深度卷积】。
存在的问题就是:通道数太少,特征图的维度太少,不能获取到足够的有效信息,可以通过结合逐点卷积进行解决,即【深度可分离卷积】。
深度卷积(逐通道卷积)参数量的计算
4.2 Pytorch中的API及案例
groups就是实现深度卷积的关键,默认为1,意思是将输入分为一组,此时是常规卷积
当将其设为in channels时,意思是将输入的每一个通道作为一组,然后分别对其卷积。
torch.nn.Conv2d(
in_channels,
out_channels=in_channels, # 深度卷积,out_channels=in_channels
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=in_channels, # 深度卷积,将输入的每一个通道作为一组,groups=in_channels
bias=True,
padding_mode='zeros'
)
与标准卷积网络不一样的是,我们将卷积核拆分成为但单通道形式,在不改变输入特征图像的深度的情况下,对每一通道进行卷积操作,这样就得到了和输入特征图通道数一致的输出特征图。如上图:输入12×12×3的特征图,经过5×5×1×3的深度卷积之后,得到了8×8×3的输出特征图。输入个输出的维度是不变的3。
x = torch.rand(size=(1, 3, 12, 12))
model = nn.Sequential(
nn.Conv2d(
in_channels=3,
out_channels=3, # out_channels=in_channels
kernel_size=(5,5),
groups=3 # groups=in_channels
)
)
# torch.Size([1, 3, 8, 8])
print(model(x).shape)
5、深度可分离卷积(PW+DW)
5.1 深度分离卷积的理解
在深度卷积中,我们将所有的组都进行了分离。但是,此时我们发现:第一个输出特征只依赖于第一个输入特征。
这个模式会在网络的更深层次中持续。这样我们永远都不会得到像只有一组那样的全部表达能力。
一个滤波器从原始图中得到第一个输出特征,此时表达能力强
如何解决这个问题呢?我们可以在每个深度卷积后,加上1个标准的1✖1的卷积【逐点卷积】,而不是堆叠深度卷积。
【逐点卷积】在空间上只有一个像素,同时接收所有输入特征。
这与深度卷积完美地互补,深度卷积在空间上有3x3的接受区域,但只有一个特征。
当我们把它们结合起来,两层的输出都有3x3的空间接受区域和所有原始特征。这完美地匹了有一组的3x3券积的接收区域。这就是【深度可分离卷积】。你可能已经注意到了,点对点卷积让我们原来特征数量翻倍导致计算量增4倍的问题又回来了,但我们相对标准卷积仍然领先。如果你看看3x3深度可分离卷积执行的总计算量,它只是标准3x3卷积的计算量的大约11%,换句话说,它快了9倍。严格地说,
加速取决干特征数量,但随着特征数量的增加,加速越来越接近理想的9倍速度。
5.2 深度分离卷积的案例
深度可分离卷积就是将普通卷积拆分成为一个深度卷积和一个逐点卷积。
输入一个12×12×3的一个输入特征图,经过5×5×3的卷积核卷积得到一个8×8×1的输出特征图。如果此时我们有256个特征图,我们将会得到一个8×8×256的输出特征图。
标准卷积
# 标准卷积
x = torch.rand(size=(1, 3, 12, 12))
model = nn.Sequential(
nn.Conv2d(
in_channels=3,
out_channels=256,
kernel_size=(5,5)
)
)
# torch.Size([1, 256, 8, 8])
print(model(x).shape)
深度可分离卷积
# 深度可分离卷积
x = torch.rand(size=(1, 3, 12, 12))
model = nn.Sequential(
# 深度卷积
nn.Conv2d(
in_channels=3,
out_channels=3, # out_channels=in_channels
kernel_size=(5,5),
groups=3 # groups=in_channels
),
# 逐点卷积
nn.Conv2d(
in_channels=3,
out_channels=256, # 输出通道数,即卷积核的个数,256个1×1×3的卷积核
kernel_size=(1,1) # 逐点卷积,卷积核的大小为1
)
)
# torch.Size([1, 256, 8, 8])
print(model(x).shape)
5.3 计算量的对比
标准卷积
深度可分离卷积
因此:
我们通常所使用的是3×3的卷积核,也就是会下降到原来的九分之一到八分之一。
5.4 代码实现
"""
深度分离卷积
"""
import torch
import torch.nn as nn
class Depth_Wise_Conv(nn.Module):
"""
深度可分离卷积 = 深度卷积 + 逐点卷积调整通道
"""
def __init__(self, in_channel, out_channel):
super(Depth_Wise_Conv, self).__init__()
# 深度卷积
self.conv_group = nn.Conv2d(in_channel, in_channel, kernel_size=3, stride=1, padding=1, groups=in_channel)
# 逐点卷积调整通道
self.conv_point = nn.Conv2d(in_channel, out_channel, kernel_size=1, stride=1, padding=1, groups=1)
# BN
self.bn = nn.BatchNorm2d(out_channel)
# activate
self.act = nn.ReLU()
def forward(self, inputs):
"""
前向传播
"""
x = self.conv_group(inputs)
x = self.conv_point(x)
x = self.bn(x)
x = self.act(x)
return x
if __name__ == '__main__':
# 均匀分布产生数据
x = torch.rand(1, 3, 16, 16)
model = Depth_Wise_Conv(3, 16)
model = model(x)
print(model)
参考博客:
图像部分 https://animatedai.github.io/
常用卷积总结 https://zhuanlan.zhihu.com/p/490761167
轻量级神经网络“巡礼”(二)—— MobileNet,从V1到V3 https://zhuanlan.zhihu.com/p/70703846