|
1 | 1 | import numpy as np
|
2 |
| -from typing import Callable, List |
| 2 | +import warnings |
| 3 | +from typing import Union, List, Iterable |
3 | 4 | from . import Optimizer, linmap
|
4 | 5 |
|
5 | 6 | def sigmoid(x):
|
6 | 7 | return 1 / (1 + np.exp(-x))
|
7 | 8 | def logit(x, inf=100):
|
8 | 9 | return np.where(x==0, -inf, np.where(x==1, inf, np.log(x) - np.log(1-x)))
|
| 10 | +def softmax(x): |
| 11 | + assert x.ndim == 2, "Softmax only implemented for 2D arrays" |
| 12 | + return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True) |
9 | 13 |
|
10 |
| -class NelderMead(Optimizer): |
| 14 | +class NelderMeadBounded(Optimizer): |
11 | 15 | def __init__(self, population_size: int, ranges: List[float], rng_seed: int = 0, hypercube_radius = 100):
|
12 | 16 | """
|
13 | 17 | This Nelder-Mead algorithm assumes that the optimization function 'f' is to be minimized and time independent.
|
14 | 18 |
|
| 19 | + The space is assumed to be a hypercube, and the algorithm will map the hypercube to the unit hypercube, bounded by a sigmoid function. |
| 20 | +
|
15 | 21 | ranges list of pairs:
|
16 | 22 | Maxima and minima that each parameter can cover.
|
17 | 23 | Example: [(0, 10), (0, 10)] for two parameters ranging from 0 to 10.
|
@@ -59,6 +65,126 @@ def ask_oracle(self, X: np.ndarray) -> np.ndarray:
|
59 | 65 | def init_oracle(self):
|
60 | 66 | return self.ask_oracle(self.view_g())
|
61 | 67 |
|
| 68 | + def step(self): |
| 69 | + """ |
| 70 | + This function performs a single step of the Nelder-Mead algorithm. |
| 71 | + """ |
| 72 | + |
| 73 | + # Sort the population by the value of the oracle |
| 74 | + y = self.ask_oracle(self.view_g()) |
| 75 | + idx = np.argsort(y) |
| 76 | + self.population = self.population[idx] |
| 77 | + |
| 78 | + # Compute the centroid of the population |
| 79 | + centroid = self.population[:-1].mean(axis=0) |
| 80 | + |
| 81 | + # Reflect the worst point through the centroid |
| 82 | + reflected = centroid + (centroid - self.population[-1]) |
| 83 | + |
| 84 | + # Evaluate the reflected point |
| 85 | + |
| 86 | + y_reflected = self.ask_oracle(self.view(reflected.reshape(1,-1))) |
| 87 | + |
| 88 | + # If the reflected point is better than the second worst, but not better than the best, then expand |
| 89 | + |
| 90 | + if y_reflected < y[-2] and y_reflected > y[0]: |
| 91 | + expanded = centroid + (reflected - centroid) |
| 92 | + y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) |
| 93 | + if y_expanded < y_reflected: |
| 94 | + self.population[-1] = expanded |
| 95 | + else: |
| 96 | + self.population[-1] = reflected |
| 97 | + # If the reflected point is better than the best, then expand |
| 98 | + elif y_reflected < y[0]: |
| 99 | + expanded = centroid + 2 * (reflected - centroid) |
| 100 | + y_expanded = self.ask_oracle(self.view(expanded.reshape(1,-1))) |
| 101 | + if y_expanded < y_reflected: |
| 102 | + self.population[-1] = expanded |
| 103 | + else: |
| 104 | + self.population[-1] = reflected |
| 105 | + # If the reflected point is worse than the second worst, then contract |
| 106 | + elif y_reflected > y[-2]: |
| 107 | + contracted = centroid + 0.5 * (self.population[-1] - centroid) |
| 108 | + y_contracted = self.ask_oracle(self.view(contracted.reshape(1,-1))) |
| 109 | + if y_contracted < y[-1]: |
| 110 | + self.population[-1] = contracted |
| 111 | + else: |
| 112 | + for i in range(1, len(self.population)): |
| 113 | + self.population[i] = 0.5 * (self.population[i] + self.population[0]) |
| 114 | + # If the reflected point is worse than the worst, then shrink |
| 115 | + elif y_reflected > y[-1]: |
| 116 | + for i in range(1, len(self.population)): |
| 117 | + self.population[i] = 0.5 * (self.population[i] + self.population[0]) |
| 118 | + |
| 119 | + self.y = y.copy() |
| 120 | + return self.view_g() |
| 121 | + |
| 122 | +class NelderMeadConstant(Optimizer): |
| 123 | + def __init__(self, population_size: int, ranges: Union[int, Iterable], rng_seed: int = 0, energy=10): |
| 124 | + """ |
| 125 | + This Nelder-Mead algorithm assumes that the optimization function 'f' is to be minimized and time independent. |
| 126 | +
|
| 127 | + The parameters are constrained so that their sum always add up to `energy` though a softmax function. |
| 128 | +
|
| 129 | + Parameters: |
| 130 | + - population_size (int): The size of the population. |
| 131 | + - ranges (int or list): The number of parameters or a list of ranges for each parameter. |
| 132 | + - rng_seed (int): The seed for the random number generator. |
| 133 | + - energy (float): The energy parameter used in the optimization. |
| 134 | +
|
| 135 | + Note: The ranges parameter is merely a placeholder. The ranges are set to (0, energy) for all parameters. |
| 136 | + """ |
| 137 | + self.population_size = population_size |
| 138 | + if isinstance(ranges, Iterable): |
| 139 | + self.ranges = np.array([(0, energy) for _ in range(len(ranges))]) |
| 140 | + warnings.warn("While using Nelder-MeadConstant, the ranges are set to (0, energy) for all parameters. The parameter `ranges` is merely a placeholder.") |
| 141 | + elif isinstance(ranges, int): |
| 142 | + self.ranges = np.array([(0, energy) for _ in range(ranges)]) |
| 143 | + self.rng_seed = np.random.default_rng(rng_seed) |
| 144 | + |
| 145 | + # Initialize the population (random position and initial momentum) |
| 146 | + self.population = self.rng_seed.random((self.population_size, len(self.ranges))) |
| 147 | + |
| 148 | + # Derived attributes |
| 149 | + self.energy = energy |
| 150 | + self.e_summation = 1 |
| 151 | + # Initialize y as vector of nans |
| 152 | + self.y = np.full(self.population_size, np.nan) |
| 153 | + |
| 154 | + def forward(self, x): |
| 155 | + self.e_summation = np.sum(np.exp(x), axis=1, keepdims=True) |
| 156 | + return self.energy * softmax(x) |
| 157 | + |
| 158 | + def backward(self, x): |
| 159 | + """ |
| 160 | + Softmax is not injective. This is a pseudo-inverse. |
| 161 | + """ |
| 162 | + return np.log(self.e_summation * x/self.energy) |
| 163 | + |
| 164 | + def view(self, x): |
| 165 | + """ |
| 166 | + Maps the input from the domain to the codomain. |
| 167 | + """ |
| 168 | + return self.forward(x) |
| 169 | + |
| 170 | + def view_g(self): |
| 171 | + """ |
| 172 | + Maps the input from the domain to the codomain. |
| 173 | + """ |
| 174 | + return self.forward(self.population) |
| 175 | + |
| 176 | + def inverse_view(self, x): |
| 177 | + """ |
| 178 | + Maps the input from the codomain to the domain. |
| 179 | + """ |
| 180 | + return self.backward(x) |
| 181 | + |
| 182 | + def ask_oracle(self, X: np.ndarray) -> np.ndarray: |
| 183 | + return super().ask_oracle() |
| 184 | + |
| 185 | + def init_oracle(self): |
| 186 | + return self.ask_oracle(self.view_g()) |
| 187 | + |
62 | 188 | def step(self):
|
63 | 189 | """
|
64 | 190 | This function performs a single step of the Nelder-Mead algorithm.
|
|
0 commit comments