¿Cuál es la diferencia entre matrices contiguas y no contiguas?

101

En el manual numpy sobre la función reshape (), dice

>>> a = np.zeros((10, 2))
# A transpose make the array non-contiguous
>>> b = a.T
# Taking a view makes it possible to modify the shape without modifying the
# initial object.
>>> c = b.view()
>>> c.shape = (20)
AttributeError: incompatible shape for a non-contiguous array

Mis preguntas son:

  1. ¿Qué son las matrices continuas y no contiguas? ¿Es similar al bloque de memoria contiguo en C como ¿Qué es un bloque de memoria contiguo?
  2. ¿Existe alguna diferencia de rendimiento entre estos dos? ¿Cuándo debemos usar uno u otro?
  3. ¿Por qué la transposición hace que la matriz no sea contigua?
  4. ¿Por qué c.shape = (20)arroja un error incompatible shape for a non-contiguous array?

¡Gracias por tu respuesta!

jdeng
fuente

Respuestas:

222

Una matriz contigua es simplemente una matriz almacenada en un bloque de memoria ininterrumpido: para acceder al siguiente valor en la matriz, simplemente nos movemos a la siguiente dirección de memoria.

Considere la matriz 2D arr = np.arange(12).reshape(3,4). Se parece a esto:

ingrese la descripción de la imagen aquí

En la memoria de la computadora, los valores de arrse almacenan así:

ingrese la descripción de la imagen aquí

Esto significa que arres una matriz contigua en C porque las filas se almacenan como bloques contiguos de memoria. La siguiente dirección de memoria contiene el siguiente valor de fila en esa fila. Si queremos bajar una columna, solo tenemos que saltar tres bloques (por ejemplo, saltar de 0 a 4 significa que saltamos 1,2 y 3).

Transponer la matriz con arr.Tsignifica que la contigüidad C se pierde porque las entradas de filas adyacentes ya no están en direcciones de memoria adyacentes. Sin embargo, Fortranarr.T es contiguo ya que las columnas están en bloques contiguos de memoria:

ingrese la descripción de la imagen aquí


En cuanto al rendimiento, acceder a las direcciones de memoria que están una al lado de la otra suele ser más rápido que acceder a las direcciones que están más "dispersas" (obtener un valor de la RAM podría implicar que se obtengan y se almacenen en caché varias direcciones vecinas para la CPU). significa que las operaciones sobre matrices contiguas a menudo serán más rápidas.

Como consecuencia del diseño de memoria contigua de C, las operaciones por filas suelen ser más rápidas que las operaciones por columnas. Por ejemplo, normalmente encontrará que

np.sum(arr, axis=1) # sum the rows

es un poco más rápido que:

np.sum(arr, axis=0) # sum the columns

De manera similar, las operaciones en columnas serán un poco más rápidas para las matrices contiguas de Fortran.


Finalmente, ¿por qué no podemos aplanar la matriz contigua de Fortran asignando una nueva forma?

>>> arr2 = arr.T
>>> arr2.shape = 12
AttributeError: incompatible shape for a non-contiguous array

Para que esto sea posible, NumPy tendría que juntar las filas de arr.Testa manera:

ingrese la descripción de la imagen aquí

(La configuración del shapeatributo asume directamente el orden C, es decir, NumPy intenta realizar la operación por filas).

Esto es imposible de hacer. Para cualquier eje, NumPy debe tener una longitud de zancada constante (el número de bytes a mover) para llegar al siguiente elemento de la matriz. Aplanar arr.Tde esta manera requeriría saltar hacia adelante y hacia atrás en la memoria para recuperar valores consecutivos de la matriz.

Si arr2.reshape(12)escribiéramos en su lugar, NumPy copiaría los valores de arr2 en un nuevo bloque de memoria (ya que no puede devolver una vista a los datos originales para esta forma).

