package me.mcnelis.rudder.ml.unsupervised.clustering; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import me.mcnelis.rudder.data.collections.IRudderList; import me.mcnelis.rudder.data.collections.RudderList; import org.apache.commons.math.stat.descriptive.SynchronizedSummaryStatistics; import org.apache.commons.math.util.MathUtils; import org.apache.log4j.Logger; public class KMeans<T> { private static final Logger LOG = Logger.getLogger(KMeans.class); protected int k; protected List<Cluster<T>> clusters; protected double[][] previousCenters; protected long maxIterations; protected long currentIteration = 0l; protected double minMovement = .0000001d; protected IRudderList<T> sourceData; public KMeans(int k, IRudderList<T> data) { this.k = k; this.sourceData = data; this.init(); } public KMeans(int k) { this.k = k; this.init(); } public KMeans() { this.init(); } private void init() { this.maxIterations = 1000000000000l; this.minMovement = .0000001d; } public List<Cluster<T>> cluster() { // Get the random centers this.chooseRandomCenters(); do { this.currentIteration++; this.assignClusters(); // This could be parallelized for (Cluster<?> c : this.clusters) { LOG.debug("Cluster size: " + c.size()); if(c.size() > 0) { c.calculateCentroid(); } } } while (this.keepClustering()); return this.clusters; } protected boolean keepClustering() { SynchronizedSummaryStatistics distanceStats = new SynchronizedSummaryStatistics(); if (this.previousCenters == null) { this.previousCenters = new double[this.clusters.size()][]; this.assignPreviousClusters(); return true; } for (int i = 0; i < this.previousCenters.length; i++) { distanceStats.addValue(MathUtils.distance(this.previousCenters[i], this.clusters.get(i).getCentroid())); } this.assignPreviousClusters(); return !(this.currentIteration > this.maxIterations || this.minMovement <= distanceStats.getMean()); } protected void assignPreviousClusters() { for (int i = 0; i < this.clusters.size(); i++) { this.previousCenters[i] = this.clusters.get(i).getCentroid(); } } /** * Assign records to the nearest cluster based on Euclidean distance */ protected void assignClusters() { LOG.debug("Source data size: " + this.sourceData.size()); List<Cluster<T>> tempClusters = new ArrayList<Cluster<T>>(); for(int i = 0; i < this.clusters.size(); i++) { tempClusters.add(new Cluster<T>()); } for (T o : this.sourceData) { double min = Double.NaN; int clusterIdx = -1; for (int i = 0; i < this.clusters.size(); i++) { double distance = MathUtils.distance(this.clusters.get(i) .getCentroid(), this.sourceData.getUnsupervisedDoubleArray(o)); LOG.trace("Record: " + Arrays.toString(this.sourceData.getUnsupervisedDoubleArray(o))); if (Double.isNaN(min) || distance < min) { clusterIdx = i; min = distance; } } tempClusters.get(clusterIdx).addRecord(o); } this.clusters = tempClusters; } /** * Choose random K elements as initial center points. * * Other KMeans implementations have better initial centroid selection. This * could be overloaded with other implementations. * * @return clusters with a center set */ protected List<Cluster<T>> chooseRandomCenters() { ArrayList<Cluster<T>> randomCluster = new ArrayList<Cluster<T>>(); Random generator = new Random(); for (int i = 0; i < this.k; i++) { Cluster<T> c = new Cluster<T>(); c.setCentroid( this.sourceData.getUnsupervisedDoubleArray( this.sourceData.get( generator.nextInt( this.sourceData.size() ) ) ) ); randomCluster.add(c); } this.clusters = randomCluster; return this.clusters; } public int getK() { return k; } public void setK(int k) { this.k = k; } public List<Cluster<T>> getClusters() { return clusters; } public void setClusters(List<Cluster<T>> clusters) { this.clusters = clusters; } public double getMinMovement() { return minMovement; } public void setMinMovement(double minMovement) { this.minMovement = minMovement; } public IRudderList<T> getSourceData() { return sourceData; } public void setSourceData(IRudderList<T> sourceData) { this.sourceData = sourceData; } }