/* Copyright 2009-2015 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.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.NotSerializableException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.commons.math3.stat.StatUtils;
import org.apache.commons.math3.util.MathArrays;
import org.moeaframework.core.Initialization;
import org.moeaframework.core.NondominatedPopulation;
import org.moeaframework.core.ParallelPRNG;
import org.moeaframework.core.Population;
import org.moeaframework.core.Problem;
import org.moeaframework.core.Solution;
import org.moeaframework.core.Variation;
/**
* Implementation of MOEA/D, the multiobjective evolutionary algorithm with
* decomposition. This implementation supports both the original MOEA/D
* specification from [1] as well as the utility-based search extension from
* [2].
* <p>
* References:
* <ol>
* <li>Li, H. and Zhang, Q. "Multiobjective Optimization problems with
* Complicated Pareto Sets, MOEA/D and NSGA-II." IEEE Transactions on
* Evolutionary Computation, 13(2):284-302, 2009.
* <li>Zhang, Q., et al. "The Performance of a New Version of MOEA/D on CEC09
* Unconstrained MOP Test Instances." IEEE Congress on Evolutionary Computation,
* 2009.
* </ol>
*/
public class MOEAD extends AbstractAlgorithm {
/**
* Represents an individual (population slot) in the MOEA/D algorithm.
*/
protected static class Individual implements Serializable {
private static final long serialVersionUID = 868794189268472009L;
/**
* The current solution occupying this individual.
*/
private Solution solution;
/**
* The Chebyshev weights for this individual.
*/
private double[] weights;
/**
* The neighborhood of this individual.
*/
private List<Individual> neighbors;
/**
* The utility of this individual.
*/
private double utility;
/**
* The cached fitness of the solution currently occupying this
* individual when the utility was last updated.
*/
private double fitness;
/**
* Constructs an individual with the specified Chebyshev weights.
*
* @param weights the Chebyshev weights for this individual
*/
public Individual(double[] weights) {
this.weights = weights;
neighbors = new ArrayList<Individual>();
utility = 1.0;
}
/**
* Returns the current solution occupying this individual.
*
* @return the current solution occupying this individual
*/
public Solution getSolution() {
return solution;
}
/**
* Sets the current solution occupying this individual.
*
* @param solution the new solution occupying this individual
*/
public void setSolution(Solution solution) {
this.solution = solution;
}
/**
* Returns the Chebyshev weights for this individual.
*
* @return the Chebyshev weights for this individual
*/
public double[] getWeights() {
return weights;
}
/**
* Returns the neighborhood of this individual.
*
* @return the neighborhood of this individual
*/
public List<Individual> getNeighbors() {
return neighbors;
}
/**
* Adds a neighboring individual to the neighborhood of this individual.
*
* @param neighbor the individual to be added to the neighborhood
*/
public void addNeighbor(Individual neighbor) {
neighbors.add(neighbor);
}
/**
* Returns the utility of this individual.
*
* @return the utility of this individual
*/
public double getUtility() {
return utility;
}
/**
* Sets the utility of this individual.
*
* @param utility the new utility of this individual
*/
public void setUtility(double utility) {
this.utility = utility;
}
/**
* Returns the cached fitness of the solution currently occupying this
* individual when the utility was last updated.
*
* @return the cached fitness of the solution currently occupying this
* individual when the utility was last updated
*/
public double getFitness() {
return fitness;
}
/**
* Sets the cached fitness of the solution currently occupying this
* individual when the utility is updated
*
* @param fitness the new fitness of the solution currently occupying
* this individual when the utility is updated
*/
public void setFitness(double fitness) {
this.fitness = fitness;
}
}
/**
* Compares individuals based on their distance from a specified individual.
*/
private static class WeightSorter implements Comparator<Individual> {
/**
* The individual from which weight distances are computed.
*/
private final Individual individual;
/**
* Constructs a comparator for comparing individuals based on their
* distance from the specified individual.
*
* @param individual the individual from which weight distances are
* computed
*/
public WeightSorter(Individual individual) {
this.individual = individual;
}
@Override
public int compare(Individual o1, Individual o2) {
double d1 = MathArrays.distance(
individual.getWeights(), o1.getWeights());
double d2 = MathArrays.distance(
individual.getWeights(), o2.getWeights());
return Double.compare(d1, d2);
}
}
/**
* The current population.
*/
protected List<Individual> population;
/**
* The ideal point; each index stores the best observed value for each
* objective.
*/
private double[] idealPoint;
/**
* The size of the neighborhood used for mating.
*/
private final int neighborhoodSize;
/**
* The probability of mating with a solution in the neighborhood rather than
* the entire population.
*/
private final double delta;
/**
* The maximum number of population slots a solution can replace.
*/
private final double eta;
/**
* The initialization operator.
*/
private final Initialization initialization;
/**
* The variation operator.
*/
private Variation variation;
/**
* The frequency, in generations, in which utility values are updated. Set
* to {@code -1} to disable utility-based search.
*/
protected final int updateUtility;
/**
* The current generation number.
*/
protected int generation;
/**
* The indices of subproblems where the weights are 1 in only one index
*/
private List<Integer> objectiveSubProblems;
private ParallelPRNG pprng = new ParallelPRNG();
/**
* Constructs the MOEA/D algorithm with the specified components. This
* version of MOEA/D uses utility-based search as described in [2].
*
* @param problem the problem being solved
* @param neighborhoodSize the size of the neighborhood used for mating,
* which must be at least {@code variation.getArity()-1}.
* @param initialization the initialization method
* @param variation the variation operator
* @param delta the probability of mating with a solution in the
* neighborhood rather than the entire population
* @param eta the maximum number of population slots a solution can replace
* @param updateUtility the frequency, in generations, in which utility
* values are updated; set to {@code -1} to disable utility-based search
*/
public MOEAD(Problem problem, int neighborhoodSize,
Initialization initialization, Variation variation, double delta,
double eta, int updateUtility) {
super(problem);
this.neighborhoodSize = neighborhoodSize;
this.initialization = initialization;
this.variation = variation;
this.delta = delta;
this.eta = eta;
this.updateUtility = updateUtility;
}
/**
* Constructs the MOEA/D algorithm with the specified components. This
* constructs the original MOEA/D implementation without utility-based
* search.
*
* @param problem the problem being solved
* @param neighborhoodSize the size of the neighborhood used for mating,
* which must be at least {@code variation.getArity()-1}.
* @param initialization the initialization method
* @param variation the variation operator
* @param delta the probability of mating with a solution in the
* neighborhood rather than the entire population
* @param eta the maximum number of population slots a solution can replace
*/
public MOEAD(Problem problem, int neighborhoodSize,
Initialization initialization, Variation variation, double delta,
double eta) {
this(problem, neighborhoodSize, initialization, variation, delta, eta,
-1);
}
@Override
public void initialize() {
super.initialize();
Solution[] initialSolutions = initialization.initialize();
initializePopulation(initialSolutions.length);
initializeNeighborhoods();
initializeIdealPoint();
evaluateAll(initialSolutions);
for (int i = 0; i < initialSolutions.length; i++) {
Solution solution = initialSolutions[i];
updateIdealPoint(solution);
population.get(i).setSolution(solution);
}
for (int i = 0; i < initialSolutions.length; i++) {
population.get(i).setFitness(fitness(
population.get(i).getSolution(),
population.get(i).getWeights()));
}
}
/**
* Initializes the population using a procedure attempting to create a
* uniform distribution of weights.
*
* @param populationSize the population size
*/
private void initializePopulation(int populationSize) {
if (!loadWeights(populationSize, problem.getNumberOfObjectives())) {
if (problem.getNumberOfObjectives() == 2) {
initializePopulation2D(populationSize);
} else {
initializePopulationND(populationSize);
}
}
//Finds the subproblems that are the individual objectives
objectiveSubProblems = new ArrayList<>();
for (int i=0; i<population.size(); i++) {
double[] wts = population.get(i).getWeights();
for (int j = 0; j < problem.getNumberOfObjectives(); j++) {
if (wts[j] == 1.0) {
objectiveSubProblems.add(i);
}
}
if(objectiveSubProblems.size()==problem.getNumberOfObjectives())
break;
}
}
/**
* Initializes the population for 2D problems.
*
* @param populationSize the population size
*/
private void initializePopulation2D(int populationSize) {
population = new ArrayList<Individual>(populationSize);
// ensure boundary weights are at front of the population
population.add(new Individual(new double[]{0.0, 1.0}));
population.add(new Individual(new double[]{1.0, 0.0}));
for (int i = 1; i < populationSize - 1; i++) {
double a = i / (double) (populationSize - 1);
population.add(new Individual(new double[]{a, 1 - a}));
}
}
/**
* Initializes the population for problems of arbitrary dimension.
*
* @param populationSize the population size
*/
private void initializePopulationND(int populationSize) {
int N = 50;
int numberOfObjectives = problem.getNumberOfObjectives();
List<double[]> weights = new ArrayList<double[]>(populationSize * N);
// create random weights
for (int i = 0; i < populationSize * N; i++) {
double[] weight = new double[numberOfObjectives];
for (int j = 0; j < numberOfObjectives; j++) {
weight[j] = pprng.nextDouble();
}
double sum = StatUtils.sum(weight);
for (int j = 0; j < numberOfObjectives; j++) {
weight[j] /= sum;
}
weights.add(weight);
}
population = new ArrayList<Individual>(populationSize);
// initialize population with weights (1,0,...,0), (0,1,...,0), ...,
// (0,...,0,1)
for (int i = 0; i < numberOfObjectives; i++) {
double[] weight = new double[numberOfObjectives];
weight[i] = 1.0;
population.add(new Individual(weight));
}
// fill in remaining weights with the weight vector with the largest
// distance from the assigned weights
while (population.size() < populationSize) {
double[] weight = null;
double distance = Double.NEGATIVE_INFINITY;
for (int i = 0; i < weights.size(); i++) {
double d = Double.POSITIVE_INFINITY;
for (int j = 0; j < population.size(); j++) {
d = Math.min(d, MathArrays.distance(weights.get(i),
population.get(j).getWeights()));
}
if (d > distance) {
weight = weights.get(i);
distance = d;
}
}
population.add(new Individual(weight));
weights.remove(weight);
}
}
/**
* Method from jmetal to load the weights for problems meeting certain
* criteria such as number of objectives and population size. Returns true
* if the weights can be loaded and false if the weights data is
* unavailable.
*
* @return
*/
private boolean loadWeights(int populationSize, int numObjectives) {
String dataFileName;
dataFileName = "W" + numObjectives + "D_"
+ populationSize + ".dat";
try {
// Open the file
FileInputStream fis = new FileInputStream("weight" + File.separator
+ dataFileName);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
population = new ArrayList<Individual>(populationSize);
int j = 0;
String aux = br.readLine();
while (aux != null) {
StringTokenizer st = new StringTokenizer(aux);
j = 0;
double[] wts = new double[numObjectives];
while (st.hasMoreTokens()) {
double value = (new Double(st.nextToken())).doubleValue();
wts[j] = value;
j++;
}
population.add(new Individual(wts));
aux = br.readLine();
}
br.close();
return true;
} catch (Exception e) {
System.out
.println("initUniformWeight: failed when reading for file: "
+ "weight" + File.separator + dataFileName);
return false;
}
}
/**
* Constructs the neighborhoods for all individuals in the population based
* on the distances between weights.
*/
private void initializeNeighborhoods() {
List<Individual> sortedPopulation = new ArrayList<Individual>(
population);
for (Individual individual : population) {
Collections.sort(sortedPopulation, new WeightSorter(individual));
for (int i = 0; i < neighborhoodSize; i++) {
individual.addNeighbor(sortedPopulation.get(i));
}
}
}
/**
* Initializes the ideal point.
*/
private void initializeIdealPoint() {
idealPoint = new double[problem.getNumberOfObjectives()];
Arrays.fill(idealPoint, Double.POSITIVE_INFINITY);
}
/**
* Updates the ideal point with the specified solution.
*
* @param solution the solution
*/
protected void updateIdealPoint(Solution solution) {
for (int i = 0; i < solution.getNumberOfObjectives(); i++) {
idealPoint[i] = Math.min(idealPoint[i], solution.getObjective(i));
}
}
@Override
public NondominatedPopulation getResult() {
NondominatedPopulation result = new NondominatedPopulation();
if (population != null) {
for (Individual individual : population) {
result.add(individual.getSolution());
}
}
return result;
}
/**
* Returns the population indices to be operated on in the current
* generation. If the utility update frequency has been set, then this
* method follows the utility-based MOEA/D search described in [2].
* Otherwise, this follows the original MOEA/D specification from [1].
*
* @return the population indices to be operated on in the current
* generation
*/
protected List<Integer> getSubproblemsToSearch() {
List<Integer> indices = new ArrayList<Integer>();
if (updateUtility < 0) {
// return all indices
for (int i = 0; i < population.size(); i++) {
indices.add(i);
}
} else {
List<Integer> candidates = new ArrayList<>();
for(int i=0; i<population.size(); i++){
candidates.add(i);
}
//add the indices that represent the subproblems along individual objectives
indices.addAll(objectiveSubProblems);
candidates.removeAll(indices);
// return 1/5 of the indices chosen by their utility
while(indices.size() < population.size()/5){
int index = pprng.nextItem(candidates);
for (int j = 1; j < 10; j++) {
int temp = pprng.nextItem(candidates);
if (population.get(temp).getUtility()
> population.get(index).getUtility()) {
index = temp;
}
}
indices.add(index);
candidates.remove((Integer)index);
}
}
pprng.shuffle(indices);
return indices;
}
/**
* Returns the population indices to be considered during mating. With
* probability {@code delta} the neighborhood is returned; otherwise, the
* entire population is returned.
*
* @param index the index of the first parent
* @return the population indices to be considered during mating
*/
protected List<Integer> getMatingIndices(int index) {
List<Integer> matingIndices = new ArrayList<Integer>();
if (pprng.nextDouble() <= delta) {
for (Individual individual : population.get(index).getNeighbors()) {
matingIndices.add(population.indexOf(individual));
}
} else {
for (int i = 0; i < population.size(); i++) {
matingIndices.add(i);
}
}
return matingIndices;
}
/**
* Evaluates the fitness of the specified solution using the Chebyshev
* weights.
*
* @param solution the solution
* @param weights the weights
* @return the fitness of the specified solution using the Chebyshev weights
*/
protected double fitness(Solution solution, double[] weights) {
double max = Double.NEGATIVE_INFINITY;
for (int i = 0; i < solution.getNumberOfObjectives(); i++) {
max = Math.max(max, weights[i] * Math.abs(solution.getObjective(i) - idealPoint[i]));
}
if (solution.violatesConstraints()) {
max += 10000.0;
}
return max;
}
/**
* Updates the population with the specified solution. Only the specified
* population indices are considered for updating. A maximum of {@code eta}
* indices will be modified.
*
* @param solution the solution
* @param matingIndices the population indices that are available for
* updating
* @return Nozomi added for hyper-heuristic. Returns the indices that were
* replaced by offspring solution
*/
protected double updateSolution(Solution solution,
List<Integer> matingIndices) {
int c = 0;
pprng.shuffle(matingIndices);
double out = 0;
for (int i = 0; i < matingIndices.size(); i++) {
Individual individual = population.get(matingIndices.get(i));
double fitness1 = fitness(solution, individual.getWeights());
double fitness2 = fitness(individual.getSolution(), individual.getWeights());
if (fitness1 < fitness2) {
individual.setSolution(solution);
c = c + 1;
out+=(fitness2 - fitness1) / fitness2;
}
if (c >= eta) {
break;
}
}
return out;
}
/**
* Updates the utility of each individual.
*/
protected void updateUtility() {
for (Individual individual : population) {
double oldFitness = individual.getFitness();
double newFitness = fitness(individual.getSolution(), idealPoint);
double relativeDecrease = oldFitness - newFitness;
if (relativeDecrease > 0.001) {
individual.setUtility(1.0);
} else {
double utility = Math.min(1.0, 0.95 * (1.0 + relativeDecrease / 0.001)
* individual.getUtility());
individual.setUtility(utility);
}
individual.setFitness(newFitness);
}
}
@Override
public void iterate() {
List<Integer> indices = getSubproblemsToSearch();
for (Integer index : indices) {
List<Integer> matingIndices = getMatingIndices(index);
Solution[] parents = new Solution[variation.getArity()];
parents[0] = population.get(index).getSolution();
if (variation.getArity() > 2) {
// mimic MOEA/D parent selection for differential evolution
pprng.shuffle(matingIndices);
for (int i = 1; i < variation.getArity() - 1; i++) {
parents[i] = population.get(
matingIndices.get(i - 1)).getSolution();
}
parents[variation.getArity() - 1]
= population.get(index).getSolution();
} else {
for (int i = 1; i < variation.getArity(); i++) {
parents[i] = population.get(
pprng.nextItem(matingIndices)).getSolution();
}
}
Solution[] offspring = variation.evolve(parents);
for (Solution child : offspring) {
evaluate(child);
updateIdealPoint(child);
updateSolution(child, matingIndices);
}
}
generation++;
if ((updateUtility >= 0) && (generation % updateUtility == 0)) {
updateUtility();
}
}
/**
* Gets the solutions in the neighborhood of the ith subproblem
* @param index of the ith subproblem
* @return
*/
protected Population getNeighborhoodSolutions(int index){
Population neighbors = new Population();
List<Integer> neightborIndices = new ArrayList<Integer>();
for (Individual individual : population.get(index).getNeighbors()) {
neightborIndices.add(population.indexOf(individual));
}
for(Integer mateIndex:neightborIndices){
neighbors.add(population.get(mateIndex).getSolution());
}
return neighbors;
}
/**
* Proxy for serializing and deserializing the state of a {@code MOEAD}
* instance. This proxy supports saving the {@code population},
* {@code idealPoint} and {@code generation}.
*/
private static class MOEADState implements Serializable {
private static final long serialVersionUID = 8694911146929397897L;
/**
* The {@code population} from the {@code MOEAD} instance.
*/
private final List<Individual> population;
/**
* The value of the {@code idealPoint} from the {@code MOEAD} instance.
*/
private final double[] idealPoint;
/**
* The value of {@code numberOfEvaluations} from the {@code MOEAD}
* instance.
*/
private final int numberOfEvaluations;
/**
* The value of {@code generation} from the {@code MOEAD} instance.
*/
private final int generation;
/**
* Constructs a proxy for serializing and deserializing the state of a
* {@code MOEAD} instance.
*
* @param population the {@code population} from the {@code MOEAD}
* instance
* @param idealPoint the value of the {@code idealPoint} from the
* {@code MOEAD} instance
* @param numberOfEvaluations the value of {@code numberOfEvaluations}
* from the {@code MOEAD} instance
* @param generation the value of {@code generation} from the
* {@code MOEAD} instance
*/
public MOEADState(List<Individual> population, double[] idealPoint,
int numberOfEvaluations, int generation) {
super();
this.population = population;
this.idealPoint = idealPoint;
this.numberOfEvaluations = numberOfEvaluations;
this.generation = generation;
}
/**
* Returns the {@code population} from the {@code MOEAD} instance.
*
* @return the {@code population} from the {@code MOEAD} instance
*/
public List<Individual> getPopulation() {
return population;
}
/**
* Returns the value of the {@code idealPoint} from the {@code MOEAD}
* instance.
*
* @return the value of the {@code idealPoint} from the {@code MOEAD}
* instance
*/
public double[] getIdealPoint() {
return idealPoint;
}
/**
* Returns the value of {@code numberOfEvaluations} from the
* {@code MOEAD} instance.
*
* @return the value of {@code numberOfEvaluations} from the
* {@code MOEAD} instance
*/
public int getNumberOfEvaluations() {
return numberOfEvaluations;
}
/**
* Returns the value of {@code generation} from the {@code MOEAD}
* instance.
*
* @return the value of {@code generation} from the {@code MOEAD}
* instance
*/
public int getGeneration() {
return generation;
}
}
@Override
public Serializable getState() throws NotSerializableException {
return new MOEADState(population, idealPoint, numberOfEvaluations,
generation);
}
@Override
public void setState(Object objState) throws NotSerializableException {
super.initialize();
MOEADState state = (MOEADState) objState;
population = state.getPopulation();
idealPoint = state.getIdealPoint();
numberOfEvaluations = state.getNumberOfEvaluations();
generation = state.getGeneration();
}
/**
* Gets the current ideal point
* @return the current ideal point
*/
public double[] getIdealPoint() {
return idealPoint;
}
}