TensorFlow 的 `conv2d_transpose()` 操作是做什么的?
What does TensorFlow's `conv2d_transpose()` operation do?
conv2d_transpose()
操作的文档没有清楚地解释它的作用:
The transpose of conv2d.
This operation is sometimes called "deconvolution" after
Deconvolutional Networks, but is actually the transpose (gradient) of
conv2d rather than an actual deconvolution.
我查看了文档指向的论文,但没有帮助。
此操作的作用是什么?您为什么要使用它?
这是我在网上看到的关于卷积转置如何工作的最好解释here。
我将给出我自己的简短描述。它应用小数步长的卷积。换句话说,分隔输入值(带零)以将过滤器应用于可能小于过滤器大小的区域。
至于为什么要使用它。它可以用作一种具有学习权重的上采样,而不是双线性插值或其他一些固定形式的上采样。
这是 "gradients" 的另一个观点,即为什么 TensorFlow 文档说 conv2d_transpose()
是 "actually the transpose (gradient) of conv2d rather than an actual deconvolution"。 有关 conv2d_transpose
中实际计算的更多详细信息,我强烈推荐 this article,从第 19 页开始。
四个相关函数
在tf.nn
中,有4个密切相关但比较容易混淆的2d卷积函数:
tf.nn.conv2d
tf.nn.conv2d_backprop_filter
tf.nn.conv2d_backprop_input
tf.nn.conv2d_transpose
一句话总结:都是2d卷积。它们的区别在于它们的输入参数排序、输入旋转或转置、步幅(包括小数步幅大小)、填充等。有了 tf.nn.conv2d
,可以通过转换输入和更改conv2d
个参数。
问题设置
- 正向和反向计算:
# forward
out = conv2d(x, w)
# backward, given d_out
=> find d_x?
=> find d_w?
在正向计算中,我们计算输入图像x
与过滤器w
的卷积,结果为out
。
在反向计算中,假设我们得到 d_out
,即梯度 w.r.t。 out
。我们的目标是找到d_x
和d_w
,也就是梯度w.r.t。 x
和 w
分别。
为了便于讨论,我们假设:
- 所有步幅为
1
- 所有
in_channels
和out_channels
都是1
- 使用
VALID
填充
- 奇数过滤器大小,这避免了一些不对称的形状问题
简答
从概念上讲,根据上述假设,我们有以下关系:
out = conv2d(x, w, padding='VALID')
d_x = conv2d(d_out, rot180(w), padding='FULL')
d_w = conv2d(x, d_out, padding='VALID')
其中rot180
是旋转180度的二维矩阵(左右翻转和自上而下翻转),FULL
表示"apply filter wherever it partly overlaps with the input"(参见theano docs ).请注意,这仅在上述假设下有效,但是,可以更改 conv2d 参数以对其进行概括。
要点:
- 输入梯度
d_x
是输出梯度d_out
和权重w
的卷积,有一些修改。
- 权重梯度
d_w
是输入x
和输出梯度d_out
的卷积,有一些修改。
长答案
现在,让我们给出一个实际工作的代码示例,说明如何使用上述 4 个函数来计算给定 d_out
的 d_x
和 d_w
。这表明如何
conv2d
,
conv2d_backprop_filter
,
conv2d_backprop_input
,以及
conv2d_transpose
相互关联。
Please find the full scripts here。
以 4 种不同的方式计算 d_x
:
# Method 1: TF's autodiff
d_x = tf.gradients(f, x)[0]
# Method 2: manually using conv2d
d_x_manual = tf.nn.conv2d(input=tf_pad_to_full_conv2d(d_out, w_size),
filter=tf_rot180(w),
strides=strides,
padding='VALID')
# Method 3: conv2d_backprop_input
d_x_backprop_input = tf.nn.conv2d_backprop_input(input_sizes=x_shape,
filter=w,
out_backprop=d_out,
strides=strides,
padding='VALID')
# Method 4: conv2d_transpose
d_x_transpose = tf.nn.conv2d_transpose(value=d_out,
filter=w,
output_shape=x_shape,
strides=strides,
padding='VALID')
以 3 种不同的方式计算 d_w
:
# Method 1: TF's autodiff
d_w = tf.gradients(f, w)[0]
# Method 2: manually using conv2d
d_w_manual = tf_NHWC_to_HWIO(tf.nn.conv2d(input=x,
filter=tf_NHWC_to_HWIO(d_out),
strides=strides,
padding='VALID'))
# Method 3: conv2d_backprop_filter
d_w_backprop_filter = tf.nn.conv2d_backprop_filter(input=x,
filter_sizes=w_shape,
out_backprop=d_out,
strides=strides,
padding='VALID')
tf_rot180
、tf_pad_to_full_conv2d
、tf_NHWC_to_HWIO
的实现请参见full scripts。在脚本中,我们检查不同方法的最终输出值是否相同;也可以使用 numpy 实现。
conv2d_transpose() 简单地转置权重并将它们翻转 180 度。然后它应用标准的 conv2d()。 "Transposes" 实际上意味着它改变了权重张量中 "columns" 的顺序。请检查下面的示例。
这里有一个使用 stride=1 和 padding='SAME' 的卷积的例子。这是一个简单的案例,但同样的推理可以应用于其他案例。
假设我们有:
- 输入:28x28x1 的 MNIST 图像,形状 = [28,28,1]
- 卷积层:7x7 的 32 个过滤器,权重形状 = [7, 7, 1, 32],名称 = W_conv1
如果我们对输入执行卷积,那么激活的形状将是:[1,28,28,32]。
activations = sess.run(h_conv1,feed_dict={x:np.reshape(image,[1,784])})
其中:
W_conv1 = weight_variable([7, 7, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = conv2d(x, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1
要获得 "deconvolution" 或 "transposed convolution",我们可以使用 conv2d_transpose() 这样的卷积激活:
deconv = conv2d_transpose(activations,W_conv1, output_shape=[1,28,28,1],padding='SAME')
或者使用 conv2d() 我们需要转置和翻转权重:
transposed_weights = tf.transpose(W_conv1, perm=[0, 1, 3, 2])
这里我们将"colums"的顺序从[0,1,2,3]改为[0,1,3,2]。所以从[7,7,1,32]我们将获得形状为 [7,7,32,1] 的张量。然后我们翻转权重:
for i in range(n_filters):
# Flip the weights by 180 degrees
transposed_and_flipped_weights[:,:,i,0] = sess.run(tf.reverse(transposed_weights[:,:,i,0], axis=[0, 1]))
然后我们可以用 conv2d() 计算卷积为:
strides = [1,1,1,1]
deconv = conv2d(activations,transposed_and_flipped_weights,strides=strides,padding='SAME')
我们会得到和之前一样的结果。使用 conv2d_backprop_input() 也可以获得完全相同的结果:
deconv = conv2d_backprop_input([1,28,28,1],W_conv1,activations, strides=strides, padding='SAME')
结果显示在这里:
Test of the conv2d(), conv2d_tranposed() and conv2d_backprop_input()
我们可以看到结果是一样的。要以更好的方式查看它,请查看我的代码:
https://github.com/simo23/conv2d_transpose
在这里,我使用标准 conv2d() 复制了 conv2d_transpose() 函数的输出。
conv2d_transpose 的一个应用程序是升级,下面是一个解释其工作原理的示例:
a = np.array([[0, 0, 1.5],
[0, 1, 0],
[0, 0, 0]]).reshape(1,3,3,1)
filt = np.array([[1, 2],
[3, 4.0]]).reshape(2,2,1,1)
b = tf.nn.conv2d_transpose(a,
filt,
output_shape=[1,6,6,1],
strides=[1,2,2,1],
padding='SAME')
print(tf.squeeze(b))
tf.Tensor(
[[0. 0. 0. 0. 1.5 3. ]
[0. 0. 0. 0. 4.5 6. ]
[0. 0. 1. 2. 0. 0. ]
[0. 0. 3. 4. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]], shape=(6, 6), dtype=float64)
这是对 U-Net
中使用的特殊情况的简单解释 - 这是转置卷积的主要用例之一。
我们对以下图层感兴趣:
Conv2DTranspose(64, (2, 2), strides=(2, 2))
这一层具体做什么?我们可以复制它的作品吗?
这是答案:
- 首先,本例中的默认填充是有效的。这意味着我们没有填充。
- 输出的大小将增加2倍:如果输入(m, n),输出将是(2m, 2n)。这是为什么?看下一点。
- 从输入中取出第一个元素并乘以形状为 (2,2) 的滤波器权重。把它放到输出中。取下一个元素,相乘并在不重叠的情况下将输出放在第一个结果旁边。这是为什么?我们的步幅为 (2, 2)。
这是一个输入和输出示例(查看详细信息here and here):
In [15]: X.reshape(n, m)
Out[15]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [16]: y_resh
Out[16]:
array([[ 0., 0., 1., 1., 2., 2., 3., 3., 4., 4.],
[ 0., 0., 1., 1., 2., 2., 3., 3., 4., 4.],
[ 5., 5., 6., 6., 7., 7., 8., 8., 9., 9.],
[ 5., 5., 6., 6., 7., 7., 8., 8., 9., 9.],
[10., 10., 11., 11., 12., 12., 13., 13., 14., 14.],
[10., 10., 11., 11., 12., 12., 13., 13., 14., 14.]], dtype=float32)
斯坦福大学 cs231n 的这张幻灯片对我们的问题很有用:
任何线性变换,包括卷积,都可以表示为矩阵。转置卷积可以解释为在应用之前转置卷积矩阵。例如,考虑核大小为 3、步幅为 2 的简单一维卷积。
如果我们转置卷积矩阵并将其应用于 3 元素向量,我们将得到转置卷积运算
乍一看,这看起来不再像卷积运算了。但是如果我们首先在 y 向量中插入一些零,我们可以将其等价地重写为
此示例演示了步幅卷积运算符的转置等效于通过插入零、然后添加一些额外的填充、最后执行无步幅(即步幅 = 1)卷积按步幅因子进行上采样。
对于更高维度的转置卷积,相同的 upsampling-by-inserting-zeros 方法在执行无跨度卷积之前应用于每个维度。
conv2d_transpose()
操作的文档没有清楚地解释它的作用:
The transpose of conv2d.
This operation is sometimes called "deconvolution" after Deconvolutional Networks, but is actually the transpose (gradient) of conv2d rather than an actual deconvolution.
我查看了文档指向的论文,但没有帮助。
此操作的作用是什么?您为什么要使用它?
这是我在网上看到的关于卷积转置如何工作的最好解释here。
我将给出我自己的简短描述。它应用小数步长的卷积。换句话说,分隔输入值(带零)以将过滤器应用于可能小于过滤器大小的区域。
至于为什么要使用它。它可以用作一种具有学习权重的上采样,而不是双线性插值或其他一些固定形式的上采样。
这是 "gradients" 的另一个观点,即为什么 TensorFlow 文档说 conv2d_transpose()
是 "actually the transpose (gradient) of conv2d rather than an actual deconvolution"。 有关 conv2d_transpose
中实际计算的更多详细信息,我强烈推荐 this article,从第 19 页开始。
四个相关函数
在tf.nn
中,有4个密切相关但比较容易混淆的2d卷积函数:
tf.nn.conv2d
tf.nn.conv2d_backprop_filter
tf.nn.conv2d_backprop_input
tf.nn.conv2d_transpose
一句话总结:都是2d卷积。它们的区别在于它们的输入参数排序、输入旋转或转置、步幅(包括小数步幅大小)、填充等。有了 tf.nn.conv2d
,可以通过转换输入和更改conv2d
个参数。
问题设置
- 正向和反向计算:
# forward
out = conv2d(x, w)
# backward, given d_out
=> find d_x?
=> find d_w?
在正向计算中,我们计算输入图像x
与过滤器w
的卷积,结果为out
。
在反向计算中,假设我们得到 d_out
,即梯度 w.r.t。 out
。我们的目标是找到d_x
和d_w
,也就是梯度w.r.t。 x
和 w
分别。
为了便于讨论,我们假设:
- 所有步幅为
1
- 所有
in_channels
和out_channels
都是1
- 使用
VALID
填充 - 奇数过滤器大小,这避免了一些不对称的形状问题
简答
从概念上讲,根据上述假设,我们有以下关系:
out = conv2d(x, w, padding='VALID')
d_x = conv2d(d_out, rot180(w), padding='FULL')
d_w = conv2d(x, d_out, padding='VALID')
其中rot180
是旋转180度的二维矩阵(左右翻转和自上而下翻转),FULL
表示"apply filter wherever it partly overlaps with the input"(参见theano docs ).请注意,这仅在上述假设下有效,但是,可以更改 conv2d 参数以对其进行概括。
要点:
- 输入梯度
d_x
是输出梯度d_out
和权重w
的卷积,有一些修改。 - 权重梯度
d_w
是输入x
和输出梯度d_out
的卷积,有一些修改。
长答案
现在,让我们给出一个实际工作的代码示例,说明如何使用上述 4 个函数来计算给定 d_out
的 d_x
和 d_w
。这表明如何
conv2d
,
conv2d_backprop_filter
,
conv2d_backprop_input
,以及
conv2d_transpose
相互关联。
Please find the full scripts here。
以 4 种不同的方式计算 d_x
:
# Method 1: TF's autodiff
d_x = tf.gradients(f, x)[0]
# Method 2: manually using conv2d
d_x_manual = tf.nn.conv2d(input=tf_pad_to_full_conv2d(d_out, w_size),
filter=tf_rot180(w),
strides=strides,
padding='VALID')
# Method 3: conv2d_backprop_input
d_x_backprop_input = tf.nn.conv2d_backprop_input(input_sizes=x_shape,
filter=w,
out_backprop=d_out,
strides=strides,
padding='VALID')
# Method 4: conv2d_transpose
d_x_transpose = tf.nn.conv2d_transpose(value=d_out,
filter=w,
output_shape=x_shape,
strides=strides,
padding='VALID')
以 3 种不同的方式计算 d_w
:
# Method 1: TF's autodiff
d_w = tf.gradients(f, w)[0]
# Method 2: manually using conv2d
d_w_manual = tf_NHWC_to_HWIO(tf.nn.conv2d(input=x,
filter=tf_NHWC_to_HWIO(d_out),
strides=strides,
padding='VALID'))
# Method 3: conv2d_backprop_filter
d_w_backprop_filter = tf.nn.conv2d_backprop_filter(input=x,
filter_sizes=w_shape,
out_backprop=d_out,
strides=strides,
padding='VALID')
tf_rot180
、tf_pad_to_full_conv2d
、tf_NHWC_to_HWIO
的实现请参见full scripts。在脚本中,我们检查不同方法的最终输出值是否相同;也可以使用 numpy 实现。
conv2d_transpose() 简单地转置权重并将它们翻转 180 度。然后它应用标准的 conv2d()。 "Transposes" 实际上意味着它改变了权重张量中 "columns" 的顺序。请检查下面的示例。
这里有一个使用 stride=1 和 padding='SAME' 的卷积的例子。这是一个简单的案例,但同样的推理可以应用于其他案例。
假设我们有:
- 输入:28x28x1 的 MNIST 图像,形状 = [28,28,1]
- 卷积层:7x7 的 32 个过滤器,权重形状 = [7, 7, 1, 32],名称 = W_conv1
如果我们对输入执行卷积,那么激活的形状将是:[1,28,28,32]。
activations = sess.run(h_conv1,feed_dict={x:np.reshape(image,[1,784])})
其中:
W_conv1 = weight_variable([7, 7, 1, 32])
b_conv1 = bias_variable([32])
h_conv1 = conv2d(x, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1
要获得 "deconvolution" 或 "transposed convolution",我们可以使用 conv2d_transpose() 这样的卷积激活:
deconv = conv2d_transpose(activations,W_conv1, output_shape=[1,28,28,1],padding='SAME')
或者使用 conv2d() 我们需要转置和翻转权重:
transposed_weights = tf.transpose(W_conv1, perm=[0, 1, 3, 2])
这里我们将"colums"的顺序从[0,1,2,3]改为[0,1,3,2]。所以从[7,7,1,32]我们将获得形状为 [7,7,32,1] 的张量。然后我们翻转权重:
for i in range(n_filters):
# Flip the weights by 180 degrees
transposed_and_flipped_weights[:,:,i,0] = sess.run(tf.reverse(transposed_weights[:,:,i,0], axis=[0, 1]))
然后我们可以用 conv2d() 计算卷积为:
strides = [1,1,1,1]
deconv = conv2d(activations,transposed_and_flipped_weights,strides=strides,padding='SAME')
我们会得到和之前一样的结果。使用 conv2d_backprop_input() 也可以获得完全相同的结果:
deconv = conv2d_backprop_input([1,28,28,1],W_conv1,activations, strides=strides, padding='SAME')
结果显示在这里:
Test of the conv2d(), conv2d_tranposed() and conv2d_backprop_input()
我们可以看到结果是一样的。要以更好的方式查看它,请查看我的代码:
https://github.com/simo23/conv2d_transpose
在这里,我使用标准 conv2d() 复制了 conv2d_transpose() 函数的输出。
conv2d_transpose 的一个应用程序是升级,下面是一个解释其工作原理的示例:
a = np.array([[0, 0, 1.5],
[0, 1, 0],
[0, 0, 0]]).reshape(1,3,3,1)
filt = np.array([[1, 2],
[3, 4.0]]).reshape(2,2,1,1)
b = tf.nn.conv2d_transpose(a,
filt,
output_shape=[1,6,6,1],
strides=[1,2,2,1],
padding='SAME')
print(tf.squeeze(b))
tf.Tensor(
[[0. 0. 0. 0. 1.5 3. ]
[0. 0. 0. 0. 4.5 6. ]
[0. 0. 1. 2. 0. 0. ]
[0. 0. 3. 4. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0. ]], shape=(6, 6), dtype=float64)
这是对 U-Net
中使用的特殊情况的简单解释 - 这是转置卷积的主要用例之一。
我们对以下图层感兴趣:
Conv2DTranspose(64, (2, 2), strides=(2, 2))
这一层具体做什么?我们可以复制它的作品吗?
这是答案:
- 首先,本例中的默认填充是有效的。这意味着我们没有填充。
- 输出的大小将增加2倍:如果输入(m, n),输出将是(2m, 2n)。这是为什么?看下一点。
- 从输入中取出第一个元素并乘以形状为 (2,2) 的滤波器权重。把它放到输出中。取下一个元素,相乘并在不重叠的情况下将输出放在第一个结果旁边。这是为什么?我们的步幅为 (2, 2)。
这是一个输入和输出示例(查看详细信息here and here):
In [15]: X.reshape(n, m)
Out[15]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [16]: y_resh
Out[16]:
array([[ 0., 0., 1., 1., 2., 2., 3., 3., 4., 4.],
[ 0., 0., 1., 1., 2., 2., 3., 3., 4., 4.],
[ 5., 5., 6., 6., 7., 7., 8., 8., 9., 9.],
[ 5., 5., 6., 6., 7., 7., 8., 8., 9., 9.],
[10., 10., 11., 11., 12., 12., 13., 13., 14., 14.],
[10., 10., 11., 11., 12., 12., 13., 13., 14., 14.]], dtype=float32)
斯坦福大学 cs231n 的这张幻灯片对我们的问题很有用:
任何线性变换,包括卷积,都可以表示为矩阵。转置卷积可以解释为在应用之前转置卷积矩阵。例如,考虑核大小为 3、步幅为 2 的简单一维卷积。
如果我们转置卷积矩阵并将其应用于 3 元素向量,我们将得到转置卷积运算
乍一看,这看起来不再像卷积运算了。但是如果我们首先在 y 向量中插入一些零,我们可以将其等价地重写为
此示例演示了步幅卷积运算符的转置等效于通过插入零、然后添加一些额外的填充、最后执行无步幅(即步幅 = 1)卷积按步幅因子进行上采样。
对于更高维度的转置卷积,相同的 upsampling-by-inserting-zeros 方法在执行无跨度卷积之前应用于每个维度。