tf.nn.conv2d 在 tensorflow 中做了啥?

Posted

技术标签:

【中文标题】tf.nn.conv2d 在 tensorflow 中做了啥?【英文标题】:What does tf.nn.conv2d do in tensorflow?tf.nn.conv2d 在 tensorflow 中做了什么? 【发布时间】:2016-04-09 17:16:51 【问题描述】:

我正在查看有关 tf.nn.conv2d here 的 tensorflow 文档。但我无法理解它的作用或它试图实现的目标。它在文档上说,

#1 : 将过滤器展平为具有形状的二维矩阵

[filter_height * filter_width * in_channels, output_channels].

现在这是做什么的?那是逐元素乘法还是只是简单的矩阵乘法?我也无法理解文档中提到的其他两点。我把它们写在下面:

#2:从输入张量中提取图像块,形成形状的虚拟张量

[batch, out_height, out_width, filter_height * filter_width * in_channels].

# 3:对于每个补丁,将过滤器矩阵和图像补丁向量右乘。

如果有人能举个例子,一段代码(非常有帮助)并解释那里发生了什么以及为什么这样的操作,那将是非常有帮助的。

我尝试编写一小部分代码并打印出操作的形状。不过还是看不懂。

我尝试过这样的事情:

op = tf.shape(tf.nn.conv2d(tf.random_normal([1,10,10,10]), 
              tf.random_normal([2,10,10,10]), 
              strides=[1, 2, 2, 1], padding='SAME'))

with tf.Session() as sess:
    result = sess.run(op)
    print(result)

我了解卷积神经网络的点点滴滴。我研究了他们here。但是在 tensorflow 上的实现并不是我所期望的。所以它提出了这个问题。

编辑: 所以,我实现了一个更简单的代码。但我无法弄清楚发生了什么。我的意思是结果如何。如果有人能告诉我这个输出是什么过程产生的,那将非常有帮助。

