Source code for btb.tuning.tunable

# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd

from btb.tuning.hyperparams.boolean import BooleanHyperParam
from btb.tuning.hyperparams.categorical import CategoricalHyperParam
from btb.tuning.hyperparams.numerical import FloatHyperParam, IntHyperParam

"""Package where the Tunable class is defined."""


[docs]class Tunable: """Tunable class. The Tunable class represents a collection of ``HyperParams`` that need to be tuned as a whole, at once. Attributes: hyperparams: Dict of HyperParams. cardinality: Int or ``np.inf`` amount that indicates the number of combinations possible for this tunable. Args: hyperparams (dict): Dictionary object that contains the name and the hyperparameter asociated to it. """ hyperparams = None names = None dimensions = 0 cardinality = 1 def __init__(self, hyperparams): self.hyperparams = hyperparams self.names = list(hyperparams) for hyperparam in hyperparams.values(): self.dimensions = self.dimensions + hyperparam.dimensions self.cardinality = self.cardinality * hyperparam.cardinality
[docs] def transform(self, values): """Transform one or more hyperparameter configurations. Transform one or more hyperparameter configurations from the original hyperparameter space to the normalized search space. Args: values (pandas.DataFrame, pandas.Series, dict, list(dict), 2D array-like): Values of shape ``(n, len(self.hyperparams))``. Returns: numpy.ndarray: 2D array of shape ``(len(values), dimensions)`` where ``dimensions`` is the sum of dimensions from all the ``HyperParams`` that compose this ``tunable``. Example: The example below shows a simple usage of a Tunable class which will transform a valid data from a 2D list and a ``numpy.ndarray`` is being returned. >>> from btb.tuning.hyperparams.boolean import BooleanHyperParam >>> from btb.tuning.hyperparams.categorical import CategoricalHyperParam >>> from btb.tuning.hyperparams.numerical import IntHyperParam >>> chp = CategoricalHyperParam(['cat', 'dog']) >>> bhp = BooleanHyperParam() >>> ihp = IntHyperParam(1, 10) >>> hyperparams = { ... 'chp': chp, ... 'bhp': bhp, ... 'ihp': ihp ... } >>> tunable = Tunable(hyperparams) >>> values = [ ... ['cat', False, 10], ... ['dog', True, 1], ... ] >>> tunable.transform(values) array([[1. , 0. , 0. , 0.95], [0. , 1. , 1. , 0.05]]) """ if isinstance(values, dict): values = pd.DataFrame([values]) elif isinstance(values, list) and isinstance(values[0], dict): values = pd.DataFrame(values, columns=self.names) elif isinstance(values, list) and not isinstance(values[0], list): values = pd.DataFrame([values], columns=self.names) elif isinstance(values, pd.Series): values = values.to_frame().T elif not isinstance(values, pd.DataFrame): values = pd.DataFrame(values, columns=self.names) transformed = list() for name in self.names: hyperparam = self.hyperparams[name] value = values[name].values transformed.append(hyperparam.transform(value)) return np.concatenate(transformed, axis=1)
[docs] def inverse_transform(self, values): """Invert one or more hyperparameter configurations. Invert one or more hyperparameter configurations from the normalized search space :math:`[0, 1]^K` to the original hyperparameter space. Args: values (array-like): 2D array of normalized values with shape ``(n, dimensions)`` where ``dimensions`` is the sum of dimensions from all the ``HyperParams`` that compose this ``tunable``. Returns: pandas.DataFrame Example: The example below shows a simple usage of a Tunable class which will inverse transform a valid data from a 2D list and a ``pandas.DataFrame`` will be returned. >>> from btb.tuning.hyperparams.boolean import BooleanHyperParam >>> from btb.tuning.hyperparams.categorical import CategoricalHyperParam >>> from btb.tuning.hyperparams.numerical import IntHyperParam >>> chp = CategoricalHyperParam(['cat', 'dog']) >>> bhp = BooleanHyperParam() >>> ihp = IntHyperParam(1, 10) >>> hyperparams = { ... 'chp': chp, ... 'bhp': bhp, ... 'ihp': ihp ... } >>> tunable = Tunable(hyperparams) >>> values = [ ... [1, 0, 0, 0.95], ... [0, 1, 1, 0.05] ... ] >>> tunable.inverse_transform(values) chp bhp ihp 0 cat False 10 1 dog True 1 """ inverse_transform = list() for value in values: transformed = list() for name in self.names: hyperparam = self.hyperparams[name] item = value[:hyperparam.dimensions] transformed.append(hyperparam.inverse_transform(item)) value = value[hyperparam.dimensions:] transformed = np.array(transformed, dtype=object) inverse_transform.append(np.concatenate(transformed, axis=1)) return pd.DataFrame(np.concatenate(inverse_transform), columns=self.names)
[docs] def sample(self, n_samples): """Sample values in the hyperparameters space for this tunable. Args: n_samlpes (int): Number of values to sample. Returns: numpy.ndarray: 2D array with shape of ``(n_samples, dimensions)`` where ``dimensions`` is the sum of dimensions from all the ``HyperParams`` that compose this ``tunable``. Example: The example below shows a simple usage of a Tunable class which will generate 2 samples by calling it's sample method. This will return a ``numpy.ndarray``. >>> from btb.tuning.hyperparams.boolean import BooleanHyperParam >>> from btb.tuning.hyperparams.categorical import CategoricalHyperParam >>> from btb.tuning.hyperparams.numerical import IntHyperParam >>> chp = CategoricalHyperParam(['cat', 'dog']) >>> bhp = BooleanHyperParam() >>> ihp = IntHyperParam(1, 10) >>> hyperparams = { ... 'chp': chp, ... 'bhp': bhp, ... 'ihp': ihp ... } >>> tunable = Tunable(hyperparams) >>> tunable.sample(2) array([[0. , 1. , 0. , 0.45], [1. , 0. , 1. , 0.95]]) """ samples = list() for name, hyperparam in self.hyperparams.items(): items = hyperparam.sample(n_samples) samples.append(items) return np.concatenate(samples, axis=1)
[docs] def get_defaults(self): """Return the default combination for the hyperparameters.""" return { name: hyperparam.default for name, hyperparam in self.hyperparams.items() }
[docs] @classmethod def from_dict(cls, dict_hyperparams): """Create an instance from a dictionary containing information over hyperparameters. Class method that creates an instance from a dictionary that describes the type of a hyperparameter, the range or values that this can have and the default value of the hyperparameter. Args: dict_hyperparams (dict): A python dictionary containing as `key` the given name for the hyperparameter and as value a dictionary containing the following keys: - Type (str): ``bool`` for ``BoolHyperParam``, ``int`` for ``IntHyperParam``, ``float`` for ``FloatHyperParam``, ``str`` for ``CategoricalHyperParam``. - Range or Values (list): Range / values that this hyperparameter can take, in case of ``CategoricalHyperParam`` those will be used as the ``choices``, for ``NumericalHyperParams`` the ``min`` value will be used as the minimum value and the ``max`` value will be used as the ``maximum`` value. - Default (str, bool, int, float or None): The default value for the hyperparameter. Returns: Tunable: A ``Tunable`` instance with the given hyperparameters. """ if not isinstance(dict_hyperparams, dict): raise TypeError('Hyperparams must be a dictionary.') hyperparams = {} for name, hyperparam in dict_hyperparams.items(): hp_type = hyperparam['type'] hp_default = hyperparam.get('default') if hp_type == 'int': hp_range = hyperparam.get('range') or hyperparam.get('values') hp_min = min(hp_range) if hp_range else None hp_max = max(hp_range) if hp_range else None hp_instance = IntHyperParam(min=hp_min, max=hp_max, default=hp_default) elif hp_type == 'float': hp_range = hyperparam.get('range') or hyperparam.get('values') hp_min = min(hp_range) hp_max = max(hp_range) hp_instance = FloatHyperParam(min=hp_min, max=hp_max, default=hp_default) elif hp_type == 'bool': hp_instance = BooleanHyperParam(default=hp_default) elif hp_type == 'str': hp_choices = hyperparam.get('range') or hyperparam.get('values') hp_instance = CategoricalHyperParam(choices=hp_choices, default=hp_default) hyperparams[name] = hp_instance return cls(hyperparams)
def __repr__(self): return 'Tunable({})'.format(self.hyperparams)