MindSpore易点通·精讲系列–网络构建之Conv2d算子
本文开发环境
本文内容摘要
老传统,先看官方文档。
参数解读:
NCHW
或NHWC
普通卷积,又可以称为常规卷积。由于是在深度学习相关课程中最先接触到的CNN
卷积方式,本文不再对其原理展开介绍。下面通过一个实例来介绍MindSpore
中的用法。
例如:
对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。
假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,padding
方式为same
,即输入和输出的高和宽一致。
如何用MindSpore
来定义这样的普通卷积呢,示例代码如下:
这里的批数为2
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def common_conv_demo():
img_data = np.random.rand(2, 3, 8, 8)
ms_in = Tensor(img_data, dtype=mstype.float32)
conv_op = nn.Conv2d(3, 4, 3, 1)
ms_out = conv_op(ms_in)
print("in shape: {}".format(ms_in.shape), flush=True)
print("out shape: {}".format(ms_out.shape), flush=True)
def main():
common_conv_demo()
if __name__ == "__main__":
main()
代码解读:
核心代码为
nn.Conv2d(3, 4, 3, 1)
参数数字3表示输入通道
参数数字4表示输出通道。
参数数字3表示卷积核大小,这里因为高&宽的卷积值相等,所以使用整型表示。
参数数字1表示卷积移动步长。
nn.Conv2d
默认卷积方式为same
,故没有在这里的参数中体现。
将上述代码保存到common_conv2d.py
文件,使用如下命令运行:
python3 common_conv2d.py
输出内容为:
可以看到输出的通道为4,因为填充方式为
same
,输出的高度和宽度与输入数据相同。
in shape: (2, 3, 8, 8)
out shape: (2, 4, 8, 8)
深度卷积(Depthwise Convolution)的一个卷积核负责一个通道,一个通道只被一个卷积核卷积,可以看出其卷积方式与普通卷积明显不同。深度卷积一般与逐点卷积(Pointwise Convolution)结合,组成深度可分离卷积(Depthwise Separable Convolution),当然也可以单独使用,比如,经典的MobileNet网络就用到了深度可分离卷积。
那么在MindSpore中如何实现深度卷积呢,我们先从文档说起。
- group (int) – Splits filter into groups, in_channels and out_channels must be divisible by group. If the group is equal to in_channels and out_channels, this 2D convolution layer also can be called 2D depthwise convolution layer. Default: 1.
- group (int) – 将过滤器拆分为组, in_channels 和 out_channels 必须可被 group 整除。如果组数等于 in_channels 和 out_channels ,这个二维卷积层也被称为二维深度卷积层。默认值:1.
从文档可以看出,当in_channels
、out_channels
、group
三个参数的值相等时,可以认为即为2D的深度卷积。下面通过一个案例来进一步讲解。
例如:
对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。
假设我们对上述图片进行深度卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为3(与输入通道一致),padding
方式为same
,即输入和输出的高和宽一致。
MindSpore示例代码如下:
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def depthwise_conv_demo():
img_data = np.random.rand(2, 3, 8, 8)
ms_in = Tensor(img_data, dtype=mstype.float32)
conv_op = nn.Conv2d(3, 3, 3, 1, group=3)
ms_out = conv_op(ms_in)
print("in shape: {}".format(ms_in.shape), flush=True)
print("out shape: {}".format(ms_out.shape), flush=True)
def main():
depthwise_conv_demo()
if __name__ == "__main__":
main()
代码解读:
核心代码为
nn.Conv2d(3, 3, 3, 1, group=3)
参数数字3表示输入通道。
参数数字4表示输出通道。
参数数字3表示卷积核大小,这里因为高&宽的卷积值相等,所以使用整型表示。
参数数字1表示卷积移动步长。
nn.Conv2d
默认卷积方式为same
,故没有在这里的参数中体现。参数
gropu=3
与前面的输入通道3和输出通道3一致,是这里实现深度卷积的关键参数。
将上述代码保存到depthwise_conv2d.py
文件,使用如下命令运行:
python3 depthwise_conv2d.py
输出内容为:
可以看到输出的通道为3(与输入数据通道数一致),因为填充方式为
same
,输出的高度和宽度与输入数据相同。
in shape: (2, 3, 8, 8)
out shape: (2, 3, 8, 8)
一点补充
细心的读者可能会问,逐点卷积如何实现呢?这里逐点卷积可以看成普通卷积的特例,即卷积核为1×1的普通卷积(其他参数视具体而定),再参考第2节的内容,就可以很容易的实现出来了。
空洞卷积(Dilated Convolution),又称扩张卷积、膨胀卷积,是在标准的卷积核中注入空洞,以此来增加模型的感受野(reception field)。相比原来的正常卷积操作,扩张卷积多了一个参数: dilation rate,指的是卷积核的点的间隔数量,比如常规的卷积操作dilatation rate为1。
(a)图对应3x3的1-dilated conv,和普通的卷积操作一样,(b)图对应3x3的2-dilated conv,实际的卷积kernel size还是3x3,但是空洞为1,也就是对于一个7x7的图像patch,只有9个红色的点和3x3的kernel发生卷积操作,其余的点略过。也可以理解为kernel的size为7x7,但是只有图中的9个点的权重不为0,其余都为0。 可以看到虽然kernel size只有3x3,但是这个卷积的感受野已经增大到了7x7(如果考虑到这个2-dilated conv的前一层是一个1-dilated conv的话,那么每个红点就是1-dilated的卷积输出,所以感受野为3x3,所以1-dilated和2-dilated合起来就能达到7x7的conv),©图是4-dilated conv操作,同理跟在两个1-dilated和2-dilated conv的后面,能达到15x15的感受野。对比传统的conv操作,3层3x3的卷积加起来,stride为1的话,只能达到(kernel-1)*layer+1=7的感受野,也就是和层数layer成线性关系,而dilated conv的感受野是指数级的增长。
空洞卷积的好处是不做pooling损失信息的情况下,加大了感受野,让每个卷积输出都包含较大范围的信息。在图像需要全局信息或者语音文本需要较长的sequence信息依赖的问题中,都能很好的应用Dilated Convolution,比如语音合成WaveNet、机器翻译ByteNet中。
一点补充
kernel_size
为3×3,dilation rate=2
的情况,其实际kernel_size
大小为7×7。MindSpore(Pytorch)
框架内,其计算公式为dilation∗(kernelsize−1)+1
,即实际kernel_size
大小为5×5。下面通过一段代码示例,来看看MindSpore中的具体实现。代码如下:
为了方便观察输出数据的高度和宽度,这里将
padding
方式设置为valid
。
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def dilated_conv_demo():
img_data = np.random.rand(2, 3, 8, 8)
ms_in = Tensor(img_data, dtype=mstype.float32)
common_conv_op_0 = nn.Conv2d(3, 4, 3, 1, pad_mode="valid")
common_conv_op_1 = nn.Conv2d(3, 4, 5, 1, pad_mode="valid")
dilated_conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="valid", dilation=2)
common_out_0 = common_conv_op_0(ms_in)
common_out_1 = common_conv_op_1(ms_in)
dilated_out = dilated_conv_op(ms_in)
print("common out 0 shape: {}".format(common_out_0.shape), flush=True)
print("common out 1 shape: {}".format(common_out_1.shape), flush=True)
print("dilated out shape: {}".format(dilated_out.shape), flush=True)
def main():
dilated_conv_demo()
if __name__ == "__main__":
main()
代码解读:
common_conv_op_0
和common_conv_op_1
皆为普通卷积,其卷积核大小分别为3×3和5×5。dilated_conv_op
为空洞卷积,卷积核为3×3,但dilation设置为2。- 根据公式
dilation∗(kernelsize−1)+1
可知,dilated_conv_op
就卷积核大小来看,效果类似于5×5普通卷积。验证参见输出数据的数据维度。
将上述代码保存到dilated_conv2d.py
文件,使用如下命令运行:
python3 dilated_conv2d.py
输出内容为:
可以看出
common out 1 shape
和dilated out shape
相等,验证了代码解读的第三条。
common out 0 shape: (2, 4, 6, 6)
common out 1 shape: (2, 4, 4, 4)
dilated out shape: (2, 4, 4, 4)
特别注意:
NHWC
数据格式目前只支持在GPU
硬件下使用。
在Conv2d
中,输入数据的数据格式可选值有NHWC
和NCHW
,默认值为NCHW
。其中各个字母的含义如下:
那么两种数据格式又有什么区别呢,先从一段错误代码讲起:
在下面的代码中,我们创建数据img_data,并且将通道放置到了最后一个维度,即数据格式为
NHWC
。但是Conv2d
中默认的数据格式为NCHW
,那么运行起来如何呢?
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def data_format_demo():
img_data = np.random.rand(2, 8, 8, 3)
ms_in = Tensor(img_data, dtype=mstype.float32)
common_conv_op = nn.Conv2d(3, 4, 3, 1)
ms_out = common_conv_op(ms_in)
print("common out shape: {}".format(ms_out.shape), flush=True)
def main():
data_format_demo()
if __name__ == "__main__":
main()
将上述代码保存到format_conv2d.py
文件,使用如下命令运行:
python3 format_conv2d.py
会输出报错信息,报错内容如下:
错误信息中并没有显式提示是数据格式问题,所以对于新手来说这个问题可能具有迷惑性。
WARNING: Logging before InitGoogleLogging() is written to STDERR
[CRITICAL] CORE(29160,0x102270580,Python):2022-07-31-16:35:26.406.143 [build/mindspore/merge/mindspore/core/ops_merge.cc:6753] Conv2dInferShape] For 'Conv2D', 'C_in' of input 'x' shape divide by parameter 'group' should be equal to 'C_in' of input 'weight' shape: 3, but got 'C_in' of input 'x' shape: 8, and 'group': 1
[WARNING] UTILS(29160,0x102270580,Python):2022-07-31-16:35:26.409.046 [mindspore/ccsrc/utils/comm_manager.cc:78] GetInstance] CommManager instance for CPU not found, return default instance.
Traceback (most recent call last):
File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 74, in <module>
main()
File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 70, in main
data_format_demo()
File "/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/01_conv2d.py", line 64, in data_format_demo
ms_out = common_conv_op(ms_in)
File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 586, in __call__
out = self.compile_and_run(*args)
File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 964, in compile_and_run
self.compile(*inputs)
File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/cell.py", line 937, in compile
_cell_graph_executor.compile(self, *inputs, phase=self.phase, auto_parallel_mode=self._auto_parallel_mode)
File "/Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/common/api.py", line 1006, in compile
result = self._graph_executor.compile(obj, args_list, phase, self._use_vm_mode())
RuntimeError: build/mindspore/merge/mindspore/core/ops_merge.cc:6753 Conv2dInferShape] For 'Conv2D', 'C_in' of input 'x' shape divide by parameter 'group' should be equal to 'C_in' of input 'weight' shape: 3, but got 'C_in' of input 'x' shape: 8, and 'group': 1
The function call stack (See file '/Users/kaierlong/Codes/OpenI/kaierlong/Dive_Into_MindSpore/code/chapter_02/rank_0/om/analyze_fail.dat' for more details):
# 0 In file /Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/layer/conv.py(286)
if self.has_bias:
# 1 In file /Users/kaierlong/Pyenvs/env_mix_dl/lib/python3.9/site-packages/mindspore/nn/layer/conv.py(285)
output = self.conv2d(x, self.weight)
^
那么如何才能正常运行呢,有两种做法,一种是修改输入数据的数据格式;一种是对算子中的data_format
参数进行调整。展开来说,可以有三种方案。
方案1:
NCHW
。方案2:
ms_in
数据做一次转置操作(Transpose),将数据调整为NCHW
。方案3:
data_format
设置为NHWC
。特别注意,这一设置只在GPU
下可用,CPU
和Ascend
下目前不可用。在Conv2d
中,填充模式(pad_mode
)可选值为same
、valid
、pad
,默认值:same
。下面来介绍这三种填充方式。
对于same
填充方式,官方描述如下:
输出的高度和宽度分别与输入整除 stride 后的值相同。若设置该模式,
padding
的值必须为0。
具体示例代码参见第2小节。
特别注意
Pytorch
中Conv2d
的区别,在Pytorch
中,填充方式为same
时,只允许stride
为1,而MindSpore
可以允许大于1的整数值。stride
允许1之外的整数,所以same
模式下输出数据的高度和宽度未必和输入数据一致,这一点一定要谨记,至于输出数据的高度和宽度请参考第7小节。对于valid
填充方式,官方描述如下:
在不填充的前提下返回有效计算所得的输出。不满足计算的多余像素会被丢弃。如果设置此模式,则
padding
的值必须为0。
具体示例代码参见第4小节。
本节重点来讲解一下pad
填充方式,对于pad
填充方式,官方描述如下:
对输入进行填充。在输入的高度和宽度方向上填充
padding
大小的0。如果设置此模式,padding
必须大于或等于0。
与pad
填充方式配合使用的,还有padding
参数。下面来看一下官方对padding
参数的描述:
输入的高度和宽度方向上填充的数量。数据类型为int或包含4个整数的tuple。如果 padding 是一个整数,那么上、下、左、右的填充都等于 padding 。如果 padding 是一个有4个整数的tuple,那么上、下、左、右的填充分别等于 padding[0] 、 padding[1] 、 padding[2] 和 padding[3] 。值应该要大于等于0,默认值:0。
padding参数解读:
允许两种数据形式
一个整数 – 此时表示上下左右填充值皆为padding
tuple,且tuple内含四个整数 – 此时表示上、下、左、右的填充分别等于 padding[0] 、 padding[1] 、 padding[2] 和 padding[3]
这里的上、下、左、右表示的高和宽,通俗解释即为上高、下高、左宽、右宽。
下面通过两个示例来讲解两种数据形式。
例如,对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。
假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,要求输出数据的高度和宽度与输入数据一致。
简单分析:上面的案例要求第2节中的代码就可以实现,在第2节中采用的pad_mode
为same
,那么如果采用pad_mode
为pad
呢,代码如下:
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def pad_demo_01():
img_data = np.random.rand(2, 3, 8, 8)
ms_in = Tensor(img_data, dtype=mstype.float32)
conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=1)
ms_out = conv_op(ms_in)
print("in shape: {}".format(ms_in.shape), flush=True)
print("out shape: {}".format(ms_out.shape), flush=True)
def main():
pad_demo_01()
if __name__ == "__main__":
main()
代码解读:
- 在卷积核大小为3×3,卷积步长为1的情况下,要想保证输出数据的高宽值与输入数据一致,在
pad_mode
为pad
模式下,padding
的值应该设置1。- 这里计算
padding
数值的公式参加第7小节输出维度部分。
将上述代码保存到pad_conv2d_01.py
文件,使用如下命令运行:
python3 pad_conv2d_01.py
输出内容为:
in shape: (2, 3, 8, 8)
out shape: (2, 4, 8, 8)
"padding为四个整数tuple"是"padding为一个整数"的一般情况。下面我们通过一个示例进行讲解。
例如:输入数据仍然保持同6.3.1
中一致,但是这次我们输出数据的高度和宽度要求有所变化,要求高度与输入数据一致,宽度为7(输入数据为8),这种情况下应该如何设定padding呢?
实例代码如下:
import numpy as np
from mindspore import nn
from mindspore.common import dtype as mstype
from mindspore.common import Tensor
def pad_demo_02():
img_data = np.random.rand(2, 3, 8, 8)
ms_in = Tensor(img_data, dtype=mstype.float32)
conv_op = nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=(1, 1, 1, 0))
ms_out = conv_op(ms_in)
print("in shape: {}".format(ms_in.shape), flush=True)
print("out shape: {}".format(ms_out.shape), flush=True)
def main():
pad_demo_02()
if __name__ == "__main__":
main()
代码解读:
还记得padding[0] 、 padding[1] 、 padding[2] 和 padding[3] 这四个参数的意义,不记得没关系,再来一遍:通俗解释即为上高、下高、左宽、右宽。
这里的要求是输出数据的高度与输入数据一致,宽度为7(输入数据为8)。所以上高、下高的
padding
与6.3.1
中一致,即1;左宽、右宽加起来的padding为1,因为不能存在非整数,这里我们分别设置为1、0(这里没有特别要求,也可以设置为0、1)。这里计算
padding
数值的公式参加第7小节输出维度部分。最终的核心代码即为
nn.Conv2d(3, 4, 3, 1, pad_mode="pad", padding=(1, 1, 1, 0))
。
将上述代码保存到pad_conv2d_02.py
文件,使用如下命令运行:
python3 pad_conv2d_02.py
输出内容为:
可以看到输出数据的高度和宽度符合我们的上面的要求。
in shape: (2, 3, 8, 8)
out shape: (2, 4, 8, 7)
本节来单独介绍一下Conv2d
中数据输出维度的计算,在前面的6小节中,我们已经对部分做了铺垫。
在面对具体情况时,将相关参数带入公式即可算到要计算的部分。这里不再对公式展开解释。
本文重点介绍了MindSpore
中的Conv2d
算子。通过几种不同卷积模式(普通卷积、深度卷积、空洞卷积)的具体实现,以及数据格式、填充方式、输出维度多个角度来深入讲解Conv2d
算子的具体应用。
Conv2d官方文档
普通卷积与深度可分离卷积的区别
卷积网络基础知识—Depthwise Convolution && Pointwise Convolution && Separable Convolution
Depthwise卷积与Pointwise卷积
Depthwise Convolution与普通卷积的区别以及其他卷积方法
如何理解空洞卷积(dilated convolution)?
如何理解扩张卷积(dilated convolution)
本文为原创文章,版权归作者所有,未经授权不得转载!