mirror of https://github.com/microsoft/autogen.git
warning -> info for low cost partial config (#231)
* warning -> info for low cost partial config #195, #110 * when n_estimators < 0, use trained_estimator's * log debug info * test random seed * remove "objective"; avoid ZeroDivisionError * hp config to estimator params * check type of searcher * default n_jobs * try import * Update searchalgo_auto.py * CLASSIFICATION * auto_augment flag * min_sample_size * make catboost optional
This commit is contained in:
parent
a99e939404
commit
f48ca2618f
|
@ -103,7 +103,7 @@ print(automl.model)
|
|||
|
||||
```python
|
||||
from flaml import AutoML
|
||||
from sklearn.datasets import load_boston
|
||||
from sklearn.datasets import fetch_california_housing
|
||||
# Initialize an AutoML instance
|
||||
automl = AutoML()
|
||||
# Specify automl goal and constraint
|
||||
|
@ -113,7 +113,7 @@ automl_settings = {
|
|||
"task": 'regression',
|
||||
"log_file_name": "test/boston.log",
|
||||
}
|
||||
X_train, y_train = load_boston(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
# Train with labeled input data
|
||||
automl.fit(X_train=X_train, y_train=y_train,
|
||||
**automl_settings)
|
||||
|
|
|
@ -36,7 +36,7 @@ from .config import (
|
|||
N_SPLITS,
|
||||
SAMPLE_MULTIPLY_FACTOR,
|
||||
)
|
||||
from .data import concat
|
||||
from .data import concat, CLASSIFICATION
|
||||
from . import tune
|
||||
from .training_log import training_log_reader, training_log_writer
|
||||
|
||||
|
@ -619,7 +619,8 @@ class AutoML:
|
|||
if issparse(X_train_all):
|
||||
X_train_all = X_train_all.tocsr()
|
||||
if (
|
||||
self._state.task in ("binary", "multi")
|
||||
self._state.task in CLASSIFICATION
|
||||
and self._auto_augment
|
||||
and self._state.fit_kwargs.get("sample_weight") is None
|
||||
and self._split_type not in ["time", "group"]
|
||||
):
|
||||
|
@ -725,7 +726,7 @@ class AutoML:
|
|||
y_train, y_val = y_train_all[train_idx], y_train_all[val_idx]
|
||||
self._state.groups = self._state.groups_all[train_idx]
|
||||
self._state.groups_val = self._state.groups_all[val_idx]
|
||||
elif self._state.task in ("binary", "multi"):
|
||||
elif self._state.task in CLASSIFICATION:
|
||||
# for classification, make sure the labels are complete in both
|
||||
# training and validation data
|
||||
label_set, first = np.unique(y_train_all, return_index=True)
|
||||
|
@ -904,10 +905,11 @@ class AutoML:
|
|||
n_splits=N_SPLITS,
|
||||
split_type=None,
|
||||
groups=None,
|
||||
n_jobs=1,
|
||||
n_jobs=-1,
|
||||
train_best=True,
|
||||
train_full=False,
|
||||
record_id=-1,
|
||||
auto_augment=True,
|
||||
**fit_kwargs,
|
||||
):
|
||||
"""Retrain from log file
|
||||
|
@ -943,7 +945,8 @@ class AutoML:
|
|||
groups: None or array-like | Group labels (with matching length to
|
||||
y_train) or groups counts (with sum equal to length of y_train)
|
||||
for training data.
|
||||
n_jobs: An integer of the number of threads for training.
|
||||
n_jobs: An integer of the number of threads for training. Use all
|
||||
available resources when n_jobs == -1.
|
||||
train_best: A boolean of whether to train the best config in the
|
||||
time budget; if false, train the last config in the budget.
|
||||
train_full: A boolean of whether to train on the full data. If true,
|
||||
|
@ -952,6 +955,8 @@ class AutoML:
|
|||
be retrained. By default `record_id = -1` which means this will be
|
||||
ignored. `record_id = 0` corresponds to the first trial, and
|
||||
when `record_id >= 0`, `time_budget` will be ignored.
|
||||
auto_augment: boolean, default=True | Whether to automatically
|
||||
augment rare classes.
|
||||
**fit_kwargs: Other key word arguments to pass to fit() function of
|
||||
the searched learners, such as sample_weight.
|
||||
"""
|
||||
|
@ -1018,6 +1023,7 @@ class AutoML:
|
|||
elif eval_method == "auto":
|
||||
eval_method = self._decide_eval_method(time_budget)
|
||||
self.modelcount = 0
|
||||
self._auto_augment = auto_augment
|
||||
self._prepare_data(eval_method, split_ratio, n_splits)
|
||||
self._state.time_budget = None
|
||||
self._state.n_jobs = n_jobs
|
||||
|
@ -1032,7 +1038,7 @@ class AutoML:
|
|||
self._state.task = get_classification_objective(
|
||||
len(np.unique(self._y_train_all))
|
||||
)
|
||||
if self._state.task in ("binary", "multi"):
|
||||
if self._state.task in CLASSIFICATION:
|
||||
assert split_type in [None, "stratified", "uniform", "time", "group"]
|
||||
self._split_type = (
|
||||
split_type or self._state.groups is None and "stratified" or "group"
|
||||
|
@ -1191,7 +1197,7 @@ class AutoML:
|
|||
Returns:
|
||||
A float for the minimal sample size or None
|
||||
"""
|
||||
return MIN_SAMPLE_TRAIN if self._sample else None
|
||||
return self._min_sample_size if self._sample else None
|
||||
|
||||
@property
|
||||
def max_resource(self) -> Optional[float]:
|
||||
|
@ -1282,7 +1288,7 @@ class AutoML:
|
|||
sample_weight_val=None,
|
||||
groups_val=None,
|
||||
groups=None,
|
||||
verbose=1,
|
||||
verbose=3,
|
||||
retrain_full=True,
|
||||
split_type=None,
|
||||
learner_selector="sample",
|
||||
|
@ -1291,8 +1297,10 @@ class AutoML:
|
|||
seed=None,
|
||||
n_concurrent_trials=1,
|
||||
keep_search_state=False,
|
||||
append_log=False,
|
||||
early_stop=False,
|
||||
append_log=False,
|
||||
auto_augment=True,
|
||||
min_sample_size=MIN_SAMPLE_TRAIN,
|
||||
**fit_kwargs,
|
||||
):
|
||||
"""Find a model for a given task
|
||||
|
@ -1375,7 +1383,7 @@ class AutoML:
|
|||
groups: None or array-like | Group labels (with matching length to
|
||||
y_train) or groups counts (with sum equal to length of y_train)
|
||||
for training data.
|
||||
verbose: int, default=1 | Controls the verbosity, higher means more
|
||||
verbose: int, default=3 | Controls the verbosity, higher means more
|
||||
messages.
|
||||
retrain_full: bool or str, default=True | whether to retrain the
|
||||
selected model on the full training data when using holdout.
|
||||
|
@ -1412,8 +1420,12 @@ class AutoML:
|
|||
saving.
|
||||
early_stop: boolean, default=False | Whether to stop early if the
|
||||
search is considered to converge.
|
||||
append_log: boolean, default=False | whetehr to directly append the log
|
||||
append_log: boolean, default=False | Whetehr to directly append the log
|
||||
records to the input log file if it exists.
|
||||
auto_augment: boolean, default=True | Whether to automatically
|
||||
augment rare classes.
|
||||
min_sample_size: int, default=MIN_SAMPLE_TRAIN | the minimal sample
|
||||
size when sample=True.
|
||||
**fit_kwargs: Other key word arguments to pass to fit() function of
|
||||
the searched learners, such as sample_weight. Include period as
|
||||
a key word argument for 'forecast' task.
|
||||
|
@ -1435,8 +1447,8 @@ class AutoML:
|
|||
self._learner_selector = learner_selector
|
||||
old_level = logger.getEffectiveLevel()
|
||||
self.verbose = verbose
|
||||
if verbose == 0:
|
||||
logger.setLevel(logging.WARNING)
|
||||
# if verbose == 0:
|
||||
logger.setLevel(50 - verbose * 10)
|
||||
if (not mlflow or not mlflow.active_run()) and not logger.handlers:
|
||||
# Add the console handler.
|
||||
_ch = logging.StreamHandler()
|
||||
|
@ -1457,12 +1469,14 @@ class AutoML:
|
|||
and (eval_method == "holdout" and self._state.X_val is None)
|
||||
or (eval_method == "cv")
|
||||
)
|
||||
self._auto_augment = auto_augment
|
||||
self._min_sample_size = min_sample_size
|
||||
self._prepare_data(eval_method, split_ratio, n_splits)
|
||||
self._sample = (
|
||||
sample
|
||||
and task != "rank"
|
||||
and eval_method != "cv"
|
||||
and (MIN_SAMPLE_TRAIN * SAMPLE_MULTIPLY_FACTOR < self._state.data_size)
|
||||
and (self._min_sample_size * SAMPLE_MULTIPLY_FACTOR < self._state.data_size)
|
||||
)
|
||||
if "auto" == metric:
|
||||
if "binary" in self._state.task:
|
||||
|
@ -1584,8 +1598,8 @@ class AutoML:
|
|||
for state in self._search_states.values():
|
||||
if state.trained_estimator:
|
||||
del state.trained_estimator
|
||||
if verbose == 0:
|
||||
logger.setLevel(old_level)
|
||||
# if verbose == 0:
|
||||
logger.setLevel(old_level)
|
||||
|
||||
def _search_parallel(self):
|
||||
try:
|
||||
|
@ -1631,6 +1645,8 @@ class AutoML:
|
|||
points_to_evaluate=points_to_evaluate,
|
||||
)
|
||||
else:
|
||||
self._state.time_from_start = time.time() - self._start_time_flag
|
||||
time_left = self._state.time_budget - self._state.time_from_start
|
||||
search_alg = SearchAlgo(
|
||||
metric="val_loss",
|
||||
space=space,
|
||||
|
@ -1645,13 +1661,9 @@ class AutoML:
|
|||
],
|
||||
metric_constraints=self.metric_constraints,
|
||||
seed=self._seed,
|
||||
time_budget_s=time_left,
|
||||
)
|
||||
search_alg = ConcurrencyLimiter(search_alg, self._n_concurrent_trials)
|
||||
self._state.time_from_start = time.time() - self._start_time_flag
|
||||
time_left = self._state.time_budget - self._state.time_from_start
|
||||
search_alg.set_search_properties(
|
||||
None, None, config={"time_budget_s": time_left}
|
||||
)
|
||||
resources_per_trial = (
|
||||
{"cpu": self._state.n_jobs} if self._state.n_jobs > 1 else None
|
||||
)
|
||||
|
@ -1782,7 +1794,7 @@ class AutoML:
|
|||
search_space = search_state.search_space
|
||||
if self._sample:
|
||||
prune_attr = "FLAML_sample_size"
|
||||
min_resource = MIN_SAMPLE_TRAIN
|
||||
min_resource = self._min_sample_size
|
||||
max_resource = self._state.data_size
|
||||
else:
|
||||
prune_attr = min_resource = max_resource = None
|
||||
|
@ -1840,10 +1852,10 @@ class AutoML:
|
|||
else:
|
||||
search_space = None
|
||||
if self._hpo_method in ("bs", "cfo", "cfocat"):
|
||||
search_state.search_alg.set_search_properties(
|
||||
search_state.search_alg.searcher.set_search_properties(
|
||||
metric=None,
|
||||
mode=None,
|
||||
config={
|
||||
setting={
|
||||
"metric_target": self._state.best_loss,
|
||||
},
|
||||
)
|
||||
|
@ -1852,7 +1864,7 @@ class AutoML:
|
|||
search_state.training_function,
|
||||
search_alg=search_state.search_alg,
|
||||
time_budget_s=min(budget_left, self._state.train_time_limit),
|
||||
verbose=max(self.verbose - 1, 0),
|
||||
verbose=max(self.verbose - 3, 0),
|
||||
use_ray=False,
|
||||
)
|
||||
time_used = time.time() - start_run_time
|
||||
|
@ -2077,7 +2089,7 @@ class AutoML:
|
|||
logger.info(estimators)
|
||||
if len(estimators) <= 1:
|
||||
return
|
||||
if self._state.task in ("binary", "multi"):
|
||||
if self._state.task in CLASSIFICATION:
|
||||
from sklearn.ensemble import StackingClassifier as Stacker
|
||||
else:
|
||||
from sklearn.ensemble import StackingRegressor as Stacker
|
||||
|
@ -2184,7 +2196,7 @@ class AutoML:
|
|||
speed = delta_loss / delta_time
|
||||
if speed:
|
||||
estimated_cost = max(2 * gap / speed, estimated_cost)
|
||||
estimated_cost == estimated_cost or 1e-10
|
||||
estimated_cost == estimated_cost or 1e-9
|
||||
inv.append(1 / estimated_cost)
|
||||
else:
|
||||
estimated_cost = self._eci[i]
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
"""!
|
||||
* Copyright (c) 2020-2021 Microsoft Corporation. All rights reserved.
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import vstack, issparse
|
||||
import pandas as pd
|
||||
|
||||
from .training_log import training_log_reader
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
CLASSIFICATION = ("binary", "multi", "classification")
|
||||
|
||||
|
||||
def load_openml_dataset(
|
||||
dataset_id, data_dir=None, random_state=0, dataset_format="dataframe"
|
||||
|
@ -300,11 +303,7 @@ class DataTransformer:
|
|||
)
|
||||
self._drop = drop
|
||||
|
||||
if task in (
|
||||
"binary",
|
||||
"multi",
|
||||
"classification",
|
||||
) or not pd.api.types.is_numeric_dtype(y):
|
||||
if task in CLASSIFICATION or not pd.api.types.is_numeric_dtype(y):
|
||||
from sklearn.preprocessing import LabelEncoder
|
||||
|
||||
self.label_transformer = LabelEncoder()
|
||||
|
|
|
@ -33,7 +33,7 @@ from .model import (
|
|||
ARIMA,
|
||||
SARIMAX,
|
||||
)
|
||||
from .data import group_counts
|
||||
from .data import CLASSIFICATION, group_counts
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -301,7 +301,7 @@ def evaluate_model_CV(
|
|||
valid_fold_num = total_fold_num = 0
|
||||
n = kf.get_n_splits()
|
||||
X_train_split, y_train_split = X_train_all, y_train_all
|
||||
if task in ("binary", "multi"):
|
||||
if task in CLASSIFICATION:
|
||||
labels = np.unique(y_train_all)
|
||||
else:
|
||||
labels = None
|
||||
|
|
349
flaml/model.py
349
flaml/model.py
|
@ -13,11 +13,11 @@ from lightgbm import LGBMClassifier, LGBMRegressor, LGBMRanker
|
|||
from scipy.sparse import issparse
|
||||
import pandas as pd
|
||||
from . import tune
|
||||
from .data import group_counts
|
||||
from .data import group_counts, CLASSIFICATION
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger("flaml.automl")
|
||||
|
||||
|
||||
class BaseEstimator:
|
||||
|
@ -30,24 +30,23 @@ class BaseEstimator:
|
|||
for both regression and classification
|
||||
"""
|
||||
|
||||
def __init__(self, task="binary", **params):
|
||||
def __init__(self, task="binary", **config):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
task: A string of the task type, one of
|
||||
'binary', 'multi', 'regression', 'rank', 'forecast'
|
||||
n_jobs: An integer of the number of parallel threads
|
||||
params: A dictionary of the hyperparameter names and values
|
||||
config: A dictionary containing the hyperparameter names
|
||||
and 'n_jobs' as keys. n_jobs is the number of parallel threads.
|
||||
"""
|
||||
self.params = params
|
||||
self.params = self.config2params(config)
|
||||
self.estimator_class = self._model = None
|
||||
self._task = task
|
||||
if "_estimator_type" in params:
|
||||
self._estimator_type = params["_estimator_type"]
|
||||
del self.params["_estimator_type"]
|
||||
if "_estimator_type" in config:
|
||||
self._estimator_type = self.params.pop("_estimator_type")
|
||||
else:
|
||||
self._estimator_type = (
|
||||
"classifier" if task in ("binary", "multi") else "regressor"
|
||||
"classifier" if task in CLASSIFICATION else "regressor"
|
||||
)
|
||||
|
||||
def get_params(self, deep=False):
|
||||
|
@ -83,8 +82,9 @@ class BaseEstimator:
|
|||
current_time = time.time()
|
||||
if "groups" in kwargs:
|
||||
kwargs = kwargs.copy()
|
||||
groups = kwargs.pop("groups")
|
||||
if self._task == "rank":
|
||||
kwargs["group"] = group_counts(kwargs["groups"])
|
||||
kwargs["group"] = group_counts(groups)
|
||||
# groups_val = kwargs.get('groups_val')
|
||||
# if groups_val is not None:
|
||||
# kwargs['eval_group'] = [group_counts(groups_val)]
|
||||
|
@ -92,10 +92,13 @@ class BaseEstimator:
|
|||
# (kwargs['X_val'], kwargs['y_val'])]
|
||||
# kwargs['verbose'] = False
|
||||
# del kwargs['groups_val'], kwargs['X_val'], kwargs['y_val']
|
||||
del kwargs["groups"]
|
||||
X_train = self._preprocess(X_train)
|
||||
model = self.estimator_class(**self.params)
|
||||
if logger.level == logging.DEBUG:
|
||||
logger.debug(f"flaml.model - {model} fit started")
|
||||
model.fit(X_train, y_train, **kwargs)
|
||||
if logger.level == logging.DEBUG:
|
||||
logger.debug(f"flaml.model - {model} fit finished")
|
||||
train_time = time.time() - current_time
|
||||
self._model = model
|
||||
return train_time
|
||||
|
@ -143,9 +146,8 @@ class BaseEstimator:
|
|||
Each element at (i,j) is the probability for instance i to be in
|
||||
class j
|
||||
"""
|
||||
assert self._task in (
|
||||
"binary",
|
||||
"multi",
|
||||
assert (
|
||||
self._task in CLASSIFICATION
|
||||
), "predict_prob() only for classification task."
|
||||
X_test = self._preprocess(X_test)
|
||||
return self._model.predict_proba(X_test)
|
||||
|
@ -160,9 +162,10 @@ class BaseEstimator:
|
|||
Returns:
|
||||
A dictionary of the search space.
|
||||
Each key is the name of a hyperparameter, and value is a dict with
|
||||
its domain and init_value (optional), cat_hp_cost (optional)
|
||||
its domain (required) and low_cost_init_value, init_value,
|
||||
cat_hp_cost (if applicable).
|
||||
e.g.,
|
||||
{'domain': tune.randint(lower=1, upper=10), 'init_value': 1}
|
||||
{'domain': tune.randint(lower=1, upper=10), 'init_value': 1}.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
@ -171,11 +174,11 @@ class BaseEstimator:
|
|||
"""[optional method] memory size of the estimator in bytes
|
||||
|
||||
Args:
|
||||
config - the dict of the hyperparameter config
|
||||
config - A dict of the hyperparameter config.
|
||||
|
||||
Returns:
|
||||
A float of the memory size required by the estimator to train the
|
||||
given config
|
||||
given config.
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
|
@ -189,10 +192,21 @@ class BaseEstimator:
|
|||
"""[optional method] initialize the class"""
|
||||
pass
|
||||
|
||||
def config2params(self, config: dict) -> dict:
|
||||
"""[optional method] config dict to params dict
|
||||
|
||||
Args:
|
||||
config - A dict of the hyperparameter config.
|
||||
|
||||
Returns:
|
||||
A dict that will be passed to self.estimator_class's constructor.
|
||||
"""
|
||||
return config.copy()
|
||||
|
||||
|
||||
class SKLearnEstimator(BaseEstimator):
|
||||
def __init__(self, task="binary", **params):
|
||||
super().__init__(task, **params)
|
||||
def __init__(self, task="binary", **config):
|
||||
super().__init__(task, **config)
|
||||
|
||||
def _preprocess(self, X):
|
||||
if isinstance(X, pd.DataFrame):
|
||||
|
@ -255,39 +269,22 @@ class LGBMEstimator(BaseEstimator):
|
|||
},
|
||||
}
|
||||
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
if "log_max_bin" in params:
|
||||
params["max_bin"] = (1 << params.pop("log_max_bin")) - 1
|
||||
return params
|
||||
|
||||
@classmethod
|
||||
def size(cls, config):
|
||||
num_leaves = int(round(config.get("num_leaves") or config["max_leaves"]))
|
||||
n_estimators = int(round(config["n_estimators"]))
|
||||
return (num_leaves * 3 + (num_leaves - 1) * 4 + 1.0) * n_estimators * 8
|
||||
|
||||
def __init__(self, task="binary", log_max_bin=8, **params):
|
||||
super().__init__(task, **params)
|
||||
if "objective" not in self.params:
|
||||
# Default: ‘regression’ for LGBMRegressor,
|
||||
# ‘binary’ or ‘multiclass’ for LGBMClassifier
|
||||
objective = "regression"
|
||||
if "binary" in task:
|
||||
objective = "binary"
|
||||
elif "multi" in task:
|
||||
objective = "multiclass"
|
||||
elif "rank" == task:
|
||||
objective = "lambdarank"
|
||||
self.params["objective"] = objective
|
||||
if "n_estimators" in self.params:
|
||||
self.params["n_estimators"] = int(round(self.params["n_estimators"]))
|
||||
if "num_leaves" in self.params:
|
||||
self.params["num_leaves"] = int(round(self.params["num_leaves"]))
|
||||
if "min_child_samples" in self.params:
|
||||
self.params["min_child_samples"] = int(
|
||||
round(self.params["min_child_samples"])
|
||||
)
|
||||
if "max_bin" not in self.params:
|
||||
self.params["max_bin"] = 1 << int(round(log_max_bin)) - 1
|
||||
def __init__(self, task="binary", **config):
|
||||
super().__init__(task, **config)
|
||||
if "verbose" not in self.params:
|
||||
self.params["verbose"] = -1
|
||||
# if "subsample_freq" not in self.params:
|
||||
# self.params['subsample_freq'] = 1
|
||||
if "regression" == task:
|
||||
self.estimator_class = LGBMRegressor
|
||||
elif "rank" == task:
|
||||
|
@ -355,7 +352,7 @@ class LGBMEstimator(BaseEstimator):
|
|||
if self.params["n_estimators"] > 0:
|
||||
self._fit(X_train, y_train, **kwargs)
|
||||
else:
|
||||
self.params["n_estimators"] = n_iter
|
||||
self.params["n_estimators"] = self._model.n_estimators
|
||||
train_time = time.time() - start_time
|
||||
return train_time
|
||||
|
||||
|
@ -415,56 +412,30 @@ class XGBoostEstimator(SKLearnEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return 1.6
|
||||
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["max_depth"] = params.get("max_depth", 0)
|
||||
params["grow_policy"] = params.get("grow_policy", "lossguide")
|
||||
params["booster"] = params.get("booster", "gbtree")
|
||||
params["use_label_encoder"] = params.get("use_label_encoder", False)
|
||||
params["tree_method"] = params.get("tree_method", "hist")
|
||||
if "n_jobs" in config:
|
||||
params["nthread"] = params.pop("n_jobs")
|
||||
return params
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task="regression",
|
||||
all_thread=False,
|
||||
n_jobs=1,
|
||||
n_estimators=4,
|
||||
max_leaves=4,
|
||||
subsample=1.0,
|
||||
min_child_weight=1,
|
||||
learning_rate=0.1,
|
||||
reg_lambda=1.0,
|
||||
reg_alpha=0.0,
|
||||
colsample_bylevel=1.0,
|
||||
colsample_bytree=1.0,
|
||||
tree_method="auto",
|
||||
**params,
|
||||
**config,
|
||||
):
|
||||
super().__init__(task, **params)
|
||||
self._n_estimators = int(round(n_estimators))
|
||||
self.params.update(
|
||||
{
|
||||
"max_leaves": int(round(max_leaves)),
|
||||
"max_depth": params.get("max_depth", 0),
|
||||
"grow_policy": params.get("grow_policy", "lossguide"),
|
||||
"tree_method": tree_method,
|
||||
"verbosity": params.get("verbosity", 0),
|
||||
"nthread": n_jobs,
|
||||
"learning_rate": float(learning_rate),
|
||||
"subsample": float(subsample),
|
||||
"reg_alpha": float(reg_alpha),
|
||||
"reg_lambda": float(reg_lambda),
|
||||
"min_child_weight": float(min_child_weight),
|
||||
"booster": params.get("booster", "gbtree"),
|
||||
"colsample_bylevel": float(colsample_bylevel),
|
||||
"colsample_bytree": float(colsample_bytree),
|
||||
"objective": params.get("objective"),
|
||||
}
|
||||
)
|
||||
if all_thread:
|
||||
del self.params["nthread"]
|
||||
|
||||
def get_params(self, deep=False):
|
||||
params = super().get_params()
|
||||
params["n_jobs"] = params["nthread"]
|
||||
return params
|
||||
super().__init__(task, **config)
|
||||
self.params["verbosity"] = 0
|
||||
|
||||
def fit(self, X_train, y_train, budget=None, **kwargs):
|
||||
start_time = time.time()
|
||||
if not issparse(X_train):
|
||||
self.params["tree_method"] = "hist"
|
||||
if issparse(X_train):
|
||||
self.params["tree_method"] = "auto"
|
||||
else:
|
||||
X_train = self._preprocess(X_train)
|
||||
if "sample_weight" in kwargs:
|
||||
dtrain = xgb.DMatrix(X_train, label=y_train, weight=kwargs["sample_weight"])
|
||||
|
@ -478,8 +449,10 @@ class XGBoostEstimator(SKLearnEstimator):
|
|||
obj = objective
|
||||
if "objective" in self.params:
|
||||
del self.params["objective"]
|
||||
self._model = xgb.train(self.params, dtrain, self._n_estimators, obj=obj)
|
||||
_n_estimators = self.params.pop("n_estimators")
|
||||
self._model = xgb.train(self.params, dtrain, _n_estimators, obj=obj)
|
||||
self.params["objective"] = objective
|
||||
self.params["n_estimators"] = _n_estimators
|
||||
del dtrain
|
||||
train_time = time.time() - start_time
|
||||
return train_time
|
||||
|
@ -502,54 +475,29 @@ class XGBoostSklearnEstimator(SKLearnEstimator, LGBMEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return XGBoostEstimator.cost_relative2lgbm()
|
||||
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["max_depth"] = 0
|
||||
params["grow_policy"] = params.get("grow_policy", "lossguide")
|
||||
params["booster"] = params.get("booster", "gbtree")
|
||||
params["use_label_encoder"] = params.get("use_label_encoder", False)
|
||||
params["tree_method"] = params.get("tree_method", "hist")
|
||||
return params
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task="binary",
|
||||
n_jobs=1,
|
||||
n_estimators=4,
|
||||
max_leaves=4,
|
||||
subsample=1.0,
|
||||
min_child_weight=1,
|
||||
learning_rate=0.1,
|
||||
reg_lambda=1.0,
|
||||
reg_alpha=0.0,
|
||||
colsample_bylevel=1.0,
|
||||
colsample_bytree=1.0,
|
||||
tree_method="hist",
|
||||
**params,
|
||||
**config,
|
||||
):
|
||||
super().__init__(task, **params)
|
||||
del self.params["objective"]
|
||||
del self.params["max_bin"]
|
||||
super().__init__(task, **config)
|
||||
del self.params["verbose"]
|
||||
self.params.update(
|
||||
{
|
||||
"n_estimators": int(round(n_estimators)),
|
||||
"max_leaves": int(round(max_leaves)),
|
||||
"max_depth": 0,
|
||||
"grow_policy": params.get("grow_policy", "lossguide"),
|
||||
"tree_method": tree_method,
|
||||
"n_jobs": n_jobs,
|
||||
"verbosity": 0,
|
||||
"learning_rate": float(learning_rate),
|
||||
"subsample": float(subsample),
|
||||
"reg_alpha": float(reg_alpha),
|
||||
"reg_lambda": float(reg_lambda),
|
||||
"min_child_weight": float(min_child_weight),
|
||||
"booster": params.get("booster", "gbtree"),
|
||||
"colsample_bylevel": float(colsample_bylevel),
|
||||
"colsample_bytree": float(colsample_bytree),
|
||||
"use_label_encoder": params.get("use_label_encoder", False),
|
||||
}
|
||||
)
|
||||
self.params["verbosity"] = 0
|
||||
|
||||
self.estimator_class = xgb.XGBRegressor
|
||||
if "rank" == task:
|
||||
self.estimator_class = xgb.XGBRanker
|
||||
elif task in ("binary", "multi"):
|
||||
elif task in CLASSIFICATION:
|
||||
self.estimator_class = xgb.XGBClassifier
|
||||
self._time_per_iter = None
|
||||
self._train_size = 0
|
||||
|
||||
def fit(self, X_train, y_train, budget=None, **kwargs):
|
||||
if issparse(X_train):
|
||||
|
@ -578,7 +526,7 @@ class RandomForestEstimator(SKLearnEstimator, LGBMEstimator):
|
|||
"low_cost_init_value": 4,
|
||||
},
|
||||
}
|
||||
if task in ("binary", "multi"):
|
||||
if task in CLASSIFICATION:
|
||||
space["criterion"] = {
|
||||
"domain": tune.choice(["gini", "entropy"]),
|
||||
# 'init_value': 'gini',
|
||||
|
@ -589,36 +537,24 @@ class RandomForestEstimator(SKLearnEstimator, LGBMEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return 2.0
|
||||
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
if "max_leaves" in params:
|
||||
params["max_leaf_nodes"] = params.get(
|
||||
"max_leaf_nodes", params.pop("max_leaves")
|
||||
)
|
||||
return params
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task="binary",
|
||||
n_jobs=1,
|
||||
n_estimators=4,
|
||||
max_features=1.0,
|
||||
criterion="gini",
|
||||
max_leaves=4,
|
||||
**params,
|
||||
):
|
||||
super().__init__(task, **params)
|
||||
del self.params["objective"]
|
||||
del self.params["max_bin"]
|
||||
self.params.update(
|
||||
{
|
||||
"n_estimators": int(round(n_estimators)),
|
||||
"n_jobs": n_jobs,
|
||||
"verbose": 0,
|
||||
"max_features": float(max_features),
|
||||
"max_leaf_nodes": params.get("max_leaf_nodes", int(round(max_leaves))),
|
||||
}
|
||||
)
|
||||
self.params["verbose"] = 0
|
||||
self.estimator_class = RandomForestRegressor
|
||||
if task in ("binary", "multi"):
|
||||
if task in CLASSIFICATION:
|
||||
self.estimator_class = RandomForestClassifier
|
||||
self.params["criterion"] = criterion
|
||||
|
||||
def get_params(self, deep=False):
|
||||
params = super().get_params()
|
||||
return params
|
||||
|
||||
|
||||
class ExtraTreeEstimator(RandomForestEstimator):
|
||||
|
@ -648,21 +584,16 @@ class LRL1Classifier(SKLearnEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return 160
|
||||
|
||||
def __init__(self, task="binary", n_jobs=1, tol=0.0001, C=1.0, **params):
|
||||
super().__init__(task, **params)
|
||||
self.params.update(
|
||||
{
|
||||
"penalty": params.get("penalty", "l1"),
|
||||
"tol": float(tol),
|
||||
"C": float(C),
|
||||
"solver": params.get("solver", "saga"),
|
||||
"n_jobs": n_jobs,
|
||||
}
|
||||
)
|
||||
assert task in (
|
||||
"binary",
|
||||
"multi",
|
||||
), "LogisticRegression for classification task only"
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["tol"] = params.get("tol", 0.0001)
|
||||
params["solver"] = params.get("solver", "saga")
|
||||
params["penalty"] = params.get("penalty", "l1")
|
||||
return params
|
||||
|
||||
def __init__(self, task="binary", **config):
|
||||
super().__init__(task, **config)
|
||||
assert task in CLASSIFICATION, "LogisticRegression for classification task only"
|
||||
self.estimator_class = LogisticRegression
|
||||
|
||||
|
||||
|
@ -675,21 +606,16 @@ class LRL2Classifier(SKLearnEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return 25
|
||||
|
||||
def __init__(self, task="binary", n_jobs=1, tol=0.0001, C=1.0, **params):
|
||||
super().__init__(task, **params)
|
||||
self.params.update(
|
||||
{
|
||||
"penalty": params.get("penalty", "l2"),
|
||||
"tol": float(tol),
|
||||
"C": float(C),
|
||||
"solver": params.get("solver", "lbfgs"),
|
||||
"n_jobs": n_jobs,
|
||||
}
|
||||
)
|
||||
assert task in (
|
||||
"binary",
|
||||
"multi",
|
||||
), "LogisticRegression for classification task only"
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["tol"] = params.get("tol", 0.0001)
|
||||
params["solver"] = params.get("solver", "lbfgs")
|
||||
params["penalty"] = params.get("penalty", "l2")
|
||||
return params
|
||||
|
||||
def __init__(self, task="binary", **config):
|
||||
super().__init__(task, **config)
|
||||
assert task in CLASSIFICATION, "LogisticRegression for classification task only"
|
||||
self.estimator_class = LogisticRegression
|
||||
|
||||
|
||||
|
@ -749,39 +675,33 @@ class CatBoostEstimator(BaseEstimator):
|
|||
X = X.to_numpy()
|
||||
return X
|
||||
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["n_estimators"] = params.get("n_estimators", 8192)
|
||||
if "n_jobs" in params:
|
||||
params["thread_count"] = params.pop("n_jobs")
|
||||
return params
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task="binary",
|
||||
n_jobs=1,
|
||||
n_estimators=8192,
|
||||
learning_rate=0.1,
|
||||
early_stopping_rounds=4,
|
||||
**params,
|
||||
**config,
|
||||
):
|
||||
super().__init__(task, **params)
|
||||
super().__init__(task, **config)
|
||||
self.params.update(
|
||||
{
|
||||
"early_stopping_rounds": int(round(early_stopping_rounds)),
|
||||
"n_estimators": n_estimators,
|
||||
"learning_rate": learning_rate,
|
||||
"thread_count": n_jobs,
|
||||
"verbose": params.get("verbose", False),
|
||||
"random_seed": params.get("random_seed", 10242048),
|
||||
"verbose": config.get("verbose", False),
|
||||
"random_seed": config.get("random_seed", 10242048),
|
||||
}
|
||||
)
|
||||
from catboost import CatBoostRegressor
|
||||
|
||||
self.estimator_class = CatBoostRegressor
|
||||
if task in ("binary", "multi"):
|
||||
if task in CLASSIFICATION:
|
||||
from catboost import CatBoostClassifier
|
||||
|
||||
self.estimator_class = CatBoostClassifier
|
||||
|
||||
def get_params(self, deep=False):
|
||||
params = super().get_params()
|
||||
params["n_jobs"] = params["thread_count"]
|
||||
return params
|
||||
|
||||
def fit(self, X_train, y_train, budget=None, **kwargs):
|
||||
import shutil
|
||||
|
||||
|
@ -881,7 +801,7 @@ class CatBoostEstimator(BaseEstimator):
|
|||
kwargs["sample_weight"] = weight
|
||||
self._model = model
|
||||
else:
|
||||
self.params["n_estimators"] = n_iter
|
||||
self.params["n_estimators"] = self._model.tree_count_
|
||||
# except CatBoostError:
|
||||
# self._model = None
|
||||
train_time = time.time() - start_time
|
||||
|
@ -904,22 +824,21 @@ class KNeighborsEstimator(BaseEstimator):
|
|||
def cost_relative2lgbm(cls):
|
||||
return 30
|
||||
|
||||
def __init__(self, task="binary", n_jobs=1, n_neighbors=5, **params):
|
||||
super().__init__(task, **params)
|
||||
self.params.update(
|
||||
{
|
||||
"n_neighbors": int(round(n_neighbors)),
|
||||
"weights": params.get("weights", "distance"),
|
||||
"n_jobs": n_jobs,
|
||||
}
|
||||
)
|
||||
from sklearn.neighbors import KNeighborsRegressor
|
||||
def config2params(cls, config: dict) -> dict:
|
||||
params = config.copy()
|
||||
params["weights"] = params.get("weights", "distance")
|
||||
return params
|
||||
|
||||
self.estimator_class = KNeighborsRegressor
|
||||
if task in ("binary", "multi"):
|
||||
def __init__(self, task="binary", **config):
|
||||
super().__init__(task, **config)
|
||||
if task in CLASSIFICATION:
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
|
||||
self.estimator_class = KNeighborsClassifier
|
||||
else:
|
||||
from sklearn.neighbors import KNeighborsRegressor
|
||||
|
||||
self.estimator_class = KNeighborsRegressor
|
||||
|
||||
def _preprocess(self, X):
|
||||
if isinstance(X, pd.DataFrame):
|
||||
|
@ -963,9 +882,7 @@ class Prophet(BaseEstimator):
|
|||
}
|
||||
return space
|
||||
|
||||
def __init__(self, task="forecast", **params):
|
||||
if "n_jobs" in params:
|
||||
params.pop("n_jobs")
|
||||
def __init__(self, task="forecast", n_jobs=1, **params):
|
||||
super().__init__(task, **params)
|
||||
|
||||
def _join(self, X_train, y_train):
|
||||
|
|
|
@ -13,7 +13,7 @@ SEARCH_ALGO_MAPPING = OrderedDict(
|
|||
("bs", BlendSearch),
|
||||
("grid", None),
|
||||
("gridbert", None),
|
||||
("rs", None)
|
||||
("rs", None),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -35,14 +35,16 @@ class AutoSearchAlgorithm:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def from_method_name(cls,
|
||||
search_algo_name,
|
||||
search_algo_args_mode,
|
||||
hpo_search_space,
|
||||
time_budget,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
**custom_hpo_args):
|
||||
def from_method_name(
|
||||
cls,
|
||||
search_algo_name,
|
||||
search_algo_args_mode,
|
||||
hpo_search_space,
|
||||
time_budget,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
**custom_hpo_args
|
||||
):
|
||||
"""
|
||||
Instantiating one of the search algorithm classes based on the search algorithm name, search algorithm
|
||||
argument mode, hpo search space and other keyword args
|
||||
|
@ -68,7 +70,9 @@ class AutoSearchAlgorithm:
|
|||
{"points_to_evaluate": [{"learning_rate": 1e-5, "num_train_epochs": 10}])
|
||||
"""
|
||||
|
||||
assert hpo_search_space, "hpo_search_space needs to be specified for calling AutoSearchAlgorithm.from_method_name"
|
||||
assert (
|
||||
hpo_search_space
|
||||
), "hpo_search_space needs to be specified for calling AutoSearchAlgorithm.from_method_name"
|
||||
if not search_algo_name:
|
||||
# TODO coverage
|
||||
search_algo_name = "grid"
|
||||
|
@ -83,9 +87,15 @@ class AutoSearchAlgorithm:
|
|||
of the constructor function
|
||||
"""
|
||||
this_search_algo_kwargs = None
|
||||
allowed_arguments = SEARCH_ALGO_MAPPING[search_algo_name].__init__.__code__.co_varnames
|
||||
allowed_custom_args = {key: custom_hpo_args[key] for key in custom_hpo_args.keys() if
|
||||
key in allowed_arguments}
|
||||
allowed_arguments = SEARCH_ALGO_MAPPING[
|
||||
search_algo_name
|
||||
].__init__.__code__.co_varnames
|
||||
custom_hpo_args["time_budget_s"] = time_budget
|
||||
allowed_custom_args = {
|
||||
key: custom_hpo_args[key]
|
||||
for key in custom_hpo_args.keys()
|
||||
if key in allowed_arguments
|
||||
}
|
||||
|
||||
"""
|
||||
If the search_algo_args_mode is "dft", set the args to the default args, e.g.,the default args for
|
||||
|
@ -94,26 +104,34 @@ class AutoSearchAlgorithm:
|
|||
"""
|
||||
if search_algo_args_mode == "dft":
|
||||
# TODO coverage
|
||||
this_search_algo_kwargs = DEFAULT_SEARCH_ALGO_ARGS_MAPPING[search_algo_name](
|
||||
this_search_algo_kwargs = DEFAULT_SEARCH_ALGO_ARGS_MAPPING[
|
||||
search_algo_name
|
||||
](
|
||||
"dft",
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=hpo_search_space,
|
||||
**allowed_custom_args)
|
||||
**allowed_custom_args
|
||||
)
|
||||
elif search_algo_args_mode == "cus":
|
||||
this_search_algo_kwargs = DEFAULT_SEARCH_ALGO_ARGS_MAPPING[search_algo_name](
|
||||
this_search_algo_kwargs = DEFAULT_SEARCH_ALGO_ARGS_MAPPING[
|
||||
search_algo_name
|
||||
](
|
||||
"cus",
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=hpo_search_space,
|
||||
**allowed_custom_args)
|
||||
**allowed_custom_args
|
||||
)
|
||||
|
||||
"""
|
||||
returning the hpo algorithm with the arguments
|
||||
"""
|
||||
search_algo = SEARCH_ALGO_MAPPING[search_algo_name](**this_search_algo_kwargs)
|
||||
search_algo = SEARCH_ALGO_MAPPING[search_algo_name](
|
||||
**this_search_algo_kwargs
|
||||
)
|
||||
if search_algo_name == "bs":
|
||||
search_algo.set_search_properties(config={"time_budget_s": time_budget})
|
||||
search_algo.set_search_properties()
|
||||
return search_algo
|
||||
raise ValueError(
|
||||
"Unrecognized method {} for this kind of AutoSearchAlgorithm: {}.\n"
|
||||
|
@ -125,29 +143,39 @@ class AutoSearchAlgorithm:
|
|||
@staticmethod
|
||||
def grid2list(grid_config):
|
||||
# TODO coverage
|
||||
key_val_list = [[(key, each_val) for each_val in val_list['grid_search']]
|
||||
for (key, val_list) in grid_config.items()]
|
||||
key_val_list = [
|
||||
[(key, each_val) for each_val in val_list["grid_search"]]
|
||||
for (key, val_list) in grid_config.items()
|
||||
]
|
||||
config_list = [dict(x) for x in itertools.product(*key_val_list)]
|
||||
return config_list
|
||||
|
||||
|
||||
def get_search_algo_args_optuna(search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args):
|
||||
def get_search_algo_args_optuna(
|
||||
search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args
|
||||
):
|
||||
# TODO coverage
|
||||
return {}
|
||||
|
||||
|
||||
def default_search_algo_args_bs(search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args):
|
||||
assert hpo_search_space, "hpo_search_space needs to be specified for calling AutoSearchAlgorithm.from_method_name"
|
||||
if "num_train_epochs" in hpo_search_space and \
|
||||
isinstance(hpo_search_space["num_train_epochs"], ray.tune.sample.Categorical):
|
||||
def default_search_algo_args_bs(
|
||||
search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
time_budget_s=None,
|
||||
**custom_hpo_args
|
||||
):
|
||||
assert (
|
||||
hpo_search_space
|
||||
), "hpo_search_space needs to be specified for calling AutoSearchAlgorithm.from_method_name"
|
||||
if "num_train_epochs" in hpo_search_space and isinstance(
|
||||
hpo_search_space["num_train_epochs"], ray.tune.sample.Categorical
|
||||
):
|
||||
min_epoch = min(hpo_search_space["num_train_epochs"].categories)
|
||||
else:
|
||||
# TODO coverage
|
||||
|
@ -156,31 +184,38 @@ def default_search_algo_args_bs(search_args_mode,
|
|||
default_search_algo_args = {
|
||||
"low_cost_partial_config": {
|
||||
"num_train_epochs": min_epoch,
|
||||
"per_device_train_batch_size": max(hpo_search_space["per_device_train_batch_size"].categories),
|
||||
"per_device_train_batch_size": max(
|
||||
hpo_search_space["per_device_train_batch_size"].categories
|
||||
),
|
||||
},
|
||||
"space": hpo_search_space,
|
||||
"metric": metric_name,
|
||||
"mode": metric_mode_name
|
||||
"mode": metric_mode_name,
|
||||
"time_budget_s": time_budget_s,
|
||||
}
|
||||
if search_args_mode == "cus":
|
||||
default_search_algo_args.update(custom_hpo_args)
|
||||
return default_search_algo_args
|
||||
|
||||
|
||||
def default_search_algo_args_grid_search(search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args):
|
||||
def default_search_algo_args_grid_search(
|
||||
search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args
|
||||
):
|
||||
# TODO coverage
|
||||
return {}
|
||||
|
||||
|
||||
def default_search_algo_args_random_search(search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args):
|
||||
def default_search_algo_args_random_search(
|
||||
search_args_mode,
|
||||
metric_name,
|
||||
metric_mode_name,
|
||||
hpo_search_space=None,
|
||||
**custom_hpo_args
|
||||
):
|
||||
# TODO coverage
|
||||
return {}
|
||||
|
||||
|
@ -191,6 +226,5 @@ DEFAULT_SEARCH_ALGO_ARGS_MAPPING = OrderedDict(
|
|||
("cfo", default_search_algo_args_bs),
|
||||
("bs", default_search_algo_args_bs),
|
||||
("grid", default_search_algo_args_grid_search),
|
||||
("gridbert", default_search_algo_args_random_search)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import time
|
||||
import numpy as np
|
||||
import math
|
||||
from flaml.tune import Trial
|
||||
from flaml.scheduler import TrialScheduler
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -45,16 +45,18 @@ class OnlineTrialRunner:
|
|||
Status change routine of a trial
|
||||
Trial.PENDING -> (Trial.RUNNING -> Trial.PAUSED -> Trial.RUNNING -> ...) -> Trial.TERMINATED(optional)
|
||||
"""
|
||||
|
||||
RANDOM_SEED = 123456
|
||||
WARMSTART_NUM = 100
|
||||
|
||||
def __init__(self,
|
||||
max_live_model_num: int,
|
||||
searcher=None,
|
||||
scheduler=None,
|
||||
champion_test_policy='loss_ucb',
|
||||
**kwargs
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
max_live_model_num: int,
|
||||
searcher=None,
|
||||
scheduler=None,
|
||||
champion_test_policy="loss_ucb",
|
||||
**kwargs
|
||||
):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
|
@ -64,7 +66,7 @@ class OnlineTrialRunner:
|
|||
Required methods of the searcher:
|
||||
- next_trial()
|
||||
Generate the next trial to add.
|
||||
- set_search_properties(metric: Optional[str], mode: Optional[str], config: dict)
|
||||
- set_search_properties(metric: Optional[str], mode: Optional[str], config: Optional[dict], setting: Optional[dict])
|
||||
Generate new challengers based on the current champion and update the challenger list
|
||||
- on_trial_result(trial_id: str, result: Dict)
|
||||
Reprot results to the scheduler.
|
||||
|
@ -87,8 +89,8 @@ class OnlineTrialRunner:
|
|||
self._scheduler = scheduler
|
||||
self._champion_test_policy = champion_test_policy
|
||||
self._max_live_model_num = max_live_model_num
|
||||
self._remove_worse = kwargs.get('remove_worse', True)
|
||||
self._bound_trial_num = kwargs.get('bound_trial_num', False)
|
||||
self._remove_worse = kwargs.get("remove_worse", True)
|
||||
self._bound_trial_num = kwargs.get("bound_trial_num", False)
|
||||
self._no_model_persistence = True
|
||||
|
||||
# stores all the trials added to the OnlineTrialRunner
|
||||
|
@ -103,21 +105,19 @@ class OnlineTrialRunner:
|
|||
# initially schedule up to max_live_model_num of live models and
|
||||
# set the first trial as the champion (which is done inside self.step())
|
||||
self._total_steps = 0
|
||||
logger.info('init step %s', self._max_live_model_num)
|
||||
logger.info("init step %s", self._max_live_model_num)
|
||||
# TODO: add more comments
|
||||
self.step()
|
||||
assert self._champion_trial is not None
|
||||
|
||||
@property
|
||||
def champion_trial(self) -> Trial:
|
||||
"""The champion trial
|
||||
"""
|
||||
"""The champion trial"""
|
||||
return self._champion_trial
|
||||
|
||||
@property
|
||||
def running_trials(self):
|
||||
"""The running/'live' trials
|
||||
"""
|
||||
"""The running/'live' trials"""
|
||||
return self._running_trials
|
||||
|
||||
def step(self, data_sample=None, prediction_trial_tuple=None):
|
||||
|
@ -147,7 +147,10 @@ class OnlineTrialRunner:
|
|||
# ***********Update running trials with observation***************************
|
||||
if data_sample is not None:
|
||||
self._total_steps += 1
|
||||
prediction_made, prediction_trial = prediction_trial_tuple[0], prediction_trial_tuple[1]
|
||||
prediction_made, prediction_trial = (
|
||||
prediction_trial_tuple[0],
|
||||
prediction_trial_tuple[1],
|
||||
)
|
||||
# assert prediction_trial.status == Trial.RUNNING
|
||||
trials_to_pause = []
|
||||
for trial in list(self._running_trials):
|
||||
|
@ -156,16 +159,27 @@ class OnlineTrialRunner:
|
|||
else:
|
||||
y_predicted = prediction_made
|
||||
trial.train_eval_model_online(data_sample, y_predicted)
|
||||
logger.debug('running trial at iter %s %s %s %s %s %s', self._total_steps,
|
||||
trial.trial_id, trial.result.loss_avg, trial.result.loss_cb,
|
||||
trial.result.resource_used, trial.resource_lease)
|
||||
logger.debug(
|
||||
"running trial at iter %s %s %s %s %s %s",
|
||||
self._total_steps,
|
||||
trial.trial_id,
|
||||
trial.result.loss_avg,
|
||||
trial.result.loss_cb,
|
||||
trial.result.resource_used,
|
||||
trial.resource_lease,
|
||||
)
|
||||
# report result to the searcher
|
||||
self._searcher.on_trial_result(trial.trial_id, trial.result)
|
||||
# report result to the scheduler and the scheduler makes a decision about
|
||||
# the running status of the trial
|
||||
decision = self._scheduler.on_trial_result(self, trial, trial.result)
|
||||
# set the status of the trial according to the decision made by the scheduler
|
||||
logger.debug('trial decision %s %s at step %s', decision, trial.trial_id, self._total_steps)
|
||||
logger.debug(
|
||||
"trial decision %s %s at step %s",
|
||||
decision,
|
||||
trial.trial_id,
|
||||
self._total_steps,
|
||||
)
|
||||
if decision == TrialScheduler.STOP:
|
||||
self.stop_trial(trial)
|
||||
elif decision == TrialScheduler.PAUSE:
|
||||
|
@ -191,38 +205,45 @@ class OnlineTrialRunner:
|
|||
else:
|
||||
break
|
||||
|
||||
def get_top_running_trials(self, top_ratio=None, top_metric='ucb') -> list:
|
||||
"""Get a list of trial ids, whose performance is among the top running trials
|
||||
"""
|
||||
running_valid_trials = [trial for trial in self._running_trials if
|
||||
trial.result is not None]
|
||||
def get_top_running_trials(self, top_ratio=None, top_metric="ucb") -> list:
|
||||
"""Get a list of trial ids, whose performance is among the top running trials"""
|
||||
running_valid_trials = [
|
||||
trial for trial in self._running_trials if trial.result is not None
|
||||
]
|
||||
if not running_valid_trials:
|
||||
return
|
||||
if top_ratio is None:
|
||||
top_number = 0
|
||||
elif isinstance(top_ratio, float):
|
||||
top_number = math.ceil(len(running_valid_trials) * top_ratio)
|
||||
elif isinstance(top_ratio, str) and 'best' in top_ratio:
|
||||
elif isinstance(top_ratio, str) and "best" in top_ratio:
|
||||
top_number = 1
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
if 'ucb' in top_metric:
|
||||
test_attribute = 'loss_ucb'
|
||||
elif 'avg' in top_metric:
|
||||
test_attribute = 'loss_avg'
|
||||
elif 'lcb' in top_metric:
|
||||
test_attribute = 'loss_lcb'
|
||||
if "ucb" in top_metric:
|
||||
test_attribute = "loss_ucb"
|
||||
elif "avg" in top_metric:
|
||||
test_attribute = "loss_avg"
|
||||
elif "lcb" in top_metric:
|
||||
test_attribute = "loss_lcb"
|
||||
else:
|
||||
raise NotImplementedError
|
||||
top_running_valid_trials = []
|
||||
logger.info('Running trial ids %s', [trial.trial_id for trial in running_valid_trials])
|
||||
logger.info(
|
||||
"Running trial ids %s", [trial.trial_id for trial in running_valid_trials]
|
||||
)
|
||||
self._random_state.shuffle(running_valid_trials)
|
||||
results = [trial.result.get_score(test_attribute) for trial in running_valid_trials]
|
||||
sorted_index = np.argsort(np.array(results)) # sorted result (small to large) index
|
||||
results = [
|
||||
trial.result.get_score(test_attribute) for trial in running_valid_trials
|
||||
]
|
||||
# sorted result (small to large) index
|
||||
sorted_index = np.argsort(np.array(results))
|
||||
for i in range(min(top_number, len(running_valid_trials))):
|
||||
top_running_valid_trials.append(running_valid_trials[sorted_index[i]])
|
||||
logger.info('Top running ids %s', [trial.trial_id for trial in top_running_valid_trials])
|
||||
logger.info(
|
||||
"Top running ids %s", [trial.trial_id for trial in top_running_valid_trials]
|
||||
)
|
||||
return top_running_valid_trials
|
||||
|
||||
def _add_trial_from_searcher(self):
|
||||
|
@ -234,12 +255,25 @@ class OnlineTrialRunner:
|
|||
"""
|
||||
# (optionally) upper bound the number of trials in the OnlineTrialRunner
|
||||
if self._bound_trial_num and self._first_challenger_pool_size is not None:
|
||||
active_trial_size = len([t for t in self._trials if t.status != Trial.TERMINATED])
|
||||
trial_num_upper_bound = int(round((np.log10(self._total_steps) + 1) * self._first_challenger_pool_size)
|
||||
) if self._first_challenger_pool_size else np.inf
|
||||
active_trial_size = len(
|
||||
[t for t in self._trials if t.status != Trial.TERMINATED]
|
||||
)
|
||||
trial_num_upper_bound = (
|
||||
int(
|
||||
round(
|
||||
(np.log10(self._total_steps) + 1)
|
||||
* self._first_challenger_pool_size
|
||||
)
|
||||
)
|
||||
if self._first_challenger_pool_size
|
||||
else np.inf
|
||||
)
|
||||
if active_trial_size > trial_num_upper_bound:
|
||||
logger.info('Not adding new trials: %s exceeds trial limit %s.',
|
||||
active_trial_size, trial_num_upper_bound)
|
||||
logger.info(
|
||||
"Not adding new trials: %s exceeds trial limit %s.",
|
||||
active_trial_size,
|
||||
trial_num_upper_bound,
|
||||
)
|
||||
return None
|
||||
|
||||
# output one trial from the trial pool (new challenger pool) maintained in the searcher
|
||||
|
@ -253,7 +287,7 @@ class OnlineTrialRunner:
|
|||
# a valid trial is added.
|
||||
# Assumption on self._searcher: the first trial generated is the champion trial
|
||||
if self._champion_trial is None:
|
||||
logger.info('Initial set up of the champion trial %s', trial.config)
|
||||
logger.info("Initial set up of the champion trial %s", trial.config)
|
||||
self._set_champion(trial)
|
||||
else:
|
||||
self._all_new_challengers_added = True
|
||||
|
@ -261,14 +295,15 @@ class OnlineTrialRunner:
|
|||
self._first_challenger_pool_size = len(self._trials)
|
||||
|
||||
def _champion_test(self):
|
||||
"""Perform tests again the latest champion, including bette_than tests and worse_than tests
|
||||
"""
|
||||
"""Perform tests again the latest champion, including bette_than tests and worse_than tests"""
|
||||
# for BetterThan test, we only need to compare the best challenger with the champion
|
||||
self._get_best_challenger()
|
||||
if self._best_challenger_trial is not None:
|
||||
assert self._best_challenger_trial.trial_id != self._champion_trial.trial_id
|
||||
# test whether a new champion is found and set the trial properties accordingly
|
||||
is_new_champion_found = self._better_than_champion_test(self._best_challenger_trial)
|
||||
is_new_champion_found = self._better_than_champion_test(
|
||||
self._best_challenger_trial
|
||||
)
|
||||
if is_new_champion_found:
|
||||
self._set_champion(new_champion_trial=self._best_challenger_trial)
|
||||
|
||||
|
@ -278,39 +313,47 @@ class OnlineTrialRunner:
|
|||
for trial_to_test in self._trials:
|
||||
if trial_to_test.status != Trial.TERMINATED:
|
||||
worse_than_champion = self._worse_than_champion_test(
|
||||
self._champion_trial, trial_to_test, self.WARMSTART_NUM)
|
||||
self._champion_trial, trial_to_test, self.WARMSTART_NUM
|
||||
)
|
||||
if worse_than_champion:
|
||||
to_stop.append(trial_to_test)
|
||||
# we want to ensure there are at least #max_live_model_num of challengers remaining
|
||||
max_to_stop_num = len([t for t in self._trials if t.status != Trial.TERMINATED]
|
||||
) - self._max_live_model_num
|
||||
max_to_stop_num = (
|
||||
len([t for t in self._trials if t.status != Trial.TERMINATED])
|
||||
- self._max_live_model_num
|
||||
)
|
||||
for i in range(min(max_to_stop_num, len(to_stop))):
|
||||
self.stop_trial(to_stop[i])
|
||||
|
||||
def _get_best_challenger(self):
|
||||
"""Get the 'best' (in terms of the champion_test_policy) challenger under consideration.
|
||||
"""
|
||||
"""Get the 'best' (in terms of the champion_test_policy) challenger under consideration."""
|
||||
if self._champion_test_policy is None:
|
||||
return
|
||||
if 'ucb' in self._champion_test_policy:
|
||||
test_attribute = 'loss_ucb'
|
||||
elif 'avg' in self._champion_test_policy:
|
||||
test_attribute = 'loss_avg'
|
||||
if "ucb" in self._champion_test_policy:
|
||||
test_attribute = "loss_ucb"
|
||||
elif "avg" in self._champion_test_policy:
|
||||
test_attribute = "loss_avg"
|
||||
else:
|
||||
raise NotImplementedError
|
||||
active_trials = [trial for trial in self._trials if
|
||||
(trial.status != Trial.TERMINATED
|
||||
and trial.trial_id != self._champion_trial.trial_id
|
||||
and trial.result is not None)]
|
||||
active_trials = [
|
||||
trial
|
||||
for trial in self._trials
|
||||
if (
|
||||
trial.status != Trial.TERMINATED
|
||||
and trial.trial_id != self._champion_trial.trial_id
|
||||
and trial.result is not None
|
||||
)
|
||||
]
|
||||
if active_trials:
|
||||
self._random_state.shuffle(active_trials)
|
||||
results = [trial.result.get_score(test_attribute) for trial in active_trials]
|
||||
results = [
|
||||
trial.result.get_score(test_attribute) for trial in active_trials
|
||||
]
|
||||
best_index = np.argmin(results)
|
||||
self._best_challenger_trial = active_trials[best_index]
|
||||
|
||||
def _set_champion(self, new_champion_trial):
|
||||
"""Set the status of the existing trials once a new champion is found.
|
||||
"""
|
||||
"""Set the status of the existing trials once a new champion is found."""
|
||||
assert new_champion_trial is not None
|
||||
is_init_update = False
|
||||
if self._champion_trial is None:
|
||||
|
@ -324,21 +367,20 @@ class OnlineTrialRunner:
|
|||
trial.set_checked_under_current_champion(False)
|
||||
self._champion_trial = new_champion_trial
|
||||
self._all_new_challengers_added = False
|
||||
logger.info('Set the champion as %s', self._champion_trial.trial_id)
|
||||
logger.info("Set the champion as %s", self._champion_trial.trial_id)
|
||||
if not is_init_update:
|
||||
self._champion_update_times += 1
|
||||
# calling set_search_properties of searcher will trigger
|
||||
# new challenger generation. we do not do this for init champion
|
||||
# as this step is already done when first constructing the searcher
|
||||
self._searcher.set_search_properties(None, None,
|
||||
{self._searcher.CHAMPION_TRIAL_NAME: self._champion_trial}
|
||||
)
|
||||
self._searcher.set_search_properties(
|
||||
setting={self._searcher.CHAMPION_TRIAL_NAME: self._champion_trial}
|
||||
)
|
||||
else:
|
||||
self._champion_update_times = 0
|
||||
|
||||
def get_trials(self) -> list:
|
||||
"""Return the list of trials managed by this TrialRunner.
|
||||
"""
|
||||
"""Return the list of trials managed by this TrialRunner."""
|
||||
return self._trials
|
||||
|
||||
def add_trial(self, new_trial):
|
||||
|
@ -357,8 +399,12 @@ class OnlineTrialRunner:
|
|||
if trial.trial_id == new_trial.trial_id:
|
||||
trial.set_checked_under_current_champion(True)
|
||||
return
|
||||
logger.info('adding trial at iter %s, %s %s', self._total_steps, new_trial.trial_id,
|
||||
len(self._trials))
|
||||
logger.info(
|
||||
"adding trial at iter %s, %s %s",
|
||||
self._total_steps,
|
||||
new_trial.trial_id,
|
||||
len(self._trials),
|
||||
)
|
||||
self._trials.append(new_trial)
|
||||
self._scheduler.on_trial_add(self, new_trial)
|
||||
|
||||
|
@ -369,8 +415,11 @@ class OnlineTrialRunner:
|
|||
if trial.status in [Trial.ERROR, Trial.TERMINATED]:
|
||||
return
|
||||
else:
|
||||
logger.info('Terminating trial %s, with trial result %s',
|
||||
trial.trial_id, trial.result)
|
||||
logger.info(
|
||||
"Terminating trial %s, with trial result %s",
|
||||
trial.trial_id,
|
||||
trial.result,
|
||||
)
|
||||
trial.set_status(Trial.TERMINATED)
|
||||
# clean up model and result
|
||||
trial.clean_up_model()
|
||||
|
@ -385,10 +434,15 @@ class OnlineTrialRunner:
|
|||
if trial.status in [Trial.ERROR, Trial.TERMINATED]:
|
||||
return
|
||||
else:
|
||||
logger.info('Pausing trial %s, with trial loss_avg: %s, loss_cb: %s, loss_ucb: %s,\
|
||||
resource_lease: %s', trial.trial_id, trial.result.loss_avg,
|
||||
trial.result.loss_cb, trial.result.loss_avg + trial.result.loss_cb,
|
||||
trial.resource_lease)
|
||||
logger.info(
|
||||
"Pausing trial %s, with trial loss_avg: %s, loss_cb: %s, loss_ucb: %s,\
|
||||
resource_lease: %s",
|
||||
trial.trial_id,
|
||||
trial.result.loss_avg,
|
||||
trial.result.loss_cb,
|
||||
trial.result.loss_avg + trial.result.loss_cb,
|
||||
trial.resource_lease,
|
||||
)
|
||||
trial.set_status(Trial.PAUSED)
|
||||
# clean up model and result if no model persistence
|
||||
if self._no_model_persistence:
|
||||
|
@ -413,11 +467,15 @@ class OnlineTrialRunner:
|
|||
A bool indicating whether a new champion is found
|
||||
"""
|
||||
if trial_to_test.result is not None and self._champion_trial.result is not None:
|
||||
if 'ucb' in self._champion_test_policy:
|
||||
return self._test_lcb_ucb(self._champion_trial, trial_to_test, self.WARMSTART_NUM)
|
||||
elif 'avg' in self._champion_test_policy:
|
||||
return self._test_avg_loss(self._champion_trial, trial_to_test, self.WARMSTART_NUM)
|
||||
elif 'martingale' in self._champion_test_policy:
|
||||
if "ucb" in self._champion_test_policy:
|
||||
return self._test_lcb_ucb(
|
||||
self._champion_trial, trial_to_test, self.WARMSTART_NUM
|
||||
)
|
||||
elif "avg" in self._champion_test_policy:
|
||||
return self._test_avg_loss(
|
||||
self._champion_trial, trial_to_test, self.WARMSTART_NUM
|
||||
)
|
||||
elif "martingale" in self._champion_test_policy:
|
||||
return self._test_martingale(self._champion_trial, trial_to_test)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
@ -426,22 +484,38 @@ class OnlineTrialRunner:
|
|||
|
||||
@staticmethod
|
||||
def _worse_than_champion_test(champion_trial, trial, warmstart_num=1) -> bool:
|
||||
"""Test whether the input trial is worse than the champion_trial
|
||||
"""
|
||||
"""Test whether the input trial is worse than the champion_trial"""
|
||||
if trial.result is not None and trial.result.resource_used >= warmstart_num:
|
||||
if trial.result.loss_lcb > champion_trial.result.loss_ucb:
|
||||
logger.info('=========trial %s is worse than champion %s=====',
|
||||
trial.trial_id, champion_trial.trial_id)
|
||||
logger.info('trial %s %s %s', trial.config, trial.result, trial.resource_lease)
|
||||
logger.info('trial loss_avg:%s, trial loss_cb %s', trial.result.loss_avg,
|
||||
trial.result.loss_cb)
|
||||
logger.info('champion loss_avg:%s, champion loss_cb %s', champion_trial.result.loss_avg,
|
||||
champion_trial.result.loss_cb)
|
||||
logger.info('champion %s', champion_trial.config)
|
||||
logger.info('trial loss_avg_recent:%s, trial loss_cb %s', trial.result.loss_avg_recent,
|
||||
trial.result.loss_cb)
|
||||
logger.info('champion loss_avg_recent:%s, champion loss_cb %s',
|
||||
champion_trial.result.loss_avg_recent, champion_trial.result.loss_cb)
|
||||
logger.info(
|
||||
"=========trial %s is worse than champion %s=====",
|
||||
trial.trial_id,
|
||||
champion_trial.trial_id,
|
||||
)
|
||||
logger.info(
|
||||
"trial %s %s %s", trial.config, trial.result, trial.resource_lease
|
||||
)
|
||||
logger.info(
|
||||
"trial loss_avg:%s, trial loss_cb %s",
|
||||
trial.result.loss_avg,
|
||||
trial.result.loss_cb,
|
||||
)
|
||||
logger.info(
|
||||
"champion loss_avg:%s, champion loss_cb %s",
|
||||
champion_trial.result.loss_avg,
|
||||
champion_trial.result.loss_cb,
|
||||
)
|
||||
logger.info("champion %s", champion_trial.config)
|
||||
logger.info(
|
||||
"trial loss_avg_recent:%s, trial loss_cb %s",
|
||||
trial.result.loss_avg_recent,
|
||||
trial.result.loss_cb,
|
||||
)
|
||||
logger.info(
|
||||
"champion loss_avg_recent:%s, champion loss_cb %s",
|
||||
champion_trial.result.loss_avg_recent,
|
||||
champion_trial.result.loss_cb,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -452,18 +526,35 @@ class OnlineTrialRunner:
|
|||
"""
|
||||
assert trial.trial_id != champion_trial.trial_id
|
||||
if trial.result.resource_used >= warmstart_num:
|
||||
if trial.result.loss_ucb < champion_trial.result.loss_lcb - champion_trial.result.loss_cb:
|
||||
logger.info('======new champion condition satisfied: using lcb vs ucb=====')
|
||||
logger.info('new champion trial %s %s %s',
|
||||
trial.trial_id, trial.result.resource_used, trial.resource_lease)
|
||||
logger.info('new champion trial loss_avg:%s, trial loss_cb %s',
|
||||
trial.result.loss_avg, trial.result.loss_cb)
|
||||
logger.info('old champion trial %s %s %s',
|
||||
champion_trial.trial_id, champion_trial.result.resource_used,
|
||||
champion_trial.resource_lease,)
|
||||
logger.info('old champion loss avg %s, loss cb %s',
|
||||
champion_trial.result.loss_avg,
|
||||
champion_trial.result.loss_cb)
|
||||
if (
|
||||
trial.result.loss_ucb
|
||||
< champion_trial.result.loss_lcb - champion_trial.result.loss_cb
|
||||
):
|
||||
logger.info(
|
||||
"======new champion condition satisfied: using lcb vs ucb====="
|
||||
)
|
||||
logger.info(
|
||||
"new champion trial %s %s %s",
|
||||
trial.trial_id,
|
||||
trial.result.resource_used,
|
||||
trial.resource_lease,
|
||||
)
|
||||
logger.info(
|
||||
"new champion trial loss_avg:%s, trial loss_cb %s",
|
||||
trial.result.loss_avg,
|
||||
trial.result.loss_cb,
|
||||
)
|
||||
logger.info(
|
||||
"old champion trial %s %s %s",
|
||||
champion_trial.trial_id,
|
||||
champion_trial.result.resource_used,
|
||||
champion_trial.resource_lease,
|
||||
)
|
||||
logger.info(
|
||||
"old champion loss avg %s, loss cb %s",
|
||||
champion_trial.result.loss_avg,
|
||||
champion_trial.result.loss_cb,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -475,13 +566,19 @@ class OnlineTrialRunner:
|
|||
assert trial.trial_id != champion_trial.trial_id
|
||||
if trial.result.resource_used >= warmstart_num:
|
||||
if trial.result.loss_avg < champion_trial.result.loss_avg:
|
||||
logger.info('=====new champion condition satisfied using avg loss=====')
|
||||
logger.info('trial %s', trial.config)
|
||||
logger.info('trial loss_avg:%s, trial loss_cb %s',
|
||||
trial.result.loss_avg, trial.result.loss_cb)
|
||||
logger.info('champion loss_avg:%s, champion loss_cb %s',
|
||||
champion_trial.result.loss_avg, champion_trial.result.loss_cb)
|
||||
logger.info('champion %s', champion_trial.config)
|
||||
logger.info("=====new champion condition satisfied using avg loss=====")
|
||||
logger.info("trial %s", trial.config)
|
||||
logger.info(
|
||||
"trial loss_avg:%s, trial loss_cb %s",
|
||||
trial.result.loss_avg,
|
||||
trial.result.loss_cb,
|
||||
)
|
||||
logger.info(
|
||||
"champion loss_avg:%s, champion loss_cb %s",
|
||||
champion_trial.result.loss_avg,
|
||||
champion_trial.result.loss_cb,
|
||||
)
|
||||
logger.info("champion %s", champion_trial.config)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
@ -129,11 +129,12 @@ class BlendSearch(Searcher):
|
|||
self._metric, self._mode = metric, mode
|
||||
init_config = low_cost_partial_config or {}
|
||||
if not init_config:
|
||||
logger.warning(
|
||||
logger.info(
|
||||
"No low-cost partial config given to the search algorithm. "
|
||||
"For cost-frugal search, "
|
||||
"consider providing low-cost values for cost-related hps via "
|
||||
"'low_cost_partial_config'."
|
||||
"'low_cost_partial_config'. More info can be found at "
|
||||
"https://github.com/microsoft/FLAML/wiki/About-%60low_cost_partial_config%60"
|
||||
)
|
||||
if evaluated_rewards and mode:
|
||||
self._points_to_evaluate = []
|
||||
|
@ -228,6 +229,7 @@ class BlendSearch(Searcher):
|
|||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
setting: Optional[Dict] = None,
|
||||
) -> bool:
|
||||
metric_changed = mode_changed = False
|
||||
if metric and self._metric != metric:
|
||||
|
@ -264,22 +266,22 @@ class BlendSearch(Searcher):
|
|||
)
|
||||
self._gs.space = self._ls.space
|
||||
self._init_search()
|
||||
if config:
|
||||
# CFO doesn't need these settings
|
||||
if "time_budget_s" in config:
|
||||
self._time_budget_s = config["time_budget_s"] # budget from now
|
||||
now = time.time()
|
||||
self._time_used += now - self._start_time
|
||||
self._start_time = now
|
||||
self._set_deadline()
|
||||
if "metric_target" in config:
|
||||
self._metric_target = config.get("metric_target")
|
||||
if "num_samples" in config:
|
||||
self._num_samples = (
|
||||
config["num_samples"]
|
||||
+ len(self._result)
|
||||
+ len(self._trial_proposed_by)
|
||||
)
|
||||
if setting:
|
||||
# CFO doesn't need these settings
|
||||
if "time_budget_s" in setting:
|
||||
self._time_budget_s = setting["time_budget_s"] # budget from now
|
||||
now = time.time()
|
||||
self._time_used += now - self._start_time
|
||||
self._start_time = now
|
||||
self._set_deadline()
|
||||
if "metric_target" in setting:
|
||||
self._metric_target = setting.get("metric_target")
|
||||
if "num_samples" in setting:
|
||||
self._num_samples = (
|
||||
setting["num_samples"]
|
||||
+ len(self._result)
|
||||
+ len(self._trial_proposed_by)
|
||||
)
|
||||
return True
|
||||
|
||||
def _set_deadline(self):
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
'''!
|
||||
* Copyright (c) 2020-2021 Microsoft Corporation. All rights reserved.
|
||||
"""!
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE file in the
|
||||
* project root for license information.
|
||||
'''
|
||||
"""
|
||||
from flaml.tune.sample import Domain
|
||||
from typing import Dict, Optional, Tuple
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from ray import __version__ as ray_version
|
||||
assert ray_version >= '1.0.0'
|
||||
|
||||
assert ray_version >= "1.0.0"
|
||||
from ray.tune.suggest import Searcher
|
||||
from ray.tune.suggest.variant_generator import generate_variants
|
||||
from ray.tune import sample
|
||||
|
@ -22,28 +24,30 @@ from ..tune.space import complete_config, denormalize, normalize
|
|||
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FLOW2(Searcher):
|
||||
'''Local search algorithm FLOW2, with adaptive step size
|
||||
'''
|
||||
"""Local search algorithm FLOW2, with adaptive step size"""
|
||||
|
||||
STEPSIZE = 0.1
|
||||
STEP_LOWER_BOUND = 0.0001
|
||||
|
||||
def __init__(self,
|
||||
init_config: dict,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
space: Optional[dict] = None,
|
||||
prune_attr: Optional[str] = None,
|
||||
min_resource: Optional[float] = None,
|
||||
max_resource: Optional[float] = None,
|
||||
resource_multiple_factor: Optional[float] = 4,
|
||||
cost_attr: Optional[str] = 'time_total_s',
|
||||
seed: Optional[int] = 20):
|
||||
'''Constructor
|
||||
def __init__(
|
||||
self,
|
||||
init_config: dict,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
space: Optional[dict] = None,
|
||||
prune_attr: Optional[str] = None,
|
||||
min_resource: Optional[float] = None,
|
||||
max_resource: Optional[float] = None,
|
||||
resource_multiple_factor: Optional[float] = 4,
|
||||
cost_attr: Optional[str] = "time_total_s",
|
||||
seed: Optional[int] = 20,
|
||||
):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
init_config: a dictionary of a partial or full initial config,
|
||||
|
@ -79,20 +83,18 @@ class FLOW2(Searcher):
|
|||
used for increasing resource.
|
||||
cost_attr: A string of the attribute used for cost.
|
||||
seed: An integer of the random seed.
|
||||
'''
|
||||
"""
|
||||
if mode:
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
|
||||
else:
|
||||
mode = "min"
|
||||
|
||||
super(FLOW2, self).__init__(
|
||||
metric=metric,
|
||||
mode=mode)
|
||||
super(FLOW2, self).__init__(metric=metric, mode=mode)
|
||||
# internally minimizes, so "max" => -1
|
||||
if mode == "max":
|
||||
self.metric_op = -1.
|
||||
self.metric_op = -1.0
|
||||
elif mode == "min":
|
||||
self.metric_op = 1.
|
||||
self.metric_op = 1.0
|
||||
self.space = space or {}
|
||||
self._space = flatten_dict(self.space, prevent_delimiter=True)
|
||||
self._random = np.random.RandomState(seed)
|
||||
|
@ -106,7 +108,7 @@ class FLOW2(Searcher):
|
|||
self.max_resource = max_resource
|
||||
self._resource = None
|
||||
self._step_lb = np.Inf
|
||||
if space:
|
||||
if space is not None:
|
||||
self._init_search()
|
||||
|
||||
def _init_search(self):
|
||||
|
@ -115,9 +117,10 @@ class FLOW2(Searcher):
|
|||
self._unordered_cat_hp = {}
|
||||
hier = False
|
||||
for key, domain in self._space.items():
|
||||
assert not (isinstance(domain, dict) and 'grid_search' in domain), \
|
||||
f"{key}'s domain is grid search, not supported in FLOW^2."
|
||||
if callable(getattr(domain, 'get_sampler', None)):
|
||||
assert not (
|
||||
isinstance(domain, dict) and "grid_search" in domain
|
||||
), f"{key}'s domain is grid search, not supported in FLOW^2."
|
||||
if callable(getattr(domain, "get_sampler", None)):
|
||||
self._tunable_keys.append(key)
|
||||
sampler = domain.get_sampler()
|
||||
# the step size lower bound for uniform variables doesn't depend
|
||||
|
@ -125,12 +128,14 @@ class FLOW2(Searcher):
|
|||
if isinstance(sampler, sample.Quantized):
|
||||
q = sampler.q
|
||||
sampler = sampler.get_sampler()
|
||||
if str(sampler) == 'Uniform':
|
||||
if str(sampler) == "Uniform":
|
||||
self._step_lb = min(
|
||||
self._step_lb, q / (domain.upper - domain.lower))
|
||||
elif isinstance(domain, sample.Integer) and str(sampler) == 'Uniform':
|
||||
self._step_lb, q / (domain.upper - domain.lower)
|
||||
)
|
||||
elif isinstance(domain, sample.Integer) and str(sampler) == "Uniform":
|
||||
self._step_lb = min(
|
||||
self._step_lb, 1.0 / (domain.upper - 1 - domain.lower))
|
||||
self._step_lb, 1.0 / (domain.upper - 1 - domain.lower)
|
||||
)
|
||||
if isinstance(domain, sample.Categorical):
|
||||
if not domain.ordered:
|
||||
self._unordered_cat_hp[key] = len(domain.categories)
|
||||
|
@ -139,13 +144,12 @@ class FLOW2(Searcher):
|
|||
if isinstance(cat, dict):
|
||||
hier = True
|
||||
break
|
||||
if str(sampler) != 'Normal':
|
||||
if str(sampler) != "Normal":
|
||||
self._bounded_keys.append(key)
|
||||
if not hier:
|
||||
self._space_keys = sorted(self._tunable_keys)
|
||||
self.hierarchical = hier
|
||||
if (self.prune_attr and self.prune_attr not in self._space
|
||||
and self.max_resource):
|
||||
if self.prune_attr and self.prune_attr not in self._space and self.max_resource:
|
||||
self.min_resource = self.min_resource or self._min_resource()
|
||||
self._resource = self._round(self.min_resource)
|
||||
if not hier:
|
||||
|
@ -169,10 +173,11 @@ class FLOW2(Searcher):
|
|||
if self.step > self.step_ub:
|
||||
self.step = self.step_ub
|
||||
# maximal # consecutive no improvements
|
||||
self.dir = 2**(min(9, self.dim))
|
||||
self.dir = 2 ** (min(9, self.dim))
|
||||
self._configs = {} # dict from trial_id to (config, stepsize)
|
||||
self._K = 0
|
||||
self._iter_best_config = self.trial_count_proposed = self.trial_count_complete = 1
|
||||
self._iter_best_config = 1
|
||||
self.trial_count_proposed = self.trial_count_complete = 1
|
||||
self._num_proposedby_incumbent = 0
|
||||
self._reset_times = 0
|
||||
# record intermediate trial cost
|
||||
|
@ -196,14 +201,18 @@ class FLOW2(Searcher):
|
|||
if isinstance(sampler, sample.Quantized):
|
||||
q = sampler.q
|
||||
sampler_inner = sampler.get_sampler()
|
||||
if str(sampler_inner) == 'LogUniform':
|
||||
if str(sampler_inner) == "LogUniform":
|
||||
step_lb = min(
|
||||
step_lb, np.log(1.0 + q / self.best_config[key])
|
||||
/ np.log(domain.upper / domain.lower))
|
||||
elif isinstance(domain, sample.Integer) and str(sampler) == 'LogUniform':
|
||||
step_lb,
|
||||
np.log(1.0 + q / self.best_config[key])
|
||||
/ np.log(domain.upper / domain.lower),
|
||||
)
|
||||
elif isinstance(domain, sample.Integer) and str(sampler) == "LogUniform":
|
||||
step_lb = min(
|
||||
step_lb, np.log(1.0 + 1.0 / self.best_config[key])
|
||||
/ np.log((domain.upper - 1) / domain.lower))
|
||||
step_lb,
|
||||
np.log(1.0 + 1.0 / self.best_config[key])
|
||||
/ np.log((domain.upper - 1) / domain.lower),
|
||||
)
|
||||
if np.isinf(step_lb):
|
||||
step_lb = self.STEP_LOWER_BOUND
|
||||
else:
|
||||
|
@ -215,13 +224,11 @@ class FLOW2(Searcher):
|
|||
return self._resource
|
||||
|
||||
def _min_resource(self) -> float:
|
||||
''' automatically decide minimal resource
|
||||
'''
|
||||
"""automatically decide minimal resource"""
|
||||
return self.max_resource / np.pow(self.resource_multiple_factor, 5)
|
||||
|
||||
def _round(self, resource) -> float:
|
||||
''' round the resource to self.max_resource if close to it
|
||||
'''
|
||||
"""round the resource to self.max_resource if close to it"""
|
||||
if resource * self.resource_multiple_factor > self.max_resource:
|
||||
return self.max_resource
|
||||
return resource
|
||||
|
@ -231,70 +238,83 @@ class FLOW2(Searcher):
|
|||
return vec
|
||||
|
||||
def complete_config(
|
||||
self, partial_config: Dict,
|
||||
lower: Optional[Dict] = None, upper: Optional[Dict] = None
|
||||
self,
|
||||
partial_config: Dict,
|
||||
lower: Optional[Dict] = None,
|
||||
upper: Optional[Dict] = None,
|
||||
) -> Tuple[Dict, Dict]:
|
||||
''' generate a complete config from the partial config input
|
||||
"""generate a complete config from the partial config input
|
||||
add minimal resource to config if available
|
||||
'''
|
||||
"""
|
||||
disturb = self._reset_times and partial_config == self.init_config
|
||||
# if not the first time to complete init_config, use random gaussian
|
||||
config, space = complete_config(
|
||||
partial_config, self.space, self, disturb, lower, upper)
|
||||
partial_config, self.space, self, disturb, lower, upper
|
||||
)
|
||||
if partial_config == self.init_config:
|
||||
self._reset_times += 1
|
||||
if self._resource:
|
||||
config[self.prune_attr] = self.min_resource
|
||||
return config, space
|
||||
|
||||
def create(self, init_config: Dict, obj: float, cost: float, space: Dict
|
||||
) -> Searcher:
|
||||
def create(
|
||||
self, init_config: Dict, obj: float, cost: float, space: Dict
|
||||
) -> Searcher:
|
||||
# space is the subspace where the init_config is located
|
||||
flow2 = self.__class__(
|
||||
init_config, self.metric, self.mode,
|
||||
space, self.prune_attr,
|
||||
self.min_resource, self.max_resource,
|
||||
self.resource_multiple_factor, self.cost_attr, self.seed + 1)
|
||||
init_config,
|
||||
self.metric,
|
||||
self.mode,
|
||||
space,
|
||||
self.prune_attr,
|
||||
self.min_resource,
|
||||
self.max_resource,
|
||||
self.resource_multiple_factor,
|
||||
self.cost_attr,
|
||||
self.seed + 1,
|
||||
)
|
||||
flow2.best_obj = obj * self.metric_op # minimize internally
|
||||
flow2.cost_incumbent = cost
|
||||
self.seed += 1
|
||||
return flow2
|
||||
|
||||
def normalize(self, config, recursive=False) -> Dict:
|
||||
''' normalize each dimension in config to [0,1]
|
||||
'''
|
||||
"""normalize each dimension in config to [0,1]"""
|
||||
return normalize(
|
||||
config, self._space, self.best_config, self.incumbent, recursive)
|
||||
config, self._space, self.best_config, self.incumbent, recursive
|
||||
)
|
||||
|
||||
def denormalize(self, config):
|
||||
''' denormalize each dimension in config from [0,1]
|
||||
'''
|
||||
"""denormalize each dimension in config from [0,1]"""
|
||||
return denormalize(
|
||||
config, self._space, self.best_config, self.incumbent, self._random)
|
||||
config, self._space, self.best_config, self.incumbent, self._random
|
||||
)
|
||||
|
||||
def set_search_properties(self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = None) -> bool:
|
||||
def set_search_properties(
|
||||
self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
) -> bool:
|
||||
if metric:
|
||||
self._metric = metric
|
||||
if mode:
|
||||
assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
|
||||
self._mode = mode
|
||||
if mode == "max":
|
||||
self.metric_op = -1.
|
||||
self.metric_op = -1.0
|
||||
elif mode == "min":
|
||||
self.metric_op = 1.
|
||||
self.metric_op = 1.0
|
||||
if config:
|
||||
self.space = config
|
||||
self._space = flatten_dict(self.space)
|
||||
self._init_search()
|
||||
return True
|
||||
|
||||
def on_trial_complete(self, trial_id: str, result: Optional[Dict] = None,
|
||||
error: bool = False):
|
||||
''' compare with incumbent
|
||||
'''
|
||||
def on_trial_complete(
|
||||
self, trial_id: str, result: Optional[Dict] = None, error: bool = False
|
||||
):
|
||||
# compare with incumbent
|
||||
# if better, move, reset num_complete and num_proposed
|
||||
# if not better and num_complete >= 2*dim, num_allowed += 2
|
||||
self.trial_count_complete += 1
|
||||
|
@ -329,15 +349,19 @@ class FLOW2(Searcher):
|
|||
if proposed_by == self.incumbent:
|
||||
# proposed by current incumbent and no better
|
||||
self._num_complete4incumbent += 1
|
||||
cost = result.get(
|
||||
self.cost_attr) if result else self._trial_cost.get(trial_id)
|
||||
cost = (
|
||||
result.get(self.cost_attr) if result else self._trial_cost.get(trial_id)
|
||||
)
|
||||
if cost:
|
||||
self._cost_complete4incumbent += cost
|
||||
if self._num_complete4incumbent >= 2 * self.dim and \
|
||||
self._num_allowed4incumbent == 0:
|
||||
if (
|
||||
self._num_complete4incumbent >= 2 * self.dim
|
||||
and self._num_allowed4incumbent == 0
|
||||
):
|
||||
self._num_allowed4incumbent = 2
|
||||
if self._num_complete4incumbent == self.dir and (
|
||||
not self._resource or self._resource == self.max_resource):
|
||||
not self._resource or self._resource == self.max_resource
|
||||
):
|
||||
# check stuck condition if using max resource
|
||||
self._num_complete4incumbent -= 2
|
||||
if self._num_allowed4incumbent < 2:
|
||||
|
@ -345,8 +369,7 @@ class FLOW2(Searcher):
|
|||
# elif proposed_by: del self._proposed_by[trial_id]
|
||||
|
||||
def on_trial_result(self, trial_id: str, result: Dict):
|
||||
''' early update of incumbent
|
||||
'''
|
||||
"""early update of incumbent"""
|
||||
if result:
|
||||
obj = result.get(self._metric)
|
||||
if obj:
|
||||
|
@ -373,27 +396,32 @@ class FLOW2(Searcher):
|
|||
def rand_vector_unit_sphere(self, dim, trunc=0) -> np.ndarray:
|
||||
vec = self._random.normal(0, 1, dim)
|
||||
if 0 < trunc < dim:
|
||||
vec[np.abs(vec).argsort()[:dim - trunc]] = 0
|
||||
vec[np.abs(vec).argsort()[: dim - trunc]] = 0
|
||||
mag = np.linalg.norm(vec)
|
||||
return vec / mag
|
||||
|
||||
def suggest(self, trial_id: str) -> Optional[Dict]:
|
||||
''' suggest a new config, one of the following cases:
|
||||
"""suggest a new config, one of the following cases:
|
||||
1. same incumbent, increase resource
|
||||
2. same resource, move from the incumbent to a random direction
|
||||
3. same resource, move from the incumbent to the opposite direction
|
||||
#TODO: better decouple FLOW2 config suggestion and stepsize update
|
||||
'''
|
||||
"""
|
||||
self.trial_count_proposed += 1
|
||||
if self._num_complete4incumbent > 0 and self.cost_incumbent and \
|
||||
self._resource and self._resource < self.max_resource and (
|
||||
if (
|
||||
self._num_complete4incumbent > 0
|
||||
and self.cost_incumbent
|
||||
and self._resource
|
||||
and self._resource < self.max_resource
|
||||
and (
|
||||
self._cost_complete4incumbent
|
||||
>= self.cost_incumbent * self.resource_multiple_factor):
|
||||
>= self.cost_incumbent * self.resource_multiple_factor
|
||||
)
|
||||
):
|
||||
# consider increasing resource using sum eval cost of complete
|
||||
# configs
|
||||
old_resource = self._resource
|
||||
self._resource = self._round(
|
||||
self._resource * self.resource_multiple_factor)
|
||||
self._resource = self._round(self._resource * self.resource_multiple_factor)
|
||||
self.cost_incumbent *= self._resource / old_resource
|
||||
config = self.best_config.copy()
|
||||
config[self.prune_attr] = self._resource
|
||||
|
@ -409,8 +437,9 @@ class FLOW2(Searcher):
|
|||
self._direction_tried = None
|
||||
else:
|
||||
# propose a new direction
|
||||
self._direction_tried = self.rand_vector_unit_sphere(
|
||||
self.dim, self._trunc) * self.step
|
||||
self._direction_tried = (
|
||||
self.rand_vector_unit_sphere(self.dim, self._trunc) * self.step
|
||||
)
|
||||
for i, key in enumerate(self._tunable_keys):
|
||||
move[key] += self._direction_tried[i]
|
||||
self._project(move)
|
||||
|
@ -442,7 +471,8 @@ class FLOW2(Searcher):
|
|||
break
|
||||
self._same = same
|
||||
if self._num_proposedby_incumbent == self.dir and (
|
||||
not self._resource or self._resource == self.max_resource):
|
||||
not self._resource or self._resource == self.max_resource
|
||||
):
|
||||
# check stuck condition if using max resource
|
||||
self._num_proposedby_incumbent -= 2
|
||||
self._init_phase = False
|
||||
|
@ -459,11 +489,11 @@ class FLOW2(Searcher):
|
|||
# random
|
||||
for i, key in enumerate(self._tunable_keys):
|
||||
if self._direction_tried[i] != 0:
|
||||
for _, generated in generate_variants({'config': {
|
||||
key: self._space[key]
|
||||
}}):
|
||||
if generated['config'][key] != best_config[key]:
|
||||
config[key] = generated['config'][key]
|
||||
for _, generated in generate_variants(
|
||||
{"config": {key: self._space[key]}}
|
||||
):
|
||||
if generated["config"][key] != best_config[key]:
|
||||
config[key] = generated["config"][key]
|
||||
return unflatten_dict(config)
|
||||
break
|
||||
else:
|
||||
|
@ -477,8 +507,7 @@ class FLOW2(Searcher):
|
|||
return unflatten_dict(config)
|
||||
|
||||
def _project(self, config):
|
||||
''' project normalized config in the feasible region and set prune_attr
|
||||
'''
|
||||
"""project normalized config in the feasible region and set prune_attr"""
|
||||
for key in self._bounded_keys:
|
||||
value = config[key]
|
||||
config[key] = max(0, min(1, value))
|
||||
|
@ -487,14 +516,13 @@ class FLOW2(Searcher):
|
|||
|
||||
@property
|
||||
def can_suggest(self) -> bool:
|
||||
''' can't suggest if 2*dim configs have been proposed for the incumbent
|
||||
while fewer are completed
|
||||
'''
|
||||
"""can't suggest if 2*dim configs have been proposed for the incumbent
|
||||
while fewer are completed
|
||||
"""
|
||||
return self._num_allowed4incumbent > 0
|
||||
|
||||
def config_signature(self, config, space: Dict = None) -> tuple:
|
||||
''' return the signature tuple of a config
|
||||
'''
|
||||
"""return the signature tuple of a config"""
|
||||
config = flatten_dict(config)
|
||||
if space:
|
||||
space = flatten_dict(space)
|
||||
|
@ -514,8 +542,11 @@ class FLOW2(Searcher):
|
|||
if self.hierarchical:
|
||||
# can't remove constant for hierarchical search space,
|
||||
# e.g., learner
|
||||
if not (domain is None or type(domain) in (str, int, float)
|
||||
or isinstance(domain, sample.Domain)):
|
||||
if not (
|
||||
domain is None
|
||||
or type(domain) in (str, int, float)
|
||||
or isinstance(domain, sample.Domain)
|
||||
):
|
||||
# not domain or hashable
|
||||
# get rid of list type for hierarchical search space.
|
||||
continue
|
||||
|
@ -527,16 +558,14 @@ class FLOW2(Searcher):
|
|||
|
||||
@property
|
||||
def converged(self) -> bool:
|
||||
''' return whether the local search has converged
|
||||
'''
|
||||
"""return whether the local search has converged"""
|
||||
if self._num_complete4incumbent < self.dir - 2:
|
||||
return False
|
||||
# check stepsize after enough configs are completed
|
||||
return self.step < self.step_lower_bound
|
||||
|
||||
def reach(self, other: Searcher) -> bool:
|
||||
''' whether the incumbent can reach the incumbent of other
|
||||
'''
|
||||
"""whether the incumbent can reach the incumbent of other"""
|
||||
config1, config2 = self.best_config, other.best_config
|
||||
incumbent1, incumbent2 = self.incumbent, other.incumbent
|
||||
if self._resource and config1[self.prune_attr] > config2[self.prune_attr]:
|
||||
|
@ -547,6 +576,9 @@ class FLOW2(Searcher):
|
|||
if config1[key] != config2.get(key):
|
||||
return False
|
||||
delta = np.array(
|
||||
[incumbent1[key] - incumbent2.get(key, np.inf)
|
||||
for key in self._tunable_keys])
|
||||
[
|
||||
incumbent1[key] - incumbent2.get(key, np.inf)
|
||||
for key in self._tunable_keys
|
||||
]
|
||||
)
|
||||
return np.linalg.norm(delta) <= self.step
|
||||
|
|
|
@ -20,14 +20,19 @@ class BaseSearcher:
|
|||
on_trial_complete()
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
):
|
||||
pass
|
||||
|
||||
def set_search_properties(self, metric: Optional[str] = None, mode: Optional[str] = None,
|
||||
config: Optional[Dict] = None):
|
||||
def set_search_properties(
|
||||
self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
):
|
||||
if metric:
|
||||
self._metric = metric
|
||||
if mode:
|
||||
|
@ -66,6 +71,7 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
(although not the same searcher_trial_id).
|
||||
searcher_trial_id will be used in suggest()
|
||||
"""
|
||||
|
||||
# ****the following constants are used when generating new challengers in
|
||||
# the _query_config_oracle function
|
||||
# how many item to add when doing the expansion
|
||||
|
@ -84,25 +90,26 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
# 0.95 of the previous best config's loss.
|
||||
# NOTE: this setting depends on the assumption that (and thus
|
||||
# _query_config_oracle) is only triggered when a better champion is found.
|
||||
CFO_SEARCHER_METRIC_NAME = 'pseudo_loss'
|
||||
CFO_SEARCHER_METRIC_NAME = "pseudo_loss"
|
||||
CFO_SEARCHER_LARGE_LOSS = 1e6
|
||||
|
||||
# the random seed used in generating numerical hyperparamter configs (when CFO is not used)
|
||||
NUM_RANDOM_SEED = 111
|
||||
|
||||
CHAMPION_TRIAL_NAME = 'champion_trial'
|
||||
CHAMPION_TRIAL_NAME = "champion_trial"
|
||||
TRIAL_CLASS = VowpalWabbitTrial
|
||||
|
||||
def __init__(self,
|
||||
init_config: Dict,
|
||||
space: Optional[Dict] = None,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
random_seed: Optional[int] = 2345,
|
||||
online_trial_args: Optional[Dict] = {},
|
||||
nonpoly_searcher_name: Optional[str] = 'CFO'
|
||||
):
|
||||
'''Constructor
|
||||
def __init__(
|
||||
self,
|
||||
init_config: Dict,
|
||||
space: Optional[Dict] = None,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
random_seed: Optional[int] = 2345,
|
||||
online_trial_args: Optional[Dict] = {},
|
||||
nonpoly_searcher_name: Optional[str] = "CFO",
|
||||
):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
init_config: dict
|
||||
|
@ -113,7 +120,7 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
online_trial_args: dict
|
||||
nonpoly_searcher_name: A string to specify the search algorithm
|
||||
for nonpoly hyperparameters
|
||||
'''
|
||||
"""
|
||||
self._init_config = init_config
|
||||
self._space = space
|
||||
self._seed = random_seed
|
||||
|
@ -122,44 +129,62 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
|
||||
self._random_state = np.random.RandomState(self._seed)
|
||||
self._searcher_for_nonpoly_hp = {}
|
||||
self._space_of_nonpoly_hp = {}
|
||||
|
||||
# dicts to remember the mapping between searcher_trial_id and trial_id
|
||||
self._searcher_trialid_to_trialid = {} # key: searcher_trial_id, value: trial_id
|
||||
self._trialid_to_searcher_trial_id = {} # value: trial_id, key: searcher_trial_id
|
||||
self._space_of_nonpoly_hp = {}
|
||||
|
||||
# key: searcher_trial_id, value: trial_id
|
||||
self._searcher_trialid_to_trialid = {}
|
||||
|
||||
# value: trial_id, key: searcher_trial_id
|
||||
self._trialid_to_searcher_trial_id = {}
|
||||
|
||||
self._challenger_list = []
|
||||
# initialize the search in set_search_properties
|
||||
self.set_search_properties(config={self.CHAMPION_TRIAL_NAME: None}, init_call=True)
|
||||
logger.debug('using random seed %s in config oracle', self._seed)
|
||||
self.set_search_properties(
|
||||
setting={self.CHAMPION_TRIAL_NAME: None}, init_call=True
|
||||
)
|
||||
logger.debug("using random seed %s in config oracle", self._seed)
|
||||
|
||||
def set_search_properties(self, metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = {},
|
||||
init_call: Optional[bool] = False):
|
||||
"""Construct search space with given config, and setup the search
|
||||
"""
|
||||
def set_search_properties(
|
||||
self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
config: Optional[Dict] = {},
|
||||
setting: Optional[Dict] = {},
|
||||
init_call: Optional[bool] = False,
|
||||
):
|
||||
"""Construct search space with given config, and setup the search"""
|
||||
super().set_search_properties(metric, mode, config)
|
||||
# *********Use ConfigOralce (i.e, self._generate_new_space to generate list of new challengers)
|
||||
logger.info('champion trial %s', config)
|
||||
champion_trial = config.get(self.CHAMPION_TRIAL_NAME, None)
|
||||
logger.info("setting %s", setting)
|
||||
champion_trial = setting.get(self.CHAMPION_TRIAL_NAME, None)
|
||||
if champion_trial is None:
|
||||
champion_trial = self._create_trial_from_config(self._init_config)
|
||||
# generate a new list of challenger trials
|
||||
new_challenger_list = self._query_config_oracle(champion_trial.config,
|
||||
champion_trial.trial_id,
|
||||
self._trialid_to_searcher_trial_id[champion_trial.trial_id])
|
||||
new_challenger_list = self._query_config_oracle(
|
||||
champion_trial.config,
|
||||
champion_trial.trial_id,
|
||||
self._trialid_to_searcher_trial_id[champion_trial.trial_id],
|
||||
)
|
||||
# add the newly generated challengers to existing challengers
|
||||
# there can be duplicates and we check duplicates when calling next_trial()
|
||||
self._challenger_list = self._challenger_list + new_challenger_list
|
||||
# add the champion as part of the new_challenger_list when called initially
|
||||
if init_call:
|
||||
self._challenger_list.append(champion_trial)
|
||||
logger.critical('Created challengers from champion %s', champion_trial.trial_id)
|
||||
logger.critical('New challenger size %s, %s', len(self._challenger_list),
|
||||
[t.trial_id for t in self._challenger_list])
|
||||
logger.info(
|
||||
"**Important** Created challengers from champion %s",
|
||||
champion_trial.trial_id,
|
||||
)
|
||||
logger.info(
|
||||
"New challenger size %s, %s",
|
||||
len(self._challenger_list),
|
||||
[t.trial_id for t in self._challenger_list],
|
||||
)
|
||||
|
||||
def next_trial(self):
|
||||
"""Return a trial from the _challenger_list
|
||||
"""
|
||||
"""Return a trial from the _challenger_list"""
|
||||
next_trial = None
|
||||
if self._challenger_list:
|
||||
next_trial = self._challenger_list.pop()
|
||||
|
@ -175,8 +200,9 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
self._trialid_to_searcher_trial_id[trial.trial_id] = searcher_trial_id
|
||||
return trial
|
||||
|
||||
def _query_config_oracle(self, seed_config, seed_config_trial_id,
|
||||
seed_config_searcher_trial_id=None) -> List[Trial]:
|
||||
def _query_config_oracle(
|
||||
self, seed_config, seed_config_trial_id, seed_config_searcher_trial_id=None
|
||||
) -> List[Trial]:
|
||||
"""Give the seed config, generate a list of new configs (which are supposed to include
|
||||
at least one config that has better performance than the input seed_config)
|
||||
"""
|
||||
|
@ -189,12 +215,16 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
config_domain = self._space[k]
|
||||
if isinstance(config_domain, PolynomialExpansionSet):
|
||||
# get candidate configs for hyperparameters of the PolynomialExpansionSet type
|
||||
partial_new_configs = self._generate_independent_hp_configs(k, v, config_domain)
|
||||
partial_new_configs = self._generate_independent_hp_configs(
|
||||
k, v, config_domain
|
||||
)
|
||||
if partial_new_configs:
|
||||
hyperparameter_config_groups.append(partial_new_configs)
|
||||
# does not have searcher_trial_ids
|
||||
searcher_trial_ids_groups.append([])
|
||||
elif isinstance(config_domain, Float) or isinstance(config_domain, Categorical):
|
||||
elif isinstance(config_domain, Float) or isinstance(
|
||||
config_domain, Categorical
|
||||
):
|
||||
# otherwise we need to deal with them in group
|
||||
nonpoly_config[k] = v
|
||||
if k not in self._space_of_nonpoly_hp:
|
||||
|
@ -204,38 +234,57 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
if nonpoly_config:
|
||||
new_searcher_trial_ids = []
|
||||
partial_new_nonpoly_configs = []
|
||||
if 'CFO' in self._nonpoly_searcher_name:
|
||||
if "CFO" in self._nonpoly_searcher_name:
|
||||
if seed_config_trial_id not in self._searcher_for_nonpoly_hp:
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id] = CFO(space=self._space_of_nonpoly_hp,
|
||||
points_to_evaluate=[nonpoly_config],
|
||||
metric=self.CFO_SEARCHER_METRIC_NAME,
|
||||
)
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id] = CFO(
|
||||
space=self._space_of_nonpoly_hp,
|
||||
points_to_evaluate=[nonpoly_config],
|
||||
metric=self.CFO_SEARCHER_METRIC_NAME,
|
||||
)
|
||||
# initialize the search in set_search_properties
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].set_search_properties(
|
||||
config={'metric_target': self.CFO_SEARCHER_LARGE_LOSS})
|
||||
self._searcher_for_nonpoly_hp[
|
||||
seed_config_trial_id
|
||||
].set_search_properties(
|
||||
setting={"metric_target": self.CFO_SEARCHER_LARGE_LOSS}
|
||||
)
|
||||
# We need to call this for once, such that the seed config in points_to_evaluate will be called
|
||||
# to be tried
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].suggest(seed_config_searcher_trial_id)
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].suggest(
|
||||
seed_config_searcher_trial_id
|
||||
)
|
||||
# assuming minimization
|
||||
if self._searcher_for_nonpoly_hp[seed_config_trial_id].metric_target is None:
|
||||
if (
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].metric_target
|
||||
is None
|
||||
):
|
||||
pseudo_loss = self.CFO_SEARCHER_LARGE_LOSS
|
||||
else:
|
||||
pseudo_loss = self._searcher_for_nonpoly_hp[seed_config_trial_id].metric_target * 0.95
|
||||
pseudo_loss = (
|
||||
self._searcher_for_nonpoly_hp[
|
||||
seed_config_trial_id
|
||||
].metric_target
|
||||
* 0.95
|
||||
)
|
||||
pseudo_result_to_report = {}
|
||||
for k, v in nonpoly_config.items():
|
||||
pseudo_result_to_report['config/' + str(k)] = v
|
||||
pseudo_result_to_report["config/" + str(k)] = v
|
||||
pseudo_result_to_report[self.CFO_SEARCHER_METRIC_NAME] = pseudo_loss
|
||||
pseudo_result_to_report['time_total_s'] = 1
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].on_trial_complete(seed_config_searcher_trial_id,
|
||||
result=pseudo_result_to_report)
|
||||
pseudo_result_to_report["time_total_s"] = 1
|
||||
self._searcher_for_nonpoly_hp[seed_config_trial_id].on_trial_complete(
|
||||
seed_config_searcher_trial_id, result=pseudo_result_to_report
|
||||
)
|
||||
while len(partial_new_nonpoly_configs) < self.NUMERICAL_NUM:
|
||||
# suggest multiple times
|
||||
new_searcher_trial_id = Trial.generate_id()
|
||||
new_searcher_trial_ids.append(new_searcher_trial_id)
|
||||
suggestion = self._searcher_for_nonpoly_hp[seed_config_trial_id].suggest(new_searcher_trial_id)
|
||||
suggestion = self._searcher_for_nonpoly_hp[
|
||||
seed_config_trial_id
|
||||
].suggest(new_searcher_trial_id)
|
||||
if suggestion is not None:
|
||||
partial_new_nonpoly_configs.append(suggestion)
|
||||
logger.info('partial_new_nonpoly_configs %s', partial_new_nonpoly_configs)
|
||||
logger.info(
|
||||
"partial_new_nonpoly_configs %s", partial_new_nonpoly_configs
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
if partial_new_nonpoly_configs:
|
||||
|
@ -244,9 +293,11 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
# ----------- coordinate generation of new challengers in the case of multiple groups
|
||||
new_trials = []
|
||||
for i in range(len(hyperparameter_config_groups)):
|
||||
logger.info('hyperparameter_config_groups[i] %s %s',
|
||||
len(hyperparameter_config_groups[i]),
|
||||
hyperparameter_config_groups[i])
|
||||
logger.info(
|
||||
"hyperparameter_config_groups[i] %s %s",
|
||||
len(hyperparameter_config_groups[i]),
|
||||
hyperparameter_config_groups[i],
|
||||
)
|
||||
for j, new_partial_config in enumerate(hyperparameter_config_groups[i]):
|
||||
new_seed_config = seed_config.copy()
|
||||
new_seed_config.update(new_partial_config)
|
||||
|
@ -260,32 +311,55 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
new_searcher_trial_id = searcher_trial_ids_groups[i][j]
|
||||
else:
|
||||
new_searcher_trial_id = None
|
||||
new_trial = self._create_trial_from_config(new_seed_config, new_searcher_trial_id)
|
||||
new_trial = self._create_trial_from_config(
|
||||
new_seed_config, new_searcher_trial_id
|
||||
)
|
||||
new_trials.append(new_trial)
|
||||
logger.info('new_configs %s', [t.trial_id for t in new_trials])
|
||||
logger.info("new_configs %s", [t.trial_id for t in new_trials])
|
||||
return new_trials
|
||||
|
||||
def _generate_independent_hp_configs(self, hp_name, current_config_value, config_domain) -> List:
|
||||
def _generate_independent_hp_configs(
|
||||
self, hp_name, current_config_value, config_domain
|
||||
) -> List:
|
||||
if isinstance(config_domain, PolynomialExpansionSet):
|
||||
seed_interactions = list(current_config_value) + list(config_domain.init_monomials)
|
||||
logger.critical('Seed namespaces (singletons and interactions): %s', seed_interactions)
|
||||
logger.info('current_config_value %s %s', current_config_value, seed_interactions)
|
||||
configs = self._generate_poly_expansion_sets(seed_interactions,
|
||||
self.EXPANSION_ORDER,
|
||||
config_domain.allow_self_inter,
|
||||
config_domain.highest_poly_order,
|
||||
self.POLY_EXPANSION_ADDITION_NUM,
|
||||
)
|
||||
seed_interactions = list(current_config_value) + list(
|
||||
config_domain.init_monomials
|
||||
)
|
||||
logger.info(
|
||||
"**Important** Seed namespaces (singletons and interactions): %s",
|
||||
seed_interactions,
|
||||
)
|
||||
logger.info("current_config_value %s", current_config_value)
|
||||
configs = self._generate_poly_expansion_sets(
|
||||
seed_interactions,
|
||||
self.EXPANSION_ORDER,
|
||||
config_domain.allow_self_inter,
|
||||
config_domain.highest_poly_order,
|
||||
self.POLY_EXPANSION_ADDITION_NUM,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
configs_w_key = [{hp_name: hp_config} for hp_config in configs]
|
||||
return configs_w_key
|
||||
|
||||
def _generate_poly_expansion_sets(self, seed_interactions, order, allow_self_inter,
|
||||
highest_poly_order, interaction_num_to_add):
|
||||
champion_all_combinations = self._generate_all_comb(seed_interactions, order, allow_self_inter, highest_poly_order)
|
||||
space = sorted(list(itertools.combinations(
|
||||
champion_all_combinations, interaction_num_to_add)))
|
||||
def _generate_poly_expansion_sets(
|
||||
self,
|
||||
seed_interactions,
|
||||
order,
|
||||
allow_self_inter,
|
||||
highest_poly_order,
|
||||
interaction_num_to_add,
|
||||
):
|
||||
champion_all_combinations = self._generate_all_comb(
|
||||
seed_interactions, order, allow_self_inter, highest_poly_order
|
||||
)
|
||||
space = sorted(
|
||||
list(
|
||||
itertools.combinations(
|
||||
champion_all_combinations, interaction_num_to_add
|
||||
)
|
||||
)
|
||||
)
|
||||
self._random_state.shuffle(space)
|
||||
candidate_configs = [set(seed_interactions) | set(item) for item in space]
|
||||
final_candidate_configs = []
|
||||
|
@ -295,9 +369,12 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
return final_candidate_configs
|
||||
|
||||
@staticmethod
|
||||
def _generate_all_comb(seed_interactions: list, seed_interaction_order: int,
|
||||
allow_self_inter: Optional[bool] = False,
|
||||
highest_poly_order: Optional[int] = None):
|
||||
def _generate_all_comb(
|
||||
seed_interactions: list,
|
||||
seed_interaction_order: int,
|
||||
allow_self_inter: Optional[bool] = False,
|
||||
highest_poly_order: Optional[int] = None,
|
||||
):
|
||||
"""Generate new interactions by doing up to seed_interaction_order on the seed_interactions
|
||||
|
||||
Args:
|
||||
|
@ -312,8 +389,7 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
"""
|
||||
|
||||
def get_interactions(list1, list2):
|
||||
"""Get combinatorial list of tuples
|
||||
"""
|
||||
"""Get combinatorial list of tuples"""
|
||||
new_list = []
|
||||
for i in list1:
|
||||
for j in list2:
|
||||
|
@ -321,19 +397,18 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
# 'abc' 'cba' 'bca' are all 'abc'
|
||||
# this is done to ensure we can use the config as the signature
|
||||
# of the trial, i.e., trial id.
|
||||
new_interaction = ''.join(sorted(i + j))
|
||||
new_interaction = "".join(sorted(i + j))
|
||||
if new_interaction not in new_list:
|
||||
new_list.append(new_interaction)
|
||||
return new_list
|
||||
|
||||
def strip_self_inter(s):
|
||||
"""Remove duplicates in an interaction string
|
||||
"""
|
||||
"""Remove duplicates in an interaction string"""
|
||||
if len(s) == len(set(s)):
|
||||
return s
|
||||
else:
|
||||
# return ''.join(sorted(set(s)))
|
||||
new_s = ''
|
||||
new_s = ""
|
||||
char_list = []
|
||||
for i in s:
|
||||
if i not in char_list:
|
||||
|
@ -351,10 +426,15 @@ class ChampionFrontierSearcher(BaseSearcher):
|
|||
all_interactions_no_self_inter = []
|
||||
for s in all_interactions:
|
||||
s_no_inter = strip_self_inter(s)
|
||||
if len(s_no_inter) > 1 and s_no_inter not in all_interactions_no_self_inter:
|
||||
if (
|
||||
len(s_no_inter) > 1
|
||||
and s_no_inter not in all_interactions_no_self_inter
|
||||
):
|
||||
all_interactions_no_self_inter.append(s_no_inter)
|
||||
all_interactions = all_interactions_no_self_inter
|
||||
if highest_poly_order is not None:
|
||||
all_interactions = [c for c in all_interactions if len(c) <= highest_poly_order]
|
||||
logger.info('all_combinations %s', all_interactions)
|
||||
all_interactions = [
|
||||
c for c in all_interactions if len(c) <= highest_poly_order
|
||||
]
|
||||
logger.info("all_combinations %s", all_interactions)
|
||||
return all_interactions
|
||||
|
|
|
@ -54,7 +54,7 @@ class SearchThread:
|
|||
|
||||
@classmethod
|
||||
def set_eps(cls, time_budget_s):
|
||||
cls._eps = max(min(time_budget_s / 1000.0, 1.0), 1e-10)
|
||||
cls._eps = max(min(time_budget_s / 1000.0, 1.0), 1e-9)
|
||||
|
||||
def suggest(self, trial_id: str) -> Optional[Dict]:
|
||||
''' use the suggest() of the underlying search algorithm
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Economical Hyperparameter Optimization
|
||||
|
||||
`flaml.tune` is a module for economical hyperparameter tuning. It frees users from manually tuning many hyperparameters for a software, such as machine learning training procedures.
|
||||
`flaml.tune` is a module for economical hyperparameter tuning. It frees users from manually tuning many hyperparameters for a software, such as machine learning training procedures.
|
||||
It can be used standalone, or together with ray tune or nni.
|
||||
|
||||
* Example for sequential tuning (recommended when compute resource is limited and each trial can consume all the resources):
|
||||
|
@ -18,8 +18,8 @@ def evaluate_config(config):
|
|||
# and the cost could be related to certain hyperparameters
|
||||
# in this example, we assume it's proportional to x
|
||||
time.sleep(config['x']/100000)
|
||||
# use tune.report to report the metric to optimize
|
||||
tune.report(metric=metric)
|
||||
# use tune.report to report the metric to optimize
|
||||
tune.report(metric=metric)
|
||||
|
||||
analysis = tune.run(
|
||||
evaluate_config, # the function to evaluate a config
|
||||
|
@ -33,7 +33,7 @@ analysis = tune.run(
|
|||
num_samples=-1, # the maximal number of configs to try, -1 means infinite
|
||||
time_budget_s=60, # the time budget in seconds
|
||||
local_dir='logs/', # the local directory to store logs
|
||||
# verbose=0, # verbosity
|
||||
# verbose=0, # verbosity
|
||||
# use_ray=True, # uncomment when performing parallel tuning using ray
|
||||
)
|
||||
|
||||
|
@ -57,8 +57,8 @@ def evaluate_config(config):
|
|||
# and the cost could be related to certain hyperparameters
|
||||
# in this example, we assume it's proportional to x
|
||||
time.sleep(config['x']/100000)
|
||||
# use tune.report to report the metric to optimize
|
||||
tune.report(metric=metric)
|
||||
# use tune.report to report the metric to optimize
|
||||
tune.report(metric=metric)
|
||||
|
||||
# provide a time budget (in seconds) for the tuning process
|
||||
time_budget_s = 60
|
||||
|
@ -77,25 +77,25 @@ cfo = CFO(low_cost_partial_config=low_cost_partial_config)
|
|||
blendsearch = BlendSearch(
|
||||
metric="metric", mode="min",
|
||||
space=config_search_space,
|
||||
low_cost_partial_config=low_cost_partial_config)
|
||||
low_cost_partial_config=low_cost_partial_config,
|
||||
time_budget_s=time_budget_s
|
||||
)
|
||||
# NOTE: when using BlendSearch as a search_alg in ray tune, you need to
|
||||
# configure the 'time_budget_s' for BlendSearch accordingly as follows such that
|
||||
# configure the 'time_budget_s' for BlendSearch accordingly such that
|
||||
# BlendSearch is aware of the time budget. This step is not needed when
|
||||
# BlendSearch is used as the search_alg in flaml.tune as it is already done
|
||||
# automatically in flaml. Also, this step needs to be done after the search
|
||||
# space is passed to BlendSearch and before raytune.run.
|
||||
blendsearch.set_search_properties(config={"time_budget_s": time_budget_s})
|
||||
# BlendSearch is used as the search_alg in flaml.tune as it is done
|
||||
# automatically in flaml.
|
||||
|
||||
analysis = raytune.run(
|
||||
evaluate_config, # the function to evaluate a config
|
||||
config=config_search_space,
|
||||
metric='metric', # the name of the metric used for optimization
|
||||
mode='min', # the optimization mode, 'min' or 'max'
|
||||
num_samples=-1, # the maximal number of configs to try, -1 means infinite
|
||||
num_samples=-1, # the maximal number of configs to try, -1 means infinite
|
||||
time_budget_s=time_budget_s, # the time budget in seconds
|
||||
local_dir='logs/', # the local directory to store logs
|
||||
search_alg=blendsearch # or cfo
|
||||
)
|
||||
)
|
||||
|
||||
print(analysis.best_trial.last_result) # the best trial's result
|
||||
print(analysis.best_config) # the best config
|
||||
|
@ -107,11 +107,10 @@ print(analysis.best_config) # the best config
|
|||
$nnictl create --config ./config.yml
|
||||
```
|
||||
|
||||
* For more examples, please check out
|
||||
* For more examples, please check out
|
||||
[notebooks](https://github.com/microsoft/FLAML/tree/main/notebook/).
|
||||
|
||||
|
||||
`flaml` offers two HPO methods: CFO and BlendSearch.
|
||||
`flaml` offers two HPO methods: CFO and BlendSearch.
|
||||
`flaml.tune` uses BlendSearch by default.
|
||||
|
||||
## CFO: Frugal Optimization for Cost-related Hyperparameters
|
||||
|
@ -121,27 +120,27 @@ $nnictl create --config ./config.yml
|
|||
<br>
|
||||
</p>
|
||||
|
||||
CFO uses the randomized direct search method FLOW<sup>2</sup> with adaptive stepsize and random restart.
|
||||
CFO uses the randomized direct search method FLOW<sup>2</sup> with adaptive stepsize and random restart.
|
||||
It requires a low-cost initial point as input if such point exists.
|
||||
The search begins with the low-cost initial point and gradually move to
|
||||
high cost region if needed. The local search method has a provable convergence
|
||||
rate and bounded cost.
|
||||
rate and bounded cost.
|
||||
|
||||
About FLOW<sup>2</sup>: FLOW<sup>2</sup> is a simple yet effective randomized direct search method.
|
||||
About FLOW<sup>2</sup>: FLOW<sup>2</sup> is a simple yet effective randomized direct search method.
|
||||
It is an iterative optimization method that can optimize for black-box functions.
|
||||
FLOW<sup>2</sup> only requires pairwise comparisons between function values to perform iterative update. Comparing to existing HPO methods, FLOW<sup>2</sup> has the following appealing properties:
|
||||
|
||||
1. It is applicable to general black-box functions with a good convergence rate in terms of loss.
|
||||
3. It provides theoretical guarantees on the total evaluation cost incurred.
|
||||
1. It provides theoretical guarantees on the total evaluation cost incurred.
|
||||
|
||||
The GIFs attached below demonstrate an example search trajectory of FLOW<sup>2</sup> shown in the loss and evaluation cost (i.e., the training time ) space respectively. From the demonstration, we can see that (1) FLOW<sup>2</sup> can quickly move toward the low-loss region, showing good convergence property and (2) FLOW<sup>2</sup> tends to avoid exploring the high-cost region until necessary.
|
||||
|
||||
<p align="center">
|
||||
<img align="center", src="https://github.com/microsoft/FLAML/blob/main/docs/images/heatmap_loss_cfo_12s.gif" width=360> <img align="center", src="https://github.com/microsoft/FLAML/blob/main/docs/images/heatmap_cost_cfo_12s.gif" width=360>
|
||||
<img align="center", src="https://github.com/microsoft/FLAML/blob/main/docs/images/heatmap_loss_cfo_12s.gif" width=360> <img align="center", src="https://github.com/microsoft/FLAML/blob/main/docs/images/heatmap_cost_cfo_12s.gif" width=360>
|
||||
<br>
|
||||
<figcaption>Figure 1. FLOW<sup>2</sup> in tuning the # of leaves and the # of trees for XGBoost. The two background heatmaps show the loss and cost distribution of all configurations. The black dots are the points evaluated in FLOW<sup>2</sup>. Black dots connected by lines are points that yield better loss performance when evaluated.</figcaption>
|
||||
</p>
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
|
@ -152,9 +151,9 @@ tune.run(...
|
|||
```
|
||||
|
||||
Recommended scenario: there exist cost-related hyperparameters and a low-cost
|
||||
initial point is known before optimization.
|
||||
initial point is known before optimization.
|
||||
If the search space is complex and CFO gets trapped into local optima, consider
|
||||
using BlendSearch.
|
||||
using BlendSearch.
|
||||
|
||||
## BlendSearch: Economical Hyperparameter Optimization With Blended Search Strategy
|
||||
|
||||
|
@ -167,7 +166,7 @@ BlendSearch combines local search with global search. It leverages the frugality
|
|||
of CFO and the space exploration ability of global search methods such as
|
||||
Bayesian optimization. Like CFO, BlendSearch requires a low-cost initial point
|
||||
as input if such point exists, and starts the search from there. Different from
|
||||
CFO, BlendSearch will not wait for the local search to fully converge before
|
||||
CFO, BlendSearch will not wait for the local search to fully converge before
|
||||
trying new start points. The new start points are suggested by the global search
|
||||
method and filtered based on their distance to the existing points in the
|
||||
cost-related dimensions. BlendSearch still gradually increases the trial cost.
|
||||
|
@ -184,19 +183,18 @@ tune.run(...
|
|||
)
|
||||
```
|
||||
|
||||
- Recommended scenario: cost-related hyperparameters exist, a low-cost
|
||||
* Recommended scenario: cost-related hyperparameters exist, a low-cost
|
||||
initial point is known, and the search space is complex such that local search
|
||||
is prone to be stuck at local optima.
|
||||
|
||||
|
||||
- Suggestion about using larger search space in BlendSearch:
|
||||
* Suggestion about using larger search space in BlendSearch:
|
||||
In hyperparameter optimization, a larger search space is desirable because it is more likely to include the optimal configuration (or one of the optimal configurations) in hindsight. However the performance (especially anytime performance) of most existing HPO methods is undesirable if the cost of the configurations in the search space has a large variation. Thus hand-crafted small search spaces (with relatively homogeneous cost) are often used in practice for these methods, which is subject to idiosyncrasy. BlendSearch combines the benefits of local search and global search, which enables a smart (economical) way of deciding where to explore in the search space even though it is larger than necessary. This allows users to specify a larger search space in BlendSearch, which is often easier and a better practice than narrowing down the search space by hand.
|
||||
|
||||
For more technical details, please check our papers.
|
||||
|
||||
* [Frugal Optimization for Cost-related Hyperparameters](https://arxiv.org/abs/2005.01571). Qingyun Wu, Chi Wang, Silu Huang. AAAI 2021.
|
||||
|
||||
```
|
||||
```bibtex
|
||||
@inproceedings{wu2021cfo,
|
||||
title={Frugal Optimization for Cost-related Hyperparameters},
|
||||
author={Qingyun Wu and Chi Wang and Silu Huang},
|
||||
|
@ -207,11 +205,11 @@ For more technical details, please check our papers.
|
|||
|
||||
* [Economical Hyperparameter Optimization With Blended Search Strategy](https://www.microsoft.com/en-us/research/publication/economical-hyperparameter-optimization-with-blended-search-strategy/). Chi Wang, Qingyun Wu, Silu Huang, Amin Saied. ICLR 2021.
|
||||
|
||||
```
|
||||
```bibtex
|
||||
@inproceedings{wang2021blendsearch,
|
||||
title={Economical Hyperparameter Optimization With Blended Search Strategy},
|
||||
author={Chi Wang and Qingyun Wu and Silu Huang and Amin Saied},
|
||||
year={2021},
|
||||
booktitle={ICLR'21},
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
'''!
|
||||
* Copyright (c) 2020-2021 Microsoft Corporation. All rights reserved.
|
||||
"""!
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE file in the
|
||||
* project root for license information.
|
||||
'''
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
# try:
|
||||
# from ray import __version__ as ray_version
|
||||
# assert ray_version >= '1.0.0'
|
||||
|
@ -11,20 +12,19 @@ from typing import Optional
|
|||
# except (ImportError, AssertionError):
|
||||
from .trial import Trial
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Nologger():
|
||||
'''Logger without logging
|
||||
'''
|
||||
class Nologger:
|
||||
"""Logger without logging"""
|
||||
|
||||
def on_result(self, result):
|
||||
pass
|
||||
|
||||
|
||||
class SimpleTrial(Trial):
|
||||
'''A simple trial class
|
||||
'''
|
||||
"""A simple trial class"""
|
||||
|
||||
def __init__(self, config, trial_id=None):
|
||||
self.trial_id = Trial.generate_id() if trial_id is None else trial_id
|
||||
|
@ -49,10 +49,13 @@ class BaseTrialRunner:
|
|||
Note that the caller usually should not mutate trial state directly.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
search_alg=None, scheduler=None,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = 'min'):
|
||||
def __init__(
|
||||
self,
|
||||
search_alg=None,
|
||||
scheduler=None,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = "min",
|
||||
):
|
||||
self._search_alg = search_alg
|
||||
self._scheduler_alg = scheduler
|
||||
self._trials = []
|
||||
|
@ -89,12 +92,12 @@ class BaseTrialRunner:
|
|||
trial.set_status(Trial.PAUSED)
|
||||
|
||||
def stop_trial(self, trial):
|
||||
"""Stops trial.
|
||||
"""
|
||||
"""Stops trial."""
|
||||
if trial.status not in [Trial.ERROR, Trial.TERMINATED]:
|
||||
if self._scheduler_alg:
|
||||
self._scheduler_alg.on_trial_complete(
|
||||
self, trial.trial_id, trial.last_result)
|
||||
self, trial.trial_id, trial.last_result
|
||||
)
|
||||
self._search_alg.on_trial_complete(trial.trial_id, trial.last_result)
|
||||
trial.set_status(Trial.TERMINATED)
|
||||
elif self._scheduler_alg:
|
||||
|
@ -102,8 +105,7 @@ class BaseTrialRunner:
|
|||
|
||||
|
||||
class SequentialTrialRunner(BaseTrialRunner):
|
||||
"""Implementation of the sequential trial runner
|
||||
"""
|
||||
"""Implementation of the sequential trial runner"""
|
||||
|
||||
def step(self) -> Trial:
|
||||
"""Runs one step of the trial event loop.
|
||||
|
@ -114,7 +116,7 @@ class SequentialTrialRunner(BaseTrialRunner):
|
|||
"""
|
||||
trial_id = Trial.generate_id()
|
||||
config = self._search_alg.suggest(trial_id)
|
||||
if config:
|
||||
if config is not None:
|
||||
trial = SimpleTrial(config, trial_id)
|
||||
self.add_trial(trial)
|
||||
trial.set_status(Trial.RUNNING)
|
||||
|
|
|
@ -13,7 +13,10 @@ try:
|
|||
|
||||
assert ray_version >= "1.0.0"
|
||||
from ray.tune.analysis import ExperimentAnalysis as EA
|
||||
|
||||
ray_import = True
|
||||
except (ImportError, AssertionError):
|
||||
ray_import = False
|
||||
from .analysis import ExperimentAnalysis as EA
|
||||
from .result import DEFAULT_METRIC
|
||||
import logging
|
||||
|
@ -278,9 +281,9 @@ def run(
|
|||
else:
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
|
||||
if search_alg is None:
|
||||
from ..searcher.blendsearch import BlendSearch
|
||||
from ..searcher.blendsearch import BlendSearch
|
||||
|
||||
if search_alg is None:
|
||||
search_alg = BlendSearch(
|
||||
metric=metric or DEFAULT_METRIC,
|
||||
mode=mode,
|
||||
|
@ -299,16 +302,27 @@ def run(
|
|||
metric_constraints=metric_constraints,
|
||||
)
|
||||
else:
|
||||
search_alg.set_search_properties(metric, mode, config)
|
||||
if metric is None or mode is None:
|
||||
metric = metric or search_alg.metric
|
||||
mode = mode or search_alg.mode
|
||||
if time_budget_s or num_samples > 0:
|
||||
search_alg.set_search_properties(
|
||||
None,
|
||||
None,
|
||||
config={"time_budget_s": time_budget_s, "num_samples": num_samples},
|
||||
)
|
||||
if ray_import:
|
||||
from ray.tune.suggest import ConcurrencyLimiter
|
||||
else:
|
||||
from flaml.searcher.suggestion import ConcurrencyLimiter
|
||||
searcher = (
|
||||
search_alg.searcher
|
||||
if isinstance(search_alg, ConcurrencyLimiter)
|
||||
else search_alg
|
||||
)
|
||||
if isinstance(searcher, BlendSearch):
|
||||
setting = {}
|
||||
if time_budget_s:
|
||||
setting["time_budget_s"] = time_budget_s
|
||||
if num_samples > 0:
|
||||
setting["num_samples"] = num_samples
|
||||
searcher.set_search_properties(metric, mode, config, setting)
|
||||
else:
|
||||
searcher.set_search_properties(metric, mode, config)
|
||||
scheduler = None
|
||||
if report_intermediate_result:
|
||||
params = {}
|
||||
|
@ -321,15 +335,10 @@ def run(
|
|||
params["grace_period"] = min_resource
|
||||
if reduction_factor:
|
||||
params["reduction_factor"] = reduction_factor
|
||||
try:
|
||||
from ray import __version__ as ray_version
|
||||
|
||||
assert ray_version >= "1.0.0"
|
||||
if ray_import:
|
||||
from ray.tune.schedulers import ASHAScheduler
|
||||
|
||||
scheduler = ASHAScheduler(**params)
|
||||
except (ImportError, AssertionError):
|
||||
pass
|
||||
if use_ray:
|
||||
try:
|
||||
from ray import tune
|
||||
|
@ -392,7 +401,9 @@ def run(
|
|||
else:
|
||||
fail += 1 # break with ub consecutive failures
|
||||
if fail == ub:
|
||||
logger.warning("fail to sample a trial for 10 times in a row, stopping.")
|
||||
logger.warning(
|
||||
f"fail to sample a trial for {max_failure} times in a row, stopping."
|
||||
)
|
||||
if verbose > 0:
|
||||
logger.handlers.clear()
|
||||
return ExperimentAnalysis(_runner.get_trials(), metric=metric, mode=mode)
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
setup.py
4
setup.py
|
@ -18,7 +18,7 @@ install_requires = [
|
|||
"lightgbm>=2.3.1",
|
||||
"xgboost>=0.90,<=1.3.3",
|
||||
"scipy>=1.4.1",
|
||||
"catboost>=0.23",
|
||||
# "catboost>=0.23", # making optional for conda
|
||||
"scikit-learn>=0.24",
|
||||
]
|
||||
|
||||
|
@ -47,6 +47,7 @@ setuptools.setup(
|
|||
"coverage>=5.3",
|
||||
"pre-commit",
|
||||
"xgboost<1.3",
|
||||
"catboost>=0.23",
|
||||
"rgf-python",
|
||||
"optuna==2.8.0",
|
||||
"vowpalwabbit",
|
||||
|
@ -58,6 +59,7 @@ setuptools.setup(
|
|||
"azure-storage-blob",
|
||||
"statsmodels>=0.12.2",
|
||||
],
|
||||
"catboost": ["catboost>=0.23"],
|
||||
"blendsearch": ["optuna==2.8.0"],
|
||||
"ray": [
|
||||
"ray[tune]==1.6.0",
|
||||
|
|
|
@ -2,7 +2,12 @@ import unittest
|
|||
|
||||
import numpy as np
|
||||
import scipy.sparse
|
||||
from sklearn.datasets import load_boston, load_iris, load_wine, load_breast_cancer
|
||||
from sklearn.datasets import (
|
||||
fetch_california_housing,
|
||||
load_iris,
|
||||
load_wine,
|
||||
load_breast_cancer,
|
||||
)
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
|
@ -17,59 +22,37 @@ from flaml.training_log import training_log_reader
|
|||
|
||||
|
||||
class MyRegularizedGreedyForest(SKLearnEstimator):
|
||||
def __init__(
|
||||
self,
|
||||
task="binary",
|
||||
n_jobs=1,
|
||||
max_leaf=4,
|
||||
n_iter=1,
|
||||
n_tree_search=1,
|
||||
opt_interval=1,
|
||||
learning_rate=1.0,
|
||||
min_samples_leaf=1,
|
||||
**params
|
||||
):
|
||||
def __init__(self, task="binary", **config):
|
||||
|
||||
super().__init__(task, **params)
|
||||
super().__init__(task, **config)
|
||||
|
||||
if "regression" in task:
|
||||
self.estimator_class = RGFRegressor
|
||||
else:
|
||||
if task in ("binary", "multi"):
|
||||
self.estimator_class = RGFClassifier
|
||||
|
||||
# round integer hyperparameters
|
||||
self.params = {
|
||||
"n_jobs": n_jobs,
|
||||
"max_leaf": int(round(max_leaf)),
|
||||
"n_iter": int(round(n_iter)),
|
||||
"n_tree_search": int(round(n_tree_search)),
|
||||
"opt_interval": int(round(opt_interval)),
|
||||
"learning_rate": learning_rate,
|
||||
"min_samples_leaf": int(round(min_samples_leaf)),
|
||||
}
|
||||
else:
|
||||
self.estimator_class = RGFRegressor
|
||||
|
||||
@classmethod
|
||||
def search_space(cls, data_size, task):
|
||||
space = {
|
||||
"max_leaf": {
|
||||
"domain": tune.qloguniform(lower=4, upper=data_size, q=1),
|
||||
"domain": tune.lograndint(lower=4, upper=data_size),
|
||||
"init_value": 4,
|
||||
},
|
||||
"n_iter": {
|
||||
"domain": tune.qloguniform(lower=1, upper=data_size, q=1),
|
||||
"domain": tune.lograndint(lower=1, upper=data_size),
|
||||
"init_value": 1,
|
||||
},
|
||||
"n_tree_search": {
|
||||
"domain": tune.qloguniform(lower=1, upper=32768, q=1),
|
||||
"domain": tune.lograndint(lower=1, upper=32768),
|
||||
"init_value": 1,
|
||||
},
|
||||
"opt_interval": {
|
||||
"domain": tune.qloguniform(lower=1, upper=10000, q=1),
|
||||
"domain": tune.lograndint(lower=1, upper=10000),
|
||||
"init_value": 100,
|
||||
},
|
||||
"learning_rate": {"domain": tune.loguniform(lower=0.01, upper=20.0)},
|
||||
"min_samples_leaf": {
|
||||
"domain": tune.qloguniform(lower=1, upper=20, q=1),
|
||||
"domain": tune.lograndint(lower=1, upper=20),
|
||||
"init_value": 20,
|
||||
},
|
||||
}
|
||||
|
@ -97,15 +80,15 @@ def logregobj(preds, dtrain):
|
|||
class MyXGB1(XGBoostEstimator):
|
||||
"""XGBoostEstimator with logregobj as the objective function"""
|
||||
|
||||
def __init__(self, **params):
|
||||
super().__init__(objective=logregobj, **params)
|
||||
def __init__(self, **config):
|
||||
super().__init__(objective=logregobj, **config)
|
||||
|
||||
|
||||
class MyXGB2(XGBoostEstimator):
|
||||
"""XGBoostEstimator with 'reg:squarederror' as the objective function"""
|
||||
|
||||
def __init__(self, **params):
|
||||
super().__init__(objective="reg:squarederror", **params)
|
||||
def __init__(self, **config):
|
||||
super().__init__(objective="reg:squarederror", **config)
|
||||
|
||||
|
||||
class MyLargeLGBM(LGBMEstimator):
|
||||
|
@ -266,7 +249,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_splits": 3,
|
||||
"metric": "accuracy",
|
||||
"log_training_metric": True,
|
||||
"verbose": 1,
|
||||
"verbose": 4,
|
||||
"ensemble": True,
|
||||
}
|
||||
automl.fit(X, y, **automl_settings)
|
||||
|
@ -281,7 +264,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_splits": 3,
|
||||
"metric": "accuracy",
|
||||
"log_training_metric": True,
|
||||
"verbose": 1,
|
||||
"verbose": 4,
|
||||
"ensemble": True,
|
||||
}
|
||||
automl.fit(X, y, **automl_settings)
|
||||
|
@ -296,7 +279,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_splits": 3,
|
||||
"metric": "accuracy",
|
||||
"log_training_metric": True,
|
||||
"verbose": 1,
|
||||
"verbose": 4,
|
||||
"ensemble": True,
|
||||
}
|
||||
automl.fit(X, y, **automl_settings)
|
||||
|
@ -311,7 +294,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_splits": 3,
|
||||
"metric": "accuracy",
|
||||
"log_training_metric": True,
|
||||
"verbose": 1,
|
||||
"verbose": 4,
|
||||
"ensemble": True,
|
||||
}
|
||||
automl.fit(X, y, **automl_settings)
|
||||
|
@ -525,7 +508,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_jobs": 1,
|
||||
"model_history": True,
|
||||
}
|
||||
X_train, y_train = load_boston(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
n = int(len(y_train) * 9 // 10)
|
||||
automl_experiment.fit(
|
||||
X_train=X_train[:n],
|
||||
|
@ -648,7 +631,7 @@ class TestAutoML(unittest.TestCase):
|
|||
"n_concurrent_trials": 2,
|
||||
"hpo_method": hpo_method,
|
||||
}
|
||||
X_train, y_train = load_boston(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
try:
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
print(automl_experiment.predict(X_train))
|
||||
|
@ -861,8 +844,8 @@ class TestAutoML(unittest.TestCase):
|
|||
automl_experiment = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 3,
|
||||
"metric": 'accuracy',
|
||||
"task": 'classification',
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"log_file_name": "test/iris.log",
|
||||
"log_training_metric": True,
|
||||
"n_jobs": 1,
|
||||
|
@ -873,16 +856,19 @@ class TestAutoML(unittest.TestCase):
|
|||
# test drop column
|
||||
X_train.columns = range(X_train.shape[1])
|
||||
X_train[X_train.shape[1]] = np.zeros(len(y_train))
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train,
|
||||
**automl_settings)
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
automl_val_accuracy = 1.0 - automl_experiment.best_loss
|
||||
print('Best ML leaner:', automl_experiment.best_estimator)
|
||||
print('Best hyperparmeter config:', automl_experiment.best_config)
|
||||
print('Best accuracy on validation data: {0:.4g}'.format(automl_val_accuracy))
|
||||
print('Training duration of best run: {0:.4g} s'.format(automl_experiment.best_config_train_time))
|
||||
print("Best ML leaner:", automl_experiment.best_estimator)
|
||||
print("Best hyperparmeter config:", automl_experiment.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(automl_val_accuracy))
|
||||
print(
|
||||
"Training duration of best run: {0:.4g} s".format(
|
||||
automl_experiment.best_config_train_time
|
||||
)
|
||||
)
|
||||
|
||||
starting_points = {}
|
||||
log_file_name = automl_settings['log_file_name']
|
||||
log_file_name = automl_settings["log_file_name"]
|
||||
with training_log_reader(log_file_name) as reader:
|
||||
for record in reader.records():
|
||||
config = record.config
|
||||
|
@ -893,25 +879,28 @@ class TestAutoML(unittest.TestCase):
|
|||
max_iter = sum([len(s) for k, s in starting_points.items()])
|
||||
automl_settings_resume = {
|
||||
"time_budget": 2,
|
||||
"metric": 'accuracy',
|
||||
"task": 'classification',
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"log_file_name": "test/iris_resume_all.log",
|
||||
"log_training_metric": True,
|
||||
"n_jobs": 1,
|
||||
"max_iter": max_iter,
|
||||
"model_history": True,
|
||||
"log_type": 'all',
|
||||
"log_type": "all",
|
||||
"starting_points": starting_points,
|
||||
"append_log": True,
|
||||
}
|
||||
new_automl_experiment = AutoML()
|
||||
new_automl_experiment.fit(X_train=X_train, y_train=y_train,
|
||||
**automl_settings_resume)
|
||||
new_automl_experiment.fit(
|
||||
X_train=X_train, y_train=y_train, **automl_settings_resume
|
||||
)
|
||||
|
||||
new_automl_val_accuracy = 1.0 - new_automl_experiment.best_loss
|
||||
# print('Best ML leaner:', new_automl_experiment.best_estimator)
|
||||
# print('Best hyperparmeter config:', new_automl_experiment.best_config)
|
||||
print('Best accuracy on validation data: {0:.4g}'.format(new_automl_val_accuracy))
|
||||
print(
|
||||
"Best accuracy on validation data: {0:.4g}".format(new_automl_val_accuracy)
|
||||
)
|
||||
# print('Training duration of best run: {0:.4g} s'.format(new_automl_experiment.best_config_train_time))
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flaml.tune.space import unflatten_hierarchical
|
||||
from flaml import AutoML
|
||||
from sklearn.datasets import load_boston
|
||||
from sklearn.datasets import fetch_california_housing
|
||||
import os
|
||||
import unittest
|
||||
import logging
|
||||
|
@ -9,7 +9,6 @@ import io
|
|||
|
||||
|
||||
class TestLogging(unittest.TestCase):
|
||||
|
||||
def test_logging_level(self):
|
||||
|
||||
from flaml import logger, logger_formatter
|
||||
|
@ -30,8 +29,8 @@ class TestLogging(unittest.TestCase):
|
|||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 1,
|
||||
"metric": 'rmse',
|
||||
"task": 'regression',
|
||||
"metric": "rmse",
|
||||
"task": "regression",
|
||||
"log_file_name": training_log,
|
||||
"log_training_metric": True,
|
||||
"n_jobs": 1,
|
||||
|
@ -39,35 +38,42 @@ class TestLogging(unittest.TestCase):
|
|||
"keep_search_state": True,
|
||||
"learner_selector": "roundrobin",
|
||||
}
|
||||
X_train, y_train = load_boston(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
n = len(y_train) >> 1
|
||||
print(automl.model, automl.classes_, automl.predict(X_train))
|
||||
automl.fit(X_train=X_train[:n], y_train=y_train[:n],
|
||||
X_val=X_train[n:], y_val=y_train[n:],
|
||||
**automl_settings)
|
||||
automl.fit(
|
||||
X_train=X_train[:n],
|
||||
y_train=y_train[:n],
|
||||
X_val=X_train[n:],
|
||||
y_val=y_train[n:],
|
||||
**automl_settings
|
||||
)
|
||||
logger.info(automl.search_space)
|
||||
logger.info(automl.low_cost_partial_config)
|
||||
logger.info(automl.points_to_evaluate)
|
||||
logger.info(automl.cat_hp_cost)
|
||||
import optuna as ot
|
||||
|
||||
study = ot.create_study()
|
||||
from flaml.tune.space import define_by_run_func, add_cost_to_space
|
||||
|
||||
sample = define_by_run_func(study.ask(), automl.search_space)
|
||||
logger.info(sample)
|
||||
logger.info(unflatten_hierarchical(sample, automl.search_space))
|
||||
add_cost_to_space(
|
||||
automl.search_space, automl.low_cost_partial_config,
|
||||
automl.cat_hp_cost
|
||||
automl.search_space, automl.low_cost_partial_config, automl.cat_hp_cost
|
||||
)
|
||||
logger.info(automl.search_space["ml"].categories)
|
||||
config = automl.best_config.copy()
|
||||
config['learner'] = automl.best_estimator
|
||||
config["learner"] = automl.best_estimator
|
||||
automl.trainable({"ml": config})
|
||||
from flaml import tune, BlendSearch
|
||||
from flaml.automl import size
|
||||
from functools import partial
|
||||
|
||||
search_alg = BlendSearch(
|
||||
metric='val_loss', mode='min',
|
||||
metric="val_loss",
|
||||
mode="min",
|
||||
space=automl.search_space,
|
||||
low_cost_partial_config=automl.low_cost_partial_config,
|
||||
points_to_evaluate=automl.points_to_evaluate,
|
||||
|
@ -75,19 +81,25 @@ class TestLogging(unittest.TestCase):
|
|||
prune_attr=automl.prune_attr,
|
||||
min_resource=automl.min_resource,
|
||||
max_resource=automl.max_resource,
|
||||
config_constraints=[(partial(size, automl._state), '<=', automl._mem_thres)],
|
||||
metric_constraints=automl.metric_constraints)
|
||||
config_constraints=[
|
||||
(partial(size, automl._state), "<=", automl._mem_thres)
|
||||
],
|
||||
metric_constraints=automl.metric_constraints,
|
||||
)
|
||||
analysis = tune.run(
|
||||
automl.trainable, search_alg=search_alg, # verbose=2,
|
||||
time_budget_s=1, num_samples=-1)
|
||||
print(min(trial.last_result["val_loss"]
|
||||
for trial in analysis.trials))
|
||||
config = analysis.trials[-1].last_result['config']['ml']
|
||||
automl._state._train_with_config(config['learner'], config)
|
||||
automl.trainable,
|
||||
search_alg=search_alg, # verbose=2,
|
||||
time_budget_s=1,
|
||||
num_samples=-1,
|
||||
)
|
||||
print(min(trial.last_result["val_loss"] for trial in analysis.trials))
|
||||
config = analysis.trials[-1].last_result["config"]["ml"]
|
||||
automl._state._train_with_config(config["learner"], config)
|
||||
# Check if the log buffer is populated.
|
||||
self.assertTrue(len(buf.getvalue()) > 0)
|
||||
|
||||
import pickle
|
||||
with open('automl.pkl', 'wb') as f:
|
||||
|
||||
with open("automl.pkl", "wb") as f:
|
||||
pickle.dump(automl, f, pickle.HIGHEST_PROTOCOL)
|
||||
print(automl.__version__)
|
||||
|
|
|
@ -2,15 +2,14 @@ import os
|
|||
import unittest
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from sklearn.datasets import load_boston
|
||||
from sklearn.datasets import fetch_california_housing
|
||||
|
||||
from flaml import AutoML
|
||||
from flaml.training_log import training_log_reader
|
||||
|
||||
|
||||
class TestTrainingLog(unittest.TestCase):
|
||||
|
||||
def test_training_log(self, path='test_training_log.log'):
|
||||
def test_training_log(self, path="test_training_log.log"):
|
||||
|
||||
with TemporaryDirectory() as d:
|
||||
filename = os.path.join(d, path)
|
||||
|
@ -19,8 +18,8 @@ class TestTrainingLog(unittest.TestCase):
|
|||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 1,
|
||||
"metric": 'mse',
|
||||
"task": 'regression',
|
||||
"metric": "mse",
|
||||
"task": "regression",
|
||||
"log_file_name": filename,
|
||||
"log_training_metric": True,
|
||||
"mem_thres": 1024 * 1024,
|
||||
|
@ -31,10 +30,9 @@ class TestTrainingLog(unittest.TestCase):
|
|||
"ensemble": True,
|
||||
"keep_search_state": True,
|
||||
}
|
||||
X_train, y_train = load_boston(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
automl.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
automl._state._train_with_config(
|
||||
automl.best_estimator, automl.best_config)
|
||||
automl._state._train_with_config(automl.best_estimator, automl.best_config)
|
||||
|
||||
# Check if the training log file is populated.
|
||||
self.assertTrue(os.path.exists(filename))
|
||||
|
@ -49,11 +47,11 @@ class TestTrainingLog(unittest.TestCase):
|
|||
automl.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
automl._selected.update(None, 0)
|
||||
automl = AutoML()
|
||||
automl.fit(X_train=X_train, y_train=y_train, max_iter=0)
|
||||
automl.fit(X_train=X_train, y_train=y_train, max_iter=0, task="regression")
|
||||
|
||||
def test_illfilename(self):
|
||||
try:
|
||||
self.test_training_log('/')
|
||||
self.test_training_log("/")
|
||||
except IsADirectoryError:
|
||||
print("IsADirectoryError happens as expected in linux.")
|
||||
except PermissionError:
|
||||
|
|
|
@ -72,8 +72,9 @@ except (ImportError, AssertionError):
|
|||
searcher = BlendSearch(
|
||||
metric="m", global_search_alg=searcher, metric_constraints=[("c", "<", 1)]
|
||||
)
|
||||
searcher.set_search_properties(metric="m2", config=config)
|
||||
searcher.set_search_properties(config={"time_budget_s": 0})
|
||||
searcher.set_search_properties(
|
||||
metric="m2", config=config, setting={"time_budget_s": 0}
|
||||
)
|
||||
c = searcher.suggest("t1")
|
||||
searcher.on_trial_complete("t1", {"config": c}, True)
|
||||
c = searcher.suggest("t2")
|
||||
|
@ -146,3 +147,11 @@ except (ImportError, AssertionError):
|
|||
print(searcher.suggest("t4"))
|
||||
searcher.on_trial_complete({"t1"}, {})
|
||||
searcher.on_trial_result({"t2"}, {})
|
||||
np.random.seed(654321)
|
||||
searcher = RandomSearch(
|
||||
space=config,
|
||||
points_to_evaluate=[{"a": 7, "b": 1e-3}, {"a": 6, "b": 3e-4}],
|
||||
)
|
||||
print(searcher.suggest("t1"))
|
||||
print(searcher.suggest("t2"))
|
||||
print(searcher.suggest("t3"))
|
||||
|
|
Loading…
Reference in New Issue