-
Notifications
You must be signed in to change notification settings - Fork 95
Example solution to adapting HP bounds and defaults dynamically #414
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
base: main
Are you sure you want to change the base?
Changes from all commits
63a6a9a
6727061
babe99e
97f4084
729bc89
0fefb3b
bc60696
f269231
cdd9643
12ddcac
6f24222
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||||
|
|
@@ -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 on lines
+141
to
+147
|
||||||||||||||||||||
| 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 on lines
+149
to
+154
|
||||||||||||||||||||
| 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 | ||||||||||||||||||||
|
thijssnelleman marked this conversation as resolved.
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+161
to
+163
|
||||||||||||||||||||
| self.__init__(**init_params) # Reinitialise | |
| previous_state = self.__dict__.copy() | |
| try: | |
| self.__init__(**init_params) # Reinitialise | |
| except Exception: | |
| self.__dict__.clear() | |
| self.__dict__.update(previous_state) | |
| raise |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||
|
|
@@ -60,7 +61,7 @@ | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| def f() -> None: | ||||||||||||||||||||||||||||||||||||
| def f(): | ||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||
|
thijssnelleman marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||
| 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 on lines
+3149
to
+3157
|
||||||||||||||||||||||||||||||||||||
| 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')", | |
| match=r"Can only set parameters passed to self\.__init__\..*'size' is not one of:", | |
| ): | |
| space["b"].size = 1_000_000 # Not an init parameter | |
| with pytest.raises( | |
| ValueError, | |
| match=( | |
| r"Can only set parameters passed to self\.__init__\..*" | |
| r"'non_existiting_attribute' is not one of:" |
Copilot
AI
Apr 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new assertions rely on stochastic sampling (sample_configuration) without seeding the ConfigurationSpace RNG. This can lead to rare but real test flakiness (especially for the weighted categorical count comparisons). Seed the space (e.g., space.seed(…)) or pass a deterministic RNG where supported so the test outcome is reproducible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The admonition title contains spaces but isn’t quoted. Elsewhere in the docs, titled admonitions use quotes (e.g.,
!!! tip "New in 1.1!"), and unquoted titles with spaces may not parse/render as intended depending on the Markdown extension configuration. Use the quoted title form (or omit the title entirely) for consistency and correct rendering.