From 2af2ce5f77142425e10dcda41d24e9775649dfed Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Sun, 8 Sep 2024 15:00:03 +0200 Subject: [PATCH 01/41] Fix json (de-)serialization by adding matching type to all rules --- suprb/json.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/suprb/json.py b/suprb/json.py index 19f26b1e..12c909eb 100644 --- a/suprb/json.py +++ b/suprb/json.py @@ -80,7 +80,11 @@ def _save_config(suprb): elif isinstance(value, primitive): json_config["config"][key] = value else: - json_config["config"][key] = _get_full_class_name(value) + cname = _get_full_class_name(value) + if cname == "NoneType": + json_config["config"][key] = None + else: + json_config["config"][key] = cname return json_config @@ -109,6 +113,7 @@ def _convert_rule_to_json(rule): return {"error_": rule.error_, "experience_": rule.experience_, "match": _convert_dict_to_json(vars(rule.match)), + "matching_type": _get_full_class_name(rule.match), "is_fitted_": rule.is_fitted_, "model": {"coef_": _convert_to_json_format(getattr(rule.model, "coef_")), "intercept_": getattr(rule.model, "intercept_")}} @@ -190,7 +195,7 @@ def _load_pool(json_dict, suprb): def _convert_json_to_rule(json_rule, json_dict): - rule = Rule(_convert_matching_type(json_rule["match"], json_dict["config"]["matching_type"]), + rule = Rule(_convert_matching_type(json_rule["match"], json_rule["matching_type"]), _convert_from_json_to_array(json_dict["input_space"]), _convert_model(json_rule["model"], json_dict["config"]["rule_generation__init__model"]), _get_class(json_dict["config"]["rule_generation__init__fitness"])) From f8f3115bed90cb9febd571356fd369182f83cc7e Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 5 Dec 2024 20:40:49 +0100 Subject: [PATCH 02/41] added BaseSupervised --- suprb/base.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++++-- suprb/suprb.py | 4 +-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/suprb/base.py b/suprb/base.py index 92118214..d64b82ac 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -3,7 +3,7 @@ from abc import abstractmethod, ABCMeta import numpy as np -from sklearn.base import BaseEstimator, RegressorMixin +from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin class BaseComponent(BaseEstimator, metaclass=ABCMeta): @@ -47,7 +47,48 @@ def _more_str_attributes(self) -> dict: return {} -class BaseRegressor(BaseEstimator, RegressorMixin, metaclass=ABCMeta): +class BaseSupervised(BaseEstimator, metaclass=ABCMeta): + """A base (composite) Estimator for supervised learning.""" + + is_fitted_: bool + + @abstractmethod + def fit(self, X: np.ndarray, y: np.ndarray) -> BaseSupervised: + """ A reference implementation of a fitting function. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + The training input samples. + y : array-like, shape (n_samples,) or (n_samples, n_outputs) + The target values. + + Returns + ------- + self : BaseEstimator + Returns self. + """ + + pass + + @abstractmethod + def predict(self, X: np.ndarray): + """ A reference implementation of a predicting function. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + The training input samples. + + Returns + ------- + y : np.ndarray + Returns the estimation with shape (n_samples,). + """ + + pass + +class BaseRegressor(BaseEstimator, BaseSupervised, RegressorMixin, metaclass=ABCMeta): """A base (composite) Regressor.""" is_fitted_: bool @@ -87,3 +128,45 @@ def predict(self, X: np.ndarray): """ pass + + +class BaseClassifier(BaseEstimator, BaseSupervised, ClassifierMixin, metaclass=ABCMeta): + """A base (composite) Classifier.""" + + is_fitted_: bool + + @abstractmethod + def fit(self, X: np.ndarray, y: np.ndarray) -> BaseClassifier: + """ A reference implementation of a fitting function. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + The training input samples. + y : array-like, shape (n_samples,) or (n_samples, n_outputs) + The target values. + + Returns + ------- + self : BaseEstimator + Returns self. + """ + + pass + + @abstractmethod + def predict(self, X: np.ndarray): + """ A reference implementation of a predicting function. + + Parameters + ---------- + X : {array-like, sparse matrix}, shape (n_samples, n_features) + The training input samples. + + Returns + ------- + y : np.ndarray + Returns the estimation with shape (n_samples,). + """ + + pass diff --git a/suprb/suprb.py b/suprb/suprb.py index ca4ebb7e..2503a052 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -6,7 +6,7 @@ from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array -from .base import BaseRegressor +from .base import BaseSupervised from .exceptions import PopulationEmptyWarning from .solution import Solution from .logging import BaseLogger @@ -21,7 +21,7 @@ from .solution.fitness import PseudoBIC -class SupRB(BaseRegressor): +class SupRB(BaseSupervised): """ The multi-solution batch learning LCS developed by the Organic Computing group at Universität Augsburg. Parameters From f576237806def845b74f537bcc899a65d77ce45c Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 12 Dec 2024 11:51:55 +0100 Subject: [PATCH 03/41] allow ClassifierMixin as param model --- suprb/base.py | 4 ++-- suprb/rule/base.py | 11 ++++++++--- suprb/rule/initialization.py | 11 +++++++---- suprb/suprb.py | 10 +++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/suprb/base.py b/suprb/base.py index d64b82ac..48fe8007 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -88,7 +88,7 @@ def predict(self, X: np.ndarray): pass -class BaseRegressor(BaseEstimator, BaseSupervised, RegressorMixin, metaclass=ABCMeta): +class BaseRegressor(BaseSupervised, RegressorMixin, metaclass=ABCMeta): """A base (composite) Regressor.""" is_fitted_: bool @@ -130,7 +130,7 @@ def predict(self, X: np.ndarray): pass -class BaseClassifier(BaseEstimator, BaseSupervised, ClassifierMixin, metaclass=ABCMeta): +class BaseClassifier(BaseSupervised, ClassifierMixin, metaclass=ABCMeta): """A base (composite) Classifier.""" is_fitted_: bool diff --git a/suprb/rule/base.py b/suprb/rule/base.py index 252df0d1..acc70db9 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -4,7 +4,7 @@ from typing import Union import numpy as np -from sklearn.base import RegressorMixin, clone +from sklearn.base import RegressorMixin, ClassifierMixin, clone from sklearn.metrics import mean_squared_error from suprb.base import SolutionBase @@ -38,11 +38,15 @@ class Rule(SolutionBase): match_set_: np.ndarray pred_: Union[np.ndarray, None] # only the prediction of matching points, so of x[match_] - def __init__(self, match: MatchingFunction, input_space: np.ndarray, model: RegressorMixin, fitness: RuleFitness): + def __init__(self, match: MatchingFunction, input_space: np.ndarray, model: RegressorMixin | ClassifierMixin, fitness: RuleFitness): self.match = match self.input_space = input_space self.model = model self.fitness = fitness + if isinstance(model, ClassifierMixin): + self.task = 'Classification' + else: + self.task = 'Regression' def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: @@ -74,7 +78,8 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: self.model.fit(X, y) self.pred_ = self.model.predict(X) - self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? + if self.task == 'Regression': + self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) diff --git a/suprb/rule/initialization.py b/suprb/rule/initialization.py index a070053a..38e5571e 100644 --- a/suprb/rule/initialization.py +++ b/suprb/rule/initialization.py @@ -2,11 +2,11 @@ from typing import Union import numpy as np from scipy.stats import halfnorm -from sklearn.base import RegressorMixin, clone +from sklearn.base import RegressorMixin, ClassifierMixin, clone from sklearn.linear_model import LinearRegression from sklearn.linear_model import Ridge -from suprb.base import BaseComponent +from suprb.base import BaseComponent, BaseSupervised from suprb.rule.matching import MatchingFunction, OrderedBound, UnorderedBound, CenterSpread, MinPercentage from suprb.utils import check_random_state, RandomState from . import Rule, RuleFitness @@ -25,16 +25,19 @@ class RuleInit(BaseComponent, metaclass=ABCMeta): """ def __init__(self, bounds: np.ndarray = None, - model: RegressorMixin = None, + model: BaseComponent = None, fitness: RuleFitness = None, matching_type: MatchingFunction = None): self.bounds = bounds self.model = model self.fitness = fitness self.matching_type = matching_type - self._validate_components(model=Ridge(alpha=0.01), fitness=VolumeWu()) + if isinstance(model, ClassifierMixin): + self.task = 'Classification' + else: + self.task = 'Regression' @property def matching_type(self): diff --git a/suprb/suprb.py b/suprb/suprb.py index 2503a052..baee262d 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -6,7 +6,7 @@ from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array -from .base import BaseSupervised +from .base import BaseSupervised, BaseRegressor, BaseClassifier from .exceptions import PopulationEmptyWarning from .solution import Solution from .logging import BaseLogger @@ -146,8 +146,12 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): self.elitist_.complexity_ = 99999 # Check that x and y have correct shape - X, y = check_X_y(X, y, dtype='float64', y_numeric=True) - y = check_array(y, ensure_2d=False, dtype='float64') + if isinstance(self, BaseRegressor): + X, y = check_X_y(X, y, dtype='float64', y_numeric=True) + y = check_array(y, ensure_2d=False, dtype='float64') + else: + X, y = check_X_y(X, y, dtype=None, y_numeric=True) + y = check_array(y, ensure_2d=False, dtype=None) # Init sklearn interface self.n_features_in_ = X.shape[1] From 8a6847e4655c8a2c523b69f73706451332ecd005 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 12 Dec 2024 16:08:55 +0100 Subject: [PATCH 04/41] using negative accuracy as score for Classifiers --- suprb/base.py | 4 ++-- suprb/json.py | 2 +- suprb/logging/stdout.py | 2 +- suprb/optimizer/rule/acceptance.py | 4 ++-- suprb/optimizer/rule/selection.py | 4 ++-- suprb/optimizer/solution/saga3/solution_extension.py | 2 +- suprb/optimizer/solution/sas/solution_extension.py | 2 +- suprb/rule/base.py | 8 +++++--- suprb/rule/fitness.py | 6 ++++-- suprb/solution/base.py | 2 +- suprb/solution/fitness.py | 4 ++-- suprb/solution/mixing_model.py | 4 ++-- suprb/suprb.py | 2 +- tests/test_mixing_model.py | 2 +- 14 files changed, 26 insertions(+), 22 deletions(-) diff --git a/suprb/base.py b/suprb/base.py index 48fe8007..ba7c871c 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -28,7 +28,7 @@ class SolutionBase(metaclass=ABCMeta): """An individual solution to some problem.""" is_fitted_: bool - error_: float + score_: float fitness_: float @abstractmethod @@ -38,7 +38,7 @@ def clone(self, **kwargs) -> SolutionBase: def __str__(self): if hasattr(self, 'is_fitted_') and self.is_fitted_: - attributes = {'error': self.error_, 'fitness': self.fitness_} | self._more_str_attributes() + attributes = {'score': self.score_, 'fitness': self.fitness_} | self._more_str_attributes() concat = ",".join([f"{key}={value}" for key, value in attributes.items()]) return f"{self.__class__.__name__}({concat})" diff --git a/suprb/json.py b/suprb/json.py index a8b94fe0..da9845e3 100644 --- a/suprb/json.py +++ b/suprb/json.py @@ -204,7 +204,7 @@ def _convert_json_to_rule(json_rule, json_dict): _convert_model(json_rule["model"], json_dict["config"]["rule_generation__init__model"]), _get_class(json_dict["config"]["rule_generation__init__fitness"])) - rule.error_ = json_rule["error_"] + rule.score_ = json_rule["score_"] rule.experience_ = json_rule["experience_"] rule.is_fitted_ = json_rule["is_fitted_"] diff --git a/suprb/logging/stdout.py b/suprb/logging/stdout.py index 90fccbf8..55082881 100644 --- a/suprb/logging/stdout.py +++ b/suprb/logging/stdout.py @@ -24,7 +24,7 @@ def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: SupRB, iteratio elitist = estimator.solution_composition_.elitist() message = str(dict( - error=elitist.error_, + error=elitist.score_, fitness=elitist.fitness_, complexity=elitist.complexity_, score=elitist.score(X, y), diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index 86e078fc..d5a59a9b 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -21,7 +21,7 @@ def __init__(self, max_error: float = 0.01): self.max_error = max_error def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: - return rule.error_ <= self.max_error + return rule.score_ <= self.max_error class Variance(RuleAcceptance): @@ -38,4 +38,4 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: return False local_y = y[rule.match_set_] default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) - return rule.error_ <= default_error + return rule.score_ <= default_error diff --git a/suprb/optimizer/rule/selection.py b/suprb/optimizer/rule/selection.py index 8aee168c..b8a83191 100644 --- a/suprb/optimizer/rule/selection.py +++ b/suprb/optimizer/rule/selection.py @@ -50,12 +50,12 @@ def __call__(self, rules: list[Rule], random_state: RandomState, size: int = 1) to_be_added = False for can in candidates: - if can.error_ < rule.error_ and can.volume_ > rule.volume_: + if can.score_ < rule.score_ and can.volume_ > rule.volume_: # classifier is dominated by this candidate and should not # become a new candidate to_be_added = False break - elif can.error_ > rule.error_ and can.volume_ < rule.volume_: + elif can.score_ > rule.score_ and can.volume_ < rule.volume_: # classifier dominates candidate candidates.remove(can) to_be_added = True diff --git a/suprb/optimizer/solution/saga3/solution_extension.py b/suprb/optimizer/solution/saga3/solution_extension.py index 01738120..6b3c3d9e 100644 --- a/suprb/optimizer/solution/saga3/solution_extension.py +++ b/suprb/optimizer/solution/saga3/solution_extension.py @@ -90,7 +90,7 @@ def __init__(self, genome: np.ndarray, def fit(self, X: np.ndarray, y: np.ndarray) -> SagaSolution: pred = self.predict(X, cache=True) - self.error_ = max(mean_squared_error(y, pred), 1e-4) + self.score_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) diff --git a/suprb/optimizer/solution/sas/solution_extension.py b/suprb/optimizer/solution/sas/solution_extension.py index 7ae85c09..56dfd94b 100644 --- a/suprb/optimizer/solution/sas/solution_extension.py +++ b/suprb/optimizer/solution/sas/solution_extension.py @@ -39,7 +39,7 @@ def __init__(self, genome: np.ndarray, def fit(self, X: np.ndarray, y: np.ndarray) -> SasSolution: pred = self.predict(X, cache=True) - self.error_ = max(mean_squared_error(y, pred), 1e-4) + self.score_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) diff --git a/suprb/rule/base.py b/suprb/rule/base.py index acc70db9..eeeb8536 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -5,7 +5,7 @@ import numpy as np from sklearn.base import RegressorMixin, ClassifierMixin, clone -from sklearn.metrics import mean_squared_error +from sklearn.metrics import mean_squared_error, accuracy_score from suprb.base import SolutionBase from suprb.fitness import BaseFitness @@ -56,7 +56,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: # No reason to fit if no data point matches if not np.any(match_set): self.is_fitted_ = False - self.error_ = np.inf + self.score_ = np.inf self.fitness_ = -np.inf self.experience_ = 0 self.pred_ = np.array([]) @@ -79,7 +79,9 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: self.pred_ = self.model.predict(X) if self.task == 'Regression': - self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? + self.score_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? + elif self.task == 'Classification': + self.score_ = -max(accuracy_score(y, self.pred_), 1e-4) self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index 59a2e724..1ceb17e7 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -16,7 +16,9 @@ class PseudoAccuracy(RuleFitness): def __call__(self, rule: Rule) -> float: # note that we multiply solely for readability reasons without # any performance impact - return pseudo_accuracy(rule.error_) * 100 + if rule.task == "Classification": + return rule.score_ + return pseudo_accuracy(rule.score_) * 100 class VolumeRuleFitness(RuleFitness, metaclass=ABCMeta): @@ -32,7 +34,7 @@ def __call__(self, rule: Rule) -> float: input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume - return self.fitness_func_(alpha=self.alpha, x1=pseudo_accuracy(rule.error_, beta=2), x2=volume_share) * 100 + return self.fitness_func_(alpha=self.alpha, x1=pseudo_accuracy(rule.score_, beta=2), x2=volume_share) * 100 class VolumeEmary(VolumeRuleFitness): diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 2078a43f..fdccb280 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -47,7 +47,7 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) - self.error_ = max(mean_squared_error(y, pred), 1e-4) + self.score_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index 5fd4fbc9..f49354c8 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -12,7 +12,7 @@ class PseudoBIC(SolutionFitness): def __call__(self, solution: Solution) -> float: # note that error is capped to 1e-4 in suprb.solution.Solution.fit - return -(solution.input_size_ * np.log(solution.error_) + return -(solution.input_size_ * np.log(solution.score_) + solution.complexity_ * np.log(solution.input_size_)) @@ -29,7 +29,7 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - return self.fitness_func_(self.alpha, pseudo_accuracy(solution.error_), + return self.fitness_func_(self.alpha, pseudo_accuracy(solution.score_), c_norm(solution.complexity_, self.max_genome_length_)) * 100 diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index 1c404632..552a9f5a 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -123,9 +123,9 @@ def _get_local_pred(self, X: np.ndarray, subpopulation: list[Rule], cache: bool) def _get_taus(self, subpopulation: list[Rule], dim: int): # Get errors and experience of all rules in subpopulation experiences = self.experience_calculation(subpopulation, dim) - errors = np.array([rule.error_ for rule in subpopulation]) + scores = np.array([rule.score_ for rule in subpopulation]) - return (1 / errors) * (experiences * self.experience_weight) + return (1 / scores) * (experiences * self.experience_weight) def _get_tau_sum(self, subpopulation: list[Rule], matches: list[Rule], taus: list[int]): # Sum all taus diff --git a/suprb/suprb.py b/suprb/suprb.py index baee262d..d2216c4c 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -142,7 +142,7 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): self.is_error_ = False self.elitist_ = Solution([0, 0, 0], [0, 0, 0], ErrorExperienceHeuristic(), PseudoBIC()) self.elitist_.fitness_ = 0 - self.elitist_.error_ = 99999 + self.elitist_.score_ = 99999 self.elitist_.complexity_ = 99999 # Check that x and y have correct shape diff --git a/tests/test_mixing_model.py b/tests/test_mixing_model.py index 5a897cd7..c9957b31 100644 --- a/tests/test_mixing_model.py +++ b/tests/test_mixing_model.py @@ -20,7 +20,7 @@ def create_rule(self, fitness, experience, error): rule.fitness_ = fitness rule.experience_ = experience - rule.error_ = error + rule.score_ = error return rule From 956a23aa9cf66315ac1a3b4a01bf797ab6edc4e0 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 16 Dec 2024 17:15:08 +0100 Subject: [PATCH 05/41] special case for single class in match set --- suprb/rule/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/suprb/rule/base.py b/suprb/rule/base.py index eeeb8536..6d555820 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -74,6 +74,16 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: # Get all data points which match the bounds. X, y = X[self.match_set_], y[self.match_set_] + # No reason to fit if only one class match_set + if self.task == 'Classification': + if len(np.unique(y)) == 1: + self.pred_ = y[0] + self.score_ = 1 - 1e-4 + self.fitness_ = self.fitness(self) + self.experience_ = float(X.shape[0]) + self.is_fitted_ = True + return self + # Create and fit the model self.model.fit(X, y) From 76b9ae7244c389199b3f78c85209e29f4c2b3453 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 16 Dec 2024 17:16:05 +0100 Subject: [PATCH 06/41] solution now supports accuracy_score --- suprb/base.py | 8 +++++++- suprb/solution/base.py | 22 ++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/suprb/base.py b/suprb/base.py index ba7c871c..e4366259 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from abc import abstractmethod, ABCMeta +from abc import abstractmethod, ABCMeta, ABC import numpy as np from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin @@ -170,3 +170,9 @@ def predict(self, X: np.ndarray): """ pass + +class SupervisedMixin(ABC): + pass + +SupervisedMixin.register(RegressorMixin) +SupervisedMixin.register(ClassifierMixin) \ No newline at end of file diff --git a/suprb/solution/base.py b/suprb/solution/base.py index fdccb280..153bd662 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -4,11 +4,11 @@ from abc import ABCMeta, abstractmethod import numpy as np -from sklearn.base import RegressorMixin -from sklearn.metrics import mean_squared_error +from sklearn.base import RegressorMixin, ClassifierMixin +from sklearn.metrics import mean_squared_error, accuracy_score from suprb.rule import Rule -from suprb.base import BaseComponent, SolutionBase +from suprb.base import BaseComponent, SolutionBase, SupervisedMixin from suprb.fitness import BaseFitness @@ -33,7 +33,7 @@ def __call__(self, solution: Solution) -> float: pass -class Solution(SolutionBase, RegressorMixin): +class Solution(SolutionBase, SupervisedMixin): """Solution that mixes a subpopulation of rules.""" input_size_: int @@ -47,7 +47,17 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) - self.score_ = max(mean_squared_error(y, pred), 1e-4) + + if isinstance(self.pool[0].model, ClassifierMixin): + self.task = 'Classification' + else: + self.task = 'Regression' + + if self.task == "Regression": + self.score_ = max(mean_squared_error(y, pred), 1e-4) + elif self.task == "Classification": + self.score_ = max(accuracy_score(y, pred), 1e-4) + self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) @@ -81,7 +91,7 @@ def clone(self, **kwargs) -> Solution: ) solution = Solution(**(args | kwargs)) if not kwargs: - attributes = ['fitness_', 'error_', 'complexity_', 'is_fitted_', 'input_size_'] + attributes = ['fitness_', 'score_', 'complexity_', 'is_fitted_', 'input_size_'] solution.__dict__ |= {key: getattr(self, key) for key in attributes} return solution From 02092644add489dead9a1a1925130a3e15a423e9 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 16 Dec 2024 21:12:43 +0100 Subject: [PATCH 07/41] fix: negative accuracy_score in solution for proper optimization --- suprb/solution/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 153bd662..40cfb1c9 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -56,7 +56,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: if self.task == "Regression": self.score_ = max(mean_squared_error(y, pred), 1e-4) elif self.task == "Classification": - self.score_ = max(accuracy_score(y, pred), 1e-4) + self.score_ = -max(accuracy_score(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster From 1548e9bf54dbacc6ac0383c3f55994122f289677 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 16 Dec 2024 21:14:31 +0100 Subject: [PATCH 08/41] fix: force pred to int on ErrorExperienceHeuristic --- suprb/solution/mixing_model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index 552a9f5a..f4b8e8cf 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -1,5 +1,7 @@ import numpy as np +from sklearn.base import RegressorMixin, ClassifierMixin + from suprb.rule import Rule from suprb.utils import check_random_state, RandomState from . import MixingModel @@ -100,6 +102,8 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. # Normalize tau_sum = self._get_tau_sum(subpopulation, matches, taus) out = pred / tau_sum + if isinstance(subpopulation[0].model, ClassifierMixin): + out = [round(x) for x in out] return out def _get_local_pred(self, X: np.ndarray, subpopulation: list[Rule], cache: bool): From 17eda3c65e56e846d3c20bd087acc50781acfeb1 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 17 Dec 2024 12:36:01 +0100 Subject: [PATCH 09/41] pseudoaccuracy for class now as expected --- suprb/rule/fitness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index 1ceb17e7..51caf7de 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -17,7 +17,7 @@ def __call__(self, rule: Rule) -> float: # note that we multiply solely for readability reasons without # any performance impact if rule.task == "Classification": - return rule.score_ + return -rule.score_ * 100 return pseudo_accuracy(rule.score_) * 100 From 0354ab6c61b61ed840b1fe641faf7c1d8ee6e51f Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 17 Dec 2024 12:36:49 +0100 Subject: [PATCH 10/41] Comments: beautified --- suprb/rule/matching.py | 4 ++-- suprb/solution/mixing_model.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/suprb/rule/matching.py b/suprb/rule/matching.py index 551e901d..36a857de 100644 --- a/suprb/rule/matching.py +++ b/suprb/rule/matching.py @@ -48,7 +48,7 @@ class OrderedBound(MatchingFunction): A standard interval-based matching function producing multi-dimensional hyperrectangular conditions. In effect, a lower (l) and upper bound (u) are specified for each dimension. Those bounds always fulfill l <= u - An example x is matched iff l_i <= x_i <= u_i for all dimensions i + An example x is matched if l_i <= x_i <= u_i for all dimensions i """ def __init__(self, bounds: np.ndarray = np.array([])): @@ -84,7 +84,7 @@ class UnorderedBound(MatchingFunction): A standard interval-based matching function producing multi-dimensional hyperrectangular conditions. Two bounds (p and q) exist which have no explicit ordering but are instead sorted during the matching process - An example x is matched iff q_i <= x_i <= p_i for all dimensions i + An example x is matched if q_i <= x_i <= p_i or p_i <= x_i <= q_i for all dimensions i """ def __init__(self, bounds: np.ndarray): diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index f4b8e8cf..86449bc7 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -102,6 +102,8 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. # Normalize tau_sum = self._get_tau_sum(subpopulation, matches, taus) out = pred / tau_sum + + # Round predicitions to int for Classification if isinstance(subpopulation[0].model, ClassifierMixin): out = [round(x) for x in out] return out From 1a3e51c174bb07f948d68f446c5b75d60f02ad8a Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 17 Dec 2024 12:52:24 +0100 Subject: [PATCH 11/41] fix: updated solution fitness to support class accuracy --- suprb/solution/fitness.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index f49354c8..008f81bf 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -8,11 +8,12 @@ class PseudoBIC(SolutionFitness): - """Tries to minimize complexity along with error. Can be negative.""" + """Tries to minimize complexity along with error. Can be negative for Regression tasks.""" def __call__(self, solution: Solution) -> float: # note that error is capped to 1e-4 in suprb.solution.Solution.fit - return -(solution.input_size_ * np.log(solution.score_) + sign = 1 if solution.task == "Classification" else -1 + return sign * (solution.input_size_ * np.log(solution.score_) + solution.complexity_ * np.log(solution.input_size_)) @@ -29,7 +30,8 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - return self.fitness_func_(self.alpha, pseudo_accuracy(solution.score_), + acc = solution.score_ if solution.task == "Classification" else pseudo_accuracy(solution.score_) + return self.fitness_func_(self.alpha, acc, c_norm(solution.complexity_, self.max_genome_length_)) * 100 From 4a543b623f10166c29e7ba15d62f2b3dde59c567 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 17 Dec 2024 13:34:20 +0100 Subject: [PATCH 12/41] fix: solution fitness functions now using acc in [0,1] for class --- suprb/solution/fitness.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index 008f81bf..1fbc8c4f 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -8,12 +8,12 @@ class PseudoBIC(SolutionFitness): - """Tries to minimize complexity along with error. Can be negative for Regression tasks.""" + """Tries to minimize complexity along with error. Can be negative.""" def __call__(self, solution: Solution) -> float: # note that error is capped to 1e-4 in suprb.solution.Solution.fit - sign = 1 if solution.task == "Classification" else -1 - return sign * (solution.input_size_ * np.log(solution.score_) + offset = 1 if solution.task == "Classification" else 0 + return -(solution.input_size_ * np.log(offset + solution.score_) + solution.complexity_ * np.log(solution.input_size_)) @@ -30,7 +30,7 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - acc = solution.score_ if solution.task == "Classification" else pseudo_accuracy(solution.score_) + acc = -solution.score_ if solution.task == "Classification" else pseudo_accuracy(solution.score_) return self.fitness_func_(self.alpha, acc, c_norm(solution.complexity_, self.max_genome_length_)) * 100 From 0304c33f336a3f74608919d4a6fae2a8c718a84b Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 17 Dec 2024 13:50:53 +0100 Subject: [PATCH 13/41] added classification example using the iris dataset --- examples/classification_smoke.py | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 examples/classification_smoke.py diff --git a/examples/classification_smoke.py b/examples/classification_smoke.py new file mode 100644 index 00000000..53794d4c --- /dev/null +++ b/examples/classification_smoke.py @@ -0,0 +1,58 @@ +from ucimlrepo import fetch_ucirepo +import sklearn +import numpy as np + +from sklearn.preprocessing import StandardScaler, MinMaxScaler +from sklearn.model_selection import cross_validate, train_test_split +from sklearn.linear_model import Ridge, LogisticRegression + + +from suprb.utils import check_random_state +from suprb.optimizer.rule.es import ES1xLambda +from suprb.optimizer.solution.ga import GeneticAlgorithm +from suprb.wrapper import SupRBWrapper + +from utils import log_scores + + +if __name__ == '__main__': + random_state = 123 + + #CLASSIFICATION + # fetch dataset + iris = fetch_ucirepo(id=53) + X = iris.data.features.to_numpy() + y = iris.data.targets.to_numpy() + dict = {"Iris-setosa": 1, 'Iris-versicolor': 2, 'Iris-virginica': 3} + y = [dict[x[0]] for x in y] + X = MinMaxScaler(feature_range=(-1, 1)).fit_transform(X) + #y = StandardScaler().fit_transform(y.reshape((-1, 1))).reshape((-1,)) + + X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=random_state) + + # Comparable with examples/example_2.py + model = SupRBWrapper(print_config=True, + + ## RULE GENERATION ## + rule_generation=ES1xLambda(), + rule_generation__n_iter=10, + rule_generation__lmbda=16, + rule_generation__operator='+', + rule_generation__delay=150, + rule_generation__random_state=random_state, + rule_generation__n_jobs=1, + rule_generation__init__model=LogisticRegression(), + + ## SOLUTION COMPOSITION ## + solution_composition=GeneticAlgorithm(), + solution_composition__n_iter=10, + solution_composition__population_size=32, + solution_composition__elitist_ratio=0.2, + solution_composition__random_state=random_state, + solution_composition__n_jobs=1) + + scores = cross_validate(model, X, y, cv=5, n_jobs=5, verbose=10, + scoring=['accuracy'], + return_estimator=True, fit_params={'cleanup': True}) + + log_scores(scores) From 2e7557592fc4af11d2b1149feabcf1ccf8ae0c2d Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 6 Jan 2025 19:37:13 +0100 Subject: [PATCH 14/41] acceptance for classification --- suprb/optimizer/rule/acceptance.py | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index d5a59a9b..3d49e63f 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -2,6 +2,8 @@ import numpy as np +from sklearn.metrics import precision_score, recall_score, f1_score + from suprb.base import BaseComponent from suprb.rule import Rule @@ -37,5 +39,34 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: if rule.experience_ < 1: return False local_y = y[rule.match_set_] - default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) - return rule.score_ <= default_error + if rule.task == "Regression": + default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) + elif rule.task == "Classification": + # default error is the trivial solution of always choosing the most common label + default_error = np.bincount(local_y).max() / (len(local_y) * self.beta) + return rule.error_ <= default_error + + +class Precision(RuleAcceptance): + """Insert if the rule has a precision greater or equal to a threshold""" + + def __init__(self, min_precission: float = 0.9): + self.min_precission = min_precission + + def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: + if rule.experience_ < 1: + return False + local_y = y[rule.match_set_] + return rule.error_ > precision_score(local_y, rule.predict(X[rule.match_set_])) + +class F1_Score(RuleAcceptance): + """Insert if the rule has a f1_score greater or equal to a threshold""" + + def __init__(self, min_f1: float = 0.9): + self.min_precission = min_f1 + + def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: + if rule.experience_ < 1: + return False + local_y = y[rule.match_set_] + return rule.error_ > f1_score(local_y, rule.predict(X[rule.match_set_])) \ No newline at end of file From f7cef4a5f907473a21c8260409f465ea54bf2c0f Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 6 Jan 2025 19:41:26 +0100 Subject: [PATCH 15/41] check if pool is empty before attempting to read it --- suprb/solution/base.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 40cfb1c9..4651e75a 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -2,6 +2,7 @@ import itertools from abc import ABCMeta, abstractmethod +from typing import Union import numpy as np from sklearn.base import RegressorMixin, ClassifierMixin @@ -9,7 +10,7 @@ from suprb.rule import Rule from suprb.base import BaseComponent, SolutionBase, SupervisedMixin -from suprb.fitness import BaseFitness +from suprb.fitness import BaseFitness, pseudo_error class MixingModel(BaseComponent, metaclass=ABCMeta): @@ -44,19 +45,19 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi self.pool = pool self.mixing = mixing self.fitness = fitness + self.task = None def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) - if isinstance(self.pool[0].model, ClassifierMixin): + if not self.pool: + self.error_ = 1 - 1e-4 + elif isinstance(self.pool[0].model, ClassifierMixin): self.task = 'Classification' + self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) else: self.task = 'Regression' - - if self.task == "Regression": - self.score_ = max(mean_squared_error(y, pred), 1e-4) - elif self.task == "Classification": - self.score_ = -max(accuracy_score(y, pred), 1e-4) + self.error_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster @@ -91,7 +92,7 @@ def clone(self, **kwargs) -> Solution: ) solution = Solution(**(args | kwargs)) if not kwargs: - attributes = ['fitness_', 'score_', 'complexity_', 'is_fitted_', 'input_size_'] + attributes = ['fitness_', 'error_', 'complexity_', 'is_fitted_', 'input_size_'] solution.__dict__ |= {key: getattr(self, key) for key in attributes} return solution From 8333b9068d8a339acbe2c103e3b4222e8d2b59ea Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 6 Jan 2025 19:42:19 +0100 Subject: [PATCH 16/41] classification error is now 1 - accuracy --- examples/classification_smoke.py | 23 +++++++++++-------- examples/wrapper_example_1.py | 6 +++-- suprb/base.py | 4 ++-- suprb/fitness.py | 8 +++++++ suprb/json.py | 2 +- suprb/logging/stdout.py | 2 +- suprb/optimizer/rule/selection.py | 4 ++-- .../solution/saga3/solution_extension.py | 2 +- .../solution/sas/solution_extension.py | 2 +- suprb/rule/base.py | 17 ++++++++------ suprb/rule/fitness.py | 10 +++++--- suprb/rule/matching.py | 2 +- suprb/solution/fitness.py | 7 +++--- suprb/solution/mixing_model.py | 7 +++--- suprb/suprb.py | 4 ++-- tests/test_mixing_model.py | 2 +- 16 files changed, 62 insertions(+), 40 deletions(-) diff --git a/examples/classification_smoke.py b/examples/classification_smoke.py index 53794d4c..dacf5e7e 100644 --- a/examples/classification_smoke.py +++ b/examples/classification_smoke.py @@ -5,10 +5,11 @@ from sklearn.preprocessing import StandardScaler, MinMaxScaler from sklearn.model_selection import cross_validate, train_test_split from sklearn.linear_model import Ridge, LogisticRegression - +from sklearn.utils import shuffle from suprb.utils import check_random_state from suprb.optimizer.rule.es import ES1xLambda +from suprb.optimizer.rule.acceptance import MaxError from suprb.optimizer.solution.ga import GeneticAlgorithm from suprb.wrapper import SupRBWrapper @@ -16,15 +17,19 @@ if __name__ == '__main__': - random_state = 123 + random_state = 125 #CLASSIFICATION # fetch dataset iris = fetch_ucirepo(id=53) X = iris.data.features.to_numpy() y = iris.data.targets.to_numpy() - dict = {"Iris-setosa": 1, 'Iris-versicolor': 2, 'Iris-virginica': 3} - y = [dict[x[0]] for x in y] + X, y = shuffle(X, y, random_state=random_state) + unique = np.unique(y) + toNum = dict(zip(unique, range(len(unique)))) + # targets = {"Iris-setosa": 1, 'Iris-versicolor': 2, 'Iris-virginica': 3} + # Conversion to int required for mixing, but possibly has unwanted sideeffects + y = [toNum[x[0]] for x in y] X = MinMaxScaler(feature_range=(-1, 1)).fit_transform(X) #y = StandardScaler().fit_transform(y.reshape((-1, 1))).reshape((-1,)) @@ -34,24 +39,24 @@ model = SupRBWrapper(print_config=True, ## RULE GENERATION ## - rule_generation=ES1xLambda(), - rule_generation__n_iter=10, + rule_generation=ES1xLambda(acceptance=MaxError(max_error=0.1)), + rule_generation__n_iter=100, rule_generation__lmbda=16, rule_generation__operator='+', - rule_generation__delay=150, + rule_generation__delay=10, rule_generation__random_state=random_state, rule_generation__n_jobs=1, rule_generation__init__model=LogisticRegression(), ## SOLUTION COMPOSITION ## solution_composition=GeneticAlgorithm(), - solution_composition__n_iter=10, + solution_composition__n_iter=100, solution_composition__population_size=32, solution_composition__elitist_ratio=0.2, solution_composition__random_state=random_state, solution_composition__n_jobs=1) - scores = cross_validate(model, X, y, cv=5, n_jobs=5, verbose=10, + scores = cross_validate(model, X, y, cv=4, n_jobs=4, verbose=10, scoring=['accuracy'], return_estimator=True, fit_params={'cleanup': True}) diff --git a/examples/wrapper_example_1.py b/examples/wrapper_example_1.py index 3a91854b..5c84dc4a 100644 --- a/examples/wrapper_example_1.py +++ b/examples/wrapper_example_1.py @@ -3,6 +3,7 @@ from sklearn.preprocessing import StandardScaler, MinMaxScaler from sklearn.model_selection import cross_validate, train_test_split +from sklearn.linear_model import Ridge from suprb.utils import check_random_state @@ -49,16 +50,17 @@ def load_higdon_gramacy_lee(n_samples=1000, noise=0, random_state=None): rule_generation__delay=150, rule_generation__random_state=random_state, rule_generation__n_jobs=1, + rule_generation__init__model=Ridge(), ## SOLUTION COMPOSITION ## solution_composition=GeneticAlgorithm(), - solution_composition__n_iter=32, + solution_composition__n_iter=10, solution_composition__population_size=32, solution_composition__elitist_ratio=0.2, solution_composition__random_state=random_state, solution_composition__n_jobs=1) - scores = cross_validate(model, X, y, cv=4, n_jobs=1, verbose=10, + scores = cross_validate(model, X, y, cv=4, n_jobs=4, verbose=10, scoring=['r2', 'neg_mean_squared_error'], return_estimator=True, fit_params={'cleanup': True}) diff --git a/suprb/base.py b/suprb/base.py index e4366259..5d5163de 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -28,7 +28,7 @@ class SolutionBase(metaclass=ABCMeta): """An individual solution to some problem.""" is_fitted_: bool - score_: float + error_: float fitness_: float @abstractmethod @@ -38,7 +38,7 @@ def clone(self, **kwargs) -> SolutionBase: def __str__(self): if hasattr(self, 'is_fitted_') and self.is_fitted_: - attributes = {'score': self.score_, 'fitness': self.fitness_} | self._more_str_attributes() + attributes = {'error': self.error_, 'fitness': self.fitness_} | self._more_str_attributes() concat = ",".join([f"{key}={value}" for key, value in attributes.items()]) return f"{self.__class__.__name__}({concat})" diff --git a/suprb/fitness.py b/suprb/fitness.py index ae3d6948..d15fc56f 100644 --- a/suprb/fitness.py +++ b/suprb/fitness.py @@ -20,6 +20,14 @@ def pseudo_accuracy(error: float, beta=2) -> float: assert beta > 0 return np.exp(-beta * error) +def pseudo_error(accuracy: float) -> float: + return 1 - accuracy + +def actual_accuracy(error: float) -> float: + ''' + for classification + ''' + return 1 - error def emary(alpha: float, x1: float, x2: float) -> float: """ diff --git a/suprb/json.py b/suprb/json.py index da9845e3..a8b94fe0 100644 --- a/suprb/json.py +++ b/suprb/json.py @@ -204,7 +204,7 @@ def _convert_json_to_rule(json_rule, json_dict): _convert_model(json_rule["model"], json_dict["config"]["rule_generation__init__model"]), _get_class(json_dict["config"]["rule_generation__init__fitness"])) - rule.score_ = json_rule["score_"] + rule.error_ = json_rule["error_"] rule.experience_ = json_rule["experience_"] rule.is_fitted_ = json_rule["is_fitted_"] diff --git a/suprb/logging/stdout.py b/suprb/logging/stdout.py index 55082881..90fccbf8 100644 --- a/suprb/logging/stdout.py +++ b/suprb/logging/stdout.py @@ -24,7 +24,7 @@ def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: SupRB, iteratio elitist = estimator.solution_composition_.elitist() message = str(dict( - error=elitist.score_, + error=elitist.error_, fitness=elitist.fitness_, complexity=elitist.complexity_, score=elitist.score(X, y), diff --git a/suprb/optimizer/rule/selection.py b/suprb/optimizer/rule/selection.py index b8a83191..8aee168c 100644 --- a/suprb/optimizer/rule/selection.py +++ b/suprb/optimizer/rule/selection.py @@ -50,12 +50,12 @@ def __call__(self, rules: list[Rule], random_state: RandomState, size: int = 1) to_be_added = False for can in candidates: - if can.score_ < rule.score_ and can.volume_ > rule.volume_: + if can.error_ < rule.error_ and can.volume_ > rule.volume_: # classifier is dominated by this candidate and should not # become a new candidate to_be_added = False break - elif can.score_ > rule.score_ and can.volume_ < rule.volume_: + elif can.error_ > rule.error_ and can.volume_ < rule.volume_: # classifier dominates candidate candidates.remove(can) to_be_added = True diff --git a/suprb/optimizer/solution/saga3/solution_extension.py b/suprb/optimizer/solution/saga3/solution_extension.py index 6b3c3d9e..01738120 100644 --- a/suprb/optimizer/solution/saga3/solution_extension.py +++ b/suprb/optimizer/solution/saga3/solution_extension.py @@ -90,7 +90,7 @@ def __init__(self, genome: np.ndarray, def fit(self, X: np.ndarray, y: np.ndarray) -> SagaSolution: pred = self.predict(X, cache=True) - self.score_ = max(mean_squared_error(y, pred), 1e-4) + self.error_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) diff --git a/suprb/optimizer/solution/sas/solution_extension.py b/suprb/optimizer/solution/sas/solution_extension.py index 56dfd94b..7ae85c09 100644 --- a/suprb/optimizer/solution/sas/solution_extension.py +++ b/suprb/optimizer/solution/sas/solution_extension.py @@ -39,7 +39,7 @@ def __init__(self, genome: np.ndarray, def fit(self, X: np.ndarray, y: np.ndarray) -> SasSolution: pred = self.predict(X, cache=True) - self.score_ = max(mean_squared_error(y, pred), 1e-4) + self.error_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster self.fitness_ = self.fitness(self) diff --git a/suprb/rule/base.py b/suprb/rule/base.py index 6d555820..8d9872e1 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -8,7 +8,7 @@ from sklearn.metrics import mean_squared_error, accuracy_score from suprb.base import SolutionBase -from suprb.fitness import BaseFitness +from suprb.fitness import BaseFitness, pseudo_error from .matching import MatchingFunction @@ -56,7 +56,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: # No reason to fit if no data point matches if not np.any(match_set): self.is_fitted_ = False - self.score_ = np.inf + self.error_ = np.inf self.fitness_ = -np.inf self.experience_ = 0 self.pred_ = np.array([]) @@ -74,14 +74,14 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: # Get all data points which match the bounds. X, y = X[self.match_set_], y[self.match_set_] - # No reason to fit if only one class match_set + # No reason to fit if only one label in match_set if self.task == 'Classification': if len(np.unique(y)) == 1: self.pred_ = y[0] - self.score_ = 1 - 1e-4 + self.error_ = 1 self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) - self.is_fitted_ = True + self.is_fitted_ = False return self # Create and fit the model @@ -89,9 +89,9 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: self.pred_ = self.model.predict(X) if self.task == 'Regression': - self.score_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? + self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? elif self.task == 'Classification': - self.score_ = -max(accuracy_score(y, self.pred_), 1e-4) + self.error_ = max(pseudo_error(accuracy_score(y, self.pred_)), 1e-4) self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) @@ -103,6 +103,9 @@ def volume_(self): return self.match.volume_ def predict(self, X: np.ndarray): + # Make array 2dim in case of single sample + if np.ndim(X) == 1: + X = X.reshape(1, -1) return self.model.predict(X) def clone(self, **kwargs) -> Rule: diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index 51caf7de..d85fa474 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -17,8 +17,8 @@ def __call__(self, rule: Rule) -> float: # note that we multiply solely for readability reasons without # any performance impact if rule.task == "Classification": - return -rule.score_ * 100 - return pseudo_accuracy(rule.score_) * 100 + return (1 - rule.error_) * 100 + return pseudo_accuracy(rule.error_) * 100 class VolumeRuleFitness(RuleFitness, metaclass=ABCMeta): @@ -34,7 +34,11 @@ def __call__(self, rule: Rule) -> float: input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume - return self.fitness_func_(alpha=self.alpha, x1=pseudo_accuracy(rule.score_, beta=2), x2=volume_share) * 100 + if rule.task == "Classification": + x1 = (1 - rule.error_) + else: + x1 = pseudo_accuracy(rule.error_, beta=2) + return self.fitness_func_(alpha=self.alpha, x1 = x1, x2=volume_share) * 100 class VolumeEmary(VolumeRuleFitness): diff --git a/suprb/rule/matching.py b/suprb/rule/matching.py index 36a857de..abc51c8a 100644 --- a/suprb/rule/matching.py +++ b/suprb/rule/matching.py @@ -173,7 +173,7 @@ class MinPercentage(MatchingFunction): A standard interval-based matching function producing multi-dimensional hyperrectangular conditions. In effect, a lower bound (l) and a distance proportion (p) are specified for each dimension. - An example x is matched iff + An example x is matched if l_i <= x_i <= l_i + p_i * (max_i - l_i) for all dimensions i """ diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index 1fbc8c4f..9168bced 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -3,7 +3,7 @@ import numpy as np -from suprb.fitness import emary, pseudo_accuracy, wu +from suprb.fitness import emary, pseudo_accuracy, wu, actual_accuracy from . import Solution, SolutionFitness @@ -12,8 +12,7 @@ class PseudoBIC(SolutionFitness): def __call__(self, solution: Solution) -> float: # note that error is capped to 1e-4 in suprb.solution.Solution.fit - offset = 1 if solution.task == "Classification" else 0 - return -(solution.input_size_ * np.log(offset + solution.score_) + return -(solution.input_size_ * np.log(solution.error_) + solution.complexity_ * np.log(solution.input_size_)) @@ -30,7 +29,7 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - acc = -solution.score_ if solution.task == "Classification" else pseudo_accuracy(solution.score_) + acc = actual_accuracy(solution.error_) if solution.task == "Classification" else pseudo_accuracy(solution.error_) return self.fitness_func_(self.alpha, acc, c_norm(solution.complexity_, self.max_genome_length_)) * 100 diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index 86449bc7..74b04c97 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -109,7 +109,8 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. return out def _get_local_pred(self, X: np.ndarray, subpopulation: list[Rule], cache: bool): - local_pred = np.zeros((len(subpopulation), self.input_size)) + if subpopulation: + local_pred = np.zeros((len(subpopulation), self.input_size)) if cache: # Use the precalculated matches and predictions from fit() @@ -129,9 +130,9 @@ def _get_local_pred(self, X: np.ndarray, subpopulation: list[Rule], cache: bool) def _get_taus(self, subpopulation: list[Rule], dim: int): # Get errors and experience of all rules in subpopulation experiences = self.experience_calculation(subpopulation, dim) - scores = np.array([rule.score_ for rule in subpopulation]) + errors = np.array([rule.error_ for rule in subpopulation]) - return (1 / scores) * (experiences * self.experience_weight) + return (1 / errors) * (experiences * self.experience_weight) def _get_tau_sum(self, subpopulation: list[Rule], matches: list[Rule], taus: list[int]): # Sum all taus diff --git a/suprb/suprb.py b/suprb/suprb.py index d2216c4c..f6f4ea59 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -142,7 +142,7 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): self.is_error_ = False self.elitist_ = Solution([0, 0, 0], [0, 0, 0], ErrorExperienceHeuristic(), PseudoBIC()) self.elitist_.fitness_ = 0 - self.elitist_.score_ = 99999 + self.elitist_.error_ = 99999 self.elitist_.complexity_ = 99999 # Check that x and y have correct shape @@ -150,7 +150,7 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): X, y = check_X_y(X, y, dtype='float64', y_numeric=True) y = check_array(y, ensure_2d=False, dtype='float64') else: - X, y = check_X_y(X, y, dtype=None, y_numeric=True) + X, y = check_X_y(X, y, dtype=None, y_numeric=False) y = check_array(y, ensure_2d=False, dtype=None) # Init sklearn interface diff --git a/tests/test_mixing_model.py b/tests/test_mixing_model.py index c9957b31..5a897cd7 100644 --- a/tests/test_mixing_model.py +++ b/tests/test_mixing_model.py @@ -20,7 +20,7 @@ def create_rule(self, fitness, experience, error): rule.fitness_ = fitness rule.experience_ = experience - rule.score_ = error + rule.error_ = error return rule From 4a84c9d5b2fdf5595c05ca14548ba079e7f25fdf Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 7 Jan 2025 15:01:45 +0100 Subject: [PATCH 17/41] added MixingModel for Classification --- examples/classification_smoke.py | 6 +- requirements.txt | 1 + suprb/solution/mixing_model.py | 118 +++++++++++++++++++++++++------ 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/examples/classification_smoke.py b/examples/classification_smoke.py index dacf5e7e..a27c6951 100644 --- a/examples/classification_smoke.py +++ b/examples/classification_smoke.py @@ -7,6 +7,7 @@ from sklearn.linear_model import Ridge, LogisticRegression from sklearn.utils import shuffle +import suprb from suprb.utils import check_random_state from suprb.optimizer.rule.es import ES1xLambda from suprb.optimizer.rule.acceptance import MaxError @@ -26,7 +27,7 @@ y = iris.data.targets.to_numpy() X, y = shuffle(X, y, random_state=random_state) unique = np.unique(y) - toNum = dict(zip(unique, range(len(unique)))) + toNum = dict(zip(unique, range(1, len(unique)+1))) # targets = {"Iris-setosa": 1, 'Iris-versicolor': 2, 'Iris-virginica': 3} # Conversion to int required for mixing, but possibly has unwanted sideeffects y = [toNum[x[0]] for x in y] @@ -39,7 +40,7 @@ model = SupRBWrapper(print_config=True, ## RULE GENERATION ## - rule_generation=ES1xLambda(acceptance=MaxError(max_error=0.1)), + rule_generation=ES1xLambda(), rule_generation__n_iter=100, rule_generation__lmbda=16, rule_generation__operator='+', @@ -50,6 +51,7 @@ ## SOLUTION COMPOSITION ## solution_composition=GeneticAlgorithm(), + solution_composition__init__mixing=suprb.solution.mixing_model.ErrorExperienceClassification(), solution_composition__n_iter=100, solution_composition__population_size=32, solution_composition__elitist_ratio=0.2, diff --git a/requirements.txt b/requirements.txt index a50b3f44..153e8220 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ joblib~=1.1.0 tqdm~=4.62.3 pytest~=6.2.5 protobuf~=3.20.0 +ucimlrepo \ No newline at end of file diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index 74b04c97..831186ea 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -69,6 +69,101 @@ def __call__(self, subpopulation: list[Rule], dim: int = None) -> list[Rule]: return np.clip(experiences, self.lower_bound * dim, self.upper_bound * dim) +def get_local_pred(X: np.ndarray, subpopulation: list[Rule], cache: bool): + input_size = X.shape[0] + if subpopulation: + local_pred = np.zeros((len(subpopulation), input_size)) + + if cache: + # Use the precalculated matches and predictions from fit() + matches = [rule.match_set_ for rule in subpopulation] + for i, rule in enumerate(subpopulation): + local_pred[i][matches[i]] = rule.pred_ + else: + # Generate all data new + matches = [rule.match(X) for rule in subpopulation] + for i, rule in enumerate(subpopulation): + if not matches[i].any(): + continue + local_pred[i][matches[i]] = rule.predict(X[matches[i]]) + + return local_pred, matches + + +class MostVoted(MixingModel): + + def __init__(self, filter_subpopulation: FilterSubpopulation = FilterSubpopulation()): + self.input_size = None + self.filter_subpopulation = filter_subpopulation + + def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np.ndarray: + self.input_size = X.shape[0] + + # No need to perform any calculation if no rule was selected. + if not subpopulation: + return np.zeros(self.input_size) + + subpopulation = self.filter_subpopulation(subpopulation) + + local_pred, matches = get_local_pred(X, subpopulation, cache) + pred_per_sample = local_pred.transpose() + out = np.zeros(self.input_size) + for i, pred in enumerate(pred_per_sample): + # strip to ensure only valid predictions are counted + stripped_pred = [i for i in pred if i != 0] + out[i] = np.bincount(stripped_pred).argmax() + out = [round(x) for x in out] + return out + + +class ErrorExperienceClassification(MixingModel): + """ + Performs mixing similar to the Inverse Variance Heuristic from + https://researchportal.bath.ac.uk/en/studentTheses/learning-classifier-systems-from-first-principles-a-probabilistic, + but using (error / experience) as a mixing function. + """ + + def __init__(self, filter_subpopulation: FilterSubpopulation = FilterSubpopulation(), + experience_calculation: ExperienceCalculation = ExperienceCalculation(), + experience_weight: float = 1): + self.input_size = None + self.filter_subpopulation = filter_subpopulation + self.experience_calculation = experience_calculation + self.experience_weight = experience_weight + + def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np.ndarray: + self.input_size = X.shape[0] + + # No need to perform any calculation if no rule was selected. + if not subpopulation: + return np.zeros(self.input_size) + + subpopulation = self.filter_subpopulation(subpopulation) + + local_pred, matches = get_local_pred(X, subpopulation, cache) + pred_per_sample = local_pred.transpose() + match_per_sample = np.array(matches).transpose() + taus = self._get_taus(subpopulation, X.shape[1]) + out = np.zeros(self.input_size) + for x, pred in enumerate(pred_per_sample): + # strip to ensure only valid predictions are counted + stripped_pred = pred[match_per_sample[x]] + stripped_pred = [int(label) for label in stripped_pred] + if not stripped_pred: + out[x] = int(np.random.choice(local_pred.flatten())) + continue + local_taus = taus[match_per_sample[x]] + out[x] = np.bincount(stripped_pred, weights=local_taus).argmax() + return out + + def _get_taus(self, subpopulation: list[Rule], dim: int): + # Get errors and experience of all rules in subpopulation + experiences = self.experience_calculation(subpopulation, dim) + errors = np.array([rule.error_ for rule in subpopulation]) + + return (1 / errors) * (experiences * self.experience_weight) + + class ErrorExperienceHeuristic(MixingModel): """ Performs mixing similar to the Inverse Variance Heuristic from @@ -93,7 +188,7 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. subpopulation = self.filter_subpopulation(subpopulation) - local_pred, matches = self._get_local_pred(X, subpopulation, cache) + local_pred, matches = get_local_pred(X, subpopulation, cache) taus = self._get_taus(subpopulation, X.shape[1]) # Stack all local predictions and sum them weighted with tau @@ -103,30 +198,11 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. tau_sum = self._get_tau_sum(subpopulation, matches, taus) out = pred / tau_sum - # Round predicitions to int for Classification + # Round predicitions to int for Classification (intended for ordered labels only) if isinstance(subpopulation[0].model, ClassifierMixin): out = [round(x) for x in out] return out - def _get_local_pred(self, X: np.ndarray, subpopulation: list[Rule], cache: bool): - if subpopulation: - local_pred = np.zeros((len(subpopulation), self.input_size)) - - if cache: - # Use the precalculated matches and predictions from fit() - matches = [rule.match_set_ for rule in subpopulation] - for i, rule in enumerate(subpopulation): - local_pred[i][matches[i]] = rule.pred_ - else: - # Generate all data new - matches = [rule.match(X) for rule in subpopulation] - for i, rule in enumerate(subpopulation): - if not matches[i].any(): - continue - local_pred[i][matches[i]] = rule.predict(X[matches[i]]) - - return local_pred, matches - def _get_taus(self, subpopulation: list[Rule], dim: int): # Get errors and experience of all rules in subpopulation experiences = self.experience_calculation(subpopulation, dim) From 61fe83905298c8d5816db4e52cceb4211f56093a Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 9 Jan 2025 15:58:46 +0100 Subject: [PATCH 18/41] fix: no 0-prediction from ErrorExpClass in case of mismatch --- suprb/solution/mixing_model.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/suprb/solution/mixing_model.py b/suprb/solution/mixing_model.py index 831186ea..ad0627fc 100644 --- a/suprb/solution/mixing_model.py +++ b/suprb/solution/mixing_model.py @@ -108,11 +108,14 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. local_pred, matches = get_local_pred(X, subpopulation, cache) pred_per_sample = local_pred.transpose() out = np.zeros(self.input_size) - for i, pred in enumerate(pred_per_sample): + for x, pred in enumerate(pred_per_sample): # strip to ensure only valid predictions are counted stripped_pred = [i for i in pred if i != 0] - out[i] = np.bincount(stripped_pred).argmax() - out = [round(x) for x in out] + if not stripped_pred: + out[x] = int(np.random.choice(local_pred.flatten())) + continue + out[x] = np.bincount(stripped_pred).argmax() + out = [int(label) for label in out] return out @@ -150,7 +153,7 @@ def __call__(self, X: np.ndarray, subpopulation: list[Rule], cache=False) -> np. stripped_pred = pred[match_per_sample[x]] stripped_pred = [int(label) for label in stripped_pred] if not stripped_pred: - out[x] = int(np.random.choice(local_pred.flatten())) + out[x] = int(np.random.choice([i for i in local_pred.flatten() if i != 0])) continue local_taus = taus[match_per_sample[x]] out[x] = np.bincount(stripped_pred, weights=local_taus).argmax() From f371bfc218b1296e250b82b2fc0e9bd0607aef90 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 12:31:06 +0100 Subject: [PATCH 19/41] accuracy to error in rule acceptance --- suprb/optimizer/rule/acceptance.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index 3d49e63f..9eb4df1c 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -6,6 +6,7 @@ from suprb.base import BaseComponent from suprb.rule import Rule +from suprb.fitness import pseudo_error class RuleAcceptance(BaseComponent, metaclass=ABCMeta): @@ -43,7 +44,9 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) elif rule.task == "Classification": # default error is the trivial solution of always choosing the most common label - default_error = np.bincount(local_y).max() / (len(local_y) * self.beta) + local_y = [round(y) for y in local_y] + default_accuracy = np.bincount(local_y).max() / (len(local_y) * self.beta) + default_error = pseudo_error(default_accuracy) return rule.error_ <= default_error From 80d422d47a2b747b19bc50f7a213838e7b1ce26d Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 12:52:59 +0100 Subject: [PATCH 20/41] added score to Solution --- suprb/base.py | 4 +++- suprb/solution/base.py | 35 ++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/suprb/base.py b/suprb/base.py index 5d5163de..1da4a13d 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -172,7 +172,9 @@ def predict(self, X: np.ndarray): pass class SupervisedMixin(ABC): - pass + @abstractmethod + def score(self, X, y, sample_weight=None): + pass SupervisedMixin.register(RegressorMixin) SupervisedMixin.register(ClassifierMixin) \ No newline at end of file diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 4651e75a..33ed0ebe 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -5,8 +5,8 @@ from typing import Union import numpy as np -from sklearn.base import RegressorMixin, ClassifierMixin -from sklearn.metrics import mean_squared_error, accuracy_score +from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator +from sklearn.metrics import mean_squared_error, accuracy_score, r2_score from suprb.rule import Rule from suprb.base import BaseComponent, SolutionBase, SupervisedMixin @@ -45,18 +45,35 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi self.pool = pool self.mixing = mixing self.fitness = fitness - self.task = None + self.isClass = None + + def score(self, X, y, sample_weight=None): + if self.isClass is None: + if not self.pool: + return 0.0 + elif isinstance(self.pool[0].model, ClassifierMixin): + self.isClass = True + else: + self.isClass = False + elif self.isClass: + return accuracy_score(y, self.predict(X), sample_weight=sample_weight) + else: + y_pred = self.predict(X) + return r2_score(y, y_pred, sample_weight=sample_weight) + def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) - - if not self.pool: - self.error_ = 1 - 1e-4 - elif isinstance(self.pool[0].model, ClassifierMixin): - self.task = 'Classification' + if self.isClass is None: + if not self.pool: + self.error = 9999 + elif isinstance(self.pool[0].model, ClassifierMixin): + self.isClass = True + else: + self.isClass = False + elif self.isClass: self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) else: - self.task = 'Regression' self.error_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] From 4cd21cb85b9ca1c68437000dac5e661cb6513f36 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 13:36:11 +0100 Subject: [PATCH 21/41] default_score 0.0 deafult_error 9999 --- suprb/solution/base.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 33ed0ebe..9a390e16 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -48,14 +48,14 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi self.isClass = None def score(self, X, y, sample_weight=None): + if not self.pool: + return 0.0 if self.isClass is None: - if not self.pool: - return 0.0 - elif isinstance(self.pool[0].model, ClassifierMixin): + if isinstance(self.pool[0].model, ClassifierMixin): self.isClass = True else: self.isClass = False - elif self.isClass: + if self.isClass: return accuracy_score(y, self.predict(X), sample_weight=sample_weight) else: y_pred = self.predict(X) @@ -64,14 +64,14 @@ def score(self, X, y, sample_weight=None): def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) + if not self.pool: + self.error_ = 9999 if self.isClass is None: - if not self.pool: - self.error = 9999 - elif isinstance(self.pool[0].model, ClassifierMixin): + if isinstance(self.pool[0].model, ClassifierMixin): self.isClass = True else: self.isClass = False - elif self.isClass: + if self.isClass: self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) else: self.error_ = max(mean_squared_error(y, pred), 1e-4) From 2ce75317b83aa142f902b1e78ad75876e47ad017 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 13:49:18 +0100 Subject: [PATCH 22/41] added bool isClass(ification) --- suprb/rule/base.py | 10 +++++----- suprb/rule/fitness.py | 10 +++++----- suprb/rule/initialization.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/suprb/rule/base.py b/suprb/rule/base.py index 8d9872e1..962c1105 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -44,9 +44,9 @@ def __init__(self, match: MatchingFunction, input_space: np.ndarray, model: Regr self.model = model self.fitness = fitness if isinstance(model, ClassifierMixin): - self.task = 'Classification' + self.isClass = True else: - self.task = 'Regression' + self.isClass = False def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: @@ -75,7 +75,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: X, y = X[self.match_set_], y[self.match_set_] # No reason to fit if only one label in match_set - if self.task == 'Classification': + if self.isClass: if len(np.unique(y)) == 1: self.pred_ = y[0] self.error_ = 1 @@ -88,9 +88,9 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: self.model.fit(X, y) self.pred_ = self.model.predict(X) - if self.task == 'Regression': + if not self.isClass: self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? - elif self.task == 'Classification': + elif self.isClass: self.error_ = max(pseudo_error(accuracy_score(y, self.pred_)), 1e-4) self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index d85fa474..7ea5cd64 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -3,7 +3,7 @@ import numpy as np -from suprb.fitness import pseudo_accuracy, emary, wu +from suprb.fitness import pseudo_accuracy, actual_accuracy, emary, wu from . import Rule, RuleFitness @@ -16,8 +16,8 @@ class PseudoAccuracy(RuleFitness): def __call__(self, rule: Rule) -> float: # note that we multiply solely for readability reasons without # any performance impact - if rule.task == "Classification": - return (1 - rule.error_) * 100 + if rule.isClass: + return actual_accuracy(rule.error_) * 100 return pseudo_accuracy(rule.error_) * 100 @@ -34,8 +34,8 @@ def __call__(self, rule: Rule) -> float: input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume - if rule.task == "Classification": - x1 = (1 - rule.error_) + if rule.isClass: + x1 = actual_accuracy(rule.error_) else: x1 = pseudo_accuracy(rule.error_, beta=2) return self.fitness_func_(alpha=self.alpha, x1 = x1, x2=volume_share) * 100 diff --git a/suprb/rule/initialization.py b/suprb/rule/initialization.py index 38e5571e..a147dc1d 100644 --- a/suprb/rule/initialization.py +++ b/suprb/rule/initialization.py @@ -35,9 +35,9 @@ def __init__(self, bounds: np.ndarray = None, self._validate_components(model=Ridge(alpha=0.01), fitness=VolumeWu()) if isinstance(model, ClassifierMixin): - self.task = 'Classification' + self.isClass = True else: - self.task = 'Regression' + self.isClass = False @property def matching_type(self): From 97cb3ac86f75bada5a5a1b81af46bbeb68f29527 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 13:56:36 +0100 Subject: [PATCH 23/41] isClass in solution fitness --- suprb/solution/fitness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index 9168bced..27a368ea 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -29,7 +29,7 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - acc = actual_accuracy(solution.error_) if solution.task == "Classification" else pseudo_accuracy(solution.error_) + acc = actual_accuracy(solution.error_) if solution.isClass else pseudo_accuracy(solution.error_) return self.fitness_func_(self.alpha, acc, c_norm(solution.complexity_, self.max_genome_length_)) * 100 From 8822b735c6a5d4e9ffa3515c1a9a80eabdde97ea Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 21 Jan 2025 14:01:28 +0100 Subject: [PATCH 24/41] fix acceptance --- suprb/optimizer/rule/acceptance.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index 9eb4df1c..b33878a3 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -40,9 +40,8 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: if rule.experience_ < 1: return False local_y = y[rule.match_set_] - if rule.task == "Regression": - default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) - elif rule.task == "Classification": + default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) + if rule.isClass: # default error is the trivial solution of always choosing the most common label local_y = [round(y) for y in local_y] default_accuracy = np.bincount(local_y).max() / (len(local_y) * self.beta) From 92a083f61632da7404947db50274413fa534392b Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 23 Jan 2025 12:01:16 +0100 Subject: [PATCH 25/41] added score and model_swap --- suprb/base.py | 3 +++ suprb/suprb.py | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/suprb/base.py b/suprb/base.py index 1da4a13d..362cd1aa 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -85,7 +85,10 @@ def predict(self, X: np.ndarray): y : np.ndarray Returns the estimation with shape (n_samples,). """ + pass + @abstractmethod + def score(self, X, y, sample_weight=None): pass class BaseRegressor(BaseSupervised, RegressorMixin, metaclass=ABCMeta): diff --git a/suprb/suprb.py b/suprb/suprb.py index f6f4ea59..e7e351d6 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -1,10 +1,13 @@ import warnings import traceback +import copy import numpy as np from sklearn import clone from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array +from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator +from sklearn.metrics import mean_squared_error, accuracy_score, r2_score from .base import BaseSupervised, BaseRegressor, BaseClassifier from .exceptions import PopulationEmptyWarning @@ -117,6 +120,21 @@ def check_early_stopping(self): return True return False + + def score(self, X, y, sample_weight=None): + if not self.pool: + return 0.0 + if self.isClass is None: + if isinstance(self.pool[0].model, ClassifierMixin): + self.isClass = True + else: + self.isClass = False + if self.isClass: + return accuracy_score(y, self.predict(X), sample_weight=sample_weight) + else: + y_pred = self.predict(X) + return r2_score(y, y_pred, sample_weight=sample_weight) + def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): """ Fit SupRB.2. @@ -275,7 +293,7 @@ def _compose_solution(self, X: np.ndarray, y: np.ndarray): self.solution_composition_.optimize(X, y) def predict(self, X: np.ndarray): - # Check is fit had been called + # Check if fit had been called check_is_fitted(self, ['is_fitted_']) # Input validation X = check_array(X) @@ -343,3 +361,11 @@ def _more_tags(self): return { 'poor_score': True, } + + def model_swap(self, local_model) -> Solution: + solution = self.elitist_.clone() + # pool = [] + for space in solution.pool: + space = space.clone(model = copy.deepcopy(local_model)) + #pool.append(rule) + return solution \ No newline at end of file From 878a92bb10d82ab74867cc9ccb85d4a062d49c4a Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 23 Jan 2025 12:01:16 +0100 Subject: [PATCH 26/41] added score and model_swap --- suprb/base.py | 3 +++ suprb/suprb.py | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/suprb/base.py b/suprb/base.py index 1da4a13d..362cd1aa 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -85,7 +85,10 @@ def predict(self, X: np.ndarray): y : np.ndarray Returns the estimation with shape (n_samples,). """ + pass + @abstractmethod + def score(self, X, y, sample_weight=None): pass class BaseRegressor(BaseSupervised, RegressorMixin, metaclass=ABCMeta): diff --git a/suprb/suprb.py b/suprb/suprb.py index f6f4ea59..4634ae1a 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -1,10 +1,13 @@ import warnings import traceback +import copy import numpy as np from sklearn import clone from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array +from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator +from sklearn.metrics import mean_squared_error, accuracy_score, r2_score from .base import BaseSupervised, BaseRegressor, BaseClassifier from .exceptions import PopulationEmptyWarning @@ -117,6 +120,21 @@ def check_early_stopping(self): return True return False + + def score(self, X, y, sample_weight=None): + if not self.pool_: + return 0.0 + if self.isClass is None: + if isinstance(self.pool_[0].model, ClassifierMixin): + self.isClass = True + else: + self.isClass = False + if self.isClass: + return accuracy_score(y, self.predict(X), sample_weight=sample_weight) + else: + y_pred = self.predict(X) + return r2_score(y, y_pred, sample_weight=sample_weight) + def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): """ Fit SupRB.2. @@ -275,7 +293,7 @@ def _compose_solution(self, X: np.ndarray, y: np.ndarray): self.solution_composition_.optimize(X, y) def predict(self, X: np.ndarray): - # Check is fit had been called + # Check if fit had been called check_is_fitted(self, ['is_fitted_']) # Input validation X = check_array(X) @@ -343,3 +361,11 @@ def _more_tags(self): return { 'poor_score': True, } + + def model_swap(self, local_model) -> Solution: + solution = self.elitist_.clone() + # pool = [] + for space in solution.pool: + space = space.clone(model = copy.deepcopy(local_model)) + #pool.append(rule) + return solution \ No newline at end of file From 0f8cd3623c00573865b309b890eb43b3fec49320 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 23 Jan 2025 12:55:14 +0100 Subject: [PATCH 27/41] fix isClass --- suprb/suprb.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/suprb/suprb.py b/suprb/suprb.py index d8c3d0d6..faeaf09c 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -105,6 +105,7 @@ def __init__(self, self.n_jobs = n_jobs self.early_stopping_patience = early_stopping_patience self.early_stopping_delta = early_stopping_delta + self.isClass = None def check_early_stopping(self): if self.early_stopping_patience > 0: @@ -135,20 +136,6 @@ def score(self, X, y, sample_weight=None): y_pred = self.predict(X) return r2_score(y, y_pred, sample_weight=sample_weight) - - def score(self, X, y, sample_weight=None): - if not self.pool: - return 0.0 - if self.isClass is None: - if isinstance(self.pool[0].model, ClassifierMixin): - self.isClass = True - else: - self.isClass = False - if self.isClass: - return accuracy_score(y, self.predict(X), sample_weight=sample_weight) - else: - y_pred = self.predict(X) - return r2_score(y, y_pred, sample_weight=sample_weight) def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): """ Fit SupRB.2. From e9324c52cdb240555d7e2ce1c3552c4686880901 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Sat, 25 Jan 2025 12:52:52 +0100 Subject: [PATCH 28/41] fix solution fit --- suprb/solution/base.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 9a390e16..39be64a8 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -66,15 +66,16 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: pred = self.predict(X, cache=True) if not self.pool: self.error_ = 9999 - if self.isClass is None: - if isinstance(self.pool[0].model, ClassifierMixin): - self.isClass = True - else: - self.isClass = False - if self.isClass: - self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) else: - self.error_ = max(mean_squared_error(y, pred), 1e-4) + if self.isClass is None: + if isinstance(self.pool[0].model, ClassifierMixin): + self.isClass = True + else: + self.isClass = False + if self.isClass: + self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) + else: + self.error_ = max(mean_squared_error(y, pred), 1e-4) self.input_size_ = self.genome.shape[0] self.complexity_ = np.sum(self.genome).item() # equivalent to np.count_nonzero, but possibly faster From 87b6e849cd4030608b715b0e01cc01fa4a872810 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Sat, 25 Jan 2025 12:54:02 +0100 Subject: [PATCH 29/41] use sklearn.base to check task_type --- suprb/suprb.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/suprb/suprb.py b/suprb/suprb.py index faeaf09c..36de404b 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -6,7 +6,7 @@ from sklearn import clone from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array -from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator +from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator, is_classifier, is_regressor from sklearn.metrics import mean_squared_error, accuracy_score, r2_score from .base import BaseSupervised, BaseRegressor, BaseClassifier @@ -126,7 +126,7 @@ def score(self, X, y, sample_weight=None): if not self.pool_: return 0.0 if self.isClass is None: - if isinstance(self.pool_[0].model, ClassifierMixin): + if is_classifier(self.pool_[0].model): self.isClass = True else: self.isClass = False @@ -166,7 +166,7 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): self.elitist_.complexity_ = 99999 # Check that x and y have correct shape - if isinstance(self, BaseRegressor): + if is_regressor(self): X, y = check_X_y(X, y, dtype='float64', y_numeric=True) y = check_array(y, ensure_2d=False, dtype='float64') else: @@ -366,8 +366,6 @@ def _more_tags(self): def model_swap(self, local_model) -> Solution: solution = self.elitist_.clone() - # pool = [] for space in solution.pool: space = space.clone(model = copy.deepcopy(local_model)) - #pool.append(rule) return solution \ No newline at end of file From f1259b0378068fe07de8a9ffc6119e6022eee8d6 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 28 Jan 2025 11:55:09 +0100 Subject: [PATCH 30/41] dont fit suprb if is_fitted_ --- suprb/suprb.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/suprb/suprb.py b/suprb/suprb.py index 36de404b..ec991d47 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -6,7 +6,7 @@ from sklearn import clone from sklearn.utils import check_X_y from sklearn.utils.validation import check_is_fitted, check_array -from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator, is_classifier, is_regressor +from sklearn.base import RegressorMixin, ClassifierMixin, BaseEstimator from sklearn.metrics import mean_squared_error, accuracy_score, r2_score from .base import BaseSupervised, BaseRegressor, BaseClassifier @@ -78,6 +78,7 @@ class SupRB(BaseSupervised): n_features_in_: int logger_: BaseLogger + is_fitted_: bool = False def __init__(self, rule_generation: RuleGeneration = None, @@ -126,7 +127,7 @@ def score(self, X, y, sample_weight=None): if not self.pool_: return 0.0 if self.isClass is None: - if is_classifier(self.pool_[0].model): + if isinstance(self.pool_[0].model, ClassifierMixin): self.isClass = True else: self.isClass = False @@ -137,7 +138,7 @@ def score(self, X, y, sample_weight=None): return r2_score(y, y_pred, sample_weight=sample_weight) - def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): + def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False, force=False): """ Fit SupRB.2. Parameters @@ -156,6 +157,9 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): Returns self. """ + if self.is_fitted_ and not force: + return self + # Set these values so we gracefully exit on error self.early_stopping_counter_ = 0 self.previous_fitness_ = 0 @@ -166,7 +170,7 @@ def fit(self, X: np.ndarray, y: np.ndarray, cleanup=False): self.elitist_.complexity_ = 99999 # Check that x and y have correct shape - if is_regressor(self): + if isinstance(self, BaseRegressor): X, y = check_X_y(X, y, dtype='float64', y_numeric=True) y = check_array(y, ensure_2d=False, dtype='float64') else: @@ -366,6 +370,8 @@ def _more_tags(self): def model_swap(self, local_model) -> Solution: solution = self.elitist_.clone() + # pool = [] for space in solution.pool: space = space.clone(model = copy.deepcopy(local_model)) + #pool.append(rule) return solution \ No newline at end of file From a448d99144e6db584471b7177c307004e4b3bd35 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 30 Jan 2025 12:25:36 +0100 Subject: [PATCH 31/41] fix: handle binary matching of rules in volume_fitness --- suprb/rule/fitness.py | 1 + 1 file changed, 1 insertion(+) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index 7ea5cd64..724c2d35 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -31,6 +31,7 @@ def __init__(self, alpha: float = None): def __call__(self, rule: Rule) -> float: diff = rule.input_space[:, 1] - rule.input_space[:, 0] + diff[diff == 0] = 1 # avoid division by zero input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume From ce8bb7922c3901d09f200c3e8c1a308344e5430f Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Sat, 22 Feb 2025 13:58:17 +0100 Subject: [PATCH 32/41] apply thesholds --- suprb/optimizer/rule/acceptance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index b33878a3..4e2f9731 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -24,7 +24,7 @@ def __init__(self, max_error: float = 0.01): self.max_error = max_error def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: - return rule.score_ <= self.max_error + return rule.error_ <= self.max_error class Variance(RuleAcceptance): @@ -59,7 +59,7 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: if rule.experience_ < 1: return False local_y = y[rule.match_set_] - return rule.error_ > precision_score(local_y, rule.predict(X[rule.match_set_])) + return precision_score(local_y, rule.predict(X[rule.match_set_]), average='macro', zero_division=np.nan) >= self.min_precission class F1_Score(RuleAcceptance): """Insert if the rule has a f1_score greater or equal to a threshold""" @@ -71,4 +71,4 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: if rule.experience_ < 1: return False local_y = y[rule.match_set_] - return rule.error_ > f1_score(local_y, rule.predict(X[rule.match_set_])) \ No newline at end of file + return f1_score(local_y, rule.predict(X[rule.match_set_])) >= self.min_f1 \ No newline at end of file From 19f6ba4fe0195ecb43ab3661dbebf426cc88c32b Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Tue, 25 Feb 2025 20:55:00 +0100 Subject: [PATCH 33/41] expanded logger --- suprb/logging/default.py | 5 ++++- suprb/rule/fitness.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/suprb/logging/default.py b/suprb/logging/default.py index 41d26c0e..c192c205 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -61,9 +61,12 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): elitist = estimator.solution_composition_.elitist() log_metric("elitist_fitness", elitist.fitness_) log_metric("elitist_error", elitist.error_) + if elitist.isClass: + log_metric("elitist_accuracy", elitist.score(X, y)) + log_metric("elitist_f1", elitist.f1_score(X, y)) log_metric("elitist_complexity", elitist.complexity_) log_metric("elitist_matched", matched_training_samples(elitist.subpopulation)) - # log_metric("elitist_rules", elitist.pool) + log_metric("elitist_rules", elitist.pool) # Log performance log_metric("training_score", elitist.score(X, y)) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index 724c2d35..efc5e52b 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -31,15 +31,15 @@ def __init__(self, alpha: float = None): def __call__(self, rule: Rule) -> float: diff = rule.input_space[:, 1] - rule.input_space[:, 0] - diff[diff == 0] = 1 # avoid division by zero + diff[diff == 0] = 1.0 # avoid division by zero input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume if rule.isClass: - x1 = actual_accuracy(rule.error_) + accuracy = actual_accuracy(rule.error_) else: - x1 = pseudo_accuracy(rule.error_, beta=2) - return self.fitness_func_(alpha=self.alpha, x1 = x1, x2=volume_share) * 100 + accuracy = pseudo_accuracy(rule.error_, beta=2) + return self.fitness_func_(alpha=self.alpha, x1 = accuracy, x2=volume_share) * 100 class VolumeEmary(VolumeRuleFitness): From 9d5fcba9d9ae74ee24d560669d3e53731a743c31 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 5 Mar 2025 23:29:17 +0100 Subject: [PATCH 34/41] fix circular import --- suprb/logging/base.py | 4 ++-- suprb/logging/default.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/suprb/logging/base.py b/suprb/logging/base.py index 4a8d5116..8a406121 100644 --- a/suprb/logging/base.py +++ b/suprb/logging/base.py @@ -2,7 +2,7 @@ import numpy as np -from suprb.base import BaseComponent, BaseRegressor +from suprb.base import BaseComponent, BaseRegressor, BaseSupervised class BaseLogger(BaseComponent): @@ -24,6 +24,6 @@ def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): pass @abstractmethod - def get_elitist(self, estimator: BaseRegressor): + def get_elitist(self, estimator: BaseSupervised): """Log the final elitist""" pass diff --git a/suprb/logging/default.py b/suprb/logging/default.py index c192c205..8484a4d9 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -6,7 +6,7 @@ from . import BaseLogger from .metrics import matched_training_samples, genome_diversity from .. import json as suprb_json -from suprb.base import BaseRegressor +from suprb.base import BaseRegressor, BaseSupervised class DefaultLogger(BaseLogger): @@ -71,7 +71,7 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): # Log performance log_metric("training_score", elitist.score(X, y)) - def get_elitist(self, estimator: BaseRegressor): + def get_elitist(self, estimator: BaseSupervised): json_data = {} suprb_json._save_pool(estimator.solution_composition_.elitist().pool, json_data) return json_data From dc32e8c7325ad17f927c97f5c7d60152b4412e0a Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Thu, 6 Mar 2025 10:35:18 +0100 Subject: [PATCH 35/41] fix model swapping --- suprb/suprb.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/suprb/suprb.py b/suprb/suprb.py index ec991d47..b253ae81 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -368,10 +368,19 @@ def _more_tags(self): 'poor_score': True, } - def model_swap(self, local_model) -> Solution: - solution = self.elitist_.clone() - # pool = [] - for space in solution.pool: - space = space.clone(model = copy.deepcopy(local_model)) - #pool.append(rule) - return solution \ No newline at end of file + def model_swap(self, local_model): + solution = self.elitist_ + pool = [] + for old_rule in solution.pool: + rule = old_rule.clone(model = copy.deepcopy(local_model)) + rule.isClass = old_rule.isClass + pool.append(rule) + solution.pool = pool + self.is_fitted_ = True + return solution + + def model_swap_fit(self, local_model, X, y): + solution = self.model_swap(local_model) + for rule in solution.pool: + rule.fit(X, y) + solution.fit(X, y, cache=False) \ No newline at end of file From 70bbcbaea72ccdd9a3330018aa2ec2bbb49fb9e9 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Mon, 10 Mar 2025 14:06:27 +0100 Subject: [PATCH 36/41] cleanup BaseSupervised --- examples/classification_smoke.py | 23 +++++++------ suprb/base.py | 57 -------------------------------- suprb/logging/base.py | 8 ++--- suprb/logging/combination.py | 10 +++--- suprb/logging/default.py | 8 ++--- suprb/rule/initialization.py | 2 +- suprb/solution/base.py | 4 +-- 7 files changed, 28 insertions(+), 84 deletions(-) diff --git a/examples/classification_smoke.py b/examples/classification_smoke.py index a27c6951..66b5db6e 100644 --- a/examples/classification_smoke.py +++ b/examples/classification_smoke.py @@ -1,13 +1,15 @@ from ucimlrepo import fetch_ucirepo import sklearn import numpy as np +import pandas as pd -from sklearn.preprocessing import StandardScaler, MinMaxScaler +from sklearn.preprocessing import StandardScaler, MinMaxScaler, OneHotEncoder from sklearn.model_selection import cross_validate, train_test_split from sklearn.linear_model import Ridge, LogisticRegression from sklearn.utils import shuffle import suprb +from sklearn.compose import make_column_transformer from suprb.utils import check_random_state from suprb.optimizer.rule.es import ES1xLambda from suprb.optimizer.rule.acceptance import MaxError @@ -19,7 +21,7 @@ if __name__ == '__main__': random_state = 125 - + local_model = LogisticRegression(penalty='l1', C=0.1, random_state=random_state, solver='saga', tol=0.001, max_iter=1000) #CLASSIFICATION # fetch dataset iris = fetch_ucirepo(id=53) @@ -28,35 +30,34 @@ X, y = shuffle(X, y, random_state=random_state) unique = np.unique(y) toNum = dict(zip(unique, range(1, len(unique)+1))) - # targets = {"Iris-setosa": 1, 'Iris-versicolor': 2, 'Iris-virginica': 3} - # Conversion to int required for mixing, but possibly has unwanted sideeffects + # Conversion of tragets to int required for mixing + # Similiar to sklearn.preprocessing.OrdinalEncoder y = [toNum[x[0]] for x in y] X = MinMaxScaler(feature_range=(-1, 1)).fit_transform(X) - #y = StandardScaler().fit_transform(y.reshape((-1, 1))).reshape((-1,)) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=random_state) # Comparable with examples/example_2.py model = SupRBWrapper(print_config=True, - + #n_iter=10, ## RULE GENERATION ## rule_generation=ES1xLambda(), - rule_generation__n_iter=100, + rule_generation__n_iter=16, rule_generation__lmbda=16, rule_generation__operator='+', rule_generation__delay=10, rule_generation__random_state=random_state, - rule_generation__n_jobs=1, - rule_generation__init__model=LogisticRegression(), + rule_generation__n_jobs=4, + rule_generation__init__model=local_model, ## SOLUTION COMPOSITION ## solution_composition=GeneticAlgorithm(), solution_composition__init__mixing=suprb.solution.mixing_model.ErrorExperienceClassification(), - solution_composition__n_iter=100, + solution_composition__n_iter=32, solution_composition__population_size=32, solution_composition__elitist_ratio=0.2, solution_composition__random_state=random_state, - solution_composition__n_jobs=1) + solution_composition__n_jobs=4) scores = cross_validate(model, X, y, cv=4, n_jobs=4, verbose=10, scoring=['accuracy'], diff --git a/suprb/base.py b/suprb/base.py index 362cd1aa..3ef60d7c 100644 --- a/suprb/base.py +++ b/suprb/base.py @@ -68,7 +68,6 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> BaseSupervised: self : BaseEstimator Returns self. """ - pass @abstractmethod @@ -98,38 +97,10 @@ class BaseRegressor(BaseSupervised, RegressorMixin, metaclass=ABCMeta): @abstractmethod def fit(self, X: np.ndarray, y: np.ndarray) -> BaseRegressor: - """ A reference implementation of a fitting function. - - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - The training input samples. - y : array-like, shape (n_samples,) or (n_samples, n_outputs) - The target values. - - Returns - ------- - self : BaseEstimator - Returns self. - """ - pass @abstractmethod def predict(self, X: np.ndarray): - """ A reference implementation of a predicting function. - - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - The training input samples. - - Returns - ------- - y : np.ndarray - Returns the estimation with shape (n_samples,). - """ - pass @@ -140,38 +111,10 @@ class BaseClassifier(BaseSupervised, ClassifierMixin, metaclass=ABCMeta): @abstractmethod def fit(self, X: np.ndarray, y: np.ndarray) -> BaseClassifier: - """ A reference implementation of a fitting function. - - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - The training input samples. - y : array-like, shape (n_samples,) or (n_samples, n_outputs) - The target values. - - Returns - ------- - self : BaseEstimator - Returns self. - """ - pass @abstractmethod def predict(self, X: np.ndarray): - """ A reference implementation of a predicting function. - - Parameters - ---------- - X : {array-like, sparse matrix}, shape (n_samples, n_features) - The training input samples. - - Returns - ------- - y : np.ndarray - Returns the estimation with shape (n_samples,). - """ - pass class SupervisedMixin(ABC): diff --git a/suprb/logging/base.py b/suprb/logging/base.py index 8a406121..267e304a 100644 --- a/suprb/logging/base.py +++ b/suprb/logging/base.py @@ -2,24 +2,24 @@ import numpy as np -from suprb.base import BaseComponent, BaseRegressor, BaseSupervised +from suprb.base import BaseComponent, BaseSupervised class BaseLogger(BaseComponent): """The base class for loggers.""" @abstractmethod - def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): """Logs initial parameters, before any iteration or fitting has taken place.""" pass @abstractmethod - def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor, iteration: int): + def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised, iteration: int): """Logs an iteration of the estimator. May actually compute errors or scores, depending on the state.""" pass @abstractmethod - def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): """Log the final state of the estimator. It is assumed that the fitting process is already completed.""" pass diff --git a/suprb/logging/combination.py b/suprb/logging/combination.py index 7e3ed934..d6f1ab8a 100644 --- a/suprb/logging/combination.py +++ b/suprb/logging/combination.py @@ -3,7 +3,7 @@ import numpy as np from sklearn import clone -from suprb.base import BaseRegressor +from suprb.base import BaseSupervised from . import BaseLogger @@ -16,7 +16,7 @@ def __init__(self, loggers: list[tuple[str, BaseLogger]]): """An unique name for every logger must be supplied, such that the parameter get/set are well-defined.""" self.loggers = loggers - def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): if any(map(lambda logger: isinstance(logger, CombinedLogger), self.loggers)): warnings.warn("Nesting loggers is not recommended. Please add all loggers to this top-level logger.") @@ -25,11 +25,11 @@ def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): for _, logger in self.loggers_: logger.log_init(X=X, y=y, estimator=estimator) - def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor, iteration: int): + def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised, iteration: int): for _, logger in self.loggers_: logger.log_iteration(X=X, y=y, estimator=estimator, iteration=iteration) - def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): for _, logger in self.loggers_: logger.log_final(X=X, y=y, estimator=estimator) @@ -71,5 +71,5 @@ def _replace_logger(self, name: str, new_val: BaseLogger): break self.loggers = new_loggers - def get_elitist(self, estimator: BaseRegressor): + def get_elitist(self, estimator: BaseSupervised): pass diff --git a/suprb/logging/default.py b/suprb/logging/default.py index 8484a4d9..4242e007 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -6,7 +6,7 @@ from . import BaseLogger from .metrics import matched_training_samples, genome_diversity from .. import json as suprb_json -from suprb.base import BaseRegressor, BaseSupervised +from suprb.base import BaseSupervised class DefaultLogger(BaseLogger): @@ -25,13 +25,13 @@ def log_params(self, **kwargs): for key, value in kwargs.items(): self.log_param(key=key, value=value) - def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_init(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): self.params_ = {} self.metrics_ = defaultdict(dict) self.log_params(**estimator.get_params()) - def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor, iteration: int): + def log_iteration(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised, iteration: int): def log_metric(key, value): self.log_metric(key=key, value=value, step=estimator.step_) @@ -76,5 +76,5 @@ def get_elitist(self, estimator: BaseSupervised): suprb_json._save_pool(estimator.solution_composition_.elitist().pool, json_data) return json_data - def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseRegressor): + def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): pass diff --git a/suprb/rule/initialization.py b/suprb/rule/initialization.py index a147dc1d..a5051ee7 100644 --- a/suprb/rule/initialization.py +++ b/suprb/rule/initialization.py @@ -6,7 +6,7 @@ from sklearn.linear_model import LinearRegression from sklearn.linear_model import Ridge -from suprb.base import BaseComponent, BaseSupervised +from suprb.base import BaseComponent from suprb.rule.matching import MatchingFunction, OrderedBound, UnorderedBound, CenterSpread, MinPercentage from suprb.utils import check_random_state, RandomState from . import Rule, RuleFitness diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 39be64a8..4fa4bd7e 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -62,8 +62,8 @@ def score(self, X, y, sample_weight=None): return r2_score(y, y_pred, sample_weight=sample_weight) - def fit(self, X: np.ndarray, y: np.ndarray) -> Solution: - pred = self.predict(X, cache=True) + def fit(self, X: np.ndarray, y: np.ndarray, cache = True) -> Solution: + pred = self.predict(X, cache=cache) if not self.pool: self.error_ = 9999 else: From 0c9db61c7a2603d9c05774cc5fbf8b5c57e56e7e Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 12 Mar 2025 16:46:10 +0100 Subject: [PATCH 37/41] naming isClassifier --- suprb/logging/default.py | 4 ++-- suprb/optimizer/rule/acceptance.py | 2 +- suprb/rule/base.py | 10 +++++----- suprb/rule/fitness.py | 4 ++-- suprb/rule/initialization.py | 4 ++-- suprb/solution/base.py | 18 +++++++++--------- suprb/solution/fitness.py | 2 +- suprb/suprb.py | 12 ++++++------ suprb/{json.py => suprb_json.py} | 0 9 files changed, 28 insertions(+), 28 deletions(-) rename suprb/{json.py => suprb_json.py} (100%) diff --git a/suprb/logging/default.py b/suprb/logging/default.py index 4242e007..98c80070 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -5,7 +5,7 @@ from . import BaseLogger from .metrics import matched_training_samples, genome_diversity -from .. import json as suprb_json +from .. import suprb_json from suprb.base import BaseSupervised @@ -61,7 +61,7 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): elitist = estimator.solution_composition_.elitist() log_metric("elitist_fitness", elitist.fitness_) log_metric("elitist_error", elitist.error_) - if elitist.isClass: + if elitist.isClassifierifier: log_metric("elitist_accuracy", elitist.score(X, y)) log_metric("elitist_f1", elitist.f1_score(X, y)) log_metric("elitist_complexity", elitist.complexity_) diff --git a/suprb/optimizer/rule/acceptance.py b/suprb/optimizer/rule/acceptance.py index 4e2f9731..222ba463 100644 --- a/suprb/optimizer/rule/acceptance.py +++ b/suprb/optimizer/rule/acceptance.py @@ -41,7 +41,7 @@ def __call__(self, rule: Rule, X: np.ndarray, y: np.ndarray) -> bool: return False local_y = y[rule.match_set_] default_error = np.sum(local_y ** 2) / (len(local_y) * self.beta) - if rule.isClass: + if rule.isClassifier: # default error is the trivial solution of always choosing the most common label local_y = [round(y) for y in local_y] default_accuracy = np.bincount(local_y).max() / (len(local_y) * self.beta) diff --git a/suprb/rule/base.py b/suprb/rule/base.py index 962c1105..3b4e5344 100644 --- a/suprb/rule/base.py +++ b/suprb/rule/base.py @@ -44,9 +44,9 @@ def __init__(self, match: MatchingFunction, input_space: np.ndarray, model: Regr self.model = model self.fitness = fitness if isinstance(model, ClassifierMixin): - self.isClass = True + self.isClassifier = True else: - self.isClass = False + self.isClassifier = False def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: @@ -75,7 +75,7 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: X, y = X[self.match_set_], y[self.match_set_] # No reason to fit if only one label in match_set - if self.isClass: + if self.isClassifier: if len(np.unique(y)) == 1: self.pred_ = y[0] self.error_ = 1 @@ -88,9 +88,9 @@ def fit(self, X: np.ndarray, y: np.ndarray) -> Rule: self.model.fit(X, y) self.pred_ = self.model.predict(X) - if not self.isClass: + if not self.isClassifier: self.error_ = max(mean_squared_error(y, self.pred_), 1e-4) # TODO: make min a parameter? - elif self.isClass: + elif self.isClassifier: self.error_ = max(pseudo_error(accuracy_score(y, self.pred_)), 1e-4) self.fitness_ = self.fitness(self) self.experience_ = float(X.shape[0]) diff --git a/suprb/rule/fitness.py b/suprb/rule/fitness.py index efc5e52b..93e14c8c 100644 --- a/suprb/rule/fitness.py +++ b/suprb/rule/fitness.py @@ -16,7 +16,7 @@ class PseudoAccuracy(RuleFitness): def __call__(self, rule: Rule) -> float: # note that we multiply solely for readability reasons without # any performance impact - if rule.isClass: + if rule.isClassifier: return actual_accuracy(rule.error_) * 100 return pseudo_accuracy(rule.error_) * 100 @@ -35,7 +35,7 @@ def __call__(self, rule: Rule) -> float: input_space_volume = np.prod(diff) volume_share = rule.volume_ / input_space_volume - if rule.isClass: + if rule.isClassifier: accuracy = actual_accuracy(rule.error_) else: accuracy = pseudo_accuracy(rule.error_, beta=2) diff --git a/suprb/rule/initialization.py b/suprb/rule/initialization.py index a5051ee7..1cc763be 100644 --- a/suprb/rule/initialization.py +++ b/suprb/rule/initialization.py @@ -35,9 +35,9 @@ def __init__(self, bounds: np.ndarray = None, self._validate_components(model=Ridge(alpha=0.01), fitness=VolumeWu()) if isinstance(model, ClassifierMixin): - self.isClass = True + self.isClassifier = True else: - self.isClass = False + self.isClassifier = False @property def matching_type(self): diff --git a/suprb/solution/base.py b/suprb/solution/base.py index 4fa4bd7e..67278707 100644 --- a/suprb/solution/base.py +++ b/suprb/solution/base.py @@ -45,17 +45,17 @@ def __init__(self, genome: np.ndarray, pool: list[Rule], mixing: MixingModel, fi self.pool = pool self.mixing = mixing self.fitness = fitness - self.isClass = None + self.isClassifier = None def score(self, X, y, sample_weight=None): if not self.pool: return 0.0 - if self.isClass is None: + if self.isClassifier is None: if isinstance(self.pool[0].model, ClassifierMixin): - self.isClass = True + self.isClassifier = True else: - self.isClass = False - if self.isClass: + self.isClassifier = False + if self.isClassifier: return accuracy_score(y, self.predict(X), sample_weight=sample_weight) else: y_pred = self.predict(X) @@ -67,12 +67,12 @@ def fit(self, X: np.ndarray, y: np.ndarray, cache = True) -> Solution: if not self.pool: self.error_ = 9999 else: - if self.isClass is None: + if self.isClassifier is None: if isinstance(self.pool[0].model, ClassifierMixin): - self.isClass = True + self.isClassifier = True else: - self.isClass = False - if self.isClass: + self.isClassifier = False + if self.isClassifier: self.error_ = max(pseudo_error(accuracy_score(y, pred)), 1e-4) else: self.error_ = max(mean_squared_error(y, pred), 1e-4) diff --git a/suprb/solution/fitness.py b/suprb/solution/fitness.py index 27a368ea..7fbc2292 100644 --- a/suprb/solution/fitness.py +++ b/suprb/solution/fitness.py @@ -29,7 +29,7 @@ def __init__(self, alpha: float): self.alpha = alpha def __call__(self, solution: Solution) -> float: - acc = actual_accuracy(solution.error_) if solution.isClass else pseudo_accuracy(solution.error_) + acc = actual_accuracy(solution.error_) if solution.isClassifier else pseudo_accuracy(solution.error_) return self.fitness_func_(self.alpha, acc, c_norm(solution.complexity_, self.max_genome_length_)) * 100 diff --git a/suprb/suprb.py b/suprb/suprb.py index b253ae81..d54963c7 100644 --- a/suprb/suprb.py +++ b/suprb/suprb.py @@ -106,7 +106,7 @@ def __init__(self, self.n_jobs = n_jobs self.early_stopping_patience = early_stopping_patience self.early_stopping_delta = early_stopping_delta - self.isClass = None + self.isClassifier = None def check_early_stopping(self): if self.early_stopping_patience > 0: @@ -126,12 +126,12 @@ def check_early_stopping(self): def score(self, X, y, sample_weight=None): if not self.pool_: return 0.0 - if self.isClass is None: + if self.isClassifier is None: if isinstance(self.pool_[0].model, ClassifierMixin): - self.isClass = True + self.isClassifier = True else: - self.isClass = False - if self.isClass: + self.isClassifier = False + if self.isClassifier: return accuracy_score(y, self.predict(X), sample_weight=sample_weight) else: y_pred = self.predict(X) @@ -373,7 +373,7 @@ def model_swap(self, local_model): pool = [] for old_rule in solution.pool: rule = old_rule.clone(model = copy.deepcopy(local_model)) - rule.isClass = old_rule.isClass + rule.isClassifier = old_rule.isClassifier pool.append(rule) solution.pool = pool self.is_fitted_ = True diff --git a/suprb/json.py b/suprb/suprb_json.py similarity index 100% rename from suprb/json.py rename to suprb/suprb_json.py From 24add375709715c26c2e3abf3f9e5a23ae1887e2 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 12 Mar 2025 17:02:53 +0100 Subject: [PATCH 38/41] rename json --- suprb/{suprb_json.py => json.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename suprb/{suprb_json.py => json.py} (100%) diff --git a/suprb/suprb_json.py b/suprb/json.py similarity index 100% rename from suprb/suprb_json.py rename to suprb/json.py From ae9e40d736c840841fb0e54350e50c561149d5ad Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 12 Mar 2025 17:04:15 +0100 Subject: [PATCH 39/41] comply with rename --- suprb/logging/default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/suprb/logging/default.py b/suprb/logging/default.py index 98c80070..9c5b3045 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -5,7 +5,7 @@ from . import BaseLogger from .metrics import matched_training_samples, genome_diversity -from .. import suprb_json +from .. import json from suprb.base import BaseSupervised @@ -73,7 +73,7 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): def get_elitist(self, estimator: BaseSupervised): json_data = {} - suprb_json._save_pool(estimator.solution_composition_.elitist().pool, json_data) + json._save_pool(estimator.solution_composition_.elitist().pool, json_data) return json_data def log_final(self, X: np.ndarray, y: np.ndarray, estimator: BaseSupervised): From 92291ba1f2bb47f6b199335c79f8c09f60840c2e Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 12 Mar 2025 17:42:42 +0100 Subject: [PATCH 40/41] fix: rename --- suprb/logging/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/suprb/logging/default.py b/suprb/logging/default.py index 9c5b3045..d2c85da4 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -61,7 +61,7 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): elitist = estimator.solution_composition_.elitist() log_metric("elitist_fitness", elitist.fitness_) log_metric("elitist_error", elitist.error_) - if elitist.isClassifierifier: + if elitist.isClassifier: log_metric("elitist_accuracy", elitist.score(X, y)) log_metric("elitist_f1", elitist.f1_score(X, y)) log_metric("elitist_complexity", elitist.complexity_) From 8e6bbbbe620cfaaee475a8f80ae3660306861c04 Mon Sep 17 00:00:00 2001 From: wehrfabi Date: Wed, 12 Mar 2025 17:55:12 +0100 Subject: [PATCH 41/41] fix: faulty f1-score logging --- suprb/logging/default.py | 1 - 1 file changed, 1 deletion(-) diff --git a/suprb/logging/default.py b/suprb/logging/default.py index d2c85da4..d414231c 100644 --- a/suprb/logging/default.py +++ b/suprb/logging/default.py @@ -63,7 +63,6 @@ def log_metric_min_max_mean(metric_name: str, attribute_name: str, lst: list): log_metric("elitist_error", elitist.error_) if elitist.isClassifier: log_metric("elitist_accuracy", elitist.score(X, y)) - log_metric("elitist_f1", elitist.f1_score(X, y)) log_metric("elitist_complexity", elitist.complexity_) log_metric("elitist_matched", matched_training_samples(elitist.subpopulation)) log_metric("elitist_rules", elitist.pool)