Module likelihood.models.ensemble
Classes
class EnsembleClassifier (n_models: int = 5,
base_model_class: Type | None = None,
param_ranges: Dict[str, Any] | None = None,
seed_range: Tuple[int, int] = (0, 100),
voting_method: str = 'soft',
verbose: int = 0)-
Expand source code
class EnsembleClassifier(BaseEstimator, ClassifierMixin): """ An ensemble of AutoClassifier models with different hyperparameters and random seeds. Parameters ---------- n_models : `int` Number of models in the ensemble. Default is `5`. base_model_class : `class` The classifier model class to use (e.g., `AutoClassifier`). Default is `AutoClassifier`. param_ranges : `dict`, `optional` Dictionary specifying ranges for hyperparameter variation: - `units`: list of integers or tuple `(min, max)` - `activation`: list of activation function names - `num_layers`: list of integers or tuple `(min, max)` - `dropout`: list of floats or tuple `(min, max)` - `l2_reg`: list of floats or tuple `(min, max)` If `None`, default values are used. seed_range : `tuple`, `optional` Range for random seeds as (start, end). Default is `(0, 100)`. voting_method : `str`, `optional` Method to combine predictions: `soft` (average probabilities) or `hard` (majority vote). Default is `soft`. verbose : `int`, `optional` Verbosity level. `0` = silent, `1` = progress bar. Default is `0`. Attributes ---------- models_ : `list` List of trained AutoClassifier instances. model_params_ : `list` List of parameter dictionaries used for each model. n_models_ : `int` Actual number of models created and trained. Methods ------- `fit(X, y)` Train all ensemble members on the provided data. `predict(X)` Return predicted class labels using the voting method. `predict_proba(X)` Return probability estimates for each class (soft voting only). `get_model_scores()` Retrieve individual model performance metrics if available. Examples -------- >>> ensemble = EnsembleClassifier( ... n_models=5, ... param_ranges={'units': (10, 20), 'activation': ['selu', 'relu']}, ... voting_method='soft' ... ) >>> ensemble.fit(X_train, y_train) >>> predictions = ensemble.predict(X_test) """ def __init__( self, n_models: int = 5, base_model_class: Optional[Type] = None, param_ranges: Optional[Dict[str, Any]] = None, seed_range: Tuple[int, int] = (0, 100), voting_method: str = "soft", verbose: int = 0, ) -> None: if base_model_class is None: from likelihood.models.deep import AutoClassifier base_model_class = AutoClassifier self.n_models = n_models self.base_model_class = base_model_class self.param_ranges = param_ranges or {} self.seed_range = seed_range self.voting_method = voting_method self.verbose = verbose # Initialize internal attributes self.scores: List[Dict[str, Any]] = [] self.configs: List[Dict[str, Any]] = [] self.models_: List[Any] = [] self.model_params_: List[Dict[str, Any]] = [] self.all_history: List[Any] = [] self.n_models_: int = 0 def _generate_model_configs(self) -> List[Dict]: """Generate unique configurations for each model in the ensemble.""" self.configs = [] default_ranges = { "units": [17], "activation": ["selu"], "num_layers": [1], "dropout": [None], "l2_reg": [0.0], } param_ranges = self.param_ranges or default_ranges def sample_param(name: str, integer: bool = False) -> int: values = param_ranges.get(name, default_ranges[name]) if isinstance(values, tuple): low, high = values if integer: return np.random.randint(low, high + 1) else: return np.random.uniform(low, high) return np.random.choice(values) for _ in range(self.n_models): config = { "units": sample_param("units", integer=True), "activation": sample_param("activation"), "num_layers": sample_param("num_layers", integer=True), "dropout": sample_param("dropout"), "l2_reg": sample_param("l2_reg"), "seed": np.random.randint(*self.seed_range), } self.configs.append(config) return self.configs def fit(self, X: np.ndarray, y: np.ndarray, **fit_kwargs) -> "EnsembleClassifier": """Train all models in the ensemble.""" self.models_ = [] self.all_history = [] self.model_params_ = [] configs = self.configs or self._generate_model_configs() for i, config in enumerate(configs): if self.verbose > 0 and (i + 1) % max(1, len(configs) // 5) == 0: print(f"Training model {i+1}/{len(configs)}...") tf.random.set_seed(config["seed"]) np.random.seed(config["seed"]) try: model = self.base_model_class( input_shape_parm=X.shape[1], num_classes=y.shape[1] if len(y.shape) > 1 else y.max() + 1, units=config["units"], activation=config["activation"], l2_reg=config["l2_reg"], num_layers=config["num_layers"], dropout=config["dropout"], ) model.compile( optimizer="adam", loss=tf.keras.losses.CategoricalCrossentropy(), metrics=[tf.keras.metrics.F1Score(threshold=0.5, average="micro")], ) history = model.fit(X, y, verbose=0, **fit_kwargs) self.models_.append(model) self.all_history.append(history) self.model_params_.append(config) except Exception as e: print(f"Warning: Model {i+1} failed to train. Error: {e}") self.n_models_ = len(self.models_) if self.verbose > 0: print(f"Ensemble trained with {self.n_models_} models.") return self def predict_proba(self, X: np.ndarray) -> np.ndarray: """Return probability estimates for each class (soft voting).""" all_probs = [] for model in self.models_: probs = model.predict(X, verbose=0) if len(probs.shape) == 1: probs = tf.nn.softmax(probs).numpy() all_probs.append(probs) return np.mean(all_probs, axis=0) def predict(self, X: np.ndarray) -> np.ndarray: """Return predicted class labels.""" if self.voting_method == "soft": probs = self.predict_proba(X) return np.argmax(probs, axis=1) elif self.voting_method == "hard": all_preds = [] for model in self.models_: preds = model.predict(X, verbose=0) if len(preds.shape) > 1: preds = np.argmax(preds, axis=1) else: preds = (preds >= 0.5).astype(int) all_preds.append(preds.reshape(-1, 1)) stacked = np.hstack(all_preds) return np.apply_along_axis(lambda x: np.bincount(x).argmax(), axis=1, arr=stacked) else: raise ValueError("voting_method must be 'soft' or 'hard'.") def get_model_scores(self) -> List[Dict[str, Any]]: """Return performance metrics for each model (if available).""" if hasattr(self, "all_history"): self.scores = [] for i, model in enumerate(self.models_): if hasattr(model, "history"): loss = self.all_history[i].history.get("loss", [None])[0] val_loss = self.all_history[i].history.get("val_loss", [None])[0] f1 = self.all_history[i].history.get("f1_score", [None])[0] val_f1 = self.all_history[i].history.get("val_f1_score", [None])[0] self.scores.append( { "model_id": i + 1, "params": self.model_params_[i], "loss": loss, "val_loss": val_loss, "f1_score": f1, "val_f1_score": val_f1, } ) return self.scores def _save_keras_model(self, model: Any, path: str) -> None: """ Internal helper to save a Keras model depending on TF version. """ is_updated = version.parse(tf.__version__) > version.parse("2.15.0") if is_updated: model.save(f"{path}") else: model.save(path, save_format="tf") def save(self, filepath: str) -> None: """ Save ensemble metadata and models inside a directory named after the last part of filepath. """ filepath = filepath.rstrip(".pkl") parent_dir = os.path.dirname(filepath) folder_name = os.path.basename(filepath) save_dir = os.path.join(parent_dir, folder_name) os.makedirs(save_dir, exist_ok=True) self.get_model_scores() save_dict = { "n_models": self.n_models, "param_ranges": self.param_ranges, "seed_range": self.seed_range, "voting_method": self.voting_method, "verbose": self.verbose, "configs": self.configs, "model_params_": self.model_params_, "scores": self.scores, "n_models_": self.n_models_, } with open(os.path.join(save_dir, "meta.json"), "w") as f: json.dump(save_dict, f, indent=4) for i, model in enumerate(self.models_): self._save_keras_model(model, os.path.join(save_dir, f"model_{i}")) @classmethod def load(cls, filepath: str, base_model_class: Optional[Type] = None) -> "EnsembleClassifier": filepath = filepath.rstrip(".pkl") parent_dir = os.path.dirname(filepath) folder_name = os.path.basename(filepath) save_dir = os.path.join(parent_dir, folder_name) with open(os.path.join(save_dir, "meta.json"), "r") as f: save_dict = json.load(f) ensemble = cls.__new__(cls) for key, value in save_dict.items(): setattr(ensemble, key, value) ensemble.models_ = [] if base_model_class is None: from likelihood.models.deep import AutoClassifier base_model_class = AutoClassifier def _load_keras_model(path: str) -> Any: """ Internal helper to load a Keras model depending on TF version. """ is_updated = version.parse(tf.__version__) > version.parse("2.15.0") if is_updated: return base_model_class.load(path) else: return tf.keras.models.load_model(path) for i in range(ensemble.n_models_): model_path = os.path.join(save_dir, f"model_{i}") model = _load_keras_model(model_path) ensemble.models_.append(model) ensemble.base_model_class = base_model_class return ensemble def __repr__(self) -> str: return ( f"EnsembleClassifier(n_models={self.n_models}, " f"voting_method='{self.voting_method}', " f"trained_models={self.n_models_})" )An ensemble of AutoClassifier models with different hyperparameters and random seeds.
Parameters
n_models:int- Number of models in the ensemble. Default is
5. base_model_class:class- The classifier model class to use (e.g.,
AutoClassifier). Default isAutoClassifier. param_ranges:dict,optional- Dictionary specifying ranges for hyperparameter variation:
-
units: list of integers or tuple(min, max)-activation: list of activation function names -num_layers: list of integers or tuple(min, max)-dropout: list of floats or tuple(min, max)-l2_reg: list of floats or tuple(min, max)IfNone, default values are used. seed_range:tuple,optional- Range for random seeds as (start, end). Default is
(0, 100). voting_method:str,optional- Method to combine predictions:
soft(average probabilities) orhard(majority vote). Default issoft. verbose:int,optional- Verbosity level.
0= silent,1= progress bar. Default is0.
Attributes
models_:list- List of trained AutoClassifier instances.
model_params_:list- List of parameter dictionaries used for each model.
n_models_:int- Actual number of models created and trained.
Methods
fit(X, y)Train all ensemble members on the provided data.predict(X)Return predicted class labels using the voting method.predict_proba(X)Return probability estimates for each class (soft voting only).get_model_scores()Retrieve individual model performance metrics if available.Examples
>>> ensemble = EnsembleClassifier( ... n_models=5, ... param_ranges={'units': (10, 20), 'activation': ['selu', 'relu']}, ... voting_method='soft' ... ) >>> ensemble.fit(X_train, y_train) >>> predictions = ensemble.predict(X_test)Ancestors
- sklearn.base.BaseEstimator
- sklearn.utils._repr_html.base.ReprHTMLMixin
- sklearn.utils._repr_html.base._HTMLDocumentationLinkMixin
- sklearn.utils._metadata_requests._MetadataRequester
- sklearn.base.ClassifierMixin
Static methods
def load(filepath: str, base_model_class: Type | None = None) ‑> EnsembleClassifier
Methods
def fit(self, X: numpy.ndarray, y: numpy.ndarray, **fit_kwargs) ‑> EnsembleClassifier-
Expand source code
def fit(self, X: np.ndarray, y: np.ndarray, **fit_kwargs) -> "EnsembleClassifier": """Train all models in the ensemble.""" self.models_ = [] self.all_history = [] self.model_params_ = [] configs = self.configs or self._generate_model_configs() for i, config in enumerate(configs): if self.verbose > 0 and (i + 1) % max(1, len(configs) // 5) == 0: print(f"Training model {i+1}/{len(configs)}...") tf.random.set_seed(config["seed"]) np.random.seed(config["seed"]) try: model = self.base_model_class( input_shape_parm=X.shape[1], num_classes=y.shape[1] if len(y.shape) > 1 else y.max() + 1, units=config["units"], activation=config["activation"], l2_reg=config["l2_reg"], num_layers=config["num_layers"], dropout=config["dropout"], ) model.compile( optimizer="adam", loss=tf.keras.losses.CategoricalCrossentropy(), metrics=[tf.keras.metrics.F1Score(threshold=0.5, average="micro")], ) history = model.fit(X, y, verbose=0, **fit_kwargs) self.models_.append(model) self.all_history.append(history) self.model_params_.append(config) except Exception as e: print(f"Warning: Model {i+1} failed to train. Error: {e}") self.n_models_ = len(self.models_) if self.verbose > 0: print(f"Ensemble trained with {self.n_models_} models.") return selfTrain all models in the ensemble.
def get_model_scores(self) ‑> List[Dict[str, Any]]-
Expand source code
def get_model_scores(self) -> List[Dict[str, Any]]: """Return performance metrics for each model (if available).""" if hasattr(self, "all_history"): self.scores = [] for i, model in enumerate(self.models_): if hasattr(model, "history"): loss = self.all_history[i].history.get("loss", [None])[0] val_loss = self.all_history[i].history.get("val_loss", [None])[0] f1 = self.all_history[i].history.get("f1_score", [None])[0] val_f1 = self.all_history[i].history.get("val_f1_score", [None])[0] self.scores.append( { "model_id": i + 1, "params": self.model_params_[i], "loss": loss, "val_loss": val_loss, "f1_score": f1, "val_f1_score": val_f1, } ) return self.scoresReturn performance metrics for each model (if available).
def predict(self, X: numpy.ndarray) ‑> numpy.ndarray-
Expand source code
def predict(self, X: np.ndarray) -> np.ndarray: """Return predicted class labels.""" if self.voting_method == "soft": probs = self.predict_proba(X) return np.argmax(probs, axis=1) elif self.voting_method == "hard": all_preds = [] for model in self.models_: preds = model.predict(X, verbose=0) if len(preds.shape) > 1: preds = np.argmax(preds, axis=1) else: preds = (preds >= 0.5).astype(int) all_preds.append(preds.reshape(-1, 1)) stacked = np.hstack(all_preds) return np.apply_along_axis(lambda x: np.bincount(x).argmax(), axis=1, arr=stacked) else: raise ValueError("voting_method must be 'soft' or 'hard'.")Return predicted class labels.
def predict_proba(self, X: numpy.ndarray) ‑> numpy.ndarray-
Expand source code
def predict_proba(self, X: np.ndarray) -> np.ndarray: """Return probability estimates for each class (soft voting).""" all_probs = [] for model in self.models_: probs = model.predict(X, verbose=0) if len(probs.shape) == 1: probs = tf.nn.softmax(probs).numpy() all_probs.append(probs) return np.mean(all_probs, axis=0)Return probability estimates for each class (soft voting).
def save(self, filepath: str) ‑> None-
Expand source code
def save(self, filepath: str) -> None: """ Save ensemble metadata and models inside a directory named after the last part of filepath. """ filepath = filepath.rstrip(".pkl") parent_dir = os.path.dirname(filepath) folder_name = os.path.basename(filepath) save_dir = os.path.join(parent_dir, folder_name) os.makedirs(save_dir, exist_ok=True) self.get_model_scores() save_dict = { "n_models": self.n_models, "param_ranges": self.param_ranges, "seed_range": self.seed_range, "voting_method": self.voting_method, "verbose": self.verbose, "configs": self.configs, "model_params_": self.model_params_, "scores": self.scores, "n_models_": self.n_models_, } with open(os.path.join(save_dir, "meta.json"), "w") as f: json.dump(save_dict, f, indent=4) for i, model in enumerate(self.models_): self._save_keras_model(model, os.path.join(save_dir, f"model_{i}"))Save ensemble metadata and models inside a directory named after the last part of filepath.
def set_score_request(self: EnsembleClassifier,
*,
sample_weight: bool | str | None = '$UNCHANGED$') ‑> EnsembleClassifier-
Expand source code
def func(*args, **kw): """Updates the `_metadata_request` attribute of the consumer (`instance`) for the parameters provided as `**kw`. This docstring is overwritten below. See REQUESTER_DOC for expected functionality. """ if not _routing_enabled(): raise RuntimeError( "This method is only available when metadata routing is enabled." " You can enable it using" " sklearn.set_config(enable_metadata_routing=True)." ) if self.validate_keys and (set(kw) - set(self.keys)): raise TypeError( f"Unexpected args: {set(kw) - set(self.keys)} in {self.name}. " f"Accepted arguments are: {set(self.keys)}" ) # This makes it possible to use the decorated method as an unbound method, # for instance when monkeypatching. # https://github.com/scikit-learn/scikit-learn/issues/28632 if instance is None: _instance = args[0] args = args[1:] else: _instance = instance # Replicating python's behavior when positional args are given other than # `self`, and `self` is only allowed if this method is unbound. if args: raise TypeError( f"set_{self.name}_request() takes 0 positional argument but" f" {len(args)} were given" ) requests = _instance._get_metadata_request() method_metadata_request = getattr(requests, self.name) for prop, alias in kw.items(): if alias is not UNCHANGED: method_metadata_request.add_request(param=prop, alias=alias) _instance._metadata_request = requests return _instanceConfigure whether metadata should be requested to be passed to the
scoremethod.Note that this method is only relevant when this estimator is used as a sub-estimator within a :term:
meta-estimatorand metadata routing is enabled withenable_metadata_routing=True(see :func:sklearn.set_config). Please check the :ref:User Guide <metadata_routing>on how the routing mechanism works.The options for each parameter are:
-
True: metadata is requested, and passed toscoreif provided. The request is ignored if metadata is not provided. -
False: metadata is not requested and the meta-estimator will not pass it toscore. -
None: metadata is not requested, and the meta-estimator will raise an error if the user provides it. -
str: metadata should be passed to the meta-estimator with this given alias instead of the original name.
The default (
sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.Added in version: 1.3
Parameters
sample_weight:str, True, False,orNone, default=sklearn.utils.metadata_routing.UNCHANGED- Metadata routing for
sample_weightparameter inscore.
Returns
self:object- The updated object.
-