/*
* Copyright (c) 2012-2015, Peter Abeles. All Rights Reserved.
*
* This file is part of DDogleg (http://ddogleg.org).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ddogleg.clustering.gmm;
import org.ddogleg.clustering.AssignCluster;
import org.ddogleg.clustering.ComputeClusters;
import org.ddogleg.struct.FastQueue;
import org.ddogleg.struct.GrowQueue_F64;
import org.ejml.ops.CommonOps;
import java.util.List;
/**
* Standard expectation maximization based approach to fitting mixture-of-Gaussian models to a set of data.
* A locally optimal maximum likelihood estimate is found. The full covariance is found. Some other
* variants will estimate just diagonal elements or a single covariance, but that isn't yet supported.
*
*
* Converged if, {@code(D[i] - D[i-1])/D[i] <= tol}, where D is the sum of point from cluster distance at iteration 'i',
* and tol is the convergence tolerance threshold.
*
*
* @author Peter Abeles
*/
// TODO Unconstrained covariance
// TODO just diagonal covariance
// TODO added shared and tied covariance?
public class ExpectationMaximizationGmm_F64 implements ComputeClusters {
// Used to select initial parameters
InitializeGmm_F64 selectInitial;
// storage for mixture models
FastQueue mixture;
// info for each points
FastQueue info = new FastQueue(PointInfo.class,true);
// Maximum number of iterations\
int maxIterations;
// If the fractional change in score is less or equal to this value then it has converged.
// ||prev-curr||/prev
double convergeTol;
// Used to compute the likelihood for each Gaussian
GaussianLikelihoodManager likelihoodManager;
// internal work space for computing the difference between the mean and point
double dx[] = new double[1];
// compute chi-square error
double errorChiSquare;
// true for verbose output to standard out
boolean verbose;
/**
* Configures EM parameters
*
* @param maxIterations Maximum number of iterations
* @param convergeTol If the relative change in score is less or equal than this amount it has converged
* @param selectInitial Used to select initial seeds for the clusters
*/
public ExpectationMaximizationGmm_F64(int maxIterations,
double convergeTol,
InitializeGmm_F64 selectInitial) {
this.maxIterations = maxIterations;
this.convergeTol = convergeTol;
this.selectInitial = selectInitial;
System.err.println("WARNING: GMM-EM is a work in progress! Might not work in your situation");
}
@Override
public void init(final int pointDimension, long randomSeed) {
mixture = new FastQueue(GaussianGmm_F64.class,true ) {
@Override
protected GaussianGmm_F64 createInstance() {
return new GaussianGmm_F64(pointDimension);
}
};
selectInitial.init(pointDimension,randomSeed);
if( dx.length < pointDimension )
dx = new double[pointDimension];
likelihoodManager = new GaussianLikelihoodManager(pointDimension,mixture.toList());
}
@Override
public void process(List points, int numCluster) {
// setup data structures
mixture.resize(numCluster);
for (int i = 0; i < points.size(); i++) {
PointInfo p = info.grow();
p.point = points.get(i);
p.weights.resize(numCluster);
}
if( verbose ) System.out.println("GMM-EM: Selecting initial seeds");
// Select initial distributions
selectInitial.selectSeeds(points,mixture.toList());
likelihoodManager.precomputeAll();
if( verbose ) System.out.println("GMM-EM: Entering main loop");
// perform EM iteration
double errorBefore = Double.MAX_VALUE;
for (int iteration = 0; iteration < maxIterations; iteration++) {
// compute the expectation for each point and compute the chi-square error
errorChiSquare = expectation();
if( verbose ) System.out.println("GMM-EM: "+iteration+" errorChiSquare "+errorChiSquare);
// check for convergence
double fractionChange = 1.0 - errorChiSquare/errorBefore;
if( fractionChange >= 0 && fractionChange <= convergeTol) {
if( verbose ) System.out.println("GMM-EM: CONVERGED");
break;
}
errorBefore = errorChiSquare;
// update the Gaussian distributions
maximization();
likelihoodManager.precomputeAll();
}
// clean up
for (int i = 0; i < info.size; i++) {
info.data[i].point = null; // de-reference so the memory could be freed
}
info.reset();
}
/**
* For each point compute the "responsibility" for each Gaussian
*
* @return The sum of chi-square. Can be used to estimate the total error.
*/
protected double expectation() {
double sumChiSq = 0;
for (int i = 0; i < info.size(); i++) {
PointInfo p = info.get(i);
// identify the best cluster match and save it's chi-square for convergence testing
double bestLikelihood = 0;
double bestChiSq = Double.MAX_VALUE;
double total = 0;
for (int j = 0; j < mixture.size; j++) {
GaussianLikelihoodManager.Likelihood g = likelihoodManager.getLikelihood(j);
double likelihood = g.likelihood(p.point);
total += p.weights.data[j] = likelihood;
if( likelihood > bestLikelihood ) {
bestLikelihood = likelihood;
bestChiSq = g.getChisq();
}
}
// make sure it sums up to 1
if( total > 0 ) {
for (int j = 0; j < mixture.size; j++) {
p.weights.data[j] /= total;
}
}
// only add the best chi-square since the other mixtures might be far away
// I guess I could use the weights to do this too.
sumChiSq += bestChiSq;
}
return sumChiSq;
}
/**
* Using points responsibility information to recompute the Gaussians and their weights, maximizing
* the likelihood of the mixture.
*/
protected void maximization() {
// discard previous parameters by zeroing
for (int i = 0; i < mixture.size; i++) {
mixture.get(i).zero();
}
// compute the new mean
for (int i = 0; i < info.size; i++) {
PointInfo p = info.get(i);
for (int j = 0; j < mixture.size; j++) {
mixture.get(j).addMean(p.point,p.weights.get(j));
}
}
for (int i = 0; i < mixture.size; i++) {
GaussianGmm_F64 g = mixture.get(i);
if( g.weight > 0 )
CommonOps.divide(g.mean,g.weight);
}
// compute new covariance
for (int i = 0; i < info.size; i++) {
PointInfo pp = info.get(i);
double[] p = pp.point;
for (int j = 0; j < mixture.size; j++) {
GaussianGmm_F64 g = mixture.get(j);
for (int k = 0; k < p.length; k++) {
dx[k] = p[k]-g.mean.data[k];
}
mixture.get(j).addCovariance(dx, pp.weights.get(j));
}
}
double totalMixtureWeight = 0;
for (int i = 0; i < mixture.size; i++) {
GaussianGmm_F64 g = mixture.get(i);
if( g.weight > 0 ) {
CommonOps.divide(g.covariance, g.weight);
totalMixtureWeight += g.weight;
}
}
// update the weight
for (int i = 0; i < mixture.size; i++) {
mixture.get(i).weight /= totalMixtureWeight;
}
}
@Override
public AssignCluster getAssignment() {
return new AssignGmm_F64(mixture.toList());
}
@Override
public double getDistanceMeasure() {
return errorChiSquare;
}
@Override
public void setVerbose(boolean verbose) {
selectInitial.setVerbose(verbose);
this.verbose = verbose;
}
public static class PointInfo
{
public double[] point; // reference to the original input point
public GrowQueue_F64 weights = new GrowQueue_F64();
}
}
Ads help maintain this website.