Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
6 changes: 5 additions & 1 deletion docs/reference/hyperparameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ What makes a hyperparameter the hyperparameter it is then:
if they share the same underlying vectorized space.


!!! tip Updating Hyperparameter properties
Comment thread
thijssnelleman marked this conversation as resolved.
Outdated

Creating your own hyperparameter class, it is important to note that its properties may be updated after creation, see [`__setattr__`][ConfigSpace.hyperparameters.Hyperparameter.__setattr__]. This method blocks any changes after initialisation to any attribute directly or the creation of new attributes. Attribute values can only be changed through the class constructor parameters (the arguments of `__init__` function), and is only possible when they share **the same name**. Take this into consideration when subclassing the `Hyperparameter` class.

!!! example "CategoricalHyperparameter"

Inside of the `__init__` method of a `CategoricalHyperparameter`, you will find something along the lines
Expand Down Expand Up @@ -497,7 +501,7 @@ class BetaIntegerHyperparamter(IntegerHyperparameter):
try:
scaler = UnitScaler(i64(self.lower), i64(self.upper), log=log, dtype=i64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e


if default_value is None:
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigSpace/hyperparameters/beta_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def __init__(
try:
scaler = UnitScaler(f64(self.lower), f64(self.upper), log=log, dtype=f64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

if (self.alpha > 1) or (self.beta > 1):
normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2)
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigSpace/hyperparameters/beta_integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def __init__(
try:
scaler = UnitScaler(i64(self.lower), i64(self.upper), log=log, dtype=i64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

if default_value is None:
if (self.alpha > 1) or (self.beta > 1):
Expand Down
26 changes: 25 additions & 1 deletion src/ConfigSpace/hyperparameters/hyperparameter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import inspect
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable, Hashable, Mapping, Sequence
Expand Down Expand Up @@ -132,11 +133,34 @@ def __init__(
if not self.legal_value(self.default_value):
raise ValueError(
f"Illegal default value {self.default_value} for"
f" hyperparamter '{self.name}'.",
f" hyperparameter '{self.name}'.",
)

self._normalized_default_value = self.to_vector(self.default_value)

def __setattr__(self, name: str, value: Any):
"""Check if attribute can be set on HP, and reinitialises the class if so."""
# NOTE: Here we first verify if the object is being initialised by checking the call stack if the parent function is called "__init__"
# This is currently the best way to check as checking the caller class etc is too complex
# Alternatively, we could compare the __code__ objects/properties of the caller for self.__init__.__code__ but this would be more computationally expensive
if inspect.stack()[1][3] == '__init__': # Init, normal __setattr__ control flow
super().__setattr__(name, value)
Comment thread
thijssnelleman marked this conversation as resolved.
else: # We are updating an existing attribute
# Extract all editable attributes
init_params: tuple[str] = self.__init__.__code__.co_varnames[:self.__init__.__code__.co_argcount]

if name not in init_params or not hasattr(self, name):
raise ValueError(f"Can only set parameters passed to self.__init__. '{name}' is not one of: {init_params}")

Comment thread
thijssnelleman marked this conversation as resolved.
try:
init_params = {key: self.__dict__[key] for key in init_params if hasattr(self, key)} # This will break if the parameter is not saved under its passed name
except KeyError as e:
raise KeyError(f"You are seeing this message because the class {self.__class__.__name__} does not define an instance attribute with the same name as the class constructor parameter. To update Hyperparameter attributes after initialization, either modify the __init__ function of the class to define the attribute with the same name or call hp_instance.__init__(**new_init_parameters) to reset the parameters") from e
init_params[name] = value # Place the update value

self.__init__(**init_params) # Reinitialise
Comment thread
thijssnelleman marked this conversation as resolved.
Outdated


Comment thread
thijssnelleman marked this conversation as resolved.
Outdated
@property
def lower_vectorized(self) -> f64:
"""Lower bound of the hyperparameter in vector space."""
Expand Down
4 changes: 2 additions & 2 deletions src/ConfigSpace/hyperparameters/normal_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def __init__(
"""
if mu <= 0 and log:
raise ValueError(
f"Hyperparameter '{name}' has illegal settings: "
f"Illegal value for Hyperparameter '{name}': "
f"mu={mu} must be positive for log-scale.",
)

Expand All @@ -111,7 +111,7 @@ def __init__(
try:
scaler = UnitScaler(f64(self.lower), f64(self.upper), log=log, dtype=f64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

if default_value is None:
_default_value = np.clip(self.mu, self.lower, self.upper)
Expand Down
4 changes: 2 additions & 2 deletions src/ConfigSpace/hyperparameters/normal_integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __init__(
"""
if mu <= 0 and log:
raise ValueError(
f"Hyperparameter '{name}' has illegal settings: "
f"Illegal value for Hyperparameter '{name}': "
f"mu={mu} must be positive for log-scale.",
)

Expand All @@ -119,7 +119,7 @@ def __init__(
dtype=i64,
)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

if default_value is None:
_default_value = int(np.rint(np.clip(self.mu, self.lower, self.upper)))
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigSpace/hyperparameters/uniform_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __init__(
try:
scaler = UnitScaler(f64(self.lower), f64(self.upper), log=log, dtype=f64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

vect_dist = UnitUniformContinuousDistribution(
pdf_max_density=1 / float(self.upper - self.lower),
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigSpace/hyperparameters/uniform_integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def __init__(
try:
scaler = UnitScaler(i64(self.lower), i64(self.upper), log=log, dtype=i64)
except ValueError as e:
raise ValueError(f"Hyperparameter '{name}' has illegal settings") from e
raise ValueError(f"Illegal value(s) for Hyperparameter '{name}'") from e

size = self.upper - self.lower + 1
if not self.log:
Expand Down
125 changes: 120 additions & 5 deletions test/test_hyperparameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from __future__ import annotations

import copy
import re
from collections import defaultdict
from collections.abc import Mapping
from dataclasses import dataclass
Expand Down Expand Up @@ -60,7 +61,7 @@
}


def f() -> None:
def f():
pass


Expand Down Expand Up @@ -286,7 +287,7 @@ def test_uniformfloat_to_integer():
def test_uniformfloat_illegal_bounds():
with pytest.raises(
ValueError,
match="Hyperparameter 'param' has illegal settings",
match=re.escape("Illegal value(s) for Hyperparameter 'param'"),
) as e:
_ = UniformFloatHyperparameter("param", 0, 10, log=True)

Expand All @@ -298,7 +299,7 @@ def test_uniformfloat_illegal_bounds():

with pytest.raises(
ValueError,
match="Hyperparameter 'param' has illegal settings",
match=re.escape("Illegal value(s) for Hyperparameter 'param'"),
) as e:
_ = UniformFloatHyperparameter("param", 1, 0)

Expand Down Expand Up @@ -1245,7 +1246,7 @@ def test_uniformint_legal_float_values():
def test_uniformint_illegal_bounds():
with pytest.raises(
ValueError,
match="Hyperparameter 'param' has illegal settings",
match=re.escape("Illegal value(s) for Hyperparameter 'param'"),
) as e:
UniformIntegerHyperparameter("param", 0, 10, log=True)

Expand All @@ -1257,7 +1258,7 @@ def test_uniformint_illegal_bounds():

with pytest.raises(
ValueError,
match="Hyperparameter 'param' has illegal settings",
match=re.escape("Illegal value(s) for Hyperparameter 'param'"),
) as e:
_ = UniformIntegerHyperparameter("param", 1, 0)

Expand Down Expand Up @@ -3080,3 +3081,117 @@ def test_arbitrary_object_allowed_in_categorical_ordinal(
list(get_one_exchange_neighbourhood(sample, seed=1)) # no raise
for n in ns:
n.check_valid_configuration() # no raise


def test_update_hyperparameters():
space = ConfigurationSpace()
space.add(
[
UniformIntegerHyperparameter("a", 0, 100),
UniformFloatHyperparameter("b", -1.0, 1.0),
CategoricalHyperparameter("c", [1, 2, 3]),
OrdinalHyperparameter("d", [1, 2, 3]),
],
)
# Test updating numerical HP min/max values
space["a"].upper = 51
assert space["a"].upper == 51
space["a"].lower = 49
assert space["a"].lower == 49

# Sample the space to verify it does not sample OOD
sample = space.sample_configuration(size=25)
for value in sample:
assert 49 <= value["a"] <= 51

# Test updating default values
space["a"].lower = 1 # Update first to avoid error
space["a"].default_value = 5
assert space["a"].default_value == 5

# Test that it cannot change to an illegal value
with pytest.raises(ValueError):
space["a"].upper = 0 # lower than lower
with pytest.raises(ValueError):
space["a"].lower = 100 # higher than upper
with pytest.raises(ValueError):
space["a"].default_value = 1000 # Out of bounds

# Test Float
space["b"].upper = 0.1
assert space["b"].upper == 0.1
space["b"].lower = -0.1
assert space["b"].lower == -0.1

# Check sampling
sample = space.sample_configuration(size=100)
for value in sample:
assert -0.1 <= value["b"] <= 0.1

# Test illegal changes
with pytest.raises(
ValueError,
match=re.escape("Illegal value(s) for Hyperparameter 'b'"),
):
space["b"].upper = -0.11 # lower than lower
with pytest.raises(
ValueError,
match=re.escape("Illegal value(s) for Hyperparameter 'b'"),
):
space["b"].lower = 0.11 # higher than upper
with pytest.raises(
ValueError,
match=re.escape("Illegal value(s) for Hyperparameter 'b'"),
):
space["b"].default_value = -10.0 # Out of bounds
Comment thread
thijssnelleman marked this conversation as resolved.
Outdated
with pytest.raises(
ValueError,
match=re.escape(
"Can only set parameters passed to self.__init__. 'size' is not one of: ('self', 'name', 'lower', 'upper', 'default_value', 'log', 'meta')",
),
):
space["b"].size = 1_000_000 # Not an init parameter
with pytest.raises(
ValueError,
match=re.escape(
"Can only set parameters passed to self.__init__. 'non_existiting_attribute' is not one of: ('self', 'name', 'lower', 'upper', 'default_value', 'log', 'meta')",
Comment thread
thijssnelleman marked this conversation as resolved.
Outdated
),
):
space["b"].non_existiting_attribute = "wrong" # cannot add new attributes

# Test categorical HP
space["c"].choices = [1, 2, 3, 4] # Change range
assert space["c"].choices == (1, 2, 3, 4)

space["c"].default_value = 4 # Change default value
assert space["c"].default_value == 4

space["c"].weights = [0.1, 0.4, 0.1, 0.4] # Change weights
assert space["c"].weights == (0.1, 0.4, 0.1, 0.4)

# Test sampling
sample_count = {1: 0, 2: 0, 3: 0, 4: 0}
sample = space.sample_configuration(size=100)
for value in sample:
sample_count[value["c"]] += 1
assert sample_count[2] > sample_count[1]
assert sample_count[2] > sample_count[3]
assert sample_count[4] > sample_count[1]
assert sample_count[4] > sample_count[3]
Comment thread
thijssnelleman marked this conversation as resolved.
Outdated

# Test ordinal HP
space["d"].sequence = [1, 2, 3, 4] # Change range
assert space["d"].sequence == (1, 2, 3, 4)

space["d"].default_value = 4 # Change default value
assert space["d"].default_value == 4

# Test sampling
sample = space.sample_configuration(size=100)
assert 4 in [s["d"] for s in sample]

# TODO: Test changing HP type int -> float
# TODO: Test changing HP type float -> int
# TODO: Test changing HP type categorical -> ordinal
# TODO: Test changing HP type ordinal -> categorical
# TODO: Check that HP type cannot change between float/int and categorical/ordinal
Loading