MindSpore易点通·精讲系列–网络构建之Conv2d算子

Dive Into MindSpore – Conv2d Operator For Network Construction

MindSpore易点通·精讲系列–网络构建之Conv2d算子

本文开发环境

  • Ubuntu 20.04
  • Python 3.8
  • MindSpore 1.7.0

本文内容摘要

  • 先看文档
  • 普通卷积
  • 深度卷积
  • 空洞卷积
  • 数据格式
  • 填充方式
  • 输出维度
  • 本文总结
  • 本文参考

1. 先看文档

老传统,先看官方文档。

参数解读:

  • in_channels – 输入通道数
  • out_channels – 输出通道数
  • kernel_size – 二维卷积核的高度和宽度。值应该为整型(代表高度和宽度均为该值)或两个整型的tuple(分别代表高度和宽度值)
  • stride – 二维卷积的移动步长
  • pad_mode – 填充模式
  • padding – 填充数量
  • dilation – 二维卷积核膨胀尺寸,输入值同kernel_size。空洞卷积参数
  • group – 将过滤器拆分为组。深度卷积参数
  • has_bias – 是否添加偏置
  • data_format – 输入数据的数据格式,NCHWNHWC

2. 普通卷积

普通卷积,又可以称为常规卷积。由于是在深度学习相关课程中最先接触到的CNN卷积方式,本文不再对其原理展开介绍。下面通过一个实例来介绍MindSpore中的用法。

例如:

对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,padding方式为same,即输入和输出的高和宽一致。

其示意图如下所示:
MindSpore易点通·精讲系列–网络构建之Conv2d算子_第1张图片

如何用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)

3. 深度卷积

深度卷积(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_channelsout_channelsgroup三个参数的值相等时,可以认为即为2D的深度卷积。下面通过一个案例来进一步讲解。

例如:

对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

假设我们对上述图片进行深度卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为3(与输入通道一致),padding方式为same,即输入和输出的高和宽一致。

其示意图如下所示:
MindSpore易点通·精讲系列–网络构建之Conv2d算子_第2张图片

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节的内容,就可以很容易的实现出来了。

4. 空洞卷积

空洞卷积(Dilated Convolution),又称扩张卷积、膨胀卷积,是在标准的卷积核中注入空洞,以此来增加模型的感受野(reception field)。相比原来的正常卷积操作,扩张卷积多了一个参数: dilation rate,指的是卷积核的点的间隔数量,比如常规的卷积操作dilatation rate为1。
MindSpore易点通·精讲系列–网络构建之Conv2d算子_第3张图片

(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中。

一点补充

  • 在上图的(b)中,对于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_0common_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 shapedilated 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)

5. 数据格式

特别注意:NHWC数据格式目前只支持在GPU硬件下使用。

Conv2d中,输入数据的数据格式可选值有NHWCNCHW,默认值为NCHW。其中各个字母的含义如下:

  • N – 批数
  • C – 通道数
  • H – 高度
  • W – 宽度

那么两种数据格式又有什么区别呢,先从一段错误代码讲起:

在下面的代码中,我们创建数据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下可用,CPUAscend下目前不可用。

6. 填充方式

Conv2d中,填充模式(pad_mode)可选值为samevalidpad,默认值:same。下面来介绍这三种填充方式。

6.1 same

对于same填充方式,官方描述如下:

输出的高度和宽度分别与输入整除 stride 后的值相同。若设置该模式,padding 的值必须为0。

具体示例代码参见第2小节。

特别注意

  1. PytorchConv2d的区别,在Pytorch中,填充方式为same时,只允许stride为1,而MindSpore可以允许大于1的整数值。
  2. 由于stride允许1之外的整数,所以same模式下输出数据的高度和宽度未必和输入数据一致,这一点一定要谨记,至于输出数据的高度和宽度请参考第7小节。

6.2 valid

对于valid填充方式,官方描述如下:

在不填充的前提下返回有效计算所得的输出。不满足计算的多余像素会被丢弃。如果设置此模式,则 padding 的值必须为0。

具体示例代码参见第4小节。

6.3 pad

本节重点来讲解一下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]

  • 这里的上、下、左、右表示的高和宽,通俗解释即为上高、下高、左宽、右宽。

下面通过两个示例来讲解两种数据形式。

6.3.1 padding为一个整数

例如,对于二维的8×8原始图像,图像格式为RGB(即通道数为3),可以认为这是一个3维图片,数据维度为 3×8×8(NCHW)或8×8×3(NHWC)。

假设我们对上述图片进行普通卷积操作,卷积核大小为3×3,步长为1,卷积后的输出通道数为4,要求输出数据的高度和宽度与输入数据一致。

其示意图如下所示:
MindSpore易点通·精讲系列–网络构建之Conv2d算子_第4张图片

简单分析:上面的案例要求第2节中的代码就可以实现,在第2节中采用的pad_modesame,那么如果采用pad_modepad呢,代码如下:

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_modepad模式下,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)

6.3.2 padding为四个整数tuple

"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)。所以上高、下高的padding6.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)

7. 输出维度

本节来单独介绍一下Conv2d中数据输出维度的计算,在前面的6小节中,我们已经对部分做了铺垫。

各种情况下的输出维度见下图公式。
MindSpore易点通·精讲系列–网络构建之Conv2d算子_第5张图片

在面对具体情况时,将相关参数带入公式即可算到要计算的部分。这里不再对公式展开解释。

本文总结

本文重点介绍了MindSpore中的Conv2d算子。通过几种不同卷积模式(普通卷积、深度卷积、空洞卷积)的具体实现,以及数据格式、填充方式、输出维度多个角度来深入讲解Conv2d算子的具体应用。

本文参考

  • Conv2d官方文档

  • 普通卷积与深度可分离卷积的区别

  • 卷积网络基础知识—Depthwise Convolution && Pointwise Convolution && Separable Convolution

  • Depthwise卷积与Pointwise卷积

  • Depthwise Convolution与普通卷积的区别以及其他卷积方法

  • 如何理解空洞卷积(dilated convolution)?

  • 如何理解扩张卷积(dilated convolution)

本文为原创文章,版权归作者所有,未经授权不得转载!

你可能感兴趣的:(深度学习,MindSpore,cnn)