View Javadoc
1   package net.bmahe.genetics4j.samples.clustering;
2   
3   import static net.bmahe.genetics4j.core.termination.Terminations.or;
4   
5   import java.io.IOException;
6   import java.util.ArrayList;
7   import java.util.List;
8   import java.util.Objects;
9   import java.util.Optional;
10  import java.util.Random;
11  
12  import org.apache.commons.cli.CommandLine;
13  import org.apache.commons.cli.CommandLineParser;
14  import org.apache.commons.cli.DefaultParser;
15  import org.apache.commons.cli.Options;
16  import org.apache.commons.cli.ParseException;
17  import org.apache.commons.lang3.Validate;
18  import org.apache.commons.lang3.time.DurationFormatUtils;
19  import org.apache.commons.math3.ml.clustering.CentroidCluster;
20  import org.apache.commons.math3.ml.clustering.KMeansPlusPlusClusterer;
21  import org.apache.logging.log4j.LogManager;
22  import org.apache.logging.log4j.Logger;
23  
24  import net.bmahe.genetics4j.core.EASystemFactory;
25  import net.bmahe.genetics4j.core.Fitness;
26  import net.bmahe.genetics4j.core.Genotype;
27  import net.bmahe.genetics4j.core.chromosomes.DoubleChromosome;
28  import net.bmahe.genetics4j.core.evolutionlisteners.EvolutionListeners;
29  import net.bmahe.genetics4j.core.spec.EAConfiguration;
30  import net.bmahe.genetics4j.core.spec.EAExecutionContexts;
31  import net.bmahe.genetics4j.core.spec.chromosome.DoubleChromosomeSpec;
32  import net.bmahe.genetics4j.core.spec.combination.MultiCombinations;
33  import net.bmahe.genetics4j.core.spec.combination.MultiPointArithmetic;
34  import net.bmahe.genetics4j.core.spec.combination.MultiPointCrossover;
35  import net.bmahe.genetics4j.core.spec.mutation.CreepMutation;
36  import net.bmahe.genetics4j.core.spec.mutation.MultiMutations;
37  import net.bmahe.genetics4j.core.spec.mutation.RandomMutation;
38  import net.bmahe.genetics4j.core.spec.selection.Tournament;
39  import net.bmahe.genetics4j.core.termination.Termination;
40  import net.bmahe.genetics4j.core.termination.Terminations;
41  import net.bmahe.genetics4j.extras.evolutionlisteners.CSVEvolutionListener;
42  import net.bmahe.genetics4j.extras.evolutionlisteners.ColumnExtractor;
43  import net.bmahe.genetics4j.samples.CLIUtils;
44  
45  public class Clustering {
46  	public static final Logger logger = LogManager.getLogger(Clustering.class);
47  
48  	public static final int DEFAULT_NUM_CLUSTERS = 6;
49  	public static final int DEFAULT_NUMBER_TOURNAMENTS = 2;
50  	public static final int DEFAULT_POPULATION_SIZE = 120;
51  	public static final double DEFAULT_RANDOM_MUTATION_RATE = 0.15D;
52  	public static final double DEFAULT_CREEP_MUTATION_RATE = 0.20D;
53  	public static final double DEFAULT_CREEP_MUTATION_MEAN = 0.0D;
54  	public static final double DEFAULT_CREEP_MUTATION_STDDEV = 5;
55  	public static final int DEFAULT_COMBINATION_ARITHMETIC = 3;
56  	public static final int DEFAULT_COMBINATION_CROSSOVER_ARITHMETIC = 3;
57  
58  	public static final String PARAM_NUM_CLUSTERS = "n";
59  	public static final String LONG_PARAM_NUM_CLUSTERS = "num-clusters";
60  
61  	public static final String PARAM_NUMBER_TOURNAMENTS = "t";
62  	public static final String LONG_PARAM_NUMBER_TOURNAMENTS = "num-tournaments";
63  
64  	public static final String PARAM_POPULATION_SIZE = "p";
65  	public static final String LONG_PARAM_POPULATION_SIZE = "population-size";
66  
67  	public static final String PARAM_SOURCE_CLUSTERS_CSV = "c";
68  	public static final String LONG_PARAM_SOURCE_CUSTERS_CSV = "source-clusters";
69  
70  	public static final String PARAM_SOURCE_DATA_CSV = "s";
71  	public static final String LONG_PARAM_SOURCE_DATA_CSV = "source-data";
72  
73  	public static final String PARAM_FIXED_TERMINATION = "f";
74  	public static final String LONG_PARAM_FIXED_TERMINATION = "fixed-termination";
75  
76  	public static final String PARAM_RANDOM_MUTATION_RATE = "r";
77  	public static final String LONG_PARAM_RANDOM_MUTATION_RATE = "random-mutation-rate";
78  
79  	public static final String PARAM_CREEP_MUTATION_RATE = "m";
80  	public static final String LONG_PARAM_CREEP_MUTATION_RATE = "creep-mutation-rate";
81  
82  	public static final String PARAM_CREEP_MUTATION_MEAN = "a";
83  	public static final String LONG_PARAM_CREEP_MUTATION_MEAN = "creep-mutation-mean";
84  
85  	public static final String PARAM_CREEP_MUTATION_STD_DEV = "d";
86  	public static final String LONG_PARAM_CREEP_MUTATION_STD_DEV = "creep-mutation-std-dev";
87  
88  	public static final String PARAM_COMBINATION_ARITHMETIC = "b";
89  	public static final String LONG_PARAM_COMBINATION_ARITHMETIC = "combination-arithmetic";
90  
91  	public static final String PARAM_COMBINATION_CROSSOVER = "e";
92  	public static final String LONG_PARAM_COMBINATION_CROSSOVER = "combination-crossover";
93  
94  	public static final String PARAM_OUTPUT_CSV = "o";
95  	public static final String LONG_PARAM_OUTPUT_CSV = "output";
96  
97  	public static final String PARAM_OUTPUT_WITH_SSE_CSV = "g";
98  	public static final String LONG_PARAM_OUTPUT_WITH_SSE_CSV = "output-sse";
99  
100 	public static final String PARAM_BASE_DIR_OUTPUT = "i";
101 	public static final String LONG_PARAM_BASE_DIR_OUTPUT = "base-dir";
102 
103 	public static void cliError(final Options options, final String errorMessage) {
104 		CLIUtils.cliHelpAndExit(logger, Clustering.class, options, errorMessage);
105 	}
106 
107 	private static double computeDistance(final double[][] array, final int i, final int j) {
108 		final double xDiff = array[j][0] - array[i][0];
109 		final double yDiff = array[j][1] - array[i][1];
110 		return Math.sqrt((xDiff * xDiff) + (yDiff * yDiff));
111 	}
112 
113 	private static double[][] computeAllDistances(final double[][] array) {
114 
115 		final double[][] distances = new double[array.length][array.length];
116 
117 		for (int i = 0; i < array.length; i++) {
118 			distances[i][i] = 0.0;
119 		}
120 
121 		for (int i = 0; i < array.length; i++) {
122 			for (int j = 0; j < i; j++) {
123 				final double distance = computeDistance(array, i, j);
124 				distances[i][j] = distance;
125 				distances[j][i] = distance;
126 			}
127 		}
128 
129 		return distances;
130 	}
131 
132 	// tag::cluster_generation[]
133 	public static double[][] generateClusters(final Random random, final int numClusters, final double minX,
134 			final double maxX, final double minY, final double maxY) {
135 		Objects.requireNonNull(random);
136 		Validate.isTrue(numClusters > 0);
137 		Validate.isTrue(minX <= maxX);
138 		Validate.isTrue(minY <= maxY);
139 
140 		logger.info("Generating {} clusters", numClusters);
141 
142 		final double[][] clusters = new double[numClusters][2];
143 		for (int i = 0; i < numClusters; i++) {
144 			clusters[i][0] = minX + random.nextDouble() * (maxX - minX);
145 			clusters[i][1] = minY + random.nextDouble() * (maxY - minY);
146 		}
147 
148 		return clusters;
149 	}
150 	// end::cluster_generation[]
151 
152 	// tag::data_generation[]
153 	public static double[][] generateDataPoints(final Random random, final double[][] clusters, final int numDataPoints,
154 			final double radius) {
155 		Objects.requireNonNull(random);
156 		Objects.requireNonNull(clusters);
157 		Validate.isTrue(clusters.length > 0);
158 
159 		final int numClusters = clusters.length;
160 		final double[][] data = new double[numDataPoints][3];
161 		for (int i = 0; i < numDataPoints; i++) {
162 			final int clusterIndex = i % numClusters;
163 
164 			data[i][0] = random.nextGaussian() * radius + clusters[clusterIndex][0];
165 			data[i][1] = random.nextGaussian() * radius + clusters[clusterIndex][1];
166 			data[i][2] = clusterIndex;
167 		}
168 
169 		return data;
170 	}
171 	// end::data_generation[]
172 
173 	public static void doGA(final int k, final double min, final double max, final int numberTournaments,
174 			final int combinationArithmetic, final int combinationCrossover, final double randomMutationRate,
175 			final double creepMutationRate, final double creepMutationMean, final double creepMutationStdDev,
176 			final Fitness<Double> fitnessFunction, final Termination<Double> terminations, final int populationSize,
177 			final String outputCSV, final double[][] data, final double[][] distances, final String baseDir,
178 			final String filenameSuffix) throws IOException {
179 
180 		// tag::ea_configuration[]
181 		final var eaConfigurationBuilder = new EAConfiguration.Builder<Double>();
182 		eaConfigurationBuilder.chromosomeSpecs(DoubleChromosomeSpec.of(k * 2, min, max))
183 				.parentSelectionPolicy(Tournament.of(numberTournaments))
184 				.combinationPolicy(
185 						MultiCombinations.of(
186 								MultiPointArithmetic.of(combinationArithmetic, 0.5),
187 								MultiPointCrossover.of(combinationCrossover)))
188 				.mutationPolicies(
189 						MultiMutations.of(
190 								RandomMutation.of(randomMutationRate),
191 								CreepMutation.ofNormal(creepMutationRate, creepMutationMean, creepMutationStdDev)))
192 				.fitness(fitnessFunction)
193 				.postEvaluationProcessor(FitnessSharingUtils.clusterDistance)
194 				.termination(terminations);
195 		final var eaConfiguration = eaConfigurationBuilder.build();
196 		// end::ea_configuration[]
197 
198 		final var eaExecutionContext = EAExecutionContexts.<Double>forScalarFitness()
199 				.populationSize(populationSize)
200 				.addEvolutionListeners(
201 						EvolutionListeners.ofLogTopN(logger, 3),
202 						new CSVEvolutionListener.Builder<Double, Double>().filename(outputCSV)
203 								.columnExtractors(
204 										List.of(
205 												ColumnExtractor.of("generation", evolutionStep -> evolutionStep.generation()),
206 												ColumnExtractor.of("fitness", evolutionStep -> evolutionStep.fitness()),
207 												ColumnExtractor
208 														.of("combination_arithmetic", evolutionStep -> combinationArithmetic),
209 												ColumnExtractor.of("combination_crossover", evolutionStep -> combinationCrossover),
210 												ColumnExtractor.of("random_mutation_rate", evolutionStep -> randomMutationRate),
211 												ColumnExtractor.of("creep_mutation_mean", evolutionStep -> creepMutationMean),
212 												ColumnExtractor.of("creep_mutation_stddev", evolutionStep -> creepMutationStdDev),
213 												ColumnExtractor.of("creep_mutation_rate", evolutionStep -> creepMutationRate)))
214 								.build())
215 				.build();
216 
217 		final var eaSystem = EASystemFactory.from(eaConfiguration, eaExecutionContext);
218 
219 		final var evolutionResult = eaSystem.evolve();
220 		logger.info("Best genotype: {}", evolutionResult.bestGenotype());
221 		logger.info("  with fitness: {}", evolutionResult.bestFitness());
222 		logger.info("  at generation: {}", evolutionResult.generation());
223 
224 		final Genotype bestGenotype = evolutionResult.bestGenotype();
225 		final double[][] bestPhenotype = PhenotypeUtils.toPhenotype(bestGenotype);
226 		logger.info("Best phenotype:");
227 		for (int i = 0; i < k; i++) {
228 			logger.info("\tx: {} - y: {}", bestPhenotype[i][0], bestPhenotype[i][1]);
229 		}
230 		final int[] bestClusterMembership = FitnessUtils.assignDataToClusters(data, distances, bestPhenotype);
231 		IOUtils
232 				.persistDataPoints(data, bestClusterMembership, baseDir + "clustering-result-ga" + filenameSuffix + ".csv");
233 		IOUtils.persistClusters(bestPhenotype, baseDir + "clustering-result-clusters-ga" + filenameSuffix + ".csv");
234 
235 	}
236 
237 	public static List<CentroidCluster<LocationWrapper>> apacheCommonsMathCluster(final double[][] clusters,
238 			final double[][] data) {
239 
240 		logger.info("Initializing kmeans from Apache Commons Math");
241 
242 		final long startTs = System.currentTimeMillis();
243 
244 		final int numClusters = clusters.length;
245 		final int numDataPoints = data.length;
246 
247 		final List<LocationWrapper> clusterInput = new ArrayList<>(numDataPoints);
248 		for (int i = 0; i < numDataPoints; i++) {
249 			clusterInput.add(new LocationWrapper(data[i]));
250 		}
251 
252 		logger.info("Running kmeans");
253 
254 		final KMeansPlusPlusClusterer<LocationWrapper> clusterer = new KMeansPlusPlusClusterer<>(numClusters, 10_000);
255 		final List<CentroidCluster<LocationWrapper>> clusterResults = clusterer.cluster(clusterInput);
256 
257 		final long durationMs = System.currentTimeMillis() - startTs;
258 		logger.info("Computation time: {}", DurationFormatUtils.formatDurationHMS(durationMs));
259 
260 		return clusterResults;
261 	}
262 
263 	public static void main(String[] args) throws IOException {
264 		logger.info("Starting");
265 
266 		final Random random = new Random();
267 
268 		final double min = -100;
269 		final double max = 100;
270 		final double minX = min;
271 		final double maxX = max;
272 		final double minY = min;
273 		final double maxY = max;
274 
275 		final double radius = 8;
276 
277 		final int numDataPoints = 1_000;
278 
279 		/**
280 		 * Parse CLI
281 		 */
282 
283 		final CommandLineParser parser = new DefaultParser();
284 
285 		final Options options = new Options();
286 		options.addOption(PARAM_NUM_CLUSTERS, LONG_PARAM_NUM_CLUSTERS, true, "number of clusters");
287 		options.addOption(PARAM_NUMBER_TOURNAMENTS, LONG_PARAM_NUMBER_TOURNAMENTS, true, "number of tournaments");
288 		options.addOption(PARAM_SOURCE_CLUSTERS_CSV, LONG_PARAM_SOURCE_CUSTERS_CSV, true, "source csv file for clusters");
289 		options.addOption(PARAM_SOURCE_DATA_CSV, LONG_PARAM_SOURCE_DATA_CSV, true, "source csv file for data");
290 		options.addOption(PARAM_OUTPUT_CSV, LONG_PARAM_OUTPUT_CSV, true, "output csv");
291 		options.addOption(PARAM_OUTPUT_WITH_SSE_CSV, LONG_PARAM_OUTPUT_WITH_SSE_CSV, true, "output with sse csv");
292 		options
293 				.addOption(PARAM_COMBINATION_ARITHMETIC, LONG_PARAM_COMBINATION_ARITHMETIC, true, "combination arithmetic");
294 		options.addOption(PARAM_COMBINATION_CROSSOVER, LONG_PARAM_COMBINATION_CROSSOVER, true, "combination crossover");
295 		options.addOption(PARAM_POPULATION_SIZE, LONG_PARAM_POPULATION_SIZE, true, "population size");
296 		options.addOption(PARAM_BASE_DIR_OUTPUT, LONG_PARAM_BASE_DIR_OUTPUT, true, "base directory");
297 		options.addOption(
298 				PARAM_CREEP_MUTATION_STD_DEV,
299 				LONG_PARAM_CREEP_MUTATION_STD_DEV,
300 				true,
301 				"creep mutation std dev. Default: " + DEFAULT_CREEP_MUTATION_STDDEV);
302 		options.addOption(
303 				PARAM_CREEP_MUTATION_MEAN,
304 				LONG_PARAM_CREEP_MUTATION_MEAN,
305 				true,
306 				"creep mutation mean. Default: " + DEFAULT_CREEP_MUTATION_MEAN);
307 		options.addOption(
308 				PARAM_CREEP_MUTATION_RATE,
309 				LONG_PARAM_CREEP_MUTATION_RATE,
310 				true,
311 				"creep mutation rate. Default: " + DEFAULT_CREEP_MUTATION_RATE);
312 		options.addOption(
313 				PARAM_RANDOM_MUTATION_RATE,
314 				LONG_PARAM_RANDOM_MUTATION_RATE,
315 				true,
316 				"random mutation rate. Default: " + DEFAULT_RANDOM_MUTATION_RATE);
317 		options.addOption(
318 				PARAM_FIXED_TERMINATION,
319 				LONG_PARAM_FIXED_TERMINATION,
320 				true,
321 				"Fix the termination to the specified number of generations");
322 
323 		Optional<String> paramSourceClustersCSV = Optional.empty();
324 		Optional<String> paramSourceDataCSV = Optional.empty();
325 		Optional<Integer> paramNumClusters = Optional.empty();
326 		Optional<String> paramOutputCSV = Optional.empty();
327 		Optional<String> paramOutputWithSSECSV = Optional.empty();
328 		Optional<Long> paramFixedTermination = Optional.empty();
329 
330 		int numberTournaments = DEFAULT_NUMBER_TOURNAMENTS;
331 		int populationSize = DEFAULT_POPULATION_SIZE;
332 		final double randomMutationRate;
333 		final double creepMutationRate;
334 		final double creepMutationMean;
335 		final double creepMutationStdDev;
336 		final int combinationArithmetic;
337 		final int combinationCrossover;
338 		final String baseDir;
339 		try {
340 			final CommandLine line = parser.parse(options, args);
341 
342 			if (line.hasOption(PARAM_NUMBER_TOURNAMENTS)) {
343 				numberTournaments = Integer.parseInt(line.getOptionValue(PARAM_NUMBER_TOURNAMENTS).strip());
344 			}
345 
346 			baseDir = Optional.ofNullable(line.getOptionValue(PARAM_BASE_DIR_OUTPUT)).orElse("");
347 
348 			combinationArithmetic = Optional.ofNullable(line.getOptionValue(PARAM_COMBINATION_ARITHMETIC))
349 					.map(String::strip)
350 					.map(Integer::parseInt)
351 					.orElse(DEFAULT_COMBINATION_ARITHMETIC);
352 
353 			combinationCrossover = Optional.ofNullable(line.getOptionValue(PARAM_COMBINATION_CROSSOVER))
354 					.map(String::strip)
355 					.map(Integer::parseInt)
356 					.orElse(DEFAULT_COMBINATION_CROSSOVER_ARITHMETIC);
357 
358 			paramNumClusters = Optional.ofNullable(line.getOptionValue(PARAM_NUM_CLUSTERS))
359 					.map(String::strip)
360 					.map(Integer::parseInt);
361 			populationSize = Optional.ofNullable(line.getOptionValue(PARAM_POPULATION_SIZE))
362 					.map(String::strip)
363 					.map(Integer::parseInt)
364 					.orElse(DEFAULT_POPULATION_SIZE);
365 
366 			paramSourceClustersCSV = Optional.ofNullable(line.getOptionValue(PARAM_SOURCE_CLUSTERS_CSV))
367 					.map(String::strip);
368 
369 			paramSourceDataCSV = Optional.ofNullable(line.getOptionValue(PARAM_SOURCE_DATA_CSV)).map(String::strip);
370 
371 			paramOutputCSV = Optional.ofNullable(line.getOptionValue(PARAM_OUTPUT_CSV)).map(String::strip);
372 			paramOutputWithSSECSV = Optional.ofNullable(line.getOptionValue(PARAM_OUTPUT_WITH_SSE_CSV)).map(String::strip);
373 
374 			paramFixedTermination = Optional.ofNullable(line.getOptionValue(PARAM_FIXED_TERMINATION))
375 					.map(String::strip)
376 					.map(Long::parseLong);
377 
378 			randomMutationRate = Optional.ofNullable(line.getOptionValue(PARAM_RANDOM_MUTATION_RATE))
379 					.map(String::strip)
380 					.map(Double::parseDouble)
381 					.orElse(DEFAULT_RANDOM_MUTATION_RATE);
382 
383 			creepMutationRate = Optional.ofNullable(line.getOptionValue(PARAM_CREEP_MUTATION_RATE))
384 					.map(String::strip)
385 					.map(Double::parseDouble)
386 					.orElse(DEFAULT_CREEP_MUTATION_RATE);
387 
388 			creepMutationMean = Optional.ofNullable(line.getOptionValue(PARAM_CREEP_MUTATION_MEAN))
389 					.map(String::strip)
390 					.map(Double::parseDouble)
391 					.orElse(DEFAULT_CREEP_MUTATION_MEAN);
392 
393 			creepMutationStdDev = Optional.ofNullable(line.getOptionValue(PARAM_CREEP_MUTATION_STD_DEV))
394 					.map(String::strip)
395 					.map(Double::parseDouble)
396 					.orElse(DEFAULT_CREEP_MUTATION_STDDEV);
397 
398 			logger.info("Unrecognized args:");
399 			boolean hasError = false;
400 			for (final String extraArg : line.getArgList()) {
401 				logger.info("\t[{}]", extraArg);
402 				if (!extraArg.isBlank()) {
403 					hasError = true;
404 				}
405 			}
406 
407 			if (hasError) {
408 				throw new RuntimeException();
409 			}
410 		} catch (ParseException exp) {
411 			cliError(options, "Unexpected exception:" + exp.getMessage());
412 
413 			// This piece will never execute
414 			throw new RuntimeException(); // java doesn't detect the System.exit in cliError and create some issues with
415 													// potential not initialized final parameters.
416 		}
417 
418 		final int numClusters = paramNumClusters.orElse(DEFAULT_NUM_CLUSTERS);
419 
420 		logger.info("Preparing {} clusters", numClusters);
421 		final double[][] clusters = paramSourceClustersCSV.map(IOUtils::loadClusters)
422 				.orElseGet(() -> generateClusters(random, numClusters, minX, maxX, minY, maxY));
423 		logger.info("Found {} clusters", clusters.length);
424 
425 		logger.info("Preparing data points");
426 		final double[][] data = paramSourceDataCSV.map(sourceDataCSVFileName -> {
427 			try {
428 				logger.info("Loading data points");
429 				return IOUtils.loadDataPoints(sourceDataCSVFileName);
430 			} catch (IOException e) {
431 				throw new RuntimeException("Could not load " + sourceDataCSVFileName, e);
432 			}
433 		}).orElseGet(() -> generateDataPoints(random, clusters, numDataPoints, radius));
434 		final double[][] distances = computeAllDistances(data);
435 
436 		final String originalClustersFilename = "originalClusters.csv";
437 		if (paramSourceClustersCSV.isPresent()) {
438 			logger.info("Not persisting clusters since it was provided");
439 		} else {
440 			logger.info("Saving clusters to CSV: {}", originalClustersFilename);
441 			IOUtils.persistClusters(clusters, originalClustersFilename);
442 		}
443 
444 		final String originalDataFilename = "originalData.csv";
445 		if (paramSourceDataCSV.isPresent()) {
446 			logger.info("Not persisting data since it was provided");
447 		} else {
448 			logger.info("Saving data to CSV: {}", originalDataFilename);
449 			IOUtils.persistDataPoints(data, originalDataFilename);
450 		}
451 
452 		logger.info("Clustering data with Apache Commons Math");
453 
454 		final List<CentroidCluster<LocationWrapper>> clusterResults = apacheCommonsMathCluster(clusters, data);
455 
456 		logger.info("Definition of genetic problem");
457 
458 		final int k = numClusters;
459 		final var fitnessFunction = FitnessUtils.computeFitness(numDataPoints, data, distances, k);
460 
461 		final var terminations = paramFixedTermination.map(Terminations::<Double>ofMaxGeneration)
462 				.orElseGet(() -> or(Terminations.<Double>ofMaxGeneration(500), Terminations.<Double>ofStableFitness(50)));
463 
464 		logger.info("Terminations: {}", paramFixedTermination);
465 		logger.info(
466 				"Parameters: random_mutation_rate: {} - creep_mutation_rate: {} - creep_mutation_stddev: {} ",
467 				randomMutationRate,
468 				creepMutationRate,
469 				creepMutationStdDev);
470 		logger.info("Combinations: arithmetic {} ; crossover {}", combinationArithmetic, combinationCrossover);
471 
472 		logger.info("Running GA with Silhouette score");
473 		doGA(
474 				k,
475 				min,
476 				max,
477 				numberTournaments,
478 				combinationArithmetic,
479 				combinationCrossover,
480 				randomMutationRate,
481 				creepMutationRate,
482 				creepMutationMean,
483 				creepMutationStdDev,
484 				fitnessFunction,
485 				terminations,
486 				populationSize,
487 				paramOutputCSV.orElse("output.csv"),
488 				data,
489 				distances,
490 				baseDir,
491 				"");
492 
493 		logger.info("Running GA with Silhouette score + SSE");
494 		final var fitnessFunctionWithSumSquareErrors = FitnessUtils
495 				.computeFitnessWithSSE(numDataPoints, data, distances, k);
496 		doGA(
497 				k,
498 				min,
499 				max,
500 				numberTournaments,
501 				combinationArithmetic,
502 				combinationCrossover,
503 				randomMutationRate,
504 				creepMutationRate,
505 				creepMutationMean,
506 				creepMutationStdDev,
507 				fitnessFunctionWithSumSquareErrors,
508 				terminations,
509 				populationSize,
510 				paramOutputWithSSECSV.orElse("output-with-sse.csv"),
511 				data,
512 				distances,
513 				baseDir,
514 				"-with-sse");
515 
516 		logger.info("Original clusters:");
517 		final double[] originalMeans = new double[numClusters * 2];
518 		for (int i = 0; i < numClusters; i++) {
519 			logger.info("\tx: {} - y: {}", clusters[i][0], clusters[i][1]);
520 			originalMeans[i * 2] = clusters[i][0];
521 			originalMeans[i * 2 + 1] = clusters[i][1];
522 		}
523 		final var originalFitness = fitnessFunction
524 				.compute(new Genotype(new DoubleChromosome(k * 2, -100.0d, 100.0d, originalMeans)));
525 		logger.info("Original fitness: {}", originalFitness);
526 		final int[] originalClusterMembership = FitnessUtils.assignDataToClusters(data, distances, clusters);
527 		IOUtils.persistDataPoints(data, originalClusterMembership, baseDir + "clustering-result-original.csv");
528 		IOUtils.persistClusters(clusters, baseDir + "clustering-result-clusters-original.csv");
529 
530 		logger.info("kmeans output:");
531 		// output the clusters
532 		final double[] kmeansClusters = new double[numClusters * 2];
533 		for (int i = 0; i < clusterResults.size(); i++) {
534 			final CentroidCluster<LocationWrapper> centroidCluster = clusterResults.get(i);
535 			logger.info("\t{}", centroidCluster.getCenter());
536 			kmeansClusters[i * 2] = centroidCluster.getCenter().getPoint()[0];
537 			kmeansClusters[i * 2 + 1] = centroidCluster.getCenter().getPoint()[1];
538 		}
539 		final var kmeansGenotype = new Genotype(new DoubleChromosome(k * 2, -100.0d, 100.0d, kmeansClusters));
540 		final var kmeansFitness = fitnessFunction.compute(kmeansGenotype);
541 		logger.info("kmeans fitness: {}", kmeansFitness);
542 
543 		final int[] kmeansClusterMembership = FitnessUtils
544 				.assignDataToClusters(data, distances, PhenotypeUtils.toPhenotype(kmeansGenotype));
545 		IOUtils.persistDataPoints(data, kmeansClusterMembership, baseDir + "clustering-result-kmeans.csv");
546 		IOUtils.persistClusters(
547 				PhenotypeUtils.toPhenotype(kmeansGenotype),
548 				baseDir + "clustering-result-clusters-kmeans.csv");
549 
550 		logger.info("Done");
551 	}
552 }