讲透卷积函数tf.nn.conv2d的使用详解

2019-09-10 15:22:34 浏览数 (1)

作者: 邓红卫,衡阳师范学院信息工程学院副院长,教授

一、卷积神经网络的padding规则

(一)卷积函数 tf.nn.conv2d

TensorFlow里使用tf.nn.conv2d函数来实现卷积,其格式如下。

代码语言:javascript复制
tf.nn.conv2d (input, filter, strides, padding, use_ cudnn on_ gpu=None,name=None)

除去参数name参数用以指定该操作的name,与方法有关的共有5个参数。

(1)input: 指需要做卷积的输入图像,它要求是一个Tensor, 具有[batch, in_height,in_width, in_channels]这样的形状,具体含义是“训练时一个batch的图片数量, 图片高度,图片宽度,图像通道数”,注意这是一个四维的Tensor,要求类型为float32和float64其中之一。

(2)filter: 相当于<font bg-colorCNN中的卷积核,它要求是一个Tensor, 具有[filter_height, filter_width,in_channels, out_channels]这样的形状,具体含义是“卷积核的高度,滤波器的宽度,图像通道数,滤波器个数”,要求类型与参数input相同。有一个地方需要注意,第三维in_channels,就是参数input的第四维。

(3)strides:卷积时在图像每一维的步长, 这是一个一维的向量,长度为4(分别是[batch方向,height方向,width方向,channels方向)很多同学只认为第一维和最后一维默认必须置1,其实strides参数确定了滑动窗口在各个维度上移动的步数。

(4)padding: 定义元素边框与元素内容之间的空间。string类型的量,只能是SAME和VALID其中之一,这个值决定了不同的卷积方式,padding 的值为VALID时,表示边缘不填充,当其为’SAME时,表示填充到滤波器可以到达图像边缘。

(5)use_ cudnn on_ gpu: bool类型,是否使用cudnn加速,默认为true.

(6)返回值: tf.nn.conv2d函数结果返回一个Tensor, 这个输出就是常说的feature map.

注意:

在卷积函数中, padding参数是最容易引起歧义的,该参数仅仅决定是否要补0,因此一定要清楚padding设为SAME的真正含义。在设为SAME的情况下,只有在步长为1时生成的feature map才会与输入值相等。

(二)padding规则介绍

padding属性的意义是定义元素边框与元素内容之间的空间。

在tf.nn.conv2d函数中,当变量padding为VALID和SAME时,行列数具体是怎么计算的呢?其实是有公式的。为了方便演示,先来定义几个变量:

  • 输入的尺寸中高和宽定义成in_height、in_width。
  • 卷积核的高和宽定义成filter_height、filter_width。
  • 输出的尺寸中高和宽定义成output_height、output_widh。
  • 步长的高宽方向定义成strides_height、 strides_width。

1.VALID情况

输出宽和高的公式代码分别为:

代码语言:javascript复制
output_width = (in_width - filter_width   1)/strides_width (结果问上取整)
output_height = (in_height - filter_height   1)/strides_height (结果向上取整)

2.SAME情况

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)
out_width = in_width / strides_width (结果向上取整)

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
pad_top = pad_height / 2(结果取整)
pad_bottom = pad_height - pad_ top
pad_left = pad_width / 2(结果取整)
pad_right = pad_width - pad_left

可以看到补零的规则是优先在输入矩阵的右边和底边补零,例如补零的行数为三行。则top一行,bottom两行;如果补零的行数为一行,则补在bottom。因为filter是从左向右从上到下滑动的,所以滑动到右边或者底边的时候有可能会因为大步长而损失边界信息,所以优先在右边和底边补零。

注意:

pad_height代表高度方向填充0的行数,pad_width代表宽度方向填充0的列数,以此类推。

二、卷积函数使用实例详解

(一) 源程序

代码语言:javascript复制
import tensorflow as tf  
# [batch, in_height, in_width, in_channels] [训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数]  
input = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 1]))
input2 = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 2]))
input3 = tf.Variable(tf.constant(1.0,shape = [1, 4, 4, 1]))
 
