Saltar al contenido

¿Cómo entender los pasos numpy para el profano?

Después de de nuestra prolongada selección de datos hemos podido resolver este rompecabezas que presentan algunos usuarios. Te compartimos la solución y nuestro objetivo es que resulte de gran ayuda.

Solución:

Los datos reales de un numpy array se almacena en un bloque de memoria homogéneo y contiguo llamado búfer de datos. Para obtener más información, consulte Elementos internos de NumPy. Usando el orden de fila principal (predeterminado), un 2D array Se ve como esto:

ingrese la descripción de la imagen aquí

Para mapear los índices i, j, k, … de un multidimensional array a las posiciones en el búfer de datos (el desplazamiento, en bytes), NumPy usa la noción de zancadas. Los pasos son el número de bytes que se deben saltar en la memoria para pasar de un elemento al Siguiente elemento a lo largo de cada dirección / dimensión del array. En otras palabras, es la separación de bytes entre elementos consecutivos para cada dimensión.

Por ejemplo:

>>> a = np.arange(1,10).reshape(3,3)
>>> a
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

Este 2D array tiene dos direcciones, axes-0 (que se ejecuta verticalmente hacia abajo en las filas) y axis-1 (que se ejecuta horizontalmente en las columnas), y cada elemento tiene un tamaño:

>>> a.itemsize  # in bytes
4  

Así que para ir de a[0, 0] -> a[0, 1] (moviéndose horizontalmente a lo largo de la fila 0, desde la columna 0 a la 1ª columna) el paso de bytes en el búfer de datos es 4. Lo mismo para a[0, 1] -> a[0, 2], a[1, 0] -> a[1, 1] etc. Esto significa que el número de pasos para la dirección horizontal (eje-1) es de 4 bytes.

Sin embargo, para pasar de a[0, 0] -> a[1, 0] (moviéndose verticalmente a lo largo de la columna 0, desde la fila 0 a la primera fila), primero debe recorrer todos los elementos restantes en la fila 0 para llegar a la primera fila, y luego moverse a través de la primera fila para llegar al elemento a[1, 0], es decir a[0, 0] -> a[0, 1] -> a[0, 2] -> a[1, 0]. Por lo tanto, el número de pasos para la dirección vertical (eje-0) es 3 * 4 = 12 bytes. Tenga en cuenta que yendo desde a[0, 2] -> a[1, 0], y en general desde el último elemento de la i-ésima fila hasta el primer elemento de la (i + 1) -ésima fila, también es de 4 bytes porque la array a se almacena en el orden de fila principal.

Es por eso

>>> a.strides  # (strides[0], strides[1])
(12, 4)  

Aquí hay otro ejemplo que muestra que los pasos en la dirección horizontal (eje-1), strides[1], de un 2D array no es necesariamente igual al tamaño del artículo (por ejemplo, un array con orden de columna mayor):

>>> b = np.array([[1, 4, 7],
                  [2, 5, 8],
                  [3, 6, 9]]).T
>>> b
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

>>> b.strides
(4, 12)

Aquí strides[1] es un múltiplo del tamaño del artículo. Aunque el array b parece idéntico al array a, es diferente array: internamente b se almacena como |1|4|7|2|5|8|3|6|9| (porque la transposición no afecta el búfer de datos, sino que solo cambia los pasos y la forma), mientras que a como |1|2|3|4|5|6|7|8|9|. Lo que los hace parecer iguales son los diferentes pasos. Es decir, el paso de bytes para b[0, 0] -> b[0, 1] es 3 * 4 = 12 bytes y para b[0, 0] -> b[1, 0] es de 4 bytes, mientras que para a[0, 0] -> a[0, 1] es de 4 bytes y para a[0, 0] -> a[1, 0] es de 12 bytes.

Por último, pero no menos importante, NumPy permite crear vistas de matrices existentes con la opción de modificar las zancadas y la forma, ver trucos de zancadas. Por ejemplo:

>>> np.lib.stride_tricks.as_strided(a, shape=a.shape[::-1], strides=a.strides[::-1])
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])

que es equivalente a transponer el array a.

Permítanme agregar, pero sin entrar en muchos detalles, que incluso se pueden definir pasos que no sean múltiplos del tamaño del artículo. He aquí un ejemplo:

>>> a = np.lib.stride_tricks.as_strided(np.array([1, 512, 0, 3], dtype=np.int16), 
                                        shape=(3,), strides=(3,))
>>> a
array([1, 2, 3], dtype=int16)

>>> a.strides[0]
3

>>> a.itemsize
2

Solo para agregar a la excelente respuesta de @AndyK, aprendí acerca de los avances numéricos de Numpy MedKit. Allí muestran el uso con un problema de la siguiente manera:

Entrada dada:

x = np.arange(20).reshape([4, 5])
>>> x
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])

Rendimiento esperado:

array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Para hacer esto, necesitamos conocer los siguientes términos:

forma – Las dimensiones del array a lo largo de cada eje.

zancadas – El número de bytes de memoria que se deben omitir para avanzar al siguiente elemento a lo largo de una determinada dimensión.

>>> x.strides
(20, 4)

