How an XLEMOO method can be used as an interactive multiobjective optimization method

In this example, we can see how an XLEMOO method can be used as an interactive multiobjective optimization method accepting reference points as preferences.

Import and helper functions

Below, we define some imports and helper functions to be utilized later in printing the results of our LEMOO model.

[1]:
import sys
sys.path.append("../../../XLEMOO")

import warnings

warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", category=FutureWarning)

from XLEMOO.LEMOO import EAParams, MLParams, LEMParams, LEMOO, PastGeneration
from XLEMOO.fitness_indicators import asf_wrapper
from XLEMOO.selection import SelectNBest
from XLEMOO.plotting import show_rules
from XLEMOO.ruleset_interpreter import extract_skoped_rules
from desdeo_emo.recombination import SBX_xover, BP_mutation
from desdeo_tools.scalarization.ASF import PointMethodASF
from desdeo_problem.testproblems import vehicle_crashworthiness

import matplotlib.pyplot as plt
import numpy as np
from imodels import SkopeRulesClassifier

def extract_rule_set(lemoo, problem, n_variables):
    rules, accuracies = extract_skoped_rules(lemoo.current_ml_model)
    problem_lower_bounds = problem.get_variable_lower_bounds()
    problem_upper_bounds = problem.get_variable_upper_bounds()

    rules_for_vars = {f"X_{i}": {">": [problem_lower_bounds[i], -1], "<=": [problem_upper_bounds[i], -1]} for i in range(n_variables)}

    for accuracy, rule in zip(accuracies, rules):
        for key in rule:
            var_name = key[0]
            op = key[1]

            # check accuracy
            if rules_for_vars[var_name][op][1] < accuracy:
                # update accuracy
                rules_for_vars[var_name][op][1] = accuracy
                if op == "<=":
                    # tighten rule, if necessary
                    if float(rule[(var_name, op)]) <= rules_for_vars[var_name][op][0]:
                        rules_for_vars[var_name][op][0] = float(rule[(var_name, op)])
                elif op == ">":
                    # tighten rule, if necessary
                    if float(rule[(var_name, op)]) > rules_for_vars[var_name][op][0]:
                        rules_for_vars[var_name][op][0] = float(rule[(var_name, op)])

    return rules_for_vars

def complete_missing_rules(lemoo, rules_for_vars):
    lower_bounds = np.min(lemoo._generation_history[-1].individuals, axis=0)
    upper_bounds = np.max(lemoo._generation_history[-1].individuals, axis=0)

    # if there are no upper or lower bound for some vars in the rules, use the min or max from the population
    for var_i, var_name in enumerate(rules_for_vars):
        for op in rules_for_vars[var_name]:
            if op == "<=":
                if rules_for_vars[var_name][op][1] == -1:
                    # replace missing rule with bound from population
                    rules_for_vars[var_name][op][0] = upper_bounds[var_i]
            if op == ">":
                if rules_for_vars[var_name][op][1] == -1:
                    rules_for_vars[var_name][op][0] = lower_bounds[var_i]

    return rules_for_vars

def print_rules(lemoo, rules_for_vars, tex=False):
    lower_bounds = np.min(lemoo._generation_history[-1].individuals, axis=0)
    upper_bounds = np.max(lemoo._generation_history[-1].individuals, axis=0)

    print("RULES:")
    print("Var\tLower(R)\t\tUpper(R)\t\tLower(P)\t\tUpper(P)")
    for i, rule in enumerate(rules_for_vars):
        if not tex:
            msg = (f"{rule}\t{np.round(rules_for_vars[rule]['>'][0], 5)}\t({np.round(rules_for_vars[rule]['>'][1], 3)})\t\t{np.round(rules_for_vars[rule]['<='][0], 5)} ({np.round(rules_for_vars[rule]['<='][1], 3)})\t\t"
                   f"{np.round(lower_bounds[i], 5)}\t\t\t{np.round(upper_bounds[i], 5)}")

        else:
            msg = (f"${rule}$ & ${np.round(rules_for_vars[rule]['>'][0], 5)}$ & $({np.round(rules_for_vars[rule]['>'][1], 3)})$ & ${np.round(rules_for_vars[rule]['<='][0], 5)}$ & $({np.round(rules_for_vars[rule]['<='][1], 3)})$ &"
               f"${np.round(lower_bounds[i], 5)}$ & ${np.round(upper_bounds[i], 5)}$ \\\\")

        print(msg)

    return None
/home/kilo/workspace/XLEMOO/docs/notebooks/../../../XLEMOO/XLEMOO/tree_interpreter.py:1: DeprecationWarning:

The Tix Tk extension is unmaintained, and the tkinter.tix wrapper module is deprecated in favor of tkinter.ttk

Initialize the problem

Next, we initialize a simple test problem with 3 objective functions and 5 variables. We use the vehicle crash worthiness problem.

[2]:
n_objectives = 3
n_variables = 5

problem = vehicle_crashworthiness()

Initialize an XLEMOO method

Now we are ready to define our LEMOO method. We start by defining the parameters of our model. In lem_params general parameters related to the LEMOO method are defined. In ea_params, parameters specific to the evolutionary phase of our LEMOO method are defined, and in ml_params parameters specific to the learning phase of the method are defined. For additional information about these parameters, see the API documentation of the XLEMOO framework.

[3]:
ideal = np.array([1600.0, 6.0, 0.038])
nadir = np.array([1700.0, 12.0, 0.2])
ref_point = np.array([1670.0, 7.61449, 0.085])  # the reference point

# define the achievement scalarizing function as the fitness function
ref_asf = asf_wrapper(PointMethodASF(ideal=ideal, nadir=nadir), {"reference_point": ref_point})
fitness_fun = ref_asf

lem_params = LEMParams(
    use_darwin=True,
    use_ml=True,
    fitness_indicator=fitness_fun,
    ml_probe = 1,
    ml_threshold = None,
    darwin_probe = None,
    darwin_threshold = None,
    total_iterations=10,
)

ea_params = EAParams(
    population_size=50,
    cross_over_op=SBX_xover(),
    mutation_op=BP_mutation(problem.get_variable_lower_bounds(), problem.get_variable_upper_bounds()),
    selection_op=SelectNBest(None, 50),  # keep population size constant
    population_init_design="LHSDesign",
    iterations_per_cycle=19,
)

ml = SkopeRulesClassifier(precision_min=0.1, n_estimators=30, max_features=None, max_depth=None, bootstrap=True, bootstrap_features=True, random_state=1)
ml_params = MLParams(
    H_split=0.20,
    L_split=0.20,
    ml_model=ml,
    instantiation_factor=10,
    generation_lookback=0,
    ancestral_recall=0,
    unique_only=True,
    iterations_per_cycle=1,
)

lemoo = LEMOO(problem, lem_params, ea_params, ml_params)

Run the XLEMOO method

We run our XLEMOO method for a set number of iterations in each mode. The printed output shows how many iterations were spent in each mode.

[4]:
lemoo.run_iterations()
[4]:
{'darwin_mode': 190, 'learning_mode': 10, 'total_iterations': 10}

Extract and print the rules from skope-rules and complete missing rules

Utilizing the trained XLEMOO model and the helper functions defined earlier, we can extract the rules in a human readable format.

[5]:
rules_for_vars = extract_rule_set(lemoo=lemoo, problem=problem, n_variables=n_variables)
rules_for_vars = complete_missing_rules(lemoo=lemoo, rules_for_vars=rules_for_vars)

print(f"Best solution x = {lemoo._generation_history[-1].individuals[0]}")
print(f"Best objective vector z = {lemoo._generation_history[-1].objectives_fitnesses[0]}")
print_rules(lemoo=lemoo, rules_for_vars=rules_for_vars, tex=False)
Best solution x = [1.00037555 2.99999995 1.         1.29893855 1.03798257]
Best objective vector z = [1.66883018e+03 7.53444094e+00 8.31048744e-02]
RULES:
Var     Lower(R)                Upper(R)                Lower(P)                Upper(P)
X_0     1.00037 (-1)            1.14978 (1.0)           1.00037                 1.00038
X_1     2.89008 (1.0)           3.0 (-1)                3.0                     3.0
X_2     1.0     (-1)            1.00001 (1.0)           1.0                     1.0
X_3     1.27361 (1.0)           1.42884 (1.0)           1.29894                 1.29894
X_4     1.02803 (1.0)           1.05154 (1.0)           1.03798                 1.03798

Modify variables and explore a new solution

We may then modify the variables to explore new solutions.

[6]:
x_new = np.atleast_2d([1.00184, 2.7135, 1.00000, 1.21741, 1.02602])
z_new = problem.evaluate(x_new).objectives
print(z_new)
[[1.66748556e+03 7.54345501e+00 9.49552335e-02]]

Or we may specify a new reference point and run the XLEMOO method again.