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?
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)
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:
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.
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.
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]]
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]])