0

In python, if x,y are vectors, I expected x @ y.T to give me the outer product of x and y, i.e. to match the result of np.outer(x,y). However, to my surprise, x @ y.T returns a scalar. Why? Are vectors (i.e. one-dimensional arrays) not considered to be column vectors by numpy?

For example, the code

import numpy as np

x=np.array([1,2])
y=np.array([3,4])

wrong_answer = x @ y.T 
right_answer = np.outer(x,y)

gives the (interactive) output

In [1]: wrong_answer
Out[1]: 11

In [2]: right_answer
Out[2]: 
array([[3, 4],
       [6, 8]])

ashman
  • 154
  • 1
  • 5
  • 1
    No, 1d arrays are not column vectors. If anything they are closer to row vectors (by the rules of broadcasting). But if you take time to read `np.transpose`, you'll learn that it does not change a 1d array into a 2d one. @ is documented at `np.matmul`. Read it. – hpaulj Nov 02 '22 at 21:15
  • `@` is basically a dot product of the two arrays, while `*` is element-wise multiplication. – chepner Nov 02 '22 at 21:20

3 Answers3

1

The @ takes the dot product, not the outer product.

Using the variables you defined in the code:

>>> x=np.array([1,2])
>>> y=np.array([3,4])
>>> np.dot(x,y)
11

You get 11, which is the dot product that was produced using '@'

EDIT: update from discussion

Based on this answer as well as digging through the PEP465, the @ operator uses __matmul__ under the hood, which is also taking the dot product.

>>> x.__matmul__(y)
11
Zach J.
  • 366
  • 6
  • Re: "The @ takes the dot product, not the outer product.": I find this behavior of Python to be very strange, as I have been interpreting `@` to be the matrix multiplication operator. I believe this interpretation is common? E.g. a quick search of the internet yielded: "PEP 465 introduced the @ infix operator that is designated to be used for matrix multiplication." – ashman Nov 02 '22 at 20:59
  • Yep, that's correct! PEP 465 did introduce that, but under the hood it's using the `__matmul__` method, which is actually taking the dot product. If you ran `x.__matmul__(y)`, you'll get the same dot product, since that's what the `@` operator is using under the hood. I'll update my answer to reflect that – Zach J. Nov 02 '22 at 21:03
  • `np.matmul(x,y)` – hpaulj Nov 02 '22 at 22:39
0

"Are vectors (i.e. one-dimensional arrays) not considered to be column vectors by numpy?" -> No, you would need to add a dimension. And @ is a dot product, not a multiplication.

You need:

x[:,None]*y

Output:

array([[3, 4],
       [6, 8]])

You can check that transposing a 1D array doesn't change anything:

y.T
array([3, 4])
mozway
  • 194,879
  • 13
  • 39
  • 75
  • Re: " No, you would need to add a dimension.": Interesting. If I do `xx=x[:,np.newaxis]` and `yy=y[:,np.newaxis]` then now `np.dim(xx)=np.ndim(yy)=2` and `xx @ yy.T` gives the intended result. But having to call `np.newaxis` all the time strikes me as annoying boilerplate. Is there something I'm missing? Is there some reason why `y` is not considered a column vector (with `y.T` giving the transpose, i.e. creating an array with shape (2,1))? – ashman Nov 02 '22 at 21:04
  • In which language were you able to do this? There are no row/columns vectors, only n-dimensional arrays in numpy (a column vector would be a (k,1) 2D array). And why using the dot product? – mozway Nov 02 '22 at 21:11
0

np.transpose, np.dot and np.matmul all document their behavior when given 1d arrays. broadcasting is also a good thing to understand.

Your two arrays are both 1d:

In [250]: x=np.array([1,2])
     ...: y=np.array([3,4])    
In [251]: x.shape
Out[251]: (2,)

transpose does not change the dimensions of a 1d array; it does not add a dimension. It just reorders existing dimensions:

In [252]: y.T.shape
Out[252]: (2,)

outer explicitly says it works with 1d arrays (and ravels others):

In [254]: np.outer(x,y)
Out[254]: 
array([[3, 4],
       [6, 8]])

As a long term numpy user, I prefer to use the broadcasted multiply:

In [255]: x[:,None]*y        # (n,1)*(m) => (n,1)*(1,m) => (n,m)   
Out[255]: 
array([[3, 4],
       [6, 8]])

outer also likens its action to np.einsum, but I'll skip that for now.

dot/matmul explicitly says what it does with 1d arrays:

In [256]: np.dot(x,y)            # dot of 1d arrays is inner product
Out[256]: 11

In [257]: np.matmul(x,y)         # x@y
Out[257]: 11

broadcasting doesn't work with @/matmul (for the last 2 dimensions, that is):

In [260]: x[:,None]@y
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [260], in <cell line: 1>()
----> 1 x[:,None]@y

ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 1)

But (n,1) with (1,m) => (n,m), with the shared size 1 dimension as the sum-of-products dimension:

In [261]: x[:,None]@y[None,:]
Out[261]: 
array([[3, 4],
       [6, 8]])

In this case using the broadcasted multiply does the same thing:

In [262]: x[:,None]*y[None,:]   # "column vector" * "row vector"
Out[262]: 
array([[3, 4],
       [6, 8]])

Another way to control dimension is to use np.einsum:

In [263]: np.einsum('i,i',x,y)
Out[263]: 11

In [264]: np.einsum('i,j',x,y)
Out[264]: 
array([[3, 4],
       [6, 8]])

In [265]: np.einsum('i,i->i',x,y)       # x*y
Out[265]: array([3, 8])

Because 1d arrays are really that, not row/column vectors in disguise, we may need to add a dimension here or there to match behavior that we are used seeing with matrix-oriented languages like MATLAB.

hpaulj
  • 221,503
  • 14
  • 230
  • 353