* 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:
Chi Wang
2021-02-28 12:43:43 -08:00
committed by GitHub
parent 6ff0ed434b
commit 7bd231e497
12 changed files with 1370 additions and 220 deletions

View File

@@ -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])

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -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:

View File

@@ -1 +1 @@
__version__ = "0.2.5"
__version__ = "0.2.6"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -57,8 +57,11 @@ setuptools.setup(
"pyyaml<5.3.1",
],
"azureml": [
"azureml-mlflow"
"azureml-mlflow",
],
"nni": [
"nni",
]
},
classifiers=[
"Programming Language :: Python :: 3",

View File

@@ -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,

View File

@@ -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()