Соответствующие приложения для функции as_strided

Я тренируюсь, чтобы понять, как использовать функцию as_strided в numpy. Я начал со следующего собственного примера, где я генерирую 5 изображений размером 3 x 3, где каждое изображение заполнено единицами, следующее - двойками и так далее до 5. Если я хочу преобразовать (5,3,3) Volume, чтобы поместить все изображения в одну строку, я могу сделать следующее, и это сработает:

import numpy as np
from numpy.lib.stride_tricks import as_strided

array_3d = np.empty((5, 3, 3))

for i in range(5):
    array_3d[i] = np.full((3, 3), i+1)

print(array_3d.itemsize)

array_2d = as_strided(array_3d, shape=(5, 9), strides=(8*9, 8))

print(array_2d)

Это выводит следующее:

8
[[1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2. 2. 2.]
 [3. 3. 3. 3. 3. 3. 3. 3. 3.]
 [4. 4. 4. 4. 4. 4. 4. 4. 4.]
 [5. 5. 5. 5. 5. 5. 5. 5. 5.]]

Теперь я хотел попробовать более сложный пример размещения изображений (3, 3) рядом следующим образом с использованием функции as_strided:

[[1. 1. 1. 2. 2. 2. 3. 3. 3. 4. 4. 4. 5. 5. 5.]
 [1. 1. 1. 2. 2. 2. 3. 3. 3. 4. 4. 4. 5. 5. 5.]
 [1. 1. 1. 2. 2. 2. 3. 3. 3. 4. 4. 4. 5. 5. 5.]]

Однако я не могу придумать, как сделать это, просто используя функцию as_strided. Поэтому мне интересно, невозможно ли это только с помощью функции as_strided, и как я могу узнать, когда уместно использовать функцию as_strided помимо операций со скользящим окном.

🤔 А знаете ли вы, что...
С Python можно создавать роботов и автоматизированные системы с использованием библиотеки Raspberry Pi.


69
3

Ответы:

Решено

Предположим, что вам действительно нужен массив 3x15, содержащий 9 "1", 9 "2", ...
И совершаете ту же ошибку, что и в первом примере.

st1,st2,st3=array_3d.strides
np.lib.stride_tricks.as_strided(array_3d, shape=(3,15), strides=(st3,st2))

Оно работает. Но при том же оскорбительном предположении, которое вы сделали для себя в первом примере: при предположении, что array_3d данные последовательны. То есть шаги array_3d - это (8*9, 8*3, 8) (шаги трехмерного массива формы (*,3,3) смежных данных с размером данных 8 байт).

array_3d = np.empty((5, 3, 3))
for i in range(5):
    array_3d[i] = np.full((3, 3), i+1)
# Or np.repeat(np.arange(1,6),9).reshape(5,3,3)
st1,st2,st3=array_3d.strides # Following your method, I could have used 8 as st3, and 8*3 for st2. 
      # But I prefer this way, that works better when array_3d is not contiguous
      # even if (see later) it is not sufficient here
as_strided(array_3d, shape=(3,15), strides=(st3,st2))

array([[1., 1., 1., 2., 2., 2., 3., 3., 3., 4., 4., 4., 5., 5., 5.],
       [1., 1., 1., 2., 2., 2., 3., 3., 3., 4., 4., 4., 5., 5., 5.],
       [1., 1., 1., 2., 2., 2., 3., 3., 3., 4., 4., 4., 5., 5., 5.]])

Но если данные не являются смежными, то есть если array_3d[x,y+1,0] не стоит сразу после array_3d[x,y,2], то и ваш код, и мой не работают.

И может быть много причин, почему этого не может быть. Во-первых, потому что это array_3d может быть и результатом другого as_strided. Или какие-то другие более простые операции. Например, reshape, transpose или даже простой подмассив.

Например, представьте, что вместо моего трюка np.repeat с заменой цикла for на конструкцию array_3d я предложил другой трюк

array_3d = np.block([np.arange(1,6)]*9).reshape(3,3,5).T

array_3d это

array([[[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]],

       [[2, 2, 2],
        [2, 2, 2],
        [2, 2, 2]],

       [[3, 3, 3],
        [3, 3, 3],
        [3, 3, 3]],

       [[4, 4, 4],
        [4, 4, 4],
        [4, 4, 4]],

       [[5, 5, 5],
        [5, 5, 5],
        [5, 5, 5]]])

как у тебя или как у np.repeat.

Но его успехи (8,40,120)

Итак, ваш код дает для первого примера

