mirror of
https://github.com/microsoft/autogen.git
synced 2026-01-21 07:18:24 -05:00
v0.2.6 (#32)
* xgboost notebook * finetuning notebook * finetuning test * experimental nni support * support nested search space * log file name * record training_iteration * eps * reset times * std set to default step size if 0
This commit is contained in:
@@ -845,7 +845,7 @@ class AutoML:
|
||||
if eval_method == 'auto' or self._state.X_val is not None:
|
||||
eval_method = self._decide_eval_method(time_budget)
|
||||
self._state.eval_method = eval_method
|
||||
if not mlflow or not mlflow.active_run() and not logger.handler:
|
||||
if (not mlflow or not mlflow.active_run()) and not logger.handlers:
|
||||
# Add the console handler.
|
||||
_ch = logging.StreamHandler()
|
||||
_ch.setFormatter(logger_formatter)
|
||||
@@ -1074,7 +1074,7 @@ class AutoML:
|
||||
search_state.best_config,
|
||||
estimator,
|
||||
search_state.sample_size)
|
||||
if mlflow is not None:
|
||||
if mlflow is not None and mlflow.active_run():
|
||||
with mlflow.start_run(nested=True) as run:
|
||||
mlflow.log_metric('iter_counter',
|
||||
self._iter_per_learner[estimator])
|
||||
|
||||
@@ -25,6 +25,8 @@ class BlendSearch(Searcher):
|
||||
'''class for BlendSearch algorithm
|
||||
'''
|
||||
|
||||
cost_attr = "time_total_s" # cost attribute in result
|
||||
|
||||
def __init__(self,
|
||||
metric: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
@@ -193,7 +195,7 @@ class BlendSearch(Searcher):
|
||||
self._search_thread_pool[self._thread_count] = SearchThread(
|
||||
self._ls.mode,
|
||||
self._ls.create(config, result[self._metric], cost=result[
|
||||
"time_total_s"])
|
||||
self.cost_attr])
|
||||
)
|
||||
thread_id = self._thread_count
|
||||
self._thread_count += 1
|
||||
@@ -393,7 +395,89 @@ class BlendSearch(Searcher):
|
||||
return True
|
||||
|
||||
|
||||
class CFO(BlendSearch):
|
||||
try:
|
||||
from nni.tuner import Tuner as NNITuner
|
||||
from nni.utils import extract_scalar_reward
|
||||
try:
|
||||
from ray.tune import (uniform, quniform, choice, randint, qrandint, randn,
|
||||
qrandn, loguniform, qloguniform)
|
||||
except:
|
||||
from .sample import (uniform, quniform, choice, randint, qrandint, randn,
|
||||
qrandn, loguniform, qloguniform)
|
||||
|
||||
class BlendSearchTuner(BlendSearch, NNITuner):
|
||||
'''Tuner class for NNI
|
||||
'''
|
||||
|
||||
def receive_trial_result(self, parameter_id, parameters, value,
|
||||
**kwargs):
|
||||
'''
|
||||
Receive trial's final result.
|
||||
parameter_id: int
|
||||
parameters: object created by 'generate_parameters()'
|
||||
value: final metrics of the trial, including default metric
|
||||
'''
|
||||
result = {}
|
||||
for key, value in parameters:
|
||||
result['config/'+key] = value
|
||||
reward = extract_scalar_reward(value)
|
||||
result[self._metric] = reward
|
||||
# if nni does not report training cost,
|
||||
# using sequence as an approximation.
|
||||
# if no sequence, using a constant 1
|
||||
result[self.cost_attr] = value.get(self.cost_attr, value.get(
|
||||
'sequence', 1))
|
||||
self.on_trial_complete(str(parameter_id), result)
|
||||
...
|
||||
|
||||
def generate_parameters(self, parameter_id, **kwargs) -> Dict:
|
||||
'''
|
||||
Returns a set of trial (hyper-)parameters, as a serializable object
|
||||
parameter_id: int
|
||||
'''
|
||||
return self.suggest(str(parameter_id))
|
||||
...
|
||||
|
||||
def update_search_space(self, search_space):
|
||||
'''
|
||||
Tuners are advised to support updating search space at run-time.
|
||||
If a tuner can only set search space once before generating first hyper-parameters,
|
||||
it should explicitly document this behaviour.
|
||||
search_space: JSON object created by experiment owner
|
||||
'''
|
||||
config = {}
|
||||
for key, value in search_space:
|
||||
v = value.get("_value")
|
||||
_type = value['_type']
|
||||
if _type == 'choice':
|
||||
config[key] = choice(v)
|
||||
elif _type == 'randint':
|
||||
config[key] = randint(v[0], v[1]-1)
|
||||
elif _type == 'uniform':
|
||||
config[key] = uniform(v[0], v[1])
|
||||
elif _type == 'quniform':
|
||||
config[key] = quniform(v[0], v[1], v[2])
|
||||
elif _type == 'loguniform':
|
||||
config[key] = loguniform(v[0], v[1])
|
||||
elif _type == 'qloguniform':
|
||||
config[key] = qloguniform(v[0], v[1], v[2])
|
||||
elif _type == 'normal':
|
||||
config[key] = randn(v[1], v[2])
|
||||
elif _type == 'qnormal':
|
||||
config[key] = qrandn(v[1], v[2], v[3])
|
||||
else:
|
||||
raise ValueError(
|
||||
f'unsupported type in search_space {_type}')
|
||||
self._ls.set_search_properties(None, None, config)
|
||||
if self._gs is not None:
|
||||
self._gs.set_search_properties(None, None, config)
|
||||
self._init_search()
|
||||
|
||||
except:
|
||||
class BlendSearchTuner(BlendSearch): pass
|
||||
|
||||
|
||||
class CFO(BlendSearchTuner):
|
||||
''' class for CFO algorithm
|
||||
'''
|
||||
|
||||
@@ -416,3 +500,5 @@ class CFO(BlendSearch):
|
||||
''' create thread condition
|
||||
'''
|
||||
return len(self._search_thread_pool) < 2
|
||||
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ try:
|
||||
from ray.tune.suggest import Searcher
|
||||
from ray.tune.suggest.variant_generator import generate_variants
|
||||
from ray.tune import sample
|
||||
from ray.tune.utils.util import flatten_dict, unflatten_dict
|
||||
except ImportError:
|
||||
from .suggestion import Searcher
|
||||
from .variant_generator import generate_variants
|
||||
from .variant_generator import generate_variants, flatten_dict, unflatten_dict
|
||||
from ..tune import sample
|
||||
|
||||
|
||||
@@ -86,6 +87,7 @@ class FLOW2(Searcher):
|
||||
elif mode == "min":
|
||||
self.metric_op = 1.
|
||||
self.space = space or {}
|
||||
self.space = flatten_dict(self.space, prevent_delimiter=True)
|
||||
self._random = np.random.RandomState(seed)
|
||||
self._seed = seed
|
||||
if not init_config:
|
||||
@@ -95,7 +97,8 @@ class FLOW2(Searcher):
|
||||
"consider providing init values for cost-related hps via "
|
||||
"'init_config'."
|
||||
)
|
||||
self.init_config = self.best_config = init_config
|
||||
self.init_config = init_config
|
||||
self.best_config = flatten_dict(init_config)
|
||||
self.cat_hp_cost = cat_hp_cost
|
||||
self.prune_attr = prune_attr
|
||||
self.min_resource = min_resource
|
||||
@@ -171,7 +174,7 @@ class FLOW2(Searcher):
|
||||
# logger.info(self._resource)
|
||||
else: self._resource = None
|
||||
self.incumbent = {}
|
||||
self.incumbent = self.normalize(self.init_config)
|
||||
self.incumbent = self.normalize(self.best_config) # flattened
|
||||
self.best_obj = self.cost_incumbent = None
|
||||
self.dim = len(self._tunable_keys) # total # tunable dimensions
|
||||
self._direction_tried = None
|
||||
@@ -247,7 +250,7 @@ class FLOW2(Searcher):
|
||||
if key not in self._unordered_cat_hp:
|
||||
if upper and lower:
|
||||
u, l = upper[key], lower[key]
|
||||
gauss_std = u-l
|
||||
gauss_std = u-l or self.STEPSIZE
|
||||
# allowed bound
|
||||
u += self.STEPSIZE
|
||||
l -= self.STEPSIZE
|
||||
@@ -261,11 +264,11 @@ class FLOW2(Searcher):
|
||||
normalized[key] = max(l, min(u, normalized[key] + delta))
|
||||
# use best config for unordered cat choice
|
||||
config = self.denormalize(normalized)
|
||||
self._reset_times += 1
|
||||
else:
|
||||
# first time init_config, or other configs, take as is
|
||||
config = partial_config.copy()
|
||||
|
||||
if partial_config == self.init_config: self._reset_times += 1
|
||||
config = flatten_dict(config)
|
||||
for key, value in self.space.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
@@ -277,13 +280,13 @@ class FLOW2(Searcher):
|
||||
|
||||
if self._resource:
|
||||
config[self.prune_attr] = self.min_resource
|
||||
return config
|
||||
return unflatten_dict(config)
|
||||
|
||||
def create(self, init_config: Dict, obj: float, cost: float) -> Searcher:
|
||||
flow2 = FLOW2(init_config, self.metric, self.mode, self._cat_hp_cost,
|
||||
self.space, self.prune_attr, self.min_resource,
|
||||
self.max_resource, self.resource_multiple_factor,
|
||||
self._seed+1)
|
||||
unflatten_dict(self.space), self.prune_attr,
|
||||
self.min_resource, self.max_resource,
|
||||
self.resource_multiple_factor, self._seed+1)
|
||||
flow2.best_obj = obj * self.metric_op # minimize internally
|
||||
flow2.cost_incumbent = cost
|
||||
return flow2
|
||||
@@ -292,7 +295,7 @@ class FLOW2(Searcher):
|
||||
''' normalize each dimension in config to [0,1]
|
||||
'''
|
||||
config_norm = {}
|
||||
for key, value in config.items():
|
||||
for key, value in flatten_dict(config).items():
|
||||
if key in self.space:
|
||||
# domain: sample.Categorical/Integer/Float/Function
|
||||
domain = self.space[key]
|
||||
@@ -426,7 +429,7 @@ class FLOW2(Searcher):
|
||||
obj = result.get(self._metric)
|
||||
if obj:
|
||||
obj *= self.metric_op
|
||||
if obj < self.best_obj:
|
||||
if self.best_obj is None or obj < self.best_obj:
|
||||
self.best_obj, self.best_config = obj, self._configs[
|
||||
trial_id]
|
||||
self.incumbent = self.normalize(self.best_config)
|
||||
@@ -437,7 +440,8 @@ class FLOW2(Searcher):
|
||||
self._cost_complete4incumbent = 0
|
||||
self._num_allowed4incumbent = 2 * self.dim
|
||||
self._proposed_by.clear()
|
||||
if self._K > 0:
|
||||
if self._K > 0:
|
||||
# self._oldK must have been set when self._K>0
|
||||
self.step *= np.sqrt(self._K/self._oldK)
|
||||
if self.step > self.step_ub: self.step = self.step_ub
|
||||
self._iter_best_config = self.trial_count
|
||||
@@ -474,7 +478,7 @@ class FLOW2(Searcher):
|
||||
obj = result.get(self._metric)
|
||||
if obj:
|
||||
obj *= self.metric_op
|
||||
if obj < self.best_obj:
|
||||
if self.best_obj is None or obj < self.best_obj:
|
||||
self.best_obj = obj
|
||||
config = self._configs[trial_id]
|
||||
if self.best_config != config:
|
||||
@@ -533,7 +537,7 @@ class FLOW2(Searcher):
|
||||
config = self.denormalize(move)
|
||||
self._proposed_by[trial_id] = self.incumbent
|
||||
self._configs[trial_id] = config
|
||||
return config
|
||||
return unflatten_dict(config)
|
||||
|
||||
def _project(self, config):
|
||||
''' project normalized config in the feasible region and set prune_attr
|
||||
@@ -553,6 +557,7 @@ class FLOW2(Searcher):
|
||||
def config_signature(self, config) -> tuple:
|
||||
''' return the signature tuple of a config
|
||||
'''
|
||||
config = flatten_dict(config)
|
||||
value_list = []
|
||||
for key in self._space_keys:
|
||||
if key in config:
|
||||
|
||||
@@ -20,6 +20,7 @@ class SearchThread:
|
||||
'''
|
||||
|
||||
cost_attr = 'time_total_s'
|
||||
eps = 1e-10
|
||||
|
||||
def __init__(self, mode: str = "min",
|
||||
search_alg: Optional[Searcher] = None):
|
||||
@@ -70,7 +71,7 @@ class SearchThread:
|
||||
# calculate speed; use 0 for invalid speed temporarily
|
||||
if self.obj_best2 > self.obj_best1:
|
||||
self.speed = (self.obj_best2 - self.obj_best1) / (
|
||||
self.cost_total - self.cost_best2)
|
||||
self.cost_total - self.cost_best2 + self.eps)
|
||||
else: self.speed = 0
|
||||
|
||||
def on_trial_complete(self, trial_id: str, result: Optional[Dict] = None,
|
||||
|
||||
@@ -28,6 +28,46 @@ from ..tune.sample import Categorical, Domain, Function
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def flatten_dict(dt, delimiter="/", prevent_delimiter=False):
|
||||
dt = copy.deepcopy(dt)
|
||||
if prevent_delimiter and any(delimiter in key for key in dt):
|
||||
# Raise if delimiter is any of the keys
|
||||
raise ValueError(
|
||||
"Found delimiter `{}` in key when trying to flatten array."
|
||||
"Please avoid using the delimiter in your specification.")
|
||||
while any(isinstance(v, dict) for v in dt.values()):
|
||||
remove = []
|
||||
add = {}
|
||||
for key, value in dt.items():
|
||||
if isinstance(value, dict):
|
||||
for subkey, v in value.items():
|
||||
if prevent_delimiter and delimiter in subkey:
|
||||
# Raise if delimiter is in any of the subkeys
|
||||
raise ValueError(
|
||||
"Found delimiter `{}` in key when trying to "
|
||||
"flatten array. Please avoid using the delimiter "
|
||||
"in your specification.")
|
||||
add[delimiter.join([key, str(subkey)])] = v
|
||||
remove.append(key)
|
||||
dt.update(add)
|
||||
for k in remove:
|
||||
del dt[k]
|
||||
return dt
|
||||
|
||||
|
||||
def unflatten_dict(dt, delimiter="/"):
|
||||
"""Unflatten dict. Does not support unflattening lists."""
|
||||
dict_type = type(dt)
|
||||
out = dict_type()
|
||||
for key, val in dt.items():
|
||||
path = key.split(delimiter)
|
||||
item = out
|
||||
for k in path[:-1]:
|
||||
item = item.setdefault(k, dict_type())
|
||||
item[path[-1]] = val
|
||||
return out
|
||||
|
||||
|
||||
class TuneError(Exception):
|
||||
"""General error class raised by ray.tune."""
|
||||
pass
|
||||
|
||||
@@ -17,6 +17,8 @@ logger = logging.getLogger(__name__)
|
||||
_use_ray = True
|
||||
_runner = None
|
||||
_verbose = 0
|
||||
_running_trial = None
|
||||
_training_iteration = 0
|
||||
|
||||
|
||||
class ExperimentAnalysis(EA):
|
||||
@@ -68,6 +70,8 @@ def report(_metric=None, **kwargs):
|
||||
'''
|
||||
global _use_ray
|
||||
global _verbose
|
||||
global _running_trial
|
||||
global _training_iteration
|
||||
if _use_ray:
|
||||
from ray import tune
|
||||
return tune.report(_metric, **kwargs)
|
||||
@@ -77,6 +81,12 @@ def report(_metric=None, **kwargs):
|
||||
logger.info(f"result: {kwargs}")
|
||||
if _metric: result['_default_anonymous_metric'] = _metric
|
||||
trial = _runner.running_trial
|
||||
if _running_trial == trial:
|
||||
_training_iteration += 1
|
||||
else:
|
||||
_training_iteration = 0
|
||||
_running_trial = trial
|
||||
result["training_iteration"] = _training_iteration
|
||||
result['config'] = trial.config
|
||||
for key, value in trial.config.items():
|
||||
result['config/'+key] = value
|
||||
@@ -213,7 +223,7 @@ def run(training_function,
|
||||
import os
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
logger.addHandler(logging.FileHandler(local_dir+'/tune_'+str(
|
||||
datetime.datetime.now())+'.log'))
|
||||
datetime.datetime.now()).replace(':', '-')+'.log'))
|
||||
if verbose<=2:
|
||||
logger.setLevel(logging.INFO)
|
||||
else:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.2.5"
|
||||
__version__ = "0.2.6"
|
||||
|
||||
File diff suppressed because one or more lines are too long
556
notebook/flaml_xgboost.ipynb
Normal file
556
notebook/flaml_xgboost.ipynb
Normal file
File diff suppressed because one or more lines are too long
5
setup.py
5
setup.py
@@ -57,8 +57,11 @@ setuptools.setup(
|
||||
"pyyaml<5.3.1",
|
||||
],
|
||||
"azureml": [
|
||||
"azureml-mlflow"
|
||||
"azureml-mlflow",
|
||||
],
|
||||
"nni": [
|
||||
"nni",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
|
||||
@@ -15,6 +15,17 @@ try:
|
||||
Trainer,
|
||||
TrainingArguments,
|
||||
)
|
||||
MODEL_CHECKPOINT = "distilbert-base-uncased"
|
||||
TASK = "cola"
|
||||
NUM_LABELS = 2
|
||||
COLUMN_NAME = "sentence"
|
||||
METRIC_NAME = "matthews_correlation"
|
||||
|
||||
# HP_METRIC, MODE = "loss", "min"
|
||||
HP_METRIC, MODE = "matthews_correlation", "max"
|
||||
|
||||
# Define tokenize method
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_CHECKPOINT, use_fast=True)
|
||||
except:
|
||||
print("pip install torch transformers datasets flaml[blendsearch,ray]")
|
||||
|
||||
@@ -25,24 +36,21 @@ logger.setLevel(logging.INFO)
|
||||
|
||||
import flaml
|
||||
|
||||
|
||||
MODEL_CHECKPOINT = "distilbert-base-uncased"
|
||||
TASK = "cola"
|
||||
NUM_LABELS = 2
|
||||
COLUMN_NAME = "sentence"
|
||||
METRIC_NAME = "matthews_correlation"
|
||||
|
||||
# HP_METRIC, MODE = "loss", "min"
|
||||
HP_METRIC, MODE = "matthews_correlation", "max"
|
||||
|
||||
def train_distilbert(config: dict):
|
||||
|
||||
# Define tokenize method
|
||||
tokenizer = AutoTokenizer.from_pretrained(MODEL_CHECKPOINT, use_fast=True)
|
||||
metric = load_metric("glue", TASK)
|
||||
|
||||
def tokenize(examples):
|
||||
return tokenizer(examples[COLUMN_NAME], truncation=True)
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return metric.compute(predictions=predictions, references=labels)
|
||||
|
||||
# Load CoLA dataset and apply tokenizer
|
||||
cola_raw = load_dataset("glue", TASK)
|
||||
|
||||
cola_encoded = cola_raw.map(tokenize, batched=True)
|
||||
train_dataset, eval_dataset = cola_encoded["train"], cola_encoded["validation"]
|
||||
|
||||
@@ -50,13 +58,6 @@ def train_distilbert(config: dict):
|
||||
MODEL_CHECKPOINT, num_labels=NUM_LABELS
|
||||
)
|
||||
|
||||
metric = load_metric("glue", TASK)
|
||||
|
||||
def compute_metrics(eval_pred):
|
||||
predictions, labels = eval_pred
|
||||
predictions = np.argmax(predictions, axis=1)
|
||||
return metric.compute(predictions=predictions, references=labels)
|
||||
|
||||
training_args = TrainingArguments(
|
||||
output_dir='.',
|
||||
do_eval=False,
|
||||
@@ -91,7 +92,7 @@ def _test_distillbert(method='BlendSearch'):
|
||||
|
||||
max_num_epoch = 64
|
||||
num_samples = -1
|
||||
time_budget_s = 10800
|
||||
time_budget_s = 3600
|
||||
|
||||
search_space = {
|
||||
# You can mix constants with search space objects.
|
||||
@@ -123,7 +124,7 @@ def _test_distillbert(method='BlendSearch'):
|
||||
from flaml import BlendSearch
|
||||
algo = BlendSearch(points_to_evaluate=[{
|
||||
"num_train_epochs": 1,
|
||||
}])
|
||||
}])
|
||||
elif 'Dragonfly' == method:
|
||||
from ray.tune.suggest.dragonfly import DragonflySearch
|
||||
algo = DragonflySearch()
|
||||
@@ -139,7 +140,7 @@ def _test_distillbert(method='BlendSearch'):
|
||||
algo = ZOOptSearch(budget=num_samples)
|
||||
elif 'Ax' == method:
|
||||
from ray.tune.suggest.ax import AxSearch
|
||||
algo = AxSearch()
|
||||
algo = AxSearch(max_concurrent=3)
|
||||
elif 'HyperOpt' == method:
|
||||
from ray.tune.suggest.hyperopt import HyperOptSearch
|
||||
algo = HyperOptSearch()
|
||||
@@ -154,8 +155,7 @@ def _test_distillbert(method='BlendSearch'):
|
||||
train_distilbert,
|
||||
metric=HP_METRIC,
|
||||
mode=MODE,
|
||||
# You can add "gpu": 1 to allocate GPUs
|
||||
resources_per_trial={"gpu": 1},
|
||||
resources_per_trial={"gpu": 4, "cpu": 4},
|
||||
config=search_space, local_dir='test/logs/',
|
||||
num_samples=num_samples, time_budget_s=time_budget_s,
|
||||
keep_checkpoints_num=1, checkpoint_score_attr=HP_METRIC,
|
||||
|
||||
@@ -49,7 +49,6 @@ def _test_xgboost(method='BlendSearch'):
|
||||
else:
|
||||
from ray import tune
|
||||
search_space = {
|
||||
# You can mix constants with search space objects.
|
||||
"max_depth": tune.randint(1, 8) if method in [
|
||||
"BlendSearch", "BOHB", "Optuna"] else tune.randint(1, 9),
|
||||
"min_child_weight": tune.choice([1, 2, 3]),
|
||||
@@ -154,6 +153,33 @@ def _test_xgboost(method='BlendSearch'):
|
||||
logger.info(f"Best model parameters: {best_trial.config}")
|
||||
|
||||
|
||||
def test_nested():
|
||||
from flaml import tune
|
||||
search_space = {
|
||||
# test nested search space
|
||||
"cost_related": {
|
||||
"a": tune.randint(1, 8),
|
||||
},
|
||||
"b": tune.uniform(0.5, 1.0),
|
||||
}
|
||||
|
||||
def simple_func(config):
|
||||
tune.report(
|
||||
metric=(config["cost_related"]["a"]-4)**2 * (config["b"]-0.7)**2)
|
||||
|
||||
analysis = tune.run(
|
||||
simple_func,
|
||||
init_config={
|
||||
"cost_related": {"a": 1,}
|
||||
},
|
||||
metric="metric",
|
||||
mode="min",
|
||||
config=search_space,
|
||||
local_dir='logs/',
|
||||
num_samples=-1,
|
||||
time_budget_s=1)
|
||||
|
||||
|
||||
def test_xgboost_bs():
|
||||
_test_xgboost()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user