[Home] DMelt member access . Java source code: com.datumbox.framework.core.machinelearning.clustering.Kmeans
/**
* Copyright (C) 2013-2017 Vasilis Vryniotis
*
* 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 com.datumbox.framework.core.machinelearning.clustering;
import com.datumbox.framework.common.Configuration;
import com.datumbox.framework.common.concurrency.ForkJoinStream;
import com.datumbox.framework.common.concurrency.StreamMethods;
import com.datumbox.framework.common.dataobjects.AssociativeArray;
import com.datumbox.framework.core.common.dataobjects.Dataframe;
import com.datumbox.framework.core.common.dataobjects.Record;
import com.datumbox.framework.common.dataobjects.TypeInference;
import com.datumbox.framework.common.storage.interfaces.BigMap;
import com.datumbox.framework.common.storage.interfaces.StorageEngine;
import com.datumbox.framework.common.storage.interfaces.StorageEngine.MapType;
import com.datumbox.framework.common.storage.interfaces.StorageEngine.StorageHint;
import com.datumbox.framework.core.common.utilities.MapMethods;
import com.datumbox.framework.core.common.utilities.PHPMethods;
import com.datumbox.framework.core.machinelearning.common.abstracts.AbstractTrainer;
import com.datumbox.framework.core.machinelearning.common.abstracts.modelers.AbstractClusterer;
import com.datumbox.framework.core.machinelearning.common.interfaces.PredictParallelizable;
import com.datumbox.framework.core.machinelearning.common.interfaces.TrainParallelizable;
import com.datumbox.framework.core.mathematics.distances.Distance;
import com.datumbox.framework.core.statistics.descriptivestatistics.Descriptives;
import com.datumbox.framework.core.statistics.sampling.SimpleRandomSampling;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* This class implements the K-means clustering algorithm supporting different
* Initialization and distance methods. Several different expansions on the standard
* algorithm are used to ensure we can handle mixed data (k-representative, k-prototype,
* etc).
*
* References:
* http://cs.gsu.edu/~wkim/index_files/papers/kprototype.pdf
* http://ilpubs.stanford.edu:8090/778/1/2006-13.pdf
* http://www.ima.umn.edu/~iwen/REU/BATS-Means.pdf
* http://web.cs.swarthmore.edu/~turnbull/Papers/Turnbull_GenreRBF_KDE05.pdf
* http://thesis.neminis.org/wp-content/plugins/downloads-manager/upload/masterThesis-VR.pdf
*
* @author Vasilis Vryniotis
*/
public class Kmeans extends AbstractClusterer implements PredictParallelizable, TrainParallelizable {
/** {@inheritDoc} */
public static class Cluster extends AbstractClusterer.AbstractCluster {
private static final long serialVersionUID = 1L;
private Record centroid;
private final AssociativeArray xi_sum;
/**
* @param clusterId
* @see AbstractClusterer.AbstractCluster#AbstractCluster(java.lang.Integer)
*/
protected Cluster(int clusterId) {
super(clusterId);
centroid = new Record(new AssociativeArray(), null);
xi_sum = new AssociativeArray();
}
/**
* Returns the centroid of the cluster.
*
* @return
*/
public Record getCentroid() {
return centroid;
}
/**
* Updates the cluster parameters by estimating again the centroid.
*
* @return
*/
protected boolean updateClusterParameters() {
boolean changed=false;
AssociativeArray centoidValues = xi_sum.copy();
if(size>0) {
centoidValues.multiplyValues(1.0/size);
}
if(!centroid.getX().equals(centoidValues)) {
changed=true;
centroid = new Record(centoidValues, centroid.getY());
}
return changed;
}
/** {@inheritDoc} */
@Override
protected void add(Record r) {
size++;
xi_sum.addValues(r.getX());
}
/** {@inheritDoc} */
@Override
protected void remove(Record r) {
throw new UnsupportedOperationException("Remove operation is not supported.");
}
/** {@inheritDoc} */
@Override
protected void clear() {
xi_sum.clear();
}
/**
* Resets the cluster while keeping the centroid the same. This is a
* method required by the implementation of Kmeans and it should not
* be linked to the clear() method.
*/
protected void reset() {
xi_sum.clear();
size = 0;
}
}
/** {@inheritDoc} */
public static class ModelParameters extends AbstractClusterer.AbstractModelParameters {
private static final long serialVersionUID = 1L;
private int totalIterations;
@BigMap(keyClass=Object.class, valueClass=Double.class, mapType=MapType.HASHMAP, storageHint=StorageHint.IN_MEMORY, concurrent=true)
private Map featureWeights;
/**
* @param storageEngine
* @see AbstractTrainer.AbstractModelParameters#AbstractModelParameters(StorageEngine)
*/
protected ModelParameters(StorageEngine storageEngine) {
super(storageEngine);
}
/**
* Getter for the total number of iterations used in training.
*
* @return
*/
public int getTotalIterations() {
return totalIterations;
}
/**
* Setter for the total number of iterations used in training.
*
* @param totalIterations
*/
protected void setTotalIterations(int totalIterations) {
this.totalIterations = totalIterations;
}
/**
* Getter for the estimated weights of each feature.
*
* @return
*/
public Map getFeatureWeights() {
return featureWeights;
}
/**
* Setter for the estimated weights of each feature.
*
* @param featureWeights
*/
protected void setFeatureWeights(Map featureWeights) {
this.featureWeights = featureWeights;
}
}
/** {@inheritDoc} */
public static class TrainingParameters extends AbstractClusterer.AbstractTrainingParameters {
private static final long serialVersionUID = 1L;
/**
* The Initialization method that we use.
*/
public enum Initialization {
/**
* Forgy.
*/
FORGY,
/**
* Random Partition.
*/
RANDOM_PARTITION,
/**
* Set First K points as Initial Centroid.
*/
SET_FIRST_K,
/**
* Furthest First.
* References:
* http://www.cs.utexas.edu/users/inderjit/elkankmeans.ppt
*/
FURTHEST_FIRST,
/**
* Subset Furthest First.
* References:
* http://www.cs.utexas.edu/users/inderjit/elkankmeans.ppt
*/
SUBSET_FURTHEST_FIRST,
/**
* Kmeans++.
* References:
* http://ilpubs.stanford.edu:8090/778/1/2006-13.pdf
* http://www.ima.umn.edu/~iwen/REU/BATS-Means.pdf
*/
PLUS_PLUS;
}
/**
* The Distance method used in the calculations.
*/
public enum Distance {
/**
* Euclidian distance.
*/
EUCLIDIAN,
/**
* Manhattan distance.
*/
MANHATTAN;
}
//Vars
private int k = 2;
private Initialization initializationMethod = Initialization.PLUS_PLUS;
private Distance distanceMethod = Distance.EUCLIDIAN;
private int maxIterations = 200;
private double subsetFurthestFirstcValue = 2;//c>1 This value is used for c*k*log k, Readmore: http://web.cs.swarthmore.edu/~turnbull/Papers/Turnbull_GenreRBF_KDE05.pdf
private double categoricalGamaMultiplier = 1.0; //used by Kprototype algorithm, multiplies the the categorical distance with this weight
private boolean weighted = false; //whether the weighted version of the algorithm will run. The weighted version estimates weights for every feature
//Getters Setters
/**
* Getter for the number of clusters k.
*
* @return
*/
public int getK() {
return k;
}
/**
* Setter for the number of clusters k.
*
* @param k
*/
public void setK(int k) {
this.k = k;
}
/**
* Getter for the initialization method that we use.
*
* @return
*/
public Initialization getInitializationMethod() {
return initializationMethod;
}
/**
* Setter for the initialization method that we use.
*
* @param initializationMethod
*/
public void setInitializationMethod(Initialization initializationMethod) {
this.initializationMethod = initializationMethod;
}
/**
* Getter for the distance method that we use.
*
* @return
*/
public Distance getDistanceMethod() {
return distanceMethod;
}
/**
* Setter for the distance method that we use.
*
* @param distanceMethod
*/
public void setDistanceMethod(Distance distanceMethod) {
this.distanceMethod = distanceMethod;
}
/**
* Getter for the maximum permitted iterations during training.
*
* @return
*/
public int getMaxIterations() {
return maxIterations;
}
/**
* Setter for the maximum permitted iterations during training.
*
* @param maxIterations
*/
public void setMaxIterations(int maxIterations) {
this.maxIterations = maxIterations;
}
/**
* Getter for the C value of the SubsetFurthestFirst initialization method.
*
* @return
*/
public double getSubsetFurthestFirstcValue() {
return subsetFurthestFirstcValue;
}
/**
* Setter for the C value of the SubsetFurthestFirst initialization method.
*
* @param subsetFurthestFirstcValue
*/
public void setSubsetFurthestFirstcValue(double subsetFurthestFirstcValue) {
this.subsetFurthestFirstcValue = subsetFurthestFirstcValue;
}
/**
* Getter for the Categorical Gama Multiplier.
*
* @return
*/
public double getCategoricalGamaMultiplier() {
return categoricalGamaMultiplier;
}
/**
* Setter for the Categorical Gama Multiplier.
*
* @param categoricalGamaMultiplier
*/
public void setCategoricalGamaMultiplier(double categoricalGamaMultiplier) {
this.categoricalGamaMultiplier = categoricalGamaMultiplier;
}
/**
* Getter for whether the algorithm should estimate the weights of the
* features dynamically.
*
* @return
*/
public boolean isWeighted() {
return weighted;
}
/**
* Setter for whether the algorithm should estimate the weights of the
* features dynamically.
*
* @param weighted
*/
public void setWeighted(boolean weighted) {
this.weighted = weighted;
}
}
/**
* @param trainingParameters
* @param configuration
* @see AbstractTrainer#AbstractTrainer(AbstractTrainingParameters, Configuration)
*/
protected Kmeans(TrainingParameters trainingParameters, Configuration configuration) {
super(trainingParameters, configuration);
streamExecutor = new ForkJoinStream(knowledgeBase.getConfiguration().getConcurrencyConfiguration());
}
/**
* @param storageName
* @param configuration
* @see AbstractTrainer#AbstractTrainer(String, Configuration)
*/
protected Kmeans(String storageName, Configuration configuration) {
super(storageName, configuration);
streamExecutor = new ForkJoinStream(knowledgeBase.getConfiguration().getConcurrencyConfiguration());
}
private boolean parallelized = true;
/**
* This executor is used for the parallel processing of streams with custom
* Thread pool.
*/
protected final ForkJoinStream streamExecutor;
/** {@inheritDoc} */
@Override
public boolean isParallelized() {
return parallelized;
}
/** {@inheritDoc} */
@Override
public void setParallelized(boolean parallelized) {
this.parallelized = parallelized;
}
/** {@inheritDoc} */
@Override
protected void _predict(Dataframe newData) {
_predictDatasetParallel(newData, knowledgeBase.getStorageEngine(), knowledgeBase.getConfiguration().getConcurrencyConfiguration());
}
/** {@inheritDoc} */
@Override
public Prediction _predictRecord(Record r) {
ModelParameters modelParameters = knowledgeBase.getModelParameters();
Map clusterMap = modelParameters.getClusterMap();
AssociativeArray clusterDistances = new AssociativeArray();
for(Map.Entry e : clusterMap.entrySet()) {
Integer clusterId = e.getKey();
Cluster c= e.getValue();
double distance = calculateDistance(r, c.getCentroid());
clusterDistances.put(clusterId, distance);
}
Descriptives.normalize(clusterDistances);
return new Prediction(getSelectedClusterFromDistances(clusterDistances), clusterDistances);
}
/** {@inheritDoc} */
@Override
protected void _fit(Dataframe trainingData) {
ModelParameters modelParameters = knowledgeBase.getModelParameters();
Set goldStandardClasses = modelParameters.getGoldStandardClasses();
//check if there are any gold standard classes
for(Record r : trainingData) {
Object theClass=r.getY();
if(theClass!=null) {
goldStandardClasses.add(theClass);
}
}
//calculate the weights of the features
calculateFeatureWeights(trainingData);
//initialize clusters
initializeClusters(trainingData);
//calculate clusters
calculateClusters(trainingData);
clearClusters();
}
/**
* Estimate the weights of each feature.
*
* @param trainingData
*/
private void calculateFeatureWeights(Dataframe trainingData) {
ModelParameters modelParameters = knowledgeBase.getModelParameters();
TrainingParameters trainingParameters = knowledgeBase.getTrainingParameters();
Map columnTypes = trainingData.getXDataTypes();
Map featureWeights = modelParameters.getFeatureWeights();
if(trainingParameters.isWeighted()==false) {
//the unweighted version of the algorithm
double gammaWeight = trainingParameters.getCategoricalGamaMultiplier();
streamExecutor.forEach(StreamMethods.stream(columnTypes.entrySet().stream(), isParallelized()), e -> {
//standard kmeans has equal weights in all numeric features
//for categorical, dummy or ordinal feature, use the Gamma Multiplier of Kprototypes
double weight = (e.getValue()!=TypeInference.DataType.NUMERICAL)?gammaWeight:1.0;
featureWeights.put(e.getKey(), weight);
});
}
else {
//calculate weights for the features
int n = trainingData.size();
StorageEngine storageEngine = knowledgeBase.getStorageEngine();
Map tmp_categoricalFrequencies = storageEngine.getBigMap("tmp_categoricalFrequencies", Object.class, Double.class, MapType.HASHMAP, StorageHint.IN_MEMORY, true, true);
Map tmp_varianceSumX = storageEngine.getBigMap("tmp_varianceSumX", Object.class, Double.class, MapType.HASHMAP, StorageHint.IN_MEMORY, true, true);
Map tmp_varianceSumXsquare = storageEngine.getBigMap("tmp_varianceSumXsquare", Object.class, Double.class, MapType.HASHMAP, StorageHint.IN_MEMORY, true, true);
//calculate variance and frequencies
for(Record r : trainingData) {
for(Map.Entry e : r.getX().entrySet()) {
Double value = TypeInference.toDouble(e.getValue());
if (value!=null && value!=0.0) {
Object feature = e.getKey();
if(columnTypes.get(feature)!=TypeInference.DataType.NUMERICAL) {
Double previousValue = tmp_categoricalFrequencies.getOrDefault(feature, 0.0);
tmp_categoricalFrequencies.put(feature, previousValue+1.0);
}
else {
Double previousValueSumX = tmp_varianceSumX.getOrDefault(feature, 0.0);
Double previousValueSumXsquare = tmp_varianceSumXsquare.getOrDefault(feature, 0.0);
tmp_varianceSumX.put(feature, previousValueSumX+value);
tmp_varianceSumXsquare.put(feature, previousValueSumXsquare+value*value);
}
}
}
}
double gammaWeight = trainingParameters.getCategoricalGamaMultiplier();
//estimate weights
streamExecutor.forEach(StreamMethods.stream(columnTypes.entrySet().stream(), isParallelized()), e -> {
Object feature = e.getKey();
TypeInference.DataType type = e.getValue();
double weight;
if(type!=TypeInference.DataType.NUMERICAL) {
double percentage = tmp_categoricalFrequencies.get(feature)/n;
weight = 1.0 -percentage*percentage;
}
else {
double mean = tmp_varianceSumX.get(feature)/n;
weight = 2.0*((tmp_varianceSumXsquare.get(feature)/n)-mean*mean);
}
if(weight>0) {
weight = 1/weight;
}
//apply muliplier if categorical
if(type!=TypeInference.DataType.NUMERICAL) {
weight *= gammaWeight;
}
featureWeights.put(feature, weight);
});
//Drop the temporary Collection
storageEngine.dropBigMap("tmp_categoricalFrequencies", tmp_categoricalFrequencies);
storageEngine.dropBigMap("tmp_varianceSumX", tmp_categoricalFrequencies);
storageEngine.dropBigMap("tmp_varianceSumXsquare", tmp_categoricalFrequencies);
}
}
private double calculateDistance(Record r1, Record r2) {
ModelParameters modelParameters = knowledgeBase.getModelParameters();
TrainingParameters trainingParameters = knowledgeBase.getTrainingParameters();
Map featureWeights = modelParameters.getFeatureWeights();
double distance;
TrainingParameters.Distance distanceMethod = trainingParameters.getDistanceMethod();
if(distanceMethod==TrainingParameters.Distance.EUCLIDIAN) {
distance = Distance.euclideanWeighted(r1.getX(), r2.getX(), featureWeights);
}
else if(distanceMethod==TrainingParameters.Distance.MANHATTAN) {
distance = Distance.manhattanWeighted(r1.getX(), r2.getX(), featureWeights);
}
else {
throw new IllegalArgumentException("Unsupported Distance method.");
}
return distance;
}
private Object getSelectedClusterFromDistances(AssociativeArray clusterDistances) {
Map.Entry minEntry = MapMethods.selectMinKeyValue(clusterDistances);
return minEntry.getKey();
}
private void initializeClusters(Dataframe trainingData) {
ModelParameters modelParameters = knowledgeBase.getModelParameters();
TrainingParameters trainingParameters = knowledgeBase.getTrainingParameters();
int k = trainingParameters.getK();
TrainingParameters.Initialization initializationMethod = trainingParameters.getInitializationMethod();
Map clusterMap = modelParameters.getClusterMap();
if(initializationMethod==TrainingParameters.Initialization.SET_FIRST_K ||
initializationMethod==TrainingParameters.Initialization.FORGY) {
int i = 0;
for(Record r : trainingData.values()) {
if(i>=k) {
break;
}
Integer clusterId= i;
Cluster c = new Cluster(clusterId);
c.add(r);
c.updateClusterParameters();
clusterMap.put(clusterId, c);
++i;
}
}
else if(initializationMethod==TrainingParameters.Initialization.RANDOM_PARTITION) {
int i = 0;
for(Record r : trainingData.values()) {
Integer clusterId = i%k;
Cluster c = clusterMap.get(clusterId);
if(c==null) {
c = new Cluster(clusterId);
}
c.add(r);
clusterMap.put(clusterId, c);
++i;
}
for(Map.Entry e : clusterMap.entrySet()) {
Integer clusterId = e.getKey();
Cluster c = e.getValue();
c.updateClusterParameters();
clusterMap.put(clusterId, c);
}
}
else if(initializationMethod==TrainingParameters.Initialization.FURTHEST_FIRST ||
initializationMethod==TrainingParameters.Initialization.SUBSET_FURTHEST_FIRST) {
int sampleSize = trainingData.size();
if(initializationMethod==TrainingParameters.Initialization.SUBSET_FURTHEST_FIRST) {
sampleSize = (int)Math.max(Math.ceil(trainingParameters.getSubsetFurthestFirstcValue()*k*PHPMethods.log(k, 2)), k);
}
Set alreadyAddedPoints = new HashSet(); //this is small. equal to k
for(int i = 0; i e : trainingData.entries()) {
Integer rId = e.getKey();
Record r = e.getValue();
if(samplePointCounter>sampleSize) {
break; //we only check a small part of the trainingData. This is equivalent to samplying only few observations from, the dataset
}
else if(alreadyAddedPoints.contains(rId)) {
continue; //ignore points that are already selected
}
double minClusterDistance = Double.MAX_VALUE;
for(Cluster c : clusterMap.values()) {
double distance = calculateDistance(r, c.getCentroid());
if(distancemaxMinDistance) {
maxMinDistance = minClusterDistance;
selectedRecordId = rId;
}
++samplePointCounter;
}
alreadyAddedPoints.add(selectedRecordId);
Integer clusterId = clusterMap.size();
Cluster c = new Cluster(clusterId);
c.add(trainingData.get(selectedRecordId));
c.updateClusterParameters();
clusterMap.put(clusterId, c);
}
//alreadyAddedPoints = null;
}
else if(initializationMethod==TrainingParameters.Initialization.PLUS_PLUS) {
StorageEngine storageEngine = knowledgeBase.getStorageEngine();
Set alreadyAddedPoints = new HashSet(); //this is small. equal to k
for(int i = 0; i < k; ++i) {
Map tmp_minClusterDistance = storageEngine.getBigMap("tmp_minClusterDistance", Object.class, Double.class, MapType.HASHMAP, StorageHint.IN_MEMORY, true, true);
AssociativeArray minClusterDistanceArray = new AssociativeArray((Map)tmp_minClusterDistance);
streamExecutor.forEach(StreamMethods.stream(trainingData.entries(), isParallelized()), e -> {
Integer rId = e.getKey();
Record r = e.getValue();
if(alreadyAddedPoints.contains(rId)==false) {
double minClusterDistance = 1.0;
if(clusterMap.size()>0) {
minClusterDistance = Double.MAX_VALUE;
for(Cluster c : clusterMap.values()) {
double distance = calculateDistance(r, c.getCentroid());
if(distance clusterMap = modelParameters.getClusterMap();
int maxIterations = trainingParameters.getMaxIterations();
modelParameters.setTotalIterations(maxIterations);
for(int iteration=0;iteration entry1 : clusterMap.entrySet()) {
Integer clusterId = entry1.getKey();
Cluster cluster = entry1.getValue();
cluster.reset();
clusterMap.put(clusterId, cluster);
}
//assign records to clusters
Map tmp_clusterAssignments = knowledgeBase.getStorageEngine().getBigMap("tmp_clusterAssignments", Integer.class, Integer.class, MapType.HASHMAP, StorageHint.IN_MEMORY, true, true);
streamExecutor.forEach(StreamMethods.stream(trainingData.entries(), isParallelized()), e -> {
Integer rId = e.getKey();
Record r = e.getValue();
//we are storing cluster references not clusterIds
AssociativeArray clusterDistances = new AssociativeArray();
for(Map.Entry entry : clusterMap.entrySet()) {
Integer cId = entry.getKey();
Cluster c = entry.getValue();
clusterDistances.put(cId, calculateDistance(r, c.getCentroid()));
}
//find the closest cluster
Integer selectedClusterId = (Integer)getSelectedClusterFromDistances(clusterDistances);
tmp_clusterAssignments.put(rId, selectedClusterId);
});
for(Map.Entry e : trainingData.entries()) {
Integer rId = e.getKey();
Record r = e.getValue();
//add the record in the cluster
Integer selectedClusterId = tmp_clusterAssignments.get(rId);
Cluster selectedCluster = clusterMap.get(selectedClusterId);
selectedCluster.add(r);
clusterMap.put(selectedClusterId, selectedCluster);
}
knowledgeBase.getStorageEngine().dropBigMap("tmp_clusterAssignments", tmp_clusterAssignments);
//update clusters
boolean changed=false;
for(Map.Entry e : clusterMap.entrySet()) {
Integer cId = e.getKey();
Cluster c = e.getValue();
changed|=c.updateClusterParameters();
clusterMap.put(cId, c);
}
//if none of the clusters changed then exit
if(changed==false) {
modelParameters.setTotalIterations(iteration);
break;
}
}
}
}
Ads help maintain this website.