Using Evolutionary Algorithms in DESDEO (Functional)¶
Here, we will show multiple ways of using Evolutionary Algorithms (EAs) in DESDEO, depending on the user's needs. Be sure to read the explanation on how these algorithms are structured. We will showcase three different ways of using EAs in DESDEO:
- Using inbuilt algorithms in DESDEO for to calculate a representative set of solutions for multi-objective optimization problems.
- Using inbuilt algorithms in DESDEO for to interactive evolutionary multi-objective optimization.
- Using components implemented in DESDEO to build custom EAs (interactive or not).
Using inbuilt algorithms in DESDEO¶
First, let's import the necessary classes and functions from DESDEO.
import warnings
# Suppress Userwarnings from not having solvers installed
# This is not a problem, as we are not using any solvers in this example.
warnings.filterwarnings("ignore", category=UserWarning)
import plotly.graph_objects as go
from desdeo.emo.hooks.archivers import NonDominatedArchive
from desdeo.emo.methods.EAs import ReferenceVectorOptions, nsga3
from desdeo.problem.testproblems import dtlz2
First, we instantiate the problem we want to solve. We will use the DTLZ2 problem from the desdeo.problems module.
Then, we will pass the problem to the nsga3 method from the desdeo.emo module. This method creates a solver and a publisher object. The solver object can be run to solve the problem using NSGA-III, while the publisher object can be used to append additional components to the algorithm. We will see the usage of publisher further down.
problem = dtlz2(n_variables=10, n_objectives=3)
solver, publisher = nsga3(problem=problem)
result = solver()
Running the solver object will return a EMOResult object, which contains the final population of solutions.
print(result.optimal_variables.head()) # Contains the decision variables values
print(result.optimal_outputs.head())
# Contains the objective values, target values (values that are minimized), and constraints, and extra functions.
shape: (5, 10) ┌──────────┬──────────┬──────────┬──────────┬───┬──────────┬──────────┬──────────┬──────────┐ │ x_1 ┆ x_2 ┆ x_3 ┆ x_4 ┆ … ┆ x_7 ┆ x_8 ┆ x_9 ┆ x_10 │ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ f64 ┆ f64 ┆ ┆ f64 ┆ f64 ┆ f64 ┆ f64 │ ╞══════════╪══════════╪══════════╪══════════╪═══╪══════════╪══════════╪══════════╪══════════╡ │ 0.159948 ┆ 0.77809 ┆ 0.474853 ┆ 0.482729 ┆ … ┆ 0.495864 ┆ 0.48108 ┆ 0.535351 ┆ 0.48465 │ │ 0.05178 ┆ 0.388254 ┆ 0.509597 ┆ 0.482102 ┆ … ┆ 0.502939 ┆ 0.488673 ┆ 0.474891 ┆ 0.505252 │ │ 0.85863 ┆ 0.810954 ┆ 0.504916 ┆ 0.505759 ┆ … ┆ 0.494227 ┆ 0.481803 ┆ 0.46666 ┆ 0.524865 │ │ 0.268906 ┆ 0.115783 ┆ 0.518448 ┆ 0.449515 ┆ … ┆ 0.542335 ┆ 0.481133 ┆ 0.510302 ┆ 0.505532 │ │ 0.043808 ┆ 0.132723 ┆ 0.511168 ┆ 0.512178 ┆ … ┆ 0.49821 ┆ 0.521688 ┆ 0.514324 ┆ 0.512196 │ └──────────┴──────────┴──────────┴──────────┴───┴──────────┴──────────┴──────────┴──────────┘ shape: (5, 7) ┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐ │ g ┆ f_1 ┆ f_2 ┆ f_3 ┆ f_1_min ┆ f_2_min ┆ f_3_min │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │ ╞══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪══════════╡ │ 0.003388 ┆ 0.331957 ┆ 0.913436 ┆ 0.249454 ┆ 0.331957 ┆ 0.913436 ┆ 0.249454 │ │ 0.001771 ┆ 0.81846 ┆ 0.571877 ┆ 0.08139 ┆ 0.81846 ┆ 0.571877 ┆ 0.08139 │ │ 0.004485 ┆ 0.064734 ┆ 0.211548 ┆ 0.97982 ┆ 0.064734 ┆ 0.211548 ┆ 0.97982 │ │ 0.005801 ┆ 0.902269 ┆ 0.16593 ┆ 0.412326 ┆ 0.902269 ┆ 0.16593 ┆ 0.412326 │ │ 0.002052 ┆ 0.978034 ┆ 0.206908 ┆ 0.068901 ┆ 0.978034 ┆ 0.206908 ┆ 0.068901 │ └──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
go.Figure(
go.Scatter3d(
x=result.optimal_outputs["f_1"],
y=result.optimal_outputs["f_2"],
z=result.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=2),
)
).show(renderer="notebook", include_plotlyjs="cdn")
Note that the results object only contains the final population of the EA.
Many users will instead want to keep an archive of solutions and use that for further analysis.
We provide many different kinds of archivers that can hook into the evolutionary process.
One such archiver is the NonDominatedArchive which keeps track of the non-dominated solutions found during the evolutionary process.
To use it, simply make sure that the archive is registered with the publisher. Do note that as this archive will be updated every generation, it will run non-dominatation checks many times, slowing down the process a bit. We will provide a more efficient archiver in the future.
problem = dtlz2(n_variables=10, n_objectives=3)
solver, publisher = nsga3(problem=problem)
archive = NonDominatedArchive(problem=problem, publisher=publisher)
publisher.auto_subscribe(archive)
results = solver()
# Lets plot the results. As we can see, using an archive, we get a much denser representation of the Pareto front using the same number of function evaluations.
go.Figure(
go.Scatter3d(
x=archive.solutions["f_1"],
y=archive.solutions["f_2"],
z=archive.solutions["f_3"],
mode="markers",
marker=dict(size=2),
)
).show(renderer="notebook", include_plotlyjs="cdn")
Using inbuilt algorithms in DESDEO for interactive evolutionary multi-objective optimization¶
Now, we will showcase how the inbuilt algorithms in DESDEO can be used for interactive evolutionary multi-objective optimization. We will also showcase how to change some default parameters of the algorithm.
problem = dtlz2(n_variables=10, n_objectives=3)
reference_point = { # This allows you to specify a reference point for the algorithm to use.
"f_1": 0.5,
"f_2": 0.5,
"f_3": 0.5,
}
solver, publisher = nsga3(
problem=problem,
reference_vector_options=ReferenceVectorOptions(
reference_point=reference_point,
number_of_vectors=30,
# Note: due to the way the reference vectors are generated,
# this number is not guaranteed to be the number of reference vectors used.
# But it is a good estimate.
),
)
results = solver()
# Let us plot the results
fig = go.Figure(
go.Scatter3d(
x=results.optimal_outputs["f_1"],
y=results.optimal_outputs["f_2"],
z=results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=2),
name="solutions",
)
)
fig.add_scatter3d(
x=[reference_point["f_1"]],
y=[reference_point["f_2"]],
z=[reference_point["f_3"]],
mode="markers",
marker=dict(size=5),
name="Reference point",
)
fig.show(renderer="notebook", include_plotlyjs="cdn")
To run multiple iterations, simply reinitalize the solver object with new preferences. Currently, the solver restarts at each new iteration, "forgetting" the previous solutions. We will provide a way to keep the previous solutions in the future.
Let's try a different preference format for the second iteration.
problem = dtlz2(n_variables=10, n_objectives=3)
preferred_solutions = { # This allows you to specify multiple regions of interest.
"f_1": [0.5, 0.1, 0.9],
"f_2": [0.5, 0.1, 0.4],
"f_3": [0.5, 0.9, 0.3],
} # Note that these are target symbols, not objective symbols
solver, publisher = nsga3(
problem=problem,
reference_vector_options=ReferenceVectorOptions(
preferred_solutions=preferred_solutions,
number_of_vectors=30,
# Note: due to the way the reference vectors are generated,
# this number is multiple of the number of preferred solutions (approximately).
),
)
results = solver()
# Let us plot the results
fig = go.Figure(
go.Scatter3d(
x=results.optimal_outputs["f_1"],
y=results.optimal_outputs["f_2"],
z=results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=2),
name="solutions",
)
)
fig.add_trace(
go.Scatter3d(
x=preferred_solutions["f_1"],
y=preferred_solutions["f_2"],
z=preferred_solutions["f_3"],
mode="markers",
marker=dict(size=10),
name="preference",
)
).show(renderer="notebook", include_plotlyjs="cdn")
Note that the solutions are not symmetrically distributed around the provided reference points. This is due to how the interactive algorithm works, and is not a bug. There are a few other ways to provide preferences, which we will showcase in the next sections.
Using components implemented in DESDEO to build custom EAs¶
Finally, we will showcase how to build custom EAs using components implemented in DESDEO. We will use the rvea algorithm as an example.
First, we import the necessary components from DESDEO.
from desdeo.emo.hooks.archivers import Archive
from desdeo.emo.methods.templates import template1
from desdeo.emo.operators.crossover import SimulatedBinaryCrossover
from desdeo.emo.operators.evaluator import EMOEvaluator
from desdeo.emo.operators.generator import RandomGenerator
from desdeo.emo.operators.mutation import BoundedPolynomialMutation
from desdeo.emo.operators.selection import (
NSGA3Selector,
ParameterAdaptationStrategy,
ReferenceVectorOptions,
RVEASelector,
)
from desdeo.emo.operators.termination import MaxEvaluationsTerminator
from desdeo.tools.patterns import Publisher
# Initialize a publisher
publisher = Publisher()
seed = 0
# Initialize EA components
# EMOEvaluator is used to evaluate the solutions
evaluator = EMOEvaluator(problem=problem, publisher=publisher, verbosity=2)
# RVEASelector is the selection operator for RVEA
preferred_ranges = {
"f_1": [0.1, 0.6],
"f_2": [0.2, 0.7],
"f_3": [0.3, 0.6],
}
reference_vector_options = ReferenceVectorOptions(
preferred_ranges=preferred_ranges,
number_of_vectors=30,
)
selector = RVEASelector(
problem=problem,
publisher=publisher,
reference_vector_options=reference_vector_options,
verbosity=2,
parameter_adaptation_strategy=ParameterAdaptationStrategy.FUNCTION_EVALUATION_BASED,
)
# Note that the initial population size is equal to the number of reference vectors
n_points = selector.reference_vectors.shape[0]
# RandomGenerator is used to generate the initial population. Other generators can be used as well.
generator = RandomGenerator(
problem=problem,
evaluator=evaluator,
publisher=publisher,
n_points=n_points,
seed=seed,
verbosity=1,
)
# SimulatedBinaryCrossover is the crossover operator for RVEA
crossover = SimulatedBinaryCrossover(
problem=problem,
publisher=publisher,
seed=seed,
verbosity=1,
)
# BoundedPolynomialMutation is the mutation operator for RVEA
mutation = BoundedPolynomialMutation(
problem=problem,
publisher=publisher,
seed=seed,
verbosity=1,
)
# MaxEvaluationsTerminator ends the optimization after a certain number of evaluations
# Actually, it stops the optimization 1 generation _after_ the number of evaluations has been reached.
# This is annoying, but it is a limitation of the current implementation.
# A temporary workaround is to set the number of evaluations to a number that less than the desired number of
# evaluations by the number of reference vectors.
terminator = MaxEvaluationsTerminator(max_evaluations=50000, publisher=publisher)
# Register the components to the publisher
components = [evaluator, generator, crossover, mutation, selector, terminator]
[publisher.auto_subscribe(x) for x in components]
[publisher.register_topics(x.provided_topics[x.verbosity], x.__class__.__name__) for x in components]
# Choose a template for the EA. template1 is a basic EA template.
results = template1(
crossover=crossover,
mutation=mutation,
selection=selector,
generator=generator,
terminator=terminator,
evaluator=evaluator,
)
# Let us plot the results
fig = go.Figure(
go.Scatter3d(
x=results.optimal_outputs["f_1"],
y=results.optimal_outputs["f_2"],
z=results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=2),
)
)
# calculate all corners of the bounding box of the preferred ranges
corners = [
[preferred_ranges["f_1"][0], preferred_ranges["f_2"][0], preferred_ranges["f_3"][0]],
[preferred_ranges["f_1"][0], preferred_ranges["f_2"][0], preferred_ranges["f_3"][1]],
[preferred_ranges["f_1"][0], preferred_ranges["f_2"][1], preferred_ranges["f_3"][0]],
[preferred_ranges["f_1"][0], preferred_ranges["f_2"][1], preferred_ranges["f_3"][1]],
[preferred_ranges["f_1"][1], preferred_ranges["f_2"][0], preferred_ranges["f_3"][0]],
[preferred_ranges["f_1"][1], preferred_ranges["f_2"][0], preferred_ranges["f_3"][1]],
[preferred_ranges["f_1"][1], preferred_ranges["f_2"][1], preferred_ranges["f_3"][0]],
[preferred_ranges["f_1"][1], preferred_ranges["f_2"][1], preferred_ranges["f_3"][1]],
]
# plot the corners
fig.add_trace(
go.Scatter3d(
x=[x[0] for x in corners],
y=[x[1] for x in corners],
z=[x[2] for x in corners],
mode="markers+lines",
marker=dict(size=3),
name="preference",
)
).show(renderer="notebook", include_plotlyjs="cdn")
Finally, let us try the remaining way of conducting optimization, this time using NSGA-3. We will use the "non-preferred solution" to define regions of disinterest.
We will also try keeping an archive of all found solutions.
# Initialize a publisher
publisher = Publisher()
seed = 0
problem = dtlz2(n_variables=10, n_objectives=3)
# Initialize EA components
# EMOEvaluator is used to evaluate the solutions
evaluator = EMOEvaluator(problem=problem, publisher=publisher, verbosity=2)
# RVEASelector is the selection operator for RVEA
non_preferred_solutions = {
"f_1": [0.5, 0.1, 0.9],
"f_2": [0.1, 0.9, 0.4],
"f_3": [0.9, 0.3, 0.1],
}
reference_vector_options = ReferenceVectorOptions(
non_preferred_solutions=non_preferred_solutions,
)
selector = NSGA3Selector(
problem=problem,
publisher=publisher,
reference_vector_options=reference_vector_options,
verbosity=2,
)
# Note that the initial population size is equal to the number of reference vectors
n_points = selector.reference_vectors.shape[0]
# RandomGenerator is used to generate the initial population. Other generators can be used as well.
generator = RandomGenerator(
problem=problem,
evaluator=evaluator,
publisher=publisher,
n_points=n_points,
seed=seed,
verbosity=1,
)
# SimulatedBinaryCrossover is the crossover operator for RVEA
crossover = SimulatedBinaryCrossover(
problem=problem,
publisher=publisher,
seed=seed,
verbosity=1,
)
# BoundedPolynomialMutation is the mutation operator for RVEA
mutation = BoundedPolynomialMutation(
problem=problem,
publisher=publisher,
seed=seed,
verbosity=1,
)
# MaxEvaluationsTerminator ends the optimization after a certain number of evaluations
# Actually, it stops the optimization 1 generation _after_ the number of evaluations has been reached.
# This is annoying, but it is a limitation of the current implementation.
# A temporary workaround is to set the number of evaluations to a number that less than the desired number of
# evaluations by the number of reference vectors.
terminator = MaxEvaluationsTerminator(max_evaluations=50000, publisher=publisher)
# Register the components to the publisher
components = [evaluator, generator, crossover, mutation, selector, terminator]
[publisher.auto_subscribe(x) for x in components]
[publisher.register_topics(x.provided_topics[x.verbosity], x.__class__.__name__) for x in components]
# Initialize and register any additional components
archive = Archive(problem=problem, publisher=publisher)
publisher.auto_subscribe(archive)
# Choose a template for the EA. template1 is a basic EA template.
results = template1(
crossover=crossover,
mutation=mutation,
selection=selector,
generator=generator,
terminator=terminator,
evaluator=evaluator,
)
# Let us plot the results
fig = go.Figure(
go.Scatter3d(
x=results.optimal_outputs["f_1"],
y=results.optimal_outputs["f_2"],
z=results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=2),
)
)
fig.add_trace(
go.Scatter3d(
x=non_preferred_solutions["f_1"],
y=non_preferred_solutions["f_2"],
z=non_preferred_solutions["f_3"],
mode="markers",
marker=dict(size=10),
name="non-preference",
)
)
fig.show(renderer="notebook", include_plotlyjs="cdn")
# Let us plot the archive
fig = go.Figure(
go.Scatter3d(
x=archive.solutions["f_1"],
y=archive.solutions["f_2"],
z=archive.solutions["f_3"],
mode="markers",
marker=dict(size=2),
)
)
fig.show(renderer="notebook", include_plotlyjs="cdn")