# [filter_height, filter_width, in_channels, out_channels] [卷积核的高度,卷积核的宽度,图像通道数,卷积核个数]   
filter1 =  tf.Variable(tf.constant([-1.0,0,0,-1],shape = [2, 2, 1, 1]))
filter2 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 2]))
filter3 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 3]))
filter4 =  tf.Variable(tf.constant([-1.0,0,0,1, -1.0,0,0,2, -1.0,0,0,3, -1.0,0,0,4],shape = [2, 2, 2, 2]))
filter5 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 2, 1]))
 
# padding的值为‘VALID’,表示边缘不填充, 当其为‘SAME’时,表示填充到卷积核可以到达图像边缘  
op1 = tf.nn.conv2d(input, filter1, strides=[1, 2, 2, 1], padding='SAME')
#1个通道输入,生成1个feature map
op2 = tf.nn.conv2d(input, filter2, strides=[1, 2, 2, 1], padding='SAME')
#1个通道输入,生成2个feature map
op3 = tf.nn.conv2d(input, filter3, strides=[1, 2, 2, 1], padding='SAME')
#1个通道输入,生成3个feature map
op4 = tf.nn.conv2d(input2, filter4, strides=[1, 2, 2, 1], padding='SAME')
# 2个通道输入,生成2个feature map
op5 = tf.nn.conv2d(input2, filter5, strides=[1, 2, 2, 1], padding='SAME')
# 2个通道输入,生成1个feature map
 
vop1 = tf.nn.conv2d(input, filter1, strides=[1, 2, 2, 1], padding='VALID')
# 5*5 对于pading不同而不同
op6 = tf.nn.conv2d(input3, filter1, strides=[1, 2, 2, 1], padding='SAME')
vop6 = tf.nn.conv2d(input3, filter1, strides=[1, 2, 2, 1], padding='VALID')  
#4*4与pading无关
 
init = tf.global_variables_initializer()  
with tf.Session() as sess:  
    sess.run(init)  
    
    print("op1:n",sess.run([op1,filter1]))#1-1  后面补0
    print("------------------")
    
    print("op2:n",sess.run([op2,filter2])) #1-2多卷积核 按列取
    print("op3:n",sess.run([op3,filter3])) #1-3
    print("------------------")   
    
    print("op4:n",sess.run([op4,filter4]))#2-2    通道叠加
    print("op5:n",sess.run([op5,filter5]))#2-1        
    print("------------------")
  
    print("op1:n",sess.run([op1,filter1]))#1-1
    print("vop1:n",sess.run([vop1,filter1]))
    print("op6:n",sess.run([op6,filter1]))
    print("vop6:n",sess.run([vop6,filter1]))

(二) 输出结果详解分析:

------------------ 第1项操作输出结果 ------------------

1.op1:

代码语言:javascript复制
[array([[[[-2.], [-2.], [-1.]], [[-2.], [-2.], [-1.]], [[-1.], [-1.], [-1.]]]], dtype=float32),
array([[[[-1.]], [[ 0.]]], [[[ 0.]], [[-1.]]]], dtype=float32)]

解释:

代码语言:javascript复制
input = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 1]))
filter1 =  tf.Variable(tf.constant([-1.0,0,0,-1],shape = [2, 2, 1, 1]))
op1 = tf.nn.conv2d(input, filter1, strides=[1, 2, 2, 1], padding='SAME')

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)=「5/2=3
out_width = in_width / strides_width (结果向上取整) =「5/2=3

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
           = max(( 3 - 1) X2   2 - 5,0)=1
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
= max(( 3 - 1) X2   2 - 5,0)=1
pad_top = pad_height / 2(结果取整)=1/2=0
pad_bottom = pad_height - pad_ top=1-0=1
pad_left = pad_width / 2(结果取整)=1/2=0
pad_right = pad_width - pad_left=1-0=1

如下图所示:

这个fillter怎么来的呢?

代码语言:javascript复制
filter1 =  tf.Variable(tf.constant([-1.0,0,0,-1],shape = [2, 2, 1, 1]))

