/* Copyright 2009-2016 David Hadka * * This file is part of the MOEA Framework. * * The MOEA Framework is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * The MOEA Framework 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the MOEA Framework. If not, see <http://www.gnu.org/licenses/>. */ package org.moeaframework.algorithm; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.apache.commons.math3.util.KthSelector; import org.apache.commons.math3.util.Pair; import org.moeaframework.core.FitnessEvaluator; import org.moeaframework.core.Initialization; import org.moeaframework.core.Population; import org.moeaframework.core.Problem; import org.moeaframework.core.Selection; import org.moeaframework.core.Solution; import org.moeaframework.core.Variation; import org.moeaframework.core.comparator.DominanceComparator; import org.moeaframework.core.comparator.FitnessComparator; import org.moeaframework.core.comparator.ParetoDominanceComparator; import org.moeaframework.core.indicator.IndicatorUtils; import org.moeaframework.core.operator.TournamentSelection; /** * Implementation of the strength-based evolutionary algorithm (SPEA2). SPEA2 * uses a novel strength-based measure of fitness for handling multiple * objectives. * <p> * Note: First, there is a naming difference between this implementation and * the original SPEA2 paper. The original SPEA2 paper defines a "population" * and an "archive", but the population is really the offspring and the archive * is the population. Secondly, the SPEA2 paper defines a parameter * {@code k = sqrt(population.size())} for computing a crowding-based niching * factor. The SPEA2 C implementation in PISA (written by the same authors * as the paper) recommends using {@code k=1} for performance reasons. This * implementation makes {@code k} a user-specified parameter to support either * option. {@code k} should be at least {@code 1} and no larger than * {@code population.size()}. * <p> * References: * <ol> * <li>Zitzler, E., M. Laumanns, and L. Thiele (2001). SPEA2: Improving the * Strength Pareto Evolutionary Algorithm. TIK-Report 103. * </ol> */ public class SPEA2 extends AbstractEvolutionaryAlgorithm { /** * The selection operator. */ private final Selection selection; /** * The variation operator. */ private final Variation variation; /** * The number of offspring. */ private final int numberOfOffspring; /** * Strength-based fitness evaluator. */ protected final StrengthFitnessEvaluator fitnessEvaluator; /** * Compares solutions based on strength. */ protected final FitnessComparator fitnessComparator; /** * Constructs a new instance of SPEA2. * * @param problem the problem * @param initialization the initialization procedure * @param variation the variation operator * @param numberOfOffspring the number of offspring generated each iteration * @param k niching parameter specifying that crowding is computed using * the {@code k}-th nearest neighbor, recommend {@code k=1} */ public SPEA2(Problem problem, Initialization initialization, Variation variation, int numberOfOffspring, int k) { super(problem, new Population(), null, initialization); this.variation = variation; this.numberOfOffspring = numberOfOffspring; fitnessEvaluator = new StrengthFitnessEvaluator(k); fitnessComparator = new FitnessComparator(fitnessEvaluator.areLargerValuesPreferred()); selection = new TournamentSelection(fitnessComparator); } @Override protected void initialize() { super.initialize(); fitnessEvaluator.evaluate(population); } @Override protected void iterate() { // mating and selection to generate offspring Population offspring = new Population(); int populationSize = population.size(); while (offspring.size() < numberOfOffspring) { Solution[] parents = selection.select(variation.getArity(), population); Solution[] children = variation.evolve(parents); offspring.addAll(children); } // evaluate the offspring evaluateAll(offspring); // evaluate the fitness of the population and offspring offspring.addAll(population); fitnessEvaluator.evaluate(offspring); // perform environmental selection to downselect the next population population.clear(); population.addAll(truncate(offspring, populationSize)); } /** * Returns the population of solutions that survive to the next generation. * * @param offspring all offspring solutions * @param size the number of solutions to retain * @return the population of solutions that survive to the next generation */ protected Population truncate(Population offspring, int size) { Population survivors = new Population(); // add all non-dominated solutions with a fitness < 1 Iterator<Solution> iterator = offspring.iterator(); while (iterator.hasNext()) { Solution solution = iterator.next(); double fitness = (Double)solution.getAttribute( FitnessEvaluator.FITNESS_ATTRIBUTE); if (fitness < 1.0) { survivors.add(solution); iterator.remove(); } } if (survivors.size() < size) { // fill remaining spaces with dominated solutions offspring.sort(fitnessComparator); while (survivors.size() < size) { survivors.add(offspring.get(0)); offspring.remove(0); } } else if (survivors.size() > size) { // some of the survivors must be truncated MutableDistanceMap map = new MutableDistanceMap(computeDistanceMatrix(survivors)); while (survivors.size() > size) { int index = map.findMostCrowdedPoint(); map.removePoint(index); survivors.remove(index); } } return survivors; } /** * Computes the distance matrix containing the pair-wise distances between * solutions in objective space. The diagonal will contain all 0's. * * @param population the population of solutions * @return the distance matrix */ protected double[][] computeDistanceMatrix(Population population) { double[][] distances = new double[population.size()][population.size()]; for (int i = 0; i < population.size(); i++) { distances[i][i] = 0.0; for (int j = i+1; j < population.size(); j++) { distances[i][j] = distances[j][i] = IndicatorUtils.euclideanDistance(problem, population.get(i), population.get(j)); } } return distances; } /** * Mapping of pair-wise distances between points. This mapping is mutable, * allowing points to be removed. */ public static class MutableDistanceMap { /** * The internal mapping of distances. */ private List<List<Pair<Integer, Double>>> distanceMatrix; /** * Constructs a new mapping of pair-wise distances between points. * * @param rawDistanceMatrix the distance matrix */ public MutableDistanceMap(double[][] rawDistanceMatrix) { super(); initialize(rawDistanceMatrix); } /** * Initializes the internal data structures. * * @param rawDistanceMatrix the distance matrix */ protected void initialize(double[][] rawDistanceMatrix) { distanceMatrix = new LinkedList<List<Pair<Integer, Double>>>(); for (int i = 0; i < rawDistanceMatrix.length; i++) { List<Pair<Integer, Double>> distances = new LinkedList<Pair<Integer, Double>>(); for (int j = 0; j < rawDistanceMatrix[i].length; j++) { if (i != j) { distances.add(new Pair<Integer, Double>(j, rawDistanceMatrix[i][j])); } } Collections.sort(distances, new Comparator<Pair<Integer, Double>>() { @Override public int compare(Pair<Integer, Double> o1, Pair<Integer, Double> o2) { return Double.compare(o1.getSecond(), o2.getSecond()); } }); distanceMatrix.add(distances); } } /** * Returns the most crowded point according to SPEA2's truncation * strategy. The most crowded point is the point with the smallest * distance to its nearest neighbor. Ties are broken by looking at * the next nearest neighbor repeatedly until a difference is found. * * @return the index of the most crowded point */ public int findMostCrowdedPoint() { double minimumDistance = Double.POSITIVE_INFINITY; int minimumIndex = -1; for (int i = 0; i < distanceMatrix.size(); i++) { List<Pair<Integer, Double>> distances = distanceMatrix.get(i); Pair<Integer, Double> point = distances.get(0); if (point.getSecond() < minimumDistance) { minimumDistance = point.getSecond(); minimumIndex = i; } else if (point.getSecond() == minimumDistance) { for (int k = 0; k < distances.size(); k++) { double kdist1 = distances.get(k).getSecond(); double kdist2 = distanceMatrix.get(minimumIndex).get(k).getSecond(); if (kdist1 < kdist2) { minimumIndex = i; break; } else if (kdist2 < kdist1) { break; } } } } return minimumIndex; } /** * Removes the point with the given index. * * @param index the index to remove */ public void removePoint(int index) { distanceMatrix.remove(index); for (List<Pair<Integer, Double>> distances : distanceMatrix) { ListIterator<Pair<Integer, Double>> iterator = distances.listIterator(); while (iterator.hasNext()) { Pair<Integer, Double> point = iterator.next(); if (point.getFirst() == index) { iterator.remove(); } else if (point.getFirst() > index) { // decrement the index so it stays aligned with the // index in distanceMatrix iterator.set(new Pair<Integer, Double>( point.getFirst()-1, point.getSecond())); } } } } } /** * Fitness evaluator for the strength measure with crowding-based niching. */ public class StrengthFitnessEvaluator implements FitnessEvaluator { /** * Crowding is based on the distance to the {@code k}-th nearest * neighbor. */ private final int k; /** * Pareto dominance comparator. */ private final DominanceComparator comparator; /** * Constructs a new fitness evaluator for computing the strength * measure with crowding-based niching. * * @param k crowding is based on the distance to the {@code k}-th * nearest neighbor */ public StrengthFitnessEvaluator(int k) { super(); this.k = k; comparator = new ParetoDominanceComparator(); } @Override public void evaluate(Population population) { int[] strength = new int[population.size()]; double[] fitness = new double[population.size()]; // count the number of individuals each solution dominates for (int i = 0; i < population.size()-1; i++) { for (int j = i+1; j < population.size(); j++) { int comparison = comparator.compare(population.get(i), population.get(j)); if (comparison < 0) { strength[i]++; } else if (comparison > 0) { strength[j]++; } } } // the raw fitness is the sum of the dominance counts (strength) // of all dominated solutions for (int i = 0; i < population.size()-1; i++) { for (int j = i+1; j < population.size(); j++) { int comparison = comparator.compare(population.get(i), population.get(j)); if (comparison < 0) { fitness[j] += strength[i]; } else if (comparison > 0) { fitness[i] += strength[j]; } } } // add density to the fitness double[][] distances = computeDistanceMatrix(population); KthSelector selector = new KthSelector(); for (int i = 0; i < population.size(); i++) { double kdist = selector.select(distances[i], null, k); fitness[i] += 1.0 / (kdist + 2.0); } // assign fitness attribute to solutions for (int i = 0; i < population.size(); i++) { population.get(i).setAttribute(FITNESS_ATTRIBUTE, fitness[i]); } } @Override public boolean areLargerValuesPreferred() { return false; } } }