Brush C++ API
A flexible interpretable machine learning framework
Loading...
Searching...
No Matches
metrics.cpp
Go to the documentation of this file.
1#include "metrics.h"
2
3#include <algorithm>
4
5namespace Brush {
6namespace Eval {
7
8/* Scoring functions */
9
11float mse(const VectorXf& y, const VectorXf& yhat, VectorXf& loss,
12 const vector<float>& class_weights)
13{
14 loss = (yhat - y).array().pow(2);
15 return loss.mean();
16}
17
18
19VectorXf log_loss(const VectorXf& y, const VectorXf& predict_proba,
20 const vector<float>& class_weights)
21{
22 // See comments on weight_optimizer to learn more about why am I using
23 // this value for eps. TL;DR: dont change, can cause weird behaviour
24 float eps = 1e-6f;
25
26 VectorXf loss;
27
28 loss.resize(y.rows());
29 for (unsigned i = 0; i < y.rows(); ++i)
30 {
31 if (predict_proba(i) < eps || 1 - predict_proba(i) < eps)
32 // clip probabilities since log loss is undefined for predict_proba=0 or predict_proba=1
33 loss(i) = -(y(i)*log(eps) + (1-y(i))*log(1-eps));
34 else
35 loss(i) = -(y(i)*log(predict_proba(i)) + (1-y(i))*log(1-predict_proba(i)));
36
37 if (loss(i)<0)
38 std::runtime_error("loss(i)= " + to_string(loss(i))
39 + ". y = " + to_string(y(i)) + ", predict_proba(i) = "
40 + to_string(predict_proba(i)));
41 }
42
43 return loss;
44}
45
47float mean_log_loss(const VectorXf& y,
48 const VectorXf& predict_proba, VectorXf& loss,
49 const vector<float>& class_weights)
50{
51 loss = log_loss(y,predict_proba,class_weights);
52
53 if (!class_weights.empty())
54 {
55 float sum_weights = 0;
56
57 // we keep loss without weights, as this may affect lexicase
58 VectorXf weighted_loss;
59 weighted_loss.resize(y.rows());
60 for (unsigned i = 0; i < y.rows(); ++i)
61 {
62 weighted_loss(i) = loss(i) * class_weights.at(y(i));
63 sum_weights += class_weights.at(y(i));
64 }
65
66 // equivalent of sklearn's log_loss with weights. It uses np.average,
67 // which returns avg = sum(a * weights) / sum(weights)
68 return weighted_loss.sum() / sum_weights; // normalize weight contributions
69 }
70
71 return loss.mean();
72}
73
74// accuracy
75float zero_one_loss(const VectorXf& y,
76 const VectorXf& predict_proba, VectorXf& loss,
77 const vector<float>& class_weights )
78{
79 VectorXi yhat = (predict_proba.array() > 0.5).cast<int>();
80
81 // we are actually finding wrong predictions here
82 loss = (yhat.array() != y.cast<int>().array()).cast<float>();
83
84 // Apply class weights if provided
85 float scale = 0.0f;
86 if (!class_weights.empty()) {
87 for (int i = 0; i < y.rows(); ++i) {
88 loss(i) *= class_weights.at(y(i));
89 scale += class_weights.at(y(i));
90 }
91 }
92 else
93 {
94 scale = static_cast<float>(loss.size());
95 }
96
97 // since `loss` contains wrong predictions, we need to invert it
98 return 1.0 - (loss.sum() / scale);
99}
100
101// balanced accuracy
102float bal_zero_one_loss(const VectorXf& y,
103 const VectorXf& predict_proba, VectorXf& loss,
104 const vector<float>& class_weights )
105{
106 VectorXi yhat = (predict_proba.array() > 0.5).cast<int>();
107
108 loss = (yhat.array() != y.cast<int>().array()).cast<float>();
109
110 float TP = 0;
111 float FP = 0;
112 float TN = 0;
113 float FN = 0;
114
115 int num_instances = y.rows();
116 for (int i = 0; i < num_instances; ++i) {
117 float weight = 1.0f; // it is a balanced metric; ignoring class weights
118 // float weight = class_weights.empty() ? 1.0f : class_weights.at(y(i));
119
120 if (yhat(i) == 1.0 && y(i) == 1.0) TP += weight;
121 else if (yhat(i) == 1.0 && y(i) == 0.0) FP += weight;
122 else if (yhat(i) == 0.0 && y(i) == 0.0) TN += weight;
123 else FN += weight;
124 }
125
126 float eps = 1e-6f;
127
128 float TPR = (TP + eps) / (TP + FN + eps);
129 float TNR = (TN + eps) / (TN + FP + eps);
130
131 return (TPR + TNR) / 2.0;
132}
133
134// anonymous namespace. make the headers private to metrics.cpp. only affects linkage
135namespace {
136
137// per-sample weights from class weights (all ones if no class weights)
138vector<float> sample_weights(const VectorXf& y, const vector<float>& class_weights)
139{
140 vector<float> w(y.size(), 1.0f);
141 if (!class_weights.empty())
142 for (int i = 0; i < y.size(); ++i)
143 w[i] = class_weights.at(static_cast<int>(y(i)));
144 return w;
145}
146
147// Binary average precision. `y` holds 0/1 labels and `w` per-sample weights.
148float binary_average_precision(const VectorXf& y, const VectorXf& predict_proba,
149 const vector<float>& w)
150{
151 int num_instances = y.size();
152 float eps = 1e-6f;
153
154 // get argsort of predict proba (descending)
155 vector<int> order(num_instances);
156 iota(order.begin(), order.end(), 0);
157 stable_sort(order.begin(), order.end(), [&](int i, int j) {
158 return predict_proba(i) > predict_proba(j); // descending
159 });
160
161 float ysum = 0.0f;
162 vector<float> y_sorted(num_instances); // y true
163 vector<float> p_sorted(num_instances); // pred probas
164 vector<float> w_sorted(num_instances); // sample weights
165 for (int i = 0; i < num_instances; ++i) {
166 int idx = order[i];
167
168 y_sorted[i] = y(idx);
169 p_sorted[i] = predict_proba(idx);
170 w_sorted[i] = w[idx];
171
172 ysum += y_sorted[i] * w_sorted[i];
173 }
174
175 // when all scores are the same, the sort order is arbitrary, so the PR curve
176 // you integrate is a staircase instead of a flat line. Sklearn avoids this by
177 // treating ties as one threshold.
178 // however, this does not produce consistent results, so we will handle flat
179 // lines below
180
181 // detect constant prediction case (all p_sorted equal within tolerance).
182 // because p_sorted is sorted, the first element is the maximum, and the last is the minimum,
183 if (fabs(p_sorted.back() - p_sorted.front()) <= eps) {
184 // All predictions are (effectively) constant.
185 float total_weight = std::accumulate(w_sorted.begin(), w_sorted.end(), 0.0f);
186
187 // Return weighted positives / total weight, matching sklearn's result for constant scores
188 // (kinda weighted prevalence)
189 return total_weight == 0.0f ? 0.0f : ysum / total_weight;
190 }
191
192 // Find the indexes where prediction changes, so we can treat it as one block
193 vector<int> unique_indices = {}; // this one will be used to calculate the AUC
194 set<float> unique_probas = {}; // keep track of unique elements (this wont be used other than that)
195
196 for (int i=0; i<p_sorted.size(); ++i)
197 if (unique_probas.insert(p_sorted.at(i)).second)
198 unique_indices.push_back(i);
199
200 unique_indices.push_back(num_instances); // last index is the number of elements
201
202 float tp = 0.0f;
203 float fp = 0.0f;
204 vector<float> precision = {1.0};
205 vector<float> recall = {0.0};
206
207 for (size_t i = 0; i < unique_indices.size() - 1; ++i) {
208 int start = unique_indices[i];
209 int end = unique_indices[i+1];
210
211 // process group with a for loop (aggregating for each sample)
212 for (int j = start; j < end; ++j) {
213 tp += y_sorted.at(j) * w_sorted.at(j);
214 fp += (1.0f - y_sorted.at(j)) * w_sorted.at(j);
215
216 float relevant = tp + fp;
217 precision.push_back(relevant == 0.0f ? 0.0f : tp / relevant);
218 recall.push_back(ysum == 0.0f ? 1.0f : tp / ysum);
219 }
220 }
221
222 // integrate PR curve
223 float average_precision = 0.0f;
224 for (size_t i = 0; i < precision.size() - 1; ++i) {
225 average_precision += (recall[i+1] - recall[i]) * precision[i+1];
226 }
227
228 return average_precision;
229}
230
231// Binary AUROC (trapezoidal rule over the ROC curve, treating tied scores as
232// a single threshold, like sklearn). `y` holds 0/1 labels and `w` per-sample
233// weights. Returns 0.5 if only one class is present (AUROC is undefined).
234float binary_roc_auc(const VectorXf& y, const VectorXf& predict_proba,
235 const vector<float>& w)
236{
237 int num_instances = y.size();
238
239 vector<int> order(num_instances);
240 iota(order.begin(), order.end(), 0);
241 stable_sort(order.begin(), order.end(), [&](int i, int j) {
242 return predict_proba(i) > predict_proba(j); // descending
243 });
244
245 float pos = 0.0f;
246 float neg = 0.0f;
247 for (int i = 0; i < num_instances; ++i) {
248 // remember: this is for the binary case!
249 pos += y(i) * w[i];
250 neg += (1.0f - y(i)) * w[i];
251 }
252
253 // default case, copying sklearn, returns 0.5 if only one class exists in the y
254 if (pos == 0.0f || neg == 0.0f)
255 return 0.5f;
256
257 float tp = 0.0f, fp = 0.0f;
258 float tp_prev = 0.0f, fp_prev = 0.0f;
259 float area = 0.0f;
260 for (int i = 0; i < num_instances; ++i) {
261 int idx = order[i];
262 tp += y(idx) * w[idx];
263 fp += (1.0f - y(idx)) * w[idx];
264
265 // only add a point to the curve at the end of a block of tied scores
266 bool last_of_block = (i == num_instances - 1)
267 || (predict_proba(order[i+1]) != predict_proba(idx));
268
269 if (last_of_block) {
270 area += (fp - fp_prev) * (tp + tp_prev) / 2.0f;
271 tp_prev = tp;
272 fp_prev = fp;
273 }
274 }
275
276 return area / (pos * neg);
277}
278
279// Weighted confusion matrix entries for class `label` (one-vs-rest).
280void confusion(const VectorXf& y, const ArrayXi& yhat, int label,
281 const vector<float>& w, float& TP, float& FP, float& FN)
282{
283 // Used to calculate precision and recall for multiclass settings.
284 // TP, FP, FN, passed as reference
285
286 TP = FP = FN = 0.0f;
287 for (int i = 0; i < y.size(); ++i) {
288 bool is_true = static_cast<int>(y(i)) == label;
289 bool is_pred = yhat(i) == label;
290
291 if ( is_true && is_pred) TP += w[i];
292 else if (!is_true && is_pred) FP += w[i];
293 else if ( is_true && !is_pred) FN += w[i];
294 }
295}
296
297ArrayXi argmax_rows(const ArrayXXf& predict_proba)
298{
299 // converting the pred proba matrix to predictions
300
301 ArrayXi yhat(predict_proba.rows());
302 for (int i = 0; i < predict_proba.rows(); ++i)
303 predict_proba.row(i).maxCoeff(&yhat(i));
304
305 return yhat;
306}
307
308// Macro average of precision or recall over the classes present in either
309// the true or predicted labels (sklearn's default label set).
310float multi_macro_precision_recall(const VectorXf& y, const ArrayXXf& predict_proba,
311 VectorXf& loss, const vector<float>& class_weights,
312 bool precision)
313{
314 if (predict_proba.rows() != y.rows())
315 HANDLE_ERROR_THROW("Multiclass probabilities and labels have different numbers of rows");
316
317 ArrayXi yhat = argmax_rows(predict_proba);
318
319 // again setting the loss here as hit or miss, a.k.a. accuracy
320 loss = (yhat != y.cast<int>().array()).cast<float>();
321
322 vector<float> w = sample_weights(y, class_weights);
323
324 float sum = 0.0f;
325 int n_labels = 0;
326 for (int label = 0; label < predict_proba.cols(); ++label) {
327 bool present = (y.cast<int>().array() == label).any() || (yhat == label).any();
328 if (!present)
329 continue;
330
331 float TP, FP, FN;
332 confusion(y, yhat, label, w, TP, FP, FN);
333
334 float denom = precision ? TP + FP : TP + FN;
335 sum += denom == 0.0f ? 0.0f : TP / denom;
336 ++n_labels;
337 }
338 return n_labels == 0 ? 0.0f : sum / n_labels;
339}
340
341} // anonymous namespace
342
343float average_precision_score(const VectorXf& y, const VectorXf& predict_proba,
344 VectorXf& loss,
345 const vector<float>& class_weights) {
346
347 // AP is implemented as AUC PR in sklearn.
348 // AP summarizes a precision-recall curve as the weighted mean of precisions
349 // achieved at each threshold, with the increase in recall from the previous threshold used as the weight
350
351 // The loss vector is used in lexicase selection. we need to set something useful here
352 // that does make sense on individual level. Using log loss here.
353 loss = log_loss(y, predict_proba, class_weights);
354
355 return binary_average_precision(y, predict_proba, sample_weights(y, class_weights));
356}
357
358// implementing precision_score and recall_score for the binary case.
359// it will be used per-class in the multiclass case below.
360float precision_score(const VectorXf& y, const VectorXf& predict_proba,
361 VectorXf& loss, const vector<float>& class_weights)
362{
363 ArrayXi yhat = (predict_proba.array() > 0.5).cast<int>();
364
365 // Again updating the loss vector. Doing the same way as binary accuracy (zero_one_loss) here
366 loss = (yhat != y.cast<int>().array()).cast<float>();
367
368 float TP, FP, FN;
369 confusion(y, yhat, 1, sample_weights(y, class_weights), TP, FP, FN);
370
371 return (TP + FP) == 0.0f ? 0.0f : TP / (TP + FP);
372}
373
374float recall_score(const VectorXf& y, const VectorXf& predict_proba,
375 VectorXf& loss, const vector<float>& class_weights)
376{
377 ArrayXi yhat = (predict_proba.array() > 0.5).cast<int>();
378 loss = (yhat != y.cast<int>().array()).cast<float>();
379
380 float TP, FP, FN;
381 confusion(y, yhat, 1, sample_weights(y, class_weights), TP, FP, FN);
382
383 return (TP + FN) == 0.0f ? 0.0f : TP / (TP + FN);
384}
385
386float roc_auc_score(const VectorXf& y, const VectorXf& predict_proba,
387 VectorXf& loss, const vector<float>& class_weights)
388{
389 // AUROC is not decomposable per sample; log loss is used for lexicase
390 loss = log_loss(y, predict_proba, class_weights);
391
392 return binary_roc_auc(y, predict_proba, sample_weights(y, class_weights));
393}
394
395// multinomial log loss
396VectorXf multi_log_loss(const VectorXf& y, const ArrayXXf& predict_proba,
397 const vector<float>& class_weights)
398{
399 if (predict_proba.rows() != y.rows())
400 HANDLE_ERROR_THROW("Multiclass probabilities and labels have different numbers of rows");
401
402 constexpr float eps = 1e-6f;
403 VectorXf loss(y.rows());
404 for (int i = 0; i < y.rows(); ++i)
405 {
406 const int label = static_cast<int>(y(i)); // labels are always encoded as integers for clf/multiclf
407
408 // if (label < 0 || label >= predict_proba.cols())
409 // HANDLE_ERROR_THROW("Class label is outside the predicted probability columns");
410
411 // per sample log loss
412 loss(i) = -std::log(std::clamp(predict_proba(i, label), eps, 1.0f - eps));
413 }
414 return loss;
415}
416
417float mean_multi_log_loss(const VectorXf& y,
418 const ArrayXXf& predict_proba, VectorXf& loss,
419 const vector<float>& class_weights)
420{
421 loss = multi_log_loss(y, predict_proba, class_weights);
422
423 if (class_weights.empty())
424 return loss.mean();
425
426 // apply class weights to the log loss
427 float sum_weights = 0.0f;
428 float weighted_loss = 0.0f;
429 for (int i = 0; i < y.rows(); ++i)
430 {
431 const float weight = class_weights.at(static_cast<int>(y(i)));
432 weighted_loss += loss(i) * weight;
433 sum_weights += weight;
434 }
435 return sum_weights == 0.0f ? 0.0f : weighted_loss / sum_weights;
436}
437
438float multi_zero_one_loss(const VectorXf& y,
439 const ArrayXXf& predict_proba, VectorXf& loss,
440 const vector<float>& class_weights )
441{
442 if (predict_proba.rows() != y.rows())
443 HANDLE_ERROR_THROW("Multiclass probabilities and labels have different numbers of rows");
444
445 ArrayXi yhat(y.rows());
446 for (int i = 0; i < predict_proba.rows(); ++i)
447 predict_proba.row(i).maxCoeff(&yhat(i)); // pick the predicted class
448
449 loss = (yhat.array() != y.cast<int>().array()).cast<float>(); // check if it was a hit or a miss
450
451 if (class_weights.empty()) // accuracy
452 return 1.0f - loss.mean();
453
454 float weighted_errors = 0.0f;
455 float sum_weights = 0.0f;
456 for (int i = 0; i < y.rows(); ++i)
457 {
458 const float weight = class_weights.at(static_cast<int>(y(i)));
459 weighted_errors += loss(i) * weight;
460 sum_weights += weight;
461 }
462 return sum_weights == 0.0f ? 0.0f : 1.0f - weighted_errors / sum_weights;
463}
464
465float multi_bal_zero_one_loss(const VectorXf& y,
466 const ArrayXXf& predict_proba, VectorXf& loss,
467 const vector<float>& class_weights)
468{
469 if (predict_proba.rows() != y.rows())
470 HANDLE_ERROR_THROW("Multiclass probabilities and labels have different numbers of rows");
471
472 ArrayXi yhat(y.rows());
473 for (int i = 0; i < predict_proba.rows(); ++i)
474 predict_proba.row(i).maxCoeff(&yhat(i));
475 loss = (yhat.array() != y.cast<int>().array()).cast<float>();
476
477 VectorXf correct = VectorXf::Zero(predict_proba.cols());
478 VectorXf support = VectorXf::Zero(predict_proba.cols());
479 for (int i = 0; i < y.rows(); ++i)
480 {
481 const int label = static_cast<int>(y(i));
482
483 // if (label < 0 || label >= predict_proba.cols())
484 // HANDLE_ERROR_THROW("Class label is outside the predicted probability columns");
485
486 // balanced, weighted by support
487 support(label) += 1.0f;
488 if (yhat(i) == label)
489 correct(label) += 1.0f;
490 }
491
492 float recall_sum = 0.0f;
493 int present_classes = 0;
494 for (int label = 0; label < support.size(); ++label)
495 if (support(label) > 0.0f)
496 {
497 recall_sum += correct(label) / support(label);
498 ++present_classes;
499 }
500 return present_classes == 0 ? 0.0f : recall_sum / present_classes;
501}
502
503float multi_precision_score(const VectorXf& y, const ArrayXXf& predict_proba,
504 VectorXf& loss, const vector<float>& class_weights)
505{
506 return multi_macro_precision_recall(y, predict_proba, loss, class_weights, true);
507}
508
509float multi_recall_score(const VectorXf& y, const ArrayXXf& predict_proba,
510 VectorXf& loss, const vector<float>& class_weights)
511{
512 return multi_macro_precision_recall(y, predict_proba, loss, class_weights, false);
513}
514
515float multi_roc_auc_score(const VectorXf& y, const ArrayXXf& predict_proba,
516 VectorXf& loss, const vector<float>& class_weights)
517{
518 loss = multi_log_loss(y, predict_proba, class_weights);
519
520 vector<float> w = sample_weights(y, class_weights);
521
522 float sum = 0.0f;
523 int n_labels = 0;
524 for (int label = 0; label < predict_proba.cols(); ++label) {
525 VectorXf y_bin = (y.cast<int>().array() == label).cast<float>();
526
527 // one-vs-rest AUROC is undefined if the class is absent (or is the only one)
528 if (y_bin.sum() == 0.0f || y_bin.sum() == y_bin.size())
529 continue;
530
531 sum += binary_roc_auc(y_bin, predict_proba.col(label).matrix(), w);
532 ++n_labels;
533 }
534 return n_labels == 0 ? 0.5f : sum / n_labels;
535}
536
537float multi_average_precision_score(const VectorXf& y, const ArrayXXf& predict_proba,
538 VectorXf& loss, const vector<float>& class_weights)
539{
540 loss = multi_log_loss(y, predict_proba, class_weights);
541
542 vector<float> w = sample_weights(y, class_weights);
543
544 float sum = 0.0f;
545 int n_labels = 0;
546 for (int label = 0; label < predict_proba.cols(); ++label) {
547 VectorXf y_bin = (y.cast<int>().array() == label).cast<float>();
548
549 // recall is undefined if the class is absent
550 if (y_bin.sum() == 0.0f)
551 continue;
552
553 sum += binary_average_precision(y_bin, predict_proba.col(label).matrix(), w);
554 ++n_labels;
555 }
556 return n_labels == 0 ? 0.0f : sum / n_labels;
557}
558
559} // metrics
560} // Brush
#define HANDLE_ERROR_THROW(err)
Definition error.h:27
float multi_zero_one_loss(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Accuracy for multi-classification.
Definition metrics.cpp:438
float precision_score(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Precision for binary classification (threshold 0.5, positive label 1).
Definition metrics.cpp:360
float zero_one_loss(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Accuracy for binary classification.
Definition metrics.cpp:75
float mean_log_loss(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
log loss
Definition metrics.cpp:47
float multi_recall_score(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Macro-averaged recall for multi-classification.
Definition metrics.cpp:509
float mean_multi_log_loss(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Calculates the mean multinomial log loss between the predicted probabilities and the true labels.
Definition metrics.cpp:417
float average_precision_score(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Calculates the average precision score between the predicted probabilities and the true labels.
Definition metrics.cpp:343
float mse(const VectorXf &y, const VectorXf &yhat, VectorXf &loss, const vector< float > &class_weights)
mean squared error
Definition metrics.cpp:11
VectorXf multi_log_loss(const VectorXf &y, const ArrayXXf &predict_proba, const vector< float > &class_weights)
Calculates the multinomial log loss between the predicted probabilities and the true labels.
Definition metrics.cpp:396
float multi_bal_zero_one_loss(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Balanced accuracy for multi-classification.
Definition metrics.cpp:465
float recall_score(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Recall for binary classification (threshold 0.5, positive label 1).
Definition metrics.cpp:374
float multi_roc_auc_score(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Macro-averaged one-vs-rest AUROC for multi-classification.
Definition metrics.cpp:515
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
float multi_precision_score(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Macro-averaged precision for multi-classification.
Definition metrics.cpp:503
float multi_average_precision_score(const VectorXf &y, const ArrayXXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Macro-averaged one-vs-rest average precision for multi-classification.
Definition metrics.cpp:537
float bal_zero_one_loss(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Balanced accuracy for binary classification.
Definition metrics.cpp:102
float roc_auc_score(const VectorXf &y, const VectorXf &predict_proba, VectorXf &loss, const vector< float > &class_weights)
Area under the ROC curve for binary classification.
Definition metrics.cpp:386
string to_string(const T &value)
template function to convert objects to string for logging
Definition utils.h:369
< nsga2 selection operator for getting the front
Definition bandit.cpp:3
Eigen::Array< int, Eigen::Dynamic, 1 > ArrayXi
Definition types.h:40