def setUp(self, list_mock): """Instantiate the ``Tunable`` and it's ``Hyperparameters`` that we will be using.""" self.bhp = MagicMock() self.chp = MagicMock() self.ihp = MagicMock() list_mock.return_value = ['bhp', 'chp', 'ihp'] hyperparams = { 'bhp': self.bhp, 'chp': self.chp, 'ihp': self.ihp, } self.instance = Tunable(hyperparams)
def test_get_defaults(self): # setup bhp = MagicMock(default=True) chp = MagicMock(default='test') ihp = MagicMock(default=1) hyperparams = { 'bhp': bhp, 'chp': chp, 'ihp': ihp, } self.instance = Tunable(hyperparams) # run result = self.instance.get_defaults() # assert assert result == {'bhp': True, 'chp': 'test', 'ihp': 1}
def test_from_dict(self, mock_bool, mock_cat, mock_float, mock_int): # setup mock_bool.return_value.dimensions = 1 mock_cat.return_value .dimensions = 1 mock_float.return_value.dimensions = 1 mock_int.return_value.dimensions = 1 mock_bool.return_value.cardinality = 1 mock_cat.return_value .cardinality = 1 mock_float.return_value.cardinality = 1 mock_int.return_value.cardinality = 1 # run hyperparameters = { 'bhp': { 'type': 'bool', 'default': False }, 'chp': { 'type': 'str', 'default': 'cat', 'range': ['a', 'b', 'cat'] }, 'fhp': { 'type': 'float', 'default': None, 'range': [0.1, 1.0] }, 'ihp': { 'type': 'int', 'default': 5, 'range': [1, 10] } } result = Tunable.from_dict(hyperparameters) # assert mock_bool.assert_called_once_with(default=False) mock_cat.assert_called_once_with(choices=['a', 'b', 'cat'], default='cat') mock_float.assert_called_once_with(min=0.1, max=1.0, default=None) mock_int.assert_called_once_with(min=1, max=10, default=5) expected_tunable_hp = { 'bhp': mock_bool.return_value, 'chp': mock_cat.return_value, 'fhp': mock_float.return_value, 'ihp': mock_int.return_value } assert result.hyperparams == expected_tunable_hp assert result.dimensions == 4 assert result.cardinality == 1
def __getitem__(self, key): value = super().__getitem__(key) if value is not None: return value path = os.path.join(self._templates_dir, key) if not path.endswith('.json'): path += '.json' template, tunable_hp = load_pipeline(path) self[key] = template return Tunable(tunable_hp)
def _tuning_function(tuner_class, scoring_function, tunable_hyperparameters, iterations): tunable = Tunable.from_dict(tunable_hyperparameters) tuner = tuner_class(tunable) best_score = -np.inf for _ in range(iterations): proposal = tuner.propose() score = scoring_function(**proposal) tuner.record(proposal, score) best_score = max(score, best_score) return best_score
def propose(self): """Propose a new configuration to score. Every time ``propose`` is called, a new tunable will be selected and a new hyperparameter proposal will be generated for it. At the begining, the default hyperparameters of each one of the tunables will be returned sequencially in the same order as they were passed to the ``BTBSession``. After that, once each tunable has been scored at least once, the tunable used to generate the new proposals will be selected optimally each time by the selector. If a tunable runs out of proposals, it will be discarded from the list and will not be proposed again. Finally, when all the tunables have ran out of proposals, a ``StopTuning`` exception will be raised. Returns: tuple (str, dict): * Name of the tunable to try next. * Hyperparameters proposal. Raises: StopTuning: If the ``BTBSession`` has run out of proposals to generate. """ if not self._tunables: raise StopTuning('There are no tunables left to try.') if len(self._tuners) < len(self._tunable_names): tunable_name = self._tunable_names[len(self._tuners)] tunable = self._tunables[tunable_name] if isinstance(tunable, dict): LOGGER.info('Creating Tunable instance from dict.') tunable = Tunable.from_dict(tunable) if not isinstance(tunable, Tunable): raise TypeError( 'Tunable can only be an instance of btb.tuning.Tunable or dict' ) LOGGER.info('Obtaining default configuration for %s', tunable_name) config = tunable.get_defaults() if tunable.cardinality == 1: LOGGER.warn( 'Skipping tuner creation for Tunable %s with cardinality 1', tunable_name) tuner = None else: tuner = self._tuner_class(tunable) self._tuners[tunable_name] = tuner else: tunable_name = self._get_next_tunable_name() tuner = self._tuners[tunable_name] try: if tuner is None: raise StopTuning( 'Tunable %s has no tunable hyperparameters', tunable_name) LOGGER.info('Generating new proposal configuration for %s', tunable_name) config = tuner.propose(1) except StopTuning: LOGGER.info('%s has no more configs to propose.', tunable_name) self._remove_tunable(tunable_name) tunable_name, config = self.propose() proposal_id = self._make_id(tunable_name, config) self.proposals[proposal_id] = { 'id': proposal_id, 'name': tunable_name, 'config': config } return tunable_name, config
def tuning_function(scoring_function, tunable_hyperparameters, iterations): tunable = Tunable.from_dict(tunable_hyperparameters) tuner = tuner_class(tunable, **tuner_kwargs) return tune(tuner, scoring_function, iterations)
class TestTunable(TestCase): """Unit test for the class ``Tunable``.""" @patch('btb.tuning.tunable.list') def setUp(self, list_mock): """Instantiate the ``Tunable`` and it's ``Hyperparameters`` that we will be using.""" self.bhp = MagicMock() self.chp = MagicMock() self.ihp = MagicMock() list_mock.return_value = ['bhp', 'chp', 'ihp'] hyperparams = { 'bhp': self.bhp, 'chp': self.chp, 'ihp': self.ihp, } self.instance = Tunable(hyperparams) def test___init__with_given_names(self): """Test that the names are being generated correctly.""" # assert assert self.instance.names == ['bhp', 'chp', 'ihp'] def test_transform_valid_dict(self): """Test transform method with a dictionary that has all the hyperparameters.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values_dict = { 'bhp': True, 'chp': 'cat', 'ihp': 1 } # run result = self.instance.transform(values_dict) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('cat') self.ihp.transform.assert_called_once_with(1) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_empty_dict(self): """Test transform method with a dictionary that has a missing hyperparameters.""" # run / assert with self.assertRaises(KeyError): self.instance.transform({}) def test_transform_invalid_dict_one_missing(self): """Test transform method with a dictionary that has a missing hyperparameters.""" # run / assert values = { 'bhp': True, 'chp': 'cat' } with self.assertRaises(KeyError): self.instance.transform(values) def test_transform_list_of_dicts(self): """Test transform method with a list of dictionaries.""" # setup self.bhp.transform.return_value = [[1], [0]] self.chp.transform.return_value = [[0], [1]] self.ihp.transform.return_value = [[1], [1]] values_list_dict = [ {'bhp': True, 'chp': 'cat', 'ihp': 2}, {'bhp': False, 'chp': 'cat', 'ihp': 3} ] # run results = self.instance.transform(values_list_dict) # assert assert_called_with_np_array(self.bhp.transform.call_args_list, [call([True, False])]) assert_called_with_np_array(self.chp.transform.call_args_list, [call(['cat', 'cat'])]) assert_called_with_np_array(self.ihp.transform.call_args_list, [call([2, 3])]) np.testing.assert_array_equal(results, np.array([[1, 0, 1], [0, 1, 1]])) def test_transform_list_of_invalid_dicts(self): """Test transform method with a list of dictionaries where one of them does not have the categorical value.""" # setup self.bhp.transform.return_value = [[1], [0]] # Here we create a CHP so we can raise an value error as there will be a NaN inside the # pandas.DataFrame. self.chp = CategoricalHyperParam(['cat', 'dog']) self.ihp.transform.return_value = [[1], [1]] values_list_dict = [ {'bhp': True, 'ihp': 2}, {'bhp': False, 'chp': 'cat', 'ihp': 3} ] # run / assert with self.assertRaises(ValueError): self.instance.transform(values_list_dict) def test_transform_empty_list(self): """Test transform method with an empty list.""" # run / assert with self.assertRaises(IndexError): self.instance.transform(list()) def test_transform_valid_pandas_series(self): """Test transform method over a valid ``pandas.Series`` object.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = pd.Series([False, 'cat', 1], index=['bhp', 'chp', 'ihp']) # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(False) self.chp.transform.assert_called_once_with('cat') self.ihp.transform.assert_called_once_with(1) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_invalid_pandas_series(self): """Test transform method over a ``pandas.Series`` object that does not have index.""" # setup values = pd.Series([False, 'cat', 1]) # run with self.assertRaises(KeyError): self.instance.transform(values) def test_transform_array_like_list(self): """Test transform a valid array like list.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = [[True, 'dog', 2], [False, 'cat', 3]] # run result = self.instance.transform(values) # assert assert_called_with_np_array( self.bhp.transform.call_args_list, [call(np.array([True, False]))] ) assert_called_with_np_array( self.chp.transform.call_args_list, [call(np.array(['dog', 'cat']))] ) assert_called_with_np_array( self.ihp.transform.call_args_list, [call(np.array([2, 3]))] ) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_simple_list(self): """Test that the method transform performs a transformation over a list with a single combination of hyperparameter valid values. """ # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = [True, 'dog', 2] # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('dog') self.ihp.transform.assert_called_once_with(2) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_pd_df(self): """Test that the method transform performs a transformation over a ``pandas.DataFrame`` with a single combination of hyperparameter valid values. """ # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = pd.DataFrame([[True, 'dog', 2]], columns=['bhp', 'chp', 'ihp']) # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('dog') self.ihp.transform.assert_called_once_with(2) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_simple_invalid_list(self): """Test that the method transform does not transform a list with a single combination of invalid hyperparameter values. """ # run / assert with self.assertRaises(TypeError): self.instance.transform([[True], 1, 2]) def test_inverse_transform_valid_data(self): """Test that the inverse transform method is calling the hyperparameters.""" # setup self.bhp.K = 1 self.chp.K = 1 self.ihp.K = 1 self.bhp.inverse_transform.return_value = [[True]] self.chp.inverse_transform.return_value = [['cat']] self.ihp.inverse_transform.return_value = [[1]] values = [[1, 0, 1]] # run result = self.instance.inverse_transform(values) # assert expected_result = pd.DataFrame( { 'bhp': [True], 'chp': ['cat'], 'ihp': [1] }, dtype=object ) self.bhp.inverse_transform.assert_called_once_with([1]) self.chp.inverse_transform.assert_called_once_with([0]) self.ihp.inverse_transform.assert_called_once_with([1]) pd.testing.assert_frame_equal(result, expected_result) def test_inverse_transform_invalid_data(self): """Test that the a ``TypeError`` is being raised when calling with the invalid data.""" # setup values = [1, 0, 1] # run with self.assertRaises(TypeError): self.instance.inverse_transform(values) def test_sample(self): """Test that the method sample generates data from all the ``hyperparams``.""" # setup # Values have been changed to ensure that each one of them is being called. self.bhp.sample.return_value = [['a']] self.chp.sample.return_value = [['b']] self.ihp.sample.return_value = [['c']] # run result = self.instance.sample(1) # assert expected_result = np.array([['a', 'b', 'c']]) assert set(result.flat) == set(expected_result.flat) self.bhp.sample.assert_called_once_with(1) self.chp.sample.assert_called_once_with(1) self.ihp.sample.assert_called_once_with(1)
def propose(self): """Propose a new configuration for a tunable. ``BTBSession``, ensures that every tunable has been scored atleast once. The following proposals use the ``self.selector`` in order to select the ``tunable`` from which a proposal is generated. If the ``tuner`` can not propose more configurations it will return ``None`` and will remove the ``tunable`` from the list. Returns: tuple (str, dict): Returns a tuple with the name of the tunable and the proposal as a dictionary. None: ``None`` is being returned When the ``tunable`` has no more combinations to be evaluated. Raises: ValueError: A ``ValueErorr`` is being raised if ``self.tunables`` is empty. """ if not self.tunables: raise ValueError('All the tunables failed.') if len(self._normalized_scores) < len(self._tunable_names): tunable_name = self._tunable_names[len(self._normalized_scores)] tunable = self.tunables[tunable_name] if isinstance(tunable, dict): LOGGER.info('Creating Tunable instance from dict.') tunable = Tunable.from_dict(tunable) if not isinstance(tunable, Tunable): raise TypeError( 'Tunable can only be an instance of btb.tuning.Tunable or dict' ) LOGGER.info('Obtaining default configuration for %s', tunable_name) config = tunable.get_defaults() self._tuners[tunable_name] = self.tuner(tunable) else: tunable_name = self.selector.select(self._normalized_scores) tuner = self._tuners[tunable_name] try: LOGGER.info('Generating new proposal configuration for %s', tunable_name) config = tuner.propose(1) except StopTuning: LOGGER.info('%s has no more configs to propose.' % tunable_name) self._normalized_scores.pop(tunable_name, None) self._tunable_names.remove(tunable_name) tunable_name, config = self.propose() proposal_id = self._make_id(tunable_name, config) self.proposals[proposal_id] = { 'id': proposal_id, 'name': tunable_name, 'config': config } return tunable_name, config
def test_from_dict_not_a_dict(self): # run with self.assertRaises(TypeError): Tunable.from_dict(1)
class TestTunable(TestCase): """Unit test for the class ``Tunable``.""" @patch('btb.tuning.tunable.list') def setUp(self, list_mock): """Instantiate the ``Tunable`` and it's ``Hyperparameters`` that we will be using.""" self.bhp = MagicMock(spec_set=BooleanHyperParam) self.chp = MagicMock(spec_set=CategoricalHyperParam) self.ihp = MagicMock(spec_set=IntHyperParam) list_mock.return_value = ['bhp', 'chp', 'ihp'] hyperparams = { 'bhp': self.bhp, 'chp': self.chp, 'ihp': self.ihp, } self.instance = Tunable(hyperparams) def test___init__with_given_names(self): """Test that the names are being generated correctly.""" # assert assert self.instance.names == ['bhp', 'chp', 'ihp'] def test_transform_valid_dict(self): """Test transform method with a dictionary that has all the hyperparameters.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values_dict = { 'bhp': True, 'chp': 'cat', 'ihp': 1 } # run result = self.instance.transform(values_dict) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('cat') self.ihp.transform.assert_called_once_with(1) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_empty_dict(self): """Test transform method with a dictionary that has a missing hyperparameters.""" # run / assert with self.assertRaises(KeyError): self.instance.transform({}) def test_transform_invalid_dict_one_missing(self): """Test transform method with a dictionary that has a missing hyperparameters.""" # run / assert values = { 'bhp': True, 'chp': 'cat' } with self.assertRaises(KeyError): self.instance.transform(values) def test_transform_list_of_dicts(self): """Test transform method with a list of dictionaries.""" # setup self.bhp.transform.return_value = [[1], [0]] self.chp.transform.return_value = [[0], [1]] self.ihp.transform.return_value = [[1], [1]] values_list_dict = [ {'bhp': True, 'chp': 'cat', 'ihp': 2}, {'bhp': False, 'chp': 'cat', 'ihp': 3} ] # run results = self.instance.transform(values_list_dict) # assert assert_called_with_np_array(self.bhp.transform.call_args_list, [call([True, False])]) assert_called_with_np_array(self.chp.transform.call_args_list, [call(['cat', 'cat'])]) assert_called_with_np_array(self.ihp.transform.call_args_list, [call([2, 3])]) np.testing.assert_array_equal(results, np.array([[1, 0, 1], [0, 1, 1]])) def test_transform_list_of_invalid_dicts(self): """Test transform method with a list of dictionaries where one of them does not have the categorical value.""" # setup self.bhp.transform.return_value = [[1], [0]] # Here we create a CHP so we can raise an value error as there will be a NaN inside the # pandas.DataFrame. self.chp = CategoricalHyperParam(['cat', 'dog']) self.ihp.transform.return_value = [[1], [1]] values_list_dict = [ {'bhp': True, 'ihp': 2}, {'bhp': False, 'chp': 'cat', 'ihp': 3} ] # run / assert with self.assertRaises(ValueError): self.instance.transform(values_list_dict) def test_transform_empty_list(self): """Test transform method with an empty list.""" # run / assert with self.assertRaises(IndexError): self.instance.transform(list()) def test_transform_valid_pandas_series(self): """Test transform method over a valid ``pandas.Series`` object.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = pd.Series([False, 'cat', 1], index=['bhp', 'chp', 'ihp']) # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(False) self.chp.transform.assert_called_once_with('cat') self.ihp.transform.assert_called_once_with(1) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_invalid_pandas_series(self): """Test transform method over a ``pandas.Series`` object that does not have index.""" # setup values = pd.Series([False, 'cat', 1]) # run with self.assertRaises(KeyError): self.instance.transform(values) def test_transform_array_like_list(self): """Test transform a valid array like list.""" # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = [[True, 'dog', 2], [False, 'cat', 3]] # run result = self.instance.transform(values) # assert assert_called_with_np_array( self.bhp.transform.call_args_list, [call(np.array([True, False]))] ) assert_called_with_np_array( self.chp.transform.call_args_list, [call(np.array(['dog', 'cat']))] ) assert_called_with_np_array( self.ihp.transform.call_args_list, [call(np.array([2, 3]))] ) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_simple_list(self): """Test that the method transform performs a transformation over a list with a single combination of hyperparameter valid values. """ # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = [True, 'dog', 2] # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('dog') self.ihp.transform.assert_called_once_with(2) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_pd_df(self): """Test that the method transform performs a transformation over a ``pandas.DataFrame`` with a single combination of hyperparameter valid values. """ # setup self.bhp.transform.return_value = [[1]] self.chp.transform.return_value = [[0]] self.ihp.transform.return_value = [[1]] values = pd.DataFrame([[True, 'dog', 2]], columns=['bhp', 'chp', 'ihp']) # run result = self.instance.transform(values) # assert self.bhp.transform.assert_called_once_with(True) self.chp.transform.assert_called_once_with('dog') self.ihp.transform.assert_called_once_with(2) np.testing.assert_array_equal(result, np.array([[1, 0, 1]])) def test_transform_simple_invalid_list(self): """Test that the method transform does not transform a list with a single combination of invalid hyperparameter values. """ # run / assert with self.assertRaises(TypeError): self.instance.transform([[True], 1, 2]) def test_inverse_transform_valid_data(self): """Test that the inverse transform method is calling the hyperparameters.""" # setup self.bhp.inverse_transform.return_value = [[True]] self.chp.inverse_transform.return_value = [['cat']] self.ihp.inverse_transform.return_value = [[1]] values = [[1, 0, 1]] # run result = self.instance.inverse_transform(values) # assert expected_result = pd.DataFrame( { 'bhp': [True], 'chp': ['cat'], 'ihp': [1] }, dtype=object ) self.bhp.inverse_transform.assert_called_once_with([1]) self.chp.inverse_transform.assert_called_once_with([0]) self.ihp.inverse_transform.assert_called_once_with([1]) pd.testing.assert_frame_equal(result, expected_result) def test_inverse_transform_invalid_data(self): """Test that the a ``TypeError`` is being raised when calling with the invalid data.""" # setup values = [1, 0, 1] # run with self.assertRaises(TypeError): self.instance.inverse_transform(values) def test_sample(self): """Test that the method sample generates data from all the ``hyperparams``.""" # setup # Values have been changed to ensure that each one of them is being called. self.bhp.sample.return_value = [['a']] self.chp.sample.return_value = [['b']] self.ihp.sample.return_value = [['c']] # run result = self.instance.sample(1) # assert expected_result = np.array([['a', 'b', 'c']]) assert set(result.flat) == set(expected_result.flat) self.bhp.sample.assert_called_once_with(1) self.chp.sample.assert_called_once_with(1) self.ihp.sample.assert_called_once_with(1) def test_get_defaults(self): # setup bhp = MagicMock(default=True) chp = MagicMock(default='test') ihp = MagicMock(default=1) hyperparams = { 'bhp': bhp, 'chp': chp, 'ihp': ihp, } self.instance = Tunable(hyperparams) # run result = self.instance.get_defaults() # assert assert result == {'bhp': True, 'chp': 'test', 'ihp': 1} def test_from_dict_not_a_dict(self): # run with self.assertRaises(TypeError): Tunable.from_dict(1) @patch('btb.tuning.tunable.IntHyperParam') @patch('btb.tuning.tunable.FloatHyperParam') @patch('btb.tuning.tunable.CategoricalHyperParam') @patch('btb.tuning.tunable.BooleanHyperParam') def test_from_dict(self, mock_bool, mock_cat, mock_float, mock_int): # setup mock_bool.return_value.dimensions = 1 mock_cat.return_value .dimensions = 1 mock_float.return_value.dimensions = 1 mock_int.return_value.dimensions = 1 mock_bool.return_value.cardinality = 1 mock_cat.return_value .cardinality = 1 mock_float.return_value.cardinality = 1 mock_int.return_value.cardinality = 1 # run hyperparameters = { 'bhp': { 'type': 'bool', 'default': False }, 'chp': { 'type': 'str', 'default': 'cat', 'range': ['a', 'b', 'cat'] }, 'fhp': { 'type': 'float', 'default': None, 'range': [0.1, 1.0] }, 'ihp': { 'type': 'int', 'default': 5, 'range': [1, 10] } } result = Tunable.from_dict(hyperparameters) # assert mock_bool.assert_called_once_with(default=False) mock_cat.assert_called_once_with(choices=['a', 'b', 'cat'], default='cat') mock_float.assert_called_once_with(min=0.1, max=1.0, default=None) mock_int.assert_called_once_with(min=1, max=10, default=5) expected_tunable_hp = { 'bhp': mock_bool.return_value, 'chp': mock_cat.return_value, 'fhp': mock_float.return_value, 'ihp': mock_int.return_value } assert result.hyperparams == expected_tunable_hp assert result.dimensions == 4 assert result.cardinality == 1