diff --git a/optimization/optimizers/nelder_mead/CMakeLists.txt b/optimization/optimizers/nelder_mead/CMakeLists.txt index dd74919f1b861a1d9e441ba618927db7911a25b8..5f2bcb508ab80d2209812321c876ad5c2f320dc1 100644 --- a/optimization/optimizers/nelder_mead/CMakeLists.txt +++ b/optimization/optimizers/nelder_mead/CMakeLists.txt @@ -3,7 +3,5 @@ add_executable(nelder_mead ) target_link_libraries(nelder_mead optimization_lib clara) -if (VISUALIZE) - make_plot_target(nelder_mead 2d ARGS -d 2) - make_plot_target(nelder_mead 10d ARGS -d 10 -n 10000) -endif() +make_plot_target(nelder_mead 2d ARGS -d 2) +make_plot_target(nelder_mead 10d ARGS -d 10 -n 10000) diff --git a/optimization/optimizers/nelder_mead/logs/everything.h b/optimization/optimizers/nelder_mead/logs/everything.h new file mode 100644 index 0000000000000000000000000000000000000000..f8bf8f03f4885bdc80eafcc279b5c48f0ae13f18 --- /dev/null +++ b/optimization/optimizers/nelder_mead/logs/everything.h @@ -0,0 +1,66 @@ +#ifndef OPTIMIZATION_NELDER_MEAD_LOGS_EVERYTHING_H +#define OPTIMIZATION_NELDER_MEAD_LOGS_EVERYTHING_H + +#include "json.hpp" +#include "../nelder_mead.h" +#include "utils/eigen_json.h" +#include <string> +#include <vector> + +namespace optimization { + +//-------------------------------------------------------------------------------------------------- +template <int32_t D> +struct NelderMeadState { + typename NelderMead<D>::MatrixDN simplex; + typename NelderMead<D>::VectorN values; +}; + +//-------------------------------------------------------------------------------------------------- +template <int32_t D> +struct NelderMeadLogEverything { + template <typename Objective> + void initialize(Objective const&) { objective_name = Objective::name; } + + void push_back( + typename NelderMead<D>::MatrixDN const& simplex, + typename NelderMead<D>::VectorN const& values + ) { + states.push_back({simplex, values}); + } + + void clear(); + + std::string objective_name; + std::vector<NelderMeadState<D>> states; +}; + +//.................................................................................................. +template <int32_t D> +void NelderMeadLogEverything<D>::clear() { + objective_name.clear(); + states.clear(); +} + +//-------------------------------------------------------------------------------------------------- +template <int32_t D> +void to_json(nlohmann::json& j, NelderMeadState<D> const& state) { + j = nlohmann::json{ + {"simplex", transpose_for_json(state.simplex)}, + {"values", state.values} + }; +} + +//-------------------------------------------------------------------------------------------------- +template <int32_t D> +void to_json(nlohmann::json& j, NelderMeadLogEverything<D> const& log) { + j = nlohmann::json{ + {"algorithm", "gradient descent"}, + {"objective", log.objective_name}, + {"data", log.states} + }; +} + +} + +#endif diff --git a/optimization/optimizers/nelder_mead/nelder_mead_vis.h b/optimization/optimizers/nelder_mead/logs/everything_vis.h similarity index 87% rename from optimization/optimizers/nelder_mead/nelder_mead_vis.h rename to optimization/optimizers/nelder_mead/logs/everything_vis.h index 648b1bca4333edf6c65ca77ebecc59d041d16af3..d7f07c4a02bf59571e9c570ad5b49223076006a2 100644 --- a/optimization/optimizers/nelder_mead/nelder_mead_vis.h +++ b/optimization/optimizers/nelder_mead/logs/everything_vis.h @@ -1,16 +1,14 @@ #ifndef OPTIMIZATION_NELDER_MEAD_VIS_H #define OPTIMIZATION_NELDER_MEAD_VIS_H -#include <iostream> -#include "nelder_mead_log.h" -#include "utils/eigen_json.h" +#include "everything.h" namespace optimization { //-------------------------------------------------------------------------------------------------- template <int32_t D> struct NelderMeadVis { - NelderMeadLog<D> const& log; + NelderMeadLogEverything<D> const& log; }; //.................................................................................................. diff --git a/optimization/optimizers/nelder_mead/logs/nothing.h b/optimization/optimizers/nelder_mead/logs/nothing.h new file mode 100644 index 0000000000000000000000000000000000000000..bb7407076866d30ee62e35cea41c4de1cfe61d22 --- /dev/null +++ b/optimization/optimizers/nelder_mead/logs/nothing.h @@ -0,0 +1,20 @@ +#ifndef OPTIMIZATION_NELDER_MEAD_LOGS_NOTHING_H +#define OPTIMIZATION_NELDER_MEAD_LOGS_NOTHING_H + +#include "utils/vector.h" +#include "utils/matrix.h" + +namespace optimization { + +//-------------------------------------------------------------------------------------------------- +struct NelderMeadLogNothing { + template <typename Objective> + void initialize(Objective const&) {} + + template <int32_t D, int32_t N> + void push_back(MatrixNs<D, N> const&, VectorNs<N> const&) {} +}; + +} + +#endif diff --git a/optimization/optimizers/nelder_mead/main.cpp b/optimization/optimizers/nelder_mead/main.cpp index b70e9897423b1ef498e35c9c5a7342fdad1b79d2..8d73373a5b9b55d8f06a0a81388deaa3cc0aee5e 100644 --- a/optimization/optimizers/nelder_mead/main.cpp +++ b/optimization/optimizers/nelder_mead/main.cpp @@ -1,18 +1,13 @@ #include "clara.hpp" +#include "logs/everything_vis.h" #include "nelder_mead.h" #include "objectives/paraboloid.h" #include "objectives/rosenbrock.h" #include <iostream> - -#ifdef VISUALIZE -#include "nelder_mead_vis.h" -#include "utils/eigen_json.h" #include <fstream> -using json = nlohmann::json; -#endif - using namespace optimization; +using json = nlohmann::json; //-------------------------------------------------------------------------------------------------- int main(int const argc, char const** argv) { @@ -49,23 +44,29 @@ int main(int const argc, char const** argv) { objective.dim() = dim; NelderMead<-1> optimizer(max_evaluations, relative_y_tolerance); - VectorXs minimum = optimizer.optimize(objective, simplex); + NelderMeadLogEverything<-1> log; + + // Only log stuff if we're going to use it. + if (log_file_path.empty() && vis_file_path.empty()) { + optimizer.optimize(objective, simplex); + } else { + optimizer.optimize(objective, simplex, log); + } + std::cout << "n evaluations: " << optimizer.n_evaluations() << '\n'; - std::cout << "final point: " << minimum << '\n'; + std::cout << "final point: " << optimizer.point() << '\n'; - #ifdef VISUALIZE if (!log_file_path.empty()) { - json data = optimizer; + json data = log; std::ofstream log_file(log_file_path); log_file << data.dump(4) << '\n'; } if (!vis_file_path.empty()) { - json data = NelderMeadVis<-1>{optimizer}; + json data = NelderMeadVis<-1>{log}; std::ofstream vis_file(vis_file_path); vis_file << data.dump(4) << '\n'; } - #endif return 0; } diff --git a/optimization/optimizers/nelder_mead/nelder_mead.h b/optimization/optimizers/nelder_mead/nelder_mead.h index 058f34c37a396bc8d8b21e4b0c41b3fafbd3b04e..e03399971444c703643dbc6bcc44cc585dad924b 100644 --- a/optimization/optimizers/nelder_mead/nelder_mead.h +++ b/optimization/optimizers/nelder_mead/nelder_mead.h @@ -1,7 +1,7 @@ #ifndef OPTIMIZATION_NELDER_MEAD_H #define OPTIMIZATION_NELDER_MEAD_H -#include "nelder_mead_log.h" +#include "logs/nothing.h" #include <cmath> #include <iostream> @@ -10,54 +10,90 @@ namespace optimization { //-------------------------------------------------------------------------------------------------- // TODO record termination condition template <int32_t D = -1> -class NelderMead : public NelderMeadLog<D> { +class NelderMead { public: + // If D != -1, N == D + 1. If D == -1, N == -1. + static constexpr int32_t N = [D_ = D](){ + if (D_ < 0) { + return D_; + } else { + return D_ + 1; + } + }(); + // vector in the space we're searching - using VectorD = typename NelderMeadLog<D>::VectorD; + using VectorD = VectorNs<D>; // vector in one higher dimension (e.g. to store a real value for each simplex vertex) - using VectorN = typename NelderMeadLog<D>::VectorN; + using VectorN = VectorNs<N>; // D x N matrix (each column is a VectorD holding a vertex, each row is a VectorN) - using MatrixDN = typename NelderMeadLog<D>::MatrixDN; + // There are two reasons to store vertices as columns. First, it avoids the use of + // transposition. Eigen will happily tranpose for you if you assign a row vector to a column + // vector, but it will complain if you do something like `c_vec = c_vec2 + r_vec`. Second, Eigen + // defaults to column major storage, so this keeps our vertex values contiguous. For logging we + // transpose the matrix, so each row is a vertex, since this is more natural to deal with in + // numpy/matplotlib. + using MatrixDN = MatrixNs<D, N>; + // Constructor NelderMead(uint32_t me = 1000, Scalar ry = 1e-8) : max_evaluations_(me), relative_y_tolerance_(ry) {} uint32_t n_evaluations() const { return n_evaluations_; } - MatrixDN const& simplex_vertices() const { return simplex_vertices_; } VectorN const& simplex_values() const { return simplex_values_; } + //VectorD const& min_point() const { return simplex_vertices_[i_lowest_]; } + decltype(auto) point() const { + return simplex_vertices_.col(i_lowest_); + } + Scalar value() const { return simplex_values_[i_lowest_]; } - // Constructs a simplex from an initial point by offset all coordinate values. + // Constructs a simplex from an initial point by offsetting all coordinate values. template <typename Objective> - VectorD optimize(Objective& objective, VectorD const& initial_point, Scalar offset = 1); - // Each row of simplex is a vertex. + VectorD optimize( + Objective& objective, + VectorD const& initial_point, + Scalar offset = 1 + ); + template <typename Objective, typename Log> + VectorD optimize( + Objective& objective, + VectorD const& initial_point, + Scalar offset, + Log& log + ); + + // Each column of simplex is a vertex. template <typename Objective> VectorD optimize(Objective& objective, MatrixDN const& simplex); - - VectorD const& min_point() const { return simplex_vertices_[i_lowest_]; } - Scalar min_value() const { return simplex_values_[i_lowest_]; } + template <typename Objective, typename Log> + VectorD optimize(Objective& objective, MatrixDN const& simplex, Log& log); private: template <typename Objective> Scalar try_new_point(Objective& objective, Scalar factor); + // hyperparameters + uint32_t max_evaluations_; + //Scalar relative_x_tolerance_; + Scalar relative_y_tolerance_; + + // algorithm state // For fixed size D (and thus N), dim_ and n_vertices_ are redundant. - uint32_t dim_; - uint32_t n_vertices_; // == dim_ + 1 - MatrixDN simplex_vertices_; - VectorN simplex_values_; - VectorD vertex_sum_; - // Invariant: simplex_values_[i_lowest] <= simplex_values_[i_second_highest] <= simplex_values_[i_highest] + uint32_t dim_; // we are searching in R^dim_ + uint32_t n_vertices_; // the simplex has n_vertices_ == dim_ + 1 different vertices + uint32_t n_evaluations_; // number of times we've evaluated the objective + MatrixDN simplex_vertices_; // the simplex itself (each column is a vertex) + VectorN simplex_values_; // the function values at the vertices + VectorD vertex_sum_; // the sum of all the vertices + VectorD x_new_; // used for trial points + // Invariant: + // simplex_values_[i_lowest] <= simplex_values_[i_second_highest] <= simplex_values_[i_highest] uint32_t i_lowest_; uint32_t i_second_highest_; uint32_t i_highest_; - uint32_t n_evaluations_; - uint32_t max_evaluations_; - //Scalar relative_x_tolerance_; - Scalar relative_y_tolerance_; - + // algorithm constants static constexpr Scalar reflection_coefficient_ = -1.0; static constexpr Scalar expansion_coefficient_ = 2.0; static constexpr Scalar contraction_coefficient_ = 0.5; @@ -72,6 +108,19 @@ auto NelderMead<D>::optimize( Objective& objective, VectorD const& initial_point, Scalar offset +) -> VectorD { + NelderMeadLogNothing log; + return optimize(objective, initial_point, offset, log); +} + +//.................................................................................................. +template <int32_t D> +template <typename Objective, typename Log> +auto NelderMead<D>::optimize( + Objective& objective, + VectorD const& initial_point, + Scalar offset, + Log& log ) -> VectorD { MatrixDN simplex; simplex.resize(initial_point.size(), initial_point.size() + 1); @@ -80,20 +129,29 @@ auto NelderMead<D>::optimize( simplex.col(i + 1) = initial_point; simplex(i, i + 1) += offset; } - return optimize(objective, simplex); + return optimize(objective, simplex, log); } //.................................................................................................. template <int32_t D> template <typename Objective> auto NelderMead<D>::optimize(Objective& objective, MatrixDN const& simplex) -> VectorD { + NelderMeadLogNothing log; + return optimize(objective, simplex, log); +} + +//.................................................................................................. +template <int32_t D> +template <typename Objective, typename Log> +auto NelderMead<D>::optimize(Objective& objective, MatrixDN const& simplex, Log& log) -> VectorD { + // initialize state simplex_vertices_ = simplex; n_vertices_ = simplex_vertices_.cols(); dim_ = simplex_vertices_.rows(); assert(n_vertices_ == dim_ + 1); simplex_values_.resize(n_vertices_); - vertex_sum_.resize(dim_); vertex_sum_ = simplex_vertices_.rowwise().sum(); + log.initialize(objective); // Evaluate the objective at all simplex vertices. for (uint32_t i = 0u; i < n_vertices_; ++i) { @@ -101,8 +159,6 @@ auto NelderMead<D>::optimize(Objective& objective, MatrixDN const& simplex) -> V } n_evaluations_ = n_vertices_; - NelderMeadLog<D>::initialize(objective); - while (true) { // Find lowest, highest, and second highest points. i_lowest_ = (simplex_values_[0] < simplex_values_[1]) ? 0 : 1; @@ -123,7 +179,7 @@ auto NelderMead<D>::optimize(Objective& objective, MatrixDN const& simplex) -> V } // Log the simplex. - NelderMeadLog<D>::push_back(simplex_vertices_, simplex_values_); + log.push_back(simplex_vertices_, simplex_values_); // Evaluate termination conditions. Scalar const difference = std::abs(simplex_values_[i_highest_] - simplex_values_[i_lowest_]); diff --git a/optimization/optimizers/nelder_mead/nelder_mead_log.h b/optimization/optimizers/nelder_mead/nelder_mead_log.h deleted file mode 100644 index 4229ad2392ad406ac1fee10376c54f6971dee8fe..0000000000000000000000000000000000000000 --- a/optimization/optimizers/nelder_mead/nelder_mead_log.h +++ /dev/null @@ -1,117 +0,0 @@ -#ifndef OPTIMIZATION_NELDER_MEAD_LOG_H -#define OPTIMIZATION_NELDER_MEAD_LOG_H - -#include "utils/vector.h" -#include "utils/matrix.h" -#include "utils/vis_only.h" - -#ifdef VISUALIZE -#include <vector> -#include "json.hpp" -#include "utils/eigen_json.h" -#endif - -namespace optimization { - -//-------------------------------------------------------------------------------------------------- -template <int32_t D> -struct NelderMeadState { - // If D != -1, N == D + 1. If D == -1, N == -1. - static constexpr int32_t N = [D_ = D](){ - if (D_ < 0) { - return D_; - } else { - return D_ + 1; - } - }(); - - // vector in the space we're searching - using VectorD = VectorNs<D>; - // vector in one higher dimension (e.g. to store a real value for each simplex vertex) - using VectorN = VectorNs<N>; - // D x N matrix (each column is a VectorD holding a vertex, each row is a VectorN) - // There are two reasons to store vertices as columns. First, it avoids the use of - // transposition. Eigen will happily tranpose for you if you assign a row vector to a column - // vector, but it will complain if you do something like `c_vec = c_vec2 + r_vec`. Second, Eigen - // defaults to column major storage, so this keeps our vertex values contiguous. For logging we - // transpose the matrix, so each row is a vertex, since this is more natural to deal with in - // numpy/matplotlib. - using MatrixDN = MatrixNs<D, N>; - - NelderMeadState(MatrixDN const& s, VectorN const& v) - : simplex(s), values(v) {} - - MatrixDN simplex; - VectorN values; -}; - -//-------------------------------------------------------------------------------------------------- -// This is used as a base class rather than a member so that the empty base class optimization can -// be applied (the member would take up space even if it is an empty class). -template <int32_t D> -struct NelderMeadLog { - using VectorD = typename NelderMeadState<D>::VectorD; - using VectorN = typename NelderMeadState<D>::VectorN; - using MatrixDN = typename NelderMeadState<D>::MatrixDN; - - void reserve(uint32_t n) VIS_ONLY_METHOD; - template <typename Objective> - void initialize(Objective const&) VIS_ONLY_METHOD; - void push_back(MatrixDN const& simplex, VectorN const& values) VIS_ONLY_METHOD; - void clear() VIS_ONLY_METHOD; - - #ifdef VISUALIZE - std::string objective_name; - std::vector<NelderMeadState<D>> states; - #endif -}; - -#ifdef VISUALIZE - -//.................................................................................................. -template <int32_t D> -void NelderMeadLog<D>::reserve(uint32_t n) { - states.reserve(n); -} - -//.................................................................................................. -template <int32_t D> -template <typename Objective> -void NelderMeadLog<D>::initialize(Objective const&) { - objective_name = Objective::name; -} - -//.................................................................................................. -template <int32_t D> -void NelderMeadLog<D>::push_back( - MatrixDN const& simplex, - VectorN const& values -) { - states.emplace_back(simplex, values); -} - - -//-------------------------------------------------------------------------------------------------- -template <int32_t D> -void to_json(nlohmann::json& j, NelderMeadState<D> const& state) { - j = nlohmann::json{ - {"simplex", transpose_for_json(state.simplex)}, - {"values", state.values} - }; -} - -//-------------------------------------------------------------------------------------------------- -template <int32_t D> -void to_json(nlohmann::json& j, NelderMeadLog<D> const& log) { - j = nlohmann::json{ - {"algorithm", "nelder-mead"}, - {"objective", log.objective_name}, - {"data", log.states} - }; -} - -#endif - -} - -#endif