首先输入权重向量的是[-1.0,0,0,-1],然后根据要求分为1个fillter,每个fillter为2X2共4个权重。分配规则为:权重向量按照顺序每1个为一组,[-1.][ 0.][ 0.][ -1.],为什么是1个一组呢?因为要分成1个fillter。

然后后两个换到第二行:

代码语言:javascript复制
A1  B1
[-1.][ 0.]
[ 0.][ -1.]

然后AB两个矩阵两两组合形如[A,B],就得到了:

1个fillter...手算一遍,和答案一致。

详细入下表:

------------------ 第2项操作输出结果 ------------------

2.op2:

代码语言:javascript复制
 [array([[[[-2., -2.], [-2., -2.], [-2.,  0.]], [[-2., -2.], [-2., -2.], [-2.,  0.]], [[-1., -1.], [-1., -1.], [-1.,  0.]]]], dtype=float32),
array([[[[-1.,  0.]], [[ 0., -1.]]], [[[-1.,  0.]], [[ 0., -1.]]]], dtype=float32)]

解释:

代码语言:javascript复制
input = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 1]))
filter2 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 2]))
op2 = tf.nn.conv2d(input, filter2, strides=[1, 2, 2, 1], padding='SAME')

#1个通道输入,生成2个feature map

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)=「5/2=3
out_width = in_width / strides_width (结果向上取整) =「5/2=3

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
           = max(( 3 - 1) X2   2 - 5,0)=1
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
= max(( 3 - 1) X2   2 - 5,0)=1
pad_top = pad_height / 2(结果取整)=1/2=0
pad_bottom = pad_height - pad_ top=1-0=1
pad_left = pad_width / 2(结果取整)=1/2=0
pad_right = pad_width - pad_left=1-0=1

这个fillter怎么来的呢?

代码语言:javascript复制
filter2 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 2]))

首先输入权重向量的是[-1.0,0,0,-1,-1.0,0,0,-1],然后根据要求分为2个fillter,每个fillter为2X2共4个权重。分配规则为:权重向量按照顺序每2个为一组,[-1.0,0][ 0,-1][ -1.0,0][ 0,-1],为什么是2个一组呢?因为要分成2个fillter。然后后两个换到第二行:

代码语言:javascript复制
A1  A2   B1  B2
[-1.0, 0]   [ 0, -1]
[ -1.0, 0]  [ 0, -1]

然后AB两个矩阵两两组合形如[A,B],就得到了:

2个fillter...手算一遍,和答案一致。

详细如下表:

------------------ 第3项操作输出结果 ------------------

3.op3:

代码语言:javascript复制
 [array([[[[-2., -2., -2.], [-2., -2., -2.], [-1., -1., -1.]], [[-2., -2., -2.], [-2., -2., -2.], [-1., -1., -1.]], [[-2., -1.,  0.], [-2., -1.,  0.], [-1.,  0.,  0.]]]], dtype=float32),
array([[[[-1.,  0.,  0.]], [[-1., -1.,  0.]]], [[[ 0., -1., -1.]], [[ 0.,  0., -1.]]]], dtype=float32)]

解释:

代码语言:javascript复制
input = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 1]))
filter3 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 3]))
op3 = tf.nn.conv2d(input, filter3, strides=[1, 2, 2, 1], padding='SAME')
#1个通道输入,生成3个feature map

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)=「5/2=3
out_width = in_width / strides_width (结果向上取整) =「5/2=3

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
           = max(( 3 - 1) X2   2 - 5,0)=1
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
= max(( 3 - 1) X2   2 - 5,0)=1
pad_top = pad_height / 2(结果取整)=1/2=0
pad_bottom = pad_height - pad_ top=1-0=1
pad_left = pad_width / 2(结果取整)=1/2=0
pad_right = pad_width - pad_left=1-0=1

这个fillter怎么来的呢?

代码语言:javascript复制
filter3 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 1, 3]))

首先输入权重向量的是[-1.0,0,0,-1,-1.0,0,0,-1,-1.0,0,0,-1],然后根据要求分为3个fillter,每个fillter为2X2共4个权重。分配规则为:权重向量按照顺序每3个为一组,[-1.0,0,0],[-1,-1.0,0],[0,-1,-1.0],[0,0,-1],为什么是3个一组呢?因为要分成3个fillter。然后后两个换到第二行:

