想理解转置卷积的实现过程,首先应该深入理解一下卷积的实现过程,请参考我的另一篇文章:https://blog.csdn.net/qq_37691909/article/details/89487724
接下来介绍一下转置卷积的情况。官方的函数原型如下:
tf.nn.conv2d_transpose(value,filter,output_shape,strides,padding='SAME',data_format='NHWC',name=None
)
其中,第一个参数value,是输入的图像,是一个四维张量,分别对应于 【batchsize, 图像宽度,图像高度,图像深度】,数据类型也是浮点型
第二个参数filter,是卷积核的情况,也是四维张量,分别对应于 【核宽度,核高度,输出图像深度,输入图像深度】,这里和卷积中的参数顺序正好相反
第三个参数output_shape,是输出图像的尺寸,是一个四个元素的向量,分别对应于【batchsize,输出图像宽度,输出图像高度,输出图像深度】,注意到,在卷积的参数列表中是没有这一项的,为什么会有这一项见下面的分析。
第四个参数strides,表示步长,是一个向量,对应于【1,水平步长,垂直步长,1】
第五个参数是填充方式,和卷积的一样,SAME对应填充,VALID对应不填充
下面将详细地对这个函数的实现过程进行解释。
1、为什么要指定输出图像的尺寸?
针对这个问题,我们在卷积的时候就已经有所介绍。在卷积时会发现,当步长不是1后,可能出现两个输入图像尺寸不一致的图,但是使用同样的卷积核及参数配置,最终得到了相同尺寸的输出图像。 这是由于不能整除导致的,因此在进行反卷积的时候,通过指定输出图像的尺寸来进行不同的操作,从而可以得到更好的效果。
2、首先看一下当 使用SAME模式,步长为二,指定不同的输出尺寸时的运行情况
import tensorflow as tfx1 = tf.constant(1,shape=[1,3,3,1],dtype=tf.float32)
kernel = tf.constant(1,shape=[3,3,1,1],dtype=tf.float32)conv1 = tf.nn.conv2d_transpose(x1,kernel,[1,6,6,1],[1,2,2,1],padding='SAME')
conv2 = tf.nn.conv2d_transpose(x1,kernel,[1,5,5,1],[1,2,2,1],padding='SAME')with tf.Session() as sess:a,b = sess.run([conv1,conv2])print(a.shape,a)print(b.shape,b)输出:
(1, 6, 6, 1)
[[[[1.][1.][2.][1.][2.][1.]][[1.][1.][2.][1.][2.][1.]][[2.][2.][4.][2.][4.][2.]][[1.][1.][2.][1.][2.][1.]][[2.][2.][4.][2.][4.][2.]][[1.][1.][2.][1.][2.][1.]]]](1, 5, 5, 1)
[[[[1.][2.][1.][2.][1.]][[2.][4.][2.][4.][2.]][[1.][2.][1.][2.][1.]][[2.][4.][2.][4.][2.]][[1.][2.][1.][2.][1.]]]]
可以发现,输出了两个不同尺寸的图像。而从输出的像素值我们可以推断出,内部实际的运行机理是这个样子的,对于输出尺寸为5X5的图像,刚好可以整除,所以它的实现过程如下图所示:
首先,是对原始3X3的图像内部行和列之间进行填0,然后在外部一圈进行padding,然后用3X3的卷积核,以步长为1进行卷积运算,即可得到最终的尺寸为5X5的图像 。
而对于要输出为6X6的图像时,由于尺寸不能对应,此时在padding一圈之后,在进行运算时,又在图像的最左侧和最上方添加了一行零,之后再进行卷积,得到了6X6的输出。
这里可能有两个问题比较迷惑人:
第一个是:既然指定的步长是2,为什么在扩充后用步长为一的卷积进行运算? 关于这个问题,其实那个步长为2并不是指卷积时的步长,这个步长决定的其实是在进行扩充填零时,在行和列之间填多少行零。
第二个是:在卷积时尺寸不够为什么在左侧和上方填0? 这个问题我个人的看法是,在进行正常的卷积时,尺寸不是整数,我们是只在右侧和下方填零,从而得到了卷积后的结果,此时在最后一行一列的值其实是偏小的。 因此,在反卷积时,如果还在右侧和下方填零,可能会导致得到的图像最后几行几列的值都是零,影响精度,所以此时在左侧和上方补零。
3、再看一下 使用VALID模式,步长为2
import tensorflow as tfx1 = tf.constant(1,shape=[1,2,2,1],dtype=tf.float32)
kernel = tf.constant(1,shape=[3,3,1,1],dtype=tf.float32)conv1 = tf.nn.conv2d_transpose(x1,kernel,[1,6,6,1],[1,2,2,1],padding='VALID')
conv2 = tf.nn.conv2d_transpose(x1,kernel,[1,5,5,1],[1,2,2,1],padding='VALID')with tf.Session() as sess:a,b = sess.run([conv1,conv2])print(a.shape,a)print(b.shape,b)输出:
(1, 6, 6, 1)
[[[[1.][1.][2.][1.][1.][0.]][[1.][1.][2.][1.][1.][0.]][[2.][2.][4.][2.][2.][0.]][[1.][1.][2.][1.][1.][0.]][[1.][1.][2.][1.][1.][0.]][[0.][0.][0.][0.][0.][0.]]]](1, 5, 5, 1)
[[[[1.][1.][2.][1.][1.]][[1.][1.][2.][1.][1.]][[2.][2.][4.][2.][2.]][[1.][1.][2.][1.][1.]][[1.][1.][2.][1.][1.]]]]
从结果可以看出,此时的运行方法和上面是有区别的(输入图像的尺寸已经是2了,但是输出的尺寸依然可以是5和6)
对于输出为5来说,其实现方式如下图所示:
首先,因为有步长,所以依然在行列之间进行插0;但是由于设置为不进行填充,外围就必须填补更多的零。这个地方可以这样理解:因为不填充,所以若想得到2X2的图像,原始图像的尺寸必须是5X5,或者6X6,所以在进行反卷积时,为了能得到这个尺寸,就必须补上足够的零。 (这里参数名起的其实有误导性,不补零其实是补了更多的0)
对于输出6X6的也类似,但是因为尺寸不对应,所以需要在最右侧和最下方再填补一行零。 那么这里为什么是在右侧和下方,而刚刚介绍的是在左侧和上方呢? 这主要是由于使用了VALID模式,在卷积时是直接忽略最后一行和最右侧一列的,所以会存在信息丢失,此时在该处补零是在补全忽略掉的信息。
总之,在SAME模式下是在最上方和最左侧补零,在VALID模式下是在最右侧和最下方补。
4、再看 在VALID模式下,步长为1 的情况
import tensorflow as tfx1 = tf.constant(1,shape=[1,2,2,1],dtype=tf.float32)
kernel = tf.constant(1,shape=[3,3,1,1],dtype=tf.float32)conv2 = tf.nn.conv2d_transpose(x1,kernel,[1,4,4,1],[1,1,1,1],padding='VALID')with tf.Session() as sess:b = sess.run(conv2)print(b.shape,b)输出:
(1, 4, 4, 1)
[[[[1.][2.][2.][1.]][[2.][4.][4.][2.]][[2.][4.][4.][2.]][[1.][2.][2.][1.]]]]
因为此时步长为1,所以就不存在除不尽的问题,所以只有一个对应的输出图像尺寸。
根据最后的输出结果,可以推断出在内部的运算过程如下图所示:
因为此时步长为1,也就是没有步长,所以此时不在原始图像的内部插入0,又因为没有padding,所以直接补足够的0,保证卷积后得到对应的尺寸。
5、再看一下 使用SAME模式,步长为1 的情况
import tensorflow as tfx1 = tf.constant(1,shape=[1,4,4,1],dtype=tf.float32)
kernel = tf.constant(1,shape=[3,3,1,1],dtype=tf.float32)conv2 = tf.nn.conv2d_transpose(x1,kernel,[1,4,4,1],[1,1,1,1],padding='SAME')with tf.Session() as sess:b = sess.run(conv2)print(b.shape,b)输出:
(1, 4, 4, 1)
[[[[4.][6.][6.][4.]][[6.][9.][9.][6.]][[6.][9.][9.][6.]][[4.][6.][6.][4.]]]]
这个情况下,就和普通的卷积操作一样了,直接在外围补一圈零,然后再进行卷积即可。输入输出的尺寸是一样的,因为此时不会改变尺寸大小。
此外还有任意填充、半填充、全填充等形式,这里不再介绍,有兴趣可以看:https://github.com/vdumoulin/conv_arithmetic