Source code for dival.hyper_param_search

# -*- coding: utf-8 -*-
"""
Optimization of hyper parameters.

Both grid search and random search using the ``hyperopt`` library are
supported.

The hyper parameter specification of a reconstructor class, optionally
including default options for optimization, are specified in the class
attribute :attr:`~dival.Reconstructor.HYPER_PARAMS`.
"""
from itertools import product
from warnings import warn
import numpy as np
from hyperopt import hp, fmin, tpe, Trials, space_eval
from tqdm import tqdm
from dival.util.std_out_err_redirect_tqdm import std_out_err_redirect_tqdm
from dival.measure import Measure
from dival import LearnedReconstructor


[docs]def optimize_hyper_params(reconstructor, validation_data, measure, dataset=None, HYPER_PARAMS_override=None, hyperopt_max_evals=1000, hyperopt_max_evals_retrain=1000, hyperopt_rstate=None, show_progressbar=True, tqdm_file=None): """Optimize hyper parameters of a reconstructor. Parameters ---------- reconstructor : :class:`.Reconstructor` The reconstructor. validation_data : :class:`.DataPairs` The test data on which the performance is measured. measure : :class:`.Measure` or str The measure to use as the objective. The sign is chosen automatically depending on the measures :attr:`~Measure.measure_type`. dataset : :class:`.Dataset`, optional The dataset used for training `reconstructor` if it is a :class:`LearnedReconstructor`. HYPER_PARAMS_override : dict, optional Hyper parameter specification overriding the defaults in ``type(reconstructor).HYPER_PARAMS``. The structure of this dict is the same as the structure of :attr:`Reconstructor.HYPER_PARAMS`, except that all fields are optional. Here, each value of a dict for one parameter is treated as an entity, i.e. specifying the dict ``HYPER_PARAMS[...]['grid_search_options']`` overrides the whole dict, not only the specified keys in it. hyperopt_max_evals : int, optional Number of evaluations for different combinations of the parameters that are optimized by ``hyperopt`` and that do not require retraining. Should be chosen depending on the complexity of dependence and the number of such parameters. hyperopt_max_evals_retrain : int, optional Number of evaluations for different combinations of the parameters that are optimized by ``hyperopt`` and that require retraining. Should be chosen depending on the complexity of dependence and the number of such parameters. hyperopt_rstate : :class:`np.random.RandomState`, optional Random state for the random searches performed by ``hyperopt``. show_progressbar : bool, optional Whether to show a progress bar for the optimization. Default: ``True``. tqdm_file : file-like object File/stream to pass to ``tqdm``. """ if isinstance(measure, str): measure = Measure.get_by_short_name(measure) if dataset is None and isinstance(reconstructor, LearnedReconstructor): raise ValueError('dataset required for training of ' '`LearnedReconstructor`') if HYPER_PARAMS_override is None: HYPER_PARAMS_override = {} for k in HYPER_PARAMS_override.keys(): if k not in type(reconstructor).HYPER_PARAMS.keys(): warn("unknown hyper param '{}' for reconstructor of type '{}'" .format(k, type(reconstructor))) params = {} params_retrain = {} for k, v in type(reconstructor).HYPER_PARAMS.items(): param = v.copy() param.update(HYPER_PARAMS_override.get(k, {})) param.setdefault('method', 'grid_search') retrain = v.get('retrain', False) if retrain: params_retrain[k] = param else: params[k] = param loss_sign = 1 if measure.measure_type == 'distance' else -1 def fn(x): reconstructor.hyper_params.update(x) reconstructions = [reconstructor.reconstruct(observation) for observation in validation_data.observations] measure_values = [measure.apply(r, g) for r, g in zip(reconstructions, validation_data.ground_truth)] loss = loss_sign * np.mean(measure_values) return {'status': 'ok', 'loss': loss} def fn_retrain(x): reconstructor.hyper_params.update(x) reconstructor.train(dataset) best_sub_hp = _optimize_hyper_params_impl( reconstructor, fn, params, hyperopt_max_evals=hyperopt_max_evals, hyperopt_rstate=hyperopt_rstate, show_progressbar=False) reconstructions = [reconstructor.reconstruct(observation) for observation in validation_data.observations] measure_values = [measure.apply(r, g) for r, g in zip(reconstructions, validation_data.ground_truth)] loss = loss_sign * np.mean(measure_values) return {'status': 'ok', 'loss': loss, 'best_sub_hp': best_sub_hp} if params_retrain: best_hyper_params = _optimize_hyper_params_impl( reconstructor, fn_retrain, params_retrain, hyperopt_max_evals=hyperopt_max_evals_retrain, hyperopt_rstate=hyperopt_rstate, show_progressbar=show_progressbar, tqdm_file=tqdm_file) else: best_hyper_params = _optimize_hyper_params_impl( reconstructor, fn, params, hyperopt_max_evals=hyperopt_max_evals, hyperopt_rstate=hyperopt_rstate, show_progressbar=show_progressbar, tqdm_file=tqdm_file) return best_hyper_params
def _optimize_hyper_params_impl(reconstructor, fn, params, hyperopt_max_evals=1000, hyperopt_rstate=None, show_progressbar=True, tqdm_file=None): grid_search_params = [] grid_search_param_choices = [] hyperopt_space = {} for k, param in params.items(): method = param['method'] if method == 'grid_search': grid_search_options = param.get('grid_search_options', {}) choices = param.get('choices') if choices is None: range_ = param.get('range') if range_ is not None: grid_type = grid_search_options.get('type', 'linear') if grid_type == 'linear': n = grid_search_options.get('num_samples', 10) choices = np.linspace(range_[0], range_[1], n) elif grid_type == 'logarithmic': n = grid_search_options.get('num_samples', 10) b = grid_search_options.get('log_base', 10.) choices = np.logspace(range_[0], range_[1], n, base=b) else: raise ValueError( "unknown grid type '{grid_type}' in {reco_cls}." "HYPER_PARAMS['{k}']['grid_search_options']". format( grid_type=grid_type, reco_cls=reconstructor.__class__.__name__, k=k)) else: raise ValueError( "neither 'choices' nor 'range' is specified in " "{reco_cls}.HYPER_PARAMS['{k}'], one of them must be " "specified for grid search".format( reco_cls=reconstructor.__class__.__name__, k=k)) grid_search_params.append(k) grid_search_param_choices.append(choices) elif method == 'hyperopt': hyperopt_options = param.get('hyperopt_options', {}) space = hyperopt_options.get('space') if space is None: choices = param.get('choices') if choices is None: range_ = param.get('range') if range_ is not None: space_type = hyperopt_options.get('type', 'uniform') if space_type == 'uniform': space = hp.uniform(k, range_[0], range_[1]) else: raise ValueError( "unknown hyperopt space type '{space_type}' " "in {reco_cls}.HYPER_PARAMS['{k}']" "['hyperopt_options']".format( space_type=space_type, reco_cls=reconstructor.__class__.__name__, k=k)) else: raise ValueError( "neither 'choices' nor 'range' is specified in " "{reco_cls}.HYPER_PARAMS['{k}']" "['hyperopt_options']. One of these or " "{reco_cls}.HYPER_PARAMS['{k}']" "['hyperopt_options']['space'] must be specified " "for hyperopt param search".format( reco_cls=reconstructor.__class__.__name__, k=k)) else: space = hp.choice(k, choices) hyperopt_space[k] = space else: raise ValueError("unknown method '{method}' for " "{reco_cls}.HYPER_PARAMS['{k}']".format( method=method, reco_cls=reconstructor.__class__.__name__, k=k)) best_loss = np.inf best_hyper_params = None with std_out_err_redirect_tqdm(tqdm_file) as orig_stdout: grid_search_total = np.prod([len(c) for c in grid_search_param_choices]) for grid_search_values in tqdm( product(*grid_search_param_choices), desc='hyper param opt. for {reco_cls}'.format( reco_cls=type(reconstructor).__name__), total=grid_search_total, file=orig_stdout, leave=False, disable=not show_progressbar): grid_search_param_dict = dict(zip(grid_search_params, grid_search_values)) reconstructor.hyper_params.update(grid_search_param_dict) if len(hyperopt_space) == 0: result = fn({}) if result['loss'] < best_loss: best_loss = result['loss'] best_hyper_params = result.get('best_sub_hp', {}) best_hyper_params.update(grid_search_param_dict) else: trials = Trials() argmin = fmin(fn=fn, space=hyperopt_space, algo=tpe.suggest, max_evals=hyperopt_max_evals, trials=trials, rstate=hyperopt_rstate, show_progressbar=False) best_trial = trials.best_trial if best_trial['result']['loss'] < best_loss: best_loss = best_trial['result']['loss'] best_hyper_params = best_trial['result'].get('best_sub_hp', {}) best_hyper_params.update(grid_search_param_dict) best_hyper_params.update(space_eval(hyperopt_space, argmin)) if best_hyper_params is not None: reconstructor.hyper_params.update(best_hyper_params) return best_hyper_params