Basic Multi-objective optimization features

In this tutorial we will learn how to use PyGMO to solve multiple-objective optimization problems. In essence we will learn how to use the methods that deal with Pareto-optimality, in particular those of the class population

This tutorial assumes an ipython interactive environment. Let us start to define our population:


In [ ]:
from PyGMO import algorithm, island, population, problem
prob = problem.zdt(1)
pop = population(prob, 10)

We here make use of first problem of the ZDT benchmark suite and we created a population containing 10 individuals randomly created within the box bounds. Which individuals belong to which preto front? We can immediately see this by typing:


In [ ]:
pop.compute_pareto_fronts()

In [ ]:
%matplotlib inline
import matplotlib.pyplot as plt

pop = population(prob, 100)
pop.plot_pareto_fronts()  # This call already plots to default pyplot canvas
plt.show()

we have instantiated the algorithm Non-dominated Sorting Genetic Algorithm, able to tackle multi-objective problems, defining the number of generations to 250. In the next line use directly the method evolve of the algorithm to evolve the population. We could have also, similarly, defined an island and use the evolve method of the island:


In [ ]:
algo = algorithm.nsga_II(gen=250)
isl = island(algo, pop)
isl.evolve(1)
isl.population.plot_pareto_fronts()
plt.show()

PADE: a parallel MOEA-D algorithm

In this tutorial we will learn how to use PADE to solve a multi-objective problem. PADE transforms a multi-objective problem into N single-objective ones, where N is the population size. It does that using the Decomposition meta problem. It then solve each problem in parallel using a given single objective algorithm. The population provided at the end of the evolution will be the union of the solutions to each single-objective problem.

Let start using PADE to solve the popular multi-objective benchmark problem ZDT1.


In [ ]:
from PyGMO import problem, algorithm, population
%matplotlib inline
import matplotlib.pyplot as plt

prob = problem.zdt(1)
alg = algorithm.pade()
pop = population(prob, 50)
pop = alg.evolve(pop)
pop.plot_pareto_fronts()
plt.show()

Each point on the pareto front corresponds to the solution to a single-objective problem. In order for the point to be well spread is then crucial to choose the proper decomposition method and the proper weight vectors. It is possible to do that as follow:


In [ ]:
alg = algorithm.pade(decomposition=problem.decompose.BI, weights=algorithm.pade.GRID)
pop = alg.evolve(pop)
pop.plot_pareto_fronts()
plt.show()

As we can see the points on the pareto front are much better spread.

In the following plots we see how different weight generation methods perform on the 3-objective benchmark problem DTLZ1. First we will use a RANDOM weight generation.


In [ ]:
from PyGMO import algorithm, problem, population
alg = algorithm.pade(decomposition=problem.decompose.BI, weights=algorithm.pade.RANDOM)
prob = problem.dtlz(1)
pop = population(prob, 100)
pop = alg.evolve(pop)
prob.plot(pop)

Let’s now try generating the weight vectors with the GRID method


In [ ]:
alg = algorithm.pade(decomposition=problem.decompose.BI, weights=algorithm.pade.GRID)
pop = population(prob, 100)
pop = alg.evolve(pop)

As we can see from the error message we cannot use any population size. The error message will suggest us the two accepted values closer to the one we wanted. Let’s then choose 105.


In [ ]:
pop = population(prob, 105)
pop = alg.evolve(pop)
prob.plot(pop)

As we can see the front is much better spread. The only drawback of the GRID method is that we are not free to choose any population size. To have a better spread than the one obtained with the RANDOM method but still be able to choose any population size, we can use the LOW_DISCREPANCY method.


In [ ]:
alg = algorithm.pade(decomposition=problem.decompose.BI, weights=algorithm.pade.LOW_DISCREPANCY)
pop = population(prob, 100)
pop = alg.evolve(pop)
prob.plot(pop)

We now introduce two more interesting features of PADE.

It is possible to choose which single-objective algorithm to use to solve each single-objective problem the original problem is decomposed into, in the following way


In [ ]:
alg = algorithm.pade(solver=algorithm.jde(50))

Moreover, as said at the beginning of the tutorial, PADE solves the single-objective problems in parallel. It is possible to set how many threads to run. This should be ideally equal to the number of logic cores available in the machine which runs the code.


In [ ]:
alg = algorithm.pade(max_parallelism=8)

Using NSGA-II, SPEA2 and NS-PSO

We will now introduce 3 more multi-objective optimization algorithms.

Let’s start with NSGA-II. NSGA-II is a non-dominated sorting based multi-objective evolutionary algorithm. It generates offspring with crossover and mutation and select the next generation according to non-dominated sorting and crowding distance comparison. As for PADE it is possible to solve a multi-objective optimization problem with NSGA-II as follow


In [ ]:
from PyGMO import algorithm, problem
prob = problem.zdt(1)
alg = algorithm.nsga_II()
pop = population(prob, 100)
pop = alg.evolve(pop)
pop.plot_pareto_fronts()

It is possible to specify the number of generations to run the algorithm for, crossover and mutation probability as well as the mutation and crossover distribution index, as follow


In [ ]:
alg = algorithm.nsga_II(gen=100, cr=0.95, eta_c=10, m=0.01, eta_m=50)

This will run the NSGA-II algorithms for 100 generations, with a crossover probability of 0.95, mutation probability of 0.01, crossover distribution index of 10 and mutation distribution index of 50.

We now introduce NSPSO. Non-dominated Sorting Particle Swarm Optimizer (NSPSO) is a modified version of PSO for multi-objective optimization. It extends the basic ideas of PSO by making a better use of personal bests and offspring for non-dominated comparison. As for PSO it is possible to set:

  • C1 and C2 (the magnitude of the force to apply towards respectively the personal best and the global best of a particle)
  • CHI the velocity scaling factor
  • m_v_coeff the velocity coefficient (determining the maximum allowed particle velocity)
  • minW and maxW which defines in which range the inertia weight will be adapted throughout the run.

Here we see how those parameters can be set when instantiating the algorithm


In [ ]:
alg = algorithm.nspso(gen=10, minW=0.4, maxW=1.0, C1=2.0, C2=2.0, CHI=1.0, v_coeff=0.5)

NSPSO selects the global best for each particles among non-dominated particles. The non-dominated particles are sorted according to one niching method (crowding distance, niche count or maxmin) and the leader is selected among the best ones. The parameter leader_selection_range define which fraction of the non-dominated particles to consider for selection as global best.

In the following code leader_selection_range is set to 20. That means that the global best for each particle is randomly selected among the top 20% (according to the niching method) non-dominated individuals.


In [ ]:
alg = algorithm.nspso(leader_selection_range=20)

It is possible to choose between 3 different niching methods: CROWDING DISTANCE, NICHE COUNT and MAXMIN. This can be done as follow:


In [ ]:
alg = algorithm.nspso(diversity_mechanism=algorithm.nspso.CROWDING_DISTANCE)
alg = algorithm.nspso(diversity_mechanism=algorithm.nspso.NICHE_COUNT)
alg = algorithm.nspso(diversity_mechanism=algorithm.nspso.MAXMIN)

The last multi-objective optimization algorithm we introduce is SPEA2. In the Strength Pareto Evolutionary Algorithm (SPEA2) the quality of an individual is measured taking into consideration its pareto strength and its distance to its K-th neighbour, where K = sqrt(pop size + archive size). It uses the same mutation and crossover operators of NSGA-II, so as for the former it is possible to specify the number of generations to run the algorithm for, crossover and mutation probability as well as the mutation and crossover distribution index, as follow.


In [ ]:
alg = algorithm.spea2(gen=100, cr=0.95, eta_c=10, m=0.01, eta_m=50)

SPEA2 uses an external archive in which are stored the non dominated solutions found so far. The size of the archive is kept constant throughout the run by mean of a truncation operator taking into consideration the distance of each individual to its closest neighbours.

It is possible to set the archive size in two ways. If archive_size is set to 0 (which is the default behaviour), then archive size is adapted to be equal to the population which is evolving. In the following case, for example, the archive size will be set equal to 100.


In [ ]:
alg = algorithm.spea2(archive_size=0)
pop = population(prob, 100)
pop = alg.evolve(pop)

Otherwise it is possible to set the archive size to any size, up to the population size.


In [ ]:
alg = algorithm.spea2(archive_size=20)