array([[1, 2, 3, 4, 5, 1, 2, 3, 4],
       [5, 1, 2, 3, 4, 5, 1, 2, 3],
       [4, 5, 1, 2, 3, 4, 5, 1, 2],
       [3, 4, 5, 1, 2, 3, 4, 5, 1],
       [2, 3, 4, 5, 1, 2, 3, 4, 5]])

И даже с моим более гибким способом (вычисление целевых шагов с фактическими шагами array_3d, а не предполагаемыми шагами, как если бы array_3d были смежными)

array([[                  1,                   1,                   1,
                       8961, 4426024058838412064, 2314885530453962528,
        2336916743122085218, 8367723430002978409, 8318818597302461550],
       [                  2,                   2,                   2,
                          1, 2842441403096198183, 7307499879225255280,
        6933981515683226426, 7810489018405119336, 2338623232261300325],
       [                  3,                   3,                   3,
                   11162752, 7883868307246424108, 8028827851415429235,
        7958530579755660158, 7575167200708226665, 2334386829830418017],
       [                  4,                   4,                   4,
                       8910, 7310589477035273328, 8386668381597627251,
        7955973979015377710, 8390317583586653294, 7020095223739741549],
       [                  5,                   5,                   5,
                         -1, 8243122375611719780, 7236828443234430825,
        2332938645024170341, 8241988044566393376, 8030041785346585459]])

(Я знаю, это выглядит еще хуже. Но, по крайней мере, первые 3 цифры каждой строки я получил правильно. Тот факт, что остальные - чушь, происходит от того, что 4-я 5 не отделена от 3-й одинаковым объемом памяти чем 3-е отделено 2-м, что является ложным предположением, которое вы делаете).

Аналогично и для второго примера с моим кодом.

Я мог бы проделать ту же демонстрацию, но проще array_3d. Например, если вы построили (5,4,4) big_array_3d, используя свой метод, а затем извлекли из него (5,3,3) array_3d = big_array_3d[:,:3,:3]. На самом деле это не какой-то хакерский хитроумный трюк, не так ли? Это вполне обычный кусок кода. И все же array_3d не является непрерывным набором данных.

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

Массив в numpy — это набор данных с типом, формой и некоторыми шагами, рассказывающими, как перевести индекс [i1,i2,i3,...] в одни из данных. Использование формулы array[i1,i2,i3,...,ik] — это dtype, найденный по адресу baseAddress + i1*stride1 + i2*stride2 + i3*stride3 + ... + ik*stridek. as_strided позволяет вам воспользоваться тем фактом, что вы это понимаете, для изменения формы и шагов массива, чтобы показать различные правила индексации (как это делает transpose. Или, в зависимости от условий, reshape, indexing, moveaxis, ... as_strided просто самая универсальная версия тех функций, которые не меняют данные, только форму и шаги)

Но это означает, что для того, чтобы проделать трюк с as_strided, вам нужно найти набор шагов, который позволит baseAddress+i1*stride1+i2*stride2+...+ik*stridek стать тем, кем вы хотите view[i1,i2,...,ik] быть. А это значит, что такие шаги должны быть. И это не всегда так. Не любая перестановка данных может быть выражена в виде формулы линейного индексирования «α.i1+β.i2+...» (α, β, ... — это шаги)


Создадим немного другой массив — без fill/repeat, чтобы мы могли сказать, как упорядочены массивы (3,3):

In [128]: array_3d = np.empty((4, 3, 3))
     ...: for i in range(4):
     ...:     array_3d[i] = np.arange(9).reshape(3,3)+i*10
     ...:     

In [129]: array_3d
Out[129]: 
array([[[ 0.,  1.,  2.],
        [ 3.,  4.,  5.],
        [ 6.,  7.,  8.]],

       [[10., 11., 12.],
        [13., 14., 15.],
        [16., 17., 18.]],

       [[20., 21., 22.],
        [23., 24., 25.],
        [26., 27., 28.]],

       [[30., 31., 32.],
        [33., 34., 35.],
        [36., 37., 38.]]])

Мы можем получить ваш первый макет, просто изменив форму:

In [130]: array_3d.reshape(4,-1)
Out[130]: 
array([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.],
       [10., 11., 12., 13., 14., 15., 16., 17., 18.],
       [20., 21., 22., 23., 24., 25., 26., 27., 28.],
       [30., 31., 32., 33., 34., 35., 36., 37., 38.]])

Добавьте транспозицию, мы сможем объединить ваши блоки:

