Source code for piglot.optimisers.spsa

"""SPSA optimiser module."""
from typing import Tuple, Callable, Optional
import numpy as np
from scipy.stats import bernoulli
from piglot.objective import Objective
from piglot.optimiser import ScalarOptimiser, boundary_check


[docs] class SPSA(ScalarOptimiser): """ Simultaneous Perturbation Stochastic Approximation method for optimisation. Reference: https://ieeexplore.ieee.org/document/705889 Methods ------- _optimise(self, func, n_dim, n_iter, bound, init_shot): Solves the optimization problem """ def __init__(self, objective: Objective, alpha=0.602, gamma=0.101, prob=0.5, seed=1, A=None, a=None, c=None): """Constructs all necessary attributes for the SPSA optimiser. Parameters ---------- objective : Objective Objective function to optimise. alpha : float, optional Model parameter, refer to documentation, by default 0.602 gamma : float, optional Model parameter, refer to documentation, by default 0.101 prob : float, optional Model parameter, refer to documentation, by default 0.5 seed : int, optional Random number generator seed, by default 1 A : float, optional Model parameter, refer to documentation, by default None. If None, this parameter is defined according to internal heuristics. a : float, optional Model parameter, refer to documentation, by default None If None, this parameter is defined according to internal heuristics. c : float, optional Model parameter, refer to documentation, by default None If None, this parameter is defined according to internal heuristics. """ super().__init__('SPSA', objective) self.alpha = alpha self.gamma = gamma self.prob = prob self.seed = seed self.A = A self.a = a self.c = 1e-6 if c is None else c 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. """ if self.A is None: self.A = n_iter / 20 if self.a is None: self.a = 2 * (self.A + 1)**self.alpha x = init_shot new_value = objective(x) if self._progress_check(0, new_value, x): return x, new_value for i in range(0, n_iter): a_k = self.a / (self.A + i + 1) ** self.alpha c_k = self.c / (i + 1) ** self.gamma # [-1,1] Bernoulli distribution delta = 2 * bernoulli.rvs(self.prob, size=n_dim, random_state=self.seed + i) - 1 # Bound check up = boundary_check(x + c_k * delta, bound) low = boundary_check(x - c_k * delta, bound) pos_loss = objective(up) neg_loss = objective(low) gradient = (pos_loss - neg_loss) / (up - low) # Update solution x = x - a_k * gradient # Bound check x = boundary_check(x, bound) new_value = objective(x) # Update progress and check convergence if self._progress_check(i+1, new_value, x): break return x, new_value