Source code for piglot.optimisers.lipo_opt

"""LIPO optimiser module."""
from typing import Tuple, Callable, Optional
import numpy as np
try:
    from lipo import GlobalOptimizer
except ImportError:
    # Show a nice exception when this package is used
    from piglot.optimiser import missing_method
    GlobalOptimizer = missing_method("LIPO", "lipo")
from piglot.objective import Objective
from piglot.optimiser import ScalarOptimiser


[docs] class GlobalOptimizerMod(GlobalOptimizer):
[docs] def run(self, optimiser, num_function_calls: int = 1): """Run optimization. Parameters ---------- num_function_calls : int Number of function calls """ for i in range(num_function_calls): candidate = self.get_candidate() candidate.set(self.function(**candidate.x)) solution = self.evaluations[i] if optimiser._progress_check(i+1, -solution[1], list(solution[0].values())): break
[docs] class LIPO(ScalarOptimiser): """ LIPO optimiser. Documentation: https://github.com/jdb78/lipo http://blog.dlib.net/2017/12/a-global-optimization-algorithm-worth.html Attributes ---------- log_args : list[str] list of arguments to treat in log space, if "auto", then a variable is optimized in log space if (default = 'auto'): - The lower bound on the variable is > 0 - The ratio of the upper bound to lower bound is > 1000 - The variable is not an integer variable flexible_bounds : dict[str, list[bool]] dictionary of parameters and list of booleans indicating if parameters are deemed flexible or not. By default all parameters are deemed flexible but only if `flexible_bound_threshold > 0` (default = {}). flexible_bound_threshold : float enlarge bounds if optimum is top or bottom flexible_bound_threshold quantile (default = -1.0) epsilon : float accuracy below which exploration will be priorities vs exploitation (default = 0) random_state : int random state Methods ------- _optimise(self, func, n_dim, n_iter, bound, init_shot): Solves the optimization problem """ def __init__(self, objective: Objective, log_args='auto', flexible_bounds={}, flexible_bound_threshold=-1.0, epsilon=0.0, seed=None): """ Constructs all the necessary attributes for the LIPO optimiser Parameters ---------- objective : Objective Objective function to optimise. log_args : list[str] list of arguments to treat in log space, if "auto", then a variable is optimized in log space if (default = 'auto'): - The lower bound on the variable is > 0 - The ratio of the upper bound to lower bound is > 1000 - The variable is not an integer variable flexible_bounds : dict[str, list[bool]] dictionary of parameters and list of booleans indicating if parameters are deemed flexible or not. By default all parameters are deemed flexible but only if `flexible_bound_threshold > 0` (default = {}). flexible_bound_threshold : float enlarge bounds if optimum is top or bottom flexible_bound_threshold quantile (default = -1.0) epsilon : float accuracy below which exploration will be prioritized vs exploitation (default = 0) seed : int random state """ super().__init__('LIPO', objective) self.log_args = log_args self.flexible_bounds = flexible_bounds self.flexible_bound_threshold = flexible_bound_threshold self.flexible_bound_threshold = flexible_bound_threshold self.epsilon = epsilon self.seed = seed def _scalar_optimise( self, objective: Callable[[np.ndarray, Optional[bool]], float], n_dim: int, n_iter: int, bound: np.ndarray, init_shot: np.ndarray, ) -> Tuple[float, np.ndarray]: """ Abstract method for optimising the objective. Parameters ---------- objective : Callable[[np.ndarray], float] Objective function to optimise. n_dim : int Number of parameters to optimise. n_iter : int Maximum number of iterations. bound : np.ndarray Array where first and second columns correspond to lower and upper bounds, respectively. init_shot : np.ndarray Initial shot for the optimisation problem. Returns ------- float Best observed objective value. np.ndarray Observed optimum of the objective. """ # Set optimization problem as a maximization one maximize = True # Convert the optimization problem to a minimization problem by negating the # optimization function def negate(**kwargs): return -objective(list(kwargs.values())) # Convert the bounds in array type to dicitionary type, as required in the # GlobalOptimizer documentation lower_bounds = {} upper_bounds = {} patterns = [par.name for par in self.parameters] if bound is not None: lower_bounds = dict(zip(patterns, bound[:, 0])) upper_bounds = dict(zip(patterns, bound[:, 1])) # Compute loss function value given the initial shot evaluations = [] if init_shot is not None: pre_eval_x = dict(zip(patterns, init_shot)) evaluations = [(pre_eval_x, negate(**pre_eval_x))] # Run optimization problem categories = {} model = GlobalOptimizerMod(negate, lower_bounds, upper_bounds, categories, self.log_args, self.flexible_bounds, self.flexible_bound_threshold, evaluations, maximize, self.epsilon, self.seed) model.run(self, n_iter) return list(model.optimum[0].values()), model.optimum[1]