Объединение представлений в NumPy

#python #arrays #numpy #reference #slice

#python #массивы #numpy #ссылка #срез

Вопрос:

Индексирование массива NumPy с помощью срезов / индексов создает облегченное представление (не копирует данные) и позволяет присваивать элементам исходного массива. Т.е.

 import numpy as np
a = np.array([1, 2, 3, 4, 5])
a[2:4] = [6, 7]
print(a)
# [1 2 6 7 5]
  

Но как насчет нескольких представлений, как мне объединить их, чтобы создать большее представление, которое по-прежнему присваивается исходному первому массиву. Например. для мнимой функции concatenate_views(...) :

 a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
concatenate_views((a[1:3], a[4:6], a[7:9])) = [11, 12, 13, 14, 15, 16]
print(a)
# should print [1 11 12 4 13 14 7 15 16 10]
  

Конечно, я могу создать список индексов для каждого представления, которое он просматривает, просто преобразовав фрагменты в индексы, а затем объединив эти индексы. Таким образом, я получу все индексы объединенных представлений и смогу использовать эти индексы для создания объединенного представления. Но это не то, чего я хочу. Я хочу, чтобы NumPy сохранял понятие представления срезов, потому что все срезы могут быть очень длинными, и преобразование и сохранение этих срезов в виде индексов будет неэффективным. Я хочу, чтобы NumPy знал о базовых фрагментах всех объединенных представлений, чтобы внутренне выполнять простой цикл диапазонов фрагментов.

Также было бы неплохо обобщить проблему. Не только объединяет представления, но также позволяет формировать любое произвольное дерево операций нарезки / индексирования, например, объединять представления, затем применять некоторую нарезку, затем индексирование, затем нарезку, затем снова объединить. Также N-мерная нарезка / индексирование. Т.Е. Все причудливые вещи, которые можно сделать с помощью одного несвязанного представления.

Основным моментом объединенных представлений является только эффективность. Конечно, мы можем представить любое представление или операцию нарезки N-D массивом целочисленных индексов (координат, например, meshgrid), а затем можем использовать этот массив для создания представления исходного массива. Но если numpy может сохранить понятие исходного набора фрагментов вместо массива целых чисел, то, во-первых, он будет легким (намного меньше потребления памяти), во-вторых, вместо чтения индексов из памяти numpy внутренне может выполнять цикл (итерацию) через каждый фрагмент более эффективно в C циклах.

Имея объединенный вид, я хотел иметь возможность эффективно применять любую операцию numpy, например np.mean(...) , к объединенному виду.

Ниже описана полная процедура объединения представлений N-D нарезки на основе 2D-примера:

     1 Step described below:
    
    2D array slicing using 3 slices for each axis
    
    a,b,c - sizes of "slices" along axis 0
    d,e,f - sizes of "slices" along axis 1
    
    Each "slice" - is either slice(start, stop, step) or 1D array of integer indexes
            
      d e f
     .......
    a.0.1.2.
     .......
    b.3.4.5.
     .......
    c.6.7.8.
     .......

    Above 0 1 2 3 4 5 6 7 8 mean not a single integer but some 2D sub-array.
    Dots (`.`) also mean some 2D sub-arrays.
    
    Sub-views shapes:
    0:(a, d), 1:(a, e), 2:(a, f)
    3:(b, d), 4:(b, e), 5:(b, f)
    6:(c, d), 7:(c, e), 8:(c, f)

    Final aggregated (concatenated) view shape:
    ((a   b   c), (d   e   f))
    containing 2D array
    012
    345
    678
    
    There can be more than one Steps, each next Step applies new sequence of slicing
    to the final view obtained on previous Step. Each Step has different set of sizes
    of slices and different amount of slices per each dimension.
    In general each next Step reduces number of total elements, except the case
    when slices or indexes overlap then you may get more elements but with duplicates.
  

Комментарии:

1. Всегда ли это будут равные интервальные фрагменты?

2. @Divakar Это может упростить решение проблемы. Но я хочу какую-то общую функцию. Это позволяет формировать любое дерево нарезки. Т.Е. Применять срезы к срезам. Примените индексацию к фрагментам и т. Д. То же самое, что можно сделать с одним несвязанным представлением. Также N-D нарезка.

3. Затем просто создайте логическую маску, инициализированную нулями, и итеративно присваивайте True для этих фрагментов. Наконец-то a[mask] = new_values .

4. В моем вопросе выше я упоминаю этот случай, говоря, что я действительно хочу, чтобы NumPy сохранял понятие нарезки исходного кода вместо преобразования в простые индексы или bools, потому что массивы могут быть очень огромными, а индексы не только потребляют много памяти, но и медленно обрабатываются. Если NumPy сохраняет исходную нарезку, то внутренне он может просто организовать, возможно, рекурсивные циклы по диапазонам фрагментов. Таким образом, срезы позволяют использовать быстрые и легкие алгоритмы внутри NumPy.

5. Маскирование не так плохо, как целочисленное индексирование, а логические маски имеют 1/8-ю нагрузку на память по сравнению с массивом int равной длины. Не просто переходите к выводам, не опробовав то, что подходит для вашего случая. Наконец, если у вас длинные фрагменты, вы можете просто выполнить итерацию и назначить.

Ответ №1:

Вы можете использовать np.r_ для объединения объектов среза и присвоения обратно индексированному массиву:

 a[np.r_[1:3, 4:6, 7:9]] = [11, 12, 13, 14, 15, 16]

print(a)
array([ 1, 11, 12,  4, 13, 14,  7, 15, 16, 10])
  

Обновить

Основываясь на вашем обновлении, я думаю, вам может понадобиться что-то вроде:

 from itertools import islice

it = iter([11, 12, 13, 14, 15, 16])
for s in slice(1,3), slice(4,6), slice(7,9):
    a[s] = list(islice(it, s.stop-s.start))

print(a)
array([ 1, 11, 12,  4, 13, 14,  7, 15, 16, 10])
  

Комментарии:

1. Я обновил свой вопрос большим абзацем, в котором говорится, что я не хочу преобразовывать фрагменты в индексы для повышения эффективности, чтобы numpy сохранял информацию о базовых фрагментах и создавал простой внутренний цикл по исходным диапазонам фрагментов.

2. Также возможно ли создать дерево нарезки? Т.е. Применить нарезку срезов. Также N-D нарезка. И т.д. все необычные вещи, которые можно использовать с обычным несвязанным представлением.

3. Обновлено @arty Дайте мне знать, если это то, что вам нужно

4. Да, это хороший подход, в принципе, если у меня есть дерево операций нарезки / индексации, я могу вручную применить это нарезание один за другим. Тем не менее, это может быть неэффективно, если также использовать индексацию в сочетании с нарезкой, а не только только нарезку. Также алгоритм должен каким-то образом учитывать нарезку фрагментов.

5. @Искусство. Все, что вы хотите, кодируется в одной операции: объединение двух представлений в другое представление. Тогда это представление само по себе является массивом, с которым вы можете делать все, что захотите: изменять форму, индексировать или объединять с дополнительным представлением среза.

Ответ №2:

Вы можете объединять представления, только если они являются смежными с точки зрения dtypes, шагов и смещений. Вот один из способов проверить. Этот способ, вероятно, неполный, но он иллюстрирует его суть. В принципе, если представления имеют общую базу, а шаги и смещения выровнены так, что они находятся в одной сетке, вы можете объединить.

В духе TDD я буду работать со следующим примером:

 x = np.arange(24).reshape(4, 6)
  

Мы (или, по крайней мере, я) хотим, чтобы следующее было объединяемым:

 a, b = x[:, :4], x[:, 4:]        # Basic case
a, b = x[:, :4:2], x[:, 4::2]    # Strided
a, b = x[:, :4:2], x[:, 2::2]    # Strided overlapping
a, b = x[1:2, 1:4], x[2:4, 1:4]  # Stacked

# Completely reshaped:
a, b = x.ravel()[:12].reshape(3, 4), x.ravel()[12:].reshape(3, 4)
# Equivalent to
a, b = x[:2, :].reshape(3, 4), x[2:, :].reshape(3, 4)
  

Мы не хотим, чтобы следующее было объединяемым:

 a, b = x, np.arange(12).reshape(2, 6)   # Buffer mismatch
a, b = x[0, :].view(np.uint), x[1:, :]  # Dtype mismatch
a, b = x[:, ::2], x[:, ::3]             # Stride mismatch
a, b = x[:, :4], x[:, 4::2]             # Stride mismatch
a, b = x[:, :3], x[:, 4:]               # Overlap mismatch
a, b = x[:, :4:2], x[:, 3::2]           # Overlap mismatch
a, b = x[:-1, :-1], x[1:, 1:]           # Overlap mismatch
a, b = x[:-1, :4], x[:, 4:]             # Shape mismatch
  

Следующее может быть интерпретировано как объединяемое, но в данном случае этого не будет:

 a, b = x, x[1:-1, 1:-1]
  

Идея в том, что все (dtype, шаги, смещения) должно точно совпадать. Только одно смещение оси может отличаться между представлениями, если оно находится на расстоянии не более одного шага от края другого представления. Единственное возможное исключение — это когда одно представление полностью содержится в другом, но мы будем игнорировать этот сценарий здесь. Обобщение на несколько измерений должно быть довольно простым, если мы используем операции с массивами для смещений и шагов.

 def cat_slices(a, b):
    if a.base is not b.base:
        raise ValueError('Buffer mismatch')
    if a.dtype != b.dtype:  # I don't thing you can use `is` here in general
        raise ValueError('Dtype mismatch')

    sa = np.array(a.strides)
    sb = np.array(b.strides)

    if (sa != sb).any():
        raise ValueError('Stride mismatch')

    oa = np.byte_bounds(a)[0]
    ob = np.byte_bounds(b)[0]

    if oa > ob:
        a, b = b, a
        oa, ob = ob, oa

    offset = ob - oa

    # Check if you can get to `b` from a by moving along exactly one axis
    # This part works consistently for arrays with internal overlap
    div = np.zeros_like(sa)
    mod = np.ones_like(sa)  # Use ones to auto-flag divide-by zero
    np.divmod(offset, sa, where=sa.astype(bool), out=(div, mod))

    zeros = np.flatnonzero((mod == 0) amp; (div >= 0) amp; (div <= a.shape))

    if not zeros.size:
        raise ValueError('Overlap mismatch')

    axis = zeros[0]

    check_shape = np.equal(a.shape, b.shape)
    check_shape[axis] = True

    if not check_shape.all():
        raise ValueError('Shape mismatch')

    shape = list(a.shape)
    shape[axis] = b.shape[axis]   div[axis]

    start = np.byte_bounds(a)[0] - np.byte_bounds(a.base)[0]

    return np.ndarray(shape, dtype=a.dtype, buffer=a.base, offset=start, strides=a.strides)
  

Некоторые вещи, которые эта функция не обрабатывает:

  • Объединение флагов
  • Трансляция
  • Обработка массивов, которые полностью содержатся друг в друге, но со смещениями по нескольким осям
  • Отрицательные успехи

Однако вы можете проверить, что он возвращает ожидаемые представления (и ошибки) для всех случаев, показанных выше. В более производительной версии я мог бы представить это улучшение np.concatenate , поэтому для неудачных случаев он просто копировал данные, а не выдавал ошибку.

Комментарии:

1. Очень хороший фрагмент кода. Определенно что-то подобное должно быть включено в numpy библиотеку, по крайней мере, в какой-то вспомогательный каталог утилит. Для этого numpy не нужно вводить ничего нового в свое базовое представление. Но это решает мою задачу лишь частично, только в случае совместимых представлений. Я уже, наверное, понял, что моя задача неразрешима в общем виде. Что я действительно хотел решить, так это следующая еще более общая вещь — предположим, у нас есть какое-либо огромное дерево сочетания нарезки / индексации, нарезка может быть применена во второй раз к нарезке индексам и т.д.

2. @Искусство. Как и в любом дереве, вы должны объединять операции в цепочку. Если вы не покажете конкретный пример, это трудно обсуждать.

3. Поэтому я хочу, чтобы это дерево где-то запоминалось numpy как свойство некоторого сложного представления, чтобы позже в любой операции, которая должна выполнять итерацию по всем элементам этого представления, просто выполнял вложенный цикл по элементам этого дерева и, таким образом, выполнял итерацию по элементам исходного массива без необходимости преобразования этого деревав простой массив целых индексов. Итерация по дереву среза может быть или не быть более эффективной, хотя во многих случаях это может быть намного эффективнее, чем с помощью преобразованного массива индексов, также сохраняющего память.

4. Похоже, вы хотите что-то подобное, но в tensorflow

5. Во-первых, операции не всегда можно объединить в цепочку, представьте, что применяете некоторую агрегирующую функцию, например вычислительное среднее. Вы должны самостоятельно вычислить среднее значение для каждого фрагмента в дереве и объединить это значение по некоторой формуле вручную. Вместо того, чтобы numpy делал это для вас все правильно. Во-вторых, ваше дерево может быть огромным в целом и содержать очень крошечные элементы, такие как фрагменты с несколькими элементами в диапазоне или несколькими целочисленными индексами. Тогда самостоятельная итерация в циклах чистого Python будет неэффективной по сравнению с C numpy.