/*
* Artificial Intelligence for Humans
* Volume 1: Fundamental Algorithms
* Java Version
* http://www.aifh.org
* http://www.jeffheaton.com
*
* Code repository:
* https://github.com/jeffheaton/aifh
* Copyright 2013 by Jeff Heaton
*
* 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.
*
* For more information on Heaton Research copyrights, licenses
* and trademarks visit:
* http://www.heatonresearch.com/copyright
*/
package com.heatonresearch.aifh.kmeans;
import com.heatonresearch.aifh.AIFHError;
import com.heatonresearch.aifh.distance.CalculateDistance;
import com.heatonresearch.aifh.distance.EuclideanDistance;
import com.heatonresearch.aifh.general.data.BasicData;
import com.heatonresearch.aifh.randomize.BasicGenerateRandom;
import com.heatonresearch.aifh.randomize.GenerateRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* KMeans Clustering. First, observations are each placed into random clusters. There are two methods to do this:
* random and Forgy. Then we iterate through assignment and update steps. Assignment places clusters in new clusters
* that they might be closer to. Update updates the center of each cluster, called the centroid. The center of each
* cluster is the mean of all observations in that cluster.
* <p/>
* This class uses a number of supporting objects:
* <p/>
* randomGeneration: The random number generator used for clustering.
* distanceMetric: The distance metric used to determine distance to centroids.
* <p/>
* http://en.wikipedia.org/wiki/Kmeans
*/
public class KMeans {
/**
* The number of clusters.
*/
private final int k;
/**
* The clusters.
*/
private final List<Cluster> clusters = new ArrayList<Cluster>();
/**
* The random number generator to use.
*/
private GenerateRandom randomGeneration = new BasicGenerateRandom();
/**
* The
*/
private CalculateDistance distanceMetric = new EuclideanDistance();
/**
* Construct the object with K clusters.
*
* @param theK The number of clusters (K).
*/
public KMeans(final int theK) {
this.k = theK;
}
/**
* Validate and find the number of dimensions from the first observation.
*
* @param theObservations The observations.
* @return The number of dimensions.
*/
private int findDimensions(final List<BasicData> theObservations) {
if (theObservations.size() == 0) {
throw new AIFHError("No observations provided to cluster, array zero length.");
}
if (theObservations.size() < this.k) {
throw new AIFHError("There are fewer observations ("
+ theObservations.size() + ") than k (" + this.k + ").");
}
final int dimensions = theObservations.get(0).getInput().length;
if (dimensions == 0) {
throw new AIFHError("Observations have no dimensions.");
}
return dimensions;
}
/**
* Init the observations to random clusters. Use the "Init Random" algorithm. The Random Partition method first
* randomly assigns a cluster to each observation and then proceeds to the update step, thus computing the initial mean to be the centroid of the cluster's randomly assigned points.
*
* @param theObservations The observations to cluster.
*/
public void initRandom(final List<BasicData> theObservations) {
final int dimensions = findDimensions(theObservations);
// create the clusters
for (int i = 0; i < this.k; i++) {
this.clusters.add(new Cluster(dimensions));
}
// assign each observation to a random cluster
for (final BasicData observation : theObservations) {
final int clusterIndex = this.randomGeneration.nextInt(this.k);
final Cluster cluster = this.clusters.get(clusterIndex);
cluster.getObservations().add(observation);
}
// handle any empty clusters
for (final Cluster cluster : this.clusters) {
if (cluster.getObservations().size() == 0) {
boolean done = false;
while (!done) {
final int sourceIndex = this.randomGeneration.nextInt(this.k);
final Cluster source = this.clusters.get(sourceIndex);
if (source != cluster && source.getObservations().size() > 1) {
final int sourceObservationIndex = this.randomGeneration.nextInt(source.getObservations().size());
final BasicData sourceObservation = source.getObservations().get(sourceObservationIndex);
source.getObservations().remove(sourceObservationIndex);
cluster.getObservations().add(sourceObservation);
done = true;
}
}
}
}
// calculate initial centers
updateStep();
}
/**
* Init the observations to random clusters. The Forgy method randomly chooses k observations from the
* data set and uses these as the initial means.
*
* @param theObservations The observations to cluster.
*/
public void initForgy(final List<BasicData> theObservations) {
final int dimensions = findDimensions(theObservations);
this.clusters.clear();
final Set<Integer> usedObservations = new HashSet<Integer>();
for (int i = 0; i < this.k; i++) {
final Cluster cluster = new Cluster(dimensions);
this.clusters.add(cluster);
int observationIndex = -1;
while (observationIndex == -1) {
observationIndex = this.randomGeneration.nextInt(theObservations.size());
if (usedObservations.contains(observationIndex)) {
observationIndex = -1;
}
}
final double[] observation = theObservations.get(observationIndex).getInput();
System.arraycopy(observation, 0, cluster.getCenter(), 0, dimensions);
usedObservations.add(observationIndex);
}
// assign all observations to a cluster
for (final BasicData observation : theObservations) {
final Cluster cluster = findNearestCluster(observation.getInput());
cluster.getObservations().add(observation);
}
// calculate initial centers
updateStep();
}
/**
* The update step updates the centroids.
*/
private void updateStep() {
for (final Cluster cluster : clusters) {
cluster.calculateCenter();
}
}
/**
* The assignment step assigns observations to the nearest clusters.
*
* @return True, if we are done. We are done if no observations moved clusters.
*/
private boolean assignmentStep() {
boolean done = true;
for (final Cluster cluster : this.clusters) {
int observationIndex = 0;
int observationCount = cluster.getObservations().size();
if (observationCount > 1) {
while (observationIndex < observationCount) {
final BasicData observation = cluster.getObservations().get(observationIndex++);
final Cluster targetCluster = findNearestCluster(observation.getInput());
if (targetCluster != cluster) {
cluster.getObservations().remove(observation);
targetCluster.getObservations().add(observation);
observationCount--;
done = false;
}
}
}
}
return done;
}
/**
* Find the nearest cluster for an observation.
*
* @param observation The observation.
* @return The nearest cluster.
*/
public Cluster findNearestCluster(final double[] observation) {
Cluster result = null;
double resultDist = Double.POSITIVE_INFINITY;
for (final Cluster cluster : this.clusters) {
final double dist = this.distanceMetric.calculate(observation, cluster.getCenter());
if (dist < resultDist) {
resultDist = dist;
result = cluster;
}
}
return result;
}
/**
* Perform one iteration of assignment and update steps.
*
* @return True, if we are done, no new assignments.
*/
public boolean iteration() {
if (this.clusters.size() == 0) {
throw new AIFHError("Must call one of the init methods first.");
}
final boolean done = assignmentStep();
if (!done) {
updateStep();
}
return done;
}
/**
* Perform the specified number of iterations. Stop early if we are done.
*
* @param maxIterations The max number of iterations.
* @return True, if we are done.
*/
public int iteration(final int maxIterations) {
int iterationCount = 1;
while (iterationCount <= maxIterations && !iteration()) {
iterationCount++;
}
return iterationCount;
}
/**
* @return The random number generator used.
*/
public GenerateRandom getRandomGeneration() {
return randomGeneration;
}
/**
* Set the random number generator to use.
*
* @param randomGeneration The random generator to use.
*/
public void setRandomGeneration(final GenerateRandom randomGeneration) {
this.randomGeneration = randomGeneration;
}
/**
* @return The distance metric used.
*/
public CalculateDistance getDistanceMetric() {
return distanceMetric;
}
/**
* Set the distance metric to use.
*
* @param distanceMetric The distance metric.
*/
public void setDistanceMetric(final CalculateDistance distanceMetric) {
this.distanceMetric = distanceMetric;
}
/**
* @return The number of clusters.
*/
public int getK() {
return k;
}
/**
* @return The clusters.
*/
public List<Cluster> getClusters() {
return clusters;
}
}