Использование правила для сопоставления пар 2D-массивов в 3D-массиве

Рассмотрим трехмерный массив формы (4, 4, 4). Каждый двухмерный массив вдоль axis=0 имеет перевернутое по вертикали совпадение где-то в трехмерном массиве. В показанном небольшом примере arr[0] и arr[3] образуют перевернутую пару, как и arr[1] и arr[2].

import numpy as np

arr = np.array([[[1, 2, 3, 4],
                 [3, 3, 3, 3],
                 [4, 6, 0, 9],
                 [4, 3, 2, 1]],
                  
                [[5, 6, 5, 6],
                 [8, 8, 8, 8],
                 [0, 9, 0, 9],
                 [1, 2, 4, 8]],

                [[1, 2, 4, 8], 
                 [0, 9, 0, 9],
                 [8, 8, 8, 8],
                 [5, 6, 5, 6]],

                [[4, 3, 2, 1],
                 [4, 6, 0, 9],
                 [3, 3, 3, 3],
                 [1, 2, 3, 4]]])    

ВОПРОС: Как найти индексы перевернутых пар двумерных массивов? Результат в приведенном примере будет:

matched_pairs = [[0, 3],
                 [1, 2]]

Фактический начальный трехмерный массив обычно имеет форму (20000, 8, 8), поэтому matched_pairs будет иметь форму (10000, 2).

Могут ли совпадения быть где угодно в массиве, например. для вашего примера результат может быть [[0, 1], [2, 3]] или они всегда будут во второй половине?

Nick 21.03.2024 06:38

Да, совпадающие пары могут появляться в любом месте трехмерного массива.

user109387 21.03.2024 13:10
Структурированный массив Numpy
Структурированный массив Numpy
Однако в реальных проектах я чаще всего имею дело со списками, состоящими из нескольких типов данных. Как мы можем использовать массивы numpy, чтобы...
T - 1Bits: Генерация последовательного массива
T - 1Bits: Генерация последовательного массива
По мере того, как мы пишем все больше кода, мы привыкаем к определенным способам действий. То тут, то там мы находим код, который заставляет нас...
Что такое деструктуризация массива в JavaScript?
Что такое деструктуризация массива в JavaScript?
Деструктуризация позволяет распаковывать значения из массивов и добавлять их в отдельные переменные.
1
2
60
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете перебирать каждый элемент arr, проверяя, равен ли он другому элементу arr, перевернутому по оси 1:

flip = np.flip(arr, 1)
matched_pairs = [[i, np.nonzero((arr[i] == flip).all(axis=(1,2)))[0][0]] for i in range(arr.shape[0])]

Выход:

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

Если вы знаете, что все пары находятся между первой и второй половиной массива, вы можете просто перебрать только первую половину массива, т.е.

i in range(arr.shape[0] // 2)

В противном случае вы можете отфильтровать дубликаты из этого списка, если это необходимо:

matched_pairs = [list(p) for p in {frozenset(p) for p in matched_pairs}]

Выход:

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

спасибо за вклад. Это хорошо работало с меньшими выборками, но вызывало затруднения, когда трехмерные массивы углублялись вдоль оси = 0.

user109387 21.03.2024 16:25

@user109387 user109387 np, я ожидал, что кто-то придумает что-то более эффективное, и не разочаровался. :) Хороший вопрос.

Nick 22.03.2024 10:11
Ответ принят как подходящий

Вы можете использовать apply_along_axis вместе с lexsort:

a = np.apply_along_axis(lambda x: x if x[0] < x[-1] else x[::-1], 1, arr)
np.lexsort(a.reshape(a.shape[0], -1).T).reshape(-1, 2)

array([[0, 3],
       [1, 2]], dtype=int64)

Мы могли бы расширить это и превратить в общую функцию:

import numpy as np

def get_pairs(arr):
  a = np.arange(np.prod(arr.shape[1:])).reshape(arr.shape[1:])[::-1].ravel()
  def fn(x):
    y = x[a]
    for i,j in zip(x,y):
      if i != j: break
    return x if i<j else y
  b = np.lexsort(np.array([fn(i) for i in  arr.reshape(arr.shape[0], -1)]).T)
  return b.reshape(-1, 2)
  
  
np.random.seed(0)
a = np.random.randint(0, 10, size=(5, 4, 5))
b = np.vstack([a, a[:,::-1]])[np.random.choice(10, 10, False)]
d = get_pairs(b)
array([[5, 9],
       [0, 4],
       [1, 2],
       [6, 7],
       [3, 8]], dtype=int64)

