Реализация на Python «Внешнего продукта» Matlab

#python #arrays #numpy #matlab

Вопрос:

Я пытаюсь переписать следующий фрагмент кода Matlab о внешнем продукте матриц в код python,

 function Y = matlab_outer_product(X,x)
A = reshape(X, [size(X) ones(1,ndims(x))]);
B = reshape(x, [ones(1,ndims(X)) size(x)]);
Y = squeeze(bsxfun(@times,A,B));
end
 

Мой однозначный перевод этого кода на python выглядит следующим образом (учитывая, как устроена форма массива numpy и матриц matlab):,

 def python_outer_product(X, x):
    X_shape = list(X.shape)
    x_shape = list(x.shape)
    A = X.reshape(*list(np.ones(np.ndim(x),dtype=int)),*X_shape)
    B = x.reshape(*x_shape,*list(np.ones(np.ndim(X),dtype=int)))
    Y = A*B
    return Y.squeeze()
 

Затем попробуйте входные данные, например,

 matlab_outer_product([1,2],[[3,4];[5,6]])
python_out_product(np.array([[1,2]], np.array([[3,4],[5,6]])))
 

Результаты не совсем совпадают. В matlab он выводит

 output(:,:,1) = [[3,5];[6,10]]
output(:,:,2) = [[4,6];[8,12]]
 

В python он выводит

 output = array([
       [[ 3,  6],
        [ 4,  8]],

       [[ 5, 10],
        [ 6, 12]]
])
 

Они почти идентичны, но не совсем. Интересно, что не так с кодом и как изменить код python в соответствии с выводом matlab?

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

1. MATLAB использует F-порядок, где конечное измерение является самым внешним. numpy использует C-порядок (по умолчанию) с самым верхним измерением. Обратите внимание, что дисплей MATLAB «повторяет» конечное измерение. numpy делит ведущее измерение на блоки. Сосредоточьтесь на получении правильных значений, но не пытайтесь заставить numpy макет соответствовать. Это не стоит затраченных усилий.

2. в коде matlab вы изменяете форму A, чтобы начать с X измерений, а затем с единиц, а в коде python все наоборот, попробуйте изменить оба одинаково и посмотрите, даст ли это вам то, что вы ожидаете

3. @TadhgMcDonald-Дженсен, нет, это не дало бы мне ответа. Потому что, например, в matlab массив (2,2,3,1) эквивалентен массиву numpy (3,1,2,2). Именно по этой причине я пишу именно так в приведенном выше коде python.

4. @hpaulj Я вижу! Но то, что я пытаюсь сделать, — это переписать гигантскую программу matlab на python. И остальная часть программы зависит от такой функции. Мне нужно сделать их точно такими же. В противном случае позже может быть затрачено больше усилий 🙁

5. хорошо, но если это так, то почему вывод неверен? вы говорите, что он почти идентичен, но не совсем, чего вы ожидаете, если уже знаете, что размеры будут изменены, потому что это единственное различие, которое я вижу.

Ответ №1:

Во всех кровавых подробностях (так как моя память MATLAB старая):

Октава

 >> X = [1,2];
>> x = [[3,4];[5,6]];
>> A = reshape(X, [size(X) ones(1,ndims(x))]);
>> B = reshape(x, [ones(1,ndims(X)) size(x)]);
>> A
A =

   1   2

>> B
B =

ans(:,:,1,1) =  3
ans(:,:,2,1) =  5
ans(:,:,1,2) =  4
ans(:,:,2,2) =  6

>> bsxfun(@times,A,B)
ans =

ans(:,:,1,1) =

   3   6

ans(:,:,2,1) =

    5   10

ans(:,:,1,2) =

   4   8

ans(:,:,2,2) =

    6   12

>> squeeze(bsxfun(@times,A,B))
ans =

ans(:,:,1) =

    3    5
    6   10

ans(:,:,2) =

    4    6
    8   12
 

Вы начинаете с (1,2) и (2,2), расширяете второе до (1,1,2,2). В bsxfun результате получается (1,2,2,2), который сжимается до (2,2,2).

A X изменяется на [1 2 1 1] , но два внешних измерения размера 1 выдавливаются, что не приводит к изменениям.

Этот выход MATLAB немного запутан, используется bsxfun для выполнения поэлементного умножения (1,2,1,1) на (1,1,1,2). По крайней мере, в октаве это то же самое, что

 A.*B
 

В numpy

 In [77]: X
Out[77]: array([[1, 2]])    # (1,2)
In [78]: x
Out[78]: 
array([[3, 4],              # (2,2)
       [5, 6]])
 

Обратите внимание, что MATLAB/Октава x при сплющивании содержит элементы (3,5,4,6), в то время как numpy ravel равен [3,4,5,6].

В numpy я могу просто сделать:

 In [79]: X[:,:,None,None]*x
Out[79]: 
array([[[[ 3,  4],          (1,2,2,2)
         [ 5,  6]],

        [[ 6,  8],
         [10, 12]]]])
 

или без дополнительного размера 1 размер X :

 In [84]: (X[0,:,None,None]*x)
Out[84]: 
array([[[ 3,  4],
        [ 5,  6]],

       [[ 6,  8],
        [10, 12]]])

In [85]: (X[0,:,None,None]*x).ravel()
Out[85]: array([ 3,  4,  5,  6,  6,  8, 10, 12])
 

сравните это с октавой равеля

 >> squeeze(bsxfun(@times,A,B))(:)'
ans =

    3    6    5   10    4    8    6   12
 

Мы могли бы добавить транспонирование в numpy

 In [96]: (X[0,:,None,None]*x).transpose(2,1,0).ravel()
Out[96]: array([ 3,  6,  5, 10,  4,  8,  6, 12])
In [97]: (X[0,:,None,None]*x).transpose(2,1,0)
Out[97]: 
array([[[ 3,  6],
        [ 5, 10]],

       [[ 4,  8],
        [ 6, 12]]])
 

По крайней мере, в numpy мы можем настроить порядок измерений множеством способов, поэтому я не буду пытаться предложить оптимальный. Я все еще думаю, что лучше писать код, который «естественен» для numpy, чем рабски соответствовать порядку MATLAB.

еще одна попытка

Выше я понял, что MATLAB просто работает A*.B с массивами (1,2,1,1) (1,1,1,2), где дополнительные 1 были добавлены в «широковещание».

Используя транспонирование в то же самое внешнее измерение (ведущее в numpy)

 In [5]: X = X.T; x = x.T
In [6]: X.shape
Out[6]: (2, 1)
In [7]: x.shape
Out[7]: (2, 2)
In [8]: x
Out[8]: 
array([[3, 5],
       [4, 6]])
In [9]: x.ravel()
Out[9]: array([3, 5, 4, 6])   # compare with MATLAB (:)'
 

Поэлементное умножение с тем же расширением размера:

 In [10]: X[None,None,:,:]*x[:,:,None,None]
Out[10]: 
array([[[[ 3],
         [ 6]],

        [[ 5],
         [10]]],


       [[[ 4],
         [ 8]],

        [[ 6],
         [12]]]])
In [11]: _.shape
Out[11]: (2, 2, 2, 1)         # compare with octave (1,2,2,2)
In [12]: __.squeeze()
Out[12]: 
array([[[ 3,  6],
        [ 5, 10]],

       [[ 4,  8],
        [ 6, 12]]])
 

равель-это то же самое, что Октава:

 In [13]: ___.ravel()
Out[13]: array([ 3,  6,  5, 10,  4,  8,  6, 12])
 

expand_dims может использоваться вместо индексации. Внутренне он использует reshape :

 In [15]: np.expand_dims(X,(0,1)).shape
Out[15]: (1, 1, 2, 1)
In [16]: np.expand_dims(x,(2,3)).shape
Out[16]: (2, 2, 1, 1)
 

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

1. хотя X[:,:,None,None] мне кажется, что это более естественный способ сделать это, я хочу отметить, что если бы исходные матрицы были выше 2D, то они не формировали бы вывод правильно, так как способ его изменения в операции все равно правильно вычислял бы внешний продукт. вы могли idx = (*itertools.repeat(slice(None), len(X.shape)), *itertools.repeat(None, len(x.shape))) ; X[idx] * x бы сделать так, чтобы это работало в целом, но это кажется еще более излишним.

2. @TadhgMcDonald-Дженсен Вы абсолютно правы в том, что если начальные матрицы выше 2D, код python волшебным образом даст мне правильный ответ.

3. @TadhgMcDonald-Дженсен, не могли бы вы подробнее рассказать о своем предложении?

4. @hpaulj. Спасибо за ваш ответ! Это потрясающе! Но этот метод не поддается обобщению, мой опыт в этом вопросе заключается в том, чтобы избежать какой-либо транспозиции, перекоса в этой задаче.

5. X[:,:,None,None] преобразует (m by n) матрицу в (m,n,1,1) матрицу формы , которая при умножении на (j by k) матрицу придает форму (m,n,j,k) (что сильно отличается от поведения matlab), если матрица имеет форму (m,n,a) , она преобразуется в размер (m,n,1,1,a) , что вызовет проблемы при умножении на (j by k) , код с repeat(slice(None)) делает эквивалентное правильное количество срезов для любого количества измерений