代码语言:javascript复制
A1  A2   A3   B1  B2  B3
[-1.0,  0,  0]   [-1, -1.0,  0]
[ 0,  -1,  -1.0]  [ 0 ,  0,  -1]

然后AB两个矩阵两两组合形如[A,B],就得到了:

3个fillter...手算一遍,和答案一致。

------------------ 第4项操作输出结果 ------------------

4.op4:

代码语言:javascript复制
 array([[[[-4., 10.], [-4., 10.], [-2.,  4.]], [[-4., 10.], [-4., 10.], [-2.,  4.]], [[-2.,  3.], [-2.,  3.], [-1.,  1.]]]], dtype=float32),
 array([[[[-1.,  0.], [ 0.,  1.]], [[-1.,  0.], [ 0.,  2.]]],  [[[-1.,  0.], [ 0.,  3.]], [[-1.,  0.], [ 0.,  4.]]]], dtype=float32)

解释:

代码语言:javascript复制
in input2 = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 2]))
filter4 =  tf.Variable(tf.constant([-1.0,0,0,1, -1.0,0,0,2, -1.0,0,0,3, -1.0,0,0,4],shape = [2, 2, 2, 2]))
op4 = tf.nn.conv2d(input2, filter4, strides=[1, 2, 2, 1], padding='SAME')
# 2个通道输入,生成2个feature

原理如下图所示。(吴恩达视频-第四门课1.6三维卷积)

这句话在这儿最管用!

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)=「5/2=3
out_width = in_width / strides_width (结果向上取整) =「5/2=3

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
           = max(( 3 - 1) X2   2 - 5,0)=1
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
= max(( 3 - 1) X2   2 - 5,0)=1
pad_top = pad_height / 2(结果取整)=1/2=0
pad_bottom = pad_height - pad_ top=1-0=1
pad_left = pad_width / 2(结果取整)=1/2=0
pad_right = pad_width - pad_left=1-0=1

这个fillter怎么来的呢?

代码语言:javascript复制
filter4 =  tf.Variable(tf.constant([-1.0,0,0,1, -1.0,0,0,2, -1.0,0,0,3, -1.0,0,0,4],shape = [2, 2, 2, 2]))

首先输入权重向量的是[-1.0,0,0,1, -1.0,0,0,2, -1.0,0,0,3, -1.0,0,0,4],然后根据要求分为2个fillter,每个fillter有2个通道,每个通道有2X2=4个权重,共有4X2=8个权重。分配规则为:权重向量按照顺序每2个为一组,共分8组:[-1.0,0],[0,1],[ -1.0,0],[0,2], [-1.0,0],[0,3], [-1.0,0],[0,4]。

因为每个fillter都有2个通道(?理由没陈述清楚!!!),先分为两大组。如下:

代码语言:javascript复制
 [  [-1.0,0],[0,1],[ -1.0,0],[0,2]  ] ,[  [-1.0,0],[0,3], [-1.0,0],[0,4]  ]

然后后两个换到第二行:

代码语言:javascript复制
A1      A2        B1       B2
[[[[-1.,  0.], [ 0.,  1.]], [[-1.,  0.], [ 0.,  2.]]],  
[[[-1.,  0.], [ 0.,  3.]], [[-1.,  0.], [ 0.,  4.]]]],

(1)A1 B1组合: (第一个fillter的2个通道)

代码语言:javascript复制
[[[-1.,  0.],[-1.,  0.] ],
[[-1.,  0.],  [-1.,  0.] ] ]

(2)A2 B2组合: (第二个fillter的2个通道)

代码语言:javascript复制
[[[0.,  1.],[0.,  2.] ],
[[0.,  3.],  [0.,  4.] ] ]

------------------ 第5项操作输出结果 ------------------

op5:

代码语言:javascript复制
 [array([[[[-4.], [-4.], [-2.]], [[-4.], [-4.], [-2.]], [[-2.], [-2.], [-1.]]]], dtype=float32),
array([[[[-1.], [ 0.]], [[ 0.], [-1.]]], [[[-1.], [ 0.]], [[ 0.], [-1.]]]], dtype=float32)]

