Как матрично-умножить двумерный массив с трехмерным массивом, чтобы получить трехмерный массив? - PullRequest
1 голос
/ 28 октября 2019

Я решаю фотометрическую стерео проблему, в которой у меня есть «n» количество источников света с 3 каналами (красный, зеленый, синий) каждый. Таким образом, световой массив имеет форму nx3: lights.shape = nx3 У меня есть изображения, соответствующие каждому условию освещения. Размеры изображения: hxw (высота x ширина), images.shape = nxhxw

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

формы:

  • изображения: (n_ims, h, w)
  • огни: (n_ims, 3)
S = lights
S_pinv =  np.linalg.inv(S.T@S)@S.T  # pinv is pseudo inverse, S_pinv.shape : (n_ims,3)
b = S_pinv @ images  # I want (3xn @ nxhxw = 3xhxw)

Но я получаю эту ошибку:

ValueError: matmul: входной операнд 1 имеет несоответствие в основном измерении 0 с сигнатурой gufunc (n?, K),(k, m?) -> (n?, m?) (размер 100 отличается от 3)

Ответы [ 4 ]

1 голос
/ 28 октября 2019

Проблема состоит в том, что numpy рассматривает многомерные массивы как стеки матриц, и всегда последние два измерения предполагаются линейными измерениями пространства. Это означает, что точечный продукт не будет работать, если свернуть измерение first вашего 3d-массива.

Вместо этого самое простое, что вы можете сделать, это преобразовать ваш 3d-массив в 2d, выполнивумножение матрицы и преобразование обратно в трехмерный массив. Это также будет использовать оптимизированный код BLAS, который является одним из больших преимуществ numpy.

import numpy as np 

S_pinv = np.random.rand(3, 4)
images = np.random.rand(4, 5, 6)

# error: 
# (S_pinv @ images).shape 
res_shape = S_pinv.shape[:1] + images.shape[1:]  # (3, 5, 6) 
res = (S_pinv @ images.reshape(images.shape[0], -1)).reshape(res_shape)
print(res.shape)  # (3, 5, 6)

Так что вместо (3,n) x (n,h,w) мы делаем (3,n) x (n, h*w) -> (3, h*w), который мы преобразуем обратно в (3, h, w). Изменение формы является бесплатным, потому что это не означает каких-либо реальных манипуляций с данными в памяти (только переосмысление единственного блока памяти, лежащего в основе массива), и, как я сказал, правильные матричные продукты сильно оптимизированы в numpy.


Поскольку вы попросили более интуитивно понятный способ , вот альтернативный вариант использования numpy.einsum. Возможно, он будет медленнее, но он будет очень прозрачным, если вы немного привыкнете к его обозначениям:

res_einsum = np.einsum('tn,nhw -> thw', S_pinv, images)
print(np.array_equal(res, res_einsum))  # True

В этой записи обозначены все измерения входных массивов: для S_pinv первое и второеразмеры называются t и n, соответственно, и аналогично n, h и w для images. Выходные данные имеют размеры thw, что означает, что любые оставшиеся измерения, отсутствующие в выходной форме, будут суммироваться после умножения входных массивов. Это именно то, что вам нужно.


Как вы отметили в комментарии, вы также можете транспонировать свои массивы так, чтобы np.dot находил нужные измерения в нужном месте. Но это также будет медленным, потому что это может привести к копированию в памяти или, по крайней мере, к неоптимальному циклу над вашими массивами.

Я сделал быстрое сравнение по времени, используя следующие определения:

def reshaped(S_pinv, images): 
    res_shape = S_pinv.shape[:1] + images.shape[1:] 
    return (S_pinv @ images.reshape(images.shape[0], -1)).reshape(res_shape)

def einsummed(S_pinv, images): 
    return np.einsum('tn,nhw -> thw', S_pinv, images) 

def transposed(S_pinv, images): 
    return (S_pinv @ images.transpose(2, 0, 1)).transpose(1, 2, 0)          

А вот временной тест, использующий встроенную в IPython магию %timeit и некоторые более реалистичные размеры массивов:

>>> S_pinv = np.random.rand(3, 100) 
... images = np.random.rand(100, 200, 300) 
... args = S_pinv, images 
... %timeit reshaped(*args) 
... %timeit einsummed(*args) 
... %timeit transposed(*args)                                          
5.92 ms ± 460 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
15.9 ms ± 190 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
44.5 ms ± 329 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
0 голосов
/ 28 октября 2019

Это в основном то, для чего np.einsum.

Вместо:

b = S_pinv @ images

Используйте

b = np.einsum('ij, ikl -> jkl', S_pinv, images)

в этом случаеi = n_ims, j = 3, k = h и l = w

Поскольку это однократное сокращение, вы также можете сделать это с помощью np.tensordot()

b = np.tensordot(S_pinv.T, images, axes = 1)

или

b = np.tensordot(S_pinv, images, axes = ([0], [0]))
0 голосов
/ 28 октября 2019

Один простой способ будет np.inner;inner уменьшается вдоль последней оси и сохраняет все остальные;следовательно, все зависит от транспонирования:

n,h,w = 10,384,512
images = np.random.randint(1,10,(n,h,w))
S_pinv = np.random.randint(1,10,(n,3))

res_inr = np.inner(images.T,S_pinv.T).T
res_inr.shape
# (3, 384, 512)

Аналогично, использование транспонирования matmul на самом деле делает правильно:

res_mml = (images.T@S_pinv).T
assert (res_mml==res_inr).all()

Эти два, кажется, примерно одинаково быстрыаналогично методу @ AndrasDeak einsum.

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

0 голосов
/ 28 октября 2019

ответ np.swapaxes

import numpy as np

q= np.random.random([2, 5,5])
q.shape

w = np.random.random([3,2])
w.shape

w@q

и у нас есть ValueError но

import numpy as np

q= np.random.random([5, 2,5])
q.shape

w = np.random.random([3,2])
w.shape

res = (w@q).swapaxes(0,1)
res.shape # =[3, 5, 5]
...