为numpy 3D数组加速卷积循环?

Posted

技术标签:

【中文标题】为numpy 3D数组加速卷积循环?【英文标题】:Speed up for loop in convolution for numpy 3D array? 【发布时间】:2015-11-08 19:50:01 【问题描述】:

沿 3d numpy 数组的 Z 向量执行卷积,然后对结果进行其他操作,但它现在实现起来很慢。是 for 循环在这里减慢了我的速度还是卷积?我尝试整形为一维向量并在 1 次通过中执行卷积(就像我在 Matlab 中所做的那样),没有 for 循环,但它并没有提高性能。我的 Matlab 版本比我在 Python 中想出的任何东西都要快 50%。相关代码段:

convolved=np.zeros((y_lines,x_lines,z_depth))
for i in range(0, y_lines):
    for j in range(0, x_lines):
        convolved[i,j,:]= fftconvolve(data[i,j,:], Gauss) #80% of time here
        result[i,j,:]= other_calculations(convolved[i,j,:]) #20% of time here

有比 for 循环更好的方法吗?听说过 Cython,但我目前在 Python 方面的经验有限,会以最简单的解决方案为目标。

【问题讨论】:

什么是Gauss?某种一维高斯核?如果是这样,相对于z_depth 的大小是多少? 高斯核在循环之前生成一次。数据是一维向量(z_depth),通常长度约为 1535 个元素,长度通常为 79 的一维高斯核。我在 fftconvolve 中清理了一堆开销,基本上只是直接进入 irfftn(rfftn(in1, fshape) * rfftn(in2 , fshape), fshape)[fslice].copy() 【参考方案1】:

您使用的fftconvolve 函数大概来自SciPy。如果是这样,请注意它需要 N 维数组。因此,一种更快的卷积方法是生成 3d 内核,对应于在 xy 维度中什么都不做,并在 z. 中进行 1d 高斯卷积

一些代码和计时结果如下。在我的机器上和一些玩具数据,这导致了 10 倍的加速,如您所见:

import numpy as np
from scipy.signal import fftconvolve
from scipy.ndimage.filters import gaussian_filter

# use scipy filtering functions designed to apply kernels to isolate a 1d gaussian kernel
kernel_base = np.ones(shape=(5))
kernel_1d = gaussian_filter(kernel_base, sigma=1, mode='constant')
kernel_1d = kernel_1d / np.sum(kernel_1d)

# make the 3d kernel that does gaussian convolution in z axis only
kernel_3d = np.zeros(shape=(1, 1, 5,))
kernel_3d[0, 0, :] = kernel_1d

# generate random data
data = np.random.random(size=(50, 50, 50))

# define a function for loop based convolution for easy timeit invocation
def convolve_with_loops(data):
    nx, ny, nz = data.shape
    convolved=np.zeros((nx, ny, nz))
    for i in range(0, nx):
        for j in range(0, ny):
            convolved[i,j,:]= fftconvolve(data[i, j, :], kernel_1d, mode='same') 
    return convolved

# compute the convolution two diff. ways: with loops (first) or as a 3d convolution (2nd)
convolved = convolve_with_loops(data)
convolved_2 = fftconvolve(data, kernel_3d, mode='same')

# raise an error unless the two computations return equivalent results
assert np.all(np.isclose(convolved, convolved_2))

# time the two routes of the computation
%timeit convolved = convolve_with_loops(data)
%timeit convolved_2 = fftconvolve(data, kernel_3d, mode='same')

timeit 结果:

10 loops, best of 3: 198 ms per loop
100 loops, best of 3: 18.1 ms per loop

【讨论】:

尝试生成长度为 64 的数据,看看这是否会使其更快。 FFT 通常在 2 次方上效率更高。 实现了一个 3D 版本,它比我的慢:时间卷积:5851.7 毫秒(新 3D 版本)时间卷积:4093.4 毫秒(旧版本) 每个人的数据的确切形状是什么? 3-D fftconvolve 对我来说在 256 x 256 x 256 大小的数组中保持更快,尽管比我上面发布的代码的 10 倍略低于 2 倍。 数据为(最小尺寸)200x200x1535,高斯为1x79。正如我所提到的,我从 fftconvolve(if 语句等)中删除了所有开销,因此它直接进入卷积,在未修改的 fftconvolve 中节省了大约 30% 的时间。【参考方案2】:

我想你已经找到fftconvolve函数的source code了。通常,对于实际输入,它使用计算 N 维变换的 numpy.fft.rfftn.irfftn 函数。由于目标是进行多次一维转换,因此您基本上可以像这样(简化)重写fftconvolve

from scipy.signal.signaltools import _next_regular

def fftconvolve_1d(in1, in2):

    outlen = in1.shape[-1] + in2.shape[-1] - 1
    n = _next_regular(outlen)

    tr1 = np.fft.rfft(in1, n)
    tr2 = np.fft.rfft(in2, n)
    out = np.fft.irfft(tr1 * tr2, n)

    return out[..., :outlen].copy()

并计算出想要的结果:

result = fftconvolve_1d(data, Gauss)

这是因为numpy.fft.rfft.irfft(注意名称中缺少n)在输入数组的单个轴(默认情况下为最后一个轴)上变换。这比我系统上的 OP 代码快大约 40%。


使用不同的 FFT 后端可以进一步加快速度。

一方面,scipy.fftpack 中的函数似乎比它们的 Numpy 等效函数要快一些。然而,Scipy 变体的输出格式非常尴尬(参见docs),这使得乘法变得困难。

另一个可能的后端是通过 pyFFTW 包装器实现的 FFTW。缺点是转换之前有一个缓慢的“规划阶段”,并且输入必须是 16 字节对齐才能获得最佳性能。这在pyFFTW tutorial 中有很好的解释。生成的代码可能是例如:

from scipy.signal.signaltools import _next_regular
import pyfftw
pyfftw.interfaces.cache.enable()  # Cache for the "planning"
pyfftw.interfaces.cache.set_keepalive_time(1.0)

def fftwconvolve_1d(in1, in2):

    outlen = in1.shape[-1] + in2.shape[-1] - 1
    n = _next_regular(outlen)

    tr1 = pyfftw.interfaces.numpy_fft.rfft(in1, n)
    tr2 = pyfftw.interfaces.numpy_fft.rfft(in2, n)

    sh = np.broadcast(tr1, tr2).shape
    dt = np.common_type(tr1, tr2)
    pr = pyfftw.n_byte_align_empty(sh, 16, dt)
    np.multiply(tr1, tr2, out=pr)
    out = pyfftw.interfaces.numpy_fft.irfft(pr, n)

    return out[..., :outlen].copy()

使用对齐的输入和缓存的“计划”,我发现 OP 中的代码速度提高了近 3 倍。通过查看 Numpy 数组的 ctypes.data 属性中的内存地址来对齐内存对齐 can be easily checked。

【讨论】:

将 rfftn 替换为 rfft 将性能提高了约 30%。但是 pyfftw 方法没有帮助: pyFFTW: 6.3 sec numpy rfft: 4.6 sec pyFFTW: 86.1 sec numpy rfft: 62.4 sec

以上是关于为numpy 3D数组加速卷积循环?的主要内容,如果未能解决你的问题,请参考以下文章

Numpy 沿轴卷积 2 个二维数组

奉献pytorch 搭建 CNN 卷积神经网络训练图像识别的模型,配合numpy 和matplotlib 一起使用调用 cuda GPU进行加速训练

使用 scipy.signal 在 Python 中进行卷积和反卷积

不同维度的python卷积

More is Less——卷积网络加速

Convolve2d 只需使用 Numpy