diff --git a/docs/optilab.optimizers.rst b/docs/optilab.optimizers.rst index b5aead8..f1826b9 100644 --- a/docs/optilab.optimizers.rst +++ b/docs/optilab.optimizers.rst @@ -4,6 +4,30 @@ optilab.optimizers package Submodules ---------- +optilab.optimizers.cma\_es module +--------------------------------- + +.. automodule:: optilab.optimizers.cma_es + :members: + :undoc-members: + :show-inheritance: + +optilab.optimizers.knn\_cma\_es module +-------------------------------------- + +.. automodule:: optilab.optimizers.knn_cma_es + :members: + :undoc-members: + :show-inheritance: + +optilab.optimizers.lmm\_cma\_es module +-------------------------------------- + +.. automodule:: optilab.optimizers.lmm_cma_es + :members: + :undoc-members: + :show-inheritance: + optilab.optimizers.optimizer module ----------------------------------- diff --git a/experiments/002_cmaes_knn_metamodel/cmaes_variations.py b/experiments/002_cmaes_knn_metamodel/cmaes_variations.py deleted file mode 100644 index 00a2ec1..0000000 --- a/experiments/002_cmaes_knn_metamodel/cmaes_variations.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Variations of CMA-ES encapsulated in easy to call functions. -""" - -# pylint: disable=too-many-arguments, too-many-locals -import cma - -from optilab.data_classes import Bounds, PointList -from optilab.functions import ObjectiveFunction -from optilab.functions.surrogate import ( - LocallyWeightedPolynomialRegression, - SurrogateObjectiveFunction, - KNNSurrogateObjectiveFunction -) -from optilab.metamodels import ApproximateRankingMetamodel - - -def cma_es( - function: ObjectiveFunction, - population_size: int, - call_budget: int, - bounds: Bounds, - *, - sigma0: float = 1, - target: float = 0.0, - tolerance: float = 1e-8, -) -> PointList: - """ - Run optimization with regular CMA-ES. - """ - - x0 = bounds.random_point(function.dim).x - - res_log = PointList(points=[]) - - es = cma.CMAEvolutionStrategy( - x0, - sigma0, - { - "popsize": population_size, - "bounds": bounds.to_list(), - "maxfevals": call_budget, - "ftarget": target, - "verbose": -9, - "tolfun": tolerance, - }, - ) - - while not es.stop(): - solutions = PointList.from_list(es.ask()) - results = PointList(points=[function(x) for x in solutions.points]) - res_log.extend(results) - x, y = results.pairs() - es.tell(x, y) - - return res_log - - -def knn_cma_es( - function: ObjectiveFunction, - population_size: int, - call_budget: int, - bounds: Bounds, - *, - sigma0: float = 1, - target: float = 0.0, - tolerance: float = 1e-8, - num_neighbors: int = 5, -) -> PointList: - """ - Run optimization with KNN-CMA-ES - """ - - metamodel = ApproximateRankingMetamodel( - population_size, - population_size // 2, - function, - KNNSurrogateObjectiveFunction(num_neighbors), - ) - - x0 = bounds.random_point(function.dim).x - - es = cma.CMAEvolutionStrategy( - x0, - sigma0, - { - "popsize": population_size, - "bounds": bounds.to_list(), - "maxfevals": call_budget, - "ftarget": target, - "verbose": -9, - "tolfun": tolerance, - }, - ) - - while ( - metamodel.get_log().best_y() > tolerance and len(metamodel.get_log()) <= call_budget - ): - solutions = PointList.from_list(es.ask()) - - if len(metamodel.train_set) < num_neighbors: - xy_pairs = metamodel.evaluate(solutions) - else: - metamodel.adapt(solutions) - xy_pairs = metamodel(solutions) - - x, y = xy_pairs.pairs() - es.tell(x, y) - - return metamodel.get_log() diff --git a/experiments/002_cmaes_knn_metamodel/main.py b/experiments/002_cmaes_knn_metamodel/main.py index 6c87c8e..a4fb795 100644 --- a/experiments/002_cmaes_knn_metamodel/main.py +++ b/experiments/002_cmaes_knn_metamodel/main.py @@ -4,74 +4,37 @@ # pylint: disable=import-error -from cmaes_variations import cma_es, knn_cma_es -from tqdm import tqdm import numpy as np from optilab.data_classes import Bounds from optilab.functions.unimodal import SphereFunction -from optilab.functions.multimodal import RosenbrockFunction from optilab.plotting import plot_ecdf_curves -from optilab.data_classes import OptimizationRun, OptimizerMetadata from optilab.utils import dump_to_pickle +from optilab.optimizers import CmaEs, KnnCmaEs if __name__ == "__main__": # hyperparams: - DIM = 10 + DIM = 2 POPSIZE = DIM * 4 NUM_NEIGHBORS = POPSIZE * 5 NUM_RUNS = 51 CALL_BUDGET = 1e4 * DIM TOL = 1e-8 + SIGMA0 = 1 # optimized problem BOUNDS = Bounds(-100, 100) FUNC = SphereFunction(DIM) - # perform optimization with vanilla cmaes - cmaes_logs = [ - cma_es(FUNC, POPSIZE, CALL_BUDGET, BOUNDS, tolerance=TOL) - for _ in tqdm(range(NUM_RUNS), unit="run") - ] + cmaes_optimizer = CmaEs(POPSIZE, SIGMA0) + cmaes_results = cmaes_optimizer.run_optimization(NUM_RUNS, FUNC, BOUNDS, CALL_BUDGET, TOL) - cmaes_run = OptimizationRun( - model_metadata=OptimizerMetadata( - name='CMA_ES', - population_size=POPSIZE, - hyperparameters={ - 'sigma0': 1 - } - ), - function_metadata=FUNC.get_metadata(), - bounds=BOUNDS, - tolerance=TOL, - logs=cmaes_logs - ) - - # perform optimization with knn cmaes - knn_cmaes_logs = [ - knn_cma_es(FUNC, POPSIZE, CALL_BUDGET, BOUNDS, tolerance=TOL, num_neighbors=NUM_NEIGHBORS) - for _ in tqdm(range(NUM_RUNS), unit="run") - ] - - knn_cmaes_run = OptimizationRun( - model_metadata=OptimizerMetadata( - name='knn_CMA_ES', - population_size=POPSIZE, - hyperparameters={ - 'sigma0': 1, - 'num_neighbor': NUM_NEIGHBORS - } - ), - function_metadata=FUNC.get_metadata(), - bounds=BOUNDS, - tolerance=TOL, - logs=knn_cmaes_logs - ) + knn_optimizer = KnnCmaEs(POPSIZE, SIGMA0, NUM_NEIGHBORS) + knn_results = knn_optimizer.run_optimization(NUM_RUNS, FUNC, BOUNDS, CALL_BUDGET, TOL) # print stats - vanilla_times = [len(log) for log in cmaes_logs] - knn_times = [len(log) for log in knn_cmaes_logs] + vanilla_times = [len(log) for log in cmaes_results.logs] + knn_times = [len(log) for log in knn_results.logs] print(f'vanilla {np.average(vanilla_times)} {np.std(vanilla_times)}') print(f'knn {np.average(knn_times)} {np.std(knn_times)}') @@ -79,12 +42,12 @@ # plot results plot_ecdf_curves( { - "cma-es": cmaes_logs, - "knn-cma-es": knn_cmaes_logs, + "cma-es": cmaes_results.logs, + "knn-cma-es": knn_results.logs, }, n_dimensions=DIM, savepath=f"ecdf_{FUNC.name}_{DIM}.png", allowed_error=TOL ) - dump_to_pickle([cmaes_run, knn_cmaes_run], f'knn_reproduction_{FUNC.name}_{DIM}.pkl') + dump_to_pickle([cmaes_results, knn_results], f'knn_reproduction_{FUNC.name}_{DIM}.pkl') diff --git a/src/optilab/optimizers/__init__.py b/src/optilab/optimizers/__init__.py index 36bbc8e..5353e15 100644 --- a/src/optilab/optimizers/__init__.py +++ b/src/optilab/optimizers/__init__.py @@ -3,5 +3,6 @@ """ from .cma_es import CmaEs +from .knn_cma_es import KnnCmaEs from .lmm_cma_es import LmmCmaEs from .optimizer import Optimizer diff --git a/src/optilab/optimizers/knn_cma_es.py b/src/optilab/optimizers/knn_cma_es.py new file mode 100644 index 0000000..2fd7b6f --- /dev/null +++ b/src/optilab/optimizers/knn_cma_es.py @@ -0,0 +1,100 @@ +""" +KNN-CMA-ES optimizer. CMA-ES is enhanced with a KNN metamodel similar to the one from LMM-CMA-ES. +""" + +# pylint: disable=too-many-arguments, too-many-positional-arguments, duplicate-code + +import cma + +from ..data_classes import Bounds, PointList +from ..functions import ObjectiveFunction +from ..functions.surrogate import KNNSurrogateObjectiveFunction +from ..metamodels import ApproximateRankingMetamodel +from .optimizer import Optimizer + + +class KnnCmaEs(Optimizer): + """ + KNN-CMA-ES optimizer. CMA-ES is enhanced with a KNN metamodel similar + to the one from LMM-CMA-ES. + """ + + def __init__(self, population_size: int, sigma0: float, num_neighbors: int): + """ + Class constructor. + + Args: + population_size (int): Size of the population. + sigma0 (float): Starting value of the sigma, + num_neighbors (int): Number of neighbors used by KNN metamodel. + """ + super().__init__( + "knn-cma-es", + population_size, + {"sigma0": sigma0, "num_neighbors": num_neighbors}, + ) + + def optimize( + self, + function: ObjectiveFunction, + bounds: Bounds, + call_budget: int, + tolerance: float, + target: float = 0.0, + ) -> PointList: + """ + Run a single optimization of provided objective function. + + Args: + function (ObjectiveFunction): Objective function to optimize. + bounds (Bounds): Search space of the function. + call_budget (int): Max number of calls to the objective function. + tolerance (float): Tolerance of y value to count a solution as acceptable. + target (float): Objective function value target, default 0. + + Returns: + PointList: Results log from the optimization. + """ + metamodel = ApproximateRankingMetamodel( + self.metadata.population_size, + self.metadata.population_size // 2, + function, + KNNSurrogateObjectiveFunction( + self.metadata.hyperparameters["num_neighbors"] + ), + ) + + x0 = bounds.random_point(function.dim).x + + es = cma.CMAEvolutionStrategy( + x0, + self.metadata.hyperparameters["sigma0"], + { + "popsize": self.metadata.population_size, + "bounds": bounds.to_list(), + "maxfevals": call_budget, + "ftarget": target, + "verbose": -9, + "tolfun": tolerance, + }, + ) + + while ( + metamodel.get_log().best_y() > tolerance + and len(metamodel.get_log()) <= call_budget + ): + solutions = PointList.from_list(es.ask()) + + if ( + len(metamodel.train_set) + < self.metadata.hyperparameters["num_neighbors"] + ): + xy_pairs = metamodel.evaluate(solutions) + else: + metamodel.adapt(solutions) + xy_pairs = metamodel(solutions) + + x, y = xy_pairs.pairs() + es.tell(x, y) + + return metamodel.get_log()