|
| 1 | +import numpy as np |
| 2 | +from pints import PopulationBasedOptimiser |
| 3 | + |
| 4 | + |
| 5 | +class RandomSearchImpl(PopulationBasedOptimiser): |
| 6 | + """ |
| 7 | + Random Search (RS) optimisation algorithm. |
| 8 | + This algorithm explores the parameter space by randomly sampling points. |
| 9 | +
|
| 10 | + The algorithm does the following: |
| 11 | + 1. Initialise a population of solutions. |
| 12 | + 2. At each iteration, generate `n` number of random positions within boundaries. |
| 13 | + 3. Evaluate the quality/fitness of the positions. |
| 14 | + 4. Replace the best position with improved position if found. |
| 15 | +
|
| 16 | + Parameters: |
| 17 | + population_size (optional): Number of solutions to evaluate per iteration. |
| 18 | +
|
| 19 | + References: |
| 20 | + The Random Search algorithm implemented in this work is based on principles outlined |
| 21 | + in "Introduction to Stochastic Search and Optimization: Estimation, Simulation, and |
| 22 | + Control" by Spall, J. C. (2003). |
| 23 | +
|
| 24 | + The implementation inherits from the PINTS PopulationOptimiser. |
| 25 | + """ |
| 26 | + |
| 27 | + def __init__(self, x0, sigma0=0.05, boundaries=None): |
| 28 | + super().__init__(x0, sigma0, boundaries=boundaries) |
| 29 | + |
| 30 | + # Problem dimensionality |
| 31 | + self._dim = len(x0) |
| 32 | + |
| 33 | + # Initialise best solution |
| 34 | + self._x_best = np.copy(x0) |
| 35 | + self._f_best = np.inf |
| 36 | + self._running = False |
| 37 | + self._ready_for_tell = False |
| 38 | + |
| 39 | + def ask(self): |
| 40 | + """ |
| 41 | + Returns a list of positions to evaluate in the optimiser-space. |
| 42 | + """ |
| 43 | + self._ready_for_tell = True |
| 44 | + self._running = True |
| 45 | + |
| 46 | + # Generate random solutions |
| 47 | + if self._boundaries: |
| 48 | + self._candidates = np.random.uniform( |
| 49 | + low=self._boundaries.lower(), |
| 50 | + high=self._boundaries.upper(), |
| 51 | + size=(self._population_size, self._dim), |
| 52 | + ) |
| 53 | + return self._candidates |
| 54 | + |
| 55 | + self._candidates = np.random.normal( |
| 56 | + self._x0, self._sigma0, size=(self._population_size, self._dim) |
| 57 | + ) |
| 58 | + return self.clip_candidates(self._candidates) |
| 59 | + |
| 60 | + def tell(self, replies): |
| 61 | + """ |
| 62 | + Receives a list of cost function values from points previously specified |
| 63 | + by `self.ask()`, and updates the optimiser state accordingly. |
| 64 | + """ |
| 65 | + if not self._ready_for_tell: |
| 66 | + raise RuntimeError("ask() must be called before tell().") |
| 67 | + |
| 68 | + # Evaluate solutions and update the best |
| 69 | + for i in range(self._population_size): |
| 70 | + f_new = replies[i] |
| 71 | + if f_new < self._f_best: |
| 72 | + self._f_best = f_new |
| 73 | + self._x_best = self._candidates[i] |
| 74 | + |
| 75 | + def running(self): |
| 76 | + """ |
| 77 | + Returns ``True`` if the optimisation is in progress. |
| 78 | + """ |
| 79 | + return self._running |
| 80 | + |
| 81 | + def x_best(self): |
| 82 | + """ |
| 83 | + Returns the best parameter values found so far. |
| 84 | + """ |
| 85 | + return self._x_best |
| 86 | + |
| 87 | + def f_best(self): |
| 88 | + """ |
| 89 | + Returns the best score found so far. |
| 90 | + """ |
| 91 | + return self._f_best |
| 92 | + |
| 93 | + def name(self): |
| 94 | + """ |
| 95 | + Returns the name of the optimiser. |
| 96 | + """ |
| 97 | + return "Random Search" |
| 98 | + |
| 99 | + def clip_candidates(self, x): |
| 100 | + """ |
| 101 | + Clip the input array to the boundaries if available. |
| 102 | + """ |
| 103 | + if self._boundaries: |
| 104 | + x = np.clip(x, self._boundaries.lower(), self._boundaries.upper()) |
| 105 | + return x |
| 106 | + |
| 107 | + def _suggested_population_size(self): |
| 108 | + """ |
| 109 | + Returns a suggested population size based on the dimension of the parameter space. |
| 110 | + """ |
| 111 | + return 4 + int(3 * np.log(self._n_parameters)) |
0 commit comments