>>> np.int32().itemsize
4

Ahora, si miramos el Rendimiento esperado:

array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Necesitamos manipular el array forma y zancadas. La forma de salida debe ser (3, 2, 5), es decir, 3 elementos, cada uno con dos filas (m == 2) y cada fila con 5 elementos.

Los pasos deben cambiar de (20, 4) a (20, 20, 4). Cada elemento de la nueva salida array comienza en una nueva fila, que cada fila consta de 20 bytes (5 elementos de 4 bytes cada uno), y cada elemento ocupa 4 bytes (int32).

Entonces:

>>> from numpy.lib import stride_tricks
>>> stride_tricks.as_strided(x, shape=(3, 2, 5),
                                strides=(20, 20, 4))
...
array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Una alternativa sería:

>>> d = dict(x.__array_interface__)
>>> d['shape'] = (3, 2, 5)
>>> s['strides'] = (20, 20, 4)

>>> class Arr:
...     __array_interface__ = d
...     base = x

>>> np.array(Arr())
array([[[  0,  1,  2,  3,  4],
        [  5,  6,  7,  8,  9]],

       [[  5,  6,  7,  8,  9],
        [ 10, 11, 12, 13, 14]],

       [[ 10, 11, 12, 13, 14],
        [ 15, 16, 17, 18, 19]]])

Utilizo este método muy a menudo en lugar de numpy.hstack o numpy.vstack y confía en mí, computacionalmente es mucho más rápido.

Nota:

Cuando se utilizan matrices muy grandes con este truco, calcular la exacta zancadas no es tan trivial. Yo suelo hacer un numpy.zeroes array de la forma deseada y obtener los pasos usando array.strides y usa esto en la función stride_tricks.as_strided.

¡Espero eso ayude!

He adaptado el trabajo presentado por @Rick M. para adaptarse a mi problema, que es el corte de ventanas en movimiento de matrices numerosas de cualquier forma. Aquí está el código:

def sliding_window_slicing(a, no_items, item_type=0):
    """This method perfoms sliding window slicing of numpy arrays

    Parameters
    ----------
    a : numpy
        An array to be slided in subarrays
    no_items : int
        Number of sliced arrays or elements in sliced arrays
    item_type: int
        Indicates if no_items is number of sliced arrays (item_type=0) or
        number of elements in sliced array (item_type=1), by default 0

    Return
    ------
    numpy
        Sliced numpy array
    """
    if item_type == 0:
        no_slices = no_items
        no_elements = len(a) + 1 - no_slices
        if no_elements <=0:
            raise ValueError('Sliding slicing not possible, no_items is larger than ' + str(len(a)))
    else:
        no_elements = no_items                
        no_slices = len(a) - no_elements + 1
        if no_slices <=0:
            raise ValueError('Sliding slicing not possible, no_items is larger than ' + str(len(a)))

    subarray_shape = a.shape[1:]
    shape_cfg = (no_slices, no_elements) + subarray_shape
    strides_cfg = (a.strides[0],) + a.strides
    as_strided = np.lib.stride_tricks.as_strided #shorthand
    return as_strided(a, shape=shape_cfg, strides=strides_cfg)

Este método calcula automáticamente zancadas y funciona con numpy matrices de cualquier dimensión:

1D array - rebanado a través de una serie de rebanadas

In [11]: a                                                                                                                                                     
Out[11]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [12]: sliding_window_slicing(a, 5, item_type=0)                                                                                                                          
Out[12]: 
array([[0, 1, 2, 3, 4, 5],
       [1, 2, 3, 4, 5, 6],
       [2, 3, 4, 5, 6, 7],
       [3, 4, 5, 6, 7, 8],
       [4, 5, 6, 7, 8, 9]])

1D array - corte a través de una serie de elementos por corte

In [13]: sliding_window_slicing(a, 5, item_type=1)                                                                                                             
Out[13]: 
array([[0, 1, 2, 3, 4],
       [1, 2, 3, 4, 5],
       [2, 3, 4, 5, 6],
       [3, 4, 5, 6, 7],
       [4, 5, 6, 7, 8],
       [5, 6, 7, 8, 9]])

2D array - rebanado a través de una serie de rebanadas

In [16]: a = np.arange(10).reshape([5,2])                                                                                                                      

In [17]: a                                                                                                                                                     
Out[17]: 
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7],
       [8, 9]])

In [18]: sliding_window_slicing(a, 2, item_type=0)                                                                                                             
Out[18]: 
array([[[0, 1],
        [2, 3],
        [4, 5],
        [6, 7]],

       [[2, 3],
        [4, 5],
        [6, 7],
        [8, 9]]])

2D array - corte a través de una serie de elementos por corte

In [19]: sliding_window_slicing(a, 2, item_type=1)                                                                                                             
Out[19]: 
array([[[0, 1],
        [2, 3]],

       [[2, 3],
        [4, 5]],

       [[4, 5],
        [6, 7]],

       [[6, 7],
        [8, 9]]])

Más adelante puedes encontrar los informes de otros desarrolladores, tú también eres capaz mostrar el tuyo si lo crees conveniente.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *