I have been working with the mealpy
Python library for quite some time– the repository of around 200 metaheuristic algorithms including GA, DE, ACO, PSO, …authored by Nguyen Van Thieu, alias thieu1995
.
However, working with discrete variables (particularly categoric variables in the form of strings, or integer variables) remains a nightmare. Variables have to be encoded, then decoded again in the fitness function, or outside– which are prone to cause several anomalies and vulnerabilities, especially when there are some dependent variables in the model, alongside discrete constraints or ranges.
Same (though somewhat less painful) for integer variables, which have to be rounded off or truncated everytime a fitness function is calculated, and once again when the best solution is presented.
Is it possible to encode integer or categoric variables with mealpy
?
Till October 2023
mealpy
version 2.x.y
had its “problem_dictionary” defined as :
problem_dict = {
"fit_func": fitness_function,
"lb": lb,
"ub": ub,
"minmax": "min",
"log_to": None,
}
where lower and upper bounds lb
and ub
had to be mandatorily specified as floating point (real-valued) ranges.
New in November 2023
mealpy
version 3.0.0
onward, there are options to define several new variable types, denoted in :
FloatVar
,IntegerVar
,StringVar
,BoolVar
,PermutationVar
,BinaryVar
, andMixedSetVar
classes
Here’s the code to try them out on three simple 3-varaible problems, where :
- all variables are
int
, - all variables are
str
, and - one is
int
, another isstr
, while the third isfloat
.
""" More at : https://github.com/thieu1995/mealpy/blob/master/mealpy/utils/space.py """
import numpy as np
from mealpy import IntegerVar, StringVar, MixedSetVar
from mealpy import GA
class Optimize_With_String_Variable():
def __init__(self, number_of_variables=3):
self.number_of_variables = number_of_variables
self.one_valid_set = ['1', '3', '5', '7', '9', '11', '13']
self.bounds = StringVar(valid_sets=[self.one_valid_set]*number_of_variables)
self.problem_dict = {
"obj_func": self.objective_func,
"bounds": self.bounds,
"minmax": "min",
"log_to": "file",
"log_file": "ga_results.txt"
}
def objective_func(self, solution):
decoded_solution = self.bounds.decode(solution)
int_sol = [int(n) for n in decoded_solution]
result = np.sum((np.array(int_sol) - 3)**2)
return result
def optimize(self):
optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
optimizer.solve(self.problem_dict)
best_solution = optimizer.g_best.solution
best_solution = self.bounds.decode(best_solution)
best_fitness = optimizer.g_best.target.fitness
return best_solution, best_fitness
class Optimize_With_Integer_Variable():
def __init__(self, number_of_variables=3):
self.number_of_variables = number_of_variables
self.bounds = IntegerVar(lb=[1,]*number_of_variables, ub=[13,]*number_of_variables)
self.problem_dict = {
"obj_func": self.objective_func,
"bounds": self.bounds,
"minmax": "min",
"log_to": "file",
"log_file": "ga_results.txt"
}
def objective_func(self, solution):
result = np.sum((np.array(solution) - 3)**2)
return result
def optimize(self):
optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
optimizer.solve(self.problem_dict)
best_solution = optimizer.g_best.solution
best_fitness = optimizer.g_best.target.fitness
return best_solution, best_fitness
class Optimize_With_Mixed_Set_Variable():
def __init__(self, number_of_variables=3):
self.number_of_variables = number_of_variables
## string variable
first_valid_set = ['a', 'c', 'e', 'g', 'i', 'k', 'm']
## integer variable
second_valid_set = [1, 3, 5, 7, 9, 11, 13]
## float variable (encoded as pesudo-float with 2-digit precision)
third_valid_set = np.linspace(1, 13, 121)
self.bounds = MixedSetVar(valid_sets=[first_valid_set, second_valid_set, third_valid_set])
self.problem_dict = {
"obj_func": self.objective_func,
"bounds": self.bounds,
"minmax": "min",
"log_to": "file",
"log_file": "ga_results.txt"
}
def objective_func(self, solution):
def integer_from_mixed_type(x):
"""
if `x` is `int` or `float`:
return `x`,
elif `x` is `char`:
return `ord(x) - 96`
[assume `"a" = 1`] """
if isinstance(x, int):
return x
elif isinstance(x, float):
return x
elif isinstance(x, str):
return ord(x) - 96
else:
raise Exception('solution element', x, 'is none of int/float/str')
decoded_solution = self.bounds.decode(solution)
result = np.sum((np.array([integer_from_mixed_type(x) for x in decoded_solution]) - 3)**2)
return result
def optimize(self):
optimizer = GA.BaseGA(epoch=100, pop_size=50, pc=0.85, pm=0.1)
optimizer.solve(self.problem_dict)
best_solution = optimizer.g_best.solution
best_solution = self.bounds.decode(best_solution)
best_fitness = optimizer.g_best.target.fitness
return best_solution, best_fitness
if __name__ == "__main__":
for op_problem in (
## string (categoric) variables
Optimize_With_String_Variable(3),
##
## integer (discrete) variables
Optimize_With_Integer_Variable(3),
##
## combination of string/int/float (all discrete) variables
Optimize_With_Mixed_Set_Variable(3),
):
print (op_problem.bounds.name)
best_solution, best_fitness = op_problem.optimize()
print ('Best Solution =', best_solution)
print ('Best Fitness=", best_fitness)
print ()
which produces the output :
string Best Solution = ["3', '3', '3'] Best Fitness = 0.0 integer Best Solution = [3. 3. 3.] Best Fitness = 0.0 mixed-set-var Best Solution = ['c', 3, 3.0] Best Fitness = 0.0
Caveat
Unfortunately, a real float
variable cannot be mixed with discrete variables in mealpy
still, and they will have to be coded as “pseudo-float” with a finite precision.
We can expect this feature in their future releases. But still, what’s here is precious enough !