【问题标题】:Keras/Tensorflow - fourier pointwise multiplication implementation of conv2d running 4x slower than spatial convolutionKeras/Tensorflow - conv2d 的傅里叶逐点乘法实现比空间卷积慢 4 倍
【发布时间】:2019-04-01 10:36:59
【问题描述】:

根据卷积定理,卷积在傅立叶域中变为逐点乘法,并且由于在许多先前的工作中将卷积运算转换为逐点乘法运算,已证明进行傅立叶变换的开销被增益所掩盖像下面这样 - https://arxiv.org/abs/1312.5851.

为了复制这一点,我试图用一个接受输入数据 rfft 的自定义层替换 keras.layers.Conv2D() 层(我在将数据输入模型之前采用了 rfft 以减少训练时间) ,初始化与图像大小相同的'no_of_kernels'数量的内核,获取其rfft,将输入和内核逐点相乘并返回乘积(是的,不使用irfft,因为我想在傅立叶域本身中进一步训练网络) -

在层中,调用函数实现如下—— 注意 - 在我的数据集中,即 MNIST 图像高度 = 宽度,因此转置工作正常

def call(self, x):
        fft_x = x #(batch_size, height, width, in_channels)
        fft_kernel = tf.spectral.rfft2d(self.kernel) #(in_channels, height, width, out_channels)
        fft_kernel = tf.transpose(fft_kernel, perm=[2, 1, 0, 3]) #(width, height, in_channels, out_channels)
        output  = tf.einsum('ijkl,jklo->ijko', fft_x, fft_kernel)
        return output 

这段代码保留了 Keras Conv2D 层给出的精度,但它的运行速度比 Conv2D 慢了大约 4 倍,因此转换为傅立叶域的目的就落空了。谁能解释一下为什么会发生这种情况以及如何在傅立叶域中复制快速卷积的结果?

(注意 - 对于任何可能觉得 tf.spectral.rfft2d(self.kernel) 可能是开销的人来说,事实并非如此。

另外,我认为 Conv2D 函数可能会展平 4D 输入张量和内核,以将其减少为矩阵乘法,如此处所述 - enter link description here。除了像我对 tf.einsum 所做的那样将其视为点积之外,我想不出任何智能的展平等方法来执行逐点乘法。有没有智能的方法来做逐点乘法? ) 谢谢。

编辑 - 层的完整实现供参考-

class Fourier_Conv2D(Layer):
    def __init__(self, no_of_kernels, **kwargs):
        self.no_of_kernels = no_of_kernels
        super(Fourier_Conv2D, self).__init__(**kwargs)

    def build(self, input_shape):
        self.kernel_shape = (int(input_shape[3]), int(input_shape[1]), int(input_shape[2]), self.no_of_kernels)
        self.kernel = self.add_weight(name = 'kernel', 
                                      shape = self.kernel_shape, 
                                      initializer = 'uniform', trainable = True)
        super(Fourier_Conv2D, self).build(input_shape)

    def call(self, x):
        fft_x = x
        fft_kernel = tf.spectral.rfft2d(self.kernel)
        fft_kernel = tf.transpose(fft_kernel, perm=[2, 1, 0, 3])
        output  = tf.einsum('ijkl,jklo->ijko', fft_x, fft_kernel)
        return output       

    def compute_output_shape(self, input_shape):
        return (input_shape[0], input_shape[1], input_shape[2], int(self.no_of_kernels/2)+1)

【问题讨论】:

  • 在 Conv2D 层中,我使用了 3x3 内核。在fourier_conv2D 中,我初始化了一个与图像大小相同的内核。由于这增加了可学习参数的数量,我还尝试初始化一个 3x3 内核并将其填充到图像大小,然后再使用 fft.但这也和前者一样慢,说明瓶颈不在内核的学习阶段,而是在逐点乘法部分。我还看到fourier_conv2d 可能仅在大内核的情况下比conv2d 显着加速,但它至少应该在与conv2d 相当的时间上运行,而不是这么慢。
  • "它至少应该在时间上与 conv2d 相媲美" 为什么? conv2d 每个像素执行 3x3=9 次乘法和加法 (MAD)。一个 FFT 需要的远不止这些。你需要做 2 次 FFT,然后仍然将两个结果相乘。请注意,9 个 MAD 在现代 CPU 上花费的时间比将图像数据从 RAM 获取到 CPU 并将结果写回 RAM 的时间要少。使用 FFT 路径,您可以获得更多数据,即使您只计算一半的频域,因为您的内核现在变成了与图像本身一样多的数据。而且您需要对图像进行两次迭代,而不仅仅是一次。
  • 另外,在您链接的论文的摘要中,它说“同时多次重复使用相同的转换特征图”。也就是说,他们计算所有输入图像的 FFT,然后在转换后的数据上训练他们的网络。他们将 FFT 的成本排除在外。现在它们从每像素 9 个 MAD 变为每像素 1 次乘法。我必须阅读论文才能了解他们如何处理内核权重,似乎他们会有更多。
  • 通过两次迭代图像,您的意思是一次用于 fft,一次用于逐点乘法?关于图像/输入数据的 fft,我在开始训练之前已经获取了整个数据集的 fft,所以这里不计算时间。在内核的 fft 中,我还尝试过直接初始化 complex64 内核,而不是初始化真正的内核,然后再使用 fft,因此也消除了这种开销。但它仍然像这种情况一样慢。所以它让我觉得开销真的在逐点乘法部分,这也是复数的乘法
  • 您是否尝试删除转置?没有它你应该可以使用einsumijkl,ljko->ijko。虽然tf.einsum 很可能只是很慢。

标签: python tensorflow keras fft conv-neural-network


【解决方案1】:

我认为您的结果一点也不令人惊讶,Keras 中 Conv2D 的实现留给了后端,并且大多数后端(如 TensorFlow)都有非常优化的卷积操作版本,特别是如果您使用 CuDNN。因此,您自己的版本应该比简单的实现更快,但比高度优化的版本慢。

为了进行有意义的比较,您可能必须实现一个基线 Conv2D,它以一种简单的方式进行卷积,没有任何类型的优化。

【讨论】:

  • 是的,谢谢。我将尝试实现一个基本的 conv2D 空间实现,然后进行比较。但我没有使用 CuDNN,所以这两个代码都在 CPU 上运行。 Tensorflow 后端是否也在 CPU 上进行了优化?
  • 是的,它也应该被优化,可能使用 im2col 和 BLAS。
  • 好的,谢谢。我读过 Tensorflow 使用 Eigen 来优化 CPU 操作。那么我是否需要使用 Eigen 等编写自定义操作以使fourier_conv2d 与conv2d 相比/更快?
猜你喜欢
  • 1970-01-01
  • 2020-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-19
相关资源
最近更新 更多