[Scipy-svn] r2271 - in trunk/Lib/sandbox/pyem: . pyem
scipy-svn at scipy.org
scipy-svn at scipy.org
Thu Oct 12 09:46:05 EDT 2006
Author: cdavid
Date: 2006-10-12 08:45:58 -0500 (Thu, 12 Oct 2006)
New Revision: 2271
Added:
trunk/Lib/sandbox/pyem/pyem/gauss_mix.py
Modified:
trunk/Lib/sandbox/pyem/Changelog
trunk/Lib/sandbox/pyem/pyem/__init__.py
trunk/Lib/sandbox/pyem/pyem/gmm_em.py
Log:
[pyem @ david at ar.media.kyoto-u.ac.jp-20060714072645-a3b4e773c4c589cf]
Refactoring of EM into classes
David Cournapeau <david at ar.media.kyoto-u.ac.jp> | 2006-07-14 16:26:45 +0900 (Fri, 14 Jul 2006)
Modified: trunk/Lib/sandbox/pyem/Changelog
===================================================================
--- trunk/Lib/sandbox/pyem/Changelog 2006-10-12 13:45:51 UTC (rev 2270)
+++ trunk/Lib/sandbox/pyem/Changelog 2006-10-12 13:45:58 UTC (rev 2271)
@@ -1,3 +1,13 @@
+pyem (0.4) Fri, 14 Jul 2006 16:24:05 +0900
+
+ * put version to 0.4
+ * Heavy refactoring of EM and GMM into classes (see below)
+ * add a module gauss_mix, which implements Gaussian Mixtures.
+ * GMM is now a class, which is initialized using a Gaussian Mixture.
+ a GMM can be trained.
+
+-- David Cournapeau <david at ar.media.kyoto-u.ac.jp>
+
pyem (0.3) Thu, 13 Jul 2006 19:48:54 +0900
* put version to 0.3
@@ -4,6 +14,6 @@
* Refactoring kmean code into new module.
* Refactoring tests code into test module.
* Replace matrixmultiply and outerproduct calls by dot to use fast BLAS if
- available.
+ available. Not everything is done yet
-- David Cournapeau <david at ar.media.kyoto-u.ac.jp>
Modified: trunk/Lib/sandbox/pyem/pyem/__init__.py
===================================================================
--- trunk/Lib/sandbox/pyem/pyem/__init__.py 2006-10-12 13:45:51 UTC (rev 2270)
+++ trunk/Lib/sandbox/pyem/pyem/__init__.py 2006-10-12 13:45:58 UTC (rev 2271)
@@ -1,4 +1,4 @@
#! /usr/bin/env python
-# Last Change: Thu Jul 13 07:00 PM 2006 J
+# Last Change: Fri Jul 14 04:00 PM 2006 J
-version = '0.3'
+version = '0.4'
Added: trunk/Lib/sandbox/pyem/pyem/gauss_mix.py
===================================================================
--- trunk/Lib/sandbox/pyem/pyem/gauss_mix.py 2006-10-12 13:45:51 UTC (rev 2270)
+++ trunk/Lib/sandbox/pyem/pyem/gauss_mix.py 2006-10-12 13:45:58 UTC (rev 2271)
@@ -0,0 +1,292 @@
+# /usr/bin/python
+# Last Change: Fri Jul 14 03:00 PM 2006 J
+
+# Module to implement GaussianMixture class.
+
+import numpy as N
+import numpy.linalg as lin
+import densities
+
+MAX_DEV = 1e-10
+
+# Right now, two main usages of a Gaussian Model are possible
+# - init a Gaussian Model with meta-parameters, and trains it
+# - set-up a Gaussian Model to sample it, draw ellipsoides
+# of confidences. In this case, we would like to init it with
+# known values of parameters.
+#
+# For now, we have to init with meta-parameters, and set
+# the parameters afterward. There should be a better way ?
+class GmParamError:
+ """Exception raised for errors in gmm params
+
+ Attributes:
+ expression -- input expression in which the error occurred
+ message -- explanation of the error
+ """
+ def __init__(self, message):
+ self.message = message
+
+ def __str__(self):
+ return self.message
+
+class GM:
+ """Gaussian Mixture class. This is a simple container class
+ to hold Gaussian Mixture parameters (weights, mean, etc...).
+ It can also draw itself (confidence ellipses) and samples itself.
+
+ Is initiated by giving dimension, number of components and
+ covariance mode"""
+
+ # I am not sure it is useful to have a spherical mode...
+ _cov_mod = ['diag', 'full']
+
+ def __init__(self, d, k, mode = 'diag'):
+ """Init a Gaussian model of k components, each component being a
+ d multi-variate Gaussian, with covariance matrix of style mode"""
+ if mode not in self._cov_mod:
+ raise GmmParamError("mode %s not recognized" + str(mode))
+
+ self.d = d
+ self.k = k
+ self.mode = mode
+
+ # Init to 0 all parameters, with the right dimensions.
+ # Not sure this is useful in python from an efficiency POV ?
+ self.w = N.zeros(k, float)
+ self.mu = N.zeros((k, d), float)
+ if mode == 'diag':
+ self.va = N.zeros((k, d), float)
+ elif mode == 'full':
+ self.va = N.zeros((k * d, d), float)
+
+ self.is_valid = False
+
+ def set_param(self, weights, mu, sigma):
+ """Set parameters of the model"""
+ k, d, mode = check_gmm_param(weights, mu, sigma)
+ if not k == self.k:
+ raise GmmParamError("Number of given components is %d, expected %d"
+ % (shape(k), shape(self.k)))
+ if not d == self.d:
+ raise GmmParamError("Dimension of the given model is %d, expected %d"
+ % (shape(d), shape(self.d)))
+ if not mode == self.mode:
+ raise GmmParamError("Given covariance mode is %s, expected %d"
+ % (mode, self.mode))
+ self.w = weights
+ self.mu = mu
+ self.va = sigma
+
+ self.is_valid = True
+
+ def sample(self, nframes):
+ """ Sample nframes frames from the model """
+ if not self.is_valid:
+ raise GmmParamError("""Parameters of the model has not been
+ set yet, please set them using self.set_param()""")
+
+ # State index (ie hidden var)
+ S = gen_rand_index(self.w, nframes)
+ # standard gaussian
+ X = N.randn(nframes, self.d)
+
+ if self.mode == 'diag':
+ X = self.mu[S, :] + X * N.sqrt(self.va[S,:])
+ elif self.mode == 'full':
+ # Faster:
+ cho = N.zeros((self.k, self.va.shape[1], self.va.shape[1]), float)
+ for i in range(self.k):
+ # Using cholesky looks more stable than sqrtm; sqrtm is not
+ # available in numpy anyway, only in scipy...
+ cho[i] = lin.cholesky(self.va[i*self.d:i*self.d+self.d,:])
+
+ for s in range(self.k):
+ tmpind = N.where(S == s)[0]
+ X[tmpind] = N.dot(X[tmpind], cho[s].transpose()) + self.mu[s]
+ else:
+ raise GmmParamError('cov matrix mode not recognized, this is a bug !')
+
+ return X
+
+ def conf_ellipses(self, c = [0, 1], npoints = 100):
+ """Returns a list of confidence ellipsoids describing the Gmm
+ defined by mu and va. c is the dimension we are projecting
+ the variances on a 2d space. For now, the confidence level
+ is fixed to 0.39.
+
+ Returns:
+ -Xe: a list of x coordinates for the ellipses
+ -Ye: a list of y coordinates for the ellipses
+
+ Example:
+ Suppose we have w, mu and va as parameters for a mixture, then:
+
+ gm = GM(d, k)
+ gm.set_param(w, mu, va)
+ X = gm.sample(1000)
+ Xe, Ye = gm.conf_ellipsoids()
+ pylab.plot(X[:,0], X[:, 1], '.')
+ for k in len(w):
+ pylab.plot(Xe[k], Ye[k], 'r')
+
+ Will plot samples X draw from the mixture model, and
+ plot the ellipses of equi-probability from the mean with
+ fixed level of confidence 0.39. """
+ # TODO: adjustable level (to do in gauss_ell).
+ # For now, a level of 0.39 means that we draw
+ # ellipses for 1 standard deviation.
+ Xe = []
+ Ye = []
+ if self.mode == 'diag':
+ for i in range(self.k):
+ xe, ye = densities.gauss_ell(self.mu[i,:], self.va[i,:], dim = c,
+ npoints = npoints)
+ Xe.append(xe)
+ Ye.append(ye)
+ elif self.mode == 'full':
+ for i in range(self.k):
+ xe, ye = densities.gauss_ell(self.mu[i,:],
+ self.va[i*self.d:i*self.d+self.d,:], dim = c,
+ npoints = npoints)
+ Xe.append(xe)
+ Ye.append(ye)
+
+ return Xe, Ye
+
+ def gen_param(self, d, nc, varmode = 'diag', spread = 1):
+ """Generate valid parameters for a gaussian mixture model.
+ d is the dimension, nc the number of components, and varmode
+ the mode for cov matrices.
+
+ This is a class method.
+
+ Returns: w, mu, va
+ """
+ w = abs(N.randn(nc))
+ w = w / sum(w)
+
+ mu = spread * N.randn(nc, d)
+ if varmode == 'diag':
+ va = abs(N.randn(nc, d))
+ elif varmode == 'full':
+ va = N.randn(nc * d, d)
+ for k in range(nc):
+ va[k*d:k*d+d] = N.dot( va[k*d:k*d+d],
+ va[k*d:k*d+d].transpose())
+ else:
+ raise GmmParamError('cov matrix mode not recognized')
+
+ return w, mu, va
+
+ gen_param = classmethod(gen_param)
+
+
+# Function to generate a random index: this is kept outside any class,
+# as the function can be useful for other
+def gen_rand_index(p, n):
+ """Generate a N samples vector containing random index between 1
+ and length(p), each index i with probability p(i)"""
+ # TODO Check args here
+
+ # TODO: check each value of inverse distribution is
+ # different
+ invcdf = N.cumsum(p)
+ uni = N.rand(n)
+ index = N.zeros(n)
+
+ # This one should be a bit faster
+ for k in range(len(p)-1, 0, -1):
+ blop = N.where(N.logical_and(invcdf[k-1] <= uni,
+ uni < invcdf[k]))
+ index[blop] = k
+
+ return index
+
+def check_gmm_param(w, mu, va):
+ """Check that w, mu and va are valid parameters for
+ a mixture of gaussian: w should sum to 1, there should
+ be the same number of component in each param, the variances
+ should be positive definite, etc...
+
+ Params:
+ w = vector or list of weigths of the mixture (K elements)
+ mu = matrix: K * d
+ va = list of variances (vector K * d or square matrices Kd * d)
+
+ returns:
+ K = number of components
+ d = dimension
+ mode = 'diag' if diagonal covariance, 'full' of full matrices
+ """
+
+ # Check that w is valid
+ if N.fabs(N.sum(w) - 1) > MAX_DEV:
+ raise GmmParamError('weight does not sum to 1')
+
+ if not len(w.shape) == 1:
+ raise GmmParamError('weight is not a vector')
+
+ # Check that mean and va have the same number of components
+ K = len(w)
+
+ if N.ndim(mu) < 2:
+ msg = "mu should be a K,d matrix, and a row vector if only 1 comp"
+ raise GmmParamError(msg)
+ if N.ndim(va) < 2:
+ msg = """va should be a K,d / K *d, d matrix, and a row vector if
+ only 1 diag comp"""
+ raise GmmParamError(msg)
+
+ (Km, d) = mu.shape
+ (Ka, da) = va.shape
+
+ if not K == Km:
+ msg = "not same number of component in mean and weights"
+ raise GmmParamError(msg)
+
+ if not d == da:
+ msg = "not same number of dimensions in mean and variances"
+ raise GmmParamError(msg)
+
+ if Km == Ka:
+ mode = 'diag'
+ else:
+ mode = 'full'
+ if not Ka == Km*d:
+ msg = "not same number of dimensions in mean and variances"
+ raise GmmParamError(msg)
+
+ return K, d, mode
+
+if __name__ == '__main__':
+ # Meta parameters:
+ # - k = number of components
+ # - d = dimension
+ # - mode : mode of covariance matrices
+ d = 5
+ k = 5
+ mode = 'full'
+ nframes = 1e3
+
+ # Build a model with random parameters
+ w, mu, va = GM.gen_param(d, k, mode, spread = 3)
+ gm = GM(d, k, mode)
+ gm.set_param(w, mu, va)
+
+ # Sample nframes frames from the model
+ X = gm.sample(nframes)
+
+ # Plot
+ import pylab as P
+
+ P.plot(X[:, 0], X[:, 1], '.', label = '_nolegend_')
+
+ # Real confidence ellipses with level 0.39
+ Xre, Yre = gm.conf_ellipses()
+ P.plot(Xre[0], Yre[0], 'g', label = 'true confidence ellipsoides')
+ for i in range(1,k):
+ P.plot(Xre[i], Yre[i], 'g', label = '_nolegend_')
+
+ P.legend(loc = 0)
+ P.show()
Modified: trunk/Lib/sandbox/pyem/pyem/gmm_em.py
===================================================================
--- trunk/Lib/sandbox/pyem/pyem/gmm_em.py 2006-10-12 13:45:51 UTC (rev 2270)
+++ trunk/Lib/sandbox/pyem/pyem/gmm_em.py 2006-10-12 13:45:58 UTC (rev 2271)
@@ -1,19 +1,18 @@
# /usr/bin/python
-# Last Change: Thu Jul 13 07:00 PM 2006 J
+# Last Change: Fri Jul 14 04:00 PM 2006 J
import numpy as N
import numpy.linalg as lin
import densities
from kmean import kmean
+from gauss_mix import GM
-MAX_DEV = 1e-10
-
# Error classes
class GmmError(Exception):
"""Base class for exceptions in this module."""
pass
-class GmmParamError(GmmError):
+class GmmParamError:
"""Exception raised for errors in gmm params
Attributes:
@@ -26,139 +25,202 @@
def __str__(self):
return self.message
-# Function to generate a GMM, or valid parameters for GMM
-def gen_rand_index(p, n):
- """Generate a N samples vector containing random index between 1
- and length(p), each index i with probability p(i)"""
- # TODO Check args here
+# Not sure yet about how to design different mixture models. Most of the code
+# is different # (pdf, update part of EM, etc...) and I am not sure it makes
+# sense to use inheritance for # interface specification in python, since its
+# dynamic type systeme.
+
+# Anyway, a mixture class should encapsulates all details concerning a mixture model:
+# - internal parameters for the pdfs
+# - can compute sufficient statistics for EM
+# - can sample a model
+# - can generate random valid parameters for a new pdf (using class method)
+class MixtureModel:
+ pass
+
+class ExpMixtureModel(MixtureModel):
+ """Class to model mixture of exponential pdf (eg Gaussian, exponential, Laplace,
+ etc..). This is a special case because some parts of EM are common for those
+ modelsi..."""
+ pass
+
+class GMM(ExpMixtureModel):
+ """ A class to model a Gaussian Mixture Model (GMM). An instance of
+ this class is created by giving weights, mean and variances in the ctor.
+ An instanciated object can be sampled, trained by EM.
- # TODO: check each value of inverse distribution is
- # different
- invcdf = N.cumsum(p)
- uni = N.rand(n)
- index = N.zeros(n)
+ The class method gen_model can be used without instanciation."""
- # This one should be a bit faster
- for k in range(len(p)-1, 0, -1):
- blop = N.where(N.logical_and(invcdf[k-1] <= uni,
- uni < invcdf[k]))
- index[blop] = k
+ def init_kmean(self, data, niter = 5):
+ """ Init the model with kmean."""
+ k = self.gm.k
+ d = self.gm.d
+ init = data[0:k, :]
+
+ (code, label) = kmean(data, init, niter)
+
+ w = N.ones(k, float) / k
+ mu = code.copy()
+ if self.gm.mode == 'diag':
+ va = N.zeros((k, d), float)
+ for i in range(k):
+ for j in range(d):
+ va[i,j] = N.cov(data[N.where(label==i), j], rowvar = 0)
+ elif self.gm.mode == 'full':
+ va = N.zeros((k*d, d), float)
+ for i in range(k):
+ va[i*d:i*d+d,:] = \
+ N.cov(data[N.where(label==i)], rowvar = 0)
+ else:
+ raise GmmParamError("mode " + str(mode) + " not recognized")
+
+ self.gm.w = w
+ self.gm.mu = mu
+ self.gm.va = va
+
+ def init_random(self, data):
+ """ Init the model at random."""
+ k = self.gm.k
+ d = self.gm.d
+ if mode == 'diag':
+ w = N.ones(k, float) / k
+ mu = N.randn(k, d)
+ va = N.fabs(N.randn(k, d))
+ else:
+ raise GmmParamError("""init_random not implemented for
+ mode %s yet""", mode)
+
+ self.gm.w = w
+ self.gm.mu = mu
+ self.gm.va = va
+
+ # Possible init methods
+ _init_methods = {'kmean': init_kmean, 'random' : init_random}
+ # TODO:
+ # - format of parameters ? For variances, list of variances matrix,
+ # keep the current format, have 3d matrices ?
+ # - To handle the different modes, we could do something "fancy" such as
+ # replacing methods, to avoid checking cases everywhere and unconsistency.
+ def __init__(self, gm, init = 'kmean'):
+ """ Initialize a GMM with weight w, mean mu and variances va, and initialization
+ method for training init (kmean by default)"""
+ self.gm = gm
+
+ if init not in self._init_methods:
+ raise GmmParamError('init method %s not recognized' + str(init))
+
+ GMM.init = self._init_methods[init]
+
+ def sufficient_statistics(self, data):
+ """ Return normalized and non-normalized sufficient statistics
+ from the model.
- return index
+ Computes the latent variable distribution (a
+ posteriori probability) knowing the explicit data
+ for the Gaussian model (w, mu, var): gamma(t, i) =
+ P[state = i | observation = data(t); w, mu, va]
-def gen_gmm(w, mu, va, n):
- """Generate a gaussiam mixture model with weights w,
- mean mu and variances va. Each column of the parameters
- are one component parameter.
- """
- # Check args
- K, d, mode = check_gmm_param(w, mu, va)
+ This is basically the E step of EM for GMM."""
+ n = data.shape[0]
- # Generate the mixture
- S = gen_rand_index(w, n) # State index (ie hidden var)
- X = N.randn(n, d) # standard gaussian
+ # compute the gaussian pdf
+ tgd = multiple_gauss_den(data, self.gm.mu, self.gm.va)
+ # multiply by the weight
+ tgd *= self.gm.w
+ # Normalize to get a pdf
+ gd = tgd / N.sum(tgd, axis=1)[:, N.NewAxis]
- if mode == 'diag':
- X = mu[S, :] + X * N.sqrt(va[S,:])
- elif mode == 'full':
- # Faster:
- cho = N.zeros((K, va.shape[1], va.shape[1]), float)
- for k in range(K):
- # Using cholesky is more stable than sqrtm; sqrtm is not
- # available in numpy anyway, only in scipy...
- cho[k] = lin.cholesky(va[k*d:k*d+d,:])
+ return gd, tgd
- for s in range(K):
- tmpind = N.where(S == s)[0]
- X[tmpind] = N.matrixmultiply(X[tmpind], cho[s].transpose()) + mu[s]
- else:
- raise GmmParamError('cov matrix mode not recognized')
+ def update_em(self, data, gamma):
+ """Computes update of the Gaussian Mixture Model (M step)
+ from the a posteriori pdf, computed by gmm_posterior
+ (E step).
+ """
+ k = self.gm.k
+ d = self.gm.d
+ n = data.shape[0]
+ invn = 1.0/n
+ mGamma = N.sum(gamma)
- return X
+ if self.gm.mode == 'diag':
+ mu = N.zeros((k, d), float)
+ va = N.zeros((k, d), float)
+ gamma = gamma.transpose()
+ for c in range(k):
+ # x = N.sum(N.outerproduct(gamma[:, k],
+ # N.ones((1, d))) * data)
+ # xx = N.sum(N.outerproduct(gamma[:, k],
+ # N.ones((1, d))) * (data ** 2))
+ x = N.dot(gamma[c:c+1,:], data)[0,:]
+ xx = N.dot(gamma[c:c+1,:], data ** 2)[0,:]
-def gen_gmm_param(d, K, varmode = 'diag', spread = 1):
- """Generate valid parameters for a gaussian mixture model.
- d is the dimension, K the number of components, and varmode
- the mode for cov matrices.
+ mu[c,:] = x / mGamma[c]
+ va[c,:] = xx / mGamma[c] - mu[c,:] ** 2
+ w = invn * mGamma
- Returns:
- - w
- - mu
- - va
- """
- w = abs(N.randn(K))
- w = w / sum(w)
+ elif self.gm.mode == 'full':
+ mu = N.zeros((k, d), float)
+ va = N.zeros((k*d, d), float)
- mu = spread * N.randn(K, d)
- if varmode == 'diag':
- va = abs(N.randn(K, d))
- elif varmode == 'full':
- va = N.randn(K * d, d)
- for k in range(K):
- va[k*d:k*d+d] = N.matrixmultiply( va[k*d:k*d+d],
- va[k*d:k*d+d].transpose())
- else:
- raise GmmParamError('cov matrix mode not recognized')
+ for c in range(k):
+ x = N.sum(N.outerproduct(gamma[:, c],
+ N.ones((1, d), float)) * data)
+ xx = N.zeros((d, d), float)
+
+ # This should be much faster than recursing on n...
+ for i in range(d):
+ for j in range(d):
+ xx[i,j] = N.sum(data[:,i] * data[:,j] * gamma[:,c])
- return w, mu, va
+ mu[c,:] = x / mGamma[c]
+ va[c*d:c*d+d,:] = xx / mGamma[c] - \
+ N.outerproduct(mu[c,:], mu[c,:])
+ w = invn * mGamma
+ else:
+ raise GmmParamError("varmode not recognized")
-def check_gmm_param(w, mu, va):
- """Check that w, mu and va are valid parameters for
- a mixture of gaussian: w should sum to 1, there should
- be the same number of component in each param, the variances
- should be positive definite, etc...
-
- Params:
- w = vector or list of weigths of the mixture (K elements)
- mu = matrix: K * d
- va = list of variances (vector K * d or square matrices Kd * d)
+ self.gm.set_param(w, mu, va)
- returns:
- K = number of components
- d = dimension
- mode = 'diag' if diagonal covariance, 'full' of full matrices
- """
-
- # Check that w is valid
- if N.fabs(N.sum(w) - 1) > MAX_DEV:
- raise GmmParamError('weight does not sum to 1')
+class EM:
+ """An EM trainer. An EM trainer
+ trains from data, with a model
- if not len(w.shape) == 1:
- raise GmmParamError('weight is not a vector')
+ Not really useful yet"""
+ def __init__(self):
+ pass
- # Check that mean and va have the same number of components
- K = len(w)
+ def train(self, data, model, niter = 10):
+ """
+ Train a model using data, with niter iterations.
- if N.ndim(mu) < 2:
- msg = "mu should be a K,d matrix, and a row vector if only 1 comp"
- raise GmmParamError(msg)
- if N.ndim(va) < 2:
- msg = """va should be a K,d / K *d, d matrix, and a row vector if
- only 1 diag comp"""
- raise GmmParamError(msg)
+ Args:
+ - data: contains the observed features, one row is one frame, ie one
+ observation of dimension d
+ - model: object of class Mixture
+ - niter: number of iterations
- (Km, d) = mu.shape
- (Ka, da) = va.shape
+ The model is trained, and its parameters updated accordingly.
- if not K == Km:
- msg = "not same number of component in mean and weights"
- raise GmmParamError(msg)
+ Returns:
+ likelihood (one value per iteration).
+ """
- if not d == da:
- msg = "not same number of dimensions in mean and variances"
- raise GmmParamError(msg)
+ # Initialize the data (may do nothing depending on the model)
+ model.init(data)
- if Km == Ka:
- mode = 'diag'
- else:
- mode = 'full'
- if not Ka == Km*d:
- msg = "not same number of dimensions in mean and variances"
- raise GmmParamError(msg)
-
- return K, d, mode
-
-# For EM on GMM
+ # Likelihood is kept
+ like = N.zeros(niter, float)
+
+ # Em computation, with computation of the likelihood
+ for i in range(niter):
+ g, tgd = model.sufficient_statistics(data)
+ like[i] = N.sum(N.log(N.sum(tgd, 1)))
+ model.update_em(data, g)
+
+ return like
+
+# Misc functions
def multiple_gauss_den(data, mu, va):
"""Helper function to generate several Gaussian
pdf (different parameters) from the same data"""
@@ -180,279 +242,99 @@
return y
-def gmm_init_kmean(data, k, mode, init = [], niter = 10):
- """gmm_init_kmean(data, k, mode, init = [], niter = 10)
-
- Init EM for GMM with kmean from data, for k components.
-
- Args:
- - data: Each row of data is one frame of dimension d.
- - k: Number of components to look for
- - mode: Diagonal or Full covariance matrices
- - init: The initial centroids. The value given for k is
- ignored, and the number of row in initc is used instead.
- If initc is not given, then the centroids are initialized
- with the k first values of data.
- - niter: Number of iterations in kmean.
-
- Returns:
- (w, mu, va), initial parameters for a GMM.
-
- Method:
- Each weight is equiprobable, each mean is one centroid returned by kmean, and
- covariances for component i is initialized with covariance of
- data corresponding with label i. Other strategies are possible, this one
- is an easy one"""
- if len(init) == 0:
- init = data[0:k, :]
- else:
- k = initc.shape[0]
-
- if data.ndim == 1:
- d = 1
- else:
- d = N.shape(data)[1]
-
- (code, label) = kmean(data, init, niter)
-
- w = N.ones(k, float) / k
- mu = code.copy()
- if mode == 'diag':
- va = N.zeros((k, d), float)
- for i in range(k):
- for j in range(d):
- va[i,j] = N.cov(data[N.where(label==i), j], rowvar = 0)
- elif mode == 'full':
- va = N.zeros((k*d, d), float)
- for i in range(k):
- va[i*d:i*d+d,:] = N.cov(data[N.where(label==i)], rowvar = 0)
- else:
- raise GmmParamError("mode " + str(mode) + " not recognized")
-
- return w, mu, va
-
-# This function is just calling gmm_update and gmm_posterior, with
-# initialization. This is ugly, and we should have a class to model a GMM
-# instead of all this code to try to guess correct values and parameters...
-def gmm_em(data, niter = 10, k = 2, mode = 'diag', w = [], mu = [], va = []):
- """
- gmm_em(data, niter = 10, k = 2, mode = 'diag', w = [], mu = [], va = []):
-
- Compute the parameters of a Gaussian Mixture Model using EM algorithm,
- with initial values w, mu and va (overwritten by the function).
-
- Args:
- - data: contains the observed features, one row is one frame, ie one
- observation of dimension d
- - niter: number of iterations
- - mode: 'diag' or 'full', depending on the wanted model for cov
- matrices.
- - K: number of components
- - w, mu, va initial parameters for the GMM. All or none must be given.
- If no initial values are given, initialized by gmm_init_kmean; if they
- are given, mode and k are ignored, and guessed from the given parameters
- instead.
-
- Returns:
- w, mu, va, like as found by EM, where like is the likelihood for each
- iteration.
- """
- if len(w) == 0:
- w, mu, va = gmm_init_kmean(data, k, mode, niter = 5)
- k, d, mode = check_gmm_param(w, mu, va)
-
- like = N.zeros(niter, float)
- for i in range(niter):
- g, tgd = gmm_posterior(data, w, mu, va)
- like[i] = N.sum(N.log(N.sum(tgd, 1)))
- w, mu, va = gmm_update(data, g, d, k, mode)
-
- return w, mu, va, like
-
-def gmm_posterior(data, w, mu, va):
- """ Computes the latent variable distribution (a
- posteriori probability) knowing the explicit data
- for the Gaussian model (w, mu, var): gamma(t, i) =
- P[state = i | observation = data(t); w, mu, va]
-
- This is basically the E step of EM for GMM.
-
- the second returned value is the non normalized version
- of gamma, and may be needed for some computation,
- like eg likelihood"""
- n = data.shape[0]
- K = len(w)
-
- # compute the gaussian pdf
- tgd = multiple_gauss_den(data, mu, va)
- # multiply by the weight
- tgd *= w
- # Normalize to get a pdf
- gd = tgd / N.sum(tgd, axis=1)[:, N.NewAxis]
-
- return gd, tgd
-
-def gmm_update(data, gamma, d, K, varmode):
- """Computes update of the Gaussian Mixture Model (M step)
- from the a posteriori pdf, computed by gmm_posterior
- (E step).
- """
- n = data.shape[0]
- invn = 1.0/n
- mGamma = N.sum(gamma)
-
- if varmode == 'diag':
- mu = N.zeros((K, d), float)
- va = N.zeros((K, d), float)
- for k in range(K):
- x = N.sum(N.outerproduct(gamma[:, k],
- N.ones((1, d))) * data)
- xx = N.sum(N.outerproduct(gamma[:, k],
- N.ones((1, d))) * (data ** 2))
-
- mu[k,:] = x / mGamma[k]
- va[k,:] = xx / mGamma[k] - mu[k,:] ** 2
- w = invn * mGamma
-
- elif varmode == 'full':
- mu = N.zeros((K, d), float)
- va = N.zeros((K*d, d), float)
-
- for k in range(K):
- x = N.sum(N.outerproduct(gamma[:, k],
- N.ones((1, d), float)) * data)
- xx = N.zeros((d, d), float)
-
- # This should be much faster than reecursing on n...
- for i in range(d):
- for j in range(d):
- xx[i,j] = N.sum(data[:,i] * data[:,j] * gamma[:,k])
-
- mu[k,:] = x / mGamma[k]
- va[k*d:k*d+d,:] = xx / mGamma[k] - \
- N.outerproduct(mu[k,:], mu[k,:])
- w = invn * mGamma
- else:
- raise GmmParamError("varmode not recognized")
-
- return w, mu, va
-
-# Misc functions
-def gmm_ellipses(mu, va, c = [0, 1], npoints = 100):
- """Returns a list of ellipses describing the Gmm
- defined by mu and va. c is the dimension we are projecting
- the variances on a 2d space.
-
- Returns:
- -Xe: a list of x coordinates for the ellipses
- -Ye: a list of y coordinates for the ellipses
-
- Example:
- Suppose we have w, mu and va as parameters for a mixture, then:
-
- X = gen_gmm(w, mu, va, 1000)
- Xe, Ye = gmm_ellipses(mu, va)
- pylab.plot(X[:,0], X[:, 1], '.')
- for k in len(w):
- pylab.plot(Xe[k], Ye[k], 'r')
-
- Will plot samples X draw from the mixture model, and
- plot the ellipses of equi-probability from the mean with
- fixed level of confidence 0.39.
-
- TODO: be able to modify the confidence interval to arbitrary
- value (to do in gauss_ell)"""
- K = mu.shape[0]
- w = N.ones(K, float) / K
-
- K, d, mode = check_gmm_param(w, mu, va)
-
- # TODO: adjustable level (to do in gauss_ell).
- # For now, a level of 0.39 means that we draw
- # ellipses for 1 standard deviation.
- Xe = []
- Ye = []
- if mode == 'diag':
- for i in range(K):
- xe, ye = densities.gauss_ell(mu[i,:], va[i,:], dim = c,
- npoints = npoints)
- Xe.append(xe)
- Ye.append(ye)
- elif mode == 'full':
- for i in range(K):
- xe, ye = densities.gauss_ell(mu[i,:], va[i*d:i*d+d,:], dim = c,
- npoints = npoints)
- Xe.append(xe)
- Ye.append(ye)
-
- return Xe, Ye
-
if __name__ == "__main__":
+ import copy
#=============================
# Simple GMM with 5 components
#=============================
- import pylab as P
- k = 5
- d = 5
- mode = 'diag'
+ #+++++++++++++++++++++++++++++
+ # Meta parameters of the model
+ # - k: Number of components
+ # - d: dimension of each Gaussian
+ # - mode: Mode of covariance matrix: full or diag
+ # - nframes: number of frames (frame = one data point = one
+ # row of d elements
+ k = 3
+ d = 2
+ mode = 'diag'
+ nframes = 1e3
+
+ #+++++++++++++++++++++++++++++++++++++++++++
+ # Create an artificial GMM model, samples it
+ #+++++++++++++++++++++++++++++++++++++++++++
print "Generating the mixture"
# Generate a model with k components, d dimensions
- wr, mur, var = gen_gmm_param(d, k, mode, 3)
- X = gen_gmm(wr, mur, var, 1e3)
+ w, mu, va = GM.gen_param(d, k, mode, spread = 3)
+ gm = GM(d, k, mode)
+ gm.set_param(w, mu, va)
- print "Init the mixture"
- # Init the mixture with kmean
- w0, mu0, va0 = gmm_init_kmean(X, k, mode, niter = 5)
-
- # # Use random values instead of kmean
- # w0 = N.ones(k, float) / k
- # mu0 = N.randn(k, d)
- # va0 = N.fabs(N.randn(k, d))
+ # Sample nframes frames from the model
+ data = gm.sample(nframes)
- # Copy the initial values because we want to draw them later...
- w = w0.copy()
- mu = mu0.copy()
- va = va0.copy()
+ #++++++++++++++++++++++++
+ # Learn the model with EM
+ #++++++++++++++++++++++++
+ # Init the model
+ print "Init a model for learning, with kmean for initialization"
+ lgm = GM(d, k, mode)
+ gmm = GMM(lgm, 'kmean')
+
+ gmm.init(data)
+ # Keep the initialized model for drawing
+ gm0 = copy.copy(lgm)
+
# The actual EM, with likelihood computation
niter = 10
like = N.zeros(niter, float)
print "computing..."
for i in range(niter):
- g, tgd = gmm_posterior(X, w, mu, va)
+ g, tgd = gmm.sufficient_statistics(data)
like[i] = N.sum(N.log(N.sum(tgd, 1)))
- w, mu, va = gmm_update(X, g, d, k, mode)
+ gmm.update_em(data, g)
+ # # Alternative form, by using EM class: as the EM class
+ # # is quite rudimentary now, it is not very useful, just save
+ # # a few lines
+ # em = EM()
+ # like = em.train(data, gmm, niter)
+ #+++++++++++++++
+ # Draw the model
+ #+++++++++++++++
print "drawing..."
- # Draw what is happening
+ import pylab as P
P.subplot(2, 1, 1)
- P.plot(X[:, 0], X[:, 1], '.', label = '_nolegend_')
+ # Draw what is happening
+ P.plot(data[:, 0], data[:, 1], '.', label = '_nolegend_')
# Real confidence ellipses
- Xre, Yre = gmm_ellipses(mur, var)
+ Xre, Yre = gm.conf_ellipses()
P.plot(Xre[0], Yre[0], 'g', label = 'true confidence ellipsoides')
for i in range(1,k):
P.plot(Xre[i], Yre[i], 'g', label = '_nolegend_')
# Initial confidence ellipses as found by kmean
- X0e, Y0e = gmm_ellipses(mu0, va0)
+ X0e, Y0e = gm0.conf_ellipses()
P.plot(X0e[0], Y0e[0], 'k', label = 'initial confidence ellipsoides')
for i in range(1,k):
P.plot(X0e[i], Y0e[i], 'k', label = '_nolegend_')
# Values found by EM
- Xe, Ye = gmm_ellipses(mu, va)
+ Xe, Ye = lgm.conf_ellipses()
P.plot(Xe[0], Ye[0], 'r', label = 'confidence ellipsoides found by EM')
for i in range(1,k):
P.plot(Xe[i], Ye[i], 'r', label = '_nolegend_')
P.legend(loc = 0)
+
P.subplot(2, 1, 2)
P.plot(like)
P.title('log likelihood')
+ # #++++++++++++++++++
# # Export the figure
+ # #++++++++++++++++++
# F = P.gcf()
# DPI = F.get_dpi()
# DefaultSize = F.get_size_inches()
More information about the Scipy-svn
mailing list