1 Stride和Padding的介绍
计算卷积和反卷积绕不开stride和padding的讨论。卷积和反卷积里都有stride和padding参数,但是同一个参数在卷积和反卷积里的作用不一样,非常容易使人困惑,本文试图理清他们的关系,并用实际数值例子演示计算过程。
对于Stride而言,在卷积中,指卷积核与原始图像做卷积操作时的步长。可以想象卷积核是体检时做彩超的仪器,原始图是身体,输出图像是彩超图。步长为1表示医生每次移动仪器的幅度较小,扫描得很精细;步长较大表示医生每次移动仪器的幅度较大。在反卷积中,控制了输入图像元素之间填充0的个数。注意反卷积中的卷积运算步长固定为1。
对于Padding而言,在卷积中,指在原始图像四周填充上0,填充主要是为了保留原始图的边缘信息。在反卷积中,控制了输入图像四周填充0的个数。
Stride | Padding | |
卷积 | 卷积步长 | 控制四周填充0的个数 |
反卷积 | 内部填充0的个数 | 控制四周填充0的个数 |
2 卷积和反卷积中各维度关系
2.1 符号表示
符号 | 含义 |
i | 输入维度 |
o | 输出维度 |
k | 卷积核维度 |
s | Stride, 步长 |
p | 表示某一方向padding的个数 |
pu/pd/pl/pr | 表示上下左右(up,down,left,right)padding的个数。当需要特别说明具体方向时用此符号 |
2.2 卷积中维度关系
其中表示向下取整。
从公式字面意思上理解,首先p是左右侧填充的个数,2倍表示两侧都需要填充(实际中可以指定两侧填充数量不同),则i+2p就表示原始输入在左右侧填充了0的维度。维度i填充为p的矩阵做卷积等价于我们手动把输入矩阵两侧做填充得到的矩阵(维度为i+2p)做卷积。
k表示卷积核大小,从下图可以看出要扣除卷积核一个“身位”的大小。s表示步长,步长与输出维度有倍数的关系,步长越长,卷积时迈的步子越大,输出维度越低。向下取整是因为式子不一定可以整除,需要向下取整,在用卷积核对输入图像扫描时,如果剩余图像不够一个卷积核运算就舍弃了。这部分请大家自己动手画一画,文字很难解释清楚。
下面用一张图说明上式。
以5*5图像,填充为1,步长为2,卷积核3*3为例,求o。只看水平方向。填充后,图像宽度为7,其中蓝色部分是原始图像,绿色部分是填充的0。
最终图像宽度为o,因此一共有o次卷积操作。观察卷积核里的最左侧点A,这是卷积核的头,剩下2个圆圈是卷积核的身体。第一次在位置1,第二次在位置3,第三次在位置5,一般地,第o次位置在1+(o-1)*s,此时的位置是5,需要加上卷积核身体长度k-1才是原始图像的宽度。所以1+(o-1)*s+k-1=i+2p,得证o=(i+2p-k)/s+1
拓展一下,假设原始图像维度还可以增大,做3次卷积后剩余图像要求不够做一次卷积(否则就可以做第4次卷积了),假设增大的维度为a
那么1+(o-1)*s+k-1+a=i+2p,其中a<s。则o=(i+2p-k-a)/s+1,且右式可以整除。
(i+2p-k-a)/s=(i+2p-k)/s-a/s,注意到a/s是个小数,所以(i+2p-k)/s小数部分就等于a/s, 整数部分就是(i+2p-k)/s向下取整,整数部分还可以表示成(i+2p-k)/s-a/s,因此(i+2p-k)/s向下取整就等于(i+2p-k-a)/s。所以o为(i+2p-k)/s向下取整+1
此外还有一种表示方法:
其中表示向上取整。
下面简要说明二者等价。
当时,二者显然相等。
当时:
情况1:若能正好整除,则在式二中,多加了一定是小数,需要去除,因此向上取整后与式一相同。
情况2:若不能正好整除,则加1向下取整等价于不加1向上取整。即向上取整。注意向上取整和向上取整是等价的,分别记这2个式子为A和B,因为A式分子分母除不尽,A式等于相除后的整数部分+1。A式中分子分母都是整数,假设分子必须加上a才能使得分子和分母整除。那么a一定是一个正整数,1<=a,因此B式中分子加了1,小数部分只是更接近下一个整数而已,最多能使分子分母除尽从而使整数部分加1。因此二者相等。
2.3 卷积下Padding的模式
在Tensorflow中,Padding除了传入数值,还可以传入VALID, SAME字符串。
在VALID模式下,不做任何填充。
在数值模式下,按用户实际传入的值填充。
在SAME模式下,Tensorflow会自动填充,使得此式满足:。这里s相当于一个缩放倍数一样,将输入维度缩放成输出维度。
为了使得输出的维度为o,应该填补多少0呢?去掉向下取整符号,以水平维度为例,
,得到
这里去掉了向下取整符号,是假设括号内的部分可以正好除尽s,这好理解,因为我们填补0,正好填补到“够用”即可,没必要多填补用不到的部分。
如果上式右侧为偶数,则左侧和右侧各一半,如果上式右侧为奇数,则右侧多一个。即:
向下取整
向上取整
垂直维度同理,如果总填充数为奇数,则下侧多一个。
2.4 反卷积中维度关系
先回顾反卷积的计算流程
在反卷积中,有几点需要注意:
(1)Stride和Padding仅影响输入矩阵填充0的方式,Stride影响元素内部填充,Padding影响输入四周填充。最后一步做卷积运算时,Stride固定为1,Padding固定不填充(或者说前边的步骤已经填充过了)
(2)如果对反卷积的输出矩阵按照正向的Stride和Padding做卷积操作,结果的维度应等于反卷积的输入矩阵。这体现了卷积和反卷积是逆向过程。即如果反卷积的输入是i,输出是o,则应满足
(3)反卷积需要指定输出的维度大小,在Tensorflow中参数名是output_shape。因为满足上式中条件的o可能不止一种。例如假设Padding=VALID表示不填充,k=2, s=3, i=2.即,则o=5,6,7均可满足,所以传入output_shape可以是5、6、7维。
3 卷积和反卷积的计算
本节举数值例子演示卷积和反卷积计算过程,并使用tensorflow代码验证。
Tensorflow中有很多函数可以实现卷积和反卷积,甚至可以说有点眼花缭乱。可参考conv neural network - What does TensorFlow's `conv2d_transpose()` operation do? - Stack Overflow
本文主要使用tf.nn.conv2d和tf.nn.conv2d_transpose实现卷积和反卷积。关于函数详细用法可参考文档或其他博文。下文也会调用这2个函数,读者也可参考本文的使用方法。
3.1 卷积的计算
假设输入为3*3,卷积核为2*2
数值的例子为:
根据卷积的定义,输出为
所以卷积本质上还是的形式,可以看做全连接层,只是中间很多权重为0。计算机内部可用矩阵而非循环运算,提高运算速度。这里的矩阵是一个稀疏矩阵(很多元素为0),以为例,它只与,,,有关,但完全可以把其他5个a写上,只是系数为0而已,这样就可以转化成矩阵相乘。
输出第一项37是由两个红色虚框中的元素对应相乘之和求出。
在Tensorflow中可以验证
import numpy as np
import tensorflow as tf
x = tf.constant(np.arange(1, 10).reshape([1, 3, 3, 1]), dtype=tf.float32)
filters = tf.constant(np.arange(1, 5).reshape(2, 2, 1, 1), dtype=tf.float32)
print(tf.nn.conv2d(x, filters, [1, 1, 1, 1], "VALID").numpy().reshape([2, 2]))
输出为
[[37. 47.]
[67. 77.]]
3.2 反卷积的计算
反卷积的算法如下:
第一步,确定输出的大小。输出大小output_shape是由用户指定的,需要满足下表所示条件,不同Padding模式下是不同的。
第二步,确定最后一步做卷积操作时需要的元素数。最后一步卷积是按照不填充、步长为1运算的,因此很容易求出总共需要的元素数为o+k-1。则需要填充的个数为o+k-1-i
第三步,元素之间填充0。每个元素之间填充(s-1)个0。一共需要填充(i-1)次。
第四步,元素四周填充0。注意,网络对这里的填充规则说法不一,笔者在Tensorflow中验算,发现规则如下:在Padding=VALID和SAME模式下,左侧固定填充(k-1)个0,在数值模式下,左侧填充(k-1-pl)个0,如果此式小于0,还要对输入矩阵做裁剪,比如-1表示输入矩阵最左侧一列要删除。如果删除的列数超过了整体矩阵的列数,则整体矩阵删除,最终输出为空。总填充数减去左侧填充和内部填充,得到右侧填充数。上侧和下侧的规则同理。
第五步,将卷积核旋转180度,按照步长为1,不填充进行卷积运算。
这其中的要点总结如下:
Padding=VALID | Padding=SAME | Padding为数值 | |
反卷积的输出大小 | 满足⌈(o-k+1)/s⌉=i,是不定的 | 满足⌈0/s⌉=i,是不定的 | 满足⌈(0+p1+p2-k+1)/s⌉=i,是不定的 |
最后一步做卷积操作时需要的元素数 | o+k-1 | o+k-1 | o+k-1 |
总需要填充个数[1] | o+k-1-i | o+k-1-i | o+k-1-i |
输入矩阵元素之间填充0个数[2] | (s-1)*(i-1) | (s-1)*(i-1) | (s-1)*(i-1) |
输入矩阵左侧填充0个数[3] | k-1 | k-1 | k-1-pl |
输入矩阵右侧填充0个数 | [1]-[2]-[3] | [1]-[2]-[3] | [1]-[2]-[3] |
输入矩阵上侧填充0个数[4] | k-1 | k-1 | k-1-pad_top |
输入矩阵下侧填充0个数 | [1]-[2]-[4] | [1]-[2]-[4] | [1]-[2]-[4] |
在上一篇笔者以数值例子计算了反卷积,下面验证使用tensorflow计算,结果是否一致。
import tensorflow as tf
import numpy as np
a = tf.constant(np.arange(1, 10).reshape([1, 3, 3, 1]), dtype=tf.float32)
kernel = np.array([1, 0, -1, 0, 2, 0, 1, 0, -1]).reshape([3, 3])
kernel = tf.constant(kernel.reshape([3, 3, 1, 1]), dtype=tf.float32)
res = tf.nn.conv2d_transpose(a, kernel, output_shape=[1, 6, 6, 1], strides=[1, 2, 2, 1])
print(res.numpy().reshape([6, 6]))
可以发现结果一致。
[[ 1. 0. 1. 0. 1. 0.]
[ 0. 2. 0. 4. 0. 6.]
[ 5. 0. 2. 0. 2. 0.]
[ 0. 8. 0. 10. 0. 12.]
[11. 0. 2. 0. 2. 0.]
[ 0. 14. 0. 16. 0. 18.]]
按照反卷积的逻辑,手动填充图像用正向卷积的方式计算一下,看看结果是不是一致。
import tensorflow as tf
import numpy as np
b = tf.constant(np.array([[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 2, 0, 3, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 4, 0, 5, 0, 6, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 7, 0, 8, 0, 9, 0],
[0, 0, 0, 0, 0, 0, 0, 0]
]).reshape([1, 8, 8, 1]), dtype=tf.float32)
kernel = np.array([1, 0, -1, 0, 2, 0, 1, 0, -1]).reshape([3, 3])
kernel2 = np.flip(kernel) # 注意反卷积核和正向卷积核是reverse的关系
kernel2 = tf.constant(kernel2.reshape([3, 3, 1, 1]), dtype=tf.float32)
res = tf.nn.conv2d(b, kernel2, strides=[1, 1, 1, 1], padding="VALID")
print(res.numpy().reshape([6, 6]))
结果也是一致的。
[[ 1. 0. 1. 0. 1. 0.]
[ 0. 2. 0. 4. 0. 6.]
[ 5. 0. 2. 0. 2. 0.]
[ 0. 8. 0. 10. 0. 12.]
[11. 0. 2. 0. 2. 0.]
[ 0. 14. 0. 16. 0. 18.]]
4 Tensorflow自己动手实现反卷积
根据前文总结的反卷积计算逻辑,本节自己动手实现反卷积的逻辑,并与上一小节Tensorflow中的反卷积函数对比,验证计算逻辑。
4.1 引入模块并定义输入矩阵和卷积核
import math
import tensorflow as tf
import numpy as np
x2 = tf.constant(np.array([[1, 2],
[3, 4]]).reshape([1, 2, 2, 1]), dtype=tf.float32)
x3 = tf.constant(np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]).reshape([1, 3, 3, 1]), dtype=tf.float32)
x4 = tf.constant(np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]).reshape([1, 4, 4, 1]), dtype=tf.float32)
k2 = tf.constant(np.array([[1, 2],
[3, 4]]).reshape(2, 2, 1, 1), dtype=tf.float32)
k3 = tf.constant(np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]).reshape(3, 3, 1, 1), dtype=tf.float32)
k4 = tf.constant(np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]).reshape(4, 4, 1, 1), dtype=tf.float32)
这里定义了3种输入矩阵和3种卷积核,分别是2、3、4维。
4.2 引用Tensorflow中的反卷积计算
def conv2d_transpose(x, k, s, o, padding):
i = len(x.numpy()[0])
k_length = len(k.numpy()[0])
print(f"i={i},k={k_length},i={o},padding={padding}")
print("Tensorflow计算结果")
if isinstance(o, list):
output_shape = [1, o[0], o[1], 1]
else:
output_shape = [1, o, o, 1]
return tf.nn.conv2d_transpose(x, k, output_shape, strides=s,
padding=padding).numpy().reshape([output_shape[1], output_shape[2]])
这里调用了tensorflow中的反卷积,后续会将自己动手实现的函数与此函数对比。
4.3 定义零元素填充功能函数
def padding_zero(matrix, direction, n):
"""
在matrix的上下左右/内部补0
Args:
matrix: 原始矩阵
direction: 方向,可选left, up, right, down, inside
n: 补0的个数
Returns:
补零后的矩阵
Examples:
>>> a = tf.constant(np.array(
... [[1, 2, 3],
... [4, 5, 6],
... [7, 8, 9]]).reshape([1, 3, 3, 1]), dtype=tf.float32
>>> res = padding_zero(a, "up", 2).numpy()
>>> res.reshape([len(res[0]), len(res[0][0])])
[[0. 0. 0.]
[0. 0. 0.]
[1. 2. 3.]
[4. 5. 6.]
[7. 8. 9.]]
>>> res = padding_zero(a, "inside", 2).numpy()
>>> res.reshape([len(res[0]), len(res[0][0])])
[[1. 0. 0. 2. 0. 0. 3.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[4. 0. 0. 5. 0. 0. 6.]
[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0.]
[7. 0. 0. 8. 0. 0. 9.]]
"""
if n < 0:
assert direction != "inside" # inside不缩减零
return cutting_zero(matrix, direction, -n)
a = matrix.numpy() # 维度为1 * x * y * 1
row_length = matrix.shape[1]
column_length = matrix.shape[2]
a = a.reshape([row_length, column_length])
res = a
if direction == "up":
if row_length > 0:
res = np.pad(a, [[n, 0], [0, 0]], constant_values=0)
row_length = row_length + n
elif direction == "down":
if row_length > 0:
res = np.pad(a, [[0, n], [0, 0]], constant_values=0)
row_length = row_length + n
elif direction == "left":
if column_length > 0:
res = np.pad(a, [[0, 0], [n, 0]], constant_values=0)
column_length = column_length + n
elif direction == "right":
if column_length > 0:
res = np.pad(a, [[0, 0], [0, n]], constant_values=0)
column_length = column_length + n
else:
row_length = (row_length - 1) * n + row_length # 需要补x-1个位置,每个位置补n个0
column_length = (column_length - 1) * n + column_length # 需要补y-1个位置,每个位置补n个0
res = np.zeros([row_length, column_length])
x_position = np.arange(0, len(res), n + 1) # 原矩阵在新矩阵中的位置
y_position = np.arange(0, len(res[0]), n + 1) # 原矩阵在新矩阵中的位置
for i in range(len(x_position)):
for j in range(len(y_position)):
res[x_position[i]][y_position[j]] = a[i][j]
res = tf.constant(np.array(res).reshape([1, row_length, column_length, 1]), dtype=tf.float32)
return res
def cutting_zero(matrix, direction, n):
"""
在matrix的上下左右缩减
Args:
matrix: 原始矩阵
direction: 方向,可选left, up, right, down
n: 缩减行/列数
Returns:
缩减后的矩阵
Examples:
>>> a = tf.constant(np.array(
... [[1, 2, 3],
... [4, 5, 6],
... [7, 8, 9]]).reshape([1, 3, 3, 1]), dtype=tf.float32
>>> res = cutting_zero(a, "up", 1).numpy()
>>> res.reshape([len(res[0]), len(res[0][0])])
[[4. 5. 6.]
[7. 8. 9.]]
"""
a = matrix.numpy() # 维度为1 * x * y * 1
row_length = len(a[0])
column_length = len(a[0][0])
a = a.reshape([row_length, column_length])
if direction == "up":
res = a[n:, :]
row_length = max(row_length - n, 0)
elif direction == "down":
res = a[:-n, :]
row_length = max(row_length - n, 0)
elif direction == "left":
res = a[:, n:]
column_length = max(column_length - n, 0)
else:
res = a[:, :-n]
column_length = max(column_length - n, 0)
res = tf.constant(np.array(res).reshape([1, row_length, column_length, 1]), dtype=tf.float32)
return res
定义了零元素填充函数padding_zero,实现在四周、内部填充。注意反卷积有时需要对输入矩阵做裁剪,因此也定义了cutting_zero函数。
4.4 反卷积运算
def conv2d_transpose2(x, filters, s, o, padding):
print("手算结果")
i = len(x.numpy()[0])
k = len(filters.numpy()[0])
if isinstance(o, list):
output_shape = [1, o[0], o[1], 1]
else:
output_shape = [1, o, o, 1]
# 检查传入的i是否合适
if padding == "VALID":
assert math.ceil((o - k + 1) / s) == i
elif padding == "SAME":
assert o == i * s
else:
assert i == math.ceil((output_shape[1] + sum(padding[1]) - k + 1) / s)
assert i == math.ceil((output_shape[2] + sum(padding[2]) - k + 1) / s)
# 在x中间补0的个数
num_of_zero_inside = s - 1
if padding in ["SAME", "VALID"]:
# 做卷积需要的维度数
i_padding = o + k - 1
# 左、上补0的个数:k-1
num_of_zero_left = k - 1
num_of_zero_up = k - 1
# 右、下补0的个数
num_of_zero_right = i_padding - i - num_of_zero_inside * (i - 1) - num_of_zero_left
num_of_zero_down = i_padding - i - num_of_zero_inside * (i - 1) - num_of_zero_up
else:
# 做卷积需要的维度数
i_padding_row = output_shape[1] + k - 1
i_padding_column = output_shape[2] + k - 1
# 左、上补0的个数:k-1-pl或pu
pad_top = padding[1][0]
pad_left = padding[2][0]
num_of_zero_left = k - 1 - pad_left
num_of_zero_up = k - 1 - pad_top
# 右、下补0的个数
num_of_zero_right = i_padding_column - i - num_of_zero_inside * (i - 1) - num_of_zero_left
num_of_zero_down = i_padding_row - i - num_of_zero_inside * (i - 1) - num_of_zero_up
x_new = padding_zero(x, "inside", num_of_zero_inside)
x_new = padding_zero(x_new, "left", num_of_zero_left)
x_new = padding_zero(x_new, "up", num_of_zero_up)
x_new = padding_zero(x_new, "right", num_of_zero_right)
x_new = padding_zero(x_new, "down", num_of_zero_down)
# 对卷积核做reverse
filters_numpy = filters.numpy() # 维度为n*n*1*1
dim_filters = len(filters_numpy)
filters_reverse = np.flip(filters_numpy.reshape([dim_filters, dim_filters])) # 翻转
filters_reverse = tf.constant(filters_reverse.reshape([dim_filters, dim_filters, 1, 1]), dtype=tf.float32)
# 此处s=1,p=0,判断做卷积时i-k+1应大于0,避免极端情况例如x_new为空
if x_new.shape[1] - filters_reverse.shape[0] + 1 > 0 and x_new.shape[2] - filters_reverse.shape[1] + 1 > 0:
res = tf.nn.conv2d(x_new, filters_reverse, strides=1, padding="VALID")
return res.numpy().reshape([output_shape[1], output_shape[2]])
else:
return []
最终实现了反卷积运算函数conv2d_transpose2,下节将此函数与tensorflow自带的函数做对比。
4.5 实例验证
"""
VALID模式
"""
print("VALID模式")
print(conv2d_transpose(x3, k2, 1, 4, "VALID"))
print(conv2d_transpose2(x3, k2, 1, 4, "VALID"))
print(conv2d_transpose(x4, k2, 1, 5, "VALID"))
print(conv2d_transpose2(x4, k2, 1, 5, "VALID"))
print(conv2d_transpose(x2, k2, 2, 4, "VALID"))
print(conv2d_transpose2(x2, k2, 2, 4, "VALID"))
print(conv2d_transpose(x2, k2, 2, 5, "VALID"))
print(conv2d_transpose2(x2, k2, 2, 5, "VALID"))
print(conv2d_transpose(x3, k2, 2, 6, "VALID"))
print(conv2d_transpose2(x3, k2, 2, 6, "VALID"))
print(conv2d_transpose(x2, k2, 3, 6, "VALID"))
print(conv2d_transpose2(x2, k2, 3, 6, "VALID"))
print(conv2d_transpose(x3, k2, 4, 10, "VALID"))
print(conv2d_transpose2(x3, k2, 4, 10, "VALID"))
print(conv2d_transpose(x3, k2, 4, 11, "VALID"))
print(conv2d_transpose2(x3, k2, 4, 11, "VALID"))
print(conv2d_transpose(x3, k2, 4, 12, "VALID"))
print(conv2d_transpose2(x3, k2, 4, 12, "VALID"))
print(conv2d_transpose(x3, k2, 4, 13, "VALID"))
print(conv2d_transpose2(x3, k2, 4, 13, "VALID"))
print(conv2d_transpose(x2, k3, 8, 17, "VALID"))
print(conv2d_transpose2(x2, k3, 8, 17, "VALID"))
"""
SAME模式
"""
print("SAME模式")
print(conv2d_transpose(x3, k2, 1, 3, "SAME"))
print(conv2d_transpose2(x3, k2, 1, 3, "SAME"))
print(conv2d_transpose(x3, k2, 2, 6, "SAME"))
print(conv2d_transpose2(x3, k2, 2, 6, "SAME"))
print(conv2d_transpose(x3, k2, 3, 9, "SAME"))
print(conv2d_transpose2(x3, k2, 3, 9, "SAME"))
print(conv2d_transpose(x3, k2, 4, 12, "SAME"))
print(conv2d_transpose2(x3, k2, 4, 12, "SAME"))
print(conv2d_transpose(x3, k3, 8, 24, "SAME"))
print(conv2d_transpose2(x3, k3, 8, 24, "SAME"))
"""
数值模式
"""
pad_top = 8
pad_bottom = 0
pad_left = 0
pad_right = 0
print(conv2d_transpose(x2, k3, 3, [0, 6], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
print(conv2d_transpose2(x2, k3, 3, [0, 6], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
pad_top = 1
pad_bottom = 2
pad_left = 3
pad_right = 2
print(conv2d_transpose(x2, k3, 3, [3, 1], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
print(conv2d_transpose2(x2, k3, 3, [3, 1], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
pad_top = 1
pad_bottom = 5
pad_left = 3
pad_right = 4
# (i+k-1)/s=2
print(conv2d_transpose(x2, k4, 3, [1, 1], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
print(conv2d_transpose2(x2, k4, 3, [1, 1], [[0, 0], [pad_top, pad_bottom],
[pad_left, pad_right], [0, 0]]))
代码对比了各种模式下、各种参数下Tensorflow的结果与自己动手计算的结果。例如其中一处输出如下图所示。
经过对比所有的输出均一致,说明笔者的理解是正确的。
SAME模式
i=3,k=2,i=3,padding=SAME
Tensorflow计算结果
[[ 1. 4. 7.]
[ 7. 23. 33.]
[19. 53. 63.]]
手算结果
[[ 1. 4. 7.]
[ 7. 23. 33.]
[19. 53. 63.]]