numpy

Is there a function in numpy that tells me if two indices on two different views of an array will point to the same memory?


I have the array in of shape (h,w). For a constant s, I have

new_h = (h - k_h)//s + 1
new_w = (w - k_w)//s + 1
s_h, s_w = in.strides
new_shape = (new_h, new_w,k_h,k_w)
new_strides = (s_h * s, s_w * s, s_h, s_w)
strided_in = np.lib.stride_tricks.as_strided(in,new_shape, new_strides)

This is best thought of as a matrix of windows obtained by sliding a subarray of shape (k_h, k_w) across in left-to-right and top-to-bottom, moving over s many elements each time.

The array out is given by

out = np.max(strided_arr, axis = (2,3))

I'm given the array grad_out of shape out.shape. I need to compute the array grad_in, of shape in.shape where each entry is (almost) given by:

grad_in[i,j] = 0
for m, row in enumerate(strided_in):
    for n, block in enumerate(row):
        grad_in[i,j] += grad_out[m,n] * (np.max(block) == in[i,j])

I.e. increment by grad_out[m,n] for each block where in[i,j] gets mapped into out. The problem is that the elements of in can repeat, and this approach will overcount

This issue is best illustrated with an example.

Say I have the base array

arr1 = [1, 1]

And I have the following view:

arr2 = [[1],[1]]

I need to be able to differentiate arr1[0] and arr2[1,0]

I know that np.shares_memory exists, but this function is for an array, not an element of an array


Solution

  • Starting with a simple 1d array:

    In [40]: x = np.arange(1,7);x
    Out[40]: array([1, 2, 3, 4, 5, 6])
    
    In [41]: x.__array_interface__
    Out[41]: 
    {'data': (1256399079680, False),
     'strides': None,
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (6,),
     'version': 3}
    

    The key information about the array is displayed in this dict.

    A view of x:

    In [42]: y = x.reshape(2,3);y
    Out[42]: 
    array([[1, 2, 3],
           [4, 5, 6]])
    
    In [43]: y.__array_interface__
    Out[43]: 
    {'data': (1256399079680, False),
     'strides': None,
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (2, 3),
     'version': 3}
    
    In [44]: x.strides
    Out[44]: (8,)
    
    In [45]: y.strides
    Out[45]: (24, 8)
    

    y has the same 'data' number (where the underlying data buffer is 'stored'), but shape and strides are different. Same values, but accessed as a 2d array.

    A view doesn't have to 'start' at the same point in the database, for example:

    In [47]: z = y[::-1,::-2]; z
    Out[47]: 
    array([[6, 4],
           [3, 1]])
    
    In [48]: z.__array_interface__
    Out[48]: 
    {'data': (1256399079720, False),
     'strides': (-24, -16),
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (2, 2),
     'version': 3}
    
    In [49]: y.base
    Out[49]: array([1, 2, 3, 4, 5, 6])
    
    In [50]: z.base
    Out[50]: array([1, 2, 3, 4, 5, 6])
    

    Striding can be used to make a view. It's mainly done by playing with the strides.

    In [55]: w=np.lib.stride_tricks.sliding_window_view(x,3)
    
    In [56]: w
    Out[56]: 
    array([[1, 2, 3],
           [2, 3, 4],
           [3, 4, 5],
           [4, 5, 6]])
    
    In [57]: w.__array_interface__
    Out[57]: 
    {'data': (1256399079680, True),
     'strides': (8, 8),
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (4, 3),
     'version': 3}
    
    In [58]: w.base
    Out[58]: <numpy.lib._stride_tricks_impl.DummyArray at 0x1248a129640>
    

    Note the same 'data' number. It's that (8,8) strides that creates the sliding window, stepping to the next row by one element (of the original). The base is the result of how the view is constructed, and doesn't tell us much.

    If we take a subset of the sliding window, we can get the same array as y - same strides etc:

    In [63]: w[::3]
    Out[63]: 
    array([[1, 2, 3],
           [4, 5, 6]])
    
    In [64]: w[::3].__array_interface__
    Out[64]: 
    {'data': (1256399079680, True),
     'strides': None,
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (2, 3),
     'version': 3}
    

    Functions like shares_maemory and may_share_memory probably work from information contained in this iterface (and the FLAGS?). See their docs. But they don't try to define the 'mapping' between the two arrays. Part of the simplicity and speed of the view mechanism is that it doesn't try to construct and save the mapping. It saves just enough information in the view to access the base values in a new and different way.

    In [67]: w.flags
    Out[67]: 
      C_CONTIGUOUS : False
      F_CONTIGUOUS : False
      OWNDATA : False
      WRITEABLE : False
      ALIGNED : True
      WRITEBACKIFCOPY : False
    
    In [68]: y.flags
    Out[68]: 
      C_CONTIGUOUS : True
      F_CONTIGUOUS : False
      OWNDATA : False
      WRITEABLE : True
      ALIGNED : True
      WRITEBACKIFCOPY : False
    
    In [69]: x.flags
    Out[69]: 
      C_CONTIGUOUS : True
      F_CONTIGUOUS : True
      OWNDATA : True
      WRITEABLE : True
      ALIGNED : True
      WRITEBACKIFCOPY : False
    

    An 'unboxed' scalar element of an array also has an interface (a np.int64 has a lot of the same attributes as an array), but note that its 'data' is totally different.

    
    In [76]: x1
    Out[76]: np.int64(3)
    
    In [77]: x1.__array_interface__
    Out[77]: 
    {'data': (1256398492688, False),
     'strides': None,
     'descr': [('', '<i8')],
     'typestr': '<i8',
     'shape': (),
     'version': 3,
     '__ref': array(3)}
    

    and the 'same' element from y:

    In [78]: y[0,2]
    Out[78]: np.int64(3)
    
    In [79]: y[0,2].__array_interface__
    Out[79]: 
    {'data': (1256398492208, False),
    

    I could modify an element of y, and see the change in x, but it won't appear in the already 'unboxed' x1.

    In [80]: y[0,2]=30;y
    Out[80]: 
    array([[ 1,  2, 30],
           [ 4,  5,  6]])
    
    In [81]: x
    Out[81]: array([ 1,  2, 30,  4,  5,  6])
    
    In [82]: x1
    Out[82]: np.int64(3)
    

    The change appears multiple times in w

    In [83]: w
    Out[83]: 
    array([[ 1,  2, 30],
           [ 2, 30,  4],
           [30,  4,  5],
           [ 4,  5,  6]])