Alex Riley
fuente
Tengo dificultades para entender esto, ¿podría explicarme un poco más? En la última representación gráfica del ordenamiento imposible en la memoria, en mi opinión, los pasos son constantes. Por ejemplo, para ir de 0 a 1, el paso es de 1 byte (digamos que cada elemento es un byte) y es el mismo para cada columna. Asimismo, la zancada es de 4 bytes para pasar de un elemento de la fila al siguiente y también es constante.
Vesnog
2
@Vesnog la remodelación fallida de la forma 2D arr2a 1D (12,)usa el orden C, lo que significa que el eje 1 se desenrolla antes que el eje 0 (es decir, cada una de las cuatro filas debe colocarse una al lado de la otra para crear la matriz 1D deseada). Es imposible leer esta secuencia de números enteros (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) fuera del búfer usando una longitud de paso constante (los bytes para saltar para visitar estos elementos en secuencia serían 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy requiere una longitud de zancada constante por eje.
Alex Riley
Gracias al principio, pensé que crearía una nueva matriz, pero usa la memoria de la anterior.
Vesnog
@AlexRiley ¿Qué sucede detrás de escena cuando una matriz se marca más cerca de C o F ordenada? Por ejemplo, tome cada matriz NxD arr e imprima (arr [:, :: - 1] .flags). ¿Qué pasa en esta situación? Supongo que la matriz está ordenada en C o F, pero ¿cuál de ellas? ¿Y qué optimizaciones de numpy perdemos si ambas banderas son falsas?
Jjang
@Jjang: si NumPy considera que la matriz es de orden C o F depende completamente de la forma y los pasos (los criterios están aquí ). Entonces, aunque arr[:, ::-1]es una vista del mismo búfer de memoria que arr, NumPy no lo considera en orden C o F, ya que ha atravesado los valores en el búfer en un orden "no estándar" ...
Alex Riley
12

Quizás este ejemplo con 12 valores de matriz diferentes ayude:

In [207]: x=np.arange(12).reshape(3,4).copy()

In [208]: x.flags
Out[208]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [209]: x.T.flags
Out[209]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  OWNDATA : False
  ...

Los C ordervalores están en el orden en que fueron generados. Los transpuestos no son

In [212]: x.reshape(12,)   # same as x.ravel()
Out[212]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

In [213]: x.T.reshape(12,)
Out[213]: array([ 0,  4,  8,  1,  5,  9,  2,  6, 10,  3,  7, 11])

Puede obtener vistas 1d de ambos

In [214]: x1=x.T

In [217]: x.shape=(12,)

la forma de xtambién se puede cambiar.

In [220]: x1.shape=(12,)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-220-cf2b1a308253> in <module>()
----> 1 x1.shape=(12,)

AttributeError: incompatible shape for a non-contiguous array

Pero la forma de la transposición no se puede cambiar. El datatodavía está en el 0,1,2,3,4...orden, que no se puede acceder accede como 0,4,8...en una matriz de 1d.

Pero x1se puede cambiar una copia de :

In [227]: x2=x1.copy()

In [228]: x2.flags
Out[228]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  ...
In [229]: x2.shape=(12,)

Mirar stridestambién podría ayudar. Un paso es qué tan lejos (en bytes) tiene que dar un paso para llegar al siguiente valor. Para una matriz 2d, habrá 2 valores de zancada:

In [233]: x=np.arange(12).reshape(3,4).copy()

In [234]: x.strides
Out[234]: (16, 4)

Para ir a la siguiente fila, paso 16 bytes, la siguiente columna solo 4.

In [235]: x1.strides
Out[235]: (4, 16)

Transpose simplemente cambia el orden de los pasos. La siguiente fila tiene solo 4 bytes, es decir, el siguiente número.

In [236]: x.shape=(12,)

In [237]: x.strides
Out[237]: (4,)

Cambiar la forma también cambia las zancadas, simplemente recorra el búfer 4 bytes a la vez.

In [238]: x2=x1.copy()

In [239]: x2.strides
Out[239]: (12, 4)

Aunque x2tiene el mismo aspecto x1, tiene su propio búfer de datos, con los valores en un orden diferente. La siguiente columna ahora tiene 4 bytes más, mientras que la siguiente fila tiene 12 (3 * 4).

In [240]: x2.shape=(12,)

In [241]: x2.strides
Out[241]: (4,)

Y al igual que con x, cambiar la forma a 1d reduce los pasos a (4,).

Porque x1, con los datos en el 0,1,2,...orden, no hay un paso de 1d que daría 0,4,8....

__array_interface__ es otra forma útil de mostrar información de matriz:

In [242]: x1.__array_interface__
Out[242]: 
{'strides': (4, 16),
 'typestr': '<i4',
 'shape': (4, 3),
 'version': 3,
 'data': (163336056, False),
 'descr': [('', '<i4')]}

La x1dirección del búfer de datos será la misma que para x, con el que comparte los datos. x2tiene una dirección de búfer diferente.

También puede experimentar agregando un order='F'parámetro a los comandos copyy reshape.

hpaulj
fuente