Jaka jest różnica między ciągłymi i nieciągłymi tablicami?

107

W instrukcji numpy o funkcji reshape () jest napisane

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

Moje pytania to:

  1. Co to są tablice ciągłe i nieciągłe? Czy jest podobny do ciągłego bloku pamięci w C, na przykład Co to jest ciągły blok pamięci?
  2. Czy jest jakaś różnica w wydajności między tymi dwoma? Kiedy powinniśmy użyć jednego lub drugiego?
  3. Dlaczego transpozycja powoduje, że tablica nie jest ciągła?
  4. Dlaczego c.shape = (20)zgłasza błąd incompatible shape for a non-contiguous array?

Dzięki za odpowiedź!

jdeng
źródło

Odpowiedzi:

228

Ciągła tablica to po prostu tablica przechowywana w nieprzerwanym bloku pamięci: aby uzyskać dostęp do następnej wartości w tablicy, po prostu przechodzimy do następnego adresu pamięci.

Rozważ tablicę 2D arr = np.arange(12).reshape(3,4). To wygląda tak:

wprowadź opis obrazu tutaj

W pamięci komputera wartości arrsą przechowywane w następujący sposób:

wprowadź opis obrazu tutaj

Oznacza to, że arrjest to ciągła tablica C, ponieważ wiersze są przechowywane jako ciągłe bloki pamięci. Następny adres pamięci przechowuje wartość następnego wiersza w tym wierszu. Jeśli chcemy przeskoczyć kolumnę w dół, wystarczy przeskoczyć przez trzy bloki (np. Przeskoczenie z 0 na 4 oznacza, że ​​przeskakujemy o 1, 2 i 3).

Transpozycja tablicy za arr.Tpomocą oznacza, że ​​ciągłość C jest tracona, ponieważ sąsiednie wpisy wierszy nie znajdują się już w sąsiednich adresach pamięci. Jednak arr.Tczy Fortran jest ciągły, ponieważ kolumny są w ciągłych blokach pamięci:

wprowadź opis obrazu tutaj


Pod względem wydajności dostęp do adresów pamięci, które są obok siebie, jest bardzo często szybszy niż uzyskiwanie dostępu do adresów, które są bardziej „rozłożone” (pobieranie wartości z pamięci RAM może wiązać się z pobieraniem wielu sąsiednich adresów i zapisywaniem ich w pamięci podręcznej dla procesora). oznacza, że ​​operacje na sąsiadujących tablicach często będą szybsze.

W konsekwencji ciągłego układu pamięci w języku C operacje na wierszach są zwykle szybsze niż operacje na kolumnach. Na przykład zazwyczaj to znajdziesz

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

jest nieco szybszy niż:

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

Podobnie operacje na kolumnach będą nieco szybsze w przypadku ciągłych tablic w języku Fortran.


Wreszcie, dlaczego nie możemy spłaszczyć ciągłej tablicy Fortran przez przypisanie nowego kształtu?

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

Aby to było możliwe, NumPy musiałby złożyć takie rzędy w arr.Tnastępujący sposób:

wprowadź opis obrazu tutaj

(Ustawienie shapeatrybutu bezpośrednio zakłada kolejność C - tj. NumPy próbuje wykonać operację w wierszach).

To niemożliwe. Dla każdej osi NumPy musi mieć stałą długość kroku (liczbę bajtów do przesunięcia), aby dostać się do następnego elementu tablicy. Spłaszczeniearr.T w ten sposób wymagałoby przeskakiwania do przodu i do tyłu w pamięci w celu pobrania kolejnych wartości tablicy.

Gdybyśmy arr2.reshape(12)zamiast tego napisali , NumPy skopiowałby wartości arr2 do nowego bloku pamięci (ponieważ nie może zwrócić widoku oryginalnych danych dla tego kształtu).

