diff --git a/dependencies/required_extra.txt b/dependencies/required_extra.txt index 65940c9194..9bf0cac29a 100644 --- a/dependencies/required_extra.txt +++ b/dependencies/required_extra.txt @@ -11,3 +11,6 @@ statsmodels==0.12.0 # PPOTuner gym + +# DNGO +pybnn diff --git a/docs/en_US/Tuner/BuiltinTuner.rst b/docs/en_US/Tuner/BuiltinTuner.rst index 768913b373..0925bae104 100644 --- a/docs/en_US/Tuner/BuiltinTuner.rst +++ b/docs/en_US/Tuner/BuiltinTuner.rst @@ -45,7 +45,8 @@ Currently, we support the following algorithms: - PPO Tuner is a Reinforcement Learning tuner based on PPO algorithm. `Reference Paper `__ * - `PBT Tuner <#PBTTuner>`__ - PBT Tuner is a simple asynchronous optimization algorithm which effectively utilizes a fixed computational budget to jointly optimize a population of models and their hyperparameters to maximize performance. `Reference Paper `__ - + * - `DNGO Tuner <#DNGOTuner>`__ + - Use of neural networks as an alternative to GPs to model distributions over functions in bayesian optimization. Usage of Built-in Tuners ------------------------ @@ -574,6 +575,41 @@ Population Based Training (PBT) bridges and extends parallel search methods and Note that, to use this tuner, your trial code should be modified accordingly, please refer to `the document of PBTTuner <./PBTTuner.rst>`__ for details. +DNGO Tuner +^^^^^^^^^^ + +.. + + Built-in Tuner Name: **DNGOTuner** + +DNGO advisor requires `pybnn`, which can be installed with the following command. + +.. code-block:: bash + + pip install nni[DNGO] + +**Suggested scenario** + +Applicable to large scale hyperparameter optimization. Bayesian optimization that rapidly finds competitive models on benchmark object recognition tasks using convolutional networks, and image caption generation using neural language models. + +**classArgs requirements:** + + +* **optimize_mode** (*'maximize' or 'minimize'*\ ) - If 'maximize', the tuner will target to maximize metrics. If 'minimize', the tuner will target to minimize metrics. +* **sample_size** (*int, default = 1000*) - Number of samples to select in each iteration. The best one will be picked from the samples as the next trial. +* **trials_per_update** (*int, default = 20*) - Number of trials to collect before updating the model. +* **num_epochs_per_training** (*int, default = 500*) - Number of epochs to train DNGO model. + +**Usage example** + +.. code-block:: yaml + + # config.yml + tuner: + builtinTunerName: DNGOTuner + classArgs: + optimize_mode: maximize + **Reference and Feedback** ------------------------------ diff --git a/docs/en_US/Tutorial/SearchSpaceSpec.rst b/docs/en_US/Tutorial/SearchSpaceSpec.rst index 0512e4d394..27ce2c31d3 100644 --- a/docs/en_US/Tutorial/SearchSpaceSpec.rst +++ b/docs/en_US/Tutorial/SearchSpaceSpec.rst @@ -243,13 +243,25 @@ Search Space Types Supported by Each Tuner - - - + * - DNGO Tuner + - :raw-html:`✓` + - + - :raw-html:`✓` + - :raw-html:`✓` + - :raw-html:`✓` + - :raw-html:`✓` + - :raw-html:`✓` + - + - + - + - Known Limitations: * - GP Tuner and Metis Tuner support only **numerical values** in search space (\ ``choice`` type values can be no-numerical with other tuners, e.g. string values). Both GP Tuner and Metis Tuner use Gaussian Process Regressor(GPR). GPR make predictions based on a kernel function and the 'distance' between different points, it's hard to get the true distance between no-numerical values. + GP Tuner, Metis Tuner and DNGO tuner support only **numerical values** in search space (\ ``choice`` type values can be no-numerical with other tuners, e.g. string values). Both GP Tuner and Metis Tuner use Gaussian Process Regressor(GPR). GPR make predictions based on a kernel function and the 'distance' between different points, it's hard to get the true distance between no-numerical values. * Note that for nested search space: diff --git a/nni/algorithms/hpo/dngo_tuner.py b/nni/algorithms/hpo/dngo_tuner.py new file mode 100644 index 0000000000..09257b0906 --- /dev/null +++ b/nni/algorithms/hpo/dngo_tuner.py @@ -0,0 +1,117 @@ +import logging + +import numpy as np +import torch +import nni.parameter_expressions as parameter_expressions +from nni import ClassArgsValidator +from nni.tuner import Tuner +from pybnn import DNGO +from torch.distributions import Normal + +_logger = logging.getLogger(__name__) + + +def _random_config(search_space, random_state): + chosen_config = {} + for key, val in search_space.items(): + if val['_type'] == 'choice': + choices = val['_value'] + index = random_state.randint(len(choices)) + if all([isinstance(c, (int, float)) for c in choices]): + chosen_config[key] = choices[index] + else: + raise ValueError('Choices with type other than int and float is not supported.') + elif val['_type'] == 'uniform': + chosen_config[key] = random_state.uniform(val['_value'][0], val['_value'][1]) + elif val['_type'] == 'randint': + chosen_config[key] = random_state.randint( + val['_value'][0], val['_value'][1]) + elif val['_type'] == 'quniform': + chosen_config[key] = parameter_expressions.quniform( + val['_value'][0], val['_value'][1], val['_value'][2], random_state) + elif val['_type'] == 'loguniform': + chosen_config[key] = parameter_expressions.loguniform( + val['_value'][0], val['_value'][1], random_state) + elif val['_type'] == 'qloguniform': + chosen_config[key] = parameter_expressions.qloguniform( + val['_value'][0], val['_value'][1], val['_value'][2], random_state) + else: + raise ValueError('Unknown key %s and value %s' % (key, val)) + return chosen_config + + +class DngoTuner(Tuner): + + def __init__(self, optimize_mode='maximize', sample_size=1000, trials_per_update=20, num_epochs_per_training=500): + self.searchspace_json = None + self.random_state = None + self.model = DNGO(do_mcmc=False, num_epochs=num_epochs_per_training) + self._model_initialized = False + self.sample_size = sample_size + self.trials_per_update = trials_per_update + self.optimize_mode = optimize_mode + + self.x = [] + self.y = [] + + def receive_trial_result(self, parameter_id, parameters, value, **kwargs): + self.x.append(parameters) + self.y.append(self._get_default_value(value)) + if len(self.y) % self.trials_per_update == 0: + self._update_model() + + def generate_parameters(self, parameter_id, **kwargs): + if not self._model_initialized: + return _random_config(self.searchspace_json, self.random_state) + else: + # random samples and pick best with model + candidate_x = [_random_config(self.searchspace_json, self.random_state) for _ in range(self.sample_size)] + + x_test = np.array([np.array(list(xi.values())) for xi in candidate_x]) + m, v = self.model.predict(x_test) + mean = torch.Tensor(m) + sigma = torch.Tensor(v) + u = (mean - torch.Tensor([0.95]).expand_as(mean)) / sigma + normal = Normal(torch.zeros_like(u), torch.ones_like(u)) + ucdf = normal.cdf(u) + updf = torch.exp(normal.log_prob(u)) + ei = sigma * (updf + u * ucdf) + + if self.optimize_mode == 'maximize': + ind = torch.argmax(ei) + else: + ind = torch.argmin(ei) + new_x = candidate_x[ind] + return new_x + + def update_search_space(self, search_space): + self.searchspace_json = search_space + self.random_state = np.random.RandomState() + + def import_data(self, data): + for d in data: + self.x.append(d['parameter']) + self.y.append(self._get_default_value(d['value'])) + self._update_model() + + def _update_model(self): + _logger.info('Updating model on %d samples', len(self.x)) + x_arr = [] + for x in self.x: + x_arr.append([x[k] for k in sorted(x.keys())]) + self.model.train(np.array(x_arr), np.array(self.y), do_optimize=True) + self._model_initialized = True + + def _get_default_value(self, value): + if isinstance(value, dict) and 'default' in value: + return value['default'] + elif isinstance(value, float): + return value + else: + raise ValueError(f'Unsupported value: {value}') + + +class DNGOClassArgsValidator(ClassArgsValidator): + # DNGO tuner do not have much input arg, so the validation is actually hardly used + def validate_class_args(self, **kwargs): + pass diff --git a/nni/runtime/default_config/registered_algorithms.yml b/nni/runtime/default_config/registered_algorithms.yml index 99a6c3b9b0..04c87e8de8 100644 --- a/nni/runtime/default_config/registered_algorithms.yml +++ b/nni/runtime/default_config/registered_algorithms.yml @@ -76,3 +76,7 @@ tuners: classArgsValidator: nni.algorithms.hpo.regularized_evolution_tuner.EvolutionClassArgsValidator className: nni.algorithms.hpo.regularized_evolution_tuner.RegularizedEvolutionTuner source: nni +- builtinName: DNGOTuner + classArgsValidator: nni.algorithms.hpo.dngo_tuner.DNGOClassArgsValidator + className: nni.algorithms.hpo.dngo_tuner.DNGOTuner + source: nni diff --git a/pipelines/fast-test.yml b/pipelines/fast-test.yml index a93b62944f..bee5c8fd28 100644 --- a/pipelines/fast-test.yml +++ b/pipelines/fast-test.yml @@ -161,7 +161,7 @@ stages: - script: | set -e python -m pip install -r dependencies/recommended.txt - python -m pip install -e .[SMAC,BOHB,PPOTuner] + python -m pip install -e .[SMAC,BOHB,PPOTuner,DNGO] displayName: Install extra dependencies # Need del later @@ -246,7 +246,7 @@ stages: - script: | set -e python -m pip install -r dependencies/recommended_legacy.txt - python -m pip install -e .[SMAC,BOHB,PPOTuner] + python -m pip install -e .[SMAC,BOHB,PPOTuner,DNGO] displayName: Install extra dependencies # Need del later @@ -335,7 +335,7 @@ stages: - script: | set -e python -m pip install -r dependencies/recommended.txt - python -m pip install -e .[SMAC,BOHB,PPOTuner] + python -m pip install -e .[SMAC,BOHB,PPOTuner,DNGO] displayName: Install extra dependencies # Need del later @@ -398,6 +398,7 @@ stages: - script: | python -m pip install -r dependencies/recommended.txt + python -m pip install -e .[DNGO] displayName: Install extra dependencies # Need del later diff --git a/pipelines/full-test-linux.yml b/pipelines/full-test-linux.yml index 97f8ab415a..0a6b801283 100644 --- a/pipelines/full-test-linux.yml +++ b/pipelines/full-test-linux.yml @@ -35,6 +35,7 @@ jobs: python3 -m pip install keras==2.1.6 python3 -m pip install tensorflow==2.3.1 tensorflow-estimator==2.3.0 python3 -m pip install thop + python3 -m pip install pybnn python3 -m pip install tianshou>=0.4.1 gym sudo apt-get install swig -y displayName: Install extra dependencies diff --git a/pipelines/full-test-windows.yml b/pipelines/full-test-windows.yml index 30dd072ddb..fd3efdc277 100644 --- a/pipelines/full-test-windows.yml +++ b/pipelines/full-test-windows.yml @@ -30,6 +30,7 @@ jobs: python -m pip install torch==1.6.0 torchvision==0.7.0 -f https://download.pytorch.org/whl/torch_stable.html python -m pip install 'pytorch-lightning>=1.1.1' python -m pip install tensorflow==2.3.1 tensorflow-estimator==2.3.0 + python -m pip install pybnn python -m pip install tianshou>=0.4.1 gym displayName: Install extra dependencies diff --git a/setup.py b/setup.py index f43b15fde0..5a09186034 100644 --- a/setup.py +++ b/setup.py @@ -88,7 +88,8 @@ def _setup(): extras_require = { 'SMAC': _read_requirements_txt('dependencies/required_extra.txt', 'SMAC'), 'BOHB': _read_requirements_txt('dependencies/required_extra.txt', 'BOHB'), - 'PPOTuner': _read_requirements_txt('dependencies/required_extra.txt', 'PPOTuner') + 'PPOTuner': _read_requirements_txt('dependencies/required_extra.txt', 'PPOTuner'), + 'DNGO': _read_requirements_txt('dependencies/required_extra.txt', 'DNGO'), }, setup_requires = ['requests'], diff --git a/test/ut/sdk/test_builtin_tuners.py b/test/ut/sdk/test_builtin_tuners.py index 59739dad16..56c318eb21 100644 --- a/test/ut/sdk/test_builtin_tuners.py +++ b/test/ut/sdk/test_builtin_tuners.py @@ -12,6 +12,7 @@ from unittest import TestCase, main from nni.algorithms.hpo.batch_tuner import BatchTuner +from nni.algorithms.hpo.dngo_tuner import DngoTuner from nni.algorithms.hpo.evolution_tuner import EvolutionTuner from nni.algorithms.hpo.gp_tuner import GPTuner from nni.algorithms.hpo.gridsearch_tuner import GridSearchTuner @@ -388,6 +389,12 @@ def test_pbt(self): )) self.import_data_test_for_pbt() + def test_dngo(self): + tuner_fn = lambda: DngoTuner(trials_per_update=100, num_epochs_per_training=1) + self.search_space_test_all(tuner_fn, fail_types=["choice_str", "choice_mixed", + "normal", "lognormal", "qnormal", "qlognormal"]) + self.import_data_test(tuner_fn, stype='choice_num') + def tearDown(self): file_list = glob.glob("smac3*") + ["param_config_space.pcs", "scenario.txt", "model_path"] for file in file_list: