У чому різниця між суміжними та несуміжними масивами?


101

У посібнику numpy про функцію reshape () сказано

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

Мої запитання:

  1. Що таке безперервні та несуміжні масиви? Чи подібний він до суміжного блоку пам'яті на мові C, наприклад Що таке суміжний блок пам'яті?
  2. Чи існує якась різниця в продуктивності між цими двома? Коли нам слід користуватися тим чи іншим?
  3. Чому транспонування робить масив несуміжним?
  4. Чому c.shape = (20)виникає помилка incompatible shape for a non-contiguous array?

Дякую за вашу відповідь!

Відповіді:


222

Суміжний масив - це просто масив, що зберігається в незламному блоці пам'яті: для доступу до наступного значення в масиві ми просто переходимо до наступної адреси пам'яті.

Розглянемо 2D-масив arr = np.arange(12).reshape(3,4). Це виглядає так:

введіть тут опис зображення

У пам’яті комп’ютера значення значень arrзберігаються так:

введіть тут опис зображення

Це означає arr, що це C суміжний масив, оскільки рядки зберігаються як суміжні блоки пам'яті. Наступна адреса пам'яті містить значення наступного рядка в цьому рядку. Якщо ми хочемо рухатись по колонці вниз, нам просто потрібно перестрибнути через три блоки (наприклад, перехід від 0 до 4 означає, що ми пропускаємо через 1,2 і 3).

Транспонування масиву arr.Tозначає, що суміжність C втрачається, оскільки сусідні записи рядків більше відсутні в сусідніх адресах пам'яті. Однак чи arr.Tє Фортран суміжним, оскільки стовпці знаходяться у суміжних блоках пам'яті:

введіть тут опис зображення


З точки зору продуктивності доступ до адрес пам'яті, які знаходяться поруч, дуже часто є швидшим, ніж доступ до адрес, які є більш "розкинутими" (отримання значення з оперативної пам'яті може спричинити за собою кількість сусідніх адрес, які отримуються та кешуються для ЦП.) означає, що операції над суміжними масивами часто будуть швидшими.

Як наслідок суміжного розташування пам’яті C, операції по рядках, як правило, швидші, ніж операції по стовпцях. Наприклад, ви зазвичай це знайдете

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

трохи швидше, ніж:

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

Подібним чином, операції з колонами будуть дещо швидшими для суміжних масивів Fortran.


Нарешті, чому ми не можемо згладити сусідній масив Fortran, присвоївши нову форму?

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

Для того, щоб це стало можливим, NumPy довелося б складати рядки arr.Tтак:

введіть тут опис зображення

(Встановлення shapeатрибуту безпосередньо передбачає порядок C - тобто NumPy намагається виконати операцію по порядку.)

Це неможливо зробити. Для будь-якої осі NumPy повинна мати постійну довжину кроку (кількість байтів для переміщення), щоб дістатися до наступного елемента масиву. Згладжування arr.Tтаким чином вимагало б пропуску вперед і назад у пам’яті для отримання послідовних значень масиву.

Якби ми писали arr2.reshape(12)замість цього, NumPy скопіював би значення arr2 у новий блок пам'яті (оскільки він не може повернути подання на вихідні дані для цієї фігури).


У мене виникають труднощі з розумінням цього, можете, будь ласка, трохи детальніше розказати? В останньому графічному поданні неможливого впорядкування в пам'яті насправді кроки постійні, на мій погляд. Наприклад, щоб перейти від 0 до 1, крок становить 1 байт (припустимо, кожен елемент є байтом), і він однаковий для кожного стовпця. Подібним чином крок становить 4 байти для переходу від одного елемента в рядку до наступного, і він також є постійним.
Весног

2
@Vesnog при невдалому перетворенні 2D arr2на 1D фігуру (12,)використовується порядок C, що означає, що ця вісь 1 розмотується перед віссю 0 (тобто кожен з чотирьох рядків повинен бути розміщений поруч один для створення бажаного 1D масиву). Неможливо прочитати цю послідовність цілих чисел (0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11) з буфера, використовуючи постійну довжину кроку (байти для переходу до відвідування ці елементи послідовно були б 4, 4, -7, 4, 4, -7, 4, 4, 7, 4, 4). NumPy вимагає постійної довжини кроку на вісь.
Алекс Райлі,

Спочатку дякую, я думав, що це створить новий масив, але він використовує пам’ять старого.
Весног

@AlexRiley Що відбувається за лаштунками, коли масив позначений поряд із C або F? Наприклад, візьмемо кожен масив NxD arr та надрукуємо (arr [:, :: - 1] .flags). Що відбувається в цій ситуації? Я думаю, що масив справді впорядкований C або F, але який із них? І які оптимізації numpy ми втрачаємо, якщо обидва прапори неправдиві?
Jjang

@Jjang: чи вважає NumPy масив C чи F порядком, це повністю залежить від форми та кроків (критерії тут ). Отож, хоча arr[:, ::-1]це вигляд того самого буфера пам'яті arr, що і NumPy, не вважає його порядком C або F, оскільки він перетинає значення в буфері в "нестандартному" порядку ...
Алекс Райлі,

12

Можливо, цей приклад з 12 різними значеннями масиву допоможе:

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

Ці C orderзначення в тому порядку , що вони були зроблені в. Транспоновану з них не є

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

Ви можете отримати 1d перегляд обох

In [214]: x1=x.T

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

форму xтакож можна змінити.

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

Але форму транспонування не можна змінити. Значення dataвсе ще знаходиться в 0,1,2,3,4...порядку, доступ до якого неможливий, як 0,4,8...у масиві 1d.

Але копію x1можна змінити:

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

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

Перегляд stridesможе також допомогти. Кроки - це те, наскільки далеко (у байтах) потрібно пройти крок, щоб перейти до наступного значення. Для двовимірного масиву буде 2 значення кроку:

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

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

Щоб перейти до наступного рядка, виконайте крок 16 байт, лише наступний стовпець 4.

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

Транспонування просто перемикає порядок кроків. Наступний рядок - лише 4 байти - тобто наступне число.

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

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

Зміна форми також змінює кроки - просто переходьте буфер за 4 байти за раз.

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

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

Незважаючи на те, x2виглядає так само , як x1це має свій власний буфер даних, зі значеннями в іншому порядку. Наступний стовпець тепер має 4 байти, тоді як наступний рядок - 12 (3 * 4).

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

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

І як і у випадку x, зміна форми на 1d зменшує кроки до (4,).

Адже x1, маючи дані в 0,1,2,...порядку, немає 1-го кроку, який дав би 0,4,8....

__array_interface__ є ще одним корисним способом відображення інформації про масив:

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

Адреса x1буфера даних буде такою ж, як і для x, з якою він надає дані. x2має іншу адресу буфера.

Ви також можете поекспериментувати з додаванням order='F'параметра до команд copyі reshape.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.