In [131]: array_3d.transpose(1,0,2).reshape(3,-1)
Out[131]: 
array([[ 0.,  1.,  2., 10., 11., 12., 20., 21., 22., 30., 31., 32.],
       [ 3.,  4.,  5., 13., 14., 15., 23., 24., 25., 33., 34., 35.],
       [ 6.,  7.,  8., 16., 17., 18., 26., 27., 28., 36., 37., 38.]])

А при другом транспонировании расположить блоки по-другому. Это различие теряется, когда блоки «заполнены»:

In [132]: array_3d.transpose(2,0,1).reshape(3,-1)
Out[132]: 
array([[ 0.,  3.,  6., 10., 13., 16., 20., 23., 26., 30., 33., 36.],
       [ 1.,  4.,  7., 11., 14., 17., 21., 24., 27., 31., 34., 37.],
       [ 2.,  5.,  8., 12., 15., 18., 22., 25., 28., 32., 35., 38.]])

[132] на самом деле является видом array_3d (которое мы можем изменить, проверив его .base).

[131] тоже view, но с основанием (3,4,3), транспонированием. Изменение формы после транспонирования часто включает в себя своего рода копирование.

Мы также можем получить макет [131] с помощью concatenate:

In [143]: np.concatenate(array_3d, axis=1)
Out[143]: 
array([[ 0.,  1.,  2., 10., 11., 12., 20., 21., 22., 30., 31., 32.],
       [ 3.,  4.,  5., 13., 14., 15., 23., 24., 25., 33., 34., 35.],
       [ 6.,  7.,  8., 16., 17., 18., 26., 27., 28., 36., 37., 38.]])

as_strided

as_strided чаще всего используется для разбиения массива на более мелкие части, а не для уменьшения размеров. np.lib.stride_tricks.sliding_window_view — более «безопасная» и простая в использовании версия. Помимо прочего, это не позволяет нам выполнять выборку за пределами исходного data_buffer.

Тем не менее, as_strided можно использовать для получения 2D-макетов.

Как вы обнаружили, простое изменение формы можно воспроизвести с помощью:

In [145]: np.lib.stride_tricks.as_strided(array_3d,shape=(4,9), strides=(9*8,8))
Out[145]: 
array([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.],
       [10., 11., 12., 13., 14., 15., 16., 17., 18.],
       [20., 21., 22., 23., 24., 25., 26., 27., 28.],
       [30., 31., 32., 33., 34., 35., 36., 37., 38.]])

И, используя разные шаги, получаем [132]:

In [149]: np.lib.stride_tricks.as_strided(array_3d,shape=(3,12), strides=(8,3*8))
Out[149]: 
array([[ 0.,  3.,  6., 10., 13., 16., 20., 23., 26., 30., 33., 36.],
       [ 1.,  4.,  7., 11., 14., 17., 21., 24., 27., 31., 34., 37.],
       [ 2.,  5.,  8., 12., 15., 18., 22., 25., 28., 32., 35., 38.]])

Обратите внимание, как последовательные числа [0,1,2,3...] упорядочены по столбцам order='F'. Это типично для случаев, когда отстающие шаги больше.

Я не думаю, что макета [131] можно добиться с помощью as_strided. Как я уже заметил, это не представление исходного массива.

создание (3,3) окон

Чтобы проиллюстрировать, как as_strided можно использовать для разбиения массива на небольшие блоки, мы можем использовать sliding_window_view для создания блоков (3,3) из 2d-массива [131]:

In [152]: np.lib.stride_tricks.sliding_window_view(_131,(3,3)).shape
Out[152]: (1, 10, 3, 3)

Вы можете распечатать его, чтобы увидеть, как эти блоки «перекрываются/скользят», но, взяв каждый третий блок, мы восстанавливаем исходный трехмерный массив:

In [153]: np.lib.stride_tricks.sliding_window_view(_131,(3,3))[0,:3]
Out[153]: 
array([[[ 0.,  1.,  2.],
        [ 3.,  4.,  5.],
        [ 6.,  7.,  8.]],

       [[ 1.,  2., 10.],
        [ 4.,  5., 13.],
        [ 7.,  8., 16.]],

       [[ 2., 10., 11.],
        [ 5., 13., 14.],
        [ 8., 16., 17.]]])

И делая то же самое для [132], получаем оригинал, но с транспонированными блоками:

In [154]: np.lib.stride_tricks.sliding_window_view(_132,(3,3))[0,:3]
Out[154]: 
array([[[ 0.,  3.,  6.],
        [ 1.,  4.,  7.],
        [ 2.,  5.,  8.]],

       [[ 3.,  6., 10.],
        [ 4.,  7., 11.],
        [ 5.,  8., 12.]],

       [[ 6., 10., 13.],
        [ 7., 11., 14.],
        [ 8., 12., 15.]]])

