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_xd_w,也就是梯度w.r.t。 xw 分别。

为了便于讨论,我们假设:

  • 所有步幅为 1
  • 所有in_channelsout_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_outd_xd_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_rot180tf_pad_to_full_conv2dtf_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 方法在执行无跨度卷积之前应用于每个维度。