Alex Riley
źródło
Mam trudności ze zrozumieniem tego, czy możesz to trochę rozwinąć? W najnowszym graficznym przedstawieniu niemożliwego uporządkowania w pamięci, moim zdaniem, kroki są stałe. Na przykład, aby przejść od 0 do 1, krok wynosi 1 bajt (powiedzmy, że każdy element jest bajtem) i jest taki sam dla każdej kolumny. Podobnie krok wynosi 4 bajty na przejście z jednego elementu w wierszu do następnego i jest również stały.
Vesnog
3
@Vesnog nieudana zmiana kształtu 2D arr2na 1D (12,)używa kolejności C, co oznacza, że ​​ta oś 1 jest rozwijana przed osią 0 (tj. Każdy z czterech rzędów musi być umieszczony obok siebie, aby utworzyć pożądaną tablicę 1D). Niemożliwe jest odczytanie tej sekwencji liczb całkowitych (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) z bufora przy użyciu stałej długości kroku (bajty do przejścia do odwiedzenia te elementy w kolejności to 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy wymaga stałej długości kroku na oś.
Alex Riley
Dzięki na początku myślałem, że utworzy nową tablicę, ale używa pamięci starej.
Vesnog
@AlexRiley Co dzieje się za kulisami, gdy tablica jest oznaczona jako mniejsza w kolejności C lub F? Na przykład weź każdą tablicę NxD arr i print (arr [:, :: - 1] .flags). Co się dzieje w tej sytuacji? Domyślam się, że tablica jest rzeczywiście uporządkowana w C lub F, ale która z nich? A które optymalizacje numpy tracimy, jeśli obie flagi są fałszywe?
Jjang
@Jjang: to, czy NumPy uważa, że ​​tablica jest C lub F, zależy całkowicie od kształtu i kroków (kryteria są tutaj ). Więc chociaż arr[:, ::-1]jest to widok tego samego bufora pamięci, co arrNumPy nie bierze go pod uwagę w kolejności C lub F, ponieważ przechodził przez wartości w buforze w "niestandardowej" kolejności ...
Alex Riley
13

Może ten przykład z 12 różnymi wartościami tablicowymi pomoże:

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

Te C orderwartości są w kolejności, w jakiej zostały one uzyskane w. Transponowanego te nie są

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

Możesz uzyskać 1d widoki obu

In [214]: x1=x.T

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

kształt xmożna również zmienić.

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

Ale kształtu transpozycji nie można zmienić. dataJest jeszcze w 0,1,2,3,4...porządku, który nie może być dostępna jako dostęp 0,4,8...w 1d tablicy.

Ale kopię x1można zmienić:

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

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

Patrzenie stridesmoże też pomóc. Strides to odległość (w bajtach), jaką musi przejść, aby przejść do następnej wartości. Dla tablicy 2d będą dwie wartości krokowe:

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

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

Aby przejść do następnego wiersza, krok 16 bajtów, następna kolumna tylko 4.

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

Transpozycja zmienia tylko kolejność kroków. Następny wiersz to tylko 4 bajty - czyli następna liczba.

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

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

Zmiana kształtu zmienia również kroki - po prostu przechodź przez bufor po 4 bajty na raz.

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

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

Mimo że x2wygląda tak samo x1, ma swój własny bufor danych, z wartościami w innej kolejności. Następna kolumna ma teraz 4 bajty więcej, a następny wiersz to 12 (3 * 4).

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

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

I tak jak w przypadku xzmiany kształtu na 1d zmniejsza liczbę kroków do (4,).

Ponieważ x1przy danych w 0,1,2,...kolejności nie ma kroku 1d, który by dał 0,4,8....

__array_interface__ to kolejny przydatny sposób wyświetlania informacji o tablicy:

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

x1Adres bufora danych będzie taka sama jak w przypadku x, z którym dzieli dane. x2ma inny adres bufora.

Możesz także poeksperymentować z dodaniem order='F'parametru do poleceń copyi reshape.

hpaulj
źródło