Using Evolutionary Algorithms in DESDEO (Pydantic Interface)¶
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 and see the guide on the underlying methods that implement the algorithms.
First, we import the necessary namespaces which contain the Pydantic models for the EAs:
desdeo.emo.options.algorithms: Contains a complete model for a posteriori EAs. These models can be used to create algorithms with default settings, or as a starting point that can be modified to suit the user's needs. A complete algorithm model is represented by theEMOOptionspydantic model.desdeo.emo.templates: Contains the models for templates. Templates are described here. A template
from rich.pretty import pprint # Just for pretty printing
from desdeo.emo import (
algorithms,
crossover,
preference_handling,
)
The main import is algorithms, which may be enough for most users. It contains pre-configured versions of popular evolutionary algorithms, as well as the emo_constructor method that can take these configurations, as well as a Problem and return a ready-to-use algorithm. Even for advanced use-cases where the user wants to customize the algorithm, this is often the easiest way to get started.
print("Available Evolutionary Algorithms in DESDEO:")
pprint(list(algorithms.__dict__.keys()))
# The available algorithms can be accessed by the printed list above, except
# the last key `emo_constructor`.
print("\nNSGA-III options:")
pprint(algorithms.nsga3_options())
Available Evolutionary Algorithms in DESDEO:
[ │ 'rvea_options', │ 'nsga2_options', │ 'nsga3_options', │ 'ibea_options', │ 'rvea_mixed_integer_options', │ 'nsga3_mixed_integer_options', │ 'ibea_mixed_integer_options', │ 'emo_constructor' ]
NSGA-III options:
EMOOptions( │ preference=None, │ template=Template1Options( │ │ crossover=SimulatedBinaryCrossoverOptions( │ │ │ name='SimulatedBinaryCrossover', │ │ │ xover_probability=0.5, │ │ │ xover_distribution=30.0 │ │ ), │ │ mutation=BoundedPolynomialMutationOptions( │ │ │ name='BoundedPolynomialMutation', │ │ │ mutation_probability=None, │ │ │ distribution_index=20.0 │ │ ), │ │ selection=NSGA3SelectorOptions( │ │ │ name='NSGA3Selector', │ │ │ reference_vector_options=ReferenceVectorOptions( │ │ │ │ adaptation_frequency=0, │ │ │ │ creation_type='simplex', │ │ │ │ vector_type='planar', │ │ │ │ lattice_resolution=None, │ │ │ │ number_of_vectors=100, │ │ │ │ adaptation_distance=0.2, │ │ │ │ reference_point=None, │ │ │ │ preferred_solutions=None, │ │ │ │ non_preferred_solutions=None, │ │ │ │ preferred_ranges=None │ │ │ ), │ │ │ invert_reference_vectors=False │ │ ), │ │ termination=MaxGenerationsTerminatorOptions(name='MaxGenerationsTerminator', max_generations=100), │ │ generator=LHSGeneratorOptions(n_points=100, name='LHSGenerator'), │ │ repair=NoRepairOptions(name='NoRepair'), │ │ use_archive=True, │ │ seed=42, │ │ verbosity=2, │ │ algorithm_name='NSGA3', │ │ name='Template1' │ ) )
Running Pre-configured Algorithms¶
To run a pre-configured algorithm, simply access the desired algorithm from the algorithms namespace and pass it to the emo_constructor method along with a Problem instance.
from desdeo.problem.testproblems import dtlz2
# Define the problem
problem = dtlz2(n_objectives=3, n_variables=7)
# Create the solver using RVEA with default options
solver, extras = algorithms.emo_constructor(emo_options=algorithms.rvea_options(), problem=problem)
# Solve the problem
results = solver()
# Display some of the optimal outputs
print(results.optimal_outputs.head())
print(f"Total number of optimal solutions found: {len(results.optimal_outputs)}")
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 │ ╞══════════╪════════════╪══════════╪══════════╪════════════╪══════════╪══════════╡ │ 1.001423 ┆ 4.3369e-17 ┆ 0.708267 ┆ 0.707959 ┆ 4.3369e-17 ┆ 0.708267 ┆ 0.707959 │ │ 1.001093 ┆ 0.862184 ┆ 0.494174 ┆ 0.120901 ┆ 0.862184 ┆ 0.494174 ┆ 0.120901 │ │ 1.00172 ┆ 0.944495 ┆ 0.234923 ┆ 0.237029 ┆ 0.944495 ┆ 0.234923 ┆ 0.237029 │ │ 1.000636 ┆ 0.566118 ┆ 0.423807 ┆ 0.707934 ┆ 0.566118 ┆ 0.423807 ┆ 0.707934 │ │ 1.001374 ┆ 0.001012 ┆ 0.196809 ┆ 0.981843 ┆ 0.001012 ┆ 0.196809 ┆ 0.981843 │ └──────────┴────────────┴──────────┴──────────┴────────────┴──────────┴──────────┘ Total number of optimal solutions found: 91
As seen above, the emo_constructor method return two objects: a callable solver and a ConstructorExtras object. You can call the first object to run the solver (no objective function evaluations are conducted before it runs). The second object contains other artefacts generated by the constructor. For example, it the constructor modifies the problem to handle preferences, or uses an archive to store solutions, they can be accessed by this object. The Publisher object used to communicate between the different components of the algorithm is also accessible here.
Modifying Pre-configured Algorithms¶
You can modify the pre-configured algorithms by changing the parameters of the options models before passing them to the emo_constructor method. Let's see an example of using the RVEA algorithm with a different crossover operator.
The rvea_options method returns a pre-configured EMOOptions model for the RVEA algorithm. It has two main attributes: template and preference. Here, we modify the crossover attribute of the template to use a different crossover operator.
problem = dtlz2(n_objectives=3, n_variables=7)
# Create the solver using RVEA with default options
rvea_opts = algorithms.rvea_options()
# Modify the crossover operator to use LocalCrossover with a probability of 0.9
rvea_opts.template.crossover = crossover.LocalCrossoverOptions(xover_probability=0.9)
# Create the solver with modified options
solver, extras = algorithms.emo_constructor(emo_options=rvea_opts, problem=problem)
# Solve the problem, dropping the results variable since it's not used later
_ = solver()
# Accessing archive results from the extras object
print(extras.archive.results.optimal_outputs.tail())
print(f"Total number of non-dominated solutions in archive: {len(extras.archive.results.optimal_outputs)}")
shape: (5, 8) ┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬────────────┐ │ g ┆ f_1 ┆ f_2 ┆ f_3 ┆ f_1_min ┆ f_2_min ┆ f_3_min ┆ generation │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i32 │ ╞══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪══════════╪════════════╡ │ 1.021958 ┆ 0.270631 ┆ 0.985447 ┆ 0.007179 ┆ 0.270631 ┆ 0.985447 ┆ 0.007179 ┆ 100 │ │ 1.030393 ┆ 0.652058 ┆ 0.797813 ┆ 0.004976 ┆ 0.652058 ┆ 0.797813 ┆ 0.004976 ┆ 100 │ │ 1.004146 ┆ 0.207507 ┆ 0.936886 ┆ 0.295794 ┆ 0.207507 ┆ 0.936886 ┆ 0.295794 ┆ 100 │ │ 1.004297 ┆ 0.263695 ┆ 0.915072 ┆ 0.318938 ┆ 0.263695 ┆ 0.915072 ┆ 0.318938 ┆ 100 │ │ 1.005692 ┆ 0.631099 ┆ 0.771465 ┆ 0.134057 ┆ 0.631099 ┆ 0.771465 ┆ 0.134057 ┆ 100 │ └──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴────────────┘ Total number of non-dominated solutions in archive: 4762
Preference Handling in EAs¶
Preference handling in EAs can be done by modifying the preference attribute of the EMOOptions model before passing it to the emo_constructor method.
Here, we will show two examples of handing preferences in the form of reference points. The first example uses interactive RVEA [1] and the second one uses the IOPIS algorithm [2].
J. Hakanen, T. Chugh, K. Sindhya, Y. Jin, and K. Miettinen, “Connections of reference vectors and different types of preference information in interactive multiobjective evolutionary algorithms,” in 2016 IEEE Symposium Series on Computational Intelligence (SSCI), Athens, Greece: IEEE, Dec. 2016, pp. 1-8.
B. S. Saini, J. Hakanen, & K. Miettinen (2020, September). A new paradigm in interactive evolutionary multiobjective optimization. In International Conference on Parallel Problem Solving from Nature (pp. 243-256). Cham: Springer International Publishing.
problem = dtlz2(n_objectives=3, n_variables=7)
irvea = preference_handling.ReferencePointOptions(preference={"f_1": 0.2, "f_2": 0.7, "f_3": 0.5}, method="Hakanen")
iopis = preference_handling.ReferencePointOptions(preference={"f_1": 0.2, "f_2": 0.7, "f_3": 0.5}, method="IOPIS")
irvea_opts = algorithms.rvea_options()
irvea_opts.preference = irvea
iopis_opts = algorithms.rvea_options()
iopis_opts.preference = iopis
# Create the solver with modified options
irvea_solver, _ = algorithms.emo_constructor(emo_options=irvea_opts, problem=problem)
iopis_solver, _ = algorithms.emo_constructor(emo_options=iopis_opts, problem=problem)
# Solve the problem
irvea_results = irvea_solver()
iopis_results = iopis_solver()
# visualize the results
import plotly.express as ex
# Pareto front approximations from RVEA
fig = ex.scatter_3d(
results.optimal_outputs,
x="f_1",
y="f_2",
z="f_3",
title="Pareto Front Approximations",
)
fig.update_traces(marker=dict(size=3, color="blue"), name="Pareto front approximation", showlegend=True)
# Add iRVEA results
fig.add_scatter3d(
x=irvea_results.optimal_outputs["f_1"],
y=irvea_results.optimal_outputs["f_2"],
z=irvea_results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=5, opacity=0.7),
name="iRVEA Solutions",
)
# Add IOPIS results
fig.add_scatter3d(
x=iopis_results.optimal_outputs["f_1"],
y=iopis_results.optimal_outputs["f_2"],
z=iopis_results.optimal_outputs["f_3"],
mode="markers",
marker=dict(size=5, opacity=0.7),
name="IOPIS Solutions",
)
# Add reference point
fig.add_scatter3d(
x=[0.2], y=[0.7], z=[0.5], mode="markers", marker=dict(size=3, color="red", symbol="x"), name="Reference Point"
)
fig.show(renderer="notebook", include_plotlyjs="cdn")