1

So let's say I have a list of (c x d) matrices. Like say I have a of them. And I have a coefficients for each matrix.

Is there a quick way in NumPy to scalar-multiply each matrix by its coefficient at once while still keeping the tensor data structure, or do I need to manually go through in a for loop i.e. X = np.array([np.multiply(coefs[i], X[i]) for i in range(len(coefs))])

i.e. X.shape = (3, 4, 5), coefs.shape = (3).

Andrew Latham
  • 5,982
  • 14
  • 47
  • 87
  • Can you clarify by writing a mathematical formula of what you want? – Rufflewind Dec 23 '14 at 04:25
  • 1
    Your examples ```X.shape, coeffs.shape``` are 3-D and 1-D - this conflicts with the title ... 2-D times 3-D. – wwii Dec 23 '14 at 04:40
  • Alternate for your list comprehension: ```[np.multiply(a, b) for a, b in zip(X, coeffs)]``` – wwii Dec 23 '14 at 04:58
  • @Andrew, wondering if you ended up considering my suggestion, `np.einsum`, then? – Pacific Stickler Dec 24 '14 at 03:20
  • Hey! I appreciate you writing the answer, but unfortunately, as you mentioned, for large datasets there is not a big speed increase and for a one-off use like what I am doing, the versatility is not a major advantage. Thanks though! – Andrew Latham Dec 24 '14 at 04:45

2 Answers2

2
X = np.array([[[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]],
              [[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]],
              [[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]]])

coeffs = np.array([2,4,6])

You need to add axes to coeffs so it will broadcast in the dimension(s) you want.

>>> X * coeffs[:, np.newaxis, np.newaxis]
array([[[2, 2, 2, 2, 2],
        [2, 2, 2, 2, 2],
        [2, 2, 2, 2, 2],
        [2, 2, 2, 2, 2]],

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

       [[6, 6, 6, 6, 6],
        [6, 6, 6, 6, 6],
        [6, 6, 6, 6, 6],
        [6, 6, 6, 6, 6]]])
>>> 

The np.newaxis's allow the values of coeffs to line up with the first dimension of X and then they are broadcast across the remaining dimensions.

wwii
  • 23,232
  • 7
  • 37
  • 77
0

Solution:

This is the classic problem of going the pythonic way. Following 1-liner does the trick using the powerful einstein sum, np.einsum(), function with the rich framework of subscript labels that allow for broadcasting of dimensions and controlling the output:

np.einsum('ijk,i...->ijk', X, coeffs)

Subscript String:

  • Comma , separates out the subscript labels for the first operand, on the left, from that of the second operand, to the right.
  • Subscript label after the -> symbol gives the dimensions of the output.
  • Ellipsis ... would allow for broadcasting the coeffs vector into the extra 2 dimensions of the X matrix (... is actually optional for a more explicit specification of broadcasting)

It takes a little getting used to but has many matrix-vector operations, like trace, diag, inner/outer product, element-wise multiplication, etc., all lumped into this powerful formulation.

In words:

So the string essentially says that take all the dimensions of X matrix and multiply it element-wise with the coeffs vector (broadcasted across the extra dimensions of X) and generate an output matrix of the same dimensions as that of X.

Output:

>>> np.einsum( 'ijk,i->ijk', np.ones((2,2,2)), np.array([2,4]))

array([[[ 2.,  2.],
        [ 2.,  2.]],

       [[ 4.,  4.],
        [ 4.,  4.]]])

In the 2D case, elementwise along either axes:

>>> np.einsum( 'ij,i->ij', np.ones((2,2)), np.array([2,4]))
array([[ 2.,  2.],
       [ 4.,  4.]]) 

>>> np.einsum( 'ij,j->ij', np.ones((2,2)), np.array([2,4]))
array([[ 2.,  4.],
       [ 2.,  4.]])

Alternative:

Offcourse you can manually broadcast the vector across extra dimensions using np.newaxis parameter and simple multiplication:

X * coeff[:, np.newaxis, np.newaxis]

Timing:

Using ipython line magic function %timeit we can actually see that the einsum might be a little more expensive for large data sets:

>>> %timeit np.einsum('ijk,i...->ijk',np.ones((10,100,100)),np.ones(10))
1000 loops, best of 3: 209 µs per loop

>>> %timeit np.ones((10,100,100))*np.ones(10)[:,np.newaxis, np.newaxis]
1000 loops, best of 3: 129 µs per loop

Tradeoff then is in the versatility that the einstein sum offers.

P.S. If you are a Matlab user obsessed with the rich indexing framework that it provides, you may be interested in checking this Numpy-Matlab comparison page out

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Pacific Stickler
  • 1,078
  • 8
  • 20
  • `'ijk,i->ijk'` also works. So does `'i...,i->i...'`. Contrary to earlier `einsum` documentation, ellipsis doesn't allow broadcasting (that's always allowed), rather it is a place holder for unnamed dimensions. – hpaulj Dec 23 '14 at 07:19
  • agreed that the use of ellipsis is rather optional. However, `i...,i->i...` representation doesn't work for me. – Pacific Stickler Dec 23 '14 at 07:45
  • Is `'i...,i->i...'` giving an error? It used to object about a missing ellipsis. But that's been patched in recent version(s). – hpaulj Dec 23 '14 at 07:49
  • Here's the error: `ValueError: operand 1 did not have enough dimensions to match the broadcasting, and couldn't be extended because einstein sum subscripts were specified at both the start and end` – Pacific Stickler Dec 23 '14 at 07:50
  • Regarding your `Alternative` statement: is this ```einsum``` notation doing anything other than simple multiplication while broadcasting one operand across the extra dimensions of another and was the notation somehow arrived at *automagically* rather than *manually*? – wwii Dec 23 '14 at 14:41
  • @wwii yes with this construct it is doing exactly that. I don't understand your second question about arriving at this notation "automagically" vs. "manually"? By the usage of the word "manually", I actually meant that with `einsum` you don't have to explicitly specify across how many dimensions should the second operand be broadcasted. It will take care of it for you. – Pacific Stickler Dec 23 '14 at 16:44
  • More on that `einsum` error from last year: http://stackoverflow.com/questions/16591696/ellipsis-broadcasting-in-numpy-einsum/ Version 1.9 should have the patch. – hpaulj Dec 23 '14 at 17:09