Brush C++ API
A flexible interpretable machine learning framework
Loading...
Searching...
No Matches
program.h
Go to the documentation of this file.
1/* Brush
2
3copyright 2020 William La Cava
4license: GNU/GPL v3
5*/
6#ifndef PROGRAM_H
7#define PROGRAM_H
8//external includes
9
10
11#include <string>
12#include "assert.h"
13
14// internal includes
15
16// #include "data/data.h"
17#include "../init.h"
18#include "tree_node.h"
19#include "node.h"
21#include "../params.h"
22#include "../util/utils.h"
23#include "functions.h"
24// #include "../variation.h"
25// #include "weight_optimizer.h"
26
27
28using std::cout;
29using std::string;
32
33namespace Brush {
34
35
36typedef tree<Node>::pre_order_iterator Iter;
37typedef tree<Node>::post_order_iterator PostIter;
38
40
41// for unsupervised learning, classification and regression.
42
48template<PT PType> struct Program
49{
51 static constexpr PT program_type = PType;
52
54 using RetType = typename std::conditional_t<PType == PT::Regressor, ArrayXf,
55 std::conditional_t<PType == PT::BinaryClassifier, ArrayXb,
56 std::conditional_t<PType == PT::MulticlassClassifier, ArrayXi,
57 std::conditional_t<PType == PT::Representer, ArrayXXf, ArrayXf
58 >>>>;
59
61 using TreeType = std::conditional_t<PType == PT::BinaryClassifier, ArrayXf,
62 std::conditional_t<PType == PT::MulticlassClassifier, ArrayXXf,
63 RetType>>;
64
66 bool is_fitted_ = false;
67
69 // Fitness fitness;
70
72 tree<Node> Tree;
73
75 std::optional<std::reference_wrapper<SearchSpace>> SSref;
76
77 Program() = default;
78 Program(const std::reference_wrapper<SearchSpace> s, const tree<Node> t)
79 : Tree(t), is_fitted_(false)
80 {
81 SSref = std::optional<std::reference_wrapper<SearchSpace>>{s};
82 }
83
84 Program<PType> copy() { return Program<PType>(*this); }
85
86 inline void set_search_space(const std::reference_wrapper<SearchSpace> s)
87 {
88 SSref = std::optional<std::reference_wrapper<SearchSpace>>{s};
89 }
90
93 int complexity() const{
94 auto head = Tree.begin();
95
96 return head.node->get_complexity();
97 }
98
101 int linear_complexity() const{
102 auto head = Tree.begin();
103
104 return head.node->get_linear_complexity();
105 }
106
110 int size(bool include_weight=true) const{
111 auto head = Tree.begin();
112
113 return head.node->get_size(include_weight);
114 }
115
121 int size_at(Iter& top, bool include_weight=true) const{
122
123 return top.node->get_size(include_weight);
124 }
125
128 int depth() const{
129 //tree.hh count the number of edges. We need to ensure that a single-node
130 //tree has depth>0
131 return 1+Tree.max_depth();
132 }
133
138 int depth_at(Iter& top) const{
139 return 1+Tree.max_depth(top);
140 }
141
146 int depth_to_reach(Iter& top) const{
147 return 1+Tree.depth(top);
148 }
149
151 {
152 return fit(d, {});
153 }
154
157 const vector<float>& class_weights)
158 {
159 TreeType out = Tree.begin().node->fit<TreeType>(d);
160 this->is_fitted_ = true;
161 update_weights(d, class_weights);
162 // this->valid = true;
163 return *this;
164 };
165
173 {
174 this->Tree = new_program.Tree;
175 this->is_fitted_ = false;
176
177 // Update search space reference if the new program has one, otherwise keep current
178 if (new_program.SSref.has_value())
179 this->SSref = new_program.SSref;
180 // If new_program doesn't have a search space, keep this->SSref as is
181
182 return *this;
183 };
184
192 {
193 Program<PType> new_program = j;
194 return replace_program(new_program);
195 };
196
197 template <typename R, typename W>
198 R predict_with_weights(const Dataset &d, const W** weights)
199 {
200 if (!is_fitted_)
201 HANDLE_ERROR_THROW("Program is not fitted. Call 'fit' first.\n");
202
203 return Tree.begin().node->predict<R>(d, weights);
204 };
205
206 auto predict_with_weights(const Dataset &d, const ArrayXf& weights)
207 {
208 float const * wptr = weights.data();
209 return this->predict_with_weights<RetType>(d, &wptr);
210 };
211
220 template <typename R = RetType>
221 TreeType predict(const Dataset &d) requires(is_same_v<R, TreeType>)
222 {
223 if (!is_fitted_)
224 HANDLE_ERROR_THROW("Program is not fitted. Call 'fit' first.\n");
225
226 return Tree.begin().node->predict<TreeType>(d);
227 };
228
233 template <typename R = RetType>
234 ArrayXb predict(const Dataset &d) requires(is_same_v<R, ArrayXb>)
235 {
236 if (!is_fitted_)
237 HANDLE_ERROR_THROW("Program is not fitted. Call 'fit' first.\n");
238
239 return (Tree.begin().node->predict<TreeType>(d) > 0.5);
240 };
241
246 template <typename R = RetType>
247 ArrayXi predict(const Dataset &d) requires(is_same_v<R, ArrayXi>)
248 {
249 if (!is_fitted_)
250 HANDLE_ERROR_THROW("Program is not fitted. Call 'fit' first.\n");
251
252 TreeType out = Tree.begin().node->predict<TreeType>(d);
253 auto argmax = Function<NodeType::ArgMax>{};
254 return argmax(out);
255 };
256
257 // template <typename R = RetType>
258 template <PT P = PType>
259 requires((P == PT::BinaryClassifier) || (P == PT::MulticlassClassifier))
261 {
262 return predict<TreeType>(d);
263 };
264
269 Program<PType>& fit(const Ref<const ArrayXXf>& X, const Ref<const ArrayXf>& y)
270 {
271 Dataset d(X,y);
272 return fit(d);
273 };
274
278 RetType predict(const Ref<const ArrayXXf>& X)
279 {
280 Dataset d(X);
281 return predict(d);
282 };
283
291 template <PT P = PType>
292 requires((P == PT::BinaryClassifier) || (P == PT::MulticlassClassifier))
293 TreeType predict_proba(const Ref<const ArrayXXf>& X)
294 {
295 Dataset d(X);
296 return predict_proba(d);
297 };
298
304 void update_weights(const Dataset& d,
305 const vector<float>& class_weights);
306
308 int get_n_weights() const
309 {
310 int count=0;
311 // check tree nodes for weights
312 for (PostIter i = Tree.begin_post(); i != Tree.end_post(); ++i)
313 {
314 const auto& node = i.node->data;
315
316 // we do not want to include fixed weights, because they should not be changed.
317 // get_n_weights, get_weights, and set_weights, are functions used in
318 // parameter optimization --- we can simply make weights invisible to parameter
319 // optimization, and they will not be changed (so they remain fixed).
320 if (node.weight_is_fixed)
321 continue;
322
323 // some nodes cannot have their weights optimized, others must have.
324 // It is important that this condition also matches the condition in
325 // the methods get_weights and set_weights.
326 if (Is<NodeType::OffsetSum>(node.node_type)
327 || (node.get_is_weighted() && IsWeighable(node.ret_type)
328 && IsWeighable(node.node_type)) )
329 ++count;
330 }
331 return count;
332 }
333
339 ArrayXf get_weights()
340 {
341 ArrayXf weights(get_n_weights());
342 int i = 0;
343 for (PostIter t = Tree.begin_post(); t != Tree.end_post(); ++t)
344 {
345 const auto& node = t.node->data;
346
347 // skip fixed weights (this also avoid changing offsetSum weight if is locked)
348 if (node.weight_is_fixed)
349 continue;
350
351 if ( Is<NodeType::OffsetSum>(node.node_type)
352 || (node.get_is_weighted() && IsWeighable(node.ret_type)
353 && IsWeighable(node.node_type)) )
354 {
355 weights(i) = node.W;
356 ++i;
357 }
358 }
359 return weights;
360 }
361
368 void set_weights(const ArrayXf& weights)
369 {
370 // take the weights set them in the tree.
371 // return the weights of the tree as an array
372 if (weights.size() != get_n_weights())
373 HANDLE_ERROR_THROW("Tried to set_weights of incorrect size");
374
375 int j = 0;
376 for (PostIter i = Tree.begin_post(); i != Tree.end_post(); ++i)
377 {
378 auto& node = i.node->data;
379
380 // skip fixed weights (this also avoid changing offsetSum weight if is locked)
381 if (node.weight_is_fixed)
382 continue;
383
384 if ( Is<NodeType::OffsetSum>(node.node_type)
385 || (node.get_is_weighted() && IsWeighable(node.ret_type)
386 && IsWeighable(node.node_type)) )
387 {
388 node.W = weights(j);
389 ++j;
390 }
391 }
392 }
393
404 void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true, bool keep_current_weights=false)
405 {
406 // This also support unlocking the ndoes by setting 0, true, false.
407 // OBS for unlocking the node: Do not change prob_change here, because some
408 // nodes are meant to never be replaced (e.g. logistic root).
409 // unlocking and changing probabilities are different things.
410 // every `if` performing a lock should have its counterpart `else` performing
411 // unlocking.
412
413 // iterate over the nodes, locking them if their depth does not exceed end_depth.
414 if (end_depth<0) {
415 return;
416 }
417
418 // we need the iterator to calculate the depth, but
419 // the lambda below iterate using nodes. So we are creating an iterator
420 // and using it to access depth.
421 auto tree_iter = Tree.begin();
422
423 std::for_each(Tree.begin(), Tree.end(),
424 [&](auto& n){
425 auto d = Tree.depth(tree_iter);
426 std::advance(tree_iter, 1);
427
428 // weights (this will work for all nodes)
429 if (n.get_is_weighted() && d<end_depth) // this will lock and unlock weights!
430 n.weight_is_fixed = keep_current_weights;
431 else{
432 n.weight_is_fixed = false;
433 }
434
435 // leaves (terminals, constants, and splitbest variables)
436 if (IsLeaf(n.node_type)) {
437 if (d<end_depth && !keep_leaves_unlocked)
438 n.node_is_fixed = true; // leaves should be locked based on depth
439 else {
440 // either we are outside end_depth or leaves should be unlocked.
441 // this `else` also helps unlocking th enode
442 n.node_is_fixed = false;
443 }
444
445 return; // stop here
446 }
447
448 // non-terminal nodes
449 if (d<end_depth) // This if-else could be a single line, but im trying to make it readable
450 n.node_is_fixed = true;
451 else // this else clausure helps unlocking the node
452 n.node_is_fixed = false;
453
454 // special case - split best nodes.
455 // If we are skipping leaves, then the split feature is unlocked;
456 // Otherwise, then we lock based on depth.
457 if (n.node_type==NodeType::SplitBest)
458 {
459 if (keep_leaves_unlocked)
460 {
461 n.set_keep_split_feature(false);
462 }
463 else // leaves can be locked
464 {
465 // check if we should lock based on depth
466 n.set_keep_split_feature(d+1<end_depth);
467 }
468 }
469 }
470 );
471 }
472
485 string get_model(string fmt="compact", bool pretty=false) const
486 {
487 auto head = Tree.begin();
488 if (fmt=="tree")
489 return head.node->get_tree_model(pretty);
490 else if (fmt=="dot")
491 return get_dot_model(); ;
492 return head.node->get_model(pretty);
493 }
494
501 string get_dot_model(string extras="") const
502 {
503 // TODO: make node IDs stable (hash or index) and labels reflect nodetype names.
504 // ref: https://stackoverflow.com/questions/10579041/graphviz-create-new-node-with-this-same-label#10579155
505 string out = "digraph G {\n";
506 if (! extras.empty())
507 out += fmt::format("{}\n", extras);
508
509 auto get_id = [](const auto& n){
510 if (Is<NodeType::Terminal>(n->data.node_type))
511 return n->data.get_name(false);
512
513 return fmt::format("{}",fmt::ptr(n)).substr(2);
514 };
515 // bool first = true;
516 std::map<string, unsigned int> node_count;
517 int i = 0;
518 for (Iter iter = Tree.begin(); iter!=Tree.end(); iter++)
519 {
520 const auto& parent = iter.node;
521 // const auto& parent_data = iter.node->data;
522
523 string parent_id = get_id(parent);
524 // if (Is<NodeType::Terminal>(parent_data.node_type))
525 // parent_id = parent_data.get_name(false);
526 // else{
527 // parent_id = fmt::format("{}",fmt::ptr(iter.node)).substr(2);
528 // }
529 // // parent_id = parent_id.substr(2);
530
531 // This is for the root --------------------------------------------
532 // if the first node is weighted, make a dummy output node so that the
533 // first node's weight can be shown
534 if (i==0 && parent->data.get_is_weighted())
535 {
536 // making the weight red if fixed
537 string font_color = "";
538 if (parent->data.weight_is_fixed) {
539 font_color = ", fontcolor=lightcoral";
540 }
541
542 out += "y [shape=box];\n";
543 out += fmt::format("y -> \"{}\" [label=\"{:.2f}\"{}];\n",
544 // parent_data.get_name(false),
545 parent_id,
546 parent->data.W,
547 font_color
548 );
549 }
550
551 // add the node
552 bool is_constant = Is<NodeType::Constant, NodeType::MeanLabel>(parent->data.node_type);
553 string node_label = parent->data.get_name(is_constant);
554
555 if (Is<NodeType::SplitBest>(parent->data.node_type)) {
556 std::string feature = parent->data.get_feature();
557 std::string threshold = fmt::format("{:.2f}", parent->data.W);
558
559 // Append markers for fixed flags
560 if (parent->data.keep_split_feature)
561 feature += "^"; // split feature fixed
562 if (parent->data.weight_is_fixed)
563 threshold += "*"; // split weight fixed
564
565 node_label = fmt::format("{} >= {}?", feature, threshold);
566 }
567 if (Is<NodeType::OffsetSum>(parent->data.node_type)){
568 node_label = fmt::format("Add");
569 }
570
571 string node_style = parent->data.get_prob_change() >0.0 ? "" : ", style=filled, fillcolor=lightcoral";
572 out += fmt::format("\"{}\" [label=\"{}\"{}];\n", parent_id, node_label, node_style);
573
574 // add edges to the node's children
575 auto kid = iter.node->first_child;
576 for (int j = 0; j < iter.number_of_children(); ++j)
577 {
578 string edge_label="";
579 string head_label="";
580 string tail_label="";
581 bool use_head_tail_labels = false;
582
583 string kid_id = get_id(kid);
584 // string kid_id = fmt::format("{}",fmt::ptr(kid));
585 // kid_id = kid_id.substr(2);
586
587 if (kid->data.get_is_weighted()
589 NodeType::OffsetSum, NodeType::SplitBest>(kid->data.node_type))
590 {
591 edge_label = fmt::format("{:.2f}",kid->data.W);
592 }
593
594 if (Is<NodeType::SplitOn>(parent->data.node_type)){
595 use_head_tail_labels=true;
596 if (j == 0)
597 tail_label = fmt::format(">= {:.2f}",parent->data.W);
598 else if (j==1)
599 tail_label = "Y";
600 else
601 tail_label = "N";
602
603 head_label=edge_label;
604 }
605 else if (Is<NodeType::SplitBest>(parent->data.node_type)){
606 use_head_tail_labels=true;
607 if (j == 0){
608 tail_label = "Y";
609 }
610 else
611 tail_label = "N";
612
613 head_label = edge_label;
614 }
615 else if (Is<NodeType::Softmax>(parent->data.node_type)){
616 // Each child supplies logits for one output class.
617 edge_label = fmt::format("class {}", j);
618 }
619
620 // drawing the edges
621 string font_color = "";
622 if (kid->data.weight_is_fixed) {
623 font_color = ", fontcolor=lightcoral";
624 }
625
626 if (use_head_tail_labels){
627 out += fmt::format("\"{}\" -> \"{}\" [headlabel=\"{}\",taillabel=\"{}\"{}];\n",
628 parent_id,
629 kid_id,
630 head_label,
631 tail_label,
632 font_color
633 );
634 }
635 else{
636 out += fmt::format("\"{}\" -> \"{}\" [label=\"{}\"{}];\n",
637 parent_id,
638 kid_id,
639 edge_label,
640 font_color
641 );
642 }
643 kid = kid->next_sibling;
644 }
645
646 // adding the offset as the last child
647 if (Is<NodeType::OffsetSum>(parent->data.node_type)){
648 // drawing the edge
649 out += fmt::format("\"{}\" -> \"{}\" [label=\"\"];\n",
650 parent_id,
651 parent_id+"Offset"
652 );
653
654 // drawing the node
655 out += fmt::format("\"{}\" [label=\"{:.2f}\"{}];\n",
656 parent_id+"Offset",
657 parent->data.W,
658 node_style
659 );
660 }
661
662 ++i;
663 }
664
665 out += "label=\"^ split feature fixed, * split threshold fixed\";\nlabelloc=bottom;\nfontsize=10;";
666 out += "}\n";
667
668 return out;
669 }
670
673 vector<Node> linearize() const {
674 vector<Node> linear_program;
675 for (PostIter i = Tree.begin_post(); i != Tree.end_post(); ++i)
676 linear_program.push_back(i.node->data);
677 return linear_program;
678 }
679}; // Program
680} // Brush
681
683// weight optimization
685// #include "../variation.h"
686namespace Brush{
687
688template<ProgramType PType>
690 const vector<float>& class_weights)
691{
692 // Updates the weights within a tree.
693 // make an optimizer
694 auto WO = WeightOptimizer();
695 // get new weights from optimization.
696 WO.update((*this), d, class_weights);
697};
698
699
701// serialization
702// serialization for program
703template<ProgramType PType>
704void to_json(json &j, const Program<PType> &p)
705{
706 j = json{{"Tree",p.Tree}, {"is_fitted_", p.is_fitted_}};
707}
708
709template<ProgramType PType>
710void from_json(const json &j, Program<PType>& p)
711{
712 j.at("Tree").get_to(p.Tree);
713 j.at("is_fitted_").get_to(p.is_fitted_);
714}
715
716}//namespace Brush
717
718
719
720#endif
holds variable type data.
Definition data.h:51
#define HANDLE_ERROR_THROW(err)
Definition error.h:27
< nsga2 selection operator for getting the front
Definition bandit.cpp:3
auto Isnt(DataType dt) -> bool
Definition node.h:48
Eigen::Array< bool, Eigen::Dynamic, 1 > ArrayXb
Definition types.h:39
auto IsWeighable() noexcept -> bool
Definition node.h:51
ProgramType PT
Definition program.h:39
void from_json(const json &j, Fitness &f)
Definition fitness.cpp:31
auto IsLeaf(NodeType nt) noexcept -> bool
Definition node.h:324
auto Is(NodeType nt) -> bool
Definition node.h:317
tree< Node >::pre_order_iterator Iter
Definition program.h:36
tree< Node >::post_order_iterator PostIter
Definition program.h:37
void to_json(json &j, const Fitness &f)
Definition fitness.cpp:6
Eigen::Array< int, Eigen::Dynamic, 1 > ArrayXi
Definition types.h:40
ProgramType
Definition types.h:70
An individual program, a.k.a. model.
Definition program.h:49
Program(const std::reference_wrapper< SearchSpace > s, const tree< Node > t)
Definition program.h:78
std::conditional_t< PType==PT::BinaryClassifier, ArrayXf, std::conditional_t< PType==PT::MulticlassClassifier, ArrayXXf, RetType > > TreeType
the type of output from the tree object
Definition program.h:61
Program< PType > & replace_program(const json &j)
Replace the current program from a JSON representation, invalidating fitness.
Definition program.h:191
TreeType predict_proba(const Dataset &d)
Definition program.h:260
void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true, bool keep_current_weights=false)
Iterates over the program, locking the nodes until it reaches a certain depth. If the node is a Split...
Definition program.h:404
typename std::conditional_t< PType==PT::Regressor, ArrayXf, std::conditional_t< PType==PT::BinaryClassifier, ArrayXb, std::conditional_t< PType==PT::MulticlassClassifier, ArrayXi, std::conditional_t< PType==PT::Representer, ArrayXXf, ArrayXf > > > > RetType
the return type of the tree when calling :func:predict.
Definition program.h:54
std::optional< std::reference_wrapper< SearchSpace > > SSref
Definition program.h:75
Program< PType > & fit(const Dataset &d, const vector< float > &class_weights)
Fit the program with optional per-class weights for classification.
Definition program.h:156
int linear_complexity() const
count the linear complexity of the program.
Definition program.h:101
RetType predict(const Ref< const ArrayXXf > &X)
Convenience function to call predict directly from X data.
Definition program.h:278
void set_weights(const ArrayXf &weights)
Set the weights in the tree from an array of weights.
Definition program.h:368
void update_weights(const Dataset &d, const vector< float > &class_weights)
TreeType predict(const Dataset &d)
the standard predict function. Returns the output of the Tree directly.
Definition program.h:221
static constexpr PT program_type
Definition program.h:51
R predict_with_weights(const Dataset &d, const W **weights)
Definition program.h:198
int complexity() const
count the (recursive) complexity of the program.
Definition program.h:93
Program< PType > & replace_program(const Program< PType > &new_program)
Replace the current program with a new program, invalidating fitness.
Definition program.h:172
int size_at(Iter &top, bool include_weight=true) const
count the size of a given subtree, optionally including the weights in weighted nodes....
Definition program.h:121
ArrayXf get_weights()
Get the weights of the tree as an array.
Definition program.h:339
Program< PType > & fit(const Dataset &d)
Definition program.h:150
int depth() const
count the tree depth of the program. The depth is not influenced by weighted nodes.
Definition program.h:128
auto predict_with_weights(const Dataset &d, const ArrayXf &weights)
Definition program.h:206
int get_n_weights() const
returns the number of weights in the program.
Definition program.h:308
int depth_at(Iter &top) const
count the depth of a given subtree. The depth is not influenced by weighted nodes....
Definition program.h:138
vector< Node > linearize() const
turns program tree into a linear program.
Definition program.h:673
ArrayXi predict(const Dataset &d)
Specialized predict function for multiclass classification.
Definition program.h:247
void set_search_space(const std::reference_wrapper< SearchSpace > s)
Definition program.h:86
ArrayXb predict(const Dataset &d)
Specialized predict function for binary classification.
Definition program.h:234
string get_dot_model(string extras="") const
Definition program.h:501
Program()=default
string get_model(string fmt="compact", bool pretty=false) const
Get the model as a string.
Definition program.h:485
TreeType predict_proba(const Ref< const ArrayXXf > &X)
Predict probabilities from X.
Definition program.h:293
int depth_to_reach(Iter &top) const
count the depth until reaching the given subtree. The depth is not influenced by weighted nodes....
Definition program.h:146
Program< PType > copy()
Definition program.h:84
Program< PType > & fit(const Ref< const ArrayXXf > &X, const Ref< const ArrayXf > &y)
Convenience function to call fit directly from X,y data.
Definition program.h:269
int size(bool include_weight=true) const
count the tree size of the program, including the weights in weighted nodes.
Definition program.h:110
Holds a search space, consisting of operations and terminals and functions, and methods to sample tha...