Python numpy中的二维跨步卷积

Python numpy中的二维跨步卷积,python,numpy,convolution,Python,Numpy,Convolution,我尝试使用for循环实现二维数组的跨步卷积,即 arr = np.array([[2,3,7,4,6,2,9], [6,6,9,8,7,4,3], [3,4,8,3,8,9,7], [7,8,3,6,6,3,4], [4,2,1,8,3,4,6], [3,2,4,1,9,8,3], [0,1,3,9,2,1

我尝试使用for循环实现二维数组的跨步卷积,即

arr = np.array([[2,3,7,4,6,2,9],
                [6,6,9,8,7,4,3],
                [3,4,8,3,8,9,7],
                [7,8,3,6,6,3,4],
                [4,2,1,8,3,4,6],
                [3,2,4,1,9,8,3],
                [0,1,3,9,2,1,4]])

arr2 = np.array([[3,4,4],
                 [1,0,2],
                 [-1,0,3]])

def stride_conv(arr1,arr2,s,p):
    beg = 0
    end = arr2.shape[0]
    final = []
    for i in range(0,arr1.shape[0]-1,s):
        k = []
        for j in range(0,arr1.shape[0]-1,s):
            k.append(np.sum(arr1[beg+i : end+i, beg+j:end+j] * (arr2)))
        final.append(k)

    return np.array(final)

stride_conv(arr,arr2,2,0)
这将产生3*3阵列:

array([[ 91, 100,  88],
       [ 69,  91, 117],
       [ 44,  72,  74]])

是否有numpy函数或scipy函数来执行相同的操作?我的方法不太好。如何将其矢量化

忽略padding参数和尾随窗口,这些窗口没有足够的长度对第二个数组进行卷积,下面是一种使用
np.lib.stride\u技巧的方法-

def strided4D(arr,arr2,s):
    strided = np.lib.stride_tricks.as_strided
    s0,s1 = arr.strides
    m1,n1 = arr.shape
    m2,n2 = arr2.shape    
    out_shp = (1+(m1-m2)//s, m2, 1+(n1-n2)//s, n2)
    return strided(arr, shape=out_shp, strides=(s*s0,s*s1,s0,s1))

def stride_conv_strided(arr,arr2,s):
    arr4D = strided4D(arr,arr2,s=s)
    return np.tensordot(arr4D, arr2, axes=((2,3),(0,1)))
或者,我们可以使用内置的scikit映像来优雅地获取这些窗口,如下所示-

from skimage.util.shape import view_as_windows

def strided4D_v2(arr,arr2,s):
    return view_as_windows(arr, arr2.shape, step=s)
这是一种基于O(N^d(logn)^d)fft的方法。其思想是将两个操作数分割成以所有偏移量为模的步长间隔的网格,在相应偏移量的网格之间进行常规fft卷积,然后对结果进行逐点求和。这是一个有点沉重的指数,但我恐怕无法帮助:

import numpy as np
from numpy.fft import fftn, ifftn

def strided_conv_2d(x, y, strides):
    s, t = strides
    # consensus dtype
    cdt = (x[0, 0, ...] + y[0, 0, ...]).dtype
    xi, xj = x.shape
    yi, yj = y.shape
    # round up modulo strides
    xk, xl, yk, yl = map(lambda a, b: -a//b * -b, (xi,xj,yi,yj), (s,t,s,t))
    # zero pad to avoid circular convolution
    xp, yp = (np.zeros((xk+yk, xl+yl), dtype=cdt) for i in range(2))
    xp[:xi, :xj] = x
    yp[:yi, :yj] = y
    # fold out strides
    xp = xp.reshape((xk+yk)//s, s, (xl+yl)//t, t)
    yp = yp.reshape((xk+yk)//s, s, (xl+yl)//t, t)
    # do conventional fft convolution
    xf = fftn(xp, axes=(0, 2))
    yf = fftn(yp, axes=(0, 2))
    result = ifftn(xf * yf.conj(), axes=(0, 2)).sum(axis=(1, 3))
    # restore dtype
    if cdt in (int, np.int_, np.int64, np.int32):
        result = result.real.round()
    return result.astype(cdt)

arr = np.array([[2,3,7,4,6,2,9],
                [6,6,9,8,7,4,3],
                [3,4,8,3,8,9,7],
                [7,8,3,6,6,3,4],
                [4,2,1,8,3,4,6],
                [3,2,4,1,9,8,3],
                [0,1,3,9,2,1,4]])

arr2 = np.array([[3,4,4],
                 [1,0,2],
                 [-1,0,3]])

print(strided_conv_2d(arr, arr2, (2, 2)))
结果:

[[ 91 100  88  23   0  29]
 [ 69  91 117  19   0  38]
 [ 44  72  74  17   0  22]
 [ 16  53  26  12   0   0]
 [  0   0   0   0   0   0]
 [ 19  11  21  -9   0   6]]
我认为我们可以做一个“有效的”fft卷积,并在跨步位置只提取这些结果,如下所示:

def strideConv(arr,arr2,s):
    cc=scipy.signal.fftconvolve(arr,arr2[::-1,::-1],mode='valid')
    idx=(np.arange(0,cc.shape[1],s), np.arange(0,cc.shape[0],s))
    xidx,yidx=np.meshgrid(*idx)
    return cc[yidx,xidx]
这会给出与其他人答案相同的结果。 但我想这只有在内核大小为奇数时才有效

另外,我在
arr2[::-1,::-1]
中翻转了内核,只是为了与其他内核保持一致,您可能希望根据上下文省略它

更新:

目前,我们有几种单独使用numpy和scipy进行2D或3D卷积的不同方法,我考虑进行一些比较,以了解哪种方法在不同大小的数据上更快。我希望这不会被认为是离题

方法1:FFT卷积(使用
scipy.signal.fftconvolve
):

方法3:Divakar建议的步态视图转换:

def asStride(arr,sub_shape,stride):
    '''Get a strided sub-matrices view of an ndarray.

    <arr>: ndarray of rank 2.
    <sub_shape>: tuple of length 2, window size: (ny, nx).
    <stride>: int, stride of windows.

    Return <subs>: strided window view.

    See also skimage.util.shape.view_as_windows()
    '''
    s0,s1=arr.strides[:2]
    m1,n1=arr.shape[:2]
    m2,n2=sub_shape[:2]

    view_shape=(1+(m1-m2)//stride,1+(n1-n2)//stride,m2,n2)+arr.shape[2:]
    strides=(stride*s0,stride*s1,s0,s1)+arr.strides[2:]
    subs=numpy.lib.stride_tricks.as_strided(arr,view_shape,strides=strides)

    return subs

def conv3D3(var,kernel,stride=1,pad=0):
    '''3D convolution by strided view.
    '''
    var_ndim=numpy.ndim(var)
    kernel_ndim=numpy.ndim(kernel)

    if var_ndim<2 or var_ndim>3 or kernel_ndim<2 or kernel_ndim>3:
        raise Exception("<var> and <kernel> dimension should be in 2 or 3.")

    if var_ndim==2 and kernel_ndim==3:
        raise Exception("<kernel> dimension > <var>.")

    if var_ndim==3 and kernel_ndim==2:
        kernel=numpy.repeat(kernel[:,:,None],var.shape[2],axis=2)

    if pad>0:
        var_pad=padArray(var,pad,1)
    else:
        var_pad=var

    view=asStride(var_pad,kernel.shape,stride)
    #return numpy.tensordot(aa,kernel,axes=((2,3),(0,1)))
    if numpy.ndim(kernel)==2:
        conv=numpy.sum(view*kernel,axis=(2,3))
    else:
        conv=numpy.sum(view*kernel,axis=(2,3,4))

    return conv
def asStride(arr、亚U形、跨步):
''获取数据阵列的跨步子矩阵视图。

所以“FFT conv”通常是最快的。“Special conv”和“Stride view conv”随着内核大小的增加而变慢,但随着接近输入数据的大小而再次减小。最后一个子图显示了最快的方法,因此紫色的大三角形表示FFT是赢家,但请注意,左侧有一个绿色的细列(可能太小,看不见,但它在那里),这表明“Special conv”对于非常小的内核(小于约5x5)具有优势。当内核大小接近输入时,“stride view conv”是最快的(见对角线)

比较2:三维数据的卷积

设置:pad=0,stride=2,输入维度=
nxnx5
,内核形状=
fxfx5

当内核大小在输入的中间时,我跳过了“Special Conv”和“Stride view Conv”的计算。基本上,“Special Conv”现在没有显示任何优势,“Stride view”对于大小内核都比FFT快

另一个注意事项是:当大小超过350时,我注意到“Stride view conv”的内存使用量达到了相当高的峰值

比较3:较大步幅的3D数据卷积

设置:pad=0,stride=5,输入维度=
nx10
,内核形状=
fxfx10

这次我省略了“特别会议”。对于更大的区域,“步幅视图conv”优于FFT,最后的子图显示差异接近100%。 可能是因为随着步长的增加,FFT方法会有更多的浪费数字,因此“步长视图”对于大小内核都会获得更多的优势

使用from如何

我的方法与Jason的方法类似,但使用索引

def strideConv(arr, arr2, s):
    return signal.convolve2d(arr, arr2[::-1, ::-1], mode='valid')[::s, ::s]
请注意,内核必须反转。有关详细信息,请参阅讨论和。否则使用

示例:

 >>> strideConv(arr, arr2, 1)
 array([[ 91,  80, 100,  84,  88],
        [ 99, 106, 126,  92,  77],
        [ 69,  98,  91,  93, 117],
        [ 80,  79,  87,  93,  61],
        [ 44,  72,  72,  63,  74]])
 >>> strideConv(arr, arr2, 2)
 array([[ 91, 100,  88],
        [ 69,  91, 117],
        [ 44,  72,  74]])

据我所知,在numpy或scipy中没有支持跨步和填充的卷积滤波器的直接实现,因此我认为最好使用诸如torch或tensorflow之类的DL包,然后将最终结果投射到numpy。火炬的实施可能是:

import torch
import torch.nn.functional as F

arr = torch.tensor(np.expand_dims(arr, axis=(0,1))
arr2 = torch.tensor(np.expand_dims(arr2, axis=(0,1))
output = F.conv2d(arr, arr2, stride=2, padding=0)
output = output.numpy().squeeze()

output>
array([[ 91, 100,  88],
       [ 69,  91, 117],
       [ 44,  72,  74]])

你不能使用scipy 2D conv吗?@Divakar我昨天刚学了卷积运算,scipy 2D conv没有步长参数,如果是的话,恐怕我需要更多的研究。而且arg
p
没有被使用?@Divakar它不完整,我想使用填充,我所学的只是不使用填充。我只是保留了参数以备将来更新。如果可能,我可以帮助您。引用:“假设我们有一个大小为
1x1x10x10
的图像和一个大小为
1x1x3x3
的过滤器……然后,天真地说,如果我们要对图像上的过滤器进行卷积运算,我们将在图像上循环,并在每个图像上取点积……”“但是,如果我们不想做循环呢?”。。。我们需要的是收集所有可能的位置,我们可以应用我们的过滤器,然后做一个单一的矩阵乘法,以获得每个可能的LOC点积。“.Strated4d太漂亮了,先生,对你的方法进行一个小的解释真的会很有帮助。先生,Strated4d的输出,是某种公式吗?@Dark是的,通常是
1+(m1-m2)
。结合步长,我们需要这个划分。想想看,就像我们沿着
m1
的长度倒转m2,然后看看在给定步长的情况下,从一开始可以容纳多少个窗口。有太多的东西需要理解,我必须分析每个变量和每个操作。感谢you@Divakar但是您的代码在除
arr=7x7
以外的其他形状上不起作用(这恰好给出了
1+(m1-m2)//s=3=内核大小
),有时
arr4D
甚至会包含
nan
s。更改为
(1+(m1-m2)//s,1+(n1-n2)//s,m2,n2)
可以正确匹配张力轴(
(2,3)、(0,1)
)。我想@NaN也想问这个问题,因为他的
np.sum()
方法现在也起作用了。我不是一个优秀的代码读者。一个小的解释会更好。它的输出和我的不一样,为什么会这样?@Dark它只是零填充输入。您可以在偏移量1,1处找到作为子阵列的输出。我加了一点g
 >>> strideConv(arr, arr2, 1)
 array([[ 91,  80, 100,  84,  88],
        [ 99, 106, 126,  92,  77],
        [ 69,  98,  91,  93, 117],
        [ 80,  79,  87,  93,  61],
        [ 44,  72,  72,  63,  74]])
 >>> strideConv(arr, arr2, 2)
 array([[ 91, 100,  88],
        [ 69,  91, 117],
        [ 44,  72,  74]])
import torch
import torch.nn.functional as F

arr = torch.tensor(np.expand_dims(arr, axis=(0,1))
arr2 = torch.tensor(np.expand_dims(arr2, axis=(0,1))
output = F.conv2d(arr, arr2, stride=2, padding=0)
output = output.numpy().squeeze()

output>
array([[ 91, 100,  88],
       [ 69,  91, 117],
       [ 44,  72,  74]])