From 0fb030f8456050e3e206110bc4c811304c03c4d6 Mon Sep 17 00:00:00 2001 From: Guilherme Aldeia Date: Tue, 15 Aug 2023 14:20:19 -0400 Subject: [PATCH 1/4] Mutation `toggle_weight` splitted in two different mutations Because sometimes it works as an insert mutation, and sometimes as a delete mutation. Since we want to use learners to optimize mutation choice, the less ambiguity the better --- src/brush/estimator.py | 4 ++-- src/variation.h | 41 ++++++++++++++++++++++++++---------- tests/cpp/test_data.cpp | 2 +- tests/cpp/test_variation.cpp | 6 +++--- tests/python/test_params.py | 13 ++++++------ 5 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/brush/estimator.py b/src/brush/estimator.py index df9326bd..ab988bbe 100644 --- a/src/brush/estimator.py +++ b/src/brush/estimator.py @@ -40,7 +40,7 @@ class BrushEstimator(BaseEstimator): Maximum number of nodes in a tree. Use 0 for no limit. cx_prob : float, default 0.9 Probability of applying the crossover variation when generating the offspring - mutation_options : dict, default {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight":0.2} + mutation_options : dict, default {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight_on":0.1, "toggle_weight_off":0.1} A dictionary with keys naming the types of mutation and floating point values specifying the fraction of total mutations to do with that method. functions: dict[str,float] or list[str], default {} @@ -96,7 +96,7 @@ def __init__( max_depth=3, max_size=20, cx_prob=0.9, - mutation_options = {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight":0.2}, + mutation_options = {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight_on":0.1, "toggle_weight_off":0.1}, functions: list[str]|dict[str,float] = {}, initialization="grow", random_state=None, diff --git a/src/variation.h b/src/variation.h index 23c13ab7..dcfd8288 100644 --- a/src/variation.h +++ b/src/variation.h @@ -132,15 +132,32 @@ inline bool delete_mutation(tree& Tree, Iter spot, const SearchSpace& SS) return true; }; -/// @brief toggle the node's weight on or off. +/// @brief toggle the node's weight ON. /// @param Tree the program tree /// @param spot an iterator to the node that is being mutated /// @param SS the search space (unused) /// @return boolean indicating the success (true) or fail (false) of the operation -inline bool toggle_weight_mutation(tree& Tree, Iter spot, const SearchSpace& SS) +inline bool toggle_weight_on_mutation(tree& Tree, Iter spot, const SearchSpace& SS) { - spot.node->data.set_is_weighted(!spot.node->data.get_is_weighted()); + if (spot.node->data.get_is_weighted()==true // cant turn on whats already on + || !IsWeighable(spot.node->data.ret_type)) // does not accept weights (e.g. boolean) + return false; // false indicates that mutation failed and should return std::nullopt + spot.node->data.set_is_weighted(true); + return true; +} + +/// @brief toggle the node's weight OFF. +/// @param Tree the program tree +/// @param spot an iterator to the node that is being mutated +/// @param SS the search space (unused) +/// @return boolean indicating the success (true) or fail (false) of the operation +inline bool toggle_weight_off_mutation(tree& Tree, Iter spot, const SearchSpace& SS) +{ + if (spot.node->data.get_is_weighted()==false) + return false; + + spot.node->data.set_is_weighted(false); return true; } @@ -176,8 +193,10 @@ inline bool subtree_mutation(tree& Tree, Iter spot, const SearchSpace& SS) * * - point mutation changes a single node. * - insertion mutation inserts a node as the parent of an existing node, and fills in the other arguments. - * - deletion mutation deletes a node - * - toggle_weight mutation turns a node's weight on or off. + * - deletion mutation deletes a node. + * - subtree mutation inserts a new subtree into the program. + * - toggle_weight_on mutation turns a node's weight ON. + * - toggle_weight_off mutation turns a node's weight OFF. * * Every mutation has a probability (weight) based on global parameters. The * spot where the mutation will take place is sampled based on attribute @@ -247,11 +266,12 @@ std::optional> mutate(const Program& parent, const SearchSpace& SS using MutationFunc = std::function&, Iter, const SearchSpace&)>; std::map mutations{ - {"insert", insert_mutation}, - {"delete", delete_mutation}, - {"point", point_mutation}, - {"subtree", subtree_mutation}, - {"toggle_weight", toggle_weight_mutation} + {"insert", insert_mutation}, + {"delete", delete_mutation}, + {"point", point_mutation}, + {"subtree", subtree_mutation}, + {"toggle_weight_on", toggle_weight_on_mutation}, + {"toggle_weight_off", toggle_weight_off_mutation} }; // Try to find the mutation function based on the choice @@ -270,7 +290,6 @@ std::optional> mutate(const Program& parent, const SearchSpace& SS return child; } else { - return std::nullopt; } }; diff --git a/tests/cpp/test_data.cpp b/tests/cpp/test_data.cpp index bd1b3208..09893c2c 100644 --- a/tests/cpp/test_data.cpp +++ b/tests/cpp/test_data.cpp @@ -30,7 +30,7 @@ TEST(Data, MixedVariableTypes) // We need to set at least the mutation options (and respective // probabilities) in order to call PRG.predict() PARAMS["mutation_options"] = { - {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"toggle_weight", 0.25} + {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"toggle_weight_on", 0.125}, {"toggle_weight_off", 0.125} }; MatrixXf X(5,3); diff --git a/tests/cpp/test_variation.cpp b/tests/cpp/test_variation.cpp index d7058714..d0eb9bcf 100644 --- a/tests/cpp/test_variation.cpp +++ b/tests/cpp/test_variation.cpp @@ -10,7 +10,7 @@ TEST(Operators, InsertMutationWorks) // To understand design implementation of this test, check Mutation test PARAMS["mutation_options"] = { - {"point", 0.0}, {"insert", 1.0}, {"delete", 0.0}, {"subtree", 0.0}, {"toggle_weight", 0.0} + {"point", 0.0}, {"insert", 1.0}, {"delete", 0.0}, {"subtree", 0.0}, {"toggle_weight_on", 0.0}, {"toggle_weight_off", 0.0} }; // retrieving the options to check if everything was set right @@ -117,7 +117,7 @@ TEST(Operators, Mutation) // TODO: set random seed PARAMS["mutation_options"] = { - {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"subtree", 0.0}, {"toggle_weight", 0.25} + {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"subtree", 0.0}, {"toggle_weight_on", 0.125}, {"toggle_weight_off", 0.125} }; MatrixXf X(10,2); @@ -193,7 +193,7 @@ TEST(Operators, Mutation) TEST(Operators, MutationSizeAndDepthLimit) { PARAMS["mutation_options"] = { - {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"subtree", 0.0}, {"toggle_weight", 0.25} + {"point",0.25}, {"insert", 0.25}, {"delete", 0.25}, {"subtree", 0.0}, {"toggle_weight_on", 0.125}, {"toggle_weight_off", 0.125} }; MatrixXf X(10,2); diff --git a/tests/python/test_params.py b/tests/python/test_params.py index 069a8987..03d08bc4 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -57,15 +57,16 @@ def _change_and_wait(config): 'max_gen' : 100, 'max_depth': 5, 'max_size' : 50, - 'mutation_options': {'point' : 0.0, - 'insert' : 0.0, - 'delete' : 0.0, - 'subtree' : 0.0, - 'toggle_weight': 0.0} + 'mutation_options': {'point' : 0.0, + 'insert' : 0.0, + 'delete' : 0.0, + 'subtree' : 0.0, + 'toggle_weight_on' : 0.0, + 'toggle_weight_off': 0.0} } # We need to guarantee order to use the index correctly - mutations = ['point', 'insert', 'delete', 'subtree', 'toggle_weight'] + mutations = ['point', 'insert', 'delete', 'subtree', 'toggle_weight_on', 'toggle_weight_off'] for i, m in enumerate(mutations): params['mutation_options'][m] = 0 if i != index else 1.0 From 7ea04d3459736d1c7261b5bbcc22192e1bf67d66 Mon Sep 17 00:00:00 2001 From: Guilherme Aldeia Date: Tue, 15 Aug 2023 14:32:18 -0400 Subject: [PATCH 2/4] NSGA uses either cx or mutation (but never both) --- src/brush/deap_api/nsga2.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/brush/deap_api/nsga2.py b/src/brush/deap_api/nsga2.py index a7d44f4d..a1a8c8b2 100644 --- a/src/brush/deap_api/nsga2.py +++ b/src/brush/deap_api/nsga2.py @@ -6,7 +6,7 @@ def nsga2(toolbox, NGEN, MU, CXPB, use_batch, verbosity, rnd_flt): # NGEN = 250 - # MU = 100 + # MU = 100 # CXPB = 0.9 # rnd_flt: random number generator to sample crossover prob @@ -18,15 +18,18 @@ def calculate_statistics(ind): stats = tools.Statistics(calculate_statistics) - stats.register("ave", np.mean, axis=0) + stats.register("avg", np.mean, axis=0) + stats.register("med", np.median, axis=0) stats.register("std", np.std, axis=0) stats.register("min", np.min, axis=0) stats.register("max", np.max, axis=0) logbook = tools.Logbook() - logbook.header = "gen", "evals", "ave (O1 train, O2 train, O1 val, O2 val)", \ + logbook.header = "gen", "evals", "avg (O1 train, O2 train, O1 val, O2 val)", \ + "med (O1 train, O2 train, O1 val, O2 val)", \ "std (O1 train, O2 train, O1 val, O2 val)", \ - "min (O1 train, O2 train, O1 val, O2 val)" + "min (O1 train, O2 train, O1 val, O2 val)", \ + "max (O1 train, O2 train, O1 val, O2 val)" pop = toolbox.population(n=MU) @@ -68,14 +71,12 @@ def calculate_statistics(ind): if rnd_flt() < CXPB: off1, off2 = toolbox.mate(ind1, ind2) else: - off1, off2 = ind1, ind2 - + off1 = toolbox.mutate(off1) + off2 = toolbox.mutate(off2) + # avoid inserting empty solutions - if off1 != None: off1 = toolbox.mutate(off1) - if off1 != None: offspring.extend([off1]) - - if off2 != None: off2 = toolbox.mutate(off2) - if off2 != None: offspring.extend([off2]) + if off1 is not None: offspring.extend([off1]) + if off2 is not None: offspring.extend([off2]) # archive.update(offspring) # Evaluate the individuals with an invalid fitness From efe419eb968d7f25e487f1d22078ef36f2f042e5 Mon Sep 17 00:00:00 2001 From: Guilherme Aldeia Date: Tue, 15 Aug 2023 14:41:59 -0400 Subject: [PATCH 3/4] Fixed incorrect variable being used in mutation --- src/brush/deap_api/nsga2.py | 5 +++-- src/brush/estimator.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/brush/deap_api/nsga2.py b/src/brush/deap_api/nsga2.py index a1a8c8b2..b569ad47 100644 --- a/src/brush/deap_api/nsga2.py +++ b/src/brush/deap_api/nsga2.py @@ -68,11 +68,12 @@ def calculate_statistics(ind): offspring = [] for ind1, ind2 in zip(parents[::2], parents[1::2]): + off1, off2 = None, None if rnd_flt() < CXPB: off1, off2 = toolbox.mate(ind1, ind2) else: - off1 = toolbox.mutate(off1) - off2 = toolbox.mutate(off2) + off1 = toolbox.mutate(ind1) + off2 = toolbox.mutate(ind2) # avoid inserting empty solutions if off1 is not None: offspring.extend([off1]) diff --git a/src/brush/estimator.py b/src/brush/estimator.py index ab988bbe..2039577d 100644 --- a/src/brush/estimator.py +++ b/src/brush/estimator.py @@ -189,7 +189,7 @@ def fit(self, X, y): """ _brush.set_params(self.get_params()) - if self.random_state != None: + if self.random_state is not None: _brush.set_random_state(self.random_state) self.data_ = self._make_data(X,y, validation_size=self.validation_size) From 8f930bf807d99bbba05d1bff105b18f2ffea0c56 Mon Sep 17 00:00:00 2001 From: Guilherme Aldeia Date: Tue, 15 Aug 2023 17:11:35 -0400 Subject: [PATCH 4/4] Uniform weight initialization between mutation options and cx --- src/brush/estimator.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/brush/estimator.py b/src/brush/estimator.py index 2039577d..fd4913af 100644 --- a/src/brush/estimator.py +++ b/src/brush/estimator.py @@ -38,11 +38,20 @@ class BrushEstimator(BaseEstimator): Maximum depth of GP trees in the GP program. Use 0 for no limit. max_size : int, default 0 Maximum number of nodes in a tree. Use 0 for no limit. - cx_prob : float, default 0.9 - Probability of applying the crossover variation when generating the offspring - mutation_options : dict, default {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight_on":0.1, "toggle_weight_off":0.1} + cx_prob : float, default 1/7 + Probability of applying the crossover variation when generating the offspring, + must be between 0 and 1. + Given that there are `n` mutations, and either crossover or mutation is + used to generate each individual in the offspring (but not both at the + same time), we want to have by default an uniform probability between + crossover and every possible mutation. By setting `cx_prob=1/(n+1)`, and + `1/n` for each mutation, we can achieve an uniform distribution. + mutation_options : dict, default {"point":1/6, "insert":1/6, "delete":1/6, "subtree":1/6, "toggle_weight_on":1/6, "toggle_weight_off":1/6} A dictionary with keys naming the types of mutation and floating point - values specifying the fraction of total mutations to do with that method. + values specifying the fraction of total mutations to do with that method. + The probability of having a mutation is `(1-cx_prob)` and, in case the mutation + is applied, then each mutation option is sampled based on the probabilities + defined in `mutation_options`. The set of probabilities should add up to 1.0. functions: dict[str,float] or list[str], default {} A dictionary with keys naming the function set and values giving the probability of sampling them, or a list of functions which will be weighted uniformly. @@ -95,8 +104,9 @@ def __init__( verbosity=0, max_depth=3, max_size=20, - cx_prob=0.9, - mutation_options = {"point":0.2, "insert":0.2, "delete":0.2, "subtree":0.2, "toggle_weight_on":0.1, "toggle_weight_off":0.1}, + cx_prob= 1/7, + mutation_options = {"point":1/6, "insert":1/6, "delete":1/6, "subtree":1/6, + "toggle_weight_on":1/6, "toggle_weight_off":1/6}, functions: list[str]|dict[str,float] = {}, initialization="grow", random_state=None,