From 53a62ac39ac7c9b83c825bfd7a705fbc85901d44 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 2 Nov 2025 14:19:32 -0300 Subject: [PATCH 01/30] initialize_with_decision_trees argument. default False (preserving previous behavior) --- pybrush/EstimatorInterface.py | 8 ++++ src/bindings/bind_params.cpp | 1 + src/params.h | 8 ++++ src/vary/search_space.cpp | 39 +++++++++++++---- src/vary/search_space.h | 16 ++++--- tests/python/test_params.py | 82 +++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 14 deletions(-) diff --git a/pybrush/EstimatorInterface.py b/pybrush/EstimatorInterface.py index 58038d3e..00af7fc8 100644 --- a/pybrush/EstimatorInterface.py +++ b/pybrush/EstimatorInterface.py @@ -106,6 +106,9 @@ class EstimatorInterface(): The inexact simplification algorithm works by mapping similar expressions to the same hash, and retrieving the simplest one when doing the simplification of an expression. + start_from_decision_trees: boolean, optional (default: false) + Whether the initial population should only contain decision trees + (that is, trees using only SplitOn and SplitBest operators). batch_size : float, default 1.0 Percentage of training data to sample every generation. If `1.0`, then all data is used. Very small values can improve execution time, but @@ -203,6 +206,7 @@ def __init__(self, validation_size: float = 0.2, constants_simplification=True, inexact_simplification=True, + start_from_decision_trees=False, batch_size: float = 1.0, sel: str = "lexicase", surv: str = "nsga2", @@ -238,6 +242,7 @@ def __init__(self, self.objectives = objectives self.constants_simplification=constants_simplification self.inexact_simplification=inexact_simplification + self.start_from_decision_trees=start_from_decision_trees self.scorer = scorer self.shuffle_split = shuffle_split self.initialization = initialization @@ -319,6 +324,9 @@ def _wrap_parameters(self, y, **extra_kwargs): params.max_stall = self.max_stall params.max_time = self.max_time + # Initial population + params.start_from_decision_trees = self.start_from_decision_trees + # Sampling probabilities params.weights_init = self.weights_init params.bandit = self.bandit diff --git a/src/bindings/bind_params.cpp b/src/bindings/bind_params.cpp index 653607d7..775fbcb5 100644 --- a/src/bindings/bind_params.cpp +++ b/src/bindings/bind_params.cpp @@ -27,6 +27,7 @@ void bind_params(py::module& m) .def_property("num_islands", &Brush::Parameters::get_num_islands, &Brush::Parameters::set_num_islands) .def_property("constants_simplification", &Brush::Parameters::get_constants_simplification, &Brush::Parameters::set_constants_simplification) .def_property("inexact_simplification", &Brush::Parameters::get_inexact_simplification, &Brush::Parameters::set_inexact_simplification) + .def_property("start_from_decision_trees", &Brush::Parameters::get_start_from_decision_trees, &Brush::Parameters::set_start_from_decision_trees) .def("set_n_classes", &Brush::Parameters::set_n_classes) .def("set_class_weights", &Brush::Parameters::set_class_weights) .def("set_class_weights_type", &Brush::Parameters::set_class_weights_type) diff --git a/src/params.h b/src/params.h index 3924fa2a..83ddbe8b 100644 --- a/src/params.h +++ b/src/params.h @@ -48,6 +48,9 @@ struct Parameters bool constants_simplification=true; bool inexact_simplification=true; + // population initialization + bool start_from_decision_trees=false; + // variation std::map mutation_probs = { {"point", 0.167}, @@ -244,6 +247,9 @@ struct Parameters string get_class_weights_type(){ return class_weights_type; }; void set_class_weights_type(string cwt){ class_weights_type = cwt; }; + bool get_start_from_decision_trees(){ return start_from_decision_trees; }; + void set_start_from_decision_trees(bool start_dt){ start_from_decision_trees = start_dt; }; + void set_validation_size(float s){ validation_size = s; }; float get_validation_size(){ return validation_size; }; @@ -302,6 +308,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Parameters, classification, n_classes, + start_from_decision_trees, + shuffle_split, validation_size, feature_names, diff --git a/src/vary/search_space.cpp b/src/vary/search_space.cpp index aed8d86c..062862d8 100644 --- a/src/vary/search_space.cpp +++ b/src/vary/search_space.cpp @@ -223,10 +223,21 @@ void SearchSpace::init(const Dataset& d, const unordered_map& user if (!use_all) { + // Brush can start from a population of decision trees. Thess `if`s + // below ensures that split nodes will always be in the search space + if (extended_user_ops.find("SplitOn") == extended_user_ops.end()){ + extended_user_ops.insert({"SplitOn", 0.0f}); + op_names.push_back("SplitOn"); + } + if (extended_user_ops.find("SplitBest") == extended_user_ops.end()){ + extended_user_ops.insert({"SplitBest", 0.0f}); + op_names.push_back("SplitBest"); + } + // We need some ops in the search space so we can have the logit and offset if (extended_user_ops.find("OffsetSum") == extended_user_ops.end()){ - extended_user_ops.insert({"OffsetSum", 0.0f}); - op_names.push_back("OffsetSum"); + extended_user_ops.insert({"OffsetSum", 0.0f}); + op_names.push_back("OffsetSum"); } // Convert ArrayXf to std::vector for compatibility with std::set @@ -234,12 +245,12 @@ void SearchSpace::init(const Dataset& d, const unordered_map& user std::set unique_classes(vec.begin(), vec.end()); if (unique_classes.size()==2 && (extended_user_ops.find("Logistic") == extended_user_ops.end())) { - extended_user_ops.insert({"Logistic", 0.0f}); - op_names.push_back("Logistic"); + extended_user_ops.insert({"Logistic", 0.0f}); + op_names.push_back("Logistic"); } else if (extended_user_ops.find("Softmax") == extended_user_ops.end()) { - extended_user_ops.insert({"Softmax", 0.0f}); - op_names.push_back("Softmax"); + extended_user_ops.insert({"Softmax", 0.0f}); + op_names.push_back("Softmax"); } } @@ -297,13 +308,13 @@ std::optional> SearchSpace::sample_subtree(Node root, int max_d, int // we should notice the difference between size of a PROGRAM and a TREE. // program count weights in its size, while the TREE structure dont. Wenever // using size of a program/tree, make sure you use the function from the correct class - PTC2(Tree, spot, max_d, max_size); + PTC2(Tree, spot, max_d, max_size, false); return Tree; }; tree& SearchSpace::PTC2(tree& Tree, - tree::iterator spot, int max_d, int max_size) const + tree::iterator spot, int max_d, int max_size, bool start_from_decision_trees) const { // PTC2 is agnostic of program type @@ -377,7 +388,17 @@ tree& SearchSpace::PTC2(tree& Tree, else { //choose a nonterminal of matching type - auto opt = sample_op(t); + std::optional opt; + + if (start_from_decision_trees) { // + // this sample_op is likely to never fail. Only case scenario is + // a logistic root (arrayF arg type) with no arrayF arguments. + // (in this case, sample_terminal will also fail) + opt = sample_op(NodeType::SplitBest, t, true); + } + else { + opt = sample_op(t); + } if (!opt) { // there is no operator for this node. sample a terminal instead opt = sample_terminal(t); diff --git a/src/vary/search_space.h b/src/vary/search_space.h index 6913db6b..a9971393 100644 --- a/src/vary/search_space.h +++ b/src/vary/search_space.h @@ -458,7 +458,7 @@ struct SearchSpace /// @param type the node type /// @param R the return type /// @return `std::optional` that may contain a Node of type `type` with return type `R`. - std::optional sample_op(NodeType type, DataType R, bool force_return=false) + std::optional sample_op(NodeType type, DataType R, bool force_return=false) const { check(R); if (node_map.find(R) == node_map.end()) @@ -617,7 +617,7 @@ struct SearchSpace }; private: - tree& PTC2(tree& Tree, tree::iterator root, int max_d, int max_size) const; + tree& PTC2(tree& Tree, tree::iterator root, int max_d, int max_size, bool start_from_decision_trees) const; template requires (!is_in_v) @@ -772,8 +772,14 @@ P SearchSpace::make_program(const Parameters& params, int max_d, int max_size) Node root; std::optional opt=std::nullopt; - if (max_size>1 && max_d>1) - opt = sample_op(root_type); + if (max_size>1 && max_d>1){ + if (params.start_from_decision_trees) { + opt = sample_op(NodeType::SplitBest, root_type, true); + } + else { + opt = sample_op(root_type); + } + } if (!opt) opt = sample_terminal(root_type, true); @@ -784,7 +790,7 @@ P SearchSpace::make_program(const Parameters& params, int max_d, int max_size) } // max_d-1 because we always pick the root before calling ptc2 - PTC2(Tree, spot, max_d-1, max_size); // change inplace + PTC2(Tree, spot, max_d-1, max_size, params.start_from_decision_trees); // change inplace // weighting the tree if classification problem (so it can optimize the scale by default) // if (P::program_type == ProgramType::BinaryClassifier diff --git a/tests/python/test_params.py b/tests/python/test_params.py index bf20d316..59918a7d 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -154,3 +154,85 @@ def test_class_weights(): print(f"Best individual program: {clf.best_estimator_.program.get_model()}") print(f"Best individual fitness: {clf.best_estimator_.fitness}") print(f"Best individual score (acc): {clf.score(X, y)}") + + +def _collect_models_from_estimator(estimator): + return [ind.program.get_model() for ind in estimator.population_] + + +def _has_split_node_in_models(models): + for m in models: + if "If" in m: + return True + return False + + +def test_population_split_nodes_start_from_decision_trees_on_and_off(): + y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1]) + + # multiply by a float so X is filled with float values + X = np.vstack([np.linspace(0, 1, 10), np.linspace(1, 2, 10)]).T* np.e + + for start_trees in (True, False): + print(f"\nTesting start_from_decision_trees={start_trees}") + + # create a very small run (1 generation, small pop) so splits dont get lost completely + reg = BrushRegressor( + max_gens=0, + pop_size=10, + start_from_decision_trees=start_trees, + verbosity=2, + ).fit(X, y) + + models = _collect_models_from_estimator(reg) + print(f"Collected {len(models)} model(s) for start_from_decision_trees={start_trees}") + for i, m in enumerate(models): + print(f"Individual {i}: {m}") + + # If starting from decision trees we expect at least one split-like node + if start_trees: + assert _has_split_node_in_models(models), ( + "Expected at least one individual to contain a split node when " + "start_from_decision_trees=True" + ) + else: + # When not starting from trees it's acceptable not to have split nodes, + # but we still ensure we collected at least one model string. + assert len(models) > 0, "No individuals were collected from the population" + + +def test_population_split_nodes_with_and_without_SplitOn_function(): + y = np.array([1, 1, 0, 0, 1, 0, 1, 0, 1, 0]) + + # multiply by a float so X is filled with float values + X = np.vstack([np.arange(10), np.arange(10)[::-1]]).T * np.e + + configs = [ + ("with_Splits", ["SplitOn", "SplitBest", "Add", "Mul"]), + ("without_Splits", ["Add", "Mul", "Logistic"]), + ] + + for name, functions in configs: + print(f"\nTesting functions config: {name} -> {functions}") + clf = BrushClassifier( + max_gens=0, + pop_size=10, + functions=functions, + start_from_decision_trees=True, + verbosity=2, + validation_size=0, + ).fit(X, y) + + models = _collect_models_from_estimator(clf) + + print(f"Collected {len(models)} model(s) for config {name}") + for i, m in enumerate(models): + print(f"Individual {i}: {m}") + + if "SplitOn" in functions or "SplitBest" in functions: + assert _has_split_node_in_models(models), ( + f"Expected split nodes present when 'SplitOn' or 'SplitBest', is in functions ({functions})" + ) + else: + # If SplitOn not provided, ensure at least population exists but do not require splits + assert len(models) > 0, f"No individuals collected for config {name}" From 998ebd99045b75177395b8108eb45693bb8db370 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Mon, 3 Nov 2025 08:00:27 -0300 Subject: [PATCH 02/30] Better error message --- src/vary/search_space.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vary/search_space.cpp b/src/vary/search_space.cpp index 062862d8..7a6b1aa2 100644 --- a/src/vary/search_space.cpp +++ b/src/vary/search_space.cpp @@ -408,8 +408,16 @@ tree& SearchSpace::PTC2(tree& Tree, opt = sample_terminal(t, true); } - if (!opt) { // no operator nor terminal. weird. - auto msg = fmt::format("Failed to sample operator AND terminal of data type {} during PTC2.\n", DataTypeName[t]); + if (!opt) { // no operator or terminal - search space is ill defined + auto msg = fmt::format( + "Failed to sample operator AND terminal of data type {} during PTC2.\n" + "start_from_decision_trees: {}\nops_available: {}\nterminals_available: {}\n", + DataTypeName[t], + start_from_decision_trees ? "true" : "false", + ops_count, + terms_count + ); + HANDLE_ERROR_THROW(msg); // queue.push_back(make_tuple(qspot, t, d)); From 7be971087b6e7bce6c535ab129f4120fe8059fd1 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Mon, 3 Nov 2025 08:24:45 -0300 Subject: [PATCH 03/30] Fixed print of invalid variables --- src/vary/search_space.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vary/search_space.cpp b/src/vary/search_space.cpp index 7a6b1aa2..c885df77 100644 --- a/src/vary/search_space.cpp +++ b/src/vary/search_space.cpp @@ -411,11 +411,9 @@ tree& SearchSpace::PTC2(tree& Tree, if (!opt) { // no operator or terminal - search space is ill defined auto msg = fmt::format( "Failed to sample operator AND terminal of data type {} during PTC2.\n" - "start_from_decision_trees: {}\nops_available: {}\nterminals_available: {}\n", + "start_from_decision_trees: {}\n", DataTypeName[t], - start_from_decision_trees ? "true" : "false", - ops_count, - terms_count + start_from_decision_trees ? "true" : "false" ); HANDLE_ERROR_THROW(msg); From f3e2bba837d76abb2228c1e6364dc9a58ed6d753 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Tue, 4 Nov 2025 10:56:20 -0300 Subject: [PATCH 04/30] Updating the auprc metric. Still need some work --- src/eval/metrics.cpp | 37 ++++++++++++++++++++++++++----------- src/ind/fitness.h | 4 ++-- src/vary/variation.h | 1 - 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/eval/metrics.cpp b/src/eval/metrics.cpp index cbf0cd18..06611ca6 100644 --- a/src/eval/metrics.cpp +++ b/src/eval/metrics.cpp @@ -169,7 +169,7 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, y_sorted[i] = y(idx); p_sorted[i] = predict_proba(idx); - w_sorted[i] = class_weights.empty() ? 1.0f : class_weights.at(y_sorted[i]); + w_sorted[i] = class_weights.empty() ? 1.0f : class_weights.at(y(idx)); ysum += y_sorted[i] * w_sorted[i]; } @@ -177,15 +177,31 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, // when all scores are the same, the sort order is arbitrary, so the PR curve // you integrate is a staircase instead of a flat line. Sklearn avoids this by // treating ties as one threshold. + // however, this does not produce consistent results, so we will handle flat + // lines below + + // detect constant prediction case (all p_sorted equal within tolerance). + // because p_sorted is sorted, the first element is the maximum, and the last is the minimum, + float tol = 1e-6f; + if (fabs(p_sorted.front() - p_sorted.back()) <= tol) { + // All predictions are (effectively) constant. + float total_weight = 0.0f; + for (int i = 0; i < num_instances; ++i) + total_weight += w_sorted[i]; + + // Return weighted positives / total weight, matching sklearn's result for constant scores. + return total_weight == 0.0f ? 0.0f : ysum / total_weight; + } // Find the indexes where prediction changes, so we can treat it as one block - vector unique_indices = {}; - set unique_probas = {}; // keep track of unique elements + vector unique_indices = {}; // this one will be used to calculate the AUC + set unique_probas = {}; // keep track of unique elements (this wont be used other than that) - for (int i=0; i values; - vector weights; + vector weights = {}; // weighted values - vector wvalues; + vector wvalues = {}; void set_dominated(vector& dom){ dominated=dom; }; vector get_dominated() const { return dominated; }; diff --git a/src/vary/variation.h b/src/vary/variation.h index a7f61547..3acd5f2f 100644 --- a/src/vary/variation.h +++ b/src/vary/variation.h @@ -302,7 +302,6 @@ class Variation { // ind.set_objectives(mom.get_objectives()); // it will have an invalid fitness ind.set_id(id); - ind.fitness.set_loss(mom.fitness.get_loss()); ind.fitness.set_loss_v(mom.fitness.get_loss_v()); ind.fitness.set_size(mom.fitness.get_size()); From c7a55915978390cf339ca468bb92f32e50f6004b Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Tue, 4 Nov 2025 10:56:41 -0300 Subject: [PATCH 05/30] New tests --- tests/python/test_final_model_selection.py | 11 ++++-- tests/python/test_params.py | 46 ++++++++++++++++++++++ tests/python/test_sklearn_interface.py | 8 ++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index 2a09ca71..e9e480df 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -174,14 +174,19 @@ def eval(individual, sample=None): candidates = [(l, p) for l, p in zip(new_losses, est.archive_) if lower_ci <= l <= upper_ci] print('first arch ind', est.archive_[0].get_model()) - print("Original losses from archive", [ind.fitness.loss for ind in est.archive_]) - print("Original losses_v from archive", [ind.fitness.loss_v for ind in est.archive_]) - print("Recalculated losses (should match)", new_losses) + print("Original losses from archive (brush's auprc) ", [ind.fitness.loss for ind in est.archive_]) + print("Original losses_v from archive (brush's auprc) ", [ind.fitness.loss_v for ind in est.archive_]) + print("Recalculated losses with sklearn (should match)", new_losses) print(f"Num candidates in CI: {len(candidates)}") + for i, ind in enumerate(est.archive_): + print(f"archive[{i}] program.get_model(): {ind.program.get_model()}") + # TODO: make the assert below work assert np.allclose([ind.fitness.loss_v for ind in est.archive_], new_losses) + + if candidates: chosen = min(candidates, key=lambda lp: lp[1].fitness.complexity)[1] print("Chosen candidate model:", chosen.get_model()) diff --git a/tests/python/test_params.py b/tests/python/test_params.py index 59918a7d..0d63dc46 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -236,3 +236,49 @@ def test_population_split_nodes_with_and_without_SplitOn_function(): else: # If SplitOn not provided, ensure at least population exists but do not require splits assert len(models) > 0, f"No individuals collected for config {name}" + + +@pytest.mark.parametrize("scorer, expected_weights", [ + # Second objective is always minimization for these test cases + ("mse", [-1.0, -1.0]), # lower is better + ("accuracy", [+1.0, -1.0]), # higher is better + ("balanced_accuracy", [+1.0, -1.0]), # higher is better + ("log", [-1.0, -1.0]), # lower is better + ("average_precision_score", [+1.0, -1.0]), # higher is better +]) +def test_fitness_weights_match_scorer_sign(scorer, expected_weights): + """Ensure fitness.weights has correct sign according to the scorer function, + both at estimator and individual (population) level. + """ + + # simple toy dataset + X = np.array([[1.2, 2.0], [2.0, 3.5], [3.0, 4.0], [4.0, 5.0]]) + y_reg = np.array([1.0, 2.0, 3.0, 4.0]) + y_clf = np.array([0, 1, 0, 1]) + + # Choose estimator type based on scorer + # (by default objectives are ["scorer", "linear_complexity"]) + if scorer in ("mse"): + est = BrushRegressor(scorer=scorer, pop_size=20, max_gens=10, verbosity=0) + est.fit(X, y_reg) + else: + est = BrushClassifier(scorer=scorer, pop_size=20, max_gens=10, verbosity=0) + est.fit(X, y_clf) + + # Check estimator-level weights + print(f"\nTesting scorer={scorer}") + print(f"Estimator fitness.weights={est.best_estimator_.fitness.weights}") + assert np.allclose(est.best_estimator_.fitness.weights, expected_weights), ( + f"For scorer={scorer}, expected fitness.weights={expected_weights}, " + f"but got {est.best_estimator_.fitness.weights}" + ) + + # Check that every individual in the population follows the same sign convention + for i, ind in enumerate(est.population_): + print(f"Individual {i} fitness.weights={ind.fitness.weights}") + + assert hasattr(ind, "fitness"), f"Individual {i} has no fitness attribute" + assert np.isclose(ind.fitness.weights[0], expected_weights[0]), ( + f"For scorer={scorer}, individual {i} has fitness.weights[0]={ind.fitness.weights[0]}, " + f"expected {expected_weights[0]}" + ) diff --git a/tests/python/test_sklearn_interface.py b/tests/python/test_sklearn_interface.py index 96af9c6c..b4e1cad8 100644 --- a/tests/python/test_sklearn_interface.py +++ b/tests/python/test_sklearn_interface.py @@ -114,10 +114,10 @@ def test_brush_classifier_checkpoint_training(tmp_path): est = BrushClassifier( objectives=["scorer", "linear_complexity"], scorer="balanced_accuracy", - max_gens=12, - pop_size=20, - max_depth=10, - max_size=30, + max_gens=10, + pop_size=25, + max_depth=4, + max_size=10, verbosity=0, ) From 68995092f9102f1f1ab8d1005d0aaa3836425814 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Thu, 6 Nov 2025 09:27:49 -0300 Subject: [PATCH 06/30] Avoid generating complex equations in tests --- tests/python/test_final_model_selection.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index e9e480df..c8dbe8e9 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -94,10 +94,11 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh print("Prevalence of y:", prevalence) est = BrushClassifier( - max_gens=50, + max_gens=10, pop_size=50, final_model_selection="best_validation_ci", scorer=scorer, + functions=['Add', 'Sub', 'SplitBest'], class_weights=class_weights, validation_size=0.3, verbosity=0, @@ -182,10 +183,7 @@ def eval(individual, sample=None): for i, ind in enumerate(est.archive_): print(f"archive[{i}] program.get_model(): {ind.program.get_model()}") - # TODO: make the assert below work - assert np.allclose([ind.fitness.loss_v for ind in est.archive_], new_losses) - - + assert np.allclose([ind.fitness.loss_v for ind in est.archive_], new_losses, atol=1e-2) if candidates: chosen = min(candidates, key=lambda lp: lp[1].fitness.complexity)[1] From 73a9aa25d133180eecbf8c134519727394745858 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 9 Nov 2025 16:10:59 -0300 Subject: [PATCH 07/30] Attempt to fix failing test cases of final model selection schema --- pybrush/BrushEstimator.py | 23 ++++++++++++---------- src/engine.cpp | 4 +++- src/eval/metrics.cpp | 11 ++++------- src/eval/scorer.h | 2 ++ tests/python/test_final_model_selection.py | 16 +++++++++------ tests/python/test_sklearn_interface.py | 2 +- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/pybrush/BrushEstimator.py b/pybrush/BrushEstimator.py index c35e4390..1c5d85c6 100644 --- a/pybrush/BrushEstimator.py +++ b/pybrush/BrushEstimator.py @@ -225,6 +225,8 @@ def _update_final_model(self, data=None): if data is None: data = self.validation_ #.get_validation_data() + y = np.array(data.y) + candidate = None if self.final_model_selection == "smallest_complexity": candidates = [p for p in self.archive_ if p.fitness.size > 1 + (4 if self.mode == 'classification' else 0)] @@ -244,48 +246,49 @@ def _update_final_model(self, data=None): } loss_f = loss_f_dict[self.parameters_.scorer] - def eval(ind, data, sample=None): + def eval(ind, sample=None): if sample is None: - sample = np.arange(len(data.y)) + sample = np.arange(len(y)) if self.parameters_.scorer in ["log", "average_precision_score"]: y_pred = np.array(ind.predict_proba(data)) else: # accuracy, balanced accuracy, or regression metrics y_pred = np.array(ind.predict(data)) - y_pred = np.nan_to_num(y_pred) # Protecting the evaluation + # y_pred = np.nan_to_num(y_pred) # Protecting the evaluation # if user_defined, sample_weight is given by his custom weights. if # support, I calculate it here. otherwise, no weight is used if self.class_weights not in ['unbalanced', 'balanced_accuracy']: sample_weight = [] if isinstance(self.class_weights, list): # using user-defined values - sample_weight = [self.class_weights[int(label)] for label in data.y] + sample_weight = [self.class_weights[int(label)] for label in y] else: # support # Calculate class weights by support - classes, counts = np.unique(data.y, return_counts=True) + classes, counts = np.unique(y[sample], return_counts=True) support_weights = { - int(cls): len(data.y) / (len(classes)*count) + int(cls): len(y[sample]) / (len(classes)*count) if count > 0 else 0.0 for cls, count in zip(classes, counts)} - sample_weight = [support_weights[int(label)] for label in data.y] + # classes and support weights are calculated with y[sample]. + # sample_weight will be indexed in the function call, so we use raw y. + sample_weight = [support_weights[int(label)] for label in y] sample_weight = np.array(sample_weight) return loss_f(y[sample], y_pred[sample], sample_weight=sample_weight[sample]) else: # unbalanced metrics, ignoring weights return loss_f(y[sample], y_pred[sample]) - y = np.array(data.y) np.random.seed(0) val_samples = [] for i in range(100): sample = np.random.randint(0, len(y), size=len(y)) - val_samples.append( eval(self.best_estimator_, data, sample) ) + val_samples.append( eval(self.best_estimator_, sample) ) lower_ci, upper_ci = np.quantile(val_samples,0.05), np.quantile(val_samples,0.95) # Recalculate metric with new data - new_losses = [eval(ind, data) for ind in self.archive_] + new_losses = [eval(ind) for ind in self.archive_] # Filter for overlapping points. Adding the best estimator to assert there is at least one sample candidates = [(l, p) for l, p in zip(new_losses, self.archive_) if lower_ci <= l <= upper_ci] diff --git a/src/engine.cpp b/src/engine.cpp index dfee3490..d4608581 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -520,7 +520,9 @@ void Engine::run(Dataset &data) auto finish_gen = subflow.emplace([&]() { // Set validation loss before calling update best for (int island = 0; island < this->params.num_islands; ++island) { - evaluator.update_fitness(this->pop, island, data, params, false, true); + // we can set fit to true, because already fitted individuals will be skipped, + // and we ensure we fit everyone + evaluator.update_fitness(this->pop, island, data, params, true, true); } archive.update(pop, params); diff --git a/src/eval/metrics.cpp b/src/eval/metrics.cpp index 06611ca6..0470102f 100644 --- a/src/eval/metrics.cpp +++ b/src/eval/metrics.cpp @@ -182,14 +182,12 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, // detect constant prediction case (all p_sorted equal within tolerance). // because p_sorted is sorted, the first element is the maximum, and the last is the minimum, - float tol = 1e-6f; - if (fabs(p_sorted.front() - p_sorted.back()) <= tol) { + if (abs(p_sorted.front() - p_sorted.back()) <= eps) { // All predictions are (effectively) constant. - float total_weight = 0.0f; - for (int i = 0; i < num_instances; ++i) - total_weight += w_sorted[i]; + float total_weight = std::accumulate(w_sorted.begin(), w_sorted.end(), 0.0f); - // Return weighted positives / total weight, matching sklearn's result for constant scores. + // Return weighted positives / total weight, matching sklearn's result for constant scores + // (kinda weighted prevalence) return total_weight == 0.0f ? 0.0f : ysum / total_weight; } @@ -221,7 +219,6 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, precision.push_back(relevant == 0.0f ? 0.0f : tp / relevant); recall.push_back(ysum == 0.0f ? 1.0f : tp / ysum); } - } // integrate PR curve diff --git a/src/eval/scorer.h b/src/eval/scorer.h index 27734b54..a745fba9 100644 --- a/src/eval/scorer.h +++ b/src/eval/scorer.h @@ -141,6 +141,8 @@ typedef float (*funcPointer)(const VectorXf&, } else // else it is either unbalanced or user_defined { + // if unbalanced, class_weights is empty. if user_defined, + // then we should use the provided values anyways class_weights = params.class_weights; } diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index c8dbe8e9..3144b2d8 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -116,6 +116,7 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh # Replicate the selection logic here data = est.validation_ y = np.array(data.y).astype(int) + print("Unique values in validation data", np.unique(y, return_counts=True)) loss_f_dict = { "mse": mean_squared_error, @@ -128,14 +129,17 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh def eval(individual, sample=None): if sample is None: - sample = np.arange(len(y)) + sample = np.arange(len(data.y)) y_pred = None + if est.parameters_.scorer in ["log", "average_precision_score"]: - y_pred = np.array(individual.predict_proba(data)) + y_pred = np.array(individual.predict_proba(data)).astype(float) else: - y_pred = np.array(individual.predict(data)) - # y_pred = np.nan_to_num(y_pred) + y_pred = np.array(individual.predict(data)).astype(float) + + # print(np.round(y, 2)) + # print(np.round(y_pred, 2)) if est.class_weights not in ['unbalanced', 'balanced_accuracy']: sample_weight = None @@ -150,7 +154,7 @@ def eval(individual, sample=None): "Sampled data does not have same number of classes" support_weights = { - cls: len(y) / (len(classes)*count) + cls: len(y[sample]) / (len(classes)*count) if count > 0 else 0.0 for cls, count in zip(classes, counts)} sample_weight = np.array([support_weights[label] for label in y]) @@ -159,6 +163,7 @@ def eval(individual, sample=None): return loss_f(y[sample], y_pred[sample]) # Bootstrap validation samples + print("scorer and class weights;", scorer, class_weights) print("original loss", est.best_estimator_.fitness.loss) print("original loss_v", est.best_estimator_.fitness.loss_v) print("recalculated loss", eval(est.best_estimator_)) @@ -196,6 +201,5 @@ def eval(individual, sample=None): # Assert that Brush picked the same candidate assert est.best_estimator_.get_model() == chosen.get_model() - if __name__ == "__main__": pytest.main() \ No newline at end of file diff --git a/tests/python/test_sklearn_interface.py b/tests/python/test_sklearn_interface.py index b4e1cad8..a1b00fd5 100644 --- a/tests/python/test_sklearn_interface.py +++ b/tests/python/test_sklearn_interface.py @@ -115,7 +115,7 @@ def test_brush_classifier_checkpoint_training(tmp_path): objectives=["scorer", "linear_complexity"], scorer="balanced_accuracy", max_gens=10, - pop_size=25, + pop_size=20, max_depth=4, max_size=10, verbosity=0, From 582638618702e873f540752f57496405fd836a31 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 9 Nov 2025 18:16:35 -0300 Subject: [PATCH 08/30] New attempt and improved log messages --- src/engine.cpp | 12 ++++--- src/eval/metrics.cpp | 4 +-- tests/python/test_final_model_selection.py | 41 +++++++++++++++++----- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/engine.cpp b/src/engine.cpp index d4608581..093b5ca3 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -490,7 +490,8 @@ void Engine::run(Dataset &data) // TODO: optimize this and make it work with multiple islands in parallel. for (int island = 0; island < this->params.num_islands; ++island) { - // TODO: do I have to pass data as an argument here? or can I use the instance reference + // TODO: do I have to pass data as an argument here? or can I use the instance reference. + // OBS: this function already calls fit internally! variator.vary_and_update(this->pop, island, island_parents.at(island), data, evaluator, @@ -562,12 +563,13 @@ void Engine::run(Dataset &data) [&]() { return 0; }, // jump back to the next iteration [&](tf::Subflow& subflow) { + // TODO: make sure I do not need to re-fit here and remove this later // set VALIDATION loss for archive, without refitting the model - for (int island = 0; island < this->params.num_islands; ++island) { - evaluator.update_fitness(this->pop, island, data, params, false, true); - } + // for (int island = 0; island < this->params.num_islands; ++island) { + // evaluator.update_fitness(this->pop, island, data, params, false, true); + // } - archive.update(pop, params); + // archive.update(pop, params); // calculate_stats(); if (params.save_population != "") diff --git a/src/eval/metrics.cpp b/src/eval/metrics.cpp index 0470102f..0b522c7b 100644 --- a/src/eval/metrics.cpp +++ b/src/eval/metrics.cpp @@ -140,7 +140,7 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, // Assuming y contains binary labels (0 or 1) int num_instances = y.size(); - float eps = 1e-6f; // first we set the loss vector values + float eps = 1e-4f; // first we set the loss vector values loss.resize(num_instances); for (int i = 0; i < num_instances; ++i) { float p = predict_proba(i); @@ -182,7 +182,7 @@ float average_precision_score(const VectorXf& y, const VectorXf& predict_proba, // detect constant prediction case (all p_sorted equal within tolerance). // because p_sorted is sorted, the first element is the maximum, and the last is the minimum, - if (abs(p_sorted.front() - p_sorted.back()) <= eps) { + if (abs(p_sorted.back() - p_sorted.front()) <= eps) { // All predictions are (effectively) constant. float total_weight = std::accumulate(w_sorted.begin(), w_sorted.end(), 0.0f); diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index 3144b2d8..36202c35 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -115,7 +115,8 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh # Replicate the selection logic here data = est.validation_ - y = np.array(data.y).astype(int) + y = np.array(data.y) + print("Unique values in validation data", np.unique(y, return_counts=True)) loss_f_dict = { @@ -127,10 +128,13 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh } loss_f = loss_f_dict[est.parameters_.scorer] - def eval(individual, sample=None): + def eval(individual, sample=None, log=False): if sample is None: sample = np.arange(len(data.y)) + if log: + print('(eval) sample index', sample) + y_pred = None if est.parameters_.scorer in ["log", "average_precision_score"]: @@ -138,16 +142,24 @@ def eval(individual, sample=None): else: y_pred = np.array(individual.predict(data)).astype(float) - # print(np.round(y, 2)) - # print(np.round(y_pred, 2)) + if log: # silencing eval() during bootstrap, but enabling detailed info when re-calculating losses and comparing with brush's metrics + print('evaluating', individual.program.get_model()) + print(np.round(y, 2)) + print(np.round(y_pred, 2)) if est.class_weights not in ['unbalanced', 'balanced_accuracy']: sample_weight = None if isinstance(est.class_weights, list): - sample_weight = np.array([est.class_weights[label] for label in y]) + if log: + print('(eval) using class weights as a list') + + sample_weight = np.array([est.class_weights[int(label)] for label in y]) # elif est.class_weights == 'support' and est.scorer == "average_precision_score": # using support as a way of weighting # return loss_f(y[sample], y_pred[sample], average='weighted') else: + if log: + print('(eval) using class weights as support') + classes, counts = np.unique(y[sample], return_counts=True) assert len(classes) == est.parameters_.n_classes, \ @@ -156,10 +168,23 @@ def eval(individual, sample=None): support_weights = { cls: len(y[sample]) / (len(classes)*count) if count > 0 else 0.0 for cls, count in zip(classes, counts)} - - sample_weight = np.array([support_weights[label] for label in y]) + + if log: + print("(eval) support weights", support_weights) + + sample_weight = np.array([support_weights[int(label)] for label in y]) + + if log: + print('(eval) sample weights', sample_weight) + print('(eval) loss', loss_f(y[sample], y_pred[sample], sample_weight=sample_weight[sample])) + return loss_f(y[sample], y_pred[sample], sample_weight=sample_weight[sample]) else: # Cases where we ignore weights + if log: + print('(eval) using no class weights') + print('(eval) sample weights not defined. using unbalanced version') + print('(eval) loss', loss_f(y[sample], y_pred[sample])) + return loss_f(y[sample], y_pred[sample]) # Bootstrap validation samples @@ -176,7 +201,7 @@ def eval(individual, sample=None): print(f"CI bounds: {lower_ci:.4f}, {upper_ci:.4f}") # Evaluate all archive members - new_losses = [eval(ind) for ind in est.archive_] + new_losses = [eval(ind, log=True) for ind in est.archive_] candidates = [(l, p) for l, p in zip(new_losses, est.archive_) if lower_ci <= l <= upper_ci] print('first arch ind', est.archive_[0].get_model()) From dac4b4eb71d80e8df4a3315b9168a401d8a21ed6 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 9 Nov 2025 18:44:26 -0300 Subject: [PATCH 09/30] Fixed conditional in final model selection --- pybrush/BrushEstimator.py | 2 +- tests/python/test_final_model_selection.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pybrush/BrushEstimator.py b/pybrush/BrushEstimator.py index 1c5d85c6..121445fc 100644 --- a/pybrush/BrushEstimator.py +++ b/pybrush/BrushEstimator.py @@ -259,7 +259,7 @@ def eval(ind, sample=None): # if user_defined, sample_weight is given by his custom weights. if # support, I calculate it here. otherwise, no weight is used - if self.class_weights not in ['unbalanced', 'balanced_accuracy']: + if self.class_weights not in ['unbalanced'] and self.parameters_.scorer not in ['balanced_accuracy']: sample_weight = [] if isinstance(self.class_weights, list): # using user-defined values sample_weight = [self.class_weights[int(label)] for label in y] diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index 36202c35..4951a595 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -147,7 +147,7 @@ def eval(individual, sample=None, log=False): print(np.round(y, 2)) print(np.round(y_pred, 2)) - if est.class_weights not in ['unbalanced', 'balanced_accuracy']: + if est.class_weights not in ['unbalanced'] and est.parameters_.scorer not in ['balanced_accuracy']: sample_weight = None if isinstance(est.class_weights, list): if log: From cd079d36f64e42b9dcc3fee362214905abf8fe95 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 9 Nov 2025 19:09:37 -0300 Subject: [PATCH 10/30] Trying to make tests run faster --- tests/python/test_final_model_selection.py | 5 +++-- tests/python/test_params.py | 2 +- tests/python/test_sklearn_interface.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index 4951a595..f1645ee8 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -94,8 +94,8 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh print("Prevalence of y:", prevalence) est = BrushClassifier( - max_gens=10, - pop_size=50, + max_gens=1, + pop_size=10, final_model_selection="best_validation_ci", scorer=scorer, functions=['Add', 'Sub', 'SplitBest'], @@ -146,6 +146,7 @@ def eval(individual, sample=None, log=False): print('evaluating', individual.program.get_model()) print(np.round(y, 2)) print(np.round(y_pred, 2)) + print('(sorted)', np.sort(y_pred)) if est.class_weights not in ['unbalanced'] and est.parameters_.scorer not in ['balanced_accuracy']: sample_weight = None diff --git a/tests/python/test_params.py b/tests/python/test_params.py index 0d63dc46..874490bc 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -103,7 +103,7 @@ def test_max_gens(): for max_gen in [0, 1, 10]: print(f"Testing with max_gen={max_gen}") - reg = BrushRegressor(max_gens=max_gen, verbosity=0).fit(X, y) + reg = BrushRegressor(max_gens=max_gen, pop_size=10, verbosity=0).fit(X, y) predictions = reg.predict(X) assert predictions is not None, "Prediction failed" diff --git a/tests/python/test_sklearn_interface.py b/tests/python/test_sklearn_interface.py index a1b00fd5..d2c4a4ce 100644 --- a/tests/python/test_sklearn_interface.py +++ b/tests/python/test_sklearn_interface.py @@ -121,7 +121,7 @@ def test_brush_classifier_checkpoint_training(tmp_path): verbosity=0, ) - step = 4 + step = 10 max_gens = est.max_gens est.max_gens = step est.save_population = str(checkpoint) From 4ec4c2b92a4615a61e0972ca63822ff96ecfd79a Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Sun, 9 Nov 2025 19:40:34 -0300 Subject: [PATCH 11/30] Make brush work with 0 generations again --- src/engine.cpp | 11 +++++------ tests/python/test_final_model_selection.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/engine.cpp b/src/engine.cpp index 093b5ca3..086aaf25 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -563,13 +563,12 @@ void Engine::run(Dataset &data) [&]() { return 0; }, // jump back to the next iteration [&](tf::Subflow& subflow) { - // TODO: make sure I do not need to re-fit here and remove this later - // set VALIDATION loss for archive, without refitting the model - // for (int island = 0; island < this->params.num_islands; ++island) { - // evaluator.update_fitness(this->pop, island, data, params, false, true); - // } + // set validation loss, refit (will work only on individuals not fitted) + for (int island = 0; island < this->params.num_islands; ++island) { + evaluator.update_fitness(this->pop, island, data, params, true, true); + } - // archive.update(pop, params); + archive.update(pop, params); // calculate_stats(); if (params.save_population != "") diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index f1645ee8..6d137572 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -95,7 +95,7 @@ def test_final_model_selection_best_validation_ci_replicated(scorer, class_weigh est = BrushClassifier( max_gens=1, - pop_size=10, + pop_size=50, final_model_selection="best_validation_ci", scorer=scorer, functions=['Add', 'Sub', 'SplitBest'], From 18f8e8107e0ad3c172ee8873701dba684ee28894 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 12 Nov 2025 12:08:13 -0300 Subject: [PATCH 12/30] Updated partial fit signature. It replicates the best individual by default now --- pybrush/BrushEstimator.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pybrush/BrushEstimator.py b/pybrush/BrushEstimator.py index 121445fc..1f0a991d 100644 --- a/pybrush/BrushEstimator.py +++ b/pybrush/BrushEstimator.py @@ -133,7 +133,8 @@ def fit(self, X, y): return self - def partial_fit(self, X, y, lock_nodes_depth=0, keep_leaves_unlocked=True): + def partial_fit(self, X, y, *, + lock_nodes_depth=0, keep_leaves_unlocked=True, keep_current_weights=False): """ Fit an estimator to X,y, without reseting the estimator. @@ -147,6 +148,9 @@ def partial_fit(self, X, y, lock_nodes_depth=0, keep_leaves_unlocked=True): The depth of the tree to lock. Default is 0. keep_leaves_unlocked : bool, optional Whether to skip leaves when locking nodes. Default is True. + keep_current_weights : bool, optional + Whether to keep current weights at the spot they appear, and preventing + them to be changed during optimization. Default is False. """ if isinstance(X, pd.DataFrame): @@ -172,9 +176,12 @@ def partial_fit(self, X, y, lock_nodes_depth=0, keep_leaves_unlocked=True): # This updates the parameters (such as class weights) self.engine_.params = new_parameters - self.engine_.lock_nodes(lock_nodes_depth, keep_leaves_unlocked) + # replicating the best individual + self.engine_.set_population([self.best_estimator_ for _ in range(self.pop_size)]) + + self.engine_.lock_nodes(lock_nodes_depth, keep_leaves_unlocked, keep_current_weights) self.engine_.fit(new_data) - self.engine_.lock_nodes(0, False) # unlocking everything + # self.engine_.lock_nodes(0, False, False) # unlocking everything self.archive_ = self.engine_.get_archive() self.population_ = self.engine_.get_population() From e5f854b18ca13c4ba801a8e12ed4eac7a6a49ab2 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 12 Nov 2025 12:09:34 -0300 Subject: [PATCH 13/30] lockking nodes or weights --- src/bandit/dummy.h | 2 - src/bindings/bind_engines.h | 1 + src/bindings/bind_programs.h | 1 + src/engine.cpp | 4 +- src/engine.h | 2 +- src/program/node.cpp | 14 ++-- src/program/node.h | 56 +++++++++++----- src/program/program.h | 120 ++++++++++++++++++++++++++++------- src/program/split.h | 32 ++++++---- 9 files changed, 172 insertions(+), 60 deletions(-) diff --git a/src/bandit/dummy.h b/src/bandit/dummy.h index 5313b175..3a233d95 100644 --- a/src/bandit/dummy.h +++ b/src/bandit/dummy.h @@ -8,8 +8,6 @@ namespace Brush { namespace MAB { -// TODO: rename dummy to static or fixed - class DummyBandit : public BanditOperator { public: diff --git a/src/bindings/bind_engines.h b/src/bindings/bind_engines.h index fe31169c..e86a4331 100644 --- a/src/bindings/bind_engines.h +++ b/src/bindings/bind_engines.h @@ -82,6 +82,7 @@ void bind_engine(py::module& m, string name) &T::lock_nodes, py::arg("end_depth") = 0, py::arg("keep_leaves_unlocked") = true, + py::arg("keep_current_weights") = false, stream_redirect() ) .def(py::pickle( diff --git a/src/bindings/bind_programs.h b/src/bindings/bind_programs.h index 9f8e5cb2..e22b8676 100644 --- a/src/bindings/bind_programs.h +++ b/src/bindings/bind_programs.h @@ -40,6 +40,7 @@ void bind_program(py::module& m, string name) &T::lock_nodes, py::arg("end_depth") = 0, py::arg("keep_leaves_unlocked") = true, + py::arg("keep_current_weights") = false, stream_redirect() ) .def("get_model", diff --git a/src/engine.cpp b/src/engine.cpp index 086aaf25..1e506c98 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -288,7 +288,7 @@ void Engine::set_population(vector> pop_vector) template -void Engine::lock_nodes(int end_depth, bool keep_leaves_unlocked) +void Engine::lock_nodes(int end_depth, bool keep_leaves_unlocked, bool keep_current_weights) { // iterate over the population, locking the program's tree nodes for (int island=0; island::lock_nodes(int end_depth, bool keep_leaves_unlocked) for (unsigned i = 0; iprogram.lock_nodes(end_depth, keep_leaves_unlocked); + ind->program.lock_nodes(end_depth, keep_leaves_unlocked, keep_current_weights); } } } diff --git a/src/engine.h b/src/engine.h index 4c65802b..ad082e9a 100644 --- a/src/engine.h +++ b/src/engine.h @@ -123,7 +123,7 @@ class Engine{ void set_population(vector> pop_vector); // locking and unlocking parts of the solutions - void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true); + void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true, bool keep_current_weights=false); // TODO: predict/predict_proba/archive with longitudinal data diff --git a/src/program/node.cpp b/src/program/node.cpp index 4e1adf0f..13c3d640 100644 --- a/src/program/node.cpp +++ b/src/program/node.cpp @@ -116,7 +116,8 @@ void to_json(json& j, const Node& p) j = json{ {"name", p.name}, {"center_op", p.center_op}, - {"fixed", p.fixed}, + {"node_is_fixed", p.node_is_fixed}, + {"weight_is_fixed", p.weight_is_fixed}, {"prob_change", p.prob_change}, {"is_weighted", p.is_weighted}, {"W", p.W}, @@ -301,10 +302,15 @@ void from_json(const json &j, Node& p) // after this point we set attributes that are modified in init p.init(); - // these 4 below needs to be set after init(), since it resets these values - if (j.contains("fixed")) + // these below needs to be set after init(), since `init` sets these values + if (j.contains("node_is_fixed")) { - j.at("fixed").get_to(p.fixed); + j.at("node_is_fixed").get_to(p.node_is_fixed); + } + + if (j.contains("weight_is_fixed")) + { + j.at("weight_is_fixed").get_to(p.weight_is_fixed); } if (j.contains("is_weighted")) diff --git a/src/program/node.h b/src/program/node.h index f9b99fe4..e3c14dfe 100644 --- a/src/program/node.h +++ b/src/program/node.h @@ -18,22 +18,27 @@ Node class design heavily inspired by Operon, (c) Heal Research // #include "nodes/split.h" // #include "nodes/terminal.h" //////////////////////////////////////////////////////////////////////////////// + /* Node overhaul: - Incorporating new design principles, learning much from operon: - make Node trivial, so that it is easily copied around. - - use Enums and maps to define node information. This kind of abandons the object oriented approach taken thus far, but it should make extensibility easier and performance better in the long run. + - use Enums and maps to define node information. This kind of abandons the + object oriented approach taken thus far, but it should make extensibility + easier and performance better in the long run. - Leverage ceres for parameter optimization. No more defining analytical - derivatives for every function. Let ceres do that. + derivatives for every function. Let ceres do that. - sidenote: not sure ceres can handle the data flow of split nodes. - need to figure out. + need to figure out. - this also suggests turning TimeSeries back into EigenSparse matrices. - forget all the runtime node generation. It saves space at the cost of - unclear code. I might as well just define all the nodes that are available, plainly. At run-time this will be faster. - - keep an eye towards extensibility by defining a custom node registration function that works. - + unclear code. I might as well just define all the nodes that are available, + plainly. At run-time this will be faster. + - keep an eye towards extensibility by defining a custom node registration + function that works. */ + using Brush::DataType; using Brush::Data::Dataset; @@ -97,12 +102,22 @@ struct Node { /// @brief a hash of the dual of the signature (for NLS) std::size_t sig_dual_hash; - /// whether the node is replaceable. Weights are still optimized. - bool fixed; + // TODO: node_is_fixed, weight_is_fixed, and is_weighted accessed via getters/setters + + // The three flags below will help determine how to handle the node during mutation, + // and can also be changed by the locking mechanism. prob_change is user-defined and + // also interact with the flags + + /// @brief whether the node is replaceable. Weights are still optimized. + bool node_is_fixed; + /// @brief whether the weight should be kept during variation. Notice that weight_is_fixed alows us to fix the weight of certain nodes. + bool weight_is_fixed; /// @brief whether this node is weighted (ignored in nodes that must have weights, such as meanLabel, constants, splits) bool is_weighted; - /// chance of node being selected for variation + /// @brief chance of node being selected for variation. This will take into account if the node is fixed, but not the weight + /// (a fixed weight just gets propagated). Can be affected by node_is_fixed. should not be accessed directly. float prob_change; + /// @brief the weights of the node. also used for splitting thresholds. float W; @@ -117,7 +132,8 @@ struct Node { size_t, // sig_hash bool, // is_weighted string, // feature - bool, // fixed + bool, // node_is_fixed + bool, // weight_is_fixed int // rounded W // float // prob_change >; @@ -155,13 +171,18 @@ struct Node { } void init(){ - // starting weights with neutral element of the operation. offsetsum - // is the only node that does not multiply the weight --- instead, it adds it + // is the only node that does not multiply the weight --- instead, it adds it, + // so we need to handle it differently W = (node_type == NodeType::OffsetSum) ? 0.0 : 1.0; // set_node_hash(); - fixed=false; + + // everything is unlocked. Special nodes (like the logistic root) + // should be fixed during its creation (check `vary` source code) + node_is_fixed=false; + weight_is_fixed=false; + set_prob_change(1.0); // TODO: confirm that this is really necessary (intializing this variable) and transform this line into a ternary if so @@ -201,7 +222,8 @@ struct Node { sig_hash, get_is_weighted(), feature, - fixed, + node_is_fixed, + weight_is_fixed, // include weights only if we want exact matches. // but we will indicate that constants are on using get is weighted, // just so we differentiate whether the weight exists or is ignored @@ -251,9 +273,9 @@ struct Node { //////////////////////////////////////////////////////////////////////////////// // getters and setters //TODO revisit - float get_prob_change() const { return fixed ? 0.0 : this->prob_change;}; + float get_prob_change() const { return node_is_fixed ? 0.0 : this->prob_change;}; void set_prob_change(float w){ this->prob_change = w;}; - float get_prob_keep() const { return fixed ? 1.0 : 1.0-this->prob_change;}; + float get_prob_keep() const { return node_is_fixed ? 1.0 : 1.0-this->prob_change;}; inline void set_feature(string f){ feature = f; }; inline string get_feature() const { return feature; }; @@ -264,7 +286,7 @@ struct Node { inline void set_keep_split_feature(bool keep){ this->keep_split_feature = keep; }; inline bool get_keep_split_feature() const { return this->keep_split_feature; }; - // Some types does not have weights, so we completely ignore the weights + // Some types does not support weights, so we completely ignore the weights // if is not weighable inline bool get_is_weighted() const { if (IsWeighable(this->ret_type)) diff --git a/src/program/program.h b/src/program/program.h index 1167e846..c3335ce9 100644 --- a/src/program/program.h +++ b/src/program/program.h @@ -272,12 +272,20 @@ template struct Program // check tree nodes for weights for (PostIter i = Tree.begin_post(); i != Tree.end_post(); ++i) { - const auto& node = i.node->data; + const auto& node = i.node->data; + + // we do not want to include fixed weights, because they should not be changed. + // get_n_weights, get_weights, and set_weights, are functions used in + // parameter optimization --- we can simply make weights invisible to parameter + // optimization, and they will not be changed (so they remain fixed). + if (node.weight_is_fixed) + continue; + // some nodes cannot have their weights optimized, others must have. // It is important that this condition also matches the condition in - // the methods get_weights, set_weights, . - if ( Is(node.node_type) - || (node.get_is_weighted() && IsWeighable(node.ret_type)) ) + // the methods get_weights and set_weights. + if (Is(node.node_type) + || (node.get_is_weighted() && IsWeighable(node.ret_type)) ) ++count; } return count; @@ -295,6 +303,11 @@ template struct Program for (PostIter t = Tree.begin_post(); t != Tree.end_post(); ++t) { const auto& node = t.node->data; + + // skip fixed weights + if (node.weight_is_fixed) + continue; + if ( Is(node.node_type) || (node.get_is_weighted() && IsWeighable(node.ret_type)) ) { @@ -317,10 +330,16 @@ template struct Program // return the weights of the tree as an array if (weights.size() != get_n_weights()) HANDLE_ERROR_THROW("Tried to set_weights of incorrect size"); + int j = 0; for (PostIter i = Tree.begin_post(); i != Tree.end_post(); ++i) { auto& node = i.node->data; + + // skip fixed weights + if (node.weight_is_fixed) + continue; + if ( Is(node.node_type) || (node.get_is_weighted() && IsWeighable(node.node_type)) ) { @@ -337,13 +356,22 @@ template struct Program * * @param end_depth the depth to stop locking nodes. Default 0. * @param keep_leaves_unlocked whether to skip leaves and leave them unlocked. + * @param keep_current_weights whether to keep current weights at the spot they appear. * Default true. */ - void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true) + void lock_nodes(int end_depth=0, bool keep_leaves_unlocked=true, bool keep_current_weights=false) { + // This also support unlocking the ndoes by setting 0, true, false. + // OBS for unlocking the node: Do not change prob_change here, because some + // nodes are meant to never be replaced (e.g. logistic root). + // unlocking and changing probabilities are different things. + // every `if` performing a lock should have its counterpart `else` performing + // unlocking. + // iterate over the nodes, locking them if their depth does not exceed end_depth. - if (end_depth<=0) + if (end_depth<0) { return; + } // we need the iterator to calculate the depth, but // the lambda below iterate using nodes. So we are creating an iterator @@ -355,9 +383,33 @@ template struct Program auto d = Tree.depth(tree_iter); std::advance(tree_iter, 1); - if (keep_leaves_unlocked && IsLeaf(n.node_type)) - return; + // weights (this will work for all nodes) + if (n.get_is_weighted() && d struct Program else // leaves can be locked { // check if we should lock based on depth - n.set_keep_split_feature(d+1<=end_depth); + n.set_keep_split_feature(d+1 struct Program // } // // parent_id = parent_id.substr(2); + // This is for the root -------------------------------------------- // if the first node is weighted, make a dummy output node so that the // first node's weight can be shown if (i==0 && parent->data.get_is_weighted()) { + // making the weight red if fixed + string font_color = ""; + if (parent->data.weight_is_fixed) { + font_color = ", fontcolor=lightcoral"; + } + out += "y [shape=box];\n"; - out += fmt::format("y -> \"{}\" [label=\"{:.2f}\"];\n", + out += fmt::format("y -> \"{}\" [label=\"{:.2f}\"{}];\n", // parent_data.get_name(false), parent_id, - parent->data.W + parent->data.W, + font_color ); } @@ -454,8 +510,17 @@ template struct Program bool is_constant = Is(parent->data.node_type); string node_label = parent->data.get_name(is_constant); - if (Is(parent->data.node_type)){ - node_label = fmt::format("{}>={:.2f}?", parent->data.get_feature(), parent->data.W); + if (Is(parent->data.node_type)) { + std::string feature = parent->data.get_feature(); + std::string threshold = fmt::format("{:.2f}", parent->data.W); + + // Append markers for fixed flags + if (parent->data.keep_split_feature) + feature += "^"; // split feature fixed + if (parent->data.weight_is_fixed) + threshold += "*"; // split weight fixed + + node_label = fmt::format("{} >= {}?", feature, threshold); } if (Is(parent->data.node_type)){ node_label = fmt::format("Add"); @@ -487,7 +552,7 @@ template struct Program if (Is(parent->data.node_type)){ use_head_tail_labels=true; if (j == 0) - tail_label = fmt::format(">={:.2f}",parent->data.W); + tail_label = fmt::format(">= {:.2f}",parent->data.W); else if (j==1) tail_label = "Y"; else @@ -506,19 +571,27 @@ template struct Program head_label = edge_label; } + // drawing the edges + string font_color = ""; + if (kid->data.weight_is_fixed) { + font_color = ", fontcolor=lightcoral"; + } + if (use_head_tail_labels){ - out += fmt::format("\"{}\" -> \"{}\" [headlabel=\"{}\",taillabel=\"{}\"];\n", + out += fmt::format("\"{}\" -> \"{}\" [headlabel=\"{}\",taillabel=\"{}\"{}];\n", parent_id, kid_id, head_label, - tail_label + tail_label, + font_color ); } else{ - out += fmt::format("\"{}\" -> \"{}\" [label=\"{}\"];\n", + out += fmt::format("\"{}\" -> \"{}\" [label=\"{}\"{}];\n", parent_id, kid_id, - edge_label + edge_label, + font_color ); } kid = kid->next_sibling; @@ -542,7 +615,10 @@ template struct Program ++i; } + + out += "label=\"^ split feature fixed, * split threshold fixed\";\nlabelloc=bottom;\nfontsize=10;"; out += "}\n"; + return out; } diff --git a/src/program/split.h b/src/program/split.h index f2e0474c..2bd6fc73 100644 --- a/src/program/split.h +++ b/src/program/split.h @@ -233,8 +233,10 @@ struct Operatorfit(d); + // get the best splitting threshold - tie(threshold, ignore) = Split::best_threshold(split_feature, d.y, d.classification); + if (!tn.data.weight_is_fixed) // avoid changing fixed weights + tie(threshold, ignore) = Split::best_threshold(split_feature, d.y, d.classification); } else // splitbest { @@ -245,20 +247,26 @@ struct Operator(values)) - tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); - else if (std::holds_alternative(values)) - tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); - else if (std::holds_alternative(values)) - tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); + if (!tn.data.weight_is_fixed){ + // Threshold will be optimized regardless. + if (std::holds_alternative(values)) + tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); + else if (std::holds_alternative(values)) + tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); + else if (std::holds_alternative(values)) + tie(threshold, ignore) = Split::best_threshold(std::get(values), d.y, d.classification); + } + } else // keep_split_feature == false { - string feature = ""; - tie(feature, threshold) = Split::get_best_variable_and_threshold(d, tn); - tn.data.set_feature(feature); - tn.data.set_feature_type(d.get_feature_type(feature)); + if (!tn.data.weight_is_fixed){ + string feature = ""; + tie(feature, threshold) = Split::get_best_variable_and_threshold(d, tn); + + tn.data.set_feature(feature); + tn.data.set_feature_type(d.get_feature_type(feature)); + } } } From cacf8fce613ba4efd429fe44e65e221ec321436c Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 12 Nov 2025 12:09:50 -0300 Subject: [PATCH 14/30] Variation is aware of locked weights. changed how I handle failed variations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I will not create something fully random if variation fails, because the population can lose the locked nodes or weights. Instead, I try subtree mutations and finally clone the parent if it fails. This is how mutations are handling locked weights Delete – should not work on nodes with locked weight Toggle – will not work if there is a fixed weight (cannot turn on or off) Subtree – will keep the weight Point – will keep the weight Insert – should “steal” the weight from the fixed node Crossover – will keep the weight of the receiving parent --- src/vary/search_space.h | 13 ++-- src/vary/variation.cpp | 153 +++++++++++++++++++++++----------------- src/vary/variation.h | 37 ++++++++-- 3 files changed, 128 insertions(+), 75 deletions(-) diff --git a/src/vary/search_space.h b/src/vary/search_space.h index a9971393..75322dcb 100644 --- a/src/vary/search_space.h +++ b/src/vary/search_space.h @@ -737,9 +737,10 @@ P SearchSpace::make_program(const Parameters& params, int max_d, int max_size) // sample_op should never return the empty value of optional Node node_logit = sample_op(NodeType::Logistic, DataType::ArrayF, true).value(); - node_logit.set_is_weighted(false); + node_logit.set_is_weighted(false); // doesn't matter for logistic (when at the root level) node_logit.set_prob_change(0.0); - node_logit.fixed=true; + node_logit.node_is_fixed=true; + node_logit.weight_is_fixed=true; auto spot_logit = Tree.insert(Tree.begin(), node_logit); @@ -747,7 +748,8 @@ P SearchSpace::make_program(const Parameters& params, int max_d, int max_size) Node node_offset = sample_op(NodeType::OffsetSum, DataType::ArrayF, true).value(); node_offset.set_prob_change(0.0); - node_offset.fixed=true; + node_offset.node_is_fixed=true; + node_offset.weight_is_fixed=false; auto spot_offset = Tree.append_child(spot_logit); @@ -762,8 +764,9 @@ P SearchSpace::make_program(const Parameters& params, int max_d, int max_size) Node node_softmax = sample_op(NodeType::Softmax, DataType::MatrixF, true).value(); node_softmax.set_prob_change(0.0); - node_softmax.set_is_weighted(false); - node_softmax.fixed=true; + node_softmax.set_is_weighted(false); // same as logistic roots + node_softmax.node_is_fixed=true; + node_softmax.weight_is_fixed=false; spot = Tree.insert(Tree.begin(), node_softmax); } diff --git a/src/vary/variation.cpp b/src/vary/variation.cpp index a63112c4..5cd2a8d5 100644 --- a/src/vary/variation.cpp +++ b/src/vary/variation.cpp @@ -29,7 +29,17 @@ class PointMutation : public MutationBase if (!newNode) // overload to check if newNode == nullopt return false; - // if optional contains a Node, we access its contained value + // keeping the weight if it is fixed + if (IsWeighable(spot.node->data.node_type) && spot.node->data.weight_is_fixed){ + // we do not need to check if the new node is also weightable, because + // the return type will be equivalent. + + (*newNode).W = spot.node->data.W; + (*newNode).set_is_weighted(true); + (*newNode).weight_is_fixed=true; + } + + // if optional contains a Node, we access its contained value as an address program.Tree.replace(spot, *newNode); return true; @@ -97,9 +107,26 @@ class InsertMutation : public MutationBase if (!n) // there is no operator with compatible arguments return false; - // make node n wrap the subtree at the chosen spot - auto parent_node = program.Tree.wrap(spot, *n); + // moving the fixed weight to the new inserted node + // (this should be done before Tree.wrap, because that function affects the spot reference) + if (IsWeighable(spot.node->data.node_type) && spot.node->data.weight_is_fixed){ + Node& prev_n = spot.node->data; + + // moving the fixed weight to the inserted node (n is the new node). + // because n is optional, we need to solve the reference to access the node itself + // (it is wrapped in the optional<>) + (*n).W = spot.node->data.W; + (*n).set_is_weighted(true); + (*n).weight_is_fixed=true; + + // toggling off the weight of the previous node + prev_n.set_is_weighted(false); + prev_n.weight_is_fixed=false; + } + // make node `n` wrap the subtree at the chosen spot + auto parent_node = program.Tree.wrap(spot, *n); + // now fill the arguments of n appropriately bool spot_filled = false; for (auto a: (*n).arg_types) @@ -141,6 +168,29 @@ class InsertMutation : public MutationBase class DeleteMutation : public MutationBase { public: + template + static auto find_spots(Program& program, Variation& variator, + const Parameters& params) + { + vector weights(program.Tree.size()); + + // by default, mutation can happen anywhere, based on node weights + std::transform(program.Tree.begin(), program.Tree.end(), weights.begin(), + [&](const auto& n){ + // keeping the node if the weight is fixed + if (n.weight_is_fixed){ + // we cant delete a node if its weight is fixed. + // we will let other mutations do their job and avoid deletion. + return 0.0f; + } + + return n.get_prob_change(); // this already checks for node_is_fixed + }); + + // Must have same size as tree, even if all weights <= 0.0 + return weights; + } + template static auto mutate(Program& program, Iter spot, Variation& variator, const Parameters& params) @@ -184,7 +234,8 @@ class ToggleWeightOnMutation : public MutationBase return 0.0f; // only weighted nodes can be toggled off - if (!n.get_is_weighted() + if ((!n.get_is_weighted()) + && (!n.weight_is_fixed) && IsWeighable(n.node_type)) { return n.get_prob_change(); @@ -236,6 +287,7 @@ class ToggleWeightOffMutation : public MutationBase return 0.0f; if (n.get_is_weighted() + && (!n.weight_is_fixed) && IsWeighable(n.node_type)) return n.get_prob_change(); else @@ -329,6 +381,17 @@ class SubtreeMutation : public MutationBase if (!subtree) // there is no terminal with compatible arguments return false; + // keeping the weight if it is fixed. + // I need to manipulate spot before Tree.erase_children! + if (IsWeighable(spot.node->data.node_type) && spot.node->data.weight_is_fixed){ + Node& n = subtree.value().begin().node->data; + + // moving the weight and fixing it + n.W = spot.node->data.W; + n.set_is_weighted(true); + n.weight_is_fixed=true; + } + // if optional contains a Node, we access its contained value program.Tree.erase_children(spot); @@ -338,57 +401,6 @@ class SubtreeMutation : public MutationBase } }; -/// @brief Inserts an split node in the `spot` -/// @param prog the program -/// @param Tree the program tree -/// @param spot an iterator to the node that is being mutated -/// @param SS the search space to generate a compatible subtree -/// @return boolean indicating the success (true) or fail (false) of the operation -class SplitMutation : public MutationBase -{ -public: - template - static auto find_spots(Program& program, Variation& variator, - const Parameters& params) - { - vector weights; - - if (program.Tree.size() < params.get_max_size()) { - Iter iter = program.Tree.begin(); - std::transform(program.Tree.begin(), program.Tree.end(), std::back_inserter(weights), - [&](const auto& n){ - size_t d = 1+program.Tree.depth(iter); - std::advance(iter, 1); - - // check if SS holds an operator to avoid failing `check` in sample_op_with_arg - if (d >= params.get_max_depth() - || variator.search_space.node_map.find(n.ret_type) == variator.search_space.node_map.end() - // || check if n.ret_type can be splitted (e.g. DataType::ArrayF) - ) { - return 0.0f; - } - else { - return n.get_prob_change(); - } - }); - } - else { - // fill the vector with zeros, since we're already at max_size - weights.resize(program.Tree.size()); - std::fill(weights.begin(), weights.end(), 0.0f); - } - - return weights; - } - - template - static auto mutate(Program& program, Iter spot, Variation& variator, - const Parameters& params) - { - return false; - } -}; - /** * @brief Stochastically swaps subtrees between root and other, returning a new program. * @@ -432,9 +444,9 @@ std::optional> Variation::cross( std::advance(child_iter, 1); + // We don't have to check size here, because it will be replaced + // by something with a valid new size. if ( - // We don't have to check size here, because it will be replaced - // by something with a valid new size. // s_at> Variation::cross( std::advance(other_iter, 1); // Check feasibility and matching return type - if (s <= allowed_size && d <= allowed_depth && n.ret_type == child_ret_type) { + if ( (s <= allowed_size) + && (d <= allowed_depth) + && (n.ret_type == child_ret_type) // this condition helps making sure the crossover will succeed, and also that we can keep fixed weights + ) { return n.get_prob_change(); } @@ -494,14 +509,21 @@ std::optional> Variation::cross( [](float w) { return w > 0.0f; }); if (matching_spots_found) { - auto other_spot = r.select_randomly( - other.Tree.begin(), - other.Tree.end(), - other_weights.begin(), - other_weights.end() + other.Tree.begin(), other.Tree.end(), + other_weights.begin(), other_weights.end() ); - + + // manipulate before move_ontop (it will mess references) + if (IsWeighable(child_spot.node->data.node_type) && child_spot.node->data.weight_is_fixed){ + Node& n = other_spot.node->data; + + // moving the weight and fixing it + n.W = child_spot.node->data.W; + n.set_is_weighted(true); + n.weight_is_fixed=true; + } + // fmt::print("other_spot : {}\n",other_spot.node->data); // swap subtrees at child_spot and other_spot child.Tree.move_ontop(child_spot, other_spot); @@ -639,8 +661,9 @@ std::optional> Variation::mutate( && (child.size() <= parameters.max_size) && (child.depth() <= parameters.max_depth) ) + // TODO: delete 2 commented lines below // loose mutation --- it will try its best, but may return something slightly larger. - || attempts==3 // this is the final attempt, return whatever we got. + // || attempts==3 // this is the final attempt, return whatever we got. ){ Individual ind(child); diff --git a/src/vary/variation.h b/src/vary/variation.h index 3acd5f2f..330574bd 100644 --- a/src/vary/variation.h +++ b/src/vary/variation.h @@ -229,7 +229,6 @@ class Variation { { if (pop.individuals.at(indices.at(i)) != nullptr) { - continue; // skipping if it is an individual --- we just want to fill invalid positions } @@ -258,16 +257,15 @@ class Variation { { // Notice that if everything is locked then the entire population // may be replaced (if the new random individuals dominates the old - // fixed ones) + // fixed ones). below we force to repeat individuals ind = Individual(); - ind.variation = "born"; - ind.init(search_space, parameters); // Alternative: keep it as it is, and just re-fit the constants // (comment out just the line below to disable, but keep ind.init) Program copy(mom.program); ind.program = copy; + ind.variation = "clone"; } else { @@ -295,10 +293,39 @@ class Variation { else { // no optional value was returned. creating a new random individual ind = Individual(); ind.init(search_space, parameters); - ind.variation = "born"; + + // creates a new random individual + // ind.init(search_space, parameters); + // ind.variation = "born"; + // --------------------------------------------------------------- + + // instead of creating something new (code above), I will apply + // subtree mutation, so we can still have the fixed part of programs + int tries = 0; + while (tries++<=3 && !opt) { // try subtree mutation a few times before giving up + opt = this->mutate(mom, "subtree"); + } + + // it is very unlikely that subtree will fail, but in case it does, then + // we set variation to be a clone so we know it happened + if (opt) { + ind = opt.value(); + ind.set_parents(ind_parents); + ind.variation = "subtree"; + } else { + // fallback: mark as a subtree attempt that failed to produce a new individual + Program copy(mom.program); + ind.program = copy; + ind.variation = "clone"; + } } } + // for debugging + // cout << "tried " << choice << ", got " << ind.variation << endl; + // cout << "mom : " << mom.program.get_model() << endl; + // cout << "child: " << ind.program.get_model() << endl; + // ind.set_objectives(mom.get_objectives()); // it will have an invalid fitness ind.set_id(id); From e0bd9bba5481fbc7e31c9ff8d2b8595686edb29b Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 12 Nov 2025 12:12:31 -0300 Subject: [PATCH 15/30] Sample notebook to test fixed nodes and weights --- docs/guide/fixing_nodes.ipynb | 433 ++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 docs/guide/fixing_nodes.ipynb diff --git a/docs/guide/fixing_nodes.ipynb b/docs/guide/fixing_nodes.ipynb new file mode 100644 index 00000000..1dea75cd --- /dev/null +++ b/docs/guide/fixing_nodes.ipynb @@ -0,0 +1,433 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9658a243", + "metadata": {}, + "source": [ + "# Fixing parts of a tree\n", + "\n", + "In this notebook I create a simple dataset with known ground-truth and train a brush model, then I fix parts of the model and move it to another site where the data has some distribution shifts that can be learned.\n", + "\n", + "By fixing parts of the structure we enable transfer learning and we can keep original weights to understand the new relationships between previous models and new ones." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7320ab2c", + "metadata": {}, + "outputs": [], + "source": [ + "from pybrush import BrushRegressor\n", + "import numpy as np\n", + "import graphviz\n", + "import matplotlib.pyplot as plt\n", + "from sklearn.datasets import make_regression\n", + "from sklearn.tree import DecisionTreeRegressor\n", + "from sklearn.metrics import r2_score, mean_squared_error" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "18f7e42e", + "metadata": {}, + "outputs": [], + "source": [ + "# --- 1. Generate base dataset (site A) ---\n", + "n_samples = 500\n", + "\n", + "# Three features: X1, X2, X3\n", + "X_train = np.random.randn(n_samples, 3)\n", + "\n", + "# Conditional relationship:\n", + "y_train = np.array([\n", + " 5 * x[0] if x[2] > 0 else -3 * x[1]\n", + " for x in X_train\n", + "])\n", + "\n", + "# --- 2. Generate shifted dataset (site B) ---\n", + "np.random.seed(42)\n", + "X_test = np.random.randn(n_samples, 3)\n", + "\n", + "# Introduce a covariate shift:\n", + "# scale and translate the first two features\n", + "X_test[:, 0] = X_test[:, 0] * 1.5 + 2\n", + "\n", + "# Conditional relationship with same logic but added shift\n", + "y_test = np.array([\n", + " 5 * x[0] if x[2] > 0 else -6 * x[1]\n", + " for x in X_test\n", + "])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "082ddb97", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAGGCAYAAABmGOKbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQV4U2fbx/+RNnWnlNJCcSvu7r4BY2zMx7Z3vrF9c33n7s5e5sIY7u7uUChQg5a6uyVNcr7rfiJNGmmBlha4f7sy2pOTnOecpOd+/s9tMkmSJDAMwzAMwzAMwzAMU+/I6/8tGYZhGIZhGIZhGIYhWHQzDMMwDMMwDMMwTAPBopthGIZhGIZhGIZhGggW3QzDMAzDMAzDMAzTQLDoZhiGYRiGYRiGYZgGgkU3wzAMwzAMwzAMwzQQLLoZhmEYhmEYhmEYpoFg0c0wDMMwDMMwDMMwDQSLboZhGIZhGIZhGIZpIFh0M8xFEhERgTlz5uBqYsqUKXjwwQev2PFGjRolHpcCXVu6xk2JxvzMX3rpJQwcOLBRjs0wDFPfXC82lM7Ry8urTvvKZDK8+eabVtsOHz6MIUOGwNPTUzx/4sQJNBRJSUniGL/99huuFRr7nAYNGoQXXnihUY7NNE1YdDOMkVOnTmHWrFlo3bo13Nzc0LJlS4wfPx7ffPON09edOXNGGEu6wTcU69atE8YjNDQUer3+ol67d+9ebNq0CS+++OIVHTMDpKeni+t8OZOlp59+GlFRUVi1alW9jo1hGOZatqE7duwQdtPyERAQIMTQ33//fVk2tKGpqqrCLbfcgvz8fHzxxRf4888/xXX9/vvvG1UYX6m5Q2Of5759+8R5FhYWXvJ70Pflu+++Q2ZmZr2Ojbl6YdHNMMYbbL9+/YS4odXsb7/9Fv/5z38gl8vx1VdfWe0bGxuL+fPnWxmht956q0GNEE0QyDuQkZGBbdu2XdRrP/nkE4wdOxbt27e/YmOmCQo9LgW6tnSNrxXRTdf5ckR3SEgIpk+fjk8//bRex8YwDHM92NC5c+cK0UqPN954Q4zprrvuEoLoUm1ofVNRUYHXXnvN/Pu5c+dw4cIFPPfcc3jooYfEeP39/RtdjF6J+Q7R2OdJ32c6z8sR3WS3fXx8xLkwDKHky8AwwHvvvQdfX18RzuXn52f1XHZ2ttXvKpXqio6trKwMK1euxAcffIBff/1VCPBx48bV6bU09rVr12LevHmXfHxJklBZWQl3d/c6v8bV1fWSj+fi4nLJr71WufXWW4XX4/z582jbtm1jD4dhGOaqsaHDhw8XHngTjz76qLiPLliwAI8//niD29C6QJEBNY9L1LyWzNUDLe7Q9+6PP/4QAp4iLZjrG/Z0M4xxVblbt252DVxwcLDDfDRaiSUxRIwePdocwkZhbSbWr18vjD7lZXl7e2Pq1Kk4ffp0nce2fPlysQpOx7ntttuwbNkyIYLrAk0WtFqtlUivbcx0fjfccAM2btwoPBcktn/88UfxHIn+MWPGiGtCE6euXbvihx9+qDWn2xTmt2jRIjE5CwsLE5MM8h4kJCQ4zek25WWRp/d///sf2rVrJ47dv39/McGryeLFi8W46P0jIyPF9atrnjgtMLz77rtifB4eHuL62PusKOSPPBDdu3cXOXu0mj158mTh5bE8Zxojcd9995mvs2n1fvfu3eJzaNWqlTif8PBw/N///Z/4rGti+vxo8YVhGKap0ZRtqL1FYfIaK5XKS7KhpvBvElIdOnQQtiYwMBDDhg3D5s2bbd4jLS0NM2bMELaiWbNmwnbodDqHOd10bUaOHCl+pmtDz5E9petG571z507zdbK0s+SVpXQksiVkU8gz/9FHH9mkpNF+dAxaJKHP6957762TR7e+PisKtyabSHaWxtmiRQvhFTZ5z2s7T3vU9ZxOnjwp9qNFF/rcKJLs/vvvR15ennkf+hyef/558XObNm3MYzCNr67zIILSKyhioSHz8ZmrB/Z0MwwgcqX279+P6OhoIdTqyogRI0To2tdff41XXnkFXbp0EdtN/1I4G938J06cKIxfeXm5uDmTcT5+/HidhCB5tsnAkXEg0U2FtVavXm02frWFSNFkgM6vrmM2hf/dfvvtePjhh0WoYKdOncR2GjtNrKZNmyYmLDSOxx57TBj12jwGxIcffihWf2nSUVRUhI8//hh33nknDh48WOtryStRUlIixkQGkF47c+ZM4f01ecdpgjR79mwhhikyoKCgAA888IDILawL//3vf4XopqI59Dh27BgmTJgAjUZjtR8dc8WKFeIzIKOclZUlFiZookThd5R7T9fz7bffFu9J4YE0ESGoMI5pcYC+D+R1oc/o0KFDIvcxNTVVPGcJTSRosYFyC0mYMwzDNCWasg0l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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 2, figsize=(10, 4), sharex=True, sharey=True)\n", + "\n", + "axs[0].scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap=\"viridis\")\n", + "axs[0].set_title(\"Site A (training data)\")\n", + "axs[0].set_xlabel(\"Feature 1\")\n", + "axs[0].set_ylabel(\"Feature 2\")\n", + "\n", + "axs[1].scatter(X_test[:, 0], X_test[:, 1], c=y_test, cmap=\"viridis\")\n", + "axs[1].set_title(\"Site B (shifted test data)\")\n", + "axs[1].set_xlabel(\"Feature 1\")\n", + "axs[1].set_ylabel(\"Feature 2\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "45fcab44", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed 100% [====================]\n", + "score train: 0.45095834153429726\n", + "score test: 0.12659082293924162\n" + ] + } + ], + "source": [ + "# import and make a regressor\n", + "est = BrushRegressor(\n", + " functions=['SplitBest','Mul','Add','Sub'],\n", + " max_depth=5,\n", + " max_size=20,\n", + " max_gens=10,\n", + " start_from_decision_trees=True,\n", + " constants_simplification=False,\n", + " inexact_simplification=False,\n", + " verbosity=1\n", + ")\n", + "\n", + "est.fit(X_train,y_train)\n", + "\n", + "print('score train:', est.score(X_train,y_train))\n", + "print('score test: ', est.score(X_test,y_test))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9bd67c88", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "G\n", + "\n", + "^ split feature fixed, * split threshold fixed\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "1593a8de0\n", + "\n", + "x_0 >= 3.15*?\n", + "\n", + "\n", + "\n", + "y->1593a8de0\n", + "\n", + "\n", + "3.15\n", + "\n", + "\n", + "\n", + "15bb0f930\n", + "\n", + "15.75\n", + "\n", + "\n", + "\n", + "1593a8de0->15bb0f930\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "15bb0c9e0\n", + "\n", + "Sub\n", + "\n", + "\n", + "\n", + "1593a8de0->15bb0c9e0\n", + "\n", + "\n", + "1.03\n", + "N\n", + "\n", + "\n", + "\n", + "x_0\n", + "\n", + "x_0\n", + "\n", + "\n", + "\n", + "15bb0c9e0->x_0\n", + "\n", + "\n", + "2.20\n", + "\n", + "\n", + "\n", + "x_1\n", + "\n", + "x_1\n", + "\n", + "\n", + "\n", + "15bb0c9e0->x_1\n", + "\n", + "\n", + "1.31\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# locking the best estimator (visualizing what we have locked) \n", + "est.best_estimator_.program.lock_nodes(2, keep_leaves_unlocked=True, keep_current_weights=True)\n", + "\n", + "model = est.best_estimator_.get_model(\"dot\")\n", + "graphviz.Source(model)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1f4208f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed 100% [====================]\n", + "score train: 0.3294429188771656\n", + "score test: 0.396896318305067\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "G\n", + "\n", + "^ split feature fixed, * split threshold fixed\n", + "\n", + "\n", + "y\n", + "\n", + "y\n", + "\n", + "\n", + "\n", + "11cf5c330\n", + "\n", + "x_0 >= 3.15*?\n", + "\n", + "\n", + "\n", + "y->11cf5c330\n", + "\n", + "\n", + "3.15\n", + "\n", + "\n", + "\n", + "159e9f020\n", + "\n", + "x_2 >= 0.01?\n", + "\n", + "\n", + "\n", + "11cf5c330->159e9f020\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "159e9ce30\n", + "\n", + "Sub\n", + "\n", + "\n", + "\n", + "11cf5c330->159e9ce30\n", + "\n", + "\n", + "1.03\n", + "N\n", + "\n", + "\n", + "\n", + "159e43280\n", + "\n", + "19.90\n", + "\n", + "\n", + "\n", + "159e9f020->159e43280\n", + "\n", + "\n", + "Y\n", + "\n", + "\n", + "\n", + "x_2\n", + "\n", + "x_2\n", + "\n", + "\n", + "\n", + "159e9f020->x_2\n", + "\n", + "\n", + "-0.14\n", + "N\n", + "\n", + "\n", + "\n", + "x_0\n", + "\n", + "x_0\n", + "\n", + "\n", + "\n", + "159e9ce30->x_0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "x_1\n", + "\n", + "x_1\n", + "\n", + "\n", + "\n", + "159e9ce30->x_1\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# locking the structure and using partial_fit\n", + "\n", + "# increasing it so we have more flexibility\n", + "est.max_depth = 8\n", + "est.max_size = 30 \n", + "\n", + "est.partial_fit(X_test, y_test, \n", + " lock_nodes_depth=2, keep_leaves_unlocked=True, keep_current_weights=True)\n", + "\n", + "print('score train:', est.score(X_train,y_train))\n", + "print('score test: ', est.score(X_test,y_test))\n", + "\n", + "model = est.best_estimator_.get_model(\"dot\")\n", + "graphviz.Source(model)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb28d3bd", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "brush", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From d54e0014a0b7d409044354b8440803d4e5f39f08 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 26 Nov 2025 09:08:18 -0300 Subject: [PATCH 16/30] Simplifying at last gen --- src/engine.cpp | 5 ++--- src/program/program.h | 4 ++-- src/simplification/inexact.h | 13 +++++++------ tests/cpp/test_brush.cpp | 4 ++-- tests/cpp/test_variation.cpp | 8 ++++---- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/engine.cpp b/src/engine.cpp index 1e506c98..0f683927 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -496,11 +496,10 @@ void Engine::run(Dataset &data) data, evaluator, // conditions to apply simplification. - // It starts only on the second half of generations, - // and it is not applied every generation. + // It starts only on the second half of generations. // Also, we garantee that the final generation // will be simplified. - (generation>=params.max_gens/2) || (stall_count == params.max_stall-1) + (generation>=params.max_gens/2) || (stall_count == params.max_stall) || (generation==params.max_gens-1) ); } diff --git a/src/program/program.h b/src/program/program.h index c3335ce9..e7e0bfa2 100644 --- a/src/program/program.h +++ b/src/program/program.h @@ -304,7 +304,7 @@ template struct Program { const auto& node = t.node->data; - // skip fixed weights + // skip fixed weights (this also avoid changing offsetSum weight if is locked) if (node.weight_is_fixed) continue; @@ -336,7 +336,7 @@ template struct Program { auto& node = i.node->data; - // skip fixed weights + // skip fixed weights (this also avoid changing offsetSum weight if is locked) if (node.weight_is_fixed) continue; diff --git a/src/simplification/inexact.h b/src/simplification/inexact.h index 2eae39ee..3eb6e32c 100644 --- a/src/simplification/inexact.h +++ b/src/simplification/inexact.h @@ -150,7 +150,7 @@ class Inexact_simplifier && Isnt(spot.node->data.node_type)) { index

(spot, d); - // terminals are indexed on initialization + // terminals are already indexed on initialization } } ++spot; @@ -171,7 +171,7 @@ class Inexact_simplifier Program

simplified_program(program); // prediction at the root already performs template cast and always returns a float - auto original_predictions = simplified_program.predict(d); + auto original_predictions = simplified_program.predict(d).template cast(); // iterate over the tree, trying to replace each node with a constant, and keeping the change if the pred does not change. // notice it is a post order iterator. @@ -181,7 +181,8 @@ class Inexact_simplifier // we dont index or simplify fixed stuff. // non-wheightable nodes are not simplified. TODO: revisit this and see if they should (then implement it) // This is avoiding using booleans. - if (spot.node->data.get_prob_change() > 0 + // we do not simplify branches with fixed weights, because the simplification ignores the weight (it uses normalized predictions) + if (spot.node->data.get_prob_change() > 0 && !spot.node->data.weight_is_fixed // && IsWeighable(spot.node->data.ret_type) && IsWeighable(spot.node->data.node_type) ) { // TODO: use IsLeaf here instead of checking for each possible nodetype. also search throughout the code and replace it @@ -213,9 +214,9 @@ class Inexact_simplifier spot = simplified_program.Tree.move_ontop(spot, simplified_branch.begin()); - auto new_predictions = simplified_program.predict(d); - - float diff = (original_predictions.template cast() - new_predictions.template cast()).square().mean(); + auto new_predictions = simplified_program.predict(d).template cast(); + + float diff = (original_predictions - new_predictions).square().mean(); if (diff < best_distance) { best_distance = diff; diff --git a/tests/cpp/test_brush.cpp b/tests/cpp/test_brush.cpp index 93fa6378..e100f84a 100644 --- a/tests/cpp/test_brush.cpp +++ b/tests/cpp/test_brush.cpp @@ -250,7 +250,7 @@ TEST(Engine, SavingLoadingFixedNodes) ASSERT_TRUE(cx_child_root.node_type == NodeType::Logistic); ASSERT_TRUE(cx_child_root.get_prob_change()==0.0); - ASSERT_TRUE(cx_child_root.fixed==true); + ASSERT_TRUE(cx_child_root.node_is_fixed==true); } // TODO: why if I set cx_prob to 0.0 it does not work? (maybe because Im using the same params object for the two engines? do i need to remove save_pop file first?) @@ -276,7 +276,7 @@ TEST(Engine, SavingLoadingFixedNodes) ASSERT_TRUE(cx_child_root.node_type == NodeType::Logistic); ASSERT_TRUE(cx_child_root.get_prob_change()==0.0); - ASSERT_TRUE(cx_child_root.fixed==true); + ASSERT_TRUE(cx_child_root.node_is_fixed==true); } } diff --git a/tests/cpp/test_variation.cpp b/tests/cpp/test_variation.cpp index 9d113a5d..6122e2cd 100644 --- a/tests/cpp/test_variation.cpp +++ b/tests/cpp/test_variation.cpp @@ -51,7 +51,7 @@ TEST(Variation, FixedRootDoesntChange) ASSERT_TRUE(root.ret_type == DataType::ArrayF); ASSERT_TRUE(root.sig_hash == logistic_hash); ASSERT_TRUE(root.get_prob_change()==0.0); - ASSERT_TRUE(root.fixed==true); + ASSERT_TRUE(root.node_is_fixed==true); Individual IND(PRG); auto opt_mutation = variator.mutate(IND); @@ -69,7 +69,7 @@ TEST(Variation, FixedRootDoesntChange) ASSERT_TRUE(mut_child_root.ret_type == DataType::ArrayF); ASSERT_TRUE(mut_child_root.sig_hash == logistic_hash); ASSERT_TRUE(mut_child_root.get_prob_change()==0.0); - ASSERT_TRUE(mut_child_root.fixed==true); + ASSERT_TRUE(mut_child_root.node_is_fixed==true); } ClassifierProgram PRG2 = SS.make_classifier(0, 0, params); @@ -90,7 +90,7 @@ TEST(Variation, FixedRootDoesntChange) ASSERT_TRUE(cx_child_root.ret_type == DataType::ArrayF); ASSERT_TRUE(cx_child_root.sig_hash == logistic_hash); ASSERT_TRUE(cx_child_root.get_prob_change()==0.0); - ASSERT_TRUE(cx_child_root.fixed==true); + ASSERT_TRUE(cx_child_root.node_is_fixed==true); } // root remained unchanged @@ -98,7 +98,7 @@ TEST(Variation, FixedRootDoesntChange) ASSERT_TRUE(root.ret_type == DataType::ArrayF); ASSERT_TRUE(root.sig_hash == logistic_hash); ASSERT_TRUE(root.get_prob_change()==0.0); - ASSERT_TRUE(root.fixed==true); + ASSERT_TRUE(root.node_is_fixed==true); } ASSERT_TRUE(successes > 0); } From eee717008f6e9feb00c04e12fd795bdbb41199ce Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 26 Nov 2025 13:54:47 -0300 Subject: [PATCH 17/30] Casting when calculating the diff --- src/simplification/inexact.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/simplification/inexact.h b/src/simplification/inexact.h index 3eb6e32c..d9169eea 100644 --- a/src/simplification/inexact.h +++ b/src/simplification/inexact.h @@ -171,7 +171,7 @@ class Inexact_simplifier Program

simplified_program(program); // prediction at the root already performs template cast and always returns a float - auto original_predictions = simplified_program.predict(d).template cast(); + auto original_predictions = simplified_program.predict(d); // iterate over the tree, trying to replace each node with a constant, and keeping the change if the pred does not change. // notice it is a post order iterator. @@ -214,9 +214,9 @@ class Inexact_simplifier spot = simplified_program.Tree.move_ontop(spot, simplified_branch.begin()); - auto new_predictions = simplified_program.predict(d).template cast(); + auto new_predictions = simplified_program.predict(d); - float diff = (original_predictions - new_predictions).square().mean(); + float diff = (original_predictions.template cast() - new_predictions.template cast()).square().mean(); if (diff < best_distance) { best_distance = diff; From e64839a702f2f9a69b526b5f2d7e5e7cb68b03ff Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Fri, 5 Dec 2025 06:29:05 -0300 Subject: [PATCH 18/30] Modified complexity to ignore Mul operation from weighted nodes That was making complexity values explore by considering intermediate nodes when doing the recursive calculation, often leading to overflow of the integer value. I also updated the cpp test cases to print the min and max values for each data type so we can manually check if the value is suitable for the calculations we are doing. --- src/program/tree_node.cpp | 13 ++++++------- tests/cpp/test_params.cpp | 8 ++++++++ tests/cpp/testsHeader.h | 3 ++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/program/tree_node.cpp b/src/program/tree_node.cpp index b0418adf..05ac6dc7 100644 --- a/src/program/tree_node.cpp +++ b/src/program/tree_node.cpp @@ -185,9 +185,7 @@ int TreeNode::get_linear_complexity() const // ignoring weight if it has the value of neutral element of operation if ((Is(data.node_type) && data.W != 0.0) || (data.W != 1.0)) - return operator_complexities.at(NodeType::Mul) + - operator_complexities.at(NodeType::Constant) + - tree_complexity; + return operator_complexities.at(NodeType::Constant) + tree_complexity; } return tree_complexity; @@ -212,13 +210,14 @@ int TreeNode::get_complexity() const if (data.get_is_weighted() && Isnt(data.node_type) ) { + // complexity for offsetsum or weighted nodes. // ignoring weight if it has the value of neutral element of operation if ((Is(data.node_type) && data.W != 0.0) || (data.W != 1.0)) - return operator_complexities.at(NodeType::Mul)*( - operator_complexities.at(NodeType::Constant) + - node_complexity*(children_complexity_sum) - ); + // we are taking into account the weight but ignoring the multiplication + // (to avoid int overflow for deep trees) + return operator_complexities.at(NodeType::Constant) + + node_complexity*(children_complexity_sum); } return node_complexity*(children_complexity_sum); diff --git a/tests/cpp/test_params.cpp b/tests/cpp/test_params.cpp index 4be0ef89..61265e6d 100644 --- a/tests/cpp/test_params.cpp +++ b/tests/cpp/test_params.cpp @@ -7,6 +7,14 @@ using namespace Brush::Sel; TEST(Params, ParamsTests) { + std::cout << "int: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "unsigned int: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "short: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "unsigned short: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "long: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "unsigned long: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "long long: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; + std::cout << "unsigned long long: min=" << std::numeric_limits::min() << " max=" << std::numeric_limits::max() << "\n"; Parameters params; diff --git a/tests/cpp/testsHeader.h b/tests/cpp/testsHeader.h index 2e9f56c9..bd89ab30 100644 --- a/tests/cpp/testsHeader.h +++ b/tests/cpp/testsHeader.h @@ -8,7 +8,8 @@ #include #include #include -#include +#include +#include // stuff being used From 47f9c22ac2d69c69ce60d5b4fad6aff80fef7978 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Mon, 15 Dec 2025 09:22:58 -0300 Subject: [PATCH 19/30] improves printing expressions with 1.00* weights --- src/program/node.cpp | 30 ++++++++++++++++++------------ src/program/node.h | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/program/node.cpp b/src/program/node.cpp index 13c3d640..51cd413b 100644 --- a/src/program/node.cpp +++ b/src/program/node.cpp @@ -19,10 +19,13 @@ ostream& operator<<(ostream& os, const Node& n) /// @return name auto Node::get_name(bool include_weight) const noexcept -> std::string { + constexpr float atol = 1e-6f; + const bool weight_is_one = std::fabs(W - 1.0f) <= atol; + if (Is(node_type)) { - if (is_weighted && W != 1.0 && include_weight) - return fmt::format("{:.2f}*{}",W,feature); + if (is_weighted && !weight_is_one && include_weight) + return fmt::format("{:.2f}*{}", W, feature); else return feature; } @@ -32,23 +35,26 @@ auto Node::get_name(bool include_weight) const noexcept -> std::string } else if (Is(node_type)) { - // this will show (MeanLabel) in the terminal name - // return fmt::format("{:.2f} ({})", W, feature); - + // this will show (MeanLabel) in the terminal name so we can differentiate + // a meanLabel from a constant. return fmt::format("{:.2f}", W); } - else if (Is(node_type)){ - if (is_weighted && W != 1.0) - return fmt::format("{:.2f}+Sum", W); + else if (Is(node_type)) + { + if (is_weighted && !weight_is_one) + return fmt::format("{:.2f}+Add", W); - return fmt::format("Sum"); + return "Sum"; + } + else if (is_weighted && !weight_is_one && include_weight) + { + return fmt::format("{:.2f}*{}", W, name); } - else if (is_weighted && include_weight) - return fmt::format("{:.2f}*{}",W,name); return name; } + string Node::get_model(const vector& children) const noexcept { if (children.empty()) @@ -91,7 +97,7 @@ string Node::get_model(const vector& children) const noexcept args += ","; } - return fmt::format("Sum({})", args); + return fmt::format("Add({})", args); } else{ string args = ""; diff --git a/src/program/node.h b/src/program/node.h index e3c14dfe..21ec2233 100644 --- a/src/program/node.h +++ b/src/program/node.h @@ -191,7 +191,7 @@ struct Node { } /// @brief gets a string version of the node for printing. - /// @param include_weight whether to include the node's weight in the output. + /// @param include_weight whether to include the node's weight in the output. Multiplications by one are omitted. /// @return string version of the node. string get_name(bool include_weight=true) const noexcept; string get_model(const vector&) const noexcept; From 2ae370c9ab3ddf2bc65fb18cc51dc68205261d76 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Mon, 15 Dec 2025 13:35:52 -0300 Subject: [PATCH 20/30] Fixed final model selection not working for regression --- pybrush/BrushEstimator.py | 3 +- tests/python/test_final_model_selection.py | 69 +++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/pybrush/BrushEstimator.py b/pybrush/BrushEstimator.py index 1f0a991d..4b9716e5 100644 --- a/pybrush/BrushEstimator.py +++ b/pybrush/BrushEstimator.py @@ -266,7 +266,8 @@ def eval(ind, sample=None): # if user_defined, sample_weight is given by his custom weights. if # support, I calculate it here. otherwise, no weight is used - if self.class_weights not in ['unbalanced'] and self.parameters_.scorer not in ['balanced_accuracy']: + if self.mode == 'classification' \ + and (self.class_weights not in ['unbalanced'] and self.parameters_.scorer not in ['balanced_accuracy']): sample_weight = [] if isinstance(self.class_weights, list): # using user-defined values sample_weight = [self.class_weights[int(label)] for label in y] diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index 6d137572..f7d99306 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -1,6 +1,8 @@ import pytest import numpy as np +import pickle + from pybrush import BrushRegressor, BrushClassifier, Dataset from sklearn.model_selection import GridSearchCV @@ -227,5 +229,68 @@ def eval(individual, sample=None, log=False): # Assert that Brush picked the same candidate assert est.best_estimator_.get_model() == chosen.get_model() -if __name__ == "__main__": - pytest.main() \ No newline at end of file +@pytest.mark.parametrize( + "final_model_selection", + [ + "smallest_complexity", + "best_validation_ci", + "", # default behavior + ], +) +def test_pickle_unpickle_with_different_final_model_selection(final_model_selection): + # previous test is using a classification problem. this one focuses on regression + + X, y = make_regression( + n_samples=80, n_features=6, noise=0.1, random_state=1 + ) + + model = BrushRegressor( + max_gens=5, + pop_size=12, + final_model_selection=final_model_selection, + ) + model.fit(X, y) + + # Pickle / unpickle + dumped = pickle.dumps(model) + loaded = pickle.loads(dumped) + + # Basic sanity checks + assert loaded.final_model_selection == model.final_model_selection + assert loaded.best_estimator_ is not None + assert loaded.archive_ is not None + assert len(loaded.archive_) == len(model.archive_) + + # Best estimator should still belong to archive or be valid + assert loaded.best_estimator_ in loaded.archive_ + [loaded.best_estimator_] + +# Pickle cant serialize local functions (functions defined inside another function). +# we need to declare it at the module level if we want to avoid extra dependencies +def pick_last(pop, archive): + return archive[-1] + +def test_pickle_unpickle_with_callable_final_model_selection(): + + X, y = make_classification( + n_samples=60, n_features=5, random_state=7 + ) + + model = BrushClassifier( + max_gens=5, + pop_size=10, + final_model_selection=pick_last, + ) + model.fit(X, y) + + # Ensure callable selection worked pre-pickle + assert model.best_estimator_ == model.archive_[-1] + + # Pickle / unpickle + dumped = pickle.dumps(model) + loaded = pickle.loads(dumped) + + # Callable must still exist and be callable + assert callable(loaded.final_model_selection) + + # Selection logic must still hold + assert loaded.best_estimator_ == loaded.archive_[-1] From 313bb9f2b5be79a567296b624134d74965f383b2 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 17 Dec 2025 14:32:33 -0300 Subject: [PATCH 21/30] Improved deap as an optional dependency --- pybrush/__init__.py | 19 ++++++++++-- pybrush/_versionstr copy.py | 21 ------------- pybrush/{ => deap_api}/DeapEstimator.py | 2 +- pybrush/deap_api/__init__.py | 3 +- tests/python/test_deap_api.py | 6 ++-- tests/python/test_final_model_selection.py | 36 ++++++++++++++++++++++ 6 files changed, 59 insertions(+), 28 deletions(-) delete mode 100644 pybrush/_versionstr copy.py rename pybrush/{ => deap_api}/DeapEstimator.py (99%) diff --git a/pybrush/__init__.py b/pybrush/__init__.py index 3d6bceb2..1cc6acd3 100644 --- a/pybrush/__init__.py +++ b/pybrush/__init__.py @@ -18,6 +18,21 @@ from ._brush import RegressorSelector, ClassifierSelector, MultiClassifierSelector from ._brush import RegressorVariator, ClassifierVariator, MultiClassifierVariator -# full estimator implementations -------------------- -from pybrush.DeapEstimator import DeapClassifier, DeapRegressor from pybrush.BrushEstimator import BrushClassifier, BrushRegressor + +# deap api +try: + from pybrush import deap_api +except ImportError: + import warnings + + class _DeapAPIWarning: + def __getattr__(self, name): + warnings.warn( + "deap_api could not be imported. Please install required dependencies.", + ImportWarning, + stacklevel=2 + ) + raise AttributeError(f"deap_api is not available") + + deap_api = _DeapAPIWarning() diff --git a/pybrush/_versionstr copy.py b/pybrush/_versionstr copy.py deleted file mode 100644 index 50764e26..00000000 --- a/pybrush/_versionstr copy.py +++ /dev/null @@ -1,21 +0,0 @@ -# file generated by setuptools-scm -# don't change, don't track in version control - -__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] - -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] -else: - VERSION_TUPLE = object - -version: str -__version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE - -__version__ = version = '0.1.2.dev541+g18b38829.d20250617' -__version_tuple__ = version_tuple = (0, 1, 2, 'dev541', 'g18b38829.d20250617') diff --git a/pybrush/DeapEstimator.py b/pybrush/deap_api/DeapEstimator.py similarity index 99% rename from pybrush/DeapEstimator.py rename to pybrush/deap_api/DeapEstimator.py index 47255d8d..ee7dde87 100644 --- a/pybrush/DeapEstimator.py +++ b/pybrush/deap_api/DeapEstimator.py @@ -22,7 +22,7 @@ from pandas.api.types import is_float_dtype, is_bool_dtype, is_integer_dtype from pybrush.EstimatorInterface import EstimatorInterface -from pybrush.deap_api import nsga2 +from pybrush.deap_api.nsga2 import nsga2 from pybrush import individual from pybrush import RegressorEvaluator, ClassifierEvaluator, MultiClassifierEvaluator from pybrush import RegressorSelector, ClassifierSelector, MultiClassifierSelector diff --git a/pybrush/deap_api/__init__.py b/pybrush/deap_api/__init__.py index e13697ee..feb13bf4 100644 --- a/pybrush/deap_api/__init__.py +++ b/pybrush/deap_api/__init__.py @@ -1 +1,2 @@ -from pybrush.deap_api.nsga2 import nsga2 \ No newline at end of file +from pybrush.deap_api.nsga2 import nsga2 +from pybrush.deap_api.DeapEstimator import DeapClassifier, DeapRegressor \ No newline at end of file diff --git a/tests/python/test_deap_api.py b/tests/python/test_deap_api.py index 4862ce76..cca0c77b 100644 --- a/tests/python/test_deap_api.py +++ b/tests/python/test_deap_api.py @@ -29,7 +29,7 @@ def DEAP_classification_setup(): X = df.drop(columns='target') y = df['target'] - return pybrush.DeapClassifier, X, y + return pybrush.deap_api.DeapClassifier, X, y @pytest.fixture def DEAP_multiclass_classification_setup(): @@ -37,7 +37,7 @@ def DEAP_multiclass_classification_setup(): X = df.drop(columns='target') y = df['target'] - return pybrush.DeapClassifier, X, y + return pybrush.deap_api.DeapClassifier, X, y @pytest.fixture def DEAP_regression_setup(): @@ -45,7 +45,7 @@ def DEAP_regression_setup(): X = df.drop(columns='label') y = df['label'] - return pybrush.DeapRegressor, X, y + return pybrush.deap_api.DeapRegressor, X, y @pytest.fixture diff --git a/tests/python/test_final_model_selection.py b/tests/python/test_final_model_selection.py index f7d99306..79401a73 100644 --- a/tests/python/test_final_model_selection.py +++ b/tests/python/test_final_model_selection.py @@ -294,3 +294,39 @@ def test_pickle_unpickle_with_callable_final_model_selection(): # Selection logic must still hold assert loaded.best_estimator_ == loaded.archive_[-1] + + +@pytest.mark.parametrize( + "final_model_selection", + [ + "smallest_complexity", + "best_validation_ci", + "", # default behavior + ], +) +def test_pickle_unpickle_with_different_final_model_selection(final_model_selection): + # previous test is using a classification problem. this one focuses on regression + + X, y = make_regression( + n_samples=80, n_features=6, noise=0.1, random_state=1 + ) + + model = BrushRegressor( + max_gens=5, + pop_size=12, + final_model_selection=final_model_selection, + ) + model.fit(X, y) + + # Pickle / unpickle + dumped = pickle.dumps(model) + loaded = pickle.loads(dumped) + + # Basic sanity checks + assert loaded.final_model_selection == model.final_model_selection + assert loaded.best_estimator_ is not None + assert loaded.archive_ is not None + assert len(loaded.archive_) == len(model.archive_) + + # Best estimator should still belong to archive or be valid + assert loaded.best_estimator_ in loaded.archive_ + [loaded.best_estimator_] From 28c59aec2dcdaf42af5464fc05514de05d0f7bac Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Thu, 18 Dec 2025 08:13:05 -0300 Subject: [PATCH 22/30] Trying to make brush more deterministic --- src/pop/archive.cpp | 12 +++--- src/pop/population.cpp | 8 ++-- src/selection/nsga2.cpp | 6 +-- tests/python/test_params.py | 85 ++++++++++++++++++++++--------------- 4 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/pop/archive.cpp b/src/pop/archive.cpp index fd16b833..16af0805 100644 --- a/src/pop/archive.cpp +++ b/src/pop/archive.cpp @@ -112,12 +112,12 @@ void Archive::init(Population& pop) } if (this->sort_complexity) { if (this->linear_complexity) - std::sort(individuals.begin(), individuals.end(), &sortLinearComplexity); + std::stable_sort(individuals.begin(), individuals.end(), &sortLinearComplexity); else - std::sort(individuals.begin(), individuals.end(), &sortComplexity); + std::stable_sort(individuals.begin(), individuals.end(), &sortComplexity); } else - std::sort(individuals.begin(),individuals.end(), &sortObj1); + std::stable_sort(individuals.begin(),individuals.end(), &sortObj1); } @@ -141,12 +141,12 @@ void Archive::update(Population& pop, const Parameters& params) if (this->sort_complexity) { if (this->linear_complexity) - std::sort(individuals.begin(), individuals.end(), &sortLinearComplexity); + std::stable_sort(individuals.begin(), individuals.end(), &sortLinearComplexity); else - std::sort(individuals.begin(), individuals.end(), &sortComplexity); + std::stable_sort(individuals.begin(), individuals.end(), &sortComplexity); } else { - std::sort(individuals.begin(), individuals.end(), &sortObj1); + std::stable_sort(individuals.begin(), individuals.end(), &sortObj1); } /* auto it = std::unique(individuals.begin(),individuals.end(), &sameFitComplexity); */ diff --git a/src/pop/population.cpp b/src/pop/population.cpp index 13318f0c..5445fabf 100644 --- a/src/pop/population.cpp +++ b/src/pop/population.cpp @@ -250,9 +250,9 @@ vector> Population::sorted_front(unsigned rank) } if (this->linear_complexity) - std::sort(pf.begin(),pf.end(),SortLinearComplexity(*this)); + std::stable_sort(pf.begin(),pf.end(),SortLinearComplexity(*this)); else - std::sort(pf.begin(),pf.end(),SortComplexity(*this)); + std::stable_sort(pf.begin(),pf.end(),SortComplexity(*this)); auto it = std::unique(pf.begin(),pf.end(),SameFitComplexity(*this)); @@ -312,9 +312,9 @@ vector Population::hall_of_fame(unsigned rank) } if (this->linear_complexity) - std::sort(hof.begin(),hof.end(),SortLinearComplexity(*this)); + std::stable_sort(hof.begin(),hof.end(),SortLinearComplexity(*this)); else - std::sort(hof.begin(),hof.end(),SortComplexity(*this)); + std::stable_sort(hof.begin(),hof.end(),SortComplexity(*this)); auto it = std::unique(hof.begin(),hof.end(),SameFitComplexity(*this)); diff --git a/src/selection/nsga2.cpp b/src/selection/nsga2.cpp index 57eac371..48fbd133 100644 --- a/src/selection/nsga2.cpp +++ b/src/selection/nsga2.cpp @@ -105,7 +105,7 @@ vector NSGA2::survive(Population& pop, int island, // fmt::print("crowding distance\n"); crowding_distance(pop, front, i); // calculate crowding in final front to include - std::sort(front.at(i).begin(),front.at(i).end(),sort_n(pop)); + std::stable_sort(front.at(i).begin(),front.at(i).end(),sort_n(pop)); // fmt::print("adding last front)\n"); const int extra = params.pop_size - selected.size(); @@ -166,7 +166,7 @@ vector> NSGA2::fast_nds(Population& pop, vector& islan // using OpenMP can have different orders in the front.at(0) // so let's sort it so that the algorithm is deterministic // given a seed - std::sort(front.at(0).begin(), front.at(0).end()); + std::stable_sort(front.at(0).begin(), front.at(0).end()); int fi = 1; while (front.at(fi-1).size() > 0) { @@ -227,7 +227,7 @@ void NSGA2::crowding_distance(Population& pop, vector>& front, for (int m = 0; m < limit; ++m) { // fmt::print("m {}\n", m); - std::sort(F.begin(), F.end(), comparator_obj(pop,m)); + std::stable_sort(F.begin(), F.end(), comparator_obj(pop,m)); // in the paper dist=INF for the first and last, in the code // this is only done to the first one or to the two first when size=2 diff --git a/tests/python/test_params.py b/tests/python/test_params.py index 874490bc..1165baed 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -7,44 +7,59 @@ import numpy as np -# TODO; get this to work again -# def test_param_random_state(): -# # Check if make_regressor, mutation and crossover will create the same expressions -# test_y = np.array( [1. , 0. , 1.4, 1. , 0. , 1. , 1. , 0. , 0. , 0. ]) -# test_X = np.array([[1.1, 2.0, 3.0, 4.0, 5.0, 6.5, 7.0, 8.0, 9.0, 10.0], -# [2.0, 1.2, 6.0, 4.0, 5.0, 8.0, 7.0, 5.0, 9.0, 10.0]]).T +def test_param_random_state(): + """Test that random_state produces deterministic results.""" + test_y = np.array([1., 0., 1.4, 1., 0., 1., 1., 0., 0., 0.]) + test_X = np.array([[1.1, 2.0, 3.0, 4.0, 5.0, 6.5, 7.0, 8.0, 9.0, 10.0], + [2.0, 1.2, 6.0, 4.0, 5.0, 8.0, 7.0, 5.0, 9.0, 10.0]]).T -# data = _brush.Dataset(test_X, test_y) -# SS = _brush.SearchSpace(data) + # First run with random_state=123 + reg1 = BrushRegressor( + random_state=123, + max_gens=50, + pop_size=10, + num_islands=4, + verbosity=0 + ).fit(test_X, test_y) -# _brush.set_random_state(123) - -# first_run = [] -# for d in range(1,4): -# for s in range(1,20): -# prg = SS.make_regressor(d, s) -# prg, _ = prg.mutate() - -# if prg != None: prg, _ = prg.cross(prg) -# if prg != None: first_run.append(prg.get_model()) + first_run_models = [ind.program.get_model() for ind in reg1.population_] + first_run_best = reg1.best_estimator_.program.get_model() + first_run_fitness = reg1.best_estimator_.fitness.values -# assert len(first_run) > 0, "either mutation or crossover is always failing" - -# _brush.set_random_state(123) - -# second_run = [] -# for d in range(1,4): -# for s in range(1,20): -# prg = SS.make_regressor(d, s) -# prg, _ = prg.mutate() - -# if prg != None: prg, _ = prg.cross(prg) -# if prg != None: second_run.append(prg.get_model()) - -# assert len(second_run) > 0, "either mutation or crossover is always failing" - -# for fr, sr in zip(first_run, second_run): -# assert fr==sr, "random state failed to generate same expressions" + assert len(first_run_models) > 0, "First run produced no individuals" + + # Second run with same random_state=123 + reg2 = BrushRegressor( + random_state=123, + max_gens=50, + pop_size=10, + num_islands=4, + verbosity=0 + ).fit(test_X, test_y) + + second_run_models = [ind.program.get_model() for ind in reg2.population_] + second_run_best = reg2.best_estimator_.program.get_model() + second_run_fitness = reg2.best_estimator_.fitness.values + + assert len(second_run_models) > 0, "Second run produced no individuals" + + # Check that populations match + assert len(first_run_models) == len(second_run_models), \ + f"Population sizes differ: {len(first_run_models)} vs {len(second_run_models)}" + + for i, (fr, sr) in enumerate(zip(first_run_models, second_run_models)): + print(f"{fr} vs {sr}") + assert fr == sr, f"Individual {i} differs: '{fr}' vs '{sr}'" + + # Check that best individuals match + assert first_run_best == second_run_best, \ + f"Best models differ: '{first_run_best}' vs '{second_run_best}'" + + assert np.allclose(first_run_fitness, second_run_fitness), \ + f"Best fitness values differ: {first_run_fitness} vs {second_run_fitness}" + + print(f"Best model: {first_run_best}") + print(f"Best fitness: {first_run_fitness}") # def _change_and_wait(config): From 83e348211cc05a87f549a49d607b11229387a816 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 24 Dec 2025 20:11:26 -0300 Subject: [PATCH 23/30] assert is_fitted is false --- src/program/program.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/program/program.h b/src/program/program.h index e7e0bfa2..744b745b 100644 --- a/src/program/program.h +++ b/src/program/program.h @@ -64,7 +64,7 @@ template struct Program RetType>>; /// whether fit has been called - bool is_fitted_; + bool is_fitted_ = false; /// fitness // Fitness fitness; @@ -76,7 +76,7 @@ template struct Program Program() = default; Program(const std::reference_wrapper s, const tree t) - : Tree(t) + : Tree(t), is_fitted_(false) { SSref = std::optional>{s}; } From ef9dc9970f115e3e556790af65eef3535b872a45 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 24 Dec 2025 20:14:35 -0300 Subject: [PATCH 24/30] boolean operators fixed. new comparison operators --- src/program/functions.h | 132 +++++++++++++++++++++------------------ src/program/node.cpp | 16 +++++ src/program/nodetype.cpp | 4 +- src/program/nodetype.h | 26 ++++---- src/program/signatures.h | 18 ++++++ 5 files changed, 119 insertions(+), 77 deletions(-) diff --git a/src/program/functions.h b/src/program/functions.h index 0d052336..ababd344 100644 --- a/src/program/functions.h +++ b/src/program/functions.h @@ -433,105 +433,113 @@ namespace Brush // } }; - /* logical and -- mul with boolean inputs */ + /* logical and -- boolean AND operation */ template<> struct Function { template inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - // return t1 && t2; // old, wasnt sure if it works - - return (t1 * t2).template cast(); + // For boolean arrays, use element-wise logical AND + return t1 && t2; } template requires same_as inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - // ArrayXb t1_bool(t1.size()); - // for (int i = 0; i< t1.size(); ++i) - // t1_bool(i) = t1(i).a; - - // ArrayXb t2_bool(t2.size()); - // for (int i = 0; i< t2.size(); ++i) - // t2_bool(i) = t2(i).a; - - // return (t1_bool || t2_bool).cast(); - - // line below may work better than logic above - // --- - return t1 * t2; // relies on bJet::operator* + // For bJet, operate on the underlying boolean value (.a). + // Boolean logic doesn't have meaningful derivatives, here (And, Or, Not) we set them to zero + ArrayXbJet result(t1.size()); + for (int i = 0; i < t1.size(); ++i) { + result(i).a = t1(i).a && t2(i).a; + result(i).v.setZero(); + } + return result; } }; - /* logical or -- add with boolean inputs */ + /* logical or -- boolean OR operation */ template<> struct Function { template inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - // return t1 || t2; - return ((t1 + t2).min(1)).template cast(); + // use element-wise logical OR + return t1 || t2; } template requires same_as inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - return t1 + t2; // bJet::operator+ + // For bJet, operate on the underlying boolean value (.a) + ArrayXbJet result(t1.size()); + for (int i = 0; i < t1.size(); ++i) { + result(i).a = t1(i).a || t2(i).a; + result(i).v.setZero(); + } + return result; } }; - /* logical not -- negate the input */ + /* logical not -- boolean NOT operation */ template<> struct Function { template inline auto operator()(const ArrayBase& t) { - // return !t; - return (1 - t).template cast(); + // use element-wise logical NOT + return !t; } template requires same_as inline auto operator()(const ArrayBase& t) { - // auto trues = ArrayXb::Constant(t.size(), true); - // return (t - trues); - - // for (size_t i = 0; i < t.size(); ++i) { - // t.at(i).a = !t.at(i).a; - // } - - // return t; - - return T(1) - t; + ArrayXbJet result(t.size()); + for (int i = 0; i < t.size(); ++i) { + result(i).a = !t(i).a; + result(i).v.setZero(); + } + return result; } }; // comparison operators. These will help changing the types along the tree - /* coefficient-wise greater than or equal */ - // template<> - // struct Function - // { - // template - // inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { - // return (t1 >= t2); - // } - - // template requires same_as - // inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { - // return (t1 - t2) >= bJet(0); - // } - // }; + /* coefficient-wise greater than or equal - works with ArrayXf */ + template<> + struct Function + { + template + inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { + // Element-wise >= comparison + return t1 >= t2; + } - /* coefficient-wise equal */ - // template<> - // struct Function - // { - // template - // inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - // return (t1 - t2).template cast(); - // } - - // template requires same_as - // inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) { - // return t1 * t2; // relies on bJet::operator* - // } - // }; + template requires same_as + inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { + ArrayXbJet result(t1.size()); + for (int i = 0; i < t1.size(); ++i) { + result(i).a = t1(i).a >= t2(i).a; + result(i).v.setZero(); + } + return result; + } + }; + + /* coefficient-wise equality - works with ArrayXi for exact integer comparison */ + template<> + struct Function + { + template + inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { + // Element-wise == comparison, returns boolean array + return t1 == t2; + } + + template requires same_as + inline auto operator()(const ArrayBase& t1, const ArrayBase& t2) const { + ArrayXbJet result(t1.size()); + for (int i = 0; i < t1.size(); ++i) { + result(i).a = t1(i).a == t2(i).a; + result(i).v.setZero(); + } + return result; + } + }; } // Brush #endif diff --git a/src/program/node.cpp b/src/program/node.cpp index 51cd413b..e3774482 100644 --- a/src/program/node.cpp +++ b/src/program/node.cpp @@ -243,6 +243,22 @@ void init_node_with_default_signature(Node& node) } else if (Is(n)) node.set_signature>(); + else if (Is(n)) + { + // For terminals, use feature_type to determine the correct signature + switch (node.get_feature_type()) { + case DataType::ArrayB: + node.set_signature>(); + break; + case DataType::ArrayI: + node.set_signature>(); + break; + case DataType::ArrayF: + default: + node.set_signature>(); + break; + } + } else node.set_signature>(); } diff --git a/src/program/nodetype.cpp b/src/program/nodetype.cpp index c2a41e52..873f1e28 100644 --- a/src/program/nodetype.cpp +++ b/src/program/nodetype.cpp @@ -38,11 +38,11 @@ std::map NodeNameType = { // {"Xor", NodeType::Xor}, // decision (same) - /* {"Equals", NodeType::Equals}, */ + {"Equals", NodeType::Equals}, + {"Geq", NodeType::Geq}, /* {"LessThan", NodeType::LessThan}, */ /* {"GreaterThan", NodeType::GreaterThan}, */ /* {"Leq", NodeType::Leq}, */ - /* {"Geq", NodeType::Geq}, */ // reductions {"Min", NodeType::Min}, diff --git a/src/program/nodetype.h b/src/program/nodetype.h index 96a78ad7..d7239aac 100644 --- a/src/program/nodetype.h +++ b/src/program/nodetype.h @@ -87,26 +87,26 @@ enum class NodeType : uint64_t { // Each node type must have a complexity // Xor = 1UL << 39UL, // comparison - // Equals = 1UL << 41UL, - // Geq = 1UL << 42UL, + Equals = 1UL << 41UL, + Geq = 1UL << 42UL, /* GreaterThan = 1UL << 41UL, */ /* Leq = 1UL << 42UL, */ /* LessThan = 1UL << 43UL, */ // leaves (must be the last ones in this enum) - MeanLabel = 1UL << 41UL, - Constant = 1UL << 42UL, - Terminal = 1UL << 43UL, + MeanLabel = 1UL << 43UL, + Constant = 1UL << 44UL, + Terminal = 1UL << 45UL, // TODO: implement operators below and move them before leaves - ArgMax = 1UL << 44UL, + ArgMax = 1UL << 46UL, // count the number of elements in an array. Should be the last element in the enum - Count = 1UL << 45UL, + Count = 1UL << 47UL, // // custom - CustomUnaryOp = 1UL << 46UL, - CustomBinaryOp = 1UL << 47UL, - CustomSplit = 1UL << 48UL + CustomUnaryOp = 1UL << 48UL, + CustomBinaryOp = 1UL << 49UL, + CustomSplit = 1UL << 50UL }; @@ -118,7 +118,7 @@ struct NodeTypes { // index of last available node visible to search_space. // It must match the highest bit used in the enum. //notice that we will create the nodetypes until this count - static constexpr size_t Count = 44; + static constexpr size_t Count = 46; // subtracting leaves (leaving just the ops into this) static constexpr size_t OpCount = Count-3; @@ -199,8 +199,8 @@ NLOHMANN_JSON_SERIALIZE_ENUM( NodeType, { // {NodeType::Xor,"Xor" }, // decision (same) - // {NodeType::Equals,"Equals" }, - // {NodeType::Geq,"Geq" }, + {NodeType::Equals,"Equals" }, + {NodeType::Geq,"Geq" }, /* {NodeType::LessThan,"LessThan" }, */ /* {NodeType::Leq,"Leq" }, */ /* {NodeType::Geq,"Geq" }, */ diff --git a/src/program/signatures.h b/src/program/signatures.h index 6f8f3957..4fddfe7a 100644 --- a/src/program/signatures.h +++ b/src/program/signatures.h @@ -259,6 +259,20 @@ struct Signatures{ >; }; +template<> +struct Signatures{ + using type = std::tuple< + Signature + >; + }; + +template<> +struct Signatures{ + using type = std::tuple< + Signature + >; + }; + template struct Signatures struct Signatures{ + // NOTE: the problem of having different values for first child + // is that serialization does not handle it pretty good atm. + // TODO: improve to fix the note above. + // spliton and splitbest will always compare to the weight if is a number, otherwise will use the boolean value // TODO: idea: if we have the LEQ or GEQ, we can have splitOn with all different data types without // having to make it explicit in the signature. I think there is too many types of splitOn that makes it hard to actually be used From 65cffd8ac88dbe279e22ff54e53ee97c0c0663d0 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Wed, 24 Dec 2025 20:14:55 -0300 Subject: [PATCH 25/30] New test for boolean operators. updated output of one guide notebook --- docs/guide/working_with_programs.ipynb | 758 ++++++++++++------------- tests/cpp/test_program.cpp | 109 +++- 2 files changed, 456 insertions(+), 411 deletions(-) diff --git a/docs/guide/working_with_programs.ipynb b/docs/guide/working_with_programs.ipynb index a02fdb4d..569c1ea7 100644 --- a/docs/guide/working_with_programs.ipynb +++ b/docs/guide/working_with_programs.ipynb @@ -67,7 +67,29 @@ "execution_count": 1, "id": "102e3fcb", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 768 entries, 0 to 767\n", + "Data columns (total 8 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 x0 768 non-null float64\n", + " 1 x1 768 non-null float64\n", + " 2 x2 768 non-null float64\n", + " 3 x3 768 non-null float64\n", + " 4 x4 768 non-null float64\n", + " 5 x5 768 non-null int64 \n", + " 6 x6 768 non-null float64\n", + " 7 x7 768 non-null int64 \n", + "dtypes: float64(6), int64(2)\n", + "memory usage: 48.1 KB\n" + ] + } + ], "source": [ "import pandas as pd\n", "from pybrush import BrushRegressor\n", @@ -75,7 +97,9 @@ "# load data\n", "df = pd.read_csv('../examples/datasets/d_enc.csv')\n", "X = df.drop(columns='label')\n", - "y = df['label']" + "y = df['label']\n", + "\n", + "X.info() # we have several float and two integer features" ] }, { @@ -89,20 +113,31 @@ "output_type": "stream", "text": [ "Completed 100% [====================]\n", - "score: 0.9441623847546605\n" + "Best model: If(x0>=0.76)\n", + "|- If(x0>=0.82)\n", + "| |- 29.83\n", + "| |- 48.74*x0\n", + "|- 1.36*Add\n", + "| |- 0.01*x1\n", + "| |- 8.16*x6\n", + "score: 0.8950117552104557\n" ] } ], "source": [ "# import and make a regressor\n", "est = BrushRegressor(\n", - " functions=['SplitBest','Mul','Add','Cos','Exp','Sin'],\n", - " max_depth=5,\n", + " # Uncomment the line below to constrain the search space to fewer functions\n", + " functions=['SplitBest', 'SplitOn', 'Geq', 'Eq', 'Mul', 'Add', 'Cos', 'Exp'],\n", + " max_depth=3,\n", " verbosity=1 # set verbosity==1 to see a progress bar\n", ")\n", "\n", "# use like you would a sklearn regressor\n", "est.fit(X,y)\n", + "\n", + "print(\"Best model:\", est.best_estimator_.get_model(\"tree\"))\n", + "\n", "y_pred = est.predict(X)\n", "print('score:', est.score(X,y))" ] @@ -125,7 +160,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Fitness(4.757904 74.000000 )\n", + "Fitness(10.703141 34.000000 )\n", "['scorer', 'linear_complexity']\n" ] } @@ -186,9 +221,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "28\n", - "134628\n", - "5\n" + "25\n", + "250\n", + "3\n" ] } ], @@ -218,13 +253,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "fitness {'complexity': 134628, 'crowding_dist': 0.0984368622303009, 'dcounter': 0, 'depth': 5, 'dominated': [0, 15], 'linear_complexity': 74, 'loss': 5.118783950805664, 'loss_v': 4.757904052734375, 'prev_complexity': 134628, 'prev_depth': 5, 'prev_linear_complexity': 74, 'prev_loss': 5.118783950805664, 'prev_loss_v': 4.757904052734375, 'prev_size': 28, 'rank': 1, 'size': 28, 'values': [4.757904052734375, 74.0], 'weights': [-1.0, -1.0], 'wvalues': [-4.757904052734375, -74.0]}\n", - "id 227\n", + "fitness {'complexity': 250, 'crowding_dist': 3.4028234663852886e+38, 'dcounter': 0, 'depth': 3, 'dominated': [], 'linear_complexity': 34, 'loss': 9.187416076660156, 'loss_v': 10.703141212463379, 'prev_complexity': 250, 'prev_depth': 3, 'prev_linear_complexity': 34, 'prev_loss': 9.187414169311523, 'prev_loss_v': 10.703254699707031, 'prev_size': 25, 'rank': 1, 'size': 25, 'values': [10.703141212463379, 34.0], 'weights': [-1.0, -1.0], 'wvalues': [-10.703141212463379, -34.0]}\n", + "id 291\n", "is_fitted_ False\n", "objectives ['mse', 'linear_complexity']\n", - "parent_id [245]\n", - "program {'Tree': [{'W': 0.5537276268005371, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': '', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': False, 'name': 'Add', 'node_type': 'Add', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357}, {'W': 11.378684043884277, 'arg_types': ['ArrayF'], 'center_op': True, 'feature': '', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': True, 'name': 'Exp', 'node_type': 'Exp', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 13056393536346412951, 'sig_hash': 14128685871577087634}, {'W': 0.9108647704124451, 'arg_types': [], 'center_op': True, 'feature': 'x6', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': False, 'name': 'Terminal', 'node_type': 'Terminal', 'prob_change': 0.20750471949577332, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597}, {'W': 0.7599999904632568, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': 'x0', 'feature_type': 'ArrayI', 'fixed': False, 'is_weighted': True, 'name': 'SplitBest', 'node_type': 'SplitBest', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357}, {'W': 0.8199999928474426, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': 'x0', 'feature_type': 'ArrayI', 'fixed': False, 'is_weighted': True, 'name': 'SplitBest', 'node_type': 'SplitBest', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357}, {'W': 15.890183448791504, 'arg_types': [], 'center_op': True, 'feature': 'constF', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': True, 'name': 'Constant', 'node_type': 'Constant', 'prob_change': 0.6167147755622864, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597}, {'W': 0.17655502259731293, 'arg_types': [], 'center_op': True, 'feature': 'x3', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': True, 'name': 'Terminal', 'node_type': 'Terminal', 'prob_change': 0.8625447154045105, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597}, {'W': 0.14056099951267242, 'arg_types': ['ArrayF'], 'center_op': True, 'feature': '', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': False, 'name': 'Exp', 'node_type': 'Exp', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 13056393536346412951, 'sig_hash': 14128685871577087634}, {'W': 1.7882635593414307, 'arg_types': ['ArrayF'], 'center_op': True, 'feature': '', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': True, 'name': 'Sin', 'node_type': 'Sin', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 13056393536346412951, 'sig_hash': 14128685871577087634}, {'W': 0.8196039795875549, 'arg_types': [], 'center_op': True, 'feature': 'x1', 'feature_type': 'ArrayF', 'fixed': False, 'is_weighted': True, 'name': 'Terminal', 'node_type': 'Terminal', 'prob_change': 0.6729995608329773, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597}], 'is_fitted_': True}\n", - "variation point\n" + "parent_id [274]\n", + "program {'Tree': [{'W': 0.7599999904632568, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': 'x0', 'feature_type': 'ArrayI', 'is_weighted': True, 'name': 'SplitBest', 'node_is_fixed': False, 'node_type': 'SplitBest', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357, 'weight_is_fixed': False}, {'W': 0.8199999928474426, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': 'x0', 'feature_type': 'ArrayI', 'is_weighted': True, 'name': 'SplitBest', 'node_is_fixed': False, 'node_type': 'SplitBest', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357, 'weight_is_fixed': False}, {'W': 29.8331241607666, 'arg_types': [], 'center_op': True, 'feature': 'constF', 'feature_type': 'ArrayF', 'is_weighted': True, 'name': 'Constant', 'node_is_fixed': False, 'node_type': 'Constant', 'prob_change': 0.6167147755622864, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597, 'weight_is_fixed': False}, {'W': 48.73774337768555, 'arg_types': [], 'center_op': True, 'feature': 'x0', 'feature_type': 'ArrayF', 'is_weighted': True, 'name': 'Terminal', 'node_is_fixed': False, 'node_type': 'Terminal', 'prob_change': 0.6343384981155396, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597, 'weight_is_fixed': False}, {'W': 1.3606120347976685, 'arg_types': ['ArrayF', 'ArrayF'], 'center_op': True, 'feature': '', 'feature_type': 'ArrayF', 'is_weighted': True, 'name': 'Add', 'node_is_fixed': False, 'node_type': 'Add', 'prob_change': 1.0, 'ret_type': 'ArrayF', 'sig_dual_hash': 14679000877885575597, 'sig_hash': 14400282083458657357, 'weight_is_fixed': False}, {'W': 0.013320915400981903, 'arg_types': [], 'center_op': True, 'feature': 'x1', 'feature_type': 'ArrayF', 'is_weighted': True, 'name': 'Terminal', 'node_is_fixed': False, 'node_type': 'Terminal', 'prob_change': 0.6729995608329773, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597, 'weight_is_fixed': False}, {'W': 8.156413078308105, 'arg_types': [], 'center_op': True, 'feature': 'x6', 'feature_type': 'ArrayF', 'is_weighted': True, 'name': 'Terminal', 'node_is_fixed': False, 'node_type': 'Terminal', 'prob_change': 0.20750471949577332, 'ret_type': 'ArrayF', 'sig_dual_hash': 7018942542468397869, 'sig_hash': 14162902253047951597, 'weight_is_fixed': False}], 'is_fitted_': True}\n", + "variation insert\n" ] } ], @@ -280,7 +315,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Add(11.38*Exp(x6),If(x0>=0.76,If(x0>=0.82,15.89,0.18*x3),Exp(1.79*Sin(0.82*x1))))\n" + "If(x0>=0.76,If(x0>=0.82,29.83,48.74*x0),1.36*Add(0.01*x1,8.16*x6))\n" ] } ], @@ -311,7 +346,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Add(11.38*Exp(x6),If(x0>=0.76,If(x0>=0.82,15.89,0.18*x3),Exp(1.79*Sin(0.82*x1))))\n" + "If(x0>=0.76,If(x0>=0.82,29.83,48.74*x0),1.36*Add(0.01*x1,8.16*x6))\n" ] } ], @@ -339,16 +374,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "Add\n", - "|- 11.38*Exp\n", - "| |- x6\n", - "|- If(x0>=0.76)\n", - "| |- If(x0>=0.82)\n", - "| | |- 15.89\n", - "| | |- 0.18*x3\n", - "| |- Exp\n", - "| | |- 1.79*Sin\n", - "| | | |- 0.82*x1\n" + "If(x0>=0.76)\n", + "|- If(x0>=0.82)\n", + "| |- 29.83\n", + "| |- 48.74*x0\n", + "|- 1.36*Add\n", + "| |- 0.01*x1\n", + "| |- 8.16*x6\n" ] } ], @@ -382,138 +414,116 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "G\n", - "\n", - "\n", + "\n", + "^ split feature fixed, * split threshold fixed\n", + "\n", "\n", - "141fd3450\n", - "\n", - "Add\n", + "y\n", + "\n", + "y\n", "\n", - "\n", + "\n", "\n", - "141fd2090\n", - "\n", - "Exp\n", + "177604080\n", + "\n", + "x0 >= 0.76?\n", "\n", - "\n", + "\n", "\n", - "141fd3450->141fd2090\n", - "\n", - "\n", - "11.38\n", + "y->177604080\n", + "\n", + "\n", + "0.76\n", "\n", - "\n", + "\n", "\n", - "141fcbe00\n", - "\n", - "x0>=0.76?\n", + "10fd43150\n", + "\n", + "x0 >= 0.82?\n", "\n", - "\n", + "\n", "\n", - "141fd3450->141fcbe00\n", - "\n", - "\n", + "177604080->10fd43150\n", + "\n", + "\n", + "Y\n", "\n", - "\n", + "\n", "\n", - "x6\n", - "\n", - "x6\n", + "17359a090\n", + "\n", + "Add\n", "\n", - "\n", + "\n", "\n", - "141fd2090->x6\n", - "\n", - "\n", + "177604080->17359a090\n", + "\n", + "\n", + "1.36\n", + "N\n", "\n", - "\n", + "\n", "\n", - "141fe2370\n", - "\n", - "x0>=0.82?\n", + "109926ea0\n", + "\n", + "29.83\n", "\n", - "\n", + "\n", "\n", - "141fcbe00->141fe2370\n", - "\n", - "\n", - "Y\n", + "10fd43150->109926ea0\n", + "\n", + "\n", + "Y\n", "\n", - "\n", + "\n", "\n", - "141ff19f0\n", - "\n", - "Exp\n", + "x0\n", + "\n", + "x0\n", "\n", - "\n", + "\n", "\n", - "141fcbe00->141ff19f0\n", - "\n", - "\n", - "N\n", + "10fd43150->x0\n", + "\n", + "\n", + "48.74\n", + "N\n", "\n", - "\n", + "\n", "\n", - "141fe2420\n", - "\n", - "15.89\n", + "x1\n", + "\n", + "x1\n", "\n", - "\n", + "\n", "\n", - "141fe2370->141fe2420\n", - "\n", - "\n", - "Y\n", + "17359a090->x1\n", + "\n", + "\n", + "0.01\n", "\n", - "\n", + "\n", "\n", - "x3\n", - "\n", - "x3\n", + "x6\n", + "\n", + "x6\n", "\n", - "\n", + "\n", "\n", - "141fe2370->x3\n", - "\n", - "\n", - "0.18\n", - "N\n", - "\n", - "\n", - "\n", - "141fd89e0\n", - "\n", - "Sin\n", - "\n", - "\n", - "\n", - "141ff19f0->141fd89e0\n", - "\n", - "\n", - "1.79\n", - "\n", - "\n", - "\n", - "x1\n", - "\n", - "x1\n", - "\n", - "\n", - "\n", - "141fd89e0->x1\n", - "\n", - "\n", - "0.82\n", + "17359a090->x6\n", + "\n", + "\n", + "8.16\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 11, @@ -547,26 +557,24 @@ "output_type": "stream", "text": [ "digraph G {\n", - "\"141fd3450\" [label=\"Add\"];\n", - "\"141fd3450\" -> \"141fd2090\" [label=\"11.38\"];\n", - "\"141fd3450\" -> \"141fcbe00\" [label=\"\"];\n", - "\"141fd2090\" [label=\"Exp\"];\n", - "\"141fd2090\" -> \"x6\" [label=\"\"];\n", - "\"x6\" [label=\"x6\"];\n", - "\"141fcbe00\" [label=\"x0>=0.76?\"];\n", - "\"141fcbe00\" -> \"141fe2370\" [headlabel=\"\",taillabel=\"Y\"];\n", - "\"141fcbe00\" -> \"141ff19f0\" [headlabel=\"\",taillabel=\"N\"];\n", - "\"141fe2370\" [label=\"x0>=0.82?\"];\n", - "\"141fe2370\" -> \"141fe2420\" [headlabel=\"\",taillabel=\"Y\"];\n", - "\"141fe2370\" -> \"x3\" [headlabel=\"0.18\",taillabel=\"N\"];\n", - "\"141fe2420\" [label=\"15.89\"];\n", - "\"x3\" [label=\"x3\"];\n", - "\"141ff19f0\" [label=\"Exp\"];\n", - "\"141ff19f0\" -> \"141fd89e0\" [label=\"1.79\"];\n", - "\"141fd89e0\" [label=\"Sin\"];\n", - "\"141fd89e0\" -> \"x1\" [label=\"0.82\"];\n", + "y [shape=box];\n", + "y -> \"177604080\" [label=\"0.76\"];\n", + "\"177604080\" [label=\"x0 >= 0.76?\"];\n", + "\"177604080\" -> \"10fd43150\" [headlabel=\"\",taillabel=\"Y\"];\n", + "\"177604080\" -> \"17359a090\" [headlabel=\"1.36\",taillabel=\"N\"];\n", + "\"10fd43150\" [label=\"x0 >= 0.82?\"];\n", + "\"10fd43150\" -> \"109926ea0\" [headlabel=\"\",taillabel=\"Y\"];\n", + "\"10fd43150\" -> \"x0\" [headlabel=\"48.74\",taillabel=\"N\"];\n", + "\"109926ea0\" [label=\"29.83\"];\n", + "\"x0\" [label=\"x0\"];\n", + "\"17359a090\" [label=\"Add\"];\n", + "\"17359a090\" -> \"x1\" [label=\"0.01\"];\n", + "\"17359a090\" -> \"x6\" [label=\"8.16\"];\n", "\"x1\" [label=\"x1\"];\n", - "}\n", + "\"x6\" [label=\"x6\"];\n", + "label=\"^ split feature fixed, * split threshold fixed\";\n", + "labelloc=bottom;\n", + "fontsize=10;}\n", "\n" ] } @@ -603,138 +611,116 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "G\n", - "\n", - "\n", + "\n", + "^ split feature fixed, * split threshold fixed\n", + "\n", "\n", - "141fd3450\n", - "\n", - "Add\n", + "y\n", + "\n", + "y\n", "\n", - "\n", + "\n", "\n", - "141fd2090\n", - "\n", - "Exp\n", + "177604080\n", + "\n", + "x0 >= 0.76?\n", "\n", - "\n", + "\n", "\n", - "141fd3450->141fd2090\n", - "\n", - "\n", - "11.38\n", + "y->177604080\n", + "\n", + "\n", + "0.76\n", "\n", - "\n", + "\n", "\n", - "141fcbe00\n", - "\n", - "x0>=0.76?\n", + "10fd43150\n", + "\n", + "x0 >= 0.82?\n", "\n", - "\n", + "\n", "\n", - "141fd3450->141fcbe00\n", - "\n", - "\n", + "177604080->10fd43150\n", + "\n", + "\n", + "Y\n", "\n", - "\n", + "\n", "\n", - "x6\n", - "\n", - "x6\n", + "17359a090\n", + "\n", + "Add\n", "\n", - "\n", + "\n", "\n", - "141fd2090->x6\n", - "\n", - "\n", + "177604080->17359a090\n", + "\n", + "\n", + "1.36\n", + "N\n", "\n", - "\n", + "\n", "\n", - "141fe2370\n", - "\n", - "x0>=0.82?\n", + "109926ea0\n", + "\n", + "29.83\n", "\n", - "\n", + "\n", "\n", - "141fcbe00->141fe2370\n", - "\n", - "\n", - "Y\n", + "10fd43150->109926ea0\n", + "\n", + "\n", + "Y\n", "\n", - "\n", + "\n", "\n", - "141ff19f0\n", - "\n", - "Exp\n", + "x0\n", + "\n", + "x0\n", "\n", - "\n", + "\n", "\n", - "141fcbe00->141ff19f0\n", - "\n", - "\n", - "N\n", + "10fd43150->x0\n", + "\n", + "\n", + "48.74\n", + "N\n", "\n", - "\n", + "\n", "\n", - "141fe2420\n", - "\n", - "15.89\n", + "x1\n", + "\n", + "x1\n", "\n", - "\n", + "\n", "\n", - "141fe2370->141fe2420\n", - "\n", - "\n", - "Y\n", + "17359a090->x1\n", + "\n", + "\n", + "0.01\n", "\n", - "\n", + "\n", "\n", - "x3\n", - "\n", - "x3\n", + "x6\n", + "\n", + "x6\n", "\n", - "\n", + "\n", "\n", - "141fe2370->x3\n", - "\n", - "\n", - "0.18\n", - "N\n", - "\n", - "\n", - "\n", - "141fd89e0\n", - "\n", - "Sin\n", - "\n", - "\n", - "\n", - "141ff19f0->141fd89e0\n", - "\n", - "\n", - "1.79\n", - "\n", - "\n", - "\n", - "x1\n", - "\n", - "x1\n", - "\n", - "\n", - "\n", - "141fd89e0->x1\n", - "\n", - "\n", - "0.82\n", + "17359a090->x6\n", + "\n", + "\n", + "8.16\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, "execution_count": 13, @@ -765,17 +751,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Completed 100% [====================]\n", - "Best model: Logistic\n", - "|- -0.23+Sum\n", - "| |- Sin\n", - "| | |- If(AIDS>=16068.00)\n", - "| | | |- 70.58\n", - "| | | |- If(Total>=1601948.00)\n", - "| | | | |- -1.43\n", - "| | | | |- If(AIDS>=258.00)\n", - "| | | | | |- 70.58\n", - "| | | | | |- Total\n" + "\n", + "RangeIndex: 50 entries, 0 to 49\n", + "Data columns (total 4 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 Age 50 non-null int64 \n", + " 1 Race 50 non-null int64 \n", + " 2 AIDS 50 non-null float64\n", + " 3 Total 50 non-null float64\n", + "dtypes: float64(2), int64(2)\n", + "memory usage: 1.7 KB\n" ] } ], @@ -787,14 +773,40 @@ "df = pd.read_csv('../examples/datasets/d_analcatdata_aids.csv')\n", "X = df.drop(columns='target')\n", "y = df['target']\n", + "\n", + "X.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "0ed09198", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed 100% [====================]\n", + "Best model: Logistic\n", + "|- -0.38+Add\n", + "| |- 0.00*Add\n", + "| | |- Cos\n", + "| | | |- AIDS\n", + "| | |- 0.01*AIDS\n" + ] + } + ], + "source": [ "\n", "est = BrushClassifier(\n", - " functions=['SplitBest', 'And', 'Sin', 'Cos', 'Exp'],\n", - " max_gens=500,\n", + " # Uncomment the line below to constrain the search space to fewer functions\n", + " functions=['SplitBest', 'SplitOn', 'Geq', 'Eq', 'Mul', 'Add', 'Cos', 'Exp'],\n", + " max_gens=100,\n", " max_size=50,\n", - " max_depth=15,\n", + " max_depth=5,\n", " objectives=[\"scorer\", \"linear_complexity\"], \n", - " scorer=\"log\",\n", + " scorer=\"average_precision_score\",\n", " pop_size=100,\n", " bandit='dynamic_thompson',\n", " verbosity=1\n", @@ -815,7 +827,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 24, "id": "05a76db6", "metadata": {}, "outputs": [ @@ -825,42 +837,38 @@ "text": [ "=== Search space ===\n", "terminal_map: {\"ArrayB\": [\"1.00\"], \"ArrayI\": [\"Age\", \"Race\", \"1.00\"], \"ArrayF\": [\"AIDS\", \"Total\", \"1.00\"]}\n", - "terminal_weights: {\"ArrayB\": [0.27714446], \"ArrayI\": [0.5782708, 0.85288507, 0.491237], \"ArrayF\": [0.47180814, 0.6575688, 0.07907883]}\n", - "SplitBest node_map[ArrayI][[\"ArrayI\", \"ArrayI\"]][SplitBest] = 1.00*SplitBest, weight = 0.58001214\n", - "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Sum, weight = 0\n", - "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Logistic] = 1.00*Logistic, weight = 0\n", - "Sin node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Sin] = 1.00*Sin, weight = 0.9063738\n", - "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Cos] = 1.00*Cos, weight = 0.60503954\n", - "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Exp] = 1.00*Exp, weight = 0.620445\n", - "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Sum, weight = 0\n", - "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Logistic] = 1.00*Logistic, weight = 0\n", - "Sin node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Sin] = 1.00*Sin, weight = 0.7408498\n", - "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Cos] = 1.00*Cos, weight = 0.5123497\n", - "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Exp] = 1.00*Exp, weight = 0.59719974\n", - "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Sum, weight = 0\n", - "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Logistic] = 1.00*Logistic, weight = 0\n", - "Sin node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Sin] = 1.00*Sin, weight = 0.58827883\n", - "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Cos] = 1.00*Cos, weight = 0.04645303\n", - "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Exp] = 1.00*Exp, weight = 0.5686126\n", - "SplitBest node_map[ArrayF][[\"ArrayF\", \"ArrayF\"]][SplitBest] = 1.00*SplitBest, weight = 0.35056993\n", - "OffsetSum node_map[ArrayF][[\"ArrayF\"]][OffsetSum] = 0.00+Sum, weight = 0\n", - "Logistic node_map[ArrayF][[\"ArrayF\"]][Logistic] = 1.00*Logistic, weight = 0\n", - "Sin node_map[ArrayF][[\"ArrayF\"]][Sin] = 1.00*Sin, weight = 0.90100724\n", - "Cos node_map[ArrayF][[\"ArrayF\"]][Cos] = 1.00*Cos, weight = 0.17278638\n", - "Exp node_map[ArrayF][[\"ArrayF\"]][Exp] = 1.00*Exp, weight = 0.43059298\n", + "terminal_weights: {\"ArrayB\": [0.3165285], \"ArrayI\": [0.5779346, 0.42670068, 0.10736023], \"ArrayF\": [0.539658, 0.18103841, 0.7904958]}\n", + "SplitBest node_map[ArrayI][[\"ArrayI\", \"ArrayI\"]][SplitBest] = SplitBest, weight = 0.5382333\n", + "Mul node_map[ArrayI][[\"ArrayI\", \"ArrayI\"]][Mul] = Mul, weight = 0.96832097\n", + "Add node_map[ArrayI][[\"ArrayI\", \"ArrayI\"]][Add] = Add, weight = 0.7737981\n", + "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Add, weight = 0\n", + "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Logistic] = Logistic, weight = 0\n", + "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Cos] = Cos, weight = 0.53532857\n", + "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\", \"ArrayF\"]][Exp] = Exp, weight = 0.35211778\n", + "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Add, weight = 0\n", + "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Logistic] = Logistic, weight = 0\n", + "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Cos] = Cos, weight = 0.6508668\n", + "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\", \"ArrayF\"]][Exp] = Exp, weight = 0.6810962\n", + "OffsetSum node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][OffsetSum] = 0.00+Add, weight = 0\n", + "Logistic node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Logistic] = Logistic, weight = 0\n", + "Cos node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Cos] = Cos, weight = 0.117200986\n", + "Exp node_map[MatrixF][[\"ArrayF\", \"ArrayF\"]][Exp] = Exp, weight = 0.14627638\n", + "SplitBest node_map[ArrayF][[\"ArrayF\", \"ArrayF\"]][SplitBest] = SplitBest, weight = 0.27913103\n", + "Mul node_map[ArrayF][[\"ArrayF\", \"ArrayF\"]][Mul] = Mul, weight = 0.385618\n", + "Add node_map[ArrayF][[\"ArrayF\", \"ArrayF\"]][Add] = Add, weight = 0.2764419\n", + "OffsetSum node_map[ArrayF][[\"ArrayF\"]][OffsetSum] = 0.00+Add, weight = 0\n", + "Logistic node_map[ArrayF][[\"ArrayF\"]][Logistic] = Logistic, weight = 0\n", + "Cos node_map[ArrayF][[\"ArrayF\"]][Cos] = Cos, weight = 0.32952592\n", + "Exp node_map[ArrayF][[\"ArrayF\"]][Exp] = Exp, weight = 0.7656854\n", "\n", - "Best model: Logistic(Sum(-0.23,Sin(If(AIDS>=16068.00,70.58,If(Total>=1601948.00,-1.43,If(AIDS>=258.00,70.58,Total))))))\n", - "score: 0.82\n", + "Best model: Logistic(Add(-0.38,0.00*Add(Cos(AIDS),0.01*AIDS)))\n", + "score: 0.68\n", "Best model: Logistic\n", - "|- -0.23+Sum\n", - "| |- Sin\n", - "| | |- If(AIDS>=16068.00)\n", - "| | | |- 70.58\n", - "| | | |- If(Total>=1601948.00)\n", - "| | | | |- -1.43\n", - "| | | | |- If(AIDS>=258.00)\n", - "| | | | | |- 70.58\n", - "| | | | | |- Total\n" + "|- -0.38+Add\n", + "| |- 0.00*Add\n", + "| | |- Cos\n", + "| | | |- AIDS\n", + "| | |- 0.01*AIDS\n" ] }, { @@ -872,152 +880,94 @@ "\n", "\n", - "\n", - "\n", + "\n", + "\n", "G\n", - "\n", - "\n", + "\n", + "^ split feature fixed, * split threshold fixed\n", + "\n", "\n", - "140d6f660\n", - "\n", - "Logistic\n", + "107ceee40\n", + "\n", + "Logistic\n", "\n", - "\n", + "\n", "\n", - "140d9d760\n", - "\n", - "Add\n", + "107ceeef0\n", + "\n", + "Add\n", "\n", - "\n", + "\n", "\n", - "140d6f660->140d9d760\n", - "\n", - "\n", + "107ceee40->107ceeef0\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "140d8e9c0\n", - "\n", - "Sin\n", + "107ceefa0\n", + "\n", + "Add\n", "\n", - "\n", + "\n", "\n", - "140d9d760->140d8e9c0\n", - "\n", - "\n", + "107ceeef0->107ceefa0\n", + "\n", + "\n", + "0.00\n", "\n", - "\n", + "\n", "\n", - "140d9d760Offset\n", - "\n", - "-0.23\n", + "107ceeef0Offset\n", + "\n", + "-0.38\n", "\n", - "\n", + "\n", "\n", - "140d9d760->140d9d760Offset\n", - "\n", - "\n", + "107ceeef0->107ceeef0Offset\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "140d856e0\n", - "\n", - "AIDS>=16068.00?\n", + "107cf9100\n", + "\n", + "Cos\n", "\n", - "\n", + "\n", "\n", - "140d8e9c0->140d856e0\n", - "\n", - "\n", + "107ceefa0->107cf9100\n", + "\n", + "\n", "\n", - "\n", + "\n", "\n", - "140de5660\n", - "\n", - "70.58\n", + "AIDS\n", + "\n", + "AIDS\n", "\n", - "\n", + "\n", "\n", - "140d856e0->140de5660\n", - "\n", - "\n", - "Y\n", - "\n", - "\n", - "\n", - "140d74b80\n", - "\n", - "Total>=1601948.00?\n", + "107ceefa0->AIDS\n", + "\n", + "\n", + "0.01\n", "\n", - "\n", + "\n", "\n", - "140d856e0->140d74b80\n", - "\n", - "\n", - "N\n", - "\n", - "\n", - "\n", - "140d894a0\n", - "\n", - "-1.43\n", - "\n", - "\n", - "\n", - "140d74b80->140d894a0\n", - "\n", - "\n", - "Y\n", - "\n", - "\n", - "\n", - "140db6d30\n", - "\n", - "AIDS>=258.00?\n", - "\n", - "\n", - "\n", - "140d74b80->140db6d30\n", - "\n", - "\n", - "N\n", - "\n", - "\n", - "\n", - "140ddf8d0\n", - "\n", - "70.58\n", - "\n", - "\n", - "\n", - "140db6d30->140ddf8d0\n", - "\n", - "\n", - "Y\n", - "\n", - "\n", - "\n", - "Total\n", - "\n", - "Total\n", - "\n", - "\n", - "\n", - "140db6d30->Total\n", - "\n", - "\n", - "1.00\n", - "N\n", + "107cf9100->AIDS\n", + "\n", + "\n", "\n", "\n", "\n" ], "text/plain": [ - "" + "" ] }, - "execution_count": 15, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } diff --git a/tests/cpp/test_program.cpp b/tests/cpp/test_program.cpp index 50c7f97e..37fa1691 100644 --- a/tests/cpp/test_program.cpp +++ b/tests/cpp/test_program.cpp @@ -82,12 +82,26 @@ TEST(Program, FitRegressor) RegressorProgram PRG = SS.make_regressor(0, 0, params); fmt::print( "=================================================\n" - "Tree model for depth = {}, size= {}: {}\n" + "Tree model for depth = {}, size= {}\n" "=================================================\n", - d, s, PRG.get_model("compact", true) + d, s ); + + // Get model string before fitting + auto model_before = PRG.get_model("compact", true); + fmt::print("Before fit: {}\n", model_before); + + // Fit the program PRG.fit(data); - auto y = PRG.predict(data); + + // Get model string after fitting + auto model_after = PRG.get_model("compact", true); + fmt::print("After fit: {}\n", model_after); + + // Get predictions and score after fitting + auto y_pred = PRG.predict(data); + float mse = ((data.y - y_pred).square()).mean(); + fmt::print("MSE after fit: {:.6f}\n", mse); } } // } @@ -169,15 +183,33 @@ TEST(Program, FitClassifier) fmt::print( "=================================================\n" - "Tree model for depth = {}, size= {}: {}\n" + "Tree model for depth = {}, size= {}\n" "=================================================\n", - d, s, PRG.get_model("compact", true) + d, s ); + // Get model string before fitting + auto model_before = PRG.get_model("compact", true); + fmt::print("Before fit: {}\n", model_before); + fmt::print( "Fitting the model...\n"); PRG.fit(data); - fmt::print( "predict...\n"); - auto y = PRG.predict(data); + + // Get model string after fitting + auto model_after = PRG.get_model("compact", true); + fmt::print("After fit: {}\n", model_after); + + // Calculate accuracy after fitting + auto y_pred = PRG.predict(data); + int correct = 0; + for (int i = 0; i < data.y.size(); ++i) { + if (std::abs(y_pred(i) - data.y(i)) < 0.5) { + correct++; + } + } + float accuracy = static_cast(correct) / data.y.size(); + fmt::print("Accuracy after fit: {:.4f}\n", accuracy); + fmt::print( "predict proba...\n"); auto yproba = PRG.predict_proba(data); } @@ -302,4 +334,67 @@ TEST(Operators, ProgramSizeAndDepthPARAMS) ASSERT_TRUE(PRG.depth() > 0); // depth is always positive } } +} + +TEST(Program, ComparisonAndBooleanOperators) +{ + Parameters params; + + // dataset with float and integer features + MatrixXf X(10,6); + X << 2.5, 1.5, 0.5, 15, 12, 0, + 3.7, -2.3, 1.0, 11, 10, 0, + -1.2, 4.1, -0.5, 17, 11, 0, + -1.0, 2.5, 3.0, 12, 12, 1, + 4.0, -1.0, 0.0, 13, 11, 1, + 1.0, 1.0, 1.0, 15, 15, 0, + -2.0, 0.5, 2.5, 11, 12, 1, + 3.5, 4.0, -3.0, 14, 14, 0, + 0.0, 0.0, 0.0, 10, 10, 1, + 2.0, 2.0, 2.0, 16, 13, 1; + + ArrayXf y(10); + y << 1, 0, 1, 1, 0, 1, 0, 1, 0, 1; + + Dataset dt(X, y); + fmt::print("\n=== Dataset Info ===\n"); + dt.print(); + + // search space with only comparison and boolean operators + params.functions = { + {"Equals", 1.0}, // ArrayXb(ArrayXi, ArrayXi) + {"Geq", 1.0}, // ArrayXb(ArrayXf, ArrayXf) + {"And", 1.0}, // ArrayXb(ArrayXb, ArrayXb) + {"Or", 1.0}, // ArrayXb(ArrayXb, ArrayXb) + {"Not", 1.0}, // ArrayXb(ArrayXb) + {"SplitOn", 1.0} + }; + + SearchSpace SS; + SS.init(dt, params.functions); + + fmt::print("\n=== SS===\n"); + SS.print(); + + params.max_depth = 10; + params.max_size = 50; + + // Generate several programs to see these operators in action + for (int trial = 0; trial < 100; ++trial) { + RegressorProgram PRG = SS.make_regressor(0, 0, params); + + fmt::print("--- Trial {} ---\n", trial); + + // Show model before and after fit + auto model_before = PRG.get_model("compact", true); + fmt::print("Before fit: {}\n", model_before); + + PRG.fit(dt); + + auto model_after = PRG.get_model("compact", true); + fmt::print("After fit: {}\n", model_after); + + ASSERT_GT(PRG.depth(), 0); + ASSERT_GT(PRG.size(), 0); + } } \ No newline at end of file From c473282862384756c8ad826760d67a92fbc89305 Mon Sep 17 00:00:00 2001 From: Guilherme Aldeia Date: Thu, 15 Jan 2026 08:35:18 -0300 Subject: [PATCH 26/30] Default signature for new operators --- src/program/node.cpp | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/program/node.cpp b/src/program/node.cpp index e3774482..d6fe3bcc 100644 --- a/src/program/node.cpp +++ b/src/program/node.cpp @@ -200,7 +200,39 @@ void init_node_with_default_signature(Node& node) >(n)) { node.set_signature>(); - } + } + else if (Is< + NT::Geq + >(n)) + { + node.set_signature>(); + } + else if (Is< + NT::Equals + >(n)) + { + node.set_signature>(); + } + else if (Is< + NT::Before, + NT::After, + NT::During + >(n)) + { + node.set_signature>(); + } + else if (Is< + NT::Count + >(n)) + { + node.set_signature>(); + } + else if (Is< + NT::ArgMax + >(n)) + { + node.set_signature>(); + } else if (Is< NT::Min, NT::Max, From b96ade2e8fb0fb84bbeefd6b1ff5fa0fc1205e06 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Thu, 5 Feb 2026 08:53:22 -0500 Subject: [PATCH 27/30] Improved feature type retrieval when fitting without using a pandas dataframe. Better error message when accessing the dataset directly does not find the feature by its name. New assertions in API interface tests. --- pybrush/BrushEstimator.py | 13 +++++++++ pybrush/EstimatorInterface.py | 2 +- src/bindings/bind_dataset.cpp | 10 +++++-- src/data/data.cpp | 55 +++++++++++++++++++++++++++-------- src/data/data.h | 3 +- src/program/operator.h | 2 +- tests/python/test_deap_api.py | 25 ++++++++++++++++ 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/pybrush/BrushEstimator.py b/pybrush/BrushEstimator.py index 4b9716e5..725cf2d2 100644 --- a/pybrush/BrushEstimator.py +++ b/pybrush/BrushEstimator.py @@ -94,6 +94,13 @@ def fit(self, X, y): feature_types=self.feature_types_, validation_size=self.validation_size, shuffle_split=self.shuffle_split) + + # If it failed to infer datatypes from a dict, we will retrieve it + # from the brush type sniffer, because this helps calling predict later + # (it keeps data type consistent, which is important since brush is strongly typed) + if not isinstance(X, pd.DataFrame): + self.feature_names_ = self.data_.get_feature_names() + self.feature_types_ = self.data_.get_feature_types() # These have a default behavior to return something meaningfull if # no values are set @@ -107,6 +114,7 @@ def fit(self, X, y): self.parameters_.functions, self.parameters_.weights_init) + # Creating a new brush engine self.engine_ = None if self.mode == 'classification': self.engine_ = ( ClassifierEngine @@ -183,6 +191,11 @@ def partial_fit(self, X, y, *, self.engine_.fit(new_data) # self.engine_.lock_nodes(0, False, False) # unlocking everything + # getting a new reference to the search space (it is not serialized, so + # this ensures that loading a model with pickle and calling either fit() + # or partial_fit() will restore the search space reference) + self.search_space_ = self.engine_.search_space + self.archive_ = self.engine_.get_archive() self.population_ = self.engine_.get_population() self.best_estimator_ = self.engine_.best_ind diff --git a/pybrush/EstimatorInterface.py b/pybrush/EstimatorInterface.py index 00af7fc8..c3a9db10 100644 --- a/pybrush/EstimatorInterface.py +++ b/pybrush/EstimatorInterface.py @@ -140,7 +140,7 @@ class EstimatorInterface(): * `"best_validation_ci"`: The less complex solution that is within the 95% confidence interval of the best solution's validation loss, with the confidence interval estimated with the inner validation partition of - the data passed to `fit` or `fit_partial`; + the data passed to `fit` or `partial_fit`; If a custom function is passed, then it should hhave the signature `Callable[[List[Dict], List[Dict]], Dict]]`, which means that it takes diff --git a/src/bindings/bind_dataset.cpp b/src/bindings/bind_dataset.cpp index a71bdd9e..f760a80b 100644 --- a/src/bindings/bind_dataset.cpp +++ b/src/bindings/bind_dataset.cpp @@ -64,8 +64,12 @@ void bind_dataset(py::module & m) py::arg("ref_dataset"), py::arg("feature_names") ) - - .def_readwrite("y", &br::Data::Dataset::y) + + .def("get_feature_types", &br::Data::Dataset::get_feature_types) + .def("get_feature_names", [](const br::Data::Dataset &d) {return d.feature_names; }) // wrapping it into a function to keep consistent with get_feature_types. brush feature types are not native to python, so that's why we need that function to cast it to something python can understand. + + .def_readwrite("y", &br::Data::Dataset::y) // TODO: should this be read only? + // .def_readwrite("features", &br::Data::Dataset::features) .def("get_n_samples", &br::Data::Dataset::get_n_samples) .def("get_n_features", &br::Data::Dataset::get_n_features) @@ -76,7 +80,7 @@ void bind_dataset(py::module & m) .def("get_batch_size", &br::Data::Dataset::get_batch_size) .def("set_batch_size", &br::Data::Dataset::set_batch_size) .def("split", &br::Data::Dataset::split) - .def("get_X", &br::Data::Dataset::get_X) + .def("get_X", &br::Data::Dataset::get_X) ; m.def("read_csv", &br::Data::read_csv, py::arg("path"), py::arg("target"), py::arg("sep")=','); diff --git a/src/data/data.cpp b/src/data/data.cpp index 3ba6a260..c48df338 100644 --- a/src/data/data.cpp +++ b/src/data/data.cpp @@ -197,6 +197,34 @@ array Dataset::split(const ArrayXb& mask) const Dataset Dataset::get_training_data() const { return (*this)(training_data_idx); } Dataset Dataset::get_validation_data() const { return (*this)(validation_data_idx); } +vector Dataset::get_feature_types() const { + // iterate through each feature name, get the data type, and returns it. This is + // used in the python front-end to save the feature types from the training dataset + // when calling predict. + + vector python_feature_types; + for (const auto& [name, value]: this->features) + { + // fmt::print("name:{}\n",name); + // save feature types + auto feature_type = StateType(value); + + if (feature_type == DataType::ArrayB) + python_feature_types.push_back("ArrayB"); + else if (feature_type == DataType::ArrayI) + python_feature_types.push_back("ArrayI"); + else if (feature_type == DataType::ArrayF) + python_feature_types.push_back("ArrayF"); + else + HANDLE_ERROR_THROW( + "get_feature_type does not support the type of this feature yet: " + name + + "as a notice, this function is suposed to be used in the python side, to extract data types inferred by Brush type sniffer."); + } + + return python_feature_types; +} + + /// call init at the end of constructors /// to define metafeatures of the data. void Dataset::init() @@ -225,6 +253,9 @@ void Dataset::init() // add feature to appropriate map list this->features_of_type[feature_type].push_back(name); + + // populate feature names + this->feature_names.push_back(name); } // setting the training and validation data indexes @@ -335,14 +366,14 @@ map Dataset::make_features(const ArrayXXf& X, // fmt::print("vn: {}\n",vn); // check variable names - feature_names.resize(0); + vector tmp_feature_names = {}; if (vn.empty()) { // fmt::print("vn empty\n"); for (int i = 0; i < X.cols(); ++i) { string v = "x_"+to_string(i); - feature_names.push_back(v); + tmp_feature_names.push_back(v); } } else @@ -352,7 +383,7 @@ map Dataset::make_features(const ArrayXXf& X, fmt::format("Variable names and data size mismatch: " "{} variable names and {} features in X", vn.size(), X.cols()) ); - feature_names = vn; + tmp_feature_names = vn; } // check variable types @@ -376,10 +407,10 @@ map Dataset::make_features(const ArrayXXf& X, for (int i = 0; i < X.cols(); ++i) { - // fmt::print("X({}): {} \n",i,feature_names.at(i)); + // fmt::print("X({}): {} \n",i,tmp_feature_names.at(i)); State tmp = check_type(X.col(i).array(), var_types.at(i)); - tmp_features[feature_names.at(i)] = tmp; + tmp_features[tmp_feature_names.at(i)] = tmp; } // fmt::print("tmp_features insert\n"); tmp_features.insert(Z.begin(), Z.end()); @@ -393,13 +424,13 @@ map Dataset::copy_and_make_features(const ArrayXXf& X, const vector& vn ) { - feature_names.resize(0); + vector tmp_feature_names = {}; if (vn.empty()) { for (int i = 0; i < X.cols(); ++i) { string v = "x_"+to_string(i); - feature_names.push_back(v); + tmp_feature_names.push_back(v); } } else @@ -412,15 +443,15 @@ map Dataset::copy_and_make_features(const ArrayXXf& X, X.cols() ) ); - feature_names = vn; + tmp_feature_names = vn; } - if (ref_dataset.features.size() != feature_names.size()) + if (ref_dataset.features.size() != tmp_feature_names.size()) HANDLE_ERROR_THROW( fmt::format("Reference dataset with incompatible number of variables: " "Reference has {} variable names, but X has {}", ref_dataset.features.size(), - feature_names.size() + tmp_feature_names.size() ) ); @@ -429,10 +460,10 @@ map Dataset::copy_and_make_features(const ArrayXXf& X, { State tmp = cast_type( X.col(i).array(), - ref_dataset.features.at(feature_names.at(i)) + ref_dataset.features.at(tmp_feature_names.at(i)) ); - tmp_features[feature_names.at(i)] = tmp; + tmp_features[tmp_feature_names.at(i)] = tmp; } return tmp_features; diff --git a/src/data/data.h b/src/data/data.h index dd4777b0..af13be2a 100644 --- a/src/data/data.h +++ b/src/data/data.h @@ -65,7 +65,7 @@ class Dataset std::vector feature_types; /// @brief names of the feature types as string representations. - std::vector feature_names; // TODO: remove? + std::vector feature_names; /// @brief map from data types to features having that type. std::unordered_map> features_of_type; @@ -218,6 +218,7 @@ class Dataset // if split is not set, then training = validation. Dataset get_training_data() const; Dataset get_validation_data() const; + vector get_feature_types() const; inline int get_n_samples() const { return std::visit( diff --git a/src/program/operator.h b/src/program/operator.h index bda086e1..e899a4d8 100644 --- a/src/program/operator.h +++ b/src/program/operator.h @@ -333,7 +333,7 @@ struct Operator HANDLE_ERROR_THROW(fmt::format("Failed to return type {} for '{}'. The feature's original ret type is {}.\n", DataTypeEnum::value, feature, - DataTypeEnum::value + DataTypeName.at(d.get_feature_type(feature)) )); return T(); diff --git a/tests/python/test_deap_api.py b/tests/python/test_deap_api.py index cca0c77b..aed89ff9 100644 --- a/tests/python/test_deap_api.py +++ b/tests/python/test_deap_api.py @@ -18,6 +18,7 @@ def brush_args(): max_size=50, max_depth=6, cx_prob= 1/7, + # validation_size=0.2, num_islands=1, mutation_probs = {"point":1/6, "insert":1/6, "delete":1/6, "subtree":1/6, "toggle_weight_on":1/6, "toggle_weight_off":1/6}, @@ -92,11 +93,35 @@ def test_fit(setup, algorithm, brush_args, request): Estimator, X, y = request.getfixturevalue(setup) + print(X.info()) + brush_args["algorithm"] = algorithm try: est = Estimator(**brush_args) est.fit(X, y) + # assertion block ------------------------------------------------------ + data_types = est.data_.get_feature_types() + train_types = est.train_.get_feature_types() + val_types = est.validation_.get_feature_types() + assert data_types == train_types == val_types, \ + (f"Feature types not replicated among datasets.\n" + f" data_: {data_types}\n" + f" train_: {train_types}\n" + f" validation_: {val_types}") + + data_names = est.data_.get_feature_names() + train_names = est.train_.get_feature_names() + val_names = est.validation_.get_feature_names() + assert data_names == train_names == val_names, \ + (f"Feature names not replicated among datasets.\n" + f" data_: {data_names}\n" + f" train_: {train_names}\n" + f" validation_: {val_names}") + # assertion block ------------------------------------------------------ + + print('best model:', est.best_estimator_.program.get_model()) + print('score:',est.score(X,y)) except Exception as e: From 8410b1edb70b0fdcb79836e0444c3d83302f35fd Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Mon, 9 Mar 2026 11:38:17 -0400 Subject: [PATCH 28/30] SplitOn defaults to ArrayXf(ArraXb, ArrayXf, ArrayXf) --- src/program/node.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/program/node.cpp b/src/program/node.cpp index d6fe3bcc..b690b476 100644 --- a/src/program/node.cpp +++ b/src/program/node.cpp @@ -242,7 +242,7 @@ void init_node_with_default_signature(Node& node) // NT::OffsetSum, NT::Prod, NT::Softmax, - NT::SplitOn, + // NT::SplitOn, NT::SplitBest >(n)) { @@ -252,6 +252,12 @@ void init_node_with_default_signature(Node& node) } else if (Is(n)) { + // lets make split on always defaults to floats (so it will work + // regardless of datatype). + // This only matters for weakly defined nodes when doing manual + // construction of brush programs as json objects, and this behavior + // can be ignored by avoiding the need of generating the signature + // (that is, defining the node with missing hash values and ret types) node.set_signature>(); } else if (Is(n)) From f1c3109738fb252b74f15a4c1c8a9bfa09f3c568 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Tue, 10 Mar 2026 14:28:06 -0400 Subject: [PATCH 29/30] replace program (for better interface with brush-python-parser) --- src/bindings/bind_individuals.h | 3 +++ src/bindings/bind_programs.h | 3 +++ src/ind/individual.h | 11 +++++++++++ src/program/program.h | 15 +++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/src/bindings/bind_individuals.h b/src/bindings/bind_individuals.h index dee4837a..ba5dd61a 100644 --- a/src/bindings/bind_individuals.h +++ b/src/bindings/bind_individuals.h @@ -47,6 +47,9 @@ void bind_individual(py::module& m, string name) .def("fit", static_cast &X, const Ref &y)>(&Class::fit), "fit from X,y data") + .def("replace_program", &Class::replace_program, + py::arg("new_program"), + "Replace the current program with a new program, invalidating fitness") .def("predict", static_cast(&Class::predict), "predict from Dataset object") diff --git a/src/bindings/bind_programs.h b/src/bindings/bind_programs.h index e22b8676..697f4a3a 100644 --- a/src/bindings/bind_programs.h +++ b/src/bindings/bind_programs.h @@ -30,6 +30,9 @@ void bind_program(py::module& m, string name) .def("fit", static_cast &X, const Ref &y)>(&T::fit), "fit from X,y data") + .def("replace_program", &T::replace_program, + py::arg("new_program"), + "Replace the current program with a new program, invalidating fitness") .def("predict", static_cast(&T::predict), "predict from Dataset object") diff --git a/src/ind/individual.h b/src/ind/individual.h index 41188316..bef95dec 100644 --- a/src/ind/individual.h +++ b/src/ind/individual.h @@ -76,6 +76,17 @@ class Individual{ return fit(d); }; + /** + * @brief Replace the current program with a new program, invalidating fitness + */ + Individual& replace_program(const Program& new_program) + { + program.replace_program(new_program); + this->is_fitted_ = false; + fitness.clearValues(); + return *this; + }; + auto predict(const Dataset& data) { return program.predict(data); }; auto predict(const Ref& X) { diff --git a/src/program/program.h b/src/program/program.h index 744b745b..445ceb67 100644 --- a/src/program/program.h +++ b/src/program/program.h @@ -156,6 +156,21 @@ template struct Program return *this; }; + /** + * @brief Replace the current program with a new program, invalidating fitness. + */ + Program& replace_program(const Program& new_program) + { + this->Tree = new_program.Tree; + this->is_fitted_ = false; + + // Update search space reference if the new program has one + if (new_program.SSref.has_value()) + this->SSref = new_program.SSref; + + return *this; + }; + template R predict_with_weights(const Dataset &d, const W** weights) { From 3621a26f0a9160c37789b8c460c1d6ea0bfb41b2 Mon Sep 17 00:00:00 2001 From: Guilherme Seidyo Imai Aldeia Date: Tue, 10 Mar 2026 20:50:19 -0400 Subject: [PATCH 30/30] Also providing interface to work with json --- src/bindings/bind_individuals.h | 7 ++++++- src/bindings/bind_programs.h | 7 ++++++- src/ind/individual.h | 15 +++++++++++++++ src/program/program.h | 19 ++++++++++++++++++- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/bindings/bind_individuals.h b/src/bindings/bind_individuals.h index ba5dd61a..dc40b3e5 100644 --- a/src/bindings/bind_individuals.h +++ b/src/bindings/bind_individuals.h @@ -47,9 +47,14 @@ void bind_individual(py::module& m, string name) .def("fit", static_cast &X, const Ref &y)>(&Class::fit), "fit from X,y data") - .def("replace_program", &Class::replace_program, + .def("replace_program", + static_cast&)>(&Class::replace_program), py::arg("new_program"), "Replace the current program with a new program, invalidating fitness") + .def("replace_program", + static_cast(&Class::replace_program), + py::arg("json_program"), + "Replace the current program from a JSON representation, invalidating fitness") .def("predict", static_cast(&Class::predict), "predict from Dataset object") diff --git a/src/bindings/bind_programs.h b/src/bindings/bind_programs.h index 697f4a3a..e9464cec 100644 --- a/src/bindings/bind_programs.h +++ b/src/bindings/bind_programs.h @@ -30,9 +30,14 @@ void bind_program(py::module& m, string name) .def("fit", static_cast &X, const Ref &y)>(&T::fit), "fit from X,y data") - .def("replace_program", &T::replace_program, + .def("replace_program", + static_cast(&T::replace_program), py::arg("new_program"), "Replace the current program with a new program, invalidating fitness") + .def("replace_program", + static_cast(&T::replace_program), + py::arg("json_program"), + "Replace the current program from a JSON representation, invalidating fitness") .def("predict", static_cast(&T::predict), "predict from Dataset object") diff --git a/src/ind/individual.h b/src/ind/individual.h index bef95dec..fde9a3b9 100644 --- a/src/ind/individual.h +++ b/src/ind/individual.h @@ -78,6 +78,9 @@ class Individual{ /** * @brief Replace the current program with a new program, invalidating fitness + * + * @param new_program The new program to replace the current one with + * @return reference to this individual */ Individual& replace_program(const Program& new_program) { @@ -87,6 +90,18 @@ class Individual{ return *this; }; + /** + * @brief Replace the current program from a JSON representation, invalidating fitness + * + * @param j JSON object containing the serialized program + * @return reference to this individual + */ + Individual& replace_program(const json& j) + { + Program new_program = j; + return replace_program(new_program); + }; + auto predict(const Dataset& data) { return program.predict(data); }; auto predict(const Ref& X) { diff --git a/src/program/program.h b/src/program/program.h index 445ceb67..1a2c731c 100644 --- a/src/program/program.h +++ b/src/program/program.h @@ -71,6 +71,7 @@ template struct Program /// the underlying tree tree Tree; + /// reference to search space std::optional> SSref; @@ -158,19 +159,35 @@ template struct Program /** * @brief Replace the current program with a new program, invalidating fitness. + * + * @param new_program The new program to replace the current one with + * @return reference to this program */ Program& replace_program(const Program& new_program) { this->Tree = new_program.Tree; this->is_fitted_ = false; - // Update search space reference if the new program has one + // Update search space reference if the new program has one, otherwise keep current if (new_program.SSref.has_value()) this->SSref = new_program.SSref; + // If new_program doesn't have a search space, keep this->SSref as is return *this; }; + /** + * @brief Replace the current program from a JSON representation, invalidating fitness. + * + * @param j JSON object containing the serialized program + * @return reference to this program + */ + Program& replace_program(const json& j) + { + Program new_program = j; + return replace_program(new_program); + }; + template R predict_with_weights(const Dataset &d, const W** weights) {