Source code for piglot.optimisers.random_search

"""Pure random search optimiser module."""
from typing import Tuple, Callable, Optional
import numpy as np
from scipy.stats import norm, qmc
from piglot.objective import Objective
from piglot.optimiser import ScalarOptimiser


[docs] class PureRandomSearch(ScalarOptimiser): """ Pure Random Search optimiser. Three sampling methods for generating random numbers are available: - Uniform distribution. - Normal distribution, centered around the best value, and with decreasing standard deviation throughout the iterative process. - Sampling based on the Sobol sequence (requires scipy >= 1.7). Methods ------- _optimise(self, func, n_dim, n_iter, bound, init_shot): Solves the optimization problem """ def __init__(self, objective: Objective, sampling='uniform', seed=1): """ Constructs all the necessary attributes for the PRS optimiser. Parameters ---------- objective : Objective Objective function to optimise. sampling : str Sampling method to use for the random values. Valid options are: - 'uniform': Uniform distribution. - 'normal': Normal distribution, centered around the best value, and with decreasing standard deviation throughout the iterative process. - 'sobol': Sampling based on the Sobol sequence (requires scipy >= 1.7). """ super().__init__('PRS', objective) # Check if sampling method is valid valid_samples = ['uniform', 'normal', 'sobol'] if sampling not in valid_samples: raise ValueError(f"Invalid sampling {sampling}!") # Store parameters self.sampling = sampling 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. """ # Define lower and upper bounds lower_bounds = bound[:, 0] upper_bounds = bound[:, 1] # Compute loss function value given the initial shot best_solution = init_shot best_value = objective(best_solution) # Check convergence for the initial shot if self._progress_check(0, best_value, best_solution): return best_solution, best_value # Prepare random number generators if self.sampling == 'uniform': sampler = np.random.default_rng(self.seed) elif self.sampling == 'sobol': sampler = qmc.Sobol(n_dim, seed=self.seed) # Optimization problem iterative procedure for i in range(n_iter): # Evaluate next random point if self.sampling == 'uniform': new_solution = lower_bounds + sampler.random(n_dim) \ * (upper_bounds - lower_bounds) elif self.sampling == 'normal': sigma = (upper_bounds - lower_bounds) / (2 + (100.0 * (i + 1)) / n_iter) new_solution = norm.rvs(loc=best_solution, scale=sigma, size=n_dim, random_state=i) new_solution = np.where(new_solution > bound[:, 1], bound[:, 1], new_solution) new_solution = np.where(new_solution < bound[:, 0], bound[:, 0], new_solution) elif self.sampling == 'sobol': new_solution = lower_bounds + np.squeeze(sampler.random()) \ * (upper_bounds - lower_bounds) # Compute function value and update best solution new_value = objective(new_solution) if new_value < best_value: best_solution = new_solution best_value = new_value # Update progress and check convergence if self._progress_check(i+1, new_value, new_solution): break return best_solution, best_value