Brush C++ API
A flexible interpretable machine learning framework
Loading...
Searching...
No Matches
weight_optimizer.h
Go to the documentation of this file.
1/* Brush
2copyright 2020 William La Cava
3license: GNU/GPL v3
4
5Code below heavily inspired by heal-research/operon, Copyright 2019-2022 Heal Research
6*/
7#ifndef WEIGHT_OPTIMIZER_H
8#define WEIGHT_OPTIMIZER_H
9
10#include <ceres/dynamic_autodiff_cost_function.h>
11#include <ceres/dynamic_numeric_diff_cost_function.h>
12#include <ceres/loss_function.h>
13#include <ceres/solver.h>
14#include <ceres/tiny_solver.h>
15
16#include "tiny_cost_function.h"
17
18namespace Brush
19{
20
29
30template<typename PT>
32 typedef float Scalar;
33 ResidualEvaluator(PT& program, Dataset const& dataset,
34 const vector<float>& class_weights = {})
35 : program_(program)
36 , dataset_(dataset)
37 , numParameters_(program.get_weights().size())
38 , y_true_(dataset.y)
39 , class_weights_(class_weights)
40 {}
41
42 template<typename T>
43 auto operator()(Eigen::DenseBase<T>& parameters, Eigen::DenseBase<T>& residuals) const noexcept -> void
44 {
45 return (*this)(parameters.data(), residuals.data());
46 }
47
48 template <typename T>
49 auto operator()(T const* parameters, T* residuals) const -> bool
50 {
51 const T ** new_weights = &parameters;
52 auto residualMap = Eigen::Map<Array<T, Dynamic, 1>>(
53 residuals, GetDataset().get_n_samples());
54
55 if constexpr (PT::program_type == ProgramType::MulticlassClassifier)
56 {
57 using MatrixType = Array<T, Dynamic, Dynamic>;
58 auto probabilities = GetProgram().template predict_with_weights<MatrixType>(
59 GetDataset(), new_weights);
60 for (int i = 0; i < GetDataset().get_n_samples(); ++i)
61 {
62 const int label = static_cast<int>(GetTarget()(i));
63 const T probability = probabilities(i, label);
64 const float weight = class_weights_.empty() ? 1.0f
65 : class_weights_.at(label);
66 // TinySolver minimizes squared residuals, so sqrt(loss)
67 // makes its objective the weighted multinomial log loss.
68 residualMap(i) = sqrt(T(weight) * -log(probability));
69 }
70 }
71 else
72 {
73 using ArrayType = Array<T, Dynamic, 1>;
74 ArrayType y_pred = GetProgram().template predict_with_weights<ArrayType>(
75 GetDataset(), new_weights);
76
77 if (GetDataset().classification)
78 {
79 // tolerance to avoid numeric errors.
80
81 // Using an eps with 7 significant digits to avoid weird behavior.
82 // From some experiments, I noticed that using very small values (previous
83 // was pow(10,-10) caused the optimizer to evaluate to nans), probably because
84 // we were calculating log(0) due to cpp ignoring more than 7 significant digits
85 // for floats. using -6. As a reference, tensorflow uses -7.
86 // We need to remember that we dont want to have eps too big, otherwise
87 // we are biasing the predictions away from 0 and 1
88 float eps = 1e-6f;
89
90 auto y = GetTarget();
91
92 // cout << T(1.0) << ", " << T(eps) << endl;
93
94 // clamp values and avoid log(0)
95 y_pred = y_pred.min(T(1.0) - T(eps)).max(T(eps));
96
97 for (int i = 0; i < y_pred.size(); ++i)
98 {
99 const float weight = class_weights_.empty() ? 1.0f
100 : class_weights_.at(static_cast<int>(y(i)));
101 const T log_loss = -(T(y(i)) * log(y_pred(i))
102 + (T(1.0f) - T(y(i))) * log(T(1.0f) - y_pred(i)));
103 // See multiclass branch above: this makes the least-squares
104 // objective equal weighted binary log loss.
105 residualMap(i) = sqrt(T(weight) * log_loss);
106 }
107 }
108 else
109 {
110 residualMap = y_pred - GetTarget();
111 }
112 }
113
114 return true;
115 }
116
117 [[nodiscard]] auto NumParameters() const -> size_t { return numParameters_; }
118 [[nodiscard]] auto NumResiduals() const -> size_t { return y_true_.get().size(); }
119 inline auto GetProgram() const { return program_.get();};
120 inline auto GetDataset() const { return dataset_.get();};
121 inline auto GetTarget() const { return y_true_.get();};
122
123private:
124 std::reference_wrapper<PT> program_;
125 std::reference_wrapper<Dataset const> dataset_;
126 std::reference_wrapper<ArrayXf const> y_true_;
128 size_t numParameters_; // cache the number of parameters in the tree
129};
130
131// TODO: see this struct and try to understand how to make non-templated classes
133{
138 template<typename PT>
139 void update(PT& program, const Dataset& dataset,
140 const vector<float>& class_weights = {})
141 {
142 if (program.get_n_weights() == 0)
143 return;
144
145 // fmt::print("number of weights: {}\n",program.get_n_weights());
146 auto init_weights = program.get_weights();
147
149 ResidualEvaluator<PT> evaluator(program, dataset, class_weights);
150 CFType cost_function(evaluator);
151 ceres::TinySolver<CFType> solver;
152 solver.options.max_num_iterations = 10;
153
154 typename decltype(solver)::Parameters parameters = program.get_weights();
155 solver.Solve(cost_function, &parameters);
156
157 // fmt::print("Summary:\nInitial cost: {}\nFinal Cost: {}\nIterations: {}\n",
158 // solver.summary.initial_cost,
159 // solver.summary.final_cost,
160 // solver.summary.iterations
161 // );
162 // fmt::print("Initial weights: {}\nFinal weights: {}\n",
163 // init_weights,
164 // parameters
165 // );
166
167 // Validate optimizer results before applying
168 bool costs_valid = !std::isnan(solver.summary.final_cost) &&
169 !std::isinf(solver.summary.final_cost) &&
170 !std::isnan(solver.summary.initial_cost) &&
171 !std::isinf(solver.summary.initial_cost);
172
173 bool weights_valid = parameters.allFinite();
174
175 if (costs_valid && weights_valid &&
176 solver.summary.final_cost < solver.summary.initial_cost)
177 {
178 program.set_weights(parameters);
179 }
180
181 }
182};
183
184} // namespace Brush
185#endif
holds variable type data.
Definition data.h:51
STL class.
VectorXf log_loss(const VectorXf &y, const VectorXf &predict_proba, const vector< float > &class_weights)
Calculates the log loss between the predicted probabilities and the true labels.
Definition metrics.cpp:19
< nsga2 selection operator for getting the front
Definition bandit.cpp:3
ProgramType PT
Definition program.h:39
auto NumResiduals() const -> size_t
std::reference_wrapper< Dataset const > dataset_
auto operator()(Eigen::DenseBase< T > &parameters, Eigen::DenseBase< T > &residuals) const noexcept -> void
auto NumParameters() const -> size_t
std::reference_wrapper< PT > program_
auto operator()(T const *parameters, T *residuals) const -> bool
ResidualEvaluator(PT &program, Dataset const &dataset, const vector< float > &class_weights={})
std::reference_wrapper< ArrayXf const > y_true_
void update(PT &program, const Dataset &dataset, const vector< float > &class_weights={})
Update program weights using non-linear least squares.