Почему NumPy создает представление для x[[slice(None), 1, 2]]

В документации NumPy для расширенной индексации упоминается, что

Также признать, что x[[1, 2, 3]] вызовет расширенную индексацию, тогда как x[[1, 2, slice(None)]] вызовет базовую нарезку.

Матрица хранится последовательно в памяти. Я понимаю, что имеет смысл взглянуть на x[[1, 2, slice(None)]] так как элементы хранятся последовательно в памяти. Но почему Numpy возвращает взгляд на x[[1, slice(None), 2]] или же x[[slice(None), 1, 2]], Например, предположим,

x = [[[ 0,  1,  2],
      [ 3,  4,  5],
      [ 6,  7,  8]],
     [[ 9, 10, 11],
      [12, 13, 14],
      [15, 16, 17]],
     [[18, 19, 20],
      [21, 22, 23],
      [24, 25, 26]]]

x[[1, slice(None), 2]] возвращает вид [11, 14, 17] который не хранится последовательно в памяти, а также для x[[slice(None), 1, 2]] который возвращается [5, 14, 23],

я бы хотел знать

  1. Почему NumPy даже возвращает представление в этих двух случаях

  2. Как NumPy обрабатывает адресацию памяти для создания этих представлений

2 ответа

Решение

Из кулинарной книги SciPy:

Основное правило создания вида среза заключается в том, что к просматриваемым элементам можно обращаться со смещениями, шагами и счетчиками в исходном массиве.

Когда у вас есть индексирование, как x[[1, slice(None), 2]], вы получаете представление, потому что разрезание всей оси допускает определенное смещение, шаг и счет для представления среза с исходным массивом.

Например, с x = np.arange(27).reshape(3, 3, 3).copy(), у нас есть:

In [79]: x_view = x[1, :, 2]  # or equivalently x[[1, slice(None), 2]]

In [80]: x_view
Out[80]: array([11, 14, 17])

In [81]: x_view.base
Out[81]: 
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8]],

       [[ 9, 10, 11],
        [12, 13, 14],
        [15, 16, 17]],

       [[18, 19, 20],
        [21, 22, 23],
        [24, 25, 26]]])

Тогда мы можем использовать numpy.byte_bounds (не является частью общедоступного API, YMMV), чтобы проиллюстрировать смещение, чтобы получить наш фрагмент из нашего исходного массива.

In [82]: np.byte_bounds(x_view)[0] - np.byte_bounds(x_view.base)[0]
Out[82]: 88

Это имеет смысл, поскольку перед первым значением в срезе стоят 11 8-байтовых целых чисел, 11. NumPy вычисляет это смещение по формуле, которую вы можете увидеть здесь, используя шаги исходного массива.

In [93]: (x.strides * np.array([1, 0, 2])).sum()
Out[93]: 88

Шаги в нашей части просто становятся тем, к чему они стремились x вдоль оси (или осей), на которой мы нарезаем. т.е. x.strides[1] == x_view.strides[0], Теперь вместе смещение, новые шаги и количество являются достаточной информацией для NumPy, чтобы просмотреть наш срез из нашего исходного массива.

In [94]: x_view.strides
Out[94]: (24,)

In [95]: x_view.size
Out[95]: 3

Наконец, причина, по которой вы запускаете модную индексацию с помощью x[[0, 1, 2]] Например, потому что в отсутствие полного среза оси, как правило, невозможно сформулировать какое-либо новое смещение, порядок байтов, шаг и счет, чтобы мы могли просматривать срез с теми же базовыми данными.

Я люблю использовать __array_interface__ изучить атрибуты массива:

С вашим x:

In [51]: x.__array_interface__
Out[51]: 
{'data': (43241792, False),
 'strides': None,
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3, 3, 3),
 'version': 3}
In [52]: x.strides
Out[52]: (72, 24, 8)

Это (3,3,3) массив. Последняя ось может быть отсканирована, шагая по 8 байт за раз, размер x.itemsize, 3*8 шагов по рядам и 3*3*8 шагов по плоскостям (1-й тусклый).

In [53]: y = x[:,1,2]
In [54]: y.shape
Out[54]: (3,)
In [55]: y.strides
Out[55]: (72,)
In [56]: y.__array_interface__['data']
Out[56]: (43241832, False)

y элементы можно адресовать пошагово по плоскостям, 3*3*8. 43241832 является отправной точкой, 40 байтов в буфер данных, 5*8

In [59]: y
Out[59]: array([ 5, 14, 23])

Таким образом, он начинается с 5-го элемента и переходит на одну плоскость за раз (9 элементов), всего 3 элемента.

Дело в том, что y.__array_interface__['data'] попадает в диапазон x "данные" говорит мне, что y это вид. Это представление, потому что комбинация этой начальной точки буфера, шагов и формы позволяет нам получить доступ ко всем значениям y,

При расширенном индексировании невозможно (в общем) получить доступ к элементам с этими простыми параметрами, поэтому numpy должен сделать копию данных.


Обратное представление возможно, просто изменив шаги и начальную точку "данных":

In [60]: z = y[::-1]
In [61]: z.__array_interface__
Out[61]: 
{'data': (43241976, False),
 'strides': (-72,),
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3,),
 'version': 3}

Транспонировать также меняет шаги:

In [62]: x.T.strides
Out[62]: (8, 24, 72)
Другие вопросы по тегам