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)