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 import ADMAfsar
from desdeo.emo import algorithms, preference_handling
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]
def interactive_solver(algorithm_options, reference_point):
"""Build an interactive EA solver (NSGA-III or RVEA) guided by a reference point."""
opts = algorithm_options()
opts.preference = preference_handling.ReferencePointOptions(preference=reference_point)
opts.template.selection.reference_vector_options.number_of_vectors = 100
solver, _ = algorithms.emo_constructor(emo_options=opts, problem=problem)
return solver
reference_point = dict(zip(symbols, adm.preference, strict=False))
results_nsga3 = interactive_solver(algorithms.nsga3_options, reference_point)()
results_rvea = interactive_solver(algorithms.rvea_options, reference_point)()
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())
reference_point = dict(zip(symbols, adm.preference, strict=False))
results_nsga3 = interactive_solver(algorithms.nsga3_options, reference_point)()
results_rvea = interactive_solver(algorithms.rvea_options, reference_point)()
# 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
Iteration 1 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
1 NSGA-III 0.001073 8.028691 Learning
1 RVEA 0.000211 8.017427 Learning
----------------------------------------------------------------------
Reference Point: [0.000000, 1.001520, 0.000000]
Iteration 2 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
2 NSGA-III 0.002484 8.575716 Learning
2 RVEA 0.003464 8.520141 Learning
----------------------------------------------------------------------
Reference Point: [0.933562, 0.350086, 0.116695]
Iteration 3 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
3 NSGA-III 0.011164 8.273277 Learning
3 RVEA 0.005255 8.344905 Learning
----------------------------------------------------------------------
Reference Point: [0.972256, 0.216057, 0.108028]
Iteration 4 (Learning Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
4 NSGA-III 0.000222 8.029927 Learning
4 RVEA 0.002001 8.010735 Learning
----------------------------------------------------------------------
Reference Point: [1.000430, 0.000000, 0.000000]
Iteration 5 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
5 NSGA-III 0.005327 8.627511 Decision
5 RVEA 0.013981 8.329765 Decision
----------------------------------------------------------------------
Reference Point: [0.930342, 0.348878, 0.116293]
Iteration 6 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
6 NSGA-III 0.002450 8.537624 Decision
6 RVEA 0.008894 8.468646 Decision
----------------------------------------------------------------------
Reference Point: [0.930154, 0.348808, 0.116269]
Iteration 7 (Decision Phase)
----------------------------------------------------------------------
Iteration Algorithm R-IGD R-HV Phase
7 NSGA-III 0.002450 8.537624 Decision
7 RVEA 0.008894 8.468646 Decision
----------------------------------------------------------------------
Reference Point: [0.930154, 0.348808, 0.116269]
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
def build_preference(preference_type, adm_preference):
"""Map an ADM-generated preference into the matching preference_handling option."""
if preference_type == "reference_point":
return preference_handling.ReferencePointOptions(preference=dict(zip(symbols, adm_preference, strict=False)))
if preference_type == "preferred_solutions":
preferred = {
symbol: adm_preference[:, i].tolist() if adm_preference.ndim == 2 else [adm_preference[i]] # noqa: PLR2004
for i, symbol in enumerate(symbols)
}
return preference_handling.PreferredSolutionsOptions(preference=preferred)
if preference_type == "preferred_ranges":
aspiration_levels = {symbol: adm_preference[i][0] for i, symbol in enumerate(symbols)}
reservation_levels = {symbol: adm_preference[i][1] for i, symbol in enumerate(symbols)}
return preference_handling.DesirableRangesOptions(
aspiration_levels=aspiration_levels, reservation_levels=reservation_levels
)
msg = f"Unknown preference type: {preference_type}"
raise ValueError(msg)
def solve_with_preference(preference):
"""Build and run an interactive NSGA-III solver for the given preference option."""
opts = algorithms.nsga3_options()
opts.preference = preference
opts.template.selection.reference_vector_options.number_of_vectors = 100
solver, _ = algorithms.emo_constructor(emo_options=opts, problem=problem)
return solver()
# 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,
)
results = solve_with_preference(
preference_handling.ReferencePointOptions(preference=dict(zip(symbols, adm.preference, strict=False)))
)
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)
results = solve_with_preference(build_preference(preference_type, adm.preference))
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")
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[5], line 65 61 62 # Run ADM with different preference types 63 reference_points = run_adm_with_preference("reference_point") 64 ranges = run_adm_with_preference("preferred_ranges") ---> 65 preferred_solutions = run_adm_with_preference("preferred_solutions") Cell In[5], line 54, in run_adm_with_preference(preference_type) 50 51 # Collect preferences through iterations 52 while adm.has_next(): 53 front = results.optimal_outputs.select(["f_1", "f_2", "f_3"]).to_numpy() ---> 54 adm.get_next_preference(front, preference_type=preference_type) 55 preferences.append(adm.preference) 56 57 results = solve_with_preference(build_preference(preference_type, adm.preference)) File ~/work/DESDEO/DESDEO/desdeo/adm/adm_afsar.py:107, in ADMAfsar.get_next_preference(self, preference_type, *fronts) 105 if self.iteration_counter == self.it_learning_phase: 106 self.max_assigned_vector = self.get_max_assigned_vector(assigned_vectors) --> 107 self.preference = self.generate_preference_decision( 108 ideal_point, 109 translated_front, 110 assigned_vectors, 111 self.max_assigned_vector, 112 ) 113 self.iteration_counter += 1 114 return self.preference File ~/work/DESDEO/DESDEO/desdeo/adm/adm_afsar.py:231, in ADMAfsar.generate_preference_decision(self, ideal_point, translated_front, assigned_vectors, max_assigned_vector) 229 return self.generate_ranges_decision(ideal_point, translated_front, assigned_vectors, max_assigned_vector) 230 if self.preference_type == "preferred_solutions": --> 231 return self.generate_preferred_solutions_decision( 232 ideal_point, translated_front, assigned_vectors, max_assigned_vector 233 ) 234 raise ValueError( 235 f"Invalid preference type: {self.preference_type}. " 236 "Valid options are 'reference_point', 'preferred_ranges', " 237 "'preferred_solutions', or 'non_preferred_solutions'." 238 ) File ~/work/DESDEO/DESDEO/desdeo/adm/adm_afsar.py:421, in ADMAfsar.generate_preferred_solutions_decision(self, ideal_point, translated_front, assigned_vectors, max_assigned_vector) 407 def generate_preferred_solutions_decision( 408 self, ideal_point, translated_front, assigned_vectors, max_assigned_vector 409 ) -> np.ndarray: 410 """Generate the preferred solutions during the decision phase. 411 412 Args: (...) 419 np.ndarray: The preferred solutions. 420 """ --> 421 sub_population_index = np.atleast_1d(np.squeeze(np.where(assigned_vectors == max_assigned_vector))) 422 sub_population_fitness = translated_front[sub_population_index] 423 sub_pop_fitness_magnitude = np.sqrt(np.sum(np.power(sub_population_fitness, 2), axis=1)) ValueError: operands could not be broadcast together with shapes (452,) (2,)
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()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[6], line 95 91 col=2, 92 ) 93 94 # Plot preferred solutions ---> 95 if preferred_solutions: 96 for i, sol in enumerate(preferred_solutions): 97 solution = np.array(sol) 98 color = "blue" if i < 4 else "red" # noqa: PLR2004 NameError: name 'preferred_solutions' is not defined