一. Caffe、Tensorflow的padding策略

在之前的转载过的一篇文章——《tensorflow ckpt文件转caffemodel时遇到的坑》提到过,caffe的padding方式和tensorflow的padding方式有很大的区别,输出无法对齐。这是为什么呢?

下面简单回顾一下

卷积操作输出的形状计算公式是这样的:

output_shape = (image_shape-filter_shape+2*padding)/stride + 1

因为padding前面的系数是2,所以在padding时,一般是对称地补,左/右各padding一列 或者 上下各padding一行。

那么问题来了,如果stride是2,而括号里算出来的值刚好是奇数怎么办?那就再偷偷摸摸补一列padding或者补一行padding。

于是,caffe和tensorflow的区别就出来了。

caffe偷偷摸摸地把一行0补在上面 或者 把一列0补在左边,tensorflow正好镜像对称,把一行0补在下面或者把一列0补在右边。这就是导致输出对齐不了的原因,前面几层输出的feature map的中间还能勉强对上,随着网络结构的加深,到fc之前已经完全对不上了。

也就是说

  • caffe的padding策略是把0补在左上
  • tensorflow的padding策略是把0补在右下

那么,PyTorch的padding策略是怎样的呢?在介绍padding策略之前,先简单的介绍一下PyTorch中的nn.Conv2d算子。

二. nn.Conv2d简单说明

nn.Conv2d的介绍主要译自官网

nn.Conv2d的功能是:对由多个输入平面组成的输入信号进行二维卷积,以最简单的例子进行说明:

输入信号的形式为 ( N , C i n , H , W ) (N,C_{in},H,W) (N,Cin,H,W) N N N表示batch size, C i n C_{in} Cin表示channel个数, H H H W W W分别表示特征图的高和宽。

参数说明:

  • stride(步长):控制cross-correlation的步长,可以设为1个int型数或者一个(int, int)型的tuple。

  • padding(补0):控制zero-padding的数目。

  • dilation(扩张):控制kernel点(卷积核点)的间距; 也被称为 "à trous"算法. 可以在此github地址查看:Dilated convolution animations

  • groups(卷积核个数):这个比较好理解,通常来说,卷积个数唯一,但是对某些情况,可以设置范围在1 —— in_channels中数目的卷积核:

At groups=1, all inputs are convolved to all outputs.
At groups=2, the operation becomes equivalent to having two conv layers side by side, each seeing half the input channels, and producing half the output channels, and both subsequently concatenated.
At groups=in_channels, each input channel is convolved with its own set of filters (of size ⌊out_channelsin_channels⌋
).

注意:kernel_size, stride, padding, dilation 不但可以是一个单个的int——表示在高度和宽度使用这个相同的int作为参数
也可以使用一个(int1, int2)的元组(本质上单个的int就是相同int的(int, int))。在元组中,第1个参数对应高度维度,第2个参数对应宽度维度。

还有一点需要提醒的是:卷积核的size的选择可能导致input中某几行(最后几行)没有关联起来,这是因为我们默认使用的模式是valid,而不是full(在tensorflow中也称为same)。如果想要充分利用input的话,则依赖于用户对padding以及stride等参数的设置。相比tensorflow,PyTorch需要用户清楚的知道的自己的卷积核选取对结果的影响。

参数的详细说明(基于PyTorch0.4.1)

这里写图片描述

下图中的 H o u t H_{out} Hout W o u t W_{out} Wout是根据我们在nn.Conv2d中设置的padding,dilation,kernel_size,stride等参数得到的输出特征图的高度和宽度。
这里写图片描述

三. nn.Conv2d中的padding操作

nn.Conv2d简单介绍完了,现在来讲讲padding在nn.Conv2d中怎么实现的,也就是怎么补的0,或者说补0的策略。

Q1: padding是卷积之后还是卷积之前还是卷积之后实现的?

padding是在卷积之前补0,如果愿意的话,可以通过使用torch.nn.Functional.pad来补非0的内容。

Q2:padding补0的默认策略是什么?

四周都补!如果pad输入是一个tuple的话,则第一个参数表示高度上面的padding,第2个参数表示宽度上面的
下面将展示一个padding = 1的例子:

这里写图片描述
显然,padding=1的效果是:原来的输入层基础上,上下左右各补了一行!除此之外我们看到,上下左右都是0.9862,那么,这个东西是啥呢?为什么不是0呢?

为了这个问题,我甚至还去PyTorch论坛上献丑了,估计大家可能也知道是咋回事了…
是的!是Bias!我问的问题是这样的:
Calculation detail in nn.Conv2d

Hello, I just can’t figure out the way nn.Conv2d calculate the output . 

The result calculated from torch is not the same as some machine learning course had taught.

For example, likes the code below:

>> m = torch.nn.Conv2d(1, 1, 3, padding=0)
>> m(input)
tensor([[[[ 0.5142,  0.3803,  0.2687],
          [-0.4321,  1.1637,  1.0675],
          [ 0.1742,  0.0869, -0.4451]]]], grad_fn=<ThnnConv2DBackward>)
>> input
tensor([[[[ 0.7504,  0.1157,  1.4940, -0.2619, -0.4732],
          [ 0.1497,  0.0805,  2.0829, -0.0925, -1.3367],
          [ 1.7471,  0.5205, -0.8532, -0.7358, -1.3931],
          [ 0.1159, -0.2376,  1.2683, -0.0959, -1.3171],
          [-0.1620, -1.8539,  0.0893, -0.0568, -0.0758]]]])
>> m.weight
Parameter containing:
tensor([[[[ 0.2405,  0.3018,  0.0011],
          [-0.1691, -0.0701, -0.0334],
          [-0.0429,  0.2668, -0.2152]]]], requires_grad=True)

for the left top element 0.5142, it’s not the output equals to

>> import numpy as np
>> w = np.array([[0.2405,  0.3018,  0.0011], 
>>              [-0.1691, -0.0701, -0.0334], 
>>              [-0.0429,  0.2668, -0.2152]])
# top-left 3x3 matrix of 5x5
>> x = np.array([[ 0.7504,  0.1157,  1.4940], 
>>               [ 0.1497,  0.0805,  2.0829], 
>>               [1.7471,  0.5205, -0.8532]])
>> print(np.sum(w*x))
#  0.364034 != 0.5142
0.36403412999999996

My Question here is: Why Could the output not equal to 0.5142?

Further more, when i add paramter padding into nn.Conv2d, 
The outcome seems obscure to me as below, thanks a lot for explain that to me.Thank you!

>> input
tensor([[[[ 0.7504,  0.1157,  1.4940, -0.2619, -0.4732],
          [ 0.1497,  0.0805,  2.0829, -0.0925, -1.3367],
          [ 1.7471,  0.5205, -0.8532, -0.7358, -1.3931],
          [ 0.1159, -0.2376,  1.2683, -0.0959, -1.3171],
          [-0.1620, -1.8539,  0.0893, -0.0568, -0.0758]]]])
# set padding from 0 to 1 equals to (1, 1)
>> m1 = torch.nn.Conv2d(1, 1, 1, padding=1)
>> m1(input)
tensor([[[[0.9862, 0.9862, 0.9862, 0.9862, 0.9862, 0.9862, 0.9862],
          [0.9862, 1.0771, 1.0002, 1.1672, 0.9544, 0.9288, 0.9862],
          [0.9862, 1.0043, 0.9959, 1.2385, 0.9749, 0.8242, 0.9862],
          [0.9862, 1.1978, 1.0492, 0.8828, 0.8970, 0.8174, 0.9862],
          [0.9862, 1.0002, 0.9574, 1.1398, 0.9745, 0.8266, 0.9862],
          [0.9862, 0.9665, 0.7615, 0.9970, 0.9793, 0.9770, 0.9862],
          [0.9862, 0.9862, 0.9862, 0.9862, 0.9862, 0.9862, 0.9862]]]],
       grad_fn=<ThnnConv2DBackward>)

The confused point is that how 0.9862 be calculated? 
And what is the default padding strategy in nn.Conv2d?

Thank you for reading and answer!

答案也很简单——我没考虑bias!
这里写图片描述

根据下图,Q2中神秘的0.9862的来历我们就很清楚了,是bias的值。
这里写图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