input = tf.Variable(tf.random_normal([1,2,2,1]))
filter = tf.Variable(tf.random_normal([1,1,1,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')
init = tf.initialize_all_variables()
with tf.Session() as sess:
    sess.run(init)

    print("input")
    print(input.eval())
    print("filter")
    print(filter.eval())
    print("result")
    result = sess.run(op)
    print(result)

输出

input
[[[[ 1.60314465]
   [-0.55022103]]

  [[ 0.00595062]
   [-0.69889867]]]]
filter
[[[[-0.59594476]]]]
result
[[[[-0.95538563]
   [ 0.32790133]]

  [[-0.00354624]
   [ 0.41650501]]]]

【问题讨论】:

实际上在tf.nn.conv2d()的GPU上默认启用了cudnn,因此当我们使用支持GPU的TF时,根本不会使用该方法,除非明确指定use_cudnn_on_gpu=False 有一个关于统计数据交换的答案,我发现它非常有用:stats.stackexchange.com/a/454115/44735 【参考方案1】:

好的,我认为这是解释这一切的最简单方法。


您的示例是 1 张图片,尺寸为 2x2,带有 1 个通道。您有 1 个过滤器,大小为 1x1,和 1 个通道(大小为高度 x 宽度 x 通道 x 过滤器数量)。

对于这种简单的情况,生成的 2x2、1 通道图像(大小 1x2x2x1,图像数量 x 高 x 宽 x x 通道)是过滤器值乘以图像的每个像素的结果。


现在让我们尝试更多渠道:

input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([1,1,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

这里的 3x3 图像和 1x1 过滤器各有 5 个通道。生成的图像将为 3x3,具有 1 个通道(大小为 1x3x3x1),其中每个像素的值是过滤器通道与输入图像中相应像素的点积。


现在使用 3x3 过滤器

input = tf.Variable(tf.random_normal([1,3,3,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

这里我们得到一张 1x1 的图像,有 1 个通道(大小为 1x1x1x1)。该值是 9 个 5 元素点积的总和。但你可以称之为 45 元素的点积。


现在有了更大的图像

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='VALID')

输出是 3x3 1 通道图像(尺寸 1x3x3x1)。 这些值中的每一个都是 9 个 5 元素点积的总和。

每个输出都是通过将过滤器集中在输入图像的 9 个中心像素之一上来进行的,这样过滤器就不会突出。下面的xs 代表每个输出像素的过滤器中心。

.....
.xxx.
.xxx.
.xxx.
.....

现在使用“SAME”填充:

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,1]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

这给出了一个 5x5 的输出图像(尺寸 1x5x5x1)。这是通过将过滤器集中在图像上的每个位置来完成的。

过滤器伸出图像边缘的任何 5 元素点积的值都为零。

所以角点只是 4、5 元素点积的总和。


现在有多个过滤器。

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 1, 1, 1], padding='SAME')

这仍然提供 5x5 的输出图像,但有 7 个通道(大小为 1x5x5x7)。其中每个通道由集合中的一个过滤器产生。


现在步幅为 2,2:

input = tf.Variable(tf.random_normal([1,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

现在结果仍然有 7 个通道,但只有 3x3(尺寸 1x3x3x7)。

这是因为过滤器不是在图像上的每个点居中,而是在图像上的每个其他点居中,步长(步幅)宽度为 2。下面的x 代表过滤器中心对于输入图像上的每个输出像素。

x.x.x
.....
x.x.x
.....
x.x.x

当然,输入的第一个维度是图像的数量,因此您可以将其应用于一批 10 张图像,例如:

input = tf.Variable(tf.random_normal([10,5,5,5]))
filter = tf.Variable(tf.random_normal([3,3,5,7]))

op = tf.nn.conv2d(input, filter, strides=[1, 2, 2, 1], padding='SAME')

这对每个图像独立地执行相同的操作,得到一个包含 10 个图像的堆栈作为结果(大小 10x3x3x7)

【讨论】:

@ZijunLost 不,文档规定第一个和最后一个元素必须是1。Must have strides[0] = strides[3] = 1. For the most common case of the same horizontal and vertices strides, strides = [1, stride, stride, 1]. 这是基于Toeplitz matrix的卷积实现吗? 关于这个:“这仍然给出了一个 5x5 的输出图像,但是有 7 个通道(大小 1x5x5x7)。每个通道是由集合中的一个过滤器产生的。”,我仍然很难理解7个频道来自哪里?你是什​​么意思“集合中的过滤器”?谢谢。 @mdaoust 你好,关于你的第二个例子the 3x3 image and the 1x1 filter each have 5 channels,我发现结果与手动计算的点积不同。 @derek 我有同样的问题,“输出通道”是否与“过滤器数量”相同?如果是这样,为什么在 tensorflow 文档中将它们命名为“output_channel”?【参考方案2】:

2D 卷积的计算方式与计算 1D convolution 的方式类似:您将内核滑过输入,计算元素乘法并将它们相加。但是,您的内核/输入不是数组,而是矩阵。


在最基本的示例中,没有填充和 stride=1。假设您的 inputkernel 是:

当您使用内核时,您将收到以下输出:,其计算方式如下:

14 = 4 * 1 + 3 * 0 + 1 * 1 + 2 * 2 + 1 * 1 + 0 * 0 + 1 * 0 + 2 * 0 + 4 * 1 6 = 3 * 1 + 1 * 0 + 0 * 1 + 1 * 2 + 0 * 1 + 1 * 0 + 2 * 0 + 4 * 0 + 1 * 1 6 = 2 * 1 + 1 * 0 + 0 * 1 + 1 * 2 + 2 * 1 + 4 * 0 + 3 * 0 + 1 * 0 + 0 * 1 12 = 1 * 1 + 0 * 0 + 1 * 1 + 2 * 2 + 4 * 1 + 1 * 0 + 1 * 0 + 0 * 0 + 2 * 1

TF 的conv2d 函数分批计算卷积,并使用稍微不同的格式。对于输入,它是[batch, in_height, in_width, in_channels],对于内核,它是[filter_height, filter_width, in_channels, out_channels]。所以我们需要提供正确格式的数据:

import tensorflow as tf
k = tf.constant([
    [1, 0, 1],
    [2, 1, 0],
    [0, 0, 1]
], dtype=tf.float32, name='k')
i = tf.constant([
    [4, 3, 1, 0],
    [2, 1, 0, 1],
    [1, 2, 4, 1],
    [3, 1, 0, 2]
], dtype=tf.float32, name='i')
kernel = tf.reshape(k, [3, 3, 1, 1], name='kernel')
image  = tf.reshape(i, [1, 4, 4, 1], name='image')

之后卷积计算如下:

res = tf.squeeze(tf.nn.conv2d(image, kernel, [1, 1, 1, 1], "VALID"))
# VALID means no padding
with tf.Session() as sess:
   print sess.run(res)

并且将相当于我们手动计算的那个。


对于examples with padding/strides, take a look here。

【讨论】:

很好的例子,但是有些链接坏了。 @silgon 很遗憾,这是因为 SO 决定不支持他们最初创建和宣传的文档功能。【参考方案3】:

只是为了补充其他答案,您应该考虑

中的参数
filter = tf.Variable(tf.random_normal([3,3,5,7]))

'5' 对应于每个过滤器中的通道数。每个滤镜都是一个 3d 立方体,深度为 5。您的滤镜深度必须与输入图像的深度相对应。最后一个参数 7 应该被认为是批次中过滤器的数量。忘记这是 4D,而是想象您有一组或一批 7 个过滤器。您要做的是创建 7 个尺寸为 (3,3,5) 的过滤立方体。

在傅里叶域中可视化要容易得多,因为卷积变成了逐点乘法。对于尺寸为 (100,100,3) 的输入图像,您可以将过滤器尺寸重写为

filter = tf.Variable(tf.random_normal([100,100,3,7]))

为了获得 7 个输出特征图之一,我们简单地执行过滤立方体与图像立方体的逐点乘法,然后我们将结果在通道/深度维度(这里是 3)上相加,折叠到 2d (100,100) 特征图。对每个过滤立方体执行此操作,您将获得 7 个 2D 特征图。

【讨论】:

【参考方案4】:

我尝试实现 conv2d(用于我的学习)。好吧,我写的是:

def conv(ix, w):
   # filter shape: [filter_height, filter_width, in_channels, out_channels]
   # flatten filters
   filter_height = int(w.shape[0])
   filter_width = int(w.shape[1])
   in_channels = int(w.shape[2])
   out_channels = int(w.shape[3])
   ix_height = int(ix.shape[1])
   ix_width = int(ix.shape[2])
   ix_channels = int(ix.shape[3])
   filter_shape = [filter_height, filter_width, in_channels, out_channels]
   flat_w = tf.reshape(w, [filter_height * filter_width * in_channels, out_channels])
   patches = tf.extract_image_patches(
       ix,
       ksizes=[1, filter_height, filter_width, 1],
       strides=[1, 1, 1, 1],
       rates=[1, 1, 1, 1],
       padding='SAME'
   )
   patches_reshaped = tf.reshape(patches, [-1, ix_height, ix_width, filter_height * filter_width * ix_channels])
   feature_maps = []
   for i in range(out_channels):
       feature_map = tf.reduce_sum(tf.multiply(flat_w[:, i], patches_reshaped), axis=3, keep_dims=True)
       feature_maps.append(feature_map)
   features = tf.concat(feature_maps, axis=3)
   return features

希望我做得对。检查 MNIST,结果非常接近(但这种实现速度较慢)。希望对您有所帮助。

【讨论】:

【参考方案5】:

除了其他答案之外,conv2d 操作在 c++ (cpu) 或 cuda 中运行,用于需要以某种方式展平和重塑数据并使用 gemmBLAS 或 cuBLAS(cuda) 矩阵乘法的 gpu 机器。

【讨论】:

所以在内存中,卷积实际上是作为矩阵乘法执行的,这解释了为什么更大的图像不一定会运行更大的计算时间,而是更有可能遇到 OOM(超出内存)错误。你能向我解释为什么 3D 卷积与 2D 卷积相比内存效率更低/效率更高吗?例如,在 [B, H, W, D, C] 上进行 3D 转换,而在 [B*C, H, W, D] 上进行 2D 转换。当然,它们的计算成本相同吗?【参考方案6】:

当您尝试图像分类时,它会通过图片执行抽搐,因为函数具有执行此操作所需的所有参数。

当你基本上可以选择过滤尺寸。大步前进。填充。在使用它之前需要了解卷积的概念

【讨论】:

【参考方案7】:

此解释补充:

Keras Conv2d own filters

我对 keras.conv2d 中的 filter 参数有些怀疑,因为当我得知我应该设置自己的过滤器设计时。但是这个参数告诉我们要测试多少个过滤器,而 keras 本身会尝试找到最佳过滤器权重。

【讨论】:

以上是关于tf.nn.conv2d 在 tensorflow 中做了啥?的主要内容,如果未能解决你的问题,请参考以下文章

TensorFlow理解tf.nn.conv2d方法 ( 附代码详解注释 )

tensorflow中踩过的坑

tensorflow-tf.nn.conv2d卷积运算

tf.nn.conv2d

TF-卷积函数 tf.nn.conv2d 介绍

TF-卷积函数 tf.nn.conv2d 介绍