b[d[0]]
array([[[6, 7, 7, 8, 1],
        [7, 6, 8, 8, 1],
        [9, 3, 5, 2, 4],
        [5, 0, 3, 3, 7]],

       [[5, 0, 3, 3, 7],
        [9, 3, 5, 2, 4],
        [7, 6, 8, 8, 1],
        [6, 7, 7, 8, 1]]])
b[d[1]]
array([[[4, 4, 8, 4, 3],
        [7, 5, 5, 0, 1],
        [5, 9, 3, 0, 5],
        [0, 1, 2, 4, 2]],

       [[0, 1, 2, 4, 2],
        [5, 9, 3, 0, 5],
        [7, 5, 5, 0, 1],
        [4, 4, 8, 4, 3]]])

Сравнение времени:

def pairs_chrslg(arr):
  ii,jj = np.where((arr[None,:,:,:] == arr[:,None,::-1,:]).all(axis=(2,3)))
  m=ii<jj
  return np.array([ii[m], jj[m]]).T

def pairs_nick(arr):
  flip = np.flip(arr, 1)
  matched_pairs = [[i, np.nonzero((arr[i] == flip).all(axis=(1,2)))[0][0]] for i in range(arr.shape[0])]
  return [list(p) for p in {frozenset(p) for p in matched_pairs}]

np.random.seed(0)
def create_arr(n):
  a = np.random.randint(0, 10, size=(n, 10, 10))
  return np.vstack([a, a[:,::-1]])[np.random.choice(n*2, n*2, False)]

def equal(x, y):
  x = np.array(x)
  x.sort(1)
  y = np.array(y)
  y.sort(1)
  return np.all(x[x[:,0].argsort()] == y[y[:,0].argsort()])

import perfplot
out = perfplot.bench(
    setup= create_arr, 
    kernels=[
        get_pairs,
        pairs_chrslg,
        pairs_nick
    ],
    labels=["onyambu", "chrslg", "nick"],
    n_range=[3, 10, 30, 50, 75, 100, 300, 500, 750, 1000, 1500],
    xlabel = "N",
    equality_check=equal
)
out
┌──────┬───────────────────────┬───────────────────────┬──────────────────────┐
│ n    │ onyambu               │ chrslg                │ nick                 │
├──────┼───────────────────────┼───────────────────────┼──────────────────────┤
│ 3    │ 4.9500000000000004e-… │ 1.25e-05              │ 3.3900000000000004e… │
│ 10   │ 8.2e-05               │ 6.75e-05              │ 0.0001206            │
│ 30   │ 0.0001921             │ 0.0004427             │ 0.00081420000000000… │
│ 50   │ 0.000407700000000000… │ 0.0014860000000000001 │ 0.0019667            │
│ 75   │ 0.0006580000000000001 │ 0.0037497000000000003 │ 0.0040774            │
│ 100  │ 0.0008856000000000001 │ 0.006582800000000001  │ 0.00693480000000000… │
│ 300  │ 0.0030358000000000004 │ 0.06236920000000001   │ 0.060254300000000004 │
│ 500  │ 0.0065998             │ 0.1809316             │ 0.1633378            │
│ 750  │ 0.0083125             │ 0.4043601             │ 0.3742809            │
│ 1000 │ 0.011511700000000001  │ 0.7393738000000001    │ 0.6743115000000001   │
│ 1500 │ 0.018324800000000002  │ 1.7131234000000002    │ 1.7008605            │
└──────┴───────────────────────┴───────────────────────┴──────────────────────┘


Обратите внимание, что двое других (особенно Ника) страдают от неэффективности памяти. т.е. невозможно получить пары для больших массивов. например (10000, 20, 20)

Ваша функция def_pairs сработала прекрасно! Я даже применил его к трехмерному массиву фигур (70000, 10, 6), и он нашел все двумерные совпадения за несколько секунд. Большое спасибо!

user109387 21.03.2024 16:24

@user109387 user109387 только что обновил функцию. Рассмотрите возможность использования новой функции

Onyambu 21.03.2024 16:25

Обновление работает отлично, но я думаю, что предпочитаю более раннюю версию, в которой первый столбец вывода отображался в порядке возрастания. Это важно по практическим соображениям при применении к более крупным массивам, таким как (70000, 10, 6), который я тестировал.

user109387 21.03.2024 16:41

@user109387 user109387 Вторая версия в 2 раза больше первой версии. Заказ — это всего лишь последний шаг, т. е. b[b[:,0].argsort()], который вы можете напрямую включить в качестве последнего шага во второй версии. т.е. сохраните возвращаемое значение второй версии в переменной, например c, а затем выполните return c[c[:,0].argsort()].