解释:

代码语言:javascript复制
input2 = tf.Variable(tf.constant(1.0,shape = [1, 5, 5, 2]))
filter5 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 2, 1]))  
op5 = tf.nn.conv2d(input2, filter5, strides=[1, 2, 2, 1], padding='SAME')
# 2个通道输入,生成一个feature map

输出的宽和高将与卷积核没有关系,具休公式代码如下:

代码语言:javascript复制
out_height = in_height / strides_height (结果向上取整)=「5/2=3
out_width = in_width / strides_width (结果向上取整) =「5/2=3

这里有一个很重要的知识点一补零的规则,见如下代码:

代码语言:javascript复制
pad_height = max( (out_height - 1) X strides_height   filter_height - in_height,0)
           = max(( 3 - 1) X2   2 - 5,0)=1
pad_width = max((out_width - 1) X strides_width   filter_width - in_width, 0)
= max(( 3 - 1) X2   2 - 5,0)=1
pad_top = pad_height / 2(结果取整)=1/2=0
pad_bottom = pad_height - pad_ top=1-0=1
pad_left = pad_width / 2(结果取整)=1/2=0
pad_right = pad_width - pad_left=1-0=1

这个fillter怎么来的呢?

代码语言:javascript复制
filter5 =  tf.Variable(tf.constant([-1.0,0,0,-1,-1.0,0,0,-1],shape = [2, 2, 2, 1]))

首先输入权重向量的是[-1.0,0,0,-1,-1.0,0,0,-1],然后根据要求分为1个fillter,每个fillter有2个通道,共2X2=4个权重。分配规则为:权重向量按照顺序每2个为一组:[-1.0,0][ 0,-1][ -1.0,0][ 0,-1]。

因为每个fillter都有2个通道(?理由没陈述清楚!!!),先分为两大组。如下:

代码语言:javascript复制
[[-1.0,0],[ 0,-1]],[[ -1.0,0][ 0,-1]]
A1  A2   B1  B2
[[-1.0,0],[ 0,-1]],
[[ -1.0,0][ 0,-1]]

然后AB两个矩阵两两组合形如[A,B],就得到了:

1个fillter...手算一遍,和答案一致。

总结:

1.两个单层核,一个通道:

两个核分别对此通道进行卷积,然后两个featuremap以[mapA,mapB]来表示

2.一个双层核,两个通道:

这个卷积核的两层对应着两个通道卷一次,然后每层卷出来的featuremap相加(因为是一个核,所以得相加)成一个总的featuremap。

3.两个双层核,两个通道:

综合上述1、2的方法,两个卷积核分别卷出来两个featuremap,然后以[mapA,mapB]来表示。

------------------ 第6项操作输出结果 ------------------

op1:

代码语言:javascript复制
 [array([[[[-2.], [-2.], [-1.]], [[-2.], [-2.], [-1.]], [[-1.], [-1.], [-1.]]]], dtype=float32),
array([[[[-1.]], [[ 0.]]],  [[[ 0.]], [[-1.]]]], dtype=float32)]

------------------ 第7项操作输出结果 ------------------

vop1:

代码语言:javascript复制
 [array([[[[-2.], [-2.]],  [[-2.], [-2.]]]], dtype=float32),
 array([[[[-1.]], [[ 0.]]], [[[ 0.]], [[-1.]]]], dtype=float32)]

------------------ 第8项操作输出结果 ------------------

op6:

代码语言:javascript复制
 [array([[[[-2.], [-2.]], [[-2.], [-2.]]]], dtype=float32),
array([[[[-1.]], [[ 0.]]], [[[ 0.]], [[-1.]]]], dtype=float32)]

------------------ 第9项操作输出结果 ------------------

代码语言:javascript复制
vop6:
 [array([[[[-2.], [-2.]], [[-2.], [-2.]]]], dtype=float32),
array([[[[-1.]], [[ 0.]]], [[[ 0.]], [[-1.]]]], dtype=float32)]

参考链接:

https://blog.csdn.net/ali123aa/article/details/88038444

0 人点赞