Using an Artificial Decision Maker (ADM) to Compare Interactive Evolutionary Methods¶
This tutorial demonstrates how to use Artificial Decision Makers (ADMs) to compare interactive multiobjective evolutionary optimization methods. We will focus on comparing two methods:
- Interactive RVEA (Reference Vector Guided Evolutionary Algorithm)
- Interactive NSGA-III (Non-dominated Sorting Genetic Algorithm III)
We will use the ADM proposed by Afsar et al. and evaluate performance using the R-metric framework:
- R-IGD (R-metric Inverted Generational Distance)
- R-HV (R-metric Hypervolume)
How the ADM Works¶
The ADM operates in two sequential phases:
Learning Phase:
- Explores the entire objective space
- Generates reference points in least explored areas
- Builds understanding of the Pareto front
- Identifies potential regions of interest
Decision Phase:
- Focuses on the identified ROI
- Refines solutions through targeted reference points
- Guides the search towards most preferred solutions
Iteration Process¶
At each iteration, the ADM follows these steps:
- Combines solutions from all algorithms and removes dominated ones
- Divides the combined front using uniform reference vectors
- Maps solutions to reference vectors based on angular distance
- Generates new reference points based on current phase
- Evaluates algorithm performance within the ROI
Reference:
Afsar, B., Miettinen, K., & Ruiz, A. B. (2021).
An Artificial Decision Maker for Comparing Reference Point Based Interactive Evolutionary Multiobjective Optimization Methods. In: Ishibuchi, H., et al. Evolutionary Multi-Criterion Optimization. EMO 2021. Lecture Notes in Computer Science, vol 12654. Springer, Cham.
Problem Definition¶
For this demonstration, we will use the DTLZ2 benchmark with 3 objective functions and 12 decision variables.
First, we need to import the required modules and create the problem instance:
import numpy as np
from desdeo.adm.ADMAfsar import ADMAfsar
from desdeo.emo.hooks.archivers import NonDominatedArchive
from desdeo.emo.methods.EAs import nsga3, rvea
from desdeo.problem.testproblems import dtlz2
from desdeo.tools.indicators_unary import r_metric_indicator
problem = dtlz2(n_objectives=3, n_variables=12)
Initializing ADM and Optimization Methods¶
The procedure consists of the following steps:
- Initialize ADM with the selected problem and specify the number of iterations for each phase.
- Learning phase: 4 iterations
- Decision phase: 3 iterations
- Generate an initial reference point randomly within the objective space.
- Run both interactive NSGA-III and interactive RVEA using the same reference point.
Note: The initial iteration with the random reference point is not included in the phase iterations.
adm = ADMAfsar(problem=problem, it_learning_phase=4, it_decision_phase=3, number_of_vectors=100)
symbols = [f"{x.symbol}" for x in problem.objectives]
solver_nsga3, publisher_nsga3 = nsga3(
problem=problem,
reference_vector_options={
"reference_point": dict(zip(symbols, adm.preference, strict=False)),
"interactive_adaptation": "reference_point",
"number_of_vectors": 100,
},
)
solver_rvea, publisher_rvea = rvea(
problem=problem,
reference_vector_options={
"reference_point": dict(zip(symbols, adm.preference, strict=False)),
"interactive_adaptation": "reference_point",
"number_of_vectors": 100,
},
)
archive_nsga3 = NonDominatedArchive(problem=problem, publisher=publisher_nsga3)
archive_rvea = NonDominatedArchive(problem=problem, publisher=publisher_rvea)
publisher_nsga3.auto_subscribe(archive_nsga3)
publisher_rvea.auto_subscribe(archive_rvea)
results_nsga3 = solver_nsga3()
results_rvea = solver_rvea()
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Interactive Optimization Process¶
The ADM framework steers the optimization through an automated, interactive procedure:
Phase Control¶
adm.has_next(): Checks whether additional iterations are requiredadm.get_next_reference_point(): Generates the next reference point based on the current set of solutions
Phase-Specific Behavior¶
Learning Phase
- Explores diverse regions of the objective space
- Identifies promising areas for subsequent exploration
Decision Phase
- Concentrates on the region of interest identified during the learning phase
- Produces targeted reference points to refine the search
Performance Evaluation¶
At each iteration, solution quality is evaluated using R-IGD and R-HV.
The following cell demonstrates this iterative process in practice.
import pandas as pd
iteration = 0
metrics_data = []
reference_points = []
while adm.has_next():
iteration += 1
front_rvea = results_rvea.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy()
front_nsga3 = results_nsga3.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy()
adm.get_next_preference(front_rvea, front_nsga3)
reference_points.append(adm.preference.copy())
solver_nsga3, publisher_nsga3 = nsga3(
problem=problem,
reference_vector_options={
"reference_point": dict(zip(symbols, adm.preference, strict=False)),
"interactive_adaptation": "reference_point",
"number_of_vectors": 100,
},
)
solver_rvea, publisher_rvea = rvea(
problem=problem,
reference_vector_options={
"reference_point": dict(zip(symbols, adm.preference, strict=False)),
"interactive_adaptation": "reference_point",
"number_of_vectors": 100,
},
)
results_nsga3 = solver_nsga3()
results_rvea = solver_rvea()
# Calculate metrics
metrics_nsga3 = r_metric_indicator(
results_nsga3.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy(), np.array([adm.preference])
)
metrics_rvea = r_metric_indicator(
results_rvea.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy(), np.array([adm.preference])
)
# Store metrics in the list
current_metrics = pd.DataFrame(
[
{
"Iteration": iteration,
"Algorithm": "NSGA-III",
"R-IGD": metrics_nsga3.r_igd, # First value is R-IGD
"R-HV": metrics_nsga3.r_hv, # Second value is R-HV
"Phase": "Learning" if iteration <= 4 else "Decision", # noqa: PLR2004
},
{
"Iteration": iteration,
"Algorithm": "RVEA",
"R-IGD": metrics_rvea.r_igd, # First value is R-IGD
"R-HV": metrics_rvea.r_hv, # Second value is R-HV
"Phase": "Learning" if iteration <= 4 else "Decision", # noqa: PLR2004
},
]
)
if len(metrics_data) == 0:
metrics_data = current_metrics
else:
metrics_data = pd.concat([metrics_data, current_metrics], ignore_index=True)
# Print current metrics in a formatted table
print(f"\nIteration {iteration} ({current_metrics['Phase'].iloc[0]} Phase)") # noqa: T201
print("-" * 70) # noqa: T201
print(current_metrics.to_string(index=False, float_format=lambda x: f"{x:.6f}")) # noqa: T201
print("-" * 70) # noqa: T201
# Print reference point
ref_point = np.array(adm.preference)
print(f"Reference Point: [{', '.join(f'{x:.6f}' for x in ref_point)}]") # noqa: T201
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 1 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
1 NSGA-III 0.005198 8.026253 Learning
1 RVEA 0.000896 8.007621 Learning
----------------------------------------------------------------------
Reference Point: [0.000000, 1.005541, 0.000000]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 2 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
2 NSGA-III 0.013889 8.442606 Learning
2 RVEA 0.010316 8.435004 Learning
----------------------------------------------------------------------
Reference Point: [0.116317, 0.348952, 0.930538]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 3 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
3 NSGA-III 0.002126 8.619168 Learning
3 RVEA 0.018789 8.310242 Learning
----------------------------------------------------------------------
Reference Point: [0.236087, 0.236087, 0.944348]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 4 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
4 NSGA-III 0.003442 8.399139 Learning
4 RVEA 0.007504 8.274476 Learning
----------------------------------------------------------------------
Reference Point: [0.215871, 0.971418, 0.107935]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 5 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
5 NSGA-III 0.010068 8.692422 Decision
5 RVEA 0.004701 8.629229 Decision
----------------------------------------------------------------------
Reference Point: [0.254169, 0.381254, 0.889593]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 6 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
6 NSGA-III 0.010068 8.692422 Decision
6 RVEA 0.004701 8.629229 Decision
----------------------------------------------------------------------
Reference Point: [0.254169, 0.381254, 0.889593]
/home/runner/work/DESDEO/DESDEO/desdeo/emo/methods/EAs.py:78: UserWarning: Adaptation frequency was set to 0. Setting it to 100 for RVEA selector. Set it to 0 only if you provide preference information. selector = RVEASelector(
Iteration 7 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
7 NSGA-III 0.010068 8.692422 Decision
7 RVEA 0.004701 8.629229 Decision
----------------------------------------------------------------------
Reference Point: [0.254169, 0.381254, 0.889593]
Visualization of Reference Points¶
Let's visualize how the ADM generates reference points throughout both phases of the optimization process. The following plot shows:
Learning Phase (Left): The first 4 reference points (L1-L4) demonstrate how the ADM explores different regions of the Pareto front to understand the full range of available trade-offs.
Decision Phase (Right): The subsequent 3 reference points (D1-D3) show how the ADM focuses on a specific region of interest, refining the search based on the knowledge gained during the learning phase.
The gray surface represents the Pareto front of the DTLZ2 problem, which forms a quarter-sphere in the first octant (all objectives ≥ 0). Points are labeled chronologically to show the sequence of reference point generation.
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots
pio.renderers.default = "notebook"
reference_points = np.array(reference_points)
phi = np.linspace(0, np.pi / 2, 50)
theta = np.linspace(0, np.pi / 2, 50)
phi, theta = np.meshgrid(phi, theta)
x = np.cos(theta) * np.cos(phi)
y = np.cos(theta) * np.sin(phi)
z = np.sin(theta)
# Separate reference points by phase
learning_points = reference_points[:4] # First 4 iterations
decision_points = reference_points[4:] # Remaining iterations
# Create figure with two subplots
fig = make_subplots(
rows=1, cols=2, specs=[[{"type": "scene"}, {"type": "scene"}]], subplot_titles=("Learning phase", "Decision phase")
)
# Add Pareto front to both views
fig.add_trace(
go.Surface(x=x, y=y, z=z, colorscale="Greys", showscale=False, opacity=0.3, name="Pareto Front"), row=1, col=1
)
fig.add_trace(
go.Surface(x=x, y=y, z=z, colorscale="Greys", showscale=False, opacity=0.3, name="Pareto Front"), row=1, col=2
)
# Add learning points
fig.add_trace(
go.Scatter3d(
x=learning_points[:, 0],
y=learning_points[:, 1],
z=learning_points[:, 2],
mode="markers+text",
marker={"size": 8, "color": "blue"},
text=[f"L{i + 1}" for i in range(len(learning_points))],
name="Learning Phase",
showlegend=True,
),
row=1,
col=1,
)
# Add decision points
fig.add_trace(
go.Scatter3d(
x=decision_points[:, 0],
y=decision_points[:, 1],
z=decision_points[:, 2],
mode="markers+text",
marker={"size": 8, "color": "red"},
text=[f"D{i + 1}" for i in range(len(decision_points))],
name="Decision Phase",
showlegend=True,
),
row=1,
col=2,
)
# Update layout
fig.update_layout(
title="Reference Points Generared by the ADM",
width=1200,
height=600,
showlegend=True,
)
fig.show()
The performance comparison between NSGA-III and RVEA is evaluated using two performance indicators: R-IGD (lower is better) measures how well solutions converge to and spread along the Pareto front in the region of interest, while R-HV (higher is better) indicates the volume of dominated objective space. The optimization process progresses through two phases: a learning phase (iterations 1-4) where algorithms explore the objective space with spread-out reference points, followed by a decision phase (iterations 5-7) that focuses on refining solutions in the identified preferred region. By comparing these indicators across both phases, we can assess which algorithm better balances exploration (finding diverse solutions) and exploitation (refining solutions in preferred regions).
Using ADM with different types of preference information¶
The ADM proposed by Afsar et al. supports three different ways to express preferences during the optimization process:
Reference Points: Single points in the objective space that guide the search towards specific regions of interest.
Preferred Ranges: Rectangular regions defined by minimum and maximum values for each objective, allowing the decision maker to specify acceptable ranges.
Preferred Solutions: Multiple points that define several regions of interest simultaneously, enabling parallel exploration of different trade-offs.
Reference:
Afsar, B., Ruiz, A. B., & Miettinen, K. (2023).
Comparing interactive evolutionary multiobjective optimization methods with an artificial decision maker. Complex & Intelligent Systems, Volume 9, pages 1165–1181. Springer.
In this section, we will show how to use each preference type with the ADM.
problem = dtlz2(n_objectives=3, n_variables=12)
iterations_learning = 4
iterations_decision = 3
# Function to create and run ADM with different preference types
def run_adm_with_preference(preference_type): # noqa: D103
# Initialize ADM and solver.
# The first iteration is always done with a reference point
adm = ADMAfsar(
problem=problem,
it_learning_phase=iterations_learning,
it_decision_phase=iterations_decision,
number_of_vectors=100,
)
solver_nsga3, _ = nsga3(
problem=problem,
reference_vector_options={
"interactive_adaptation": "reference_point",
"reference_point": dict(zip(symbols, adm.preference, strict=False)),
"number_of_vectors": 100,
},
)
results = solver_nsga3()
preferences = []
# Collect preferences through iterations
while adm.has_next():
front = results.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy()
adm.get_next_preference(front, preference_type=preference_type)
preferences.append(adm.preference)
solver_nsga3, _ = nsga3(
problem=problem,
reference_vector_options={
"interactive_adaptation": preference_type,
# Update preferences based on type
"preferred_ranges": {
symbol: [adm.preference[i][0], adm.preference[i][1]] for i, symbol in enumerate(symbols)
}
if preference_type == "preferred_ranges"
else None,
"reference_point": dict(zip(symbols, adm.preference, strict=False))
if preference_type == "reference_point"
else None,
"preferred_solutions": {
symbol: adm.preference[:, i].tolist() if adm.preference.ndim == 2 else [adm.preference[i]] # noqa: PLR2004
for i, symbol in enumerate(symbols)
}
if preference_type == "preferred_solutions"
else None,
"number_of_vectors": 100,
},
)
results = solver_nsga3()
return preferences
# Run ADM with different preference types
reference_points = run_adm_with_preference("reference_point")
ranges = run_adm_with_preference("preferred_ranges")
preferred_solutions = run_adm_with_preference("preferred_solutions")
Visualizing Different Preference Types¶
Here we create a three-panel visualization to compare how different preference types are expressed during the optimization process:
Left Panel: Shows reference points during both learning (blue) and decision (red) phases.
Middle Panel: Displays preferred ranges as rectangular regions.
Right Panel: Illustrates preferred solutions, where multiple points simultaneously define different regions of interest.
The gray surface in each panel represents the Pareto front of the DTLZ2 problem. Points and regions are color-coded by phase (blue for learning, red for decision).
# Create visualization
def create_pareto_surface(): # noqa: D103
phi = np.linspace(0, np.pi / 2, 50)
theta = np.linspace(0, np.pi / 2, 50)
phi, theta = np.meshgrid(phi, theta)
x = np.cos(theta) * np.cos(phi)
y = np.cos(theta) * np.sin(phi)
z = np.sin(theta)
return x, y, z
# Create subplots for different preference types
fig = make_subplots(
rows=1,
cols=3,
specs=[[{"type": "scene"}, {"type": "scene"}, {"type": "scene"}]],
subplot_titles=("Reference Points", "Preferred Ranges", "Preferred Solutions"),
)
# Add Pareto front to all subplots
x, y, z = create_pareto_surface()
for i in range(1, 4):
fig.add_trace(
go.Surface(x=x, y=y, z=z, colorscale="Greys", showscale=False, opacity=0.3, name="Pareto Front"), row=1, col=i
)
# Plot reference points
if reference_points:
ref_points_array = np.array(reference_points)
learning_points = ref_points_array[:4]
decision_points = ref_points_array[4:]
# Learning phase points
fig.add_trace(
go.Scatter3d(
x=learning_points[:, 0],
y=learning_points[:, 1],
z=learning_points[:, 2],
mode="markers+text",
marker={"size": 8, "color": "blue"},
text=[f"L{i + 1}" for i in range(len(learning_points))],
name="Learning Phase Points",
),
row=1,
col=1,
)
# Decision phase points
fig.add_trace(
go.Scatter3d(
x=decision_points[:, 0],
y=decision_points[:, 1],
z=decision_points[:, 2],
mode="markers+text",
marker={"size": 8, "color": "red"},
text=[f"D{i + 1}" for i in range(len(decision_points))],
name="Decision Phase Points",
),
row=1,
col=1,
)
# Plot ranges
if ranges:
for i, range_pref in enumerate(ranges):
color = "blue" if i < 4 else "red" # noqa: PLR2004
name = f"{'Learning' if i < 4 else 'Decision'} Range {i % 4 + 1}" # noqa: PLR2004
# Create box corners
corners = []
for x in [range_pref[0][0], range_pref[0][1]]:
for y in [range_pref[1][0], range_pref[1][1]]:
for z in [range_pref[2][0], range_pref[2][1]]:
corners.append([x, y, z])
corners = np.array(corners)
# Draw box edges
fig.add_trace(
go.Scatter3d(
x=corners[:, 0],
y=corners[:, 1],
z=corners[:, 2],
mode="lines+markers",
marker={"size": 4, "color": color},
line={"color": color, "width": 2},
name=name,
),
row=1,
col=2,
)
# Plot preferred solutions
if preferred_solutions:
for i, sol in enumerate(preferred_solutions):
solution = np.array(sol)
color = "blue" if i < 4 else "red" # noqa: PLR2004
name = f"{'Learning' if i < 4 else 'Decision'} Solution {i % 4 + 1}" # noqa: PLR2004
points = solution.reshape(1, -1) if solution.ndim == 1 else solution
# Plot all points for this solution
fig.add_trace(
go.Scatter3d(
x=points[:, 0], # All x coordinates
y=points[:, 1], # All y coordinates
z=points[:, 2], # All z coordinates
mode="markers+text",
marker={"size": 8, "color": color},
text=[f"{'L' if i < 4 else 'D'}{i % 4 + 1}_{j + 1}" for j in range(len(points))], # noqa: PLR2004
name=name,
),
row=1,
col=3,
)
# Update layout
fig.update_layout(
title="Different Types of Preferences Generated by ADM",
width=1500,
height=600,
showlegend=True,
)
# Update axes ranges and aspect ratio for all subplots
for i in range(1, 4):
fig.update_scenes(
{
"xaxis_title": "f1",
"yaxis_title": "f2",
"zaxis_title": "f3",
"xaxis": {"range": [0, 1.1]},
"yaxis": {"range": [0, 1.1]},
"zaxis": {"range": [0, 1.1]},
"aspectmode": "cube",
"camera": {
"up": {"x": 0, "y": 0, "z": 1},
"center": {"x": 0, "y": 0, "z": 0},
"eye": {"x": 1.5, "y": 1.5, "z": 1.5},
},
},
row=1,
col=i,
)
fig.show()