Source code for optilab.metamodels.approximate_ranking_metamodel

"""
Approximate ranking metamodel based on lmm-CMA-ES.
"""

import copy

from ..data_classes import PointList
from ..functions import ObjectiveFunction
from ..functions.surrogate import SurrogateObjectiveFunction


[docs] class ApproximateRankingMetamodel: """Approximate ranking metamodel based on lmm-CMA-ES"""
[docs] def __init__( self, population_size: int, mu: int, objective_function: ObjectiveFunction, surrogate_function: SurrogateObjectiveFunction, *, buffer_size: int | None = None, ) -> None: """ Class constructor. Args: population_size: The population size (lambda). mu: The number of points compared in the ranking procedure (mu). objective_function: The objective function that's being optimized. surrogate_function: Surrogate function used to estimate the optimized function. buffer_size: Number of last evaluated samples to use for training the surrogate function. If None, all samples are used. """ self.population_size = population_size self.mu = mu self.n_init: int self.n_step: int self.init_n() self.train_set = PointList(points=[]) self.objective_function = objective_function self.surrogate_function = surrogate_function self.buffer_size = buffer_size
[docs] def init_n(self) -> None: """ Initializes n_init and n_step values based on the population size. """ self.n_init = self.population_size self.n_step = max(1, self.population_size // 10)
[docs] def _update_n(self, num_iters: int) -> None: """ Updates n_init and n_step values base on the number of algorithm iterations. Args: num_iters: The number of iterations done by the algorithm. """ if num_iters > 2: self.n_init = min( self.n_init + self.n_step, self.population_size - self.n_step ) elif num_iters < 2: self.n_init = max(self.n_step, self.n_init - self.n_step)
[docs] def __call__(self, points: PointList) -> PointList: """ Approximates the values of provided points with surrogate objective function. Args: points: List of points to evaluate. Returns: List of evaluated points. """ return PointList(points=[self.surrogate_function(x) for x in points])
[docs] def train_surrogate(self) -> None: """ Retrain the surrogate function with samples from the training set. """ if self.buffer_size: self.surrogate_function.train( PointList(self.train_set[-self.buffer_size :]) ) else: self.surrogate_function.train(self.train_set)
[docs] def evaluate(self, xs: PointList) -> PointList: """ Evaluate provided point with the objective function and append results to the training set and retrain the surrogate function on new training data. Args: xs: List of point to evaluate with the objective function. Returns: List of evaluated points. """ result = PointList( points=[self.objective_function(point) for point in xs.points] ) self.train_set.extend(result) self.train_surrogate() return result
[docs] def get_log(self) -> PointList: """ Get the list of points evaluated by the objective function. Returns: List of points evaluated by the objective function. """ return self.train_set
[docs] def adapt(self, xs: PointList) -> None: """ Perform another loop of the optimization on new data. Args: xs: Solution candidates generated by the optimizer. Raises: ValueError: When number of provided points mismatches the expected input size. """ if not len(xs) == self.population_size: raise ValueError( f"The number of provided points is different than expected." f"Expected {self.population_size}, got {len(xs)}." ) if len(self.train_set) < self.population_size: self.evaluate(xs) return # points not evaluated in this run not_evaluated = copy.deepcopy(xs) # points from current and previous loop items_previous = None items_current = self(xs) # rank items items_current.rank() # evaluate first n_init items not_evaluated = self(not_evaluated) not_evaluated.rank() self.evaluate(not_evaluated[: self.n_init]) not_evaluated = not_evaluated[self.n_init :] num_iter = 0 for _ in range((self.population_size - self.n_init) // self.n_step): # start new loop num_iter += 1 items_previous = items_current items_current = self(xs) # rank items items_current.rank() # check if the mu ranking changed if all( ( new_pt == pt for new_pt, pt in zip( items_previous[: self.mu], items_current[: self.mu], ) ) ): break # else evaluate n_step next items not_evaluated = self(not_evaluated) not_evaluated.rank() self.evaluate(not_evaluated[: self.n_step]) not_evaluated = not_evaluated[self.n_step :] self._update_n(num_iter)