Qual é a diferença entre matrizes contíguas e não contíguas?

100

No manual numpy sobre a função reshape (), diz

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

Minhas perguntas são:

  1. O que são matrizes contínuas e não contíguas? É semelhante ao bloco de memória contígua em C, como O que é um bloco de memória contígua?
  2. Existe alguma diferença de desempenho entre esses dois? Quando devemos usar um ou outro?
  3. Por que transpor torna a matriz não contígua?
  4. Por que c.shape = (20)gera um erro incompatible shape for a non-contiguous array?

Obrigado pela sua resposta!

jdeng
fonte

Respostas:

220

Um array contíguo é apenas um array armazenado em um bloco ininterrupto de memória: para acessar o próximo valor no array, simplesmente passamos para o próximo endereço de memória.

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

insira a descrição da imagem aqui

Na memória do computador, os valores de arrsão armazenados assim:

insira a descrição da imagem aqui

Isso significa que arré uma matriz contígua C porque as linhas são armazenadas como blocos contíguos de memória. O próximo endereço de memória contém o próximo valor de linha nessa linha. Se quisermos descer uma coluna, só precisamos pular três blocos (por exemplo, pular de 0 para 4 significa pular 1,2 e 3).

Transpor a matriz com arr.Tsignifica que a contiguidade C é perdida porque as entradas de linha adjacentes não estão mais em endereços de memória adjacentes. No entanto, o Fortranarr.T é contíguo, pois as colunas estão em blocos contíguos de memória:

insira a descrição da imagem aqui


Em termos de desempenho, acessar endereços de memória que estão próximos um do outro é muito mais rápido do que acessar endereços que estão mais "espalhados" (buscar um valor da RAM pode implicar em vários endereços vizinhos sendo buscados e armazenados em cache para a CPU). significa que as operações em matrizes contíguas geralmente serão mais rápidas.

Como consequência do layout de memória contígua C, as operações em linha são geralmente mais rápidas do que em colunas. Por exemplo, você normalmente encontrará que

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

é ligeiramente mais rápido que:

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

Da mesma forma, as operações em colunas serão ligeiramente mais rápidas para matrizes contíguas em Fortran.


Finalmente, por que não podemos nivelar a matriz contígua do Fortran atribuindo uma nova forma?

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

Para que isso fosse possível, o NumPy teria que colocar as linhas arr.Tjuntas assim:

insira a descrição da imagem aqui

(Definir o shapeatributo diretamente assume a ordem C - ou seja, NumPy tenta realizar a operação em linha.)

Isso é impossível de fazer. Para qualquer eixo, o NumPy precisa ter um comprimento de passo constante (o número de bytes a mover) para chegar ao próximo elemento da matriz. O achatamento arr.Tdessa maneira exigiria pular para a frente e para trás na memória para recuperar valores consecutivos do array.

Se arr2.reshape(12)escrevêssemos em vez disso, NumPy copiaria os valores de arr2 em um novo bloco de memória (uma vez que não pode retornar uma visualização dos dados originais para esta forma).

Alex Riley
fonte
Estou tendo dificuldade em entender isso, pode me explicar um pouco? Na última representação gráfica da ordem impossível na memória, na verdade, os avanços são constantes em minha opinião. Por exemplo, para ir de 0 a 1, o passo é de 1 byte (digamos que cada elemento seja um byte) e é o mesmo para cada coluna. Da mesma forma, a passada é de 4 bytes para ir de um elemento na linha para o próximo e também é constante.
Vesnog
2
@Vesnog a remodelação falhada da forma 2D arr2para a forma 1D (12,)usa a ordem C, o que significa que o eixo 1 é desenrolado antes do eixo 0 (ou seja, cada uma das quatro linhas precisa ser colocada uma ao lado da outra para criar a matriz 1D desejada). É impossível ler esta sequência de inteiros (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) para fora do buffer usando um comprimento de passo constante (os bytes para pular para visitar esses elementos em sequência seriam 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy requer um comprimento de passada constante por eixo.
Alex Riley
Obrigado a princípio pensei que iria criar um novo array, mas usa a memória do antigo.
Vesnog
@AlexRiley O que acontece nos bastidores quando uma matriz é marcada como C ou F ordenada? Por exemplo, pegue cada array NxD arr e imprima (arr [:, :: - 1] .flags). O que acontece nesta situação? Acho que a matriz é de fato C ou F ordenada, mas qual delas? E quais otimizações de numpy perdemos se ambas as sinalizações forem falsas?
Jjang
@Jjang: se NumPy considera que o array é de ordem C ou F depende inteiramente da forma e dos avanços (os critérios estão aqui ). Portanto, embora arr[:, ::-1]seja uma visão do mesmo buffer de memória que arr, NumPy não o considera ordem C ou F, pois percorreu os valores no buffer em uma ordem "não padrão" ...
Alex Riley
12

Talvez este exemplo com 12 valores de array diferentes ajude:

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

Os C ordervalores estão na ordem em que foram gerados. Os transpostos não estão

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

Você pode obter 1d visualizações de ambos

In [214]: x1=x.T

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

a forma de xtambém pode ser alterada.

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

Mas a forma da transposta não pode ser alterada. O dataainda está na 0,1,2,3,4...ordem, que não pode ser acessada como 0,4,8...em uma matriz 1d.

Mas uma cópia de x1pode ser alterada:

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

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

Olhar stridestambém pode ajudar. Um passo é a distância (em bytes) que ela deve percorrer para chegar ao próximo valor. Para uma matriz 2d, haverá 2 valores de passada:

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

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

Para ir para a próxima linha, etapa 16 bytes, próxima coluna apenas 4.

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

Transpor apenas muda a ordem das passadas. A próxima linha tem apenas 4 bytes - ou seja, o próximo número.

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

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

Mudar a forma também muda os passos - basta percorrer o buffer 4 bytes por vez.

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

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

Mesmo x2parecendo x1, ele tem seu próprio buffer de dados, com os valores em uma ordem diferente. A próxima coluna agora tem 4 bytes, enquanto a próxima linha tem 12 (3 * 4).

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

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

E, como acontece com x, alterar a forma para 1d reduz os passos para (4,).

Pois x1, com os dados em 0,1,2,...ordem, não há um passo de 1d que daria 0,4,8....

__array_interface__ é outra maneira útil de exibir informações de matriz:

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

O x1endereço do buffer de dados será o mesmo de x, com o qual ele compartilha os dados. x2tem um endereço de buffer diferente.

Você também pode experimentar adicionar um order='F'parâmetro aos comandos copye reshape.

hpaulj
fonte