Search code examples
pythonnumpymatrixvectorizationnumpy-ndarray

Numpy multiplies array of vectors as if its a matrix


I am making a 3d renderer in python and an extremely common operation is multiplying an array of vectors by a matrix. I knew about numpys @ operator and board casting so I just did matrix @ arrayOfVectors which seemed to be fine, but it was giving the wrong output. I realized that numpy was treating array of vector as a matrix (4x8 in this case), and it was multiplying them accordingly. I have switched to using a for loop, which does have some advantages, but I understand that this is much slower, and should be avoided if possible. I would like to find a way to apply a matrix to each vector in an array of vectors, and in some cases also divide each vector by its w component afterwards. Is there a way of doing this as neat is matrix @ arrayOfVectors or must I resort to for vector in arrayOfVectors: matrix @ array.


Solution

  • In [14]: A = np.arange(12).reshape(3,4); V = np.arange(8).reshape(2,4)
    

    The first dimensions are different, so direct matmul not only doesn't give the desired answer, it gives an error. It is trying to do matrix multiplication for 2 arrays:

    In [15]: A@V
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    Input In [15], in <cell line: 1>()
    ----> 1 A@V
    
    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 4)
    

    What you want is to treat the first dimension of V as a "batch":

    In [17]: [A@v for v in V]
    Out[17]: [array([14, 38, 62]), array([ 38, 126, 214])]
    

    We get that by making V as 3d array - read matmul docs:

    In [18]: A@V[:,:,None]
    Out[18]: 
    array([[[ 14],
            [ 38],
            [ 62]],
    
           [[ 38],
            [126],
            [214]]])
    
    In [19]: _.shape
    Out[19]: (2, 3, 1)
    

    We can remove the trailing size 1 dimension if necessary (squeeze)

    This kind of dimension specification is easy with einsum:

    In [20]: np.einsum('ij,kj->ik',A,V)
    Out[20]: 
    array([[ 14,  38],
           [ 38, 126],
           [ 62, 214]])
    

    I could have used 'ki' to get a (2,3) answer.