Qual è la differenza tra array contigui e non contigui?


100

Nel manuale numpy sulla funzione 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

Le mie domande sono:

  1. Cosa sono gli array continui e non contigui? È simile al blocco di memoria contiguo in C come Cos'è un blocco di memoria contiguo?
  2. C'è qualche differenza di prestazioni tra questi due? Quando dovremmo usare l'uno o l'altro?
  3. Perché la trasposizione rende l'array non contiguo?
  4. Perché c.shape = (20)genera un errore incompatible shape for a non-contiguous array?

Grazie per la tua risposta!

Risposte:


220

Un array contiguo è solo un array memorizzato in un blocco di memoria ininterrotto: per accedere al valore successivo dell'array, ci limitiamo a spostarci all'indirizzo di memoria successivo.

Considera l'array 2D arr = np.arange(12).reshape(3,4). Assomiglia a questo:

inserisci qui la descrizione dell'immagine

Nella memoria del computer, i valori di arrvengono memorizzati in questo modo:

inserisci qui la descrizione dell'immagine

Ciò significa che arrè un array contiguo C perché le righe sono archiviate come blocchi di memoria contigui. L'indirizzo di memoria successivo contiene il valore della riga successiva su quella riga. Se vogliamo spostarci verso il basso di una colonna, dobbiamo solo saltare tre blocchi (es. Saltare da 0 a 4 significa saltare 1, 2 e 3).

Trasporre l'array con arr.Tsignifica che la contiguità C viene persa perché le voci di riga adiacenti non si trovano più negli indirizzi di memoria adiacenti. Tuttavia, Fortranarr.T è contiguo poiché le colonne sono in blocchi di memoria contigui:

inserisci qui la descrizione dell'immagine


Dal punto di vista delle prestazioni, l'accesso agli indirizzi di memoria che sono uno accanto all'altro è molto spesso più veloce rispetto agli indirizzi che sono più "sparsi" (il recupero di un valore dalla RAM potrebbe comportare il recupero e la memorizzazione nella cache per la CPU di un numero di indirizzi adiacenti). significa che le operazioni su array contigui saranno spesso più veloci.

Come conseguenza del layout di memoria contigua del C, le operazioni per riga sono generalmente più veloci delle operazioni per colonna. Ad esempio, in genere lo troverai

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

è leggermente più veloce di:

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

Allo stesso modo, le operazioni sulle colonne saranno leggermente più veloci per gli array contigui Fortran.


Infine, perché non possiamo appiattire l'array contiguo di Fortran assegnando una nuova forma?

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

Affinché ciò sia possibile NumPy dovrebbe mettere arr.Tinsieme le righe in questo modo:

inserisci qui la descrizione dell'immagine

(L'impostazione shapedell'attributo presuppone direttamente l'ordine C, ovvero NumPy cerca di eseguire l'operazione per riga.)

Questo è impossibile da fare. Per qualsiasi asse, NumPy deve avere una lunghezza del passo costante (il numero di byte da spostare) per raggiungere l'elemento successivo dell'array. L'appiattimento arr.Tin questo modo richiederebbe il salto avanti e indietro nella memoria per recuperare i valori consecutivi dell'array.

Se arr2.reshape(12)invece scrivessimo , NumPy copierebbe i valori di arr2 in un nuovo blocco di memoria (poiché non può restituire una vista sui dati originali per questa forma).


Ho difficoltà a capirlo, puoi approfondire un po '? Nell'ultima rappresentazione grafica dell'impossibile ordinamento nella memoria effettivamente i passi sono costanti secondo me. Ad esempio per passare da 0 a 1 il passo è di 1 byte (diciamo che ogni elemento è un byte) ed è lo stesso per ogni colonna. Allo stesso modo il passo è di 4 byte per passare da un elemento della riga a quello successivo ed è anch'esso costante.
Vesnog

2
@Vesnog il rimodellamento non riuscito del 2D arr2alla forma 1D (12,)utilizza l'ordine C, il che significa che quell'asse 1 viene svolto prima dell'asse 0 (cioè ciascuna delle quattro righe deve essere posizionata l'una accanto all'altra per creare l'array 1D desiderato). È impossibile leggere questa sequenza di numeri interi (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) dal buffer utilizzando una lunghezza del passo costante (i byte da saltare per visitare questi elementi in sequenza sarebbero 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy richiede una lunghezza del passo costante per asse.
Alex Riley

Grazie in un primo momento ho pensato che creerà un nuovo array, ma utilizza la memoria di quello vecchio.
Vesnog

@AlexRiley Cosa succede dietro le quinte quando un array è contrassegnato più vicino a C o F ordinato? Ad esempio, prendi ogni array NxD arr e scrivi (arr [:, :: - 1] .flags). Cosa succede in questa situazione? Immagino che l'array sia effettivamente ordinato C o F, ma quale di questi? E quali ottimizzazioni di numpy perdiamo se entrambi i flag sono False?
Jjang

@Jjang: se NumPy considera l'array come C o F, l'ordine dipende interamente dalla forma e dai passi (i criteri sono qui ). Quindi, sebbene arr[:, ::-1]sia una vista dello stesso buffer di memoria di arr, NumPy non considera l'ordine C o F in quanto ha attraversato i valori nel buffer in un ordine "non standard" ...
Alex Riley

12

Forse questo esempio con 12 diversi valori di array aiuterà:

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

I C ordervalori sono nell'ordine in cui sono stati generati. Quelli trasposti non lo sono

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

Puoi ottenere 1d visualizzazioni di entrambi

In [214]: x1=x.T

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

anche la forma xpuò essere modificata.

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

Ma la forma della trasposizione non può essere modificata. Il dataè ancora in 0,1,2,3,4...ordine, che non può essere letta accessibile come 0,4,8...in un array 1D.

Ma una copia di x1può essere modificata:

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

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

Anche guardare stridespotrebbe aiutare. Un passo è quanto lontano (in byte) deve fare un passo per arrivare al valore successivo. Per un array 2d, ci saranno 2 valori di stride:

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

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

Per arrivare alla riga successiva, passo 16 byte, colonna successiva solo 4.

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

Transpose cambia semplicemente l'ordine dei passi. La riga successiva è di soli 4 byte, ovvero il numero successivo.

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

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

Cambiare la forma cambia anche i passi: basta scorrere il buffer di 4 byte alla volta.

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

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

Anche se x2sembra proprio come x1, ha un proprio buffer di dati, con i valori in un ordine diverso. La colonna successiva è ora di 4 byte, mentre la riga successiva è 12 (3 * 4).

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

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

E come con x, cambiare la forma in 1d riduce i passi a (4,).

Poiché x1, con i dati 0,1,2,...nell'ordine, non c'è un passo 1d che darebbe 0,4,8....

__array_interface__ è un altro modo utile per visualizzare le informazioni sull'array:

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

L' x1indirizzo del buffer dei dati sarà lo stesso di x, con cui condivide i dati. x2ha un indirizzo di buffer diverso.

Puoi anche provare ad aggiungere un order='F'parametro ai comandi copye reshape.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.