В документации sliding_window_view есть несколько хороших примеров использования и предостережения.

заполнить/повторить

Построение заполнения/повтора можно выполнить с помощью as_strided.

Начните с простого массива 1d:

In [163]: arr = np.arange(4)

Мы можем использовать другую функцию stride_tricks для «реплицирования» значений:

In [164]: np.broadcast_to(arr,(3,3,4))
Out[164]: 
array([[[0, 1, 2, 3],
        [0, 1, 2, 3],
        [0, 1, 2, 3]],

       [[0, 1, 2, 3],
        [0, 1, 2, 3],
        [0, 1, 2, 3]],

       [[0, 1, 2, 3],
        [0, 1, 2, 3],
        [0, 1, 2, 3]]])

а затем транспонируйте, чтобы получить тот же узор, который вы создали:

In [165]: np.broadcast_to(arr,(3,3,4)).transpose(2,0,1)
Out[165]: 
array([[[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]],

       [[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]],

       [[2, 2, 2],
        [2, 2, 2],
        [2, 2, 2]],

       [[3, 3, 3],
        [3, 3, 3],
        [3, 3, 3]]])

Обратите внимание на strides этого массива:

In [166]: np.broadcast_to(arr,(3,3,4)).transpose(2,0,1).strides
Out[166]: (4, 0, 0)

Мы можем произвести то же самое с помощью as_strided:

In [167]: np.lib.stride_tricks.as_strided(arr,(4,3,3),(4,0,0))
Out[167]: 
array([[[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]],

       [[1, 1, 1],
        [1, 1, 1],
        [1, 1, 1]],

       [[2, 2, 2],
        [2, 2, 2],
        [2, 2, 2]],

       [[3, 3, 3],
        [3, 3, 3],
        [3, 3, 3]]])

broadcast_to использует другой инструмент, nditer для достижения этой цели. Я подозреваю, что они делают это, чтобы избежать проблем с записью.

Нам не часто нужно использовать broadcast_to; достаточно просто добавить размеры (плюс правила broadcasting):

In [173]: arr[:,None,None]
Out[173]: 
array([[[100]],

       [[100]],

       [[100]],

       [[100]]])

In [174]: _.strides
Out[174]: (4, 0, 0) 

У него те же ножки, но форма (4,1,1).


import numpy as np

# Create the 3D array (5 images of size 3x3)
array_3d = np.empty((5, 3, 3))
for i in range(5):
    array_3d[i] = np.full((3, 3), i+1)

# Print the original 3D array
print("Original 3D array:")
print(array_3d)
'''
Output:
[[[1. 1. 1.]
  [1. 1. 1.]
  [1. 1. 1.]]

 [[2. 2. 2.]
  [2. 2. 2.]
  [2. 2. 2.]]

 [[3. 3. 3.]
  [3. 3. 3.]
  [3. 3. 3.]]

 [[4. 4. 4.]
  [4. 4. 4.]
  [4. 4. 4.]]

 [[5. 5. 5.]
  [5. 5. 5.]
  [5. 5. 5.]]]
'''
print("Shape of the 3D array:", array_3d.shape)  # Output: (5, 3, 3)

# Step 1: Reshape the 3D array into a 2D array where each 3x3 image is flattened into a single row
flattened_2d = array_3d.reshape(5, 3 * 3)
print("Flattened 2D array:")
print(flattened_2d)
'''
Output:
[[1. 1. 1. 1. 1. 1. 1. 1. 1.]
 [2. 2. 2. 2. 2. 2. 2. 2. 2.]
 [3. 3. 3. 3. 3. 3. 3. 3. 3.]
 [4. 4. 4. 4. 4. 4. 4. 4. 4.]
 [5. 5. 5. 5. 5. 5. 5. 5. 5.]]
'''

# Step 2: Transpose the 2D array to switch rows and columns
transposed_2d = flattened_2d.T
print("Transposed 2D array:")
print(transposed_2d)
'''
Output:
[[1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5.]]
'''

# Step 3: Reshape the transposed array to arrange the images side by side
side_by_side_2d = transposed_2d.reshape(3, 5 * 3)
print("Final 2D array with images side by side:")
print(side_by_side_2d)
'''
Output:
[[1. 2. 3. 4. 5. 1. 2. 3. 4. 5. 1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5. 1. 2. 3. 4. 5. 1. 2. 3. 4. 5.]
 [1. 2. 3. 4. 5. 1. 2. 3. 4. 5. 1. 2. 3. 4. 5.]]
'''