Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove hyperparameter in a configuration space #332

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ConfigSpace/conditions.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ cdef class ConditionComponent(object):
def evaluate_vector(self, instantiated_vector):
return bool(self._evaluate_vector(instantiated_vector))

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
pass

cdef int _evaluate_vector(self, np.ndarray value):
pass

Expand Down Expand Up @@ -147,6 +150,9 @@ cdef class AbstractCondition(ConditionComponent):
hp_name = self.parent.name
return self._evaluate(instantiated_parent_hyperparameter[hp_name])

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
return self.get_children() + self.get_parents()

cdef int _evaluate_vector(self, np.ndarray instantiated_vector):
if self.parent_vector_id is None:
raise ValueError("Parent vector id should not be None when calling evaluate vector")
Expand Down Expand Up @@ -523,6 +529,9 @@ cdef class AbstractConjunction(ConditionComponent):
raise ValueError("All Conjunctions and Conditions must have "
"the same child.")

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
return sum([cond.get_referenced_hyperparameters() for cond in self.components], [])
herilalaina marked this conversation as resolved.
Show resolved Hide resolved

def __eq__(self, other: Any) -> bool:
"""
This method implements a comparison between self and another
Expand Down
14 changes: 13 additions & 1 deletion ConfigSpace/forbidden.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import numpy as np
import io
from ConfigSpace.hyperparameters import Hyperparameter
from ConfigSpace.hyperparameters.hyperparameter cimport Hyperparameter
from typing import Dict, Any, Union
from typing import Dict, Any, Union, List

from ConfigSpace.forbidden cimport AbstractForbiddenComponent

Expand Down Expand Up @@ -71,6 +71,9 @@ cdef class AbstractForbiddenComponent(object):
return (self.value == other.value and
self.hyperparameter.name == other.hyperparameter.name)

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
pass

def __hash__(self) -> int:
"""Override the default hash behavior (that returns the id or the object)"""
return hash(tuple(sorted(self.__dict__.items())))
Expand Down Expand Up @@ -103,6 +106,9 @@ cdef class AbstractForbiddenClause(AbstractForbiddenComponent):
self.hyperparameter = hyperparameter
self.vector_id = -1

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
return [self.hyperparameter]

cpdef get_descendant_literal_clauses(self):
return (self, )

Expand Down Expand Up @@ -315,6 +321,9 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent):
self.n_components = len(self.components)
self.dlcs = self.get_descendant_literal_clauses()

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
return sum([forb.get_referenced_hyperparameters() for forb in self.components], [])

def __repr__(self):
pass

Expand Down Expand Up @@ -497,6 +506,9 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent):
self.right = right
self.vector_ids = (-1, -1)

def get_referenced_hyperparameters(self) -> List[Hyperparameter]:
return [self.left, self.right]

def __eq__(self, other: Any) -> bool:
if not isinstance(other, self.__class__):
return False
Expand Down
69 changes: 68 additions & 1 deletion ConfigSpace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[
if len(new_active_hp_names) <= 0:
raise RuntimeError(
"Unexpected error: There should have been a newly activated hyperparameter"
f" for the current configuration values: {str(unchecked_grid_pts[0])}. "
f" for the current configuration values: {unchecked_grid_pts[0]!s}. "
"Please contact the developers with the code you ran and the stack trace.",
) from None

Expand All @@ -708,3 +708,70 @@ def get_cartesian_product(value_sets: list[tuple], hp_names: list[str]) -> list[
unchecked_grid_pts.popleft()

return checked_grid_pts


def remove_hyperparameter(name: str, configuration_space: ConfigurationSpace) -> ConfigurationSpace:
"""
Returns a new configuration space with the hyperparameter removed.

Parameters
----------
name: str
Name of the hyperparameter to remove

configuration_space: :class:`~ConfigSpace.configuration_space.ConfigurationSpace`
Configuration space from which to remove the hyperparameter.

Returns
-------
:class:`~ConfigSpace.configuration_space.Configuration`
A new configuration space without the hyperparameter
"""
if name not in configuration_space._hyperparameters:
raise ValueError(f"{name} not in {configuration_space}")

# First, delete children hyperparameters
for child in configuration_space._children[name]: # type: ignore
configuration_space = remove_hyperparameter(
name=child,
configuration_space=configuration_space,
)

hp_to_remove = configuration_space.get_hyperparameter(name)
hps = [
copy.deepcopy(hp) # type: ignore
for hp in configuration_space.get_hyperparameters()
if hp.name != name
]

conditions = [
cond
for cond in configuration_space.get_conditions()
if hp_to_remove not in cond.get_referenced_hyperparameters()
]
forbiddens = [
forbidden
for forbidden in configuration_space.get_forbiddens()
if hp_to_remove not in forbidden.get_referenced_hyperparameters()
]

new_space = ConfigurationSpace(
name=copy.deepcopy(configuration_space.name), # type: ignore
meta=copy.deepcopy(configuration_space.meta), # type: ignore
)
new_space.add_hyperparameters(hps)
new_space.random.set_state(configuration_space.random.get_state())

new_conditions = ConfigurationSpace.substitute_hyperparameters_in_conditions(
conditions=conditions,
new_configspace=new_space,
)
new_forbiddens = ConfigurationSpace.substitute_hyperparameters_in_forbiddens(
forbiddens=forbiddens,
new_configspace=new_space,
)

new_space.add_conditions(new_conditions)
new_space.add_forbidden_clauses(new_forbiddens)

return new_space
50 changes: 50 additions & 0 deletions test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
get_one_exchange_neighbourhood,
get_random_neighbor,
impute_inactive_values,
remove_hyperparameter,
)


Expand Down Expand Up @@ -625,3 +626,52 @@ def test_generate_grid(self):
assert dict(generated_grid[1]) == {"cat1": "F", "ord1": "2"}
assert dict(generated_grid[2]) == {"cat1": "T", "ord1": "1", "int1": 0}
assert dict(generated_grid[-1]) == {"cat1": "T", "ord1": "3", "int1": 1000}

def test_remove_hyperparameter(self):
"""Test removing hyperparameter."""
cs = ConfigurationSpace(seed=1234)

list_hps = ["cat1", "const1", "float1", "int1", "ord1"]
cat1 = CategoricalHyperparameter(name="cat1", choices=["T", "F"])
const1 = Constant(name="const1", value=4)
float1 = UniformFloatHyperparameter(name="float1", lower=-1, upper=1, log=False)
int1 = UniformIntegerHyperparameter(name="int1", lower=10, upper=100, log=True)
ord1 = OrdinalHyperparameter(name="ord1", sequence=["1", "2", "3"])

cs.add_hyperparameters([float1, int1, cat1, ord1, const1])

# test exception if hyperparamter is not in the configuration space
with self.assertRaises(ValueError):
remove_hyperparameter(name="cat", configuration_space=cs)

for hp_to_remove in list_hps:
cs1 = remove_hyperparameter(name=hp_to_remove, configuration_space=cs)

# verify that the hyperparameter is not in the configuration space anymore
assert hp_to_remove not in cs1._hyperparameters

# the other hyperparameters remain in the configuration space
remaining_hps = [hp for hp in list_hps if hp != hp_to_remove]
for hp_name in remaining_hps:
assert hp_name in cs1._hyperparameters

cs = ConfigurationSpace(seed=1234)
cs.add_hyperparameters([float1, int1, cat1, ord1, const1])
cs.add_condition(EqualsCondition(int1, cat1, "T")) # int1 only active if cat1 == T
cs.add_forbidden_clause(
ForbiddenAndConjunction( # Forbid ord1 == 3 if cat1 == F
ForbiddenEqualsClause(cat1, "F"),
ForbiddenEqualsClause(ord1, "3"),
),
)
assert len(cs.get_conditions()) == 1
assert len(cs.get_forbiddens()) == 1

# check that children hyperparameters are also removed
cs1 = remove_hyperparameter(name="cat1", configuration_space=cs)
assert "cat1" not in cs1._hyperparameters
assert "int1" not in cs1._hyperparameters

# check that referenced conditions and clauses are also removed
assert len(cs1.get_conditions()) == 0
assert len(cs1.get_forbiddens()) == 0