Skip to content

Added TypedConfigurationSpace #345

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

Open
wants to merge 3 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
2 changes: 1 addition & 1 deletion ConfigSpace/api/types/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def Categorical(
:py:class:`CategoricalHyperparameter`.

meta: dict | None = None
Any additional meta information you would like to store along with the hyperparamter.
Any additional meta information you would like to store along with the hyperparameter.
"""
if ordered and weights is not None:
raise ValueError("Can't apply `weights` to `ordered` Categorical")
Expand Down
146 changes: 141 additions & 5 deletions ConfigSpace/configuration_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
import copy
import io
import warnings
from abc import ABC, abstractmethod
from collections import OrderedDict, defaultdict, deque
from itertools import chain
from typing import Any, Iterable, Iterator, KeysView, Mapping, cast, overload
from typing import Any, Callable, Generic, Iterable, Iterator, KeysView, Mapping, TypeVar, cast, overload
from typing_extensions import Final

import numpy as np
Expand Down Expand Up @@ -144,7 +145,7 @@ def __init__(
seed: int | None = None,
meta: dict | None = None,
*,
space: None
space: None | ConfigurationSpace
| (
dict[
str,
Expand All @@ -164,7 +165,7 @@ def __init__(
meta : dict, optional
Field for holding meta data provided by the user.
Not used by the configuration space.
space:
space: dict | ConfigurationSpace, optional
A simple configuration space to use:

.. code:: python
Expand All @@ -182,13 +183,17 @@ def __init__(
"""
# If first arg is a dict, we assume this to be `space`
if isinstance(name, dict):
if space is not None:
raise ValueError(
f"If name (or the first arg) is a dict, space must be None, got: {space}",
)
space = name
name = None

self.name = name
self.meta = meta

# NOTE: The idx of a hyperparamter is tied to its order in _hyperparamters
# NOTE: The idx of a hyperparameter is tied to its order in _hyperparameters
# Having three variables to keep track of this seems excessive
self._hyperparameters: OrderedDict[str, Hyperparameter] = OrderedDict()
self._hyperparameter_idx: dict[str, int] = {}
Expand Down Expand Up @@ -795,7 +800,7 @@ def get_active_hyperparameters(
def sample_configuration(self, size: None = None) -> Configuration:
...

# Technically this is wrong given the current behaviour but it's
# Technically, this is wrong given the current behaviour but it's
# sufficient for most cases. Once deprecation warning is up,
# we can just have `1` always return a list of configurations
# because an `int` was specified, `None` for single config.
Expand Down Expand Up @@ -1582,3 +1587,134 @@ def get_hyperparameter_names(self) -> list[str]:
return list(self._hyperparameters.keys())

# ---------------------------------------------------


TOutput = TypeVar("TOutput")


class TypedConfigurationSpace(ConfigurationSpace, Generic[TOutput], ABC):
"""
An extension of ConfigurationSpace that allows to sample instances of a given type.
In this version, the construction of the chosen type has to be implemented explicitly
by implementing the abstract method `_instantiate_type_from_config`, and each
implementation of this class is bound to the type it returns via generics.

A common case is when the user already has implemented a function for
instantiating the desired type from kwargs (e.g., the type's standard constructor)
and does not wish to create a separate class for handling the sampling.
In that case, one can use :class:`TypedConfigurationSpaceFromConstructor`
instead.

Examples
--------

A typical example would be
>>> @dataclass
... class MyOutputClass:
... a: int

For spaces that contain an integer under the key "a", one can then implement
>>> class MyOutputConfigurationSpace(TypedConfigurationSpace[MyOutputClass]):
... def _instantiate_type_from_config(self, config: Configuration):
... return MyOutputClass(**dict(config))
...

Then, one can create a config space of instances of `MyOutputClass` from the configuration space as
>>> space = {"a": (1, 5)}
>>> my_config_space = MyOutputConfigurationSpace(space=space)
"""

@abstractmethod
def _instantiate_type_from_config(self, config: Configuration) -> TOutput:
pass

@overload
def sample_type(self, size: None = None) -> TOutput:
...

@overload
def sample_type(self, size: int) -> list[TOutput]:
...

def sample_type(self, size: int | None = None) -> TOutput | list[TOutput]:
"""Sample a configuration from the configuration space object.

Parameters
----------
size : int, optional
Number of configurations to sample.

Returns
-------
TOut | list[TOut]
Sampled instances of the output type. If `size=None`, a single instance is
returned. Otherwise, a list of instances is returned.
"""
configs = super().sample_configuration(size)
if size in (None, 1):
configs = [configs]

configs = cast(list[Configuration], configs)
usr_type_instances = [self._instantiate_type_from_config(conf) for conf in configs]
if size is None:
return usr_type_instances[0]
return usr_type_instances


class TypedConfigurationSpaceFromConstructor(TypedConfigurationSpace[TOutput]):
"""
An specialization of TypedConfigurationSpaceFromConstructor for the common special
case is when the type can be instantiated directly from kwargs and the user
does not desire to have a separate class for handling the sampling of the type.

Examples
--------

A typical example would be
>>> @dataclass
... class MyOutputClass:
... a: int

Contrary to the base `TypedConfigurationSpace`, one does not need to implement
a new class to reach the desired functionality and instead only needs to pass
the type constructor, which in this case is just the type itself
>>> space = {"a": (1, 5)}
>>> my_config_space = TypedConfigurationSpaceFromConstructor(MyOutputClass, space=space)


"""
def __init__(
self,
output_constructor: Callable[[...], TOutput],
name: str | dict | None = None,
seed: int | None = None,
meta: dict | None = None,
*,
space: None
| (
dict[
str,
tuple[int, int] | tuple[float, float] | list[Any] | int | float | str,
]
) = None,
) -> None:
"""

Parameters
----------
output_constructor : Callable[[...], TOutput]
Takes the unpacked samples of the config space as kwargs
and returns an instance of the desired type. A typical example
is the default constructor of a dataclass.

"""
self.output_constructor = output_constructor
super().__init__(
name=name,
seed=seed,
meta=meta,
space=space,
)

def _instantiate_type_from_config(self, config: Configuration) -> TOutput:
return self.output_constructor(**dict(config))
6 changes: 3 additions & 3 deletions ConfigSpace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def impute_inactive_values(
"""Impute inactive parameters.

Iterate through the hyperparameters of a ``Configuration`` and set the
values of the inactive hyperparamters to their default values if the choosen
values of the inactive hyperparameters to their default values if the choosen
``strategy`` is 'default'. Otherwise ``strategy`` contains a float number.
Set the hyperparameters' value to this number.

Expand Down Expand Up @@ -341,7 +341,7 @@ def deactivate_inactive_hyperparameters(
----------
configuration : dict
a configuration as a dictionary. Key: name of the hyperparameter.
Value: value of this hyperparamter
Value: value of this hyperparameter
configuration from which inactive hyperparameters will be removed
configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace`
The defined configuration space. It is necessary to find the inactive
Expand Down Expand Up @@ -427,7 +427,7 @@ def fix_types(
----------
configuration : dict
a configuration as a dictionary. Key: name of the hyperparameter.
Value: value of this hyperparamter
Value: value of this hyperparameter
configuration_space : :class:`~ConfigSpace.configuration_space.ConfigurationSpace`
Configuration space which knows the types for all parameter values

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ConfigSpace

A simple Python/Cython module implementing a domain specific language to manage
A simple Python/Cython module implementing a domain-specific language to manage
configuration spaces for algorithm configuration and hyperparameter optimization tasks.
Distributed under BSD 3-clause, see LICENSE except all files in the directory
ConfigSpace.nx, which are copied from the networkx package and licensed
Expand Down
4 changes: 2 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Version 0.6.0

* ADD #255: An easy interface of `Float`, `Integer`, `Categorical` for creating search spaces.
* ADD #243: Add forbidden relations between two hyperparamters
* ADD #243: Add forbidden relations between two hyperparameters
* MAINT #243: Change branch `master` to `main`
* FIX #259: Numpy runtime error when rounding
* FIX #247: No longer errors when serliazing spaces with an `InCondition`
Expand All @@ -32,7 +32,7 @@
* FIX #221: Normal Hyperparameters should now properly sample from correct distribution in log space
* FIX #221: Fixed boundary problems with integer hyperparameters due to numerical rounding after sampling.
* MAINT #221: Categorical Hyperparameters now always have associated probabilities, remaining uniform if non are provided. (Same behaviour)
* ADD #222: BetaFloat and BetaInteger hyperparamters, hyperparameters distributed according to a beta distribution.
* ADD #222: BetaFloat and BetaInteger hyperparameters, hyperparameters distributed according to a beta distribution.
* ADD #241: Implements support for [PiBo](https://openreview.net/forum?id=MMAeCXIa89), you can now embed some prior distribution knowledge into ConfigSpace hyperparameters.
* See the example [here](https://automl.github.io/ConfigSpace/main/User-Guide.html#th-example-placing-priors-on-the-hyperparameters).
* Hyperparameters now have a `pdf(vector: np.ndarray) -> np.ndarray` to get the probability density values for the input
Expand Down
2 changes: 1 addition & 1 deletion docs/api/hyperparameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Hyperparameters
ConfigSpace contains
:func:`~ConfigSpace.api.types.float.Float`,
:func:`~ConfigSpace.api.types.integer.Integer`
and :func:`~ConfigSpace.api.types.categorical.Categorical` hyperparamters, each with their own customizability.
and :func:`~ConfigSpace.api.types.categorical.Categorical` hyperparameters, each with their own customizability.

For :func:`~ConfigSpace.api.types.float.Float` and :func:`~ConfigSpace.api.types.integer.Integer`, you will find their
interface much the same, being able to take the same :ref:`distributions <Distributions>` and parameters.
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ And that's it!
Advanced Usage
--------------
Lets create a more complex example where we have two models, model ``A`` and model ``B``.
Model ``B`` is some kernel based algorithm and ``A`` just needs a simple float hyperparamter.
Model ``B`` is some kernel based algorithm and ``A`` just needs a simple float hyperparameter.


We're going to create a config space that will let us correctly build a randomly selected model.
Expand Down
28 changes: 28 additions & 0 deletions test/test_configuration_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import json
import unittest
from collections import OrderedDict
from dataclasses import dataclass
from itertools import product

import numpy as np
Expand Down Expand Up @@ -68,6 +69,7 @@
OrdinalHyperparameter,
UniformFloatHyperparameter,
)
from configuration_space import TOutput, TypedConfigurationSpace, TypedConfigurationSpaceFromConstructor


def byteify(input):
Expand Down Expand Up @@ -1017,6 +1019,32 @@ def test_estimate_size(self):
assert np.isinf(cs.estimate_size())


class TestTypedConfigurationSpace(unittest.TestCase):
def test_custom_type_returned(self):
@dataclass
class CustomOutput:
foo: int
bar: str

class CustomConfigSpace(TypedConfigurationSpace[CustomOutput]):
def _instantiate_type_from_config(self, config: Configuration) -> TOutput:
config_dict = dict(config)
config_dict["bar"] = config_dict["baz"]
return CustomOutput(**config_dict)

space = {"foo": (1, 5), "baz": ["a", "b"]}
typed_cs_from_custom_class = CustomConfigSpace(space=space)
typed_cs_from_passing_constructor = TypedConfigurationSpaceFromConstructor(
CustomOutput, space=space
)
for typed_cs in (typed_cs_from_passing_constructor, typed_cs_from_custom_class):
assert isinstance(typed_cs.sample_type(), CustomOutput)
assert isinstance(typed_cs.sample_type(size=1), list)
assert isinstance(typed_cs.sample_type(size=1)[0], CustomOutput)
assert isinstance(typed_cs.sample_type(size=2), list)



class ConfigurationTest(unittest.TestCase):
def setUp(self):
cs = ConfigurationSpace()
Expand Down