改进内存消耗和速度的移动窗口计算

时间:2015-01-02 20:19:08

标签: python performance numpy

在此移动窗口计算中是否有可能获得更好的性能(内存消耗和速度)?我有一个1000x1000 numpy数组,我在整个数组中取16x16个窗口,最后将一些函数应用到每个窗口(在本例中为离散余弦变换。)

import numpy as np
from scipy.fftpack import dct
from skimage.util import view_as_windows

X = np.arange(1000*1000, dtype=np.float32).reshape(1000,1000)
window_size = 16
windows = view_as_windows(X, (window_size,window_size))
dcts = np.zeros(windows.reshape(-1,window_size, window_size).shape, dtype=np.float32)
for idx, window in enumerate(windows.reshape(-1,window_size, window_size)):
    dcts[idx, :, :] = dct(window)
dcts = dcts.reshape(windows.shape)

此代码占用太多内存(在上面的示例中,内存消耗不是很严重 - windows使用1Gb而dcts也需要1Gb)并且需要25秒才能完成。我有点不确定我做错了什么,因为这应该是一个简单的计算(例如过滤图像。)有没有更好的方法来实现这个目标?

更新

我最初担心Kington的解决方案和我的初始方法生成的数组非常不同,但差异仅限于边界,因此不太可能对大多数应用程序造成严重问题。唯一剩下的问题是两种解决方案都很慢。目前,第一种解决方案需要1分10秒,第二种解决方案需要59秒。

更新2:

我注意到目前为止最大的罪魁祸首是dctnp.mean。即使generic_filter使用“{cymonized”版本的mean并且瓶颈也能正常执行(8.6秒):

import bottleneck as bp
def func(window, shape):
    window = window.reshape(shape)
    #return np.abs(dct(dct(window, axis=1), axis=0)).mean()
    return bp.nanmean(dct(window))

result = scipy.ndimage.generic_filter(X, func, (16, 16),
                                      extra_arguments=([16, 16],))

我目前正在阅读如何使用numpy包装C代码以替换scipy.fftpack.dct。如果有人知道该怎么做,我将不胜感激。

2 个答案:

答案 0 :(得分:3)

skimage.util.view_as_windows使用跨步技巧制作一系列重叠的“窗口”,不使用任何额外的内存。

但是,当您创建一个形状形状的新数组时,它将需要原始X数组或windows数组使用的内存大约32倍(16 x 16)。

根据您的评论,您的最终结果是dcts.reshape(windows.shape).mean(axis=2).mean(axis=2) - 取每个窗口dct的平均值。

因此,在循环中取平均值并且不存储巨大的中间窗口数组会更有内存效率(尽管性能相似):

import numpy as np
from scipy.fftpack import dct
from skimage.util import view_as_windows

X = np.arange(1000*1000, dtype=np.float32).reshape(1000,1000)
window_size = 16
windows = view_as_windows(X, (window_size, window_size))
dcts = np.zeros(windows.shape[:2], dtype=np.float32).ravel()
for idx, window in enumerate(windows.reshape(-1, window_size, window_size)):
    dcts[idx] = dct(window).mean()
dcts = dcts.reshape(windows.shape[:2])

另一个选项是scipy.ndimage.generic_filter。它不会提高性能(瓶颈是内循环中的python函数调用),但是你将拥有更多的边界条件选项,并且它将具有相当的内存效率:

import numpy as np
from scipy.fftpack import dct
import scipy.ndimage

X = np.arange(1000*1000, dtype=np.float32).reshape(1000,1000)

def func(window, shape):
    window = window.reshape(shape)
    return dct(window).mean()

result = scipy.ndimage.generic_filter(X, func, (16, 16),
                                      extra_arguments=([16, 16],))

答案 1 :(得分:3)

由于scipy.fftpack.dct计算沿输入数组最后一个轴的单独变换,您可以用以下代码替换循环:

windows = view_as_windows(X, (window_size,window_size))
dcts = dct(windows)
result1 = dcts.mean(axis=(2,3))

现在只有dcts数组需要大量内存,windows仅仅是X的视图。而且因为DCT是用单个函数调用计算的,所以它也快得多。但是,由于窗口重叠,因此需要进行大量重复计算。这可以通过仅计算每个子行的DCT一次,然后是窗口均值来克服:

ws = window_size
row_dcts = dct(view_as_windows(X, (1, ws)))
cs = row_dcts.squeeze().sum(axis=-1).cumsum(axis=0)
result2 = np.vstack((cs[ws-1], cs[ws:]-cs[:-ws])) / ws**2

虽然在代码清晰度方面似乎失去了效率所获得的......但基本上这里的方法是首先计算DCT,然后通过在2D窗口上求和然后除以得到窗口平均值窗口中的元素数量。 DCT已经在行方向移动窗口上计算,因此我们对这些窗口进行定期求和。但是,我们需要对列进行移动窗口总和,以得到正确的2D窗口总和。为了有效地做到这一点,我们使用cumsum技巧,其中:

sum(A[p:q])  # q-p == window_size

相当于:

cs = cumsum(A)
cs[q-1] - cs[p-1]

这避免了必须一遍又一遍地对完全相同的数字求和。不幸的是,它不适用于第一个窗口(当p == 0时),因此我们只需要cs[q-1]并将其与其他窗口总和一起堆叠。最后,我们除以元素的数量,得到2D平均窗口。

如果你喜欢2D DCT,那么第二种方法就不那么有趣了,因为你最终需要完整的985 x 985 x 16 x 16数组才能获得平均值。


上述两种方法都应该是等效的,但使用64位浮点数执行算法可能是个好主意:

np.allclose(result1, result2, atol=1e-6)
# False
np.allclose(result1, result2, atol=1e-5)
# True