web-dev-qa-db-fra.com

Convolution foulée de la 2D en numpy

J'ai essayé d'implémenter la convolution échelonnée d'un tableau 2D à l'aide de la boucle i.e 

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)

Cela donne un tableau 3 * 3: 

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

Y at-il une fonction numpy ou scipy pour faire la même chose? Mon approche n'est pas si bonne. Comment puis-je vectoriser cela? 

5
Dark

En ignorant l'argument de remplissage et les fenêtres de fin qui n'auront pas assez de longueur pour la convolution contre le second tableau, voici une solution avec np.lib.stride_tricks.as_strided -

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)))

Alternativement, nous pouvons utiliser scikit-image intégré view_as_windows pour obtenir ces fenêtres élégamment , comme si -

from skimage.util.shape import view_as_windows

def strided4D_v2(arr,arr2,s):
    return view_as_windows(arr, arr2.shape, step=s)
5
Divakar

Je pense que nous pouvons faire une convolution fft "valide" et ne sélectionner que les résultats aux emplacements marqués, comme ceci:

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]

Cela donne les mêmes résultats que les réponses des autres personnes. Mais je suppose que cela ne fonctionne que si la taille du noyau est numérotée impaire.

De plus, j'ai retourné le noyau dans arr2[::-1,::-1] juste pour rester cohérent avec les autres, vous voudrez peut-être l'omettre en fonction du contexte.

UPDATE:

Nous avons actuellement plusieurs façons différentes de faire la convolution 2D ou 3D en utilisant numpy et Scipy seuls, et j'ai envisagé de faire des comparaisons pour donner une idée de la vitesse la plus rapide avec des données de tailles différentes. J'espère que cela ne sera pas considéré comme hors sujet.

Méthode 1: convolution FFT (à l'aide de scipy.signal.fftconvolve):

def padArray(var,pad,method=1):
    if method==1:
        var_pad=numpy.zeros(Tuple(2*pad+numpy.array(var.shape[:2]))+var.shape[2:])
        var_pad[pad:-pad,pad:-pad]=var
    else:
        var_pad=numpy.pad(var,([pad,pad],[pad,pad])+([0,0],)*(numpy.ndim(var)-2),
                mode='constant',constant_values=0)
    return var_pad

def conv3D(var,kernel,stride=1,pad=0,pad_method=1):
    '''3D convolution using scipy.signal.convolve.
    '''
    var_ndim=numpy.ndim(var)
    kernel_ndim=numpy.ndim(kernel)
    stride=int(stride)

    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,pad_method)
    else:
        var_pad=var

    conv=fftconvolve(var_pad,kernel,mode='valid')

    if stride>1:
        conv=conv[::stride,::stride,...]

    return conv

Méthode 2: Conv. Spéciale (voir cette réponse ):

def conv3D2(var,kernel,stride=1,pad=0):
    '''3D convolution by sub-matrix summing.
    '''
    var_ndim=numpy.ndim(var)
    ny,nx=var.shape[:2]
    ky,kx=kernel.shape[:2]

    result=0

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

    for ii in range(ky*kx):
        yi,xi=divmod(ii,kx)
        slabii=var_pad[yi:2*pad+ny-ky+yi+1:1, xi:2*pad+nx-kx+xi+1:1,...]*kernel[yi,xi]
        if var_ndim==3:
            slabii=slabii.sum(axis=-1)
        result+=slabii

    if stride>1:
        result=result[::stride,::stride,...]

    return result

Méthode 3: Convection à vue tranchée, comme suggéré par 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

J'ai fait 3 séries de comparaisons:

  1. convolution sur des données 2D, avec une taille d'entrée différente et une taille de noyau différente, stride = 1, pad = 0. Résultats ci-dessous (couleur correspondant au temps utilisé pour la convolution répétée 10 fois):

 enter image description here

Donc, "FFT conv" est en général le plus rapide. "Special conv" et "Stride-view conv" deviennent lents à mesure que la taille du noyau augmente, mais diminue à nouveau à l'approche de la taille des données d'entrée. La dernière sous-parcelle montre la méthode la plus rapide. Le grand triangle violet indique donc FFT comme vainqueur, mais notez qu’une mince colonne verte se trouve à gauche (probablement trop petite pour être visualisée, mais elle est là), ce qui suggère que "Conv. Spéciale" présente un avantage pour les très petits noyaux (inférieurs à environ 5x5). Et lorsque la taille du noyau approche de l'entrée, "stride-view conv" est le plus rapide (voir la ligne diagonale).

Comparaison 2: convolution sur des données 3D.

Configuration: pad = 0, stride = 2, dimension d'entrée = nxnx5, forme du noyau = fxfx5.

J'ai ignoré les calculs de "Conv. Spéciale" et "Conv. Stride-view" lorsque la taille du noyau est au milieu de l'entrée. Fondamentalement, "Conv. Spéciale" ne montre aucun avantage pour le moment, et "Stride-view" est plus rapide que FFT pour les noyaux petits et grands.

 enter image description here

Une remarque supplémentaire: lorsque les tailles dépassent 350, je remarque des pics d'utilisation de mémoire considérables pour la "vue panoramique".

Comparaison 3: convolution sur des données 3D avec une foulée plus grande.

Configuration: pad = 0, stride = 5, dimension d'entrée = nxnx10, forme du noyau = fxfx10.

Cette fois, j'ai omis le "Conv Conv." Pour une zone plus grande, "Stride-view conv" dépasse la FFT, et les dernières sous-parcelles montrent que la différence est proche de 100%. Probablement parce que l'approche FFT aura davantage de nombres perdus, donc view "offre plus d'avantages pour les noyaux petits et grands.

 enter image description here

3
Jason

Voici une approche basée sur O (N ^ d (log N) ^ d) fft. L'idée est de découper les deux opérandes en grilles espacées à tous les décalages modulo, d'effectuer la convolution classique entre les grilles des décalages correspondants, puis de faire la somme des résultats. C'est un peu lourd en index, mais j'ai bien peur qu'on ne puisse rien y faire:

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)))

Résultat:

[[ 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]]
3
Paul Panzer

Pourquoi ne pas utiliser signal.convolve2d from scipy ?

Mon approche est similaire à celle de Jason mais utilise l'indexation.

def strideConv(arr, arr2, s):
    return signal.convolve2d(arr, arr2[::-1, ::-1], mode='valid')[::s, ::s]

Notez que le kernal doit être inversé. Pour plus de détails, consultez la discussion ici et ici . Sinon, utilisez signal.correlate2d .

Exemples:

 >>> 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]])
3
kitman0804