Onyambu 21.03.2024 16:58

Это действительно умное использование lexsort.

Nick 22.03.2024 01:45

@Onyambu - да, я хотел упомянуть, что включил изменение порядка. Ваше пересмотренное решение работает прекрасно и очень быстро.

user109387 22.03.2024 17:02

Одним из чистых способов использования вещания может быть

ii,jj = np.where((arr[None,:,:,:] == arr[:,None,::-1,:]).all(axis=(2,3)))

Объяснение:
arr[None,:,:,:] — это просто массив с дополнительной осью len 1 в начале и основной осью в виде axis=1 и второй матричной осью в позициях 2 и 3.
arr[:,None,::-1,:] — это массив с дополнительной осью в позиции 1, основной осью — в позиции 0 и второй осью матрицы в позициях 2 и 3. Кроме того, первая ось матрицы перевернута.
Итак, arr[None,:,:,:]==arr[:,None,::-1,:] сравнивает все пары позиций на главной оси (поскольку в этом сравнении главная ось представляет собой две разные оси) матрицы, одна из которых перевернута. Вы получаете 4D-массив логических значений, например (arr[None,:,:,:]==arr[:,None,::-1,:])[i,j,k,l] истинно, если элемент [i,k,l] из arr совпадает с его элементом [j,-k,l].
.all(axis=(2,3)) проверяет, верно ли это для всей матрицы. Итак, (arr[None,:,:,:] == arr[:,None,::-1,:]).all(axis=(2,3))[i,j] истинно тогда и только тогда, когда матрица i является перевернутой матрицей j. В каждой строке должно быть одно значение True
np.where() рассказывает, где находятся «Правда».

Итак ii,jj пары. Вам просто нужно объединить их в один массив. Что можно сделать просто так

np.array(np.where((arr[None,:,:,:] == arr[:,None,::-1,:]).all(axis=(2,3)))).T

Ради острот. Но поскольку, чтобы получить строго желаемый результат, нам нужно отфильтровать избыточные строки, давайте сначала отфильтруем ii и jj, а затем построим массив.

ii,jj = np.where((arr[None,:,:,:] == arr[:,None,::-1,:]).all(axis=(2,3)))
m=ii<jj
np.array([ii[m], jj[m]]).T

Который возвращает точный желаемый результат.

Время показывает, что это быстрее, чем другие решения. (25 мкс против 65 мкс против 143 мкс для исходного размера).

Я думаю (если я правильно понял), что решение lexsort ломается, как только оно находит несовпадающее значение. Итак, для большого массива это становится явным преимуществом, вероятно, во многом компенсирующим медлительность apply_along_axis. Таким образом, для большего размера решение Оньямбу, вероятно, выиграет (Редактировать: я тестировал массив 100×8×8, и мы все еще равны, для 1000×8×8 решение Оньямбу в 10 раз быстрее. Независимо от размера, мой Решение всегда быстрее, чем у Ника, но с пропорционально уменьшающимся преимуществом. Всего на 10% быстрее для 1000×8×8. В любом случае, когда я смотрю на него, это то же самое решение, только с явным for вместо трансляции. неявный ::-1 — это flip, а использование np.where с одним аргументом является синонимом nonzero. Но явное for позволяет избежать недостатка, упомянутого в моем решении в следующем предложении.

Кроме того, у моего решения есть недостаток: оно создает логический массив N²×W×H для проверки массива N×W×H. Для 20000,8,8 потребуется около 25 Гб (поскольку 1 логическое значение = 1 байт в numpy).

@ chrsig Это дает мне AttributeError: объект 'bool' не имеет атрибута 'all'. Я надеюсь, что это легко исправить. Ваше объяснение было очень подробным и ясно изложено - я многому научился. Спасибо.

user109387 21.03.2024 16:29

В какой из трех строк отображается эта ошибка? Я не могу воспроизвести это с вашего arr. Я полагаю, что это, скорее всего, первый (тот, что с all). Именно это и произошло бы, если бы (arr[None,:,:,:] == arr[:,None,::-1,:]) было логическим значением, а не массивом логических значений. Итак, если бы == воспринимался как примитивное сравнение Python, а не как __eq__ метод. Но этого не должно произойти, если Arr такой, как показано в вашем вопросе. Вы уверены, что arr стоял в правильной форме, когда вы это пробовали? Опять же, я просто копирую и вставляю вам arr=np.array..., затем свои 3 строчки, и все работает как задумано.

chrslg 21.03.2024 16:50

Это хороший вариант использования трансляции, чтобы избежать flip в моем ответе.

Nick 22.03.2024 01:45

Другие вопросы по теме