1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
|
// SPDX-License-Identifier: LGPL-3.0-or-later
// Author: Kristian Lytje
#include <mini/MinimumExplorer.h>
#include <mini/detail/Parameter.h>
#include <mini/detail/FittedParameter.h>
#include <mini/detail/Evaluation.h>
#include <utility/Exceptions.h>
#include <utility/Utility.h>
using namespace ausaxs;
using namespace ausaxs::mini;
MinimumExplorer::MinimumExplorer(double(&func)(std::vector<double>), unsigned int evals) : Minimizer(func) {
set_max_evals(evals);
}
MinimumExplorer::MinimumExplorer(std::function<double(std::vector<double>)> func, unsigned int evals) : Minimizer(std::move(func)) {
set_max_evals(evals);
}
MinimumExplorer::MinimumExplorer(double(&func)(std::vector<double>), const Parameter& param, unsigned int evals) : Minimizer(func) {
set_max_evals(evals);
add_parameter(param);
}
MinimumExplorer::MinimumExplorer(std::function<double(std::vector<double>)> func, const Parameter& param, unsigned int evals) : Minimizer(std::move(func)) {
set_max_evals(evals);
add_parameter(param);
}
mini::Landscape MinimumExplorer::landscape(unsigned int evals) {
if (parameters.empty()) {throw except::bad_order("MinimumExplorer::landscape: No parameters were supplied.");}
if (!evaluations.evals.empty()) {return evaluations;} // if the minimizer has already been called, we can just reuse its result
const Parameter& param = parameters[0];
double xmin = *param.guess;
double xmid = xmin;
double x = xmin;
double fmin = function({xmin});
//###############################################################//
//### DETERMINE SPACING BETWEEN EVALUATIONS ###//
//###############################################################//
// we want to find the smallest spacing that still changes the function value
double spacing = 1e-5;
unsigned int vchanges = 0; // we want at least 2 changes for a decent estimate
unsigned int counter = 0; // counter to prevent infinite loop
double factor = 2; // step scaling factor
record_evaluations(false); // disable recording while determining spacing
// reset the spacing
auto reset_spacing = [&] () {
spacing = 1e-5;
if (param.has_bounds()) {
spacing = std::min(spacing, (param.bounds->max - param.bounds->min)/evals);
}
};
// update the spacing
double fprev = fmin;
auto update_spacing = [&] (double x) {
double f = function({x});
// check if the function value changed
if (1e-6 < std::abs(f - fprev)) {
vchanges++;
if (2 < vchanges) {return true;} // stop after fval changed twice
fprev = f;
factor = 1.3; // scale slower since we're close to the final step size
} else {
spacing *= factor;
}
counter++;
return false;
};
reset_spacing();
while (counter < 20) {
x += spacing; // move to the right
if (update_spacing(x)) {break;} // check if we have enough changes
x = xmid; // move back to the middle
}
// if counter is 20 we couldn't determine the spacing by going to the right, so we try again but now going left
if (counter == 20) {
counter = 0;
x = xmid;
reset_spacing();
while (counter < 20) {
x -= spacing; // move to the left
if (update_spacing(x)) {break;} // check if we have enough changes
x = xmid; // move back to the middle
}
}
// if counter is still 20 going left didn't work either. the function is probably ill-defined
if (counter == 20) {
throw except::bad_order("MinimumExplorer::landscape: Could not determine spacing for landscape.");
}
spacing /= 2; // step size is twice the distance between fval changes after ending the earlier loop
//###############################################################//
//### ESTIMATE MINIMUM VALUE ###//
//###############################################################//
record_evaluations(true); // start recording again
// go three steps to the left
x = xmid; // go back to the middle
fprev = fmin; // keep track of last value
counter = 0; // reset counter
unsigned int iter = 0; // keep track of how many iterations we've done
double start_space = spacing;
for (int i = 0; i < 4; i++) {
x -= spacing;
double f = function({x});
// check if the function value actually changed
if (std::abs(fprev - f) < 1e-6) {
// if not, refine the spacing and try again
iter++;
x += spacing;
spacing *= 1.3;
i--;
evaluations.evals.pop_back();
// if we've tried too many times going to the left doesn't work
if (iter == 10) {
spacing = start_space;
counter = 4; // mark left side as being finished
break;
}
}
// check if this is a new minimum
if (f < fmin) {
fmin = f;
xmin = x;
continue;
}
// check if this value is higher than the previous one
if (fprev < f) {
fprev = f;
counter++;
}
}
// if counter == 4 the function is monotonically increasing to the left, and we shouldn't explore it further
bool left = !(counter == 4);
// go three steps to the right
x = xmid; // go back to the middle
fprev = fmin; // reset fprev
counter = 0; // reset counter
iter = 0; // reset iter
start_space = spacing;
for (int i = 0; i < 4; i++) {
x += spacing;
double f = function({x});
// check if the function value actually changed
if (std::abs(fprev - f) < 1e-6) {
// if not, refine the spacing and try again
iter++;
x -= spacing;
spacing *= 1.3;
i--;
evaluations.evals.pop_back();
// if we've tried too many times going to the right doesn't work
if (iter == 10) {
spacing = start_space;
counter = 4; // mark right side as being finished
break;
}
}
// check if this is a new minimum
if (f < fmin) {
fmin = f;
xmin = x;
continue;
}
// check if this value is higher than the previous one
if (fprev < f) {
fprev = f;
counter++;
}
}
// if counter == 4 the function is monotonically increasing to the right, and we shouldn't explore it further
bool right = !(counter == 4);
// we now change tactics: instead of requiring 3 monotonic increases in fval before stopping, we now just want it to be higher than the mean four times in a row
auto points = get_evaluated_points().as_dataset();
double mu = points.mean();
if (right) {
// now go the remaining steps to the right, terminating if four consecutive evals are all above the mean
counter = 0;
x = xmid + 4*spacing; // start four steps to the right of the middle
unsigned int above = 0; // number of consecutive points higher than the mean
while (above < 4 && counter++ < (evals-9)/2) {
x += spacing;
double f = function({x});
if (f < fmin) {
fmin = f;
xmin = x;
}
if (mu < f) {
above++;
} else {
above = 0;
}
}
}
if (left) {
// repeat for left-steps
counter = 0;
unsigned int above = 0;
x = xmid - 4*spacing; // start four steps to the left of the middle
while (above < 4 && counter++ < (evals-9)/2) {
x -= spacing;
double f = function({x});
if (f < fmin) {
fmin = f;
xmin = x;
}
if (mu < f) {
above++;
} else {
above = 0;
}
}
}
return get_evaluated_points();
}
Result MinimumExplorer::minimize_override() {
auto l = landscape(max_evals).as_dataset();
auto min = l.find_minimum();
FittedParameter p(parameters[0], min.x, l.span_x() - min.x);
return Result(p, l.mean(), fevals);
}
void MinimumExplorer::add_parameter(const Parameter& param) {
if (!param.has_guess()) {throw except::invalid_operation("MinimumExplorer::add_parameter: Guess value must be supplied.");}
if (!parameters.empty()) {throw except::invalid_operation("MinimumExplorer::add_parameter: This minimizer only supports 1D problems.");}
parameters.push_back(param);
}
|