From b3438e9f4288ca9d4eb0ae72d00e36e532123b2e Mon Sep 17 00:00:00 2001 From: Yunhee Jeong Date: Fri, 17 Jul 2026 14:46:19 +0000 Subject: [PATCH 1/3] custom tuner --- ...ustom_model_tuner_for_neural_network.ipynb | 282 ++++++++++++++++++ src/mother/optimization/__init__.py | 4 +- src/mother/optimization/core.py | 196 ++++++++---- 3 files changed, 418 insertions(+), 64 deletions(-) create mode 100644 examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb diff --git a/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb b/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb new file mode 100644 index 0000000..5105676 --- /dev/null +++ b/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb @@ -0,0 +1,282 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e8fa1e70", + "metadata": {}, + "source": [ + "# Customize Tuner for Neural Network Optimization \n", + "In this notebook, we will look at how to customize hyperparameter optimization for a model:\n", + "- Use different hyperparameter selection, or settings for the standard models\n", + "- How to define a custom model, and use it in the hyperparameter optimization\n", + "- How to use the pipeline-utility classes `PipelineWithHyperparameterRooting` and `ColumnTransformerWithHyperparameterRooting` " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "594c58ba", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from pathlib import Path\n", + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "from torch import nn\n", + "from six import iteritems\n", + "\n", + "import mother.optimization as opt\n", + "\n", + "X, y = load_breast_cancer(return_X_y=True, as_frame=True)\n", + "X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2)\n", + "print(\n", + " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bcd9573", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0.2540920674800873, 0.9035087719298246)\n" + ] + } + ], + "source": [ + "from collections import OrderedDict\n", + "from mother.ml import AbstractMotherPipeline\n", + "from torch.optim import Adam\n", + "from torch.utils.data import TensorDataset, DataLoader\n", + "import torch\n", + "from mother.ml.models.utils import add_prefix_to_dict_keys\n", + "from optuna.trial import Trial\n", + "import pandas as pd\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "\n", + "class TorchNNMother(AbstractMotherPipeline):\n", + " def __init__(self, lr: float = 1e-3, **kwargs) -> None:\n", + "\n", + " self.network = nn.Sequential(\n", + " OrderedDict(\n", + " [\n", + " (\"linear1\", nn.Linear(in_features=30, out_features=10)),\n", + " (\"relu\", nn.ReLU()),\n", + " (\"linear2\", nn.Linear(in_features=10, out_features=1)),\n", + " (\"sigmoid\", nn.Sigmoid()),\n", + " ]\n", + " )\n", + " )\n", + "\n", + " self.optimizer = Adam(self.network.parameters(), lr=lr)\n", + " self.lr = lr\n", + " self.loss = nn.BCELoss()\n", + " self._init_params: dict = {\"lr\": lr}\n", + "\n", + " non_optimised_params: list[str] = [\"_init_params\"]\n", + " for k, v in kwargs.items():\n", + " if k not in non_optimised_params:\n", + " self._init_params[k] = v\n", + "\n", + " def default_parameters(self, prefix: str = \"\") -> dict:\n", + " return add_prefix_to_dict_keys({\"lr\": 1e-3}, prefix=prefix)\n", + "\n", + " def get_hyperparameter_space(self, X, y, trial: Trial, prefix: str = \"\") -> dict:\n", + " suggested_params: dict = {\"lr\": trial.suggest_float(prefix + \"lr\", 1e-5, 1e-3, log=True)}\n", + " suggested_params = add_prefix_to_dict_keys(suggested_params, prefix=prefix)\n", + "\n", + " return suggested_params\n", + "\n", + " def get_params(self, deep=True) -> dict:\n", + " return self._init_params\n", + "\n", + " def set_params(self, **params):\n", + " for key, value in iteritems(params):\n", + " if key in self._init_params.keys():\n", + " self._init_params[key] = value\n", + "\n", + " # Keep runtime attributes and optimizer in sync with tuned params.\n", + " self.lr = float(self._init_params[\"lr\"])\n", + "\n", + " return self.__init__(**self._init_params)\n", + "\n", + " def _get_dataloader(self, X, y, batch_size: int = 128, shuffle: bool = False) -> DataLoader:\n", + " if isinstance(X, pd.DataFrame):\n", + " X = X.values\n", + " if isinstance(y, pd.Series):\n", + " y = y.values\n", + " return DataLoader(\n", + " TensorDataset(\n", + " torch.tensor(X).to(torch.float32),\n", + " torch.tensor(y).reshape(-1, 1).to(torch.float32),\n", + " ),\n", + " batch_size=batch_size,\n", + " shuffle=shuffle,\n", + " )\n", + "\n", + " def validation(self, X_valid, y_valid):\n", + " valid_data_loader = self._get_dataloader(X_valid, y_valid)\n", + "\n", + " self.network.eval()\n", + " valid_loss = 0.0\n", + " valid_acc = 0.0\n", + "\n", + " with torch.no_grad():\n", + " for X, y in valid_data_loader:\n", + " y_hat = self.network(X)\n", + " valid_loss += self.loss(y_hat, y).item() / len(valid_data_loader)\n", + " y_hat_pred = (y_hat > 0.5).float()\n", + " valid_acc += accuracy_score(\n", + " y.detach().numpy().ravel(),\n", + " y_hat_pred.detach().numpy().ravel(),\n", + " ) / len(valid_data_loader)\n", + "\n", + " return valid_loss, valid_acc\n", + "\n", + " def fit(self, X_train, y_train, n_epochs: int = 100):\n", + " train_data_loader = self._get_dataloader(X_train, y_train, shuffle=True)\n", + "\n", + " # Recreate optimizer so each fit starts from current tuned lr and clean state.\n", + " self.optimizer = Adam(self.network.parameters(), lr=float(self._init_params[\"lr\"]))\n", + "\n", + " for epoch in range(n_epochs):\n", + " self.network.train()\n", + " train_loss = 0.0\n", + " for X, y in train_data_loader:\n", + " self.optimizer.zero_grad()\n", + " y_hat = self.network(X)\n", + " batch_train_loss = self.loss(y_hat, y)\n", + "\n", + " if not torch.isfinite(batch_train_loss):\n", + " raise ValueError(\"Training diverged (non-finite loss). Try a lower learning rate.\")\n", + "\n", + " batch_train_loss.backward()\n", + " self.optimizer.step()\n", + " train_loss += batch_train_loss.item() / len(train_data_loader)\n", + " return self\n", + "\n", + "\n", + "model = TorchNNMother(lr=1e-3)\n", + "model.fit(X_train, y_train)\n", + "print(model.validation(X_valid, y_valid))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b36554d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``multivariate`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``group`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``constant_liar`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.001}\n", + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.0009361658778024464}\n", + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.0008048266581936503}\n", + "FrozenTrial(number=0, state=, values=[0.22355324029922485], datetime_start=datetime.datetime(2026, 7, 17, 14, 11, 33, 456914), datetime_complete=datetime.datetime(2026, 7, 17, 14, 11, 34, 358672), params={'lr': 0.001}, user_attrs={}, system_attrs={'fixed_params': {'lr': 0.001}}, intermediate_values={}, distributions={'lr': FloatDistribution(high=0.001, log=True, low=1e-05, step=None)}, trial_id=0, value=None)\n", + "{'lr': 0.001}\n", + "{'lr': 0.001}\n" + ] + } + ], + "source": [ + "from optuna import Trial\n", + "import sklearn.base as skl_base\n", + "import mother.optimization as opt\n", + "\n", + "\n", + "class TorchMotherTuner(opt.AbstractMotherTuner):\n", + " def __init__(self, **kwargs):\n", + " # create a customised scorer\n", + " super().__init__(**kwargs)\n", + "\n", + " def objective(self, trial: Trial, context: opt.ObjectiveContext) -> float:\n", + " X_train, X_valid, y_train, y_valid = train_test_split(context.X, context.y, test_size=0.2)\n", + " print(\n", + " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", + " )\n", + "\n", + " # fit\n", + " estimator = skl_base.clone(context.estimator)\n", + " suggested_params_to_train: dict = context.get_hyper_space(trial=trial, X=context.X, y=context.y)\n", + " estimator.set_params(**suggested_params_to_train)\n", + " estimator.fit(X_train, y_train)\n", + "\n", + " # calculate valid loss\n", + " valid_loss, valid_acc = context.estimator.validation(X_valid, y_valid)\n", + "\n", + " return valid_loss\n", + "\n", + " def call_optimize(self, context: opt.ObjectiveContext) -> None:\n", + " self.study.optimize(\n", + " lambda trial: self.objective(trial, context=context),\n", + " n_trials=self.n_trials_optuna,\n", + " gc_after_trial=True,\n", + " callbacks=self.get_callbacks(),\n", + " )\n", + "\n", + "\n", + "tuner = TorchMotherTuner(\n", + " n_trials_optuna=3, # number of trials for hyperparameter optimization\n", + " n_threads_optuna=10, # parallel threads for cross-validation evaluation\n", + " n_startup_trials=1, # number of random trials before using optuna\n", + " tuning_direction=\"minimize\", # Need to minimize the loss!\n", + ")\n", + "\n", + "model_tuned = tuner.optimize(\n", + " model,\n", + " X,\n", + " y,\n", + " cross_validation=None,\n", + " hyperparameter_space_function=model.get_hyperparameter_space,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mother-ml (3.13.14)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/mother/optimization/__init__.py b/src/mother/optimization/__init__.py index 1900c31..1fcc332 100644 --- a/src/mother/optimization/__init__.py +++ b/src/mother/optimization/__init__.py @@ -1,3 +1,3 @@ -from mother.optimization.core import MotherTuner +from mother.optimization.core import AbstractMotherTuner, MotherTuner, ObjectiveContext -__all__ = ["MotherTuner"] +__all__ = ["MotherTuner", "AbstractMotherTuner", "ObjectiveContext"] diff --git a/src/mother/optimization/core.py b/src/mother/optimization/core.py index 2eb8f3d..71791ce 100644 --- a/src/mother/optimization/core.py +++ b/src/mother/optimization/core.py @@ -2,6 +2,8 @@ import json import logging import typing +from abc import ABC, abstractmethod +from dataclasses import dataclass, field from functools import wraps import numpy as np @@ -93,34 +95,27 @@ def wrapper(*args, **kwargs): return wrapper -class MotherTuner: - """MotherTuner is a class that facilitates hyperparameter tuning using Optuna. - - Attributes: - n_trials_optuna (int): Number of trials for Optuna optimization. - n_startup_trials (int): Number of startup trials for Optuna. - n_threads_optuna (int): Number of threads for Optuna optimization. - early_stopping_optuna (bool): Flag to enable early stopping in Optuna. - tuning_direction (StudyDirection or string): Direction of optimization (maximize or minimize). - scorer (typing.Callable): Scoring function or string identifier for scoring. - sampler (optuna.samplers.BaseSampler): Sampler for Optuna trials. - study (typing.Optional[Study]): Optuna study object. - **kwargs (Any): additional arguments for the scorer +@dataclass +class ObjectiveContext: + """ + This is a dataclass to pass all arguments of `def optimize()` to + a customized `def objective()`. + """ - Methods: - __init__(self, scorer, sampler=None, early_stopping_optuna=False, tuning_direction=StudyDirection.MAXIMIZE, - n_trials_optuna=100, n_threads_optuna=1, n_startup_trials=12, seed=42): - Initializes the MotherTuner with the given parameters. + get_hyper_space: typing.Callable + estimator: Pipeline + X: pd.DataFrame + y: typing.Union[pd.DataFrame, pd.Series] + cross_validation: skl_model_sel.BaseCrossValidator + fit_kwargs: dict + groups_as_cross_val_args: bool + groups: typing.Optional[np.ndarray] + extras: dict[str, typing.Any] = field(default_factory=dict) - get_callbacks(self): - - optimize(self, estimator, X, y, cross_validation, groups=None, direction="maximize") - -> Pipeline: - """ +class AbstractMotherTuner(ABC): def __init__( self, - scorer: typing.Union[typing.Callable, str], sampler: typing.Optional[optuna.samplers.BaseSampler] = None, early_stopping_optuna: bool = False, tuning_direction: typing.Union[StudyDirection, str] = StudyDirection.MAXIMIZE, @@ -129,13 +124,12 @@ def __init__( n_startup_trials: int = 12, seed: int = 42, **kwargs, - ): + ) -> None: self.n_trials_optuna: int = n_trials_optuna self.n_startup_trials: int = n_startup_trials self.n_threads_optuna: int = n_threads_optuna self.early_stopping_optuna: bool = early_stopping_optuna self.tuning_direction: typing.Union[StudyDirection, str] = tuning_direction - self.scorer: typing.Callable = skl_metrics.get_scorer(scorer) if sampler is None: module_logger.debug("Setting up default sampler TPE") self.sampler = optuna.samplers.TPESampler( @@ -148,7 +142,7 @@ def __init__( else: self.sampler = sampler - self.study: typing.Optional[Study] = None + self.study: typing.Optional[Study] | None = None def get_callbacks(self): """ @@ -175,6 +169,25 @@ def get_callbacks(self): callbacks = [TerminatorCallback()] return callbacks + @abstractmethod + def objective(self, trial: optuna.trial.Trial, context: ObjectiveContext) -> float: + """ + Placeholder to implement a customized objective function + This function is the func argument of optuna.study.optimize + https://optuna.readthedocs.io/en/stable/reference/generated/optuna.study.Study.html#optuna.study.Study.optimize + """ + raise NotImplementedError + + @abstractmethod + def call_optimize(self, context: ObjectiveContext) -> None: + """ + Placeholder to implement a customized function to + 1. additional processing on data/model + 2. call the Study().optimize() function with self.objective() + """ + + raise NotImplementedError + @handle_metadata_routing def optimize( self, @@ -188,6 +201,7 @@ def optimize( ranking_groups: typing.Optional[np.ndarray] = None, fit_kwargs: typing.Optional[dict] = None, _groups_as_cross_val_args: bool = True, + **kwargs, ) -> Pipeline: """ Takes an estimator as input and optimizes the hyperparameters according to @@ -220,36 +234,18 @@ def optimize( assert fit_kwargs is not None groups_as_cross_val_args = _groups_as_cross_val_args - def objective(trial: optuna.trial.Trial) -> float: - suggested_params_to_train: dict = get_hyper_space(trial=trial, X=X, y=y) - module_logger.debug("Cloning pipeline") - pipeline_cv: Pipeline = skl_base.clone(estimator) - pipeline_cv.set_params(**suggested_params_to_train) - - cross_val_kwargs = {} - if groups_as_cross_val_args: - cross_val_kwargs["groups"] = groups - - module_logger.debug("Perform cross validation scoring") - cv_score = skl_model_sel.cross_val_score( - estimator=pipeline_cv, - X=X, - y=utils.y_toArray(y), - cv=cross_validation, - scoring=self.scorer, - n_jobs=np.min([self.n_threads_optuna, cross_validation.get_n_splits()]), - pre_dispatch=np.min([self.n_threads_optuna, cross_validation.get_n_splits()]), - error_score="raise", # only for debugging - params=fit_kwargs, - **cross_val_kwargs, - ) - - gc.collect() - module_logger.info(f"Trial {trial.number}, cv score: {cv_score}") - cv_score_not_na: np.ndarray = cv_score[~np.isnan(cv_score)] - report_cross_validation_scores(trial, list(cv_score_not_na)) - mean_cv_score: float = cv_score_not_na.mean() - return mean_cv_score + # set ObjectiveContext + obj_context = ObjectiveContext( + get_hyper_space=get_hyper_space, + estimator=estimator, + X=X, + y=y, + cross_validation=cross_validation, + fit_kwargs=fit_kwargs, + groups_as_cross_val_args=groups_as_cross_val_args, + groups=groups, + extras=kwargs, + ) module_logger.info( "Setting up Optuna to optimize hyperparameters with direction: %s", @@ -273,12 +269,9 @@ def objective(trial: optuna.trial.Trial) -> float: json.dumps(default_parameters, indent=4), ) self.study.enqueue_trial(default_parameters) - self.study.optimize( - objective, - n_trials=self.n_trials_optuna, - gc_after_trial=True, - callbacks=self.get_callbacks(), - ) + + # call optuna study optimize + self.call_optimize(obj_context) if default_parameters != {}: module_logger.info("Check if the default parameters have been evaluated in the study") @@ -306,3 +299,82 @@ def objective(trial: optuna.trial.Trial) -> float: module_logger.info("Training completed") return pipeline + + +class MotherTuner(AbstractMotherTuner): + """MotherTuner is a class that facilitates hyperparameter tuning using Optuna. + + Attributes: + scorer (typing.Callable): Scoring function or string identifier for scoring. + n_trials_optuna (int): Number of trials for Optuna optimization. + n_startup_trials (int): Number of startup trials for Optuna. + n_threads_optuna (int): Number of threads for Optuna optimization. + early_stopping_optuna (bool): Flag to enable early stopping in Optuna. + tuning_direction (StudyDirection or string): Direction of optimization (maximize or minimize). + sampler (optuna.samplers.BaseSampler): Sampler for Optuna trials. + study (typing.Optional[Study]): Optuna study object. + **kwargs (Any): additional arguments for the scorer + + Methods: + __init__(self, scorer, sampler=None, early_stopping_optuna=False, tuning_direction=StudyDirection.MAXIMIZE, + n_trials_optuna=100, n_threads_optuna=1, n_startup_trials=12, seed=42): + Initializes the MotherTuner with the given parameters. + + get_callbacks(self): + + optimize(self, estimator, X, y, cross_validation, groups=None, direction="maximize") + -> Pipeline: + """ + + def __init__(self, scorer: typing.Union[typing.Callable, str], **kwargs): + + self.scorer: typing.Callable = skl_metrics.get_scorer(scorer) + super().__init__(**kwargs) + + def objective(self, trial: optuna.trial.Trial, context: ObjectiveContext) -> float: + suggested_params_to_train: dict = context.get_hyper_space(trial=trial, X=context.X, y=context.y) + module_logger.debug("Cloning pipeline") + pipeline_cv: Pipeline = skl_base.clone(context.estimator) + pipeline_cv.set_params(**suggested_params_to_train) + + cross_val_kwargs = {} + if context.groups_as_cross_val_args: + cross_val_kwargs["groups"] = context.groups + + module_logger.debug("Perform cross validation scoring") + cv_score = skl_model_sel.cross_val_score( + estimator=pipeline_cv, + X=context.X, + y=utils.y_toArray(context.y), + cv=context.cross_validation, + scoring=self.scorer, + n_jobs=np.min([self.n_threads_optuna, context.cross_validation.get_n_splits()]), + pre_dispatch=np.min([self.n_threads_optuna, context.cross_validation.get_n_splits()]), + error_score="raise", # only for debugging + params=context.fit_kwargs, + **cross_val_kwargs, + ) + + gc.collect() + module_logger.info(f"Trial {trial.number}, cv score: {cv_score}") + cv_score_not_na: np.ndarray = cv_score[~np.isnan(cv_score)] + report_cross_validation_scores(trial, list(cv_score_not_na)) + mean_cv_score: float = cv_score_not_na.mean() + return mean_cv_score + + def call_optimize(self, context: ObjectiveContext) -> None: + """Call study.optimize() funtion. + + Can be customised in the case of data processing needed for cross validation / optimisation + + Args: + context (ObjectiveContext): optimize() function calls this function + with a ObjectiveContext object containing all necessary arguments. + self.call_optimize(obj_context) + """ + self.study.optimize( + lambda trial: self.objective(trial, context=context), + n_trials=self.n_trials_optuna, + gc_after_trial=True, + callbacks=self.get_callbacks(), + ) From 31d0ad5bef2dab0d15a92378e2794f0beac69a95 Mon Sep 17 00:00:00 2001 From: Yunhee Jeong Date: Wed, 5 Aug 2026 13:25:52 +0000 Subject: [PATCH 2/3] updated custom model tuner notebook --- .../05_advanced/05_custom_model_tuner.ipynb | 589 ++++++++++++++++++ ...ustom_model_tuner_for_neural_network.ipynb | 282 --------- 2 files changed, 589 insertions(+), 282 deletions(-) create mode 100644 examples/notebooks/05_advanced/05_custom_model_tuner.ipynb delete mode 100644 examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb diff --git a/examples/notebooks/05_advanced/05_custom_model_tuner.ipynb b/examples/notebooks/05_advanced/05_custom_model_tuner.ipynb new file mode 100644 index 0000000..d9a4afd --- /dev/null +++ b/examples/notebooks/05_advanced/05_custom_model_tuner.ipynb @@ -0,0 +1,589 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e8fa1e70", + "metadata": {}, + "source": [ + "# Customize Tuner for Neural Network Optimization\n", + "\n", + "In this notebook, we explore how to customize hyperparameter optimization for a model:\n", + "- Use different hyperparameter selections or settings for the standard models\n", + "- Optimize only part of a pipeline (e.g. tune only the classifier while keeping the embedder fixed) using `PipelineWithHyperparameterRooting` and a custom `AbstractMotherTuner`\n", + "- Define a custom model and use it in hyperparameter optimization\n", + "\n", + "## Key concepts from `mother.optimization.core`\n", + "\n", + "### `AbstractMotherTuner`\n", + "An abstract base class for building custom tuners. Subclasses must implement two methods:\n", + "- **`objective(trial, context)`** — defines a single Optuna trial: samples hyperparameters, trains and evaluates the model, and returns a scalar score.\n", + "- **`call_optimize(context)`** — controls the overall optimization loop, i.e. how `study.optimize()` is called with `self.objective`.\n", + "\n", + "The `optimize()` method (inherited from `AbstractMotherTuner`) orchestrates the full workflow: it creates the Optuna study, assembles the `ObjectiveContext`, enqueues any default parameters, and finally retrains the best model on the full dataset.\n", + "\n", + "### `ObjectiveContext`\n", + "A dataclass that bundles all arguments from `optimize()` into a single object passed to `objective()` and `call_optimize()`. Its key fields are:\n", + "\n", + "| Field | Description |\n", + "|---|---|\n", + "| `get_hyper_space` | Callable that, given a trial, returns a hyperparameter dict |\n", + "| `estimator` | The unfitted pipeline to be tuned |\n", + "| `X` / `y` | Training data and targets |\n", + "| `cross_validation` | Cross-validator (e.g. `KFold`) |\n", + "| `groups` | Optional group labels for grouped CV |\n", + "| `fit_kwargs` | Extra keyword arguments forwarded to `estimator.fit()` |\n", + "| `extras` | Arbitrary additional data your custom tuner may need |" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "594c58ba", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspaces/MotherML/.venv/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "from torch import nn\n", + "from six import iteritems\n", + "\n", + "from optuna import Trial\n", + "import sklearn.base as skl_base\n", + "from sklearn.model_selection import KFold\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "\n", + "import mother.optimization as opt\n", + "\n", + "X, y = load_breast_cancer(return_X_y=True, as_frame=True)\n", + "X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2)\n", + "print(\n", + " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "35fb9c8f", + "metadata": {}, + "source": [ + "## 1. Optimize Part of a Pipeline\n", + "\n", + "With a custom optimizer, you can optimize only a part of your pipeline.\n", + "\n", + "Here, we optimize the classification model in a pipeline consisting of `TabPFNEmbeddingTransformer` (to generate model-based embeddings from the input features) and `CatboostClassifier`.\n", + "\n", + "### 1.1 Create a Pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b7ec9436", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('embedder', TabPFNEmbeddingTransformer(use_kfold=False)),\n", + " ('classifier',\n", + " )]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from mother.ml.models.m_tabpfn import TabPFNEmbeddingTransformer\n", + "from mother.ml.models.m_catboost import CatboostClassifierMother\n", + "from mother.ml import PipelineWithHyperparameterRooting\n", + "\n", + "model = PipelineWithHyperparameterRooting(\n", + " [\n", + " (\"embedder\", TabPFNEmbeddingTransformer(\n", + " model_type=\"classification\", \n", + " use_kfold=False # these parameters will not change after optimization\n", + " )),\n", + " (\"classifier\", CatboostClassifierMother(target_type=\"single_target\", logging_level=\"Silent\")),\n", + " ]\n", + ")\n", + "\n", + "model.steps" + ] + }, + { + "cell_type": "markdown", + "id": "c8a752ee", + "metadata": {}, + "source": [ + "In an sklearn pipeline, parameter names indicate which step they belong to using the convention `{step_name}__{parameter_name}`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "55919c3e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'memory': None,\n", + " 'steps': [('embedder', TabPFNEmbeddingTransformer(use_kfold=False)),\n", + " ('classifier',\n", + " )],\n", + " 'verbose': False,\n", + " 'embedder': TabPFNEmbeddingTransformer(use_kfold=False),\n", + " 'classifier': ,\n", + " 'embedder__device': 'cpu',\n", + " 'embedder__embedding_column_name': 'tabpfnembedding',\n", + " 'embedder__ignore_pretraining_limits': True,\n", + " 'embedder__model': None,\n", + " 'embedder__model_type': 'classification',\n", + " 'embedder__n_folds': 5,\n", + " 'embedder__random_state': None,\n", + " 'embedder__return_separate_columns': True,\n", + " 'embedder__use_kfold': False,\n", + " 'classifier__learning_rate': 0.03,\n", + " 'classifier__loss_function': 'Logloss',\n", + " 'classifier__logging_level': 'Silent',\n", + " 'classifier__auto_class_weights': 'Balanced',\n", + " 'classifier__random_strength': 1,\n", + " 'classifier__boosting_type': 'Plain',\n", + " 'classifier__bootstrap_type': 'Bayesian',\n", + " 'classifier__max_depth': 6,\n", + " 'classifier__grow_policy': 'SymmetricTree',\n", + " 'classifier__posterior_sampling': True,\n", + " 'classifier__target_type': 'single_target',\n", + " 'classifier__tune_boosting_type': False,\n", + " 'classifier__model_type': 'classification_binary',\n", + " 'classifier__tune_tree_structure_type': True}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.get_params()" + ] + }, + { + "cell_type": "markdown", + "id": "40564642", + "metadata": {}, + "source": [ + "### 1.2 Custom Optimizer\n", + "\n", + "In the custom `objective` function, we select only parameters whose names start with `classifier`, so that only the classifier is optimized while the embedder remains fixed." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "9a84e18e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspaces/MotherML/src/mother/optimization/core.py:135: ExperimentalWarning: Argument ``multivariate`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:135: ExperimentalWarning: Argument ``group`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:135: ExperimentalWarning: Argument ``constant_liar`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Trial 0 parameters to tune : {'classifier__bootstrap_type': 'Bayesian', 'classifier__learning_rate': 0.03, 'classifier__random_strength': 1, 'classifier__grow_policy': 'SymmetricTree', 'classifier__max_depth': 6, 'classifier__loss_function': 'Logloss'}\n", + "Trial 1 parameters to tune : {'classifier__bootstrap_type': 'Bayesian', 'classifier__learning_rate': 0.02208007163250455, 'classifier__random_strength': 1.1961172071448225, 'classifier__grow_policy': 'SymmetricTree', 'classifier__max_depth': 5, 'classifier__loss_function': 'Logloss'}\n", + "Trial 2 parameters to tune : {'classifier__bootstrap_type': 'Bayesian', 'classifier__learning_rate': 0.18132275158254602, 'classifier__random_strength': 0.8829987555087553, 'classifier__grow_policy': 'SymmetricTree', 'classifier__max_depth': 6, 'classifier__auto_class_weights': 'None', 'classifier__loss_function': 'Focal:focal_alpha=0.3924531071968482;focal_gamma=6.2963722761684995'}\n", + "{'classifier__bootstrap_type': 'Bayesian', 'classifier__learning_rate': 0.03, 'classifier__random_strength': 1.0, 'classifier__grow_policy': 'SymmetricTree', 'classifier__max_depth': 6, 'classifier__loss_function': 'Logloss'}\n" + ] + } + ], + "source": [ + "class CustomMotherTuner(opt.AbstractMotherTuner):\n", + " def __init__(self, **kwargs):\n", + " # create a customised scorer\n", + " super().__init__(**kwargs)\n", + "\n", + " def objective(self, trial: Trial, context: opt.ObjectiveContext) -> float:\n", + " cv_score = 0.0\n", + " \n", + " suggested_params_to_train: dict = context.get_hyper_space(trial=trial, X=context.X, y=context.y)\n", + " \n", + " # select params only for \"classifier\"\n", + " suggested_params_to_train = {\n", + " k: v for k, v in suggested_params_to_train.items() if k.startswith(\"classifier\")\n", + " }\n", + "\n", + " print(f\"Trial {trial.number} parameters to tune : {suggested_params_to_train}\")\n", + " \n", + " for train_idx, test_idx in context.cross_validation.split(\n", + " context.X, context.y\n", + " ): \n", + " # Train\n", + " pipeline = skl_base.clone(context.estimator)\n", + " \n", + " # fit\n", + " pipeline.set_params(**suggested_params_to_train)\n", + " pipeline.fit(context.X.iloc[train_idx,:], context.y.iloc[train_idx])\n", + " \n", + " # Test score\n", + " y_pred_test = pipeline.predict(\n", + " X = context.X.iloc[test_idx, :]\n", + " )\n", + " cv_score += accuracy_score(context.y.iloc[test_idx], y_pred_test)\n", + " \n", + " return cv_score/(context.cross_validation.get_n_splits()) # mean acc \n", + "\n", + " def call_optimize(self, context: opt.ObjectiveContext) -> None:\n", + " self.study.optimize(\n", + " lambda trial: self.objective(trial, context=context),\n", + " n_trials=self.n_trials_optuna,\n", + " gc_after_trial=True,\n", + " callbacks=self.get_callbacks(),\n", + " )\n", + "\n", + "\n", + "tuner = CustomMotherTuner(\n", + " n_trials_optuna=3, # number of trials for hyperparameter optimization\n", + " n_threads_optuna=10, # parallel threads for cross-validation evaluation\n", + " n_startup_trials=1, # number of random trials before using optuna\n", + " tuning_direction=\"maximize\", # Maximize the accuracy\n", + ")\n", + "\n", + "model_tuned = tuner.optimize(\n", + " model,\n", + " X_train.iloc[:100,:],\n", + " y_train.iloc[:100],\n", + " cross_validation=KFold(n_splits=2),\n", + " hyperparameter_space_function=model.get_hyperparameter_space,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "45ee6a5e", + "metadata": {}, + "source": [ + "After optimization, we can verify that the embedder parameters remain unchanged." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a644880d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_tuned.get_params()[\"embedder__use_kfold\"]" + ] + }, + { + "cell_type": "markdown", + "id": "a912f3b4", + "metadata": {}, + "source": [ + "## 2. Mother Optimization for a PyTorch Model\n", + "\n", + "In this example, we create a custom `torch` model wrapper using `AbstractMotherPipeline` and a custom optimizer for the model.\n", + "\n", + "### 2.1 Torch Wrapper for Mother\n", + "In this example, we only tune the learning rate, but additional parameters can be added for optimization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bcd9573", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0.2540920674800873, 0.9035087719298246)\n" + ] + } + ], + "source": [ + "from collections import OrderedDict\n", + "from mother.ml import AbstractMotherPipeline\n", + "from torch.optim import Adam\n", + "from torch.utils.data import TensorDataset, DataLoader\n", + "import torch\n", + "from mother.ml.models.utils import add_prefix_to_dict_keys\n", + "from optuna.trial import Trial\n", + "import pandas as pd\n", + "from sklearn.metrics import accuracy_score\n", + "\n", + "\n", + "class TorchNNMother(AbstractMotherPipeline):\n", + " def __init__(self, lr: float = 1e-3, **kwargs) -> None:\n", + "\n", + " self.network = nn.Sequential(\n", + " OrderedDict(\n", + " [\n", + " (\"linear1\", nn.Linear(in_features=30, out_features=10)),\n", + " (\"relu\", nn.ReLU()),\n", + " (\"linear2\", nn.Linear(in_features=10, out_features=1)),\n", + " (\"sigmoid\", nn.Sigmoid()),\n", + " ]\n", + " )\n", + " )\n", + "\n", + " self.optimizer = Adam(self.network.parameters(), lr=lr)\n", + " self.lr = lr\n", + " self.loss = nn.BCELoss()\n", + " self._init_params: dict = {\"lr\": lr}\n", + "\n", + " non_optimised_params: list[str] = [\"_init_params\"]\n", + " for k, v in kwargs.items():\n", + " if k not in non_optimised_params:\n", + " self._init_params[k] = v\n", + "\n", + " def default_parameters(self, prefix: str = \"\") -> dict:\n", + " return add_prefix_to_dict_keys({\"lr\": 1e-3}, prefix=prefix)\n", + "\n", + " def get_hyperparameter_space(self, X, y, trial: Trial, prefix: str = \"\") -> dict:\n", + " # hyper parameter search for learning rate \n", + " suggested_params: dict = {\"lr\": trial.suggest_float(prefix + \"lr\", 1e-5, 1e-3, log=True)}\n", + " suggested_params = add_prefix_to_dict_keys(suggested_params, prefix=prefix)\n", + "\n", + " return suggested_params\n", + "\n", + " def get_params(self, deep=True) -> dict:\n", + " return self._init_params\n", + "\n", + " def set_params(self, **params):\n", + " for key, value in iteritems(params):\n", + " if key in self._init_params.keys():\n", + " self._init_params[key] = value\n", + "\n", + " # Keep runtime attributes and optimizer in sync with tuned params.\n", + " self.lr = float(self._init_params[\"lr\"])\n", + "\n", + " return self.__init__(**self._init_params)\n", + "\n", + " def _get_dataloader(self, X, y, batch_size: int = 128, shuffle: bool = False) -> DataLoader:\n", + " # Crate a dataloader for torch training\n", + " if isinstance(X, pd.DataFrame):\n", + " X = X.values\n", + " if isinstance(y, pd.Series):\n", + " y = y.values\n", + " return DataLoader(\n", + " TensorDataset(\n", + " torch.tensor(X).to(torch.float32),\n", + " torch.tensor(y).reshape(-1, 1).to(torch.float32),\n", + " ),\n", + " batch_size=batch_size,\n", + " shuffle=shuffle,\n", + " )\n", + "\n", + " def validation(self, X_valid, y_valid):\n", + " valid_data_loader = self._get_dataloader(X_valid, y_valid)\n", + "\n", + " self.network.eval()\n", + " valid_loss = 0.0\n", + " valid_acc = 0.0\n", + "\n", + " with torch.no_grad():\n", + " for X, y in valid_data_loader:\n", + " y_hat = self.network(X)\n", + " valid_loss += self.loss(y_hat, y).item() / len(valid_data_loader)\n", + " y_hat_pred = (y_hat > 0.5).float()\n", + " valid_acc += accuracy_score(\n", + " y.detach().numpy().ravel(),\n", + " y_hat_pred.detach().numpy().ravel(),\n", + " ) / len(valid_data_loader)\n", + "\n", + " return valid_loss, valid_acc\n", + "\n", + " def fit(self, X_train, y_train, n_epochs: int = 100):\n", + " train_data_loader = self._get_dataloader(X_train, y_train, shuffle=True)\n", + "\n", + " # Recreate optimizer so each fit starts from current tuned lr and clean state.\n", + " self.optimizer = Adam(self.network.parameters(), lr=float(self._init_params[\"lr\"]))\n", + "\n", + " for epoch in range(n_epochs):\n", + " self.network.train()\n", + " train_loss = 0.0\n", + " for X, y in train_data_loader:\n", + " self.optimizer.zero_grad()\n", + " y_hat = self.network(X)\n", + " batch_train_loss = self.loss(y_hat, y)\n", + "\n", + " if not torch.isfinite(batch_train_loss):\n", + " raise ValueError(\"Training diverged (non-finite loss). Try a lower learning rate.\")\n", + "\n", + " batch_train_loss.backward()\n", + " self.optimizer.step()\n", + " train_loss += batch_train_loss.item() / len(train_data_loader)\n", + " return self\n", + "\n", + "\n", + "model = TorchNNMother(lr=1e-3)\n", + "model.fit(X_train, y_train)\n", + "print(model.validation(X_valid, y_valid))" + ] + }, + { + "cell_type": "markdown", + "id": "e1872e5a", + "metadata": {}, + "source": [ + "### 2.2 Custom Mother Tuner for Torch\n", + "\n", + "Neural network training often requires splitting data into three sets (train, validation, and test). This does not align with the default `MotherTuner` design, which does not create a validation set for monitoring training epochs. To address this, we add a train-validation split inside the custom objective function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b36554d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``multivariate`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``group`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n", + "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``constant_liar`` is an experimental feature. The interface can change in the future.\n", + " self.sampler = optuna.samplers.TPESampler(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.001}\n", + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.0009361658778024464}\n", + "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", + "{'lr': 0.0008048266581936503}\n", + "FrozenTrial(number=0, state=, values=[0.22355324029922485], datetime_start=datetime.datetime(2026, 7, 17, 14, 11, 33, 456914), datetime_complete=datetime.datetime(2026, 7, 17, 14, 11, 34, 358672), params={'lr': 0.001}, user_attrs={}, system_attrs={'fixed_params': {'lr': 0.001}}, intermediate_values={}, distributions={'lr': FloatDistribution(high=0.001, log=True, low=1e-05, step=None)}, trial_id=0, value=None)\n", + "{'lr': 0.001}\n", + "{'lr': 0.001}\n" + ] + } + ], + "source": [ + "from optuna import Trial\n", + "import sklearn.base as skl_base\n", + "import mother.optimization as opt\n", + "\n", + "\n", + "class TorchMotherTuner(opt.AbstractMotherTuner):\n", + " def __init__(self, **kwargs):\n", + " # create a customised scorer\n", + " super().__init__(**kwargs)\n", + "\n", + " def objective(self, trial: Trial, context: opt.ObjectiveContext) -> float:\n", + " X_train, X_valid, y_train, y_valid = train_test_split(context.X, context.y, test_size=0.2)\n", + " print(\n", + " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", + " )\n", + "\n", + " # fit\n", + " estimator = skl_base.clone(context.estimator)\n", + " suggested_params_to_train: dict = context.get_hyper_space(trial=trial, X=context.X, y=context.y)\n", + " estimator.set_params(**suggested_params_to_train)\n", + " estimator.fit(X_train, y_train)\n", + "\n", + " # calculate valid loss\n", + " valid_loss, valid_acc = context.estimator.validation(X_valid, y_valid)\n", + "\n", + " return valid_loss\n", + "\n", + " def call_optimize(self, context: opt.ObjectiveContext) -> None:\n", + " self.study.optimize(\n", + " lambda trial: self.objective(trial, context=context),\n", + " n_trials=self.n_trials_optuna,\n", + " gc_after_trial=True,\n", + " callbacks=self.get_callbacks(),\n", + " )\n", + "\n", + "\n", + "tuner = TorchMotherTuner(\n", + " n_trials_optuna=3, # number of trials for hyperparameter optimization\n", + " n_threads_optuna=10, # parallel threads for cross-validation evaluation\n", + " n_startup_trials=1, # number of random trials before using optuna\n", + " tuning_direction=\"minimize\", # Need to minimize the loss!\n", + ")\n", + "\n", + "model_tuned = tuner.optimize(\n", + " model,\n", + " X,\n", + " y,\n", + " cross_validation=None,\n", + " hyperparameter_space_function=model.get_hyperparameter_space,\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mother-ml (3.13.14)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb b/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb deleted file mode 100644 index 5105676..0000000 --- a/examples/notebooks/05_advanced/05_custom_model_tuner_for_neural_network.ipynb +++ /dev/null @@ -1,282 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e8fa1e70", - "metadata": {}, - "source": [ - "# Customize Tuner for Neural Network Optimization \n", - "In this notebook, we will look at how to customize hyperparameter optimization for a model:\n", - "- Use different hyperparameter selection, or settings for the standard models\n", - "- How to define a custom model, and use it in the hyperparameter optimization\n", - "- How to use the pipeline-utility classes `PipelineWithHyperparameterRooting` and `ColumnTransformerWithHyperparameterRooting` " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "594c58ba", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "from pathlib import Path\n", - "from sklearn.datasets import load_breast_cancer\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "from torch import nn\n", - "from six import iteritems\n", - "\n", - "import mother.optimization as opt\n", - "\n", - "X, y = load_breast_cancer(return_X_y=True, as_frame=True)\n", - "X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2)\n", - "print(\n", - " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6bcd9573", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(0.2540920674800873, 0.9035087719298246)\n" - ] - } - ], - "source": [ - "from collections import OrderedDict\n", - "from mother.ml import AbstractMotherPipeline\n", - "from torch.optim import Adam\n", - "from torch.utils.data import TensorDataset, DataLoader\n", - "import torch\n", - "from mother.ml.models.utils import add_prefix_to_dict_keys\n", - "from optuna.trial import Trial\n", - "import pandas as pd\n", - "from sklearn.metrics import accuracy_score\n", - "\n", - "\n", - "class TorchNNMother(AbstractMotherPipeline):\n", - " def __init__(self, lr: float = 1e-3, **kwargs) -> None:\n", - "\n", - " self.network = nn.Sequential(\n", - " OrderedDict(\n", - " [\n", - " (\"linear1\", nn.Linear(in_features=30, out_features=10)),\n", - " (\"relu\", nn.ReLU()),\n", - " (\"linear2\", nn.Linear(in_features=10, out_features=1)),\n", - " (\"sigmoid\", nn.Sigmoid()),\n", - " ]\n", - " )\n", - " )\n", - "\n", - " self.optimizer = Adam(self.network.parameters(), lr=lr)\n", - " self.lr = lr\n", - " self.loss = nn.BCELoss()\n", - " self._init_params: dict = {\"lr\": lr}\n", - "\n", - " non_optimised_params: list[str] = [\"_init_params\"]\n", - " for k, v in kwargs.items():\n", - " if k not in non_optimised_params:\n", - " self._init_params[k] = v\n", - "\n", - " def default_parameters(self, prefix: str = \"\") -> dict:\n", - " return add_prefix_to_dict_keys({\"lr\": 1e-3}, prefix=prefix)\n", - "\n", - " def get_hyperparameter_space(self, X, y, trial: Trial, prefix: str = \"\") -> dict:\n", - " suggested_params: dict = {\"lr\": trial.suggest_float(prefix + \"lr\", 1e-5, 1e-3, log=True)}\n", - " suggested_params = add_prefix_to_dict_keys(suggested_params, prefix=prefix)\n", - "\n", - " return suggested_params\n", - "\n", - " def get_params(self, deep=True) -> dict:\n", - " return self._init_params\n", - "\n", - " def set_params(self, **params):\n", - " for key, value in iteritems(params):\n", - " if key in self._init_params.keys():\n", - " self._init_params[key] = value\n", - "\n", - " # Keep runtime attributes and optimizer in sync with tuned params.\n", - " self.lr = float(self._init_params[\"lr\"])\n", - "\n", - " return self.__init__(**self._init_params)\n", - "\n", - " def _get_dataloader(self, X, y, batch_size: int = 128, shuffle: bool = False) -> DataLoader:\n", - " if isinstance(X, pd.DataFrame):\n", - " X = X.values\n", - " if isinstance(y, pd.Series):\n", - " y = y.values\n", - " return DataLoader(\n", - " TensorDataset(\n", - " torch.tensor(X).to(torch.float32),\n", - " torch.tensor(y).reshape(-1, 1).to(torch.float32),\n", - " ),\n", - " batch_size=batch_size,\n", - " shuffle=shuffle,\n", - " )\n", - "\n", - " def validation(self, X_valid, y_valid):\n", - " valid_data_loader = self._get_dataloader(X_valid, y_valid)\n", - "\n", - " self.network.eval()\n", - " valid_loss = 0.0\n", - " valid_acc = 0.0\n", - "\n", - " with torch.no_grad():\n", - " for X, y in valid_data_loader:\n", - " y_hat = self.network(X)\n", - " valid_loss += self.loss(y_hat, y).item() / len(valid_data_loader)\n", - " y_hat_pred = (y_hat > 0.5).float()\n", - " valid_acc += accuracy_score(\n", - " y.detach().numpy().ravel(),\n", - " y_hat_pred.detach().numpy().ravel(),\n", - " ) / len(valid_data_loader)\n", - "\n", - " return valid_loss, valid_acc\n", - "\n", - " def fit(self, X_train, y_train, n_epochs: int = 100):\n", - " train_data_loader = self._get_dataloader(X_train, y_train, shuffle=True)\n", - "\n", - " # Recreate optimizer so each fit starts from current tuned lr and clean state.\n", - " self.optimizer = Adam(self.network.parameters(), lr=float(self._init_params[\"lr\"]))\n", - "\n", - " for epoch in range(n_epochs):\n", - " self.network.train()\n", - " train_loss = 0.0\n", - " for X, y in train_data_loader:\n", - " self.optimizer.zero_grad()\n", - " y_hat = self.network(X)\n", - " batch_train_loss = self.loss(y_hat, y)\n", - "\n", - " if not torch.isfinite(batch_train_loss):\n", - " raise ValueError(\"Training diverged (non-finite loss). Try a lower learning rate.\")\n", - "\n", - " batch_train_loss.backward()\n", - " self.optimizer.step()\n", - " train_loss += batch_train_loss.item() / len(train_data_loader)\n", - " return self\n", - "\n", - "\n", - "model = TorchNNMother(lr=1e-3)\n", - "model.fit(X_train, y_train)\n", - "print(model.validation(X_valid, y_valid))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b36554d", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``multivariate`` is an experimental feature. The interface can change in the future.\n", - " self.sampler = optuna.samplers.TPESampler(\n", - "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``group`` is an experimental feature. The interface can change in the future.\n", - " self.sampler = optuna.samplers.TPESampler(\n", - "/workspaces/MotherML/src/mother/optimization/core.py:131: ExperimentalWarning: Argument ``constant_liar`` is an experimental feature. The interface can change in the future.\n", - " self.sampler = optuna.samplers.TPESampler(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", - "{'lr': 0.001}\n", - "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", - "{'lr': 0.0009361658778024464}\n", - "Train / Validation samples = 455 / 114 with 30 features and 2 labels.\n", - "{'lr': 0.0008048266581936503}\n", - "FrozenTrial(number=0, state=, values=[0.22355324029922485], datetime_start=datetime.datetime(2026, 7, 17, 14, 11, 33, 456914), datetime_complete=datetime.datetime(2026, 7, 17, 14, 11, 34, 358672), params={'lr': 0.001}, user_attrs={}, system_attrs={'fixed_params': {'lr': 0.001}}, intermediate_values={}, distributions={'lr': FloatDistribution(high=0.001, log=True, low=1e-05, step=None)}, trial_id=0, value=None)\n", - "{'lr': 0.001}\n", - "{'lr': 0.001}\n" - ] - } - ], - "source": [ - "from optuna import Trial\n", - "import sklearn.base as skl_base\n", - "import mother.optimization as opt\n", - "\n", - "\n", - "class TorchMotherTuner(opt.AbstractMotherTuner):\n", - " def __init__(self, **kwargs):\n", - " # create a customised scorer\n", - " super().__init__(**kwargs)\n", - "\n", - " def objective(self, trial: Trial, context: opt.ObjectiveContext) -> float:\n", - " X_train, X_valid, y_train, y_valid = train_test_split(context.X, context.y, test_size=0.2)\n", - " print(\n", - " f\"Train / Validation samples = {X_train.shape[0]} / {X_valid.shape[0]} with {X_train.shape[1]} features and {y_train.nunique()} labels.\"\n", - " )\n", - "\n", - " # fit\n", - " estimator = skl_base.clone(context.estimator)\n", - " suggested_params_to_train: dict = context.get_hyper_space(trial=trial, X=context.X, y=context.y)\n", - " estimator.set_params(**suggested_params_to_train)\n", - " estimator.fit(X_train, y_train)\n", - "\n", - " # calculate valid loss\n", - " valid_loss, valid_acc = context.estimator.validation(X_valid, y_valid)\n", - "\n", - " return valid_loss\n", - "\n", - " def call_optimize(self, context: opt.ObjectiveContext) -> None:\n", - " self.study.optimize(\n", - " lambda trial: self.objective(trial, context=context),\n", - " n_trials=self.n_trials_optuna,\n", - " gc_after_trial=True,\n", - " callbacks=self.get_callbacks(),\n", - " )\n", - "\n", - "\n", - "tuner = TorchMotherTuner(\n", - " n_trials_optuna=3, # number of trials for hyperparameter optimization\n", - " n_threads_optuna=10, # parallel threads for cross-validation evaluation\n", - " n_startup_trials=1, # number of random trials before using optuna\n", - " tuning_direction=\"minimize\", # Need to minimize the loss!\n", - ")\n", - "\n", - "model_tuned = tuner.optimize(\n", - " model,\n", - " X,\n", - " y,\n", - " cross_validation=None,\n", - " hyperparameter_space_function=model.get_hyperparameter_space,\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mother-ml (3.13.14)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From a5835133c3936a181879f06e3ed0fd7f8755054d Mon Sep 17 00:00:00 2001 From: Yunhee Jeong Date: Wed, 5 Aug 2026 13:26:05 +0000 Subject: [PATCH 3/3] for merge --- uv.lock | 302 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 151 insertions(+), 151 deletions(-) diff --git a/uv.lock b/uv.lock index ba00735..795238f 100644 --- a/uv.lock +++ b/uv.lock @@ -148,14 +148,14 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "array-api-compat", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "h5py", marker = "python_full_version < '3.11'" }, - { name = "natsort", marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pandas", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "array-api-compat" }, + { name = "exceptiongroup" }, + { name = "h5py" }, + { name = "natsort" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/bb/895fa2e9f8cd6d1c058aa90759da715037d0f11e23713e692537555549d7/anndata-0.11.4.tar.gz", hash = "sha256:4ce08d09d2ccb5f37d32790363bbcc7fc1b79863842296ae4badfaf48c736e24", size = 541143, upload-time = "2025-03-26T11:38:54.566Z" } wheels = [ @@ -171,17 +171,17 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "array-api-compat", marker = "python_full_version >= '3.11'" }, - { name = "h5py", marker = "python_full_version >= '3.11'" }, - { name = "legacy-api-wrap", marker = "python_full_version >= '3.11'" }, - { name = "natsort", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pandas", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scverse-misc", version = "0.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "array-api-compat" }, + { name = "h5py" }, + { name = "legacy-api-wrap" }, + { name = "natsort" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "scverse-misc", version = "0.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "scverse-misc", version = "0.0.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "zarr", version = "2.18.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "zarr", version = "2.18.7", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/0f/4c6b3a4c5dc57bd02f0656666aefae7e83b8158b839f60cfb1c5f2fbecab/anndata-0.12.16.tar.gz", hash = "sha256:26d557fd147728993aac99c2eef676e1e316a84bbeee3c176ca6c5d66e5fc443", size = 2255740, upload-time = "2026-05-18T15:59:40.346Z" } wheels = [ @@ -362,12 +362,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "biotraj", marker = "python_full_version < '3.11'" }, - { name = "msgpack", marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "biotraj" }, + { name = "msgpack" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, + { name = "packaging" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/95/ce1bbe59adb442390f57ffc4a4b93799ce9babd755e82db0b4a55fe87ca9/biotite-1.2.0.tar.gz", hash = "sha256:8b36dd708a976db10f629ffc8f81a236a74268a4e65e1f576610331d67dab392", size = 36062367, upload-time = "2025-03-16T08:30:09.688Z" } wheels = [ @@ -398,12 +398,12 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "biotraj", marker = "python_full_version >= '3.11'" }, - { name = "msgpack", marker = "python_full_version >= '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "biotraj" }, + { name = "msgpack" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, + { name = "packaging" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/7b/99153f7bceef01034b5f19a6b123219533132d446ffcf141dfef3e386d33/biotite-1.6.0.tar.gz", hash = "sha256:4c172f6e57521220fa0fc4899142211f6f21ba83d8f6f135d4edc68981f70e7e", size = 38514388, upload-time = "2026-01-23T12:47:38.331Z" } wheels = [ @@ -779,7 +779,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -850,7 +850,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1073,34 +1073,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -1237,7 +1237,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1267,7 +1267,7 @@ name = "fast-array-utils" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/86/7345025275bd19b1303bb6267133982f4e95309f85a39529036566cf3b29/fast_array_utils-1.4.1.tar.gz", hash = "sha256:466512aa0e19ebfb2f8d7ae8736fc81b5724e9bf1a7958474206c17b8e991e9c", size = 336422, upload-time = "2026-04-10T10:05:51.039Z" } wheels = [ @@ -1276,10 +1276,10 @@ wheels = [ [package.optional-dependencies] accel = [ - { name = "numba", marker = "python_full_version >= '3.12'" }, + { name = "numba" }, ] sparse = [ - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, ] [[package]] @@ -1856,17 +1856,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -1882,18 +1882,18 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" } wheels = [ @@ -1905,7 +1905,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -2460,15 +2460,15 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "cycler", marker = "python_full_version < '3.11'" }, - { name = "fonttools", marker = "python_full_version < '3.11'" }, - { name = "kiwisolver", marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "pyparsing", marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } wheels = [ @@ -2523,15 +2523,15 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler", marker = "python_full_version >= '3.11'" }, - { name = "fonttools", marker = "python_full_version >= '3.11'" }, - { name = "kiwisolver", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", marker = "python_full_version >= '3.11'" }, - { name = "pyparsing", marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } wheels = [ @@ -2810,7 +2810,7 @@ wheels = [ [[package]] name = "mother-ml" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } dependencies = [ { name = "boruta" }, @@ -3325,7 +3325,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/56/8895a76abe4ec94ebd01eeb6d74f587bc4cddd46569670e1402852a5da13/numcodecs-0.13.1.tar.gz", hash = "sha256:a3cf37881df0898f3a9c0d4477df88133fe85185bffe57ba31bcc2fa207709bc", size = 5955215, upload-time = "2024-10-09T16:28:00.188Z" } wheels = [ @@ -3349,7 +3349,7 @@ wheels = [ [package.optional-dependencies] msgpack = [ - { name = "msgpack", marker = "python_full_version < '3.11'" }, + { name = "msgpack" }, ] [[package]] @@ -3361,8 +3361,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "deprecated", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "deprecated" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/fc/bb532969eb8236984ba65e4f0079a7da885b8ac0ce1f0835decbb3938a62/numcodecs-0.15.1.tar.gz", hash = "sha256:eeed77e4d6636641a2cc605fbc6078c7a8f2cc40f3dfa2b3f61e52e6091b04ff", size = 6267275, upload-time = "2025-02-10T10:23:33.254Z" } wheels = [ @@ -3382,7 +3382,7 @@ wheels = [ [package.optional-dependencies] msgpack = [ - { name = "msgpack", marker = "python_full_version >= '3.11'" }, + { name = "msgpack" }, ] [[package]] @@ -4999,31 +4999,31 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "anndata", version = "0.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "anndata", version = "0.12.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "h5py", marker = "python_full_version < '3.12'" }, - { name = "joblib", marker = "python_full_version < '3.12'" }, - { name = "legacy-api-wrap", marker = "python_full_version < '3.12'" }, - { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "h5py" }, + { name = "joblib" }, + { name = "legacy-api-wrap" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "natsort", marker = "python_full_version < '3.12'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "natsort" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numba", marker = "python_full_version < '3.12'" }, - { name = "numpy", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pandas", marker = "python_full_version < '3.12'" }, - { name = "patsy", marker = "python_full_version < '3.12'" }, - { name = "pynndescent", marker = "python_full_version < '3.12'" }, - { name = "scikit-learn", marker = "python_full_version < '3.12'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "pynndescent" }, + { name = "scikit-learn" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "seaborn", marker = "python_full_version < '3.12'" }, - { name = "session-info2", marker = "python_full_version < '3.12'" }, - { name = "statsmodels", marker = "python_full_version < '3.12'" }, - { name = "tqdm", marker = "python_full_version < '3.12'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, - { name = "umap-learn", marker = "python_full_version < '3.12'" }, + { name = "seaborn" }, + { name = "session-info2" }, + { name = "statsmodels" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "umap-learn" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/a8/285f1a9c995906b7e0ae3c399208fe67cfba8126dd31359dfef0908f6edc/scanpy-1.11.5.tar.gz", hash = "sha256:b2ef5476dfb1144b7dd0fae90b0198699c7988e6b27f083904150642c7ba6b89", size = 14088122, upload-time = "2025-10-21T08:24:43.999Z" } wheels = [ @@ -5038,28 +5038,28 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "anndata", version = "0.12.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "fast-array-utils", extra = ["accel", "sparse"], marker = "python_full_version >= '3.12'" }, - { name = "h5py", marker = "python_full_version >= '3.12'" }, - { name = "joblib", marker = "python_full_version >= '3.12'" }, - { name = "legacy-api-wrap", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "natsort", marker = "python_full_version >= '3.12'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "numba", marker = "python_full_version >= '3.12'" }, - { name = "numpy", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pandas", marker = "python_full_version >= '3.12'" }, - { name = "patsy", marker = "python_full_version >= '3.12'" }, - { name = "pynndescent", marker = "python_full_version >= '3.12'" }, - { name = "scikit-learn", marker = "python_full_version >= '3.12'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "seaborn", marker = "python_full_version >= '3.12'" }, - { name = "session-info2", marker = "python_full_version >= '3.12'" }, - { name = "statsmodels", marker = "python_full_version >= '3.12'" }, - { name = "tqdm", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, - { name = "umap-learn", marker = "python_full_version >= '3.12'" }, + { name = "anndata", version = "0.12.16", source = { registry = "https://pypi.org/simple" } }, + { name = "fast-array-utils", extra = ["accel", "sparse"] }, + { name = "h5py" }, + { name = "joblib" }, + { name = "legacy-api-wrap" }, + { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" } }, + { name = "natsort" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numba" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "patsy" }, + { name = "pynndescent" }, + { name = "scikit-learn" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "seaborn" }, + { name = "session-info2" }, + { name = "statsmodels" }, + { name = "tqdm" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "umap-learn" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/180968c66be48f9dab747330beb2056df5bb7a115a56d3700da149c48916/scanpy-1.12.tar.gz", hash = "sha256:8139840bb948ce0aa0798c9b8b88c1df4f06c27641a792f0995d39cd4dcf858a", size = 14418589, upload-time = "2026-01-23T13:25:23.414Z" } wheels = [ @@ -5104,7 +5104,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -5164,7 +5164,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -5218,8 +5218,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "session-info2", marker = "python_full_version == '3.11.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "session-info2" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/05/6123a4362e2810ef216f152e249a66799f9c37975dabf89b69abb2d68c42/scverse_misc-0.0.3.tar.gz", hash = "sha256:18c46eeeac8ccef8f435e41a8ee86173b3d7ef6ea1167fde97a17553f70d3210", size = 23128, upload-time = "2026-04-10T15:12:21.731Z" } wheels = [ @@ -5234,8 +5234,8 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "session-info2", marker = "python_full_version >= '3.12'" }, - { name = "typing-extensions", marker = "python_full_version == '3.12.*'" }, + { name = "session-info2" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/2e/2fc93b1c9a7dc3c9c44ba8d239a094d21c8cbf3219a57213fc14a7e9bf28/scverse_misc-0.0.8.tar.gz", hash = "sha256:7de6d46e50fc111d366d60b07f165531de97a836a4d702bbbfa2a47a0015fd9e", size = 31621, upload-time = "2026-06-08T12:01:27.683Z" } wheels = [ @@ -6043,10 +6043,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "asciitree", marker = "python_full_version < '3.11'" }, - { name = "fasteners", marker = "python_full_version < '3.11' and sys_platform != 'emscripten'" }, - { name = "numcodecs", version = "0.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "asciitree" }, + { name = "fasteners", marker = "sys_platform != 'emscripten'" }, + { name = "numcodecs", version = "0.13.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/c4/187a21ce7cf7c8f00c060dd0e04c2a81139bb7b1ab178bba83f2e1134ce2/zarr-2.18.3.tar.gz", hash = "sha256:2580d8cb6dd84621771a10d31c4d777dca8a27706a1a89b29f42d2d37e2df5ce", size = 3603224, upload-time = "2024-09-04T23:20:16.595Z" } wheels = [ @@ -6062,10 +6062,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "asciitree", marker = "python_full_version >= '3.11'" }, - { name = "fasteners", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten'" }, - { name = "numcodecs", version = "0.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "asciitree" }, + { name = "fasteners", marker = "sys_platform != 'emscripten'" }, + { name = "numcodecs", version = "0.15.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/1d/01cf9e3ab2d85190278efc3fca9f68563de35ae30ee59e7640e3af98abe3/zarr-2.18.7.tar.gz", hash = "sha256:b2b8f66f14dac4af66b180d2338819981b981f70e196c9a66e6bfaa9e59572f5", size = 3604558, upload-time = "2025-04-09T07:59:28.482Z" } wheels = [