package org.activityinfo.server.report.generator.map.cluster.genetic; /* * #%L * ActivityInfo Server * %% * Copyright (C) 2009 - 2013 UNICEF * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import com.google.common.collect.Lists; import org.activityinfo.server.report.generator.map.RadiiCalculator; import org.activityinfo.server.report.generator.map.cluster.Cluster; import java.util.*; public class GeneticSolver { private RadiiCalculator radiiCalculator; private FitnessFunctor fitnessFunctor; private List<List<MarkerGraph.Node>> subgraphs; private List<Integer> upperBounds; private List<Phenotype> population; private Random random; private ArrayList<Cluster> simpleClusters; private double solutionFitness; /** * Probability that a single chromosome will mutate over the course of a * generation */ private static final double PROB_MUTATE = 0.15; /** * The root value of the Cumulative Density Function (CDF) of the random * variable used to select phenotypes with probability proportionate to * fitness. * <p/> * Should be between 0 and 1: the lower the value, the greater the * preference for the most fit. The greater the value, the more likely that * unfit phenotypes will reproduce. */ private static final double SELECTION_CDF_ROOT = 0.1; public interface Tracer { void breeding(GeneticSolver solver, int i, int j); void evolved(GeneticSolver solver, int generation, int stagnationCount); void crossover(GeneticSolver solver, int[] p1, int[] p2, int xoverPoint, int[] c1, int[] c2); } private Tracer tracer; public class Phenotype { private int[] chromosomes; private List<Cluster> clusters; private double fitness; public Phenotype(int[] chromosomes) { this.chromosomes = Arrays.copyOf(chromosomes, chromosomes.length); this.clusters = new ArrayList<Cluster>(); for (int i = 0; i != chromosomes.length; ++i) { this.clusters.addAll(KMeans.cluster(subgraphs.get(i), chromosomes[i])); } this.clusters.addAll(simpleClusters); radiiCalculator.calculate(this.clusters); fitness = fitnessFunctor.score(clusters); } public double getFitness() { return fitness; } public List<Cluster> getClusters() { return clusters; } public int[] getChromosomes() { return chromosomes; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('['); for (int i = 0; i != chromosomes.length; ++i) { sb.append(' ').append(String.format("%3d", chromosomes[i])); } sb.append(" ]; fitness=").append(String.format("%+10.0f", fitness)); return sb.toString(); } } public List<Cluster> solve(MarkerGraph graph, RadiiCalculator radiiCalculator, FitnessFunctor fitnessFunctor, List<Integer> allUpperBounds) { this.radiiCalculator = radiiCalculator; this.fitnessFunctor = fitnessFunctor; this.random = new Random(); // for subgraphs that contain only one node, create a cluster // for them right away and keep them out of the problem. // this will make crossover / mutations more effective List<List<MarkerGraph.Node>> allSubGraphs = graph.getSubgraphs(); int popSize = 1; subgraphs = Lists.newArrayList(); simpleClusters = Lists.newArrayList(); upperBounds = Lists.newArrayList(); for (int i = 0; i != allSubGraphs.size(); i++) { List<MarkerGraph.Node> subGraph = allSubGraphs.get(i); if (subGraph.size() == 1) { simpleClusters.add(new Cluster(subGraph.get(0).getPointValue())); } else if (allUpperBounds.get(i) == 1) { simpleClusters.addAll(KMeans.cluster(subGraph, 1)); } else { upperBounds.add(allUpperBounds.get(i)); subgraphs.add(subGraph); popSize += 1 + (int) Math.log(allUpperBounds.get(i) * 32); } } if (popSize > 50) { popSize = 50; } this.population = new ArrayList<Phenotype>(popSize); addRandomPhenotypes(this.population, popSize); orderPopulation(); double lastFitnessScore = 0; int generationsStagnated = 0; if (subgraphs.size() > 0) { for (int generation = 0; generation < 100; ++generation) { double fitness = evolve(generationsStagnated); if (tracer != null) { tracer.evolved(this, generation, generationsStagnated); } if (fitness != lastFitnessScore) { lastFitnessScore = fitness; generationsStagnated = 0; } else { generationsStagnated++; } if (generationsStagnated == 8) { break; } } } solutionFitness = getFittest().getFitness(); return getFittest().getClusters(); } private void addRandomPhenotypes(List<Phenotype> population, int count) { for (int i = 0; i != count; ++i) { int[] chromosomes = new int[subgraphs.size()]; for (int j = 0; j != chromosomes.length; ++j) { chromosomes[j] = randomChromosome(j); } population.add(new Phenotype(chromosomes)); } } private int randomChromosome(int i) { return 1 + random.nextInt(upperBounds.get(i) - 1); } public double evolve(int generationsStagnated) { List<Phenotype> nextgen = new ArrayList<Phenotype>(population.size()); // preserve the best solution from the current generation // to avoid regressing backwords... nextgen.add(population.get(0)); while (nextgen.size() < population.size()) { List<int[]> parents = selectParents(); int[] child1 = Arrays.copyOf(parents.get(0), parents.get(0).length); int[] child2 = Arrays.copyOf(parents.get(1), parents.get(1).length); crossover(child1, child2); double pMutate = Math.pow(PROB_MUTATE, 1 + (generationsStagnated / 2.0)); mutate(child1, pMutate); mutate(child2, pMutate); nextgen.add(new Phenotype(child1)); nextgen.add(new Phenotype(child2)); } population = nextgen; orderPopulation(); return population.get(0).getFitness(); } private void orderPopulation() { Collections.sort(population, new Comparator<Phenotype>() { @Override public int compare(Phenotype o1, Phenotype o2) { return -Double.compare(o1.getFitness(), o2.getFitness()); } }); } public void setTracer(Tracer tracer) { this.tracer = tracer; } /** * Performs a "crossover" operation on two phenotypes. Supposed to be * analogous to what happens in biological reproduction. * <p/> * See http://en.wikipedia.org/wiki/Crossover_%28genetic_algorithm%29 * * @param p1 First phenotype (array of chromosomes) * @param p2 Second phenotype (array of chromosomes) * @return */ public List<int[]> crossover(int[] p1, int[] p2) { List<int[]> children = new ArrayList<int[]>(2); children.add(Arrays.copyOf(p1, p1.length)); children.add(Arrays.copyOf(p2, p2.length)); int xoverPoint = random.nextInt(subgraphs.size()); swap(children.get(0), children.get(1), xoverPoint); if (tracer != null) { tracer.crossover(this, p1, p2, xoverPoint, children.get(0), children.get(1)); } return children; } private void swap(int[] a, int[] b, int xoverPoint) { int[] tmp = Arrays.copyOfRange(a, 0, xoverPoint); System.arraycopy(b, 0, a, 0, xoverPoint); System.arraycopy(tmp, 0, b, 0, xoverPoint); } /** * @param chromosomes * @param pMutate Probability that an individual chromosome will mutate */ private void mutate(int[] chromosomes, double pMutate) { for (int i = 0; i != chromosomes.length; ++i) { if (random.nextDouble() < pMutate) { chromosomes[i] = randomChromosome(i); } } } /** * @return The index of a random phenotype, selected with probabality * proportionate to its fitness rank */ public int randomPhenotype() { return (int) Math.round((1 - Math.pow(random.nextDouble(), SELECTION_CDF_ROOT)) * (population.size() - 1)); } /** * @return A random pair of <code>Phenotype</code>s, selected with * probability proportionate to fitness rank. */ public List<int[]> selectParents() { int i, j; do { i = randomPhenotype(); j = randomPhenotype(); } while (i == j); if (tracer != null) { tracer.breeding(this, i, j); } List<int[]> parents = new ArrayList<int[]>(2); parents.add(population.get(i).getChromosomes()); parents.add(population.get(j).getChromosomes()); return parents; } public List<Phenotype> getPopulation() { return population; } public Phenotype getFittest() { return population.get(0); } public double getSolutionFitness() { return solutionFitness; } }