Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vectorising an equation using numpy

enter image description here

enter image description here

I am trying to implement the above formula as a vectorised form. K=3 here, X is 150x4 numpy array. mu is 3x4 numpy array. Gamma is a 150x3 numpy array. Sigma is a kx4x4 numpy array. Therefore Sigma[k] is a 4x4 numpy array. N=150

N_k = np.sum(Gamma, axis=0)
for k in range(K): # Correct
         x_new = X - mu[k] #Correct
         a = np.dot(x_new.T, x_new) #Incorrect from here I feel
         for i in range(len(data)):
             sigma[k] = Gamma[i][k] * a
         sigma[k]=sigma[k]/N_k #totally incorrect

How to fix this?

like image 592
VeilEclipse Avatar asked Dec 26 '22 09:12

VeilEclipse


2 Answers

A sum of products? sounds like a job for np.einsum:

import numpy as np
N = 150
K = 3
M = 4
x = np.random.random((N,M))
mu = np.random.random((K,M))
gamma = np.random.random((N,K))

xbar = x-mu[:,None,:] # shape (3, 150, 4)
sigma = np.einsum('nk,knm,kno->kmo', gamma, xbar, xbar)
sigma /= gamma.sum(axis=0)[:,None,None]

Decoding 'nk,knm,kno->kmo':

This subscript specification has three components to the left of the array (->) followed by one component to the right.

The three components on the left correspond with the subscripts for gamma, xbar and xbar, the operands being passed to np.einsum.

gamma has subscript nk, just as in the formula you posted. xbar has shape (3, 150, 4). You can think of it as having subscript knm, where k and n have the same meaning as in the formula you posted, and m is a subscript representing the axis of length 4 which is not explicitly mentioned in your formula, but is apparently there given your description of the shape of the arrays.

Now the third subscript component is kno. The o subscript is used because the o plays the same role as the m subscript, but we don't want summation over m. In fact, we want the m and o subscripts to be iterated over independently, not in lock-step. Hence we give the third subscript different letters.

Notice that n appears in the subscripts on the left (nk, knm, kno), but does not appear on the right (in kmo). That tells np.einsum to sum over n.

The k appears in the subscripts on the left and on the right. This tells np.einsum that we wish to advance the k subscript in lock-step, but (since it appears on the right) we don't want to sum over k.

Since kmo appears on the right, these subscripts remain in the result. This results in sigma having shape (K,M,M) (i.e. (3,4,4)).

like image 95
unutbu Avatar answered Dec 28 '22 14:12

unutbu


Just a couple of performance pointers on top of unubtu's great answer.

np.einsum has trouble optimizing calls with more than two arguments. Whenever possible, it is generally faster to manually split the calculations into groups of two arguments, e.g.:

def unubtu():
    xbar = x-mu[:,None,:] # shape (3, 150, 4)
    sigma = np.einsum('nk,knm,kno->kmo', gamma, xbar, xbar)
    sigma /= gamma.sum(axis=0)[:,None,None]
    return sigma

def faster():
    xbar = x-mu[:,None,:] # shape (3, 150, 4)
    sigma = np.einsum('knm,kno->kmo', gamma.T[..., None] * xbar, xbar)
    sigma /= gamma.sum(axis=0)[:,None,None]
    return sigma

In [50]: %timeit unubtu()
10000 loops, best of 3: 147 µs per loop

In [51]: %timeit faster()
10000 loops, best of 3: 129 µs per loop

A 12% improvement isn't much, but the difference can get (much) larger with larger arrays.

Also, even though np.einsum is a great tool, that makes very difficult things easy, it is by no means as optimized as np.dot is if your numpy is built with a good linear algebra library. In your case, given that K is small, using np.dot and a Python loop is even faster:

def even_faster():
    sigma = np.empty((K, M, M))
    for k in xrange(K):
        x_ = x - mu[k]
        sigma[k] = np.dot((x_ * gamma[:, k, None]).T, x_)
    sigma /= gamma.sum(axis=0)[:,None,None]
    return sigma

In [52]: %timeit even_faster()
10000 loops, best of 3: 101 µs per loop
like image 31
Jaime Avatar answered Dec 28 '22 15:12

Jaime