diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d152420a..4ed7856d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,63 +1,64 @@ -name: Docs +name: docs on: - # Trigger manually workflow_dispatch: - # Trigger on any push to the master + # Trigger on any push to the main push: branches: - - master + - main + - development - # Trigger on any push to a PR that targets master + # Trigger on any push to a PR that targets main pull_request: branches: - - master + - main + - development -jobs: +permissions: + contents: write + +env: + name: "ConfigSpace" +jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: "3.8" - name: Install dependencies run: | - pip install -e .[docs,examples,examples_unix] + pip install ".[dev]" - name: Make docs run: | - cd docs - make html - - - name: Run doctests - run: | - cd docs - make doctest + make clean + make docs - name: Pull latest gh-pages - if: (contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | cd .. - git clone https://github.com/automl/ConfigSpace.git --branch gh-pages --single-branch gh-pages + git clone https://github.com/${{ github.repository }}.git --branch gh-pages --single-branch gh-pages - name: Copy new docs into gh-pages - if: (contains(github.ref, 'develop') || contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | branch_name=${GITHUB_REF##*/} cd ../gh-pages rm -rf $branch_name - cp -r ../ConfigSpace/docs/build/html $branch_name + cp -r ../${{ env.name }}/docs/build/html $branch_name - name: Push to gh-pages - if: (contains(github.ref, 'master')) && github.event_name == 'push' + if: (contains(github.ref, 'develop') || contains(github.ref, 'main')) && github.event_name == 'push' run: | last_commit=$(git log --pretty=format:"%an: %s") cd ../gh-pages diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 34f23949..69b7edf0 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -8,12 +8,14 @@ on: # Trigger on any push to the master push: branches: - - master + - main + - development # Trigger on any push to a PR that targets master pull_request: branches: - - master + - main + - development jobs: diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index dcd61723..18e51f3f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -7,12 +7,14 @@ on: # Triggers with push to master push: branches: - - master + - main + - development # Triggers with push to a pr aimed at master pull_request: branches: - - master + - main + - development schedule: # Every day at 7AM UTC @@ -22,7 +24,7 @@ env: package-name: ConfigSpace test-dir: test - extra-requires: "[test]" # "" for no extra_requires + extra-requires: "[dev]" # "" for no extra_requires # Arguments used for pytest pytest-args: >- diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57e0ed3e..e34ab6c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ on: push: branches: - - master + - main # Release branches - "[0-9]+.[0-9]+.X" @@ -54,7 +54,7 @@ env: test-dir: test test-reqs: "pytest" test-cmd: "pytest -v" - extra-requires: "[test]" + extra-requires: "[dev]" jobs: @@ -74,11 +74,10 @@ jobs: # Not supported by numpy - system: "musllinux" - # Scipy doesn't have a wheel for cp310 i686 + # Scipy doesn't have a wheel for cp310 i686 - py: cp310 arch: "i686" - steps: - name: Checkout ${{ env.package-name }} uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index d812bf16..8bf83c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +.DS_Store + # Documentation docs/build/* +docs/examples/* *.py[cod] @@ -68,4 +71,4 @@ prof/ .vscode # Running pre-commit seems to generate these -.mypy_cache +.mypy_cache \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2dd2c63..3d1d9a5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,19 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.761 + rev: v0.961 hooks: - id: mypy args: [--show-error-codes, --ignore-missing-imports, --follow-imports, skip] name: mypy ConfigSpace files: ConfigSpace + - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 4.0.1 hooks: - id: flake8 name: flake8 ConfigSpace files: ConfigSpace + - id: flake8 name: flake8 test files: test diff --git a/ConfigSpace/__authors__.py b/ConfigSpace/__authors__.py new file mode 100644 index 00000000..caa17d5b --- /dev/null +++ b/ConfigSpace/__authors__.py @@ -0,0 +1,12 @@ +__authors__ = [ + "Matthias Feurer", + "Katharina Eggensperger", + "Syed Mohsin Ali", + "Christina Hernandez Wunsch", + "Julien-Charles Levesque", + "Jost Tobias Springenberg", + "Philipp Mueller", + "Marius Lindauer", + "Jorn Tuyls", + "Eddie Bergman", +] diff --git a/ConfigSpace/__init__.py b/ConfigSpace/__init__.py index 74e68396..525b50ba 100644 --- a/ConfigSpace/__init__.py +++ b/ConfigSpace/__init__.py @@ -27,32 +27,68 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from ConfigSpace.__version__ import __version__ -__authors__ = [ - "Matthias Feurer", "Katharina Eggensperger", "Syed Mohsin Ali", - "Christina Hernandez Wunsch", "Julien-Charles Levesque", - "Jost Tobias Springenberg", "Philipp Mueller", "Marius Lindauer", - "Jorn Tuyls" -] +from ConfigSpace.__authors__ import __authors__ -from ConfigSpace.configuration_space import Configuration, \ - ConfigurationSpace -from ConfigSpace.hyperparameters import CategoricalHyperparameter, \ - UniformFloatHyperparameter, UniformIntegerHyperparameter, Constant, \ - UnParametrizedHyperparameter, OrdinalHyperparameter -from ConfigSpace.conditions import AndConjunction, OrConjunction, \ - EqualsCondition, NotEqualsCondition, InCondition, GreaterThanCondition, LessThanCondition -from ConfigSpace.forbidden import ForbiddenAndConjunction, \ - ForbiddenEqualsClause, ForbiddenInClause, ForbiddenLessThanRelation, ForbiddenEqualsRelation, \ - ForbiddenGreaterThanRelation +import ConfigSpace.api.distributions as distributions +import ConfigSpace.api.types as types +from ConfigSpace.api import (Beta, Categorical, Distribution, Float, Integer, + Normal, Uniform) +from ConfigSpace.conditions import (AndConjunction, EqualsCondition, + GreaterThanCondition, InCondition, + LessThanCondition, NotEqualsCondition, + OrConjunction) +from ConfigSpace.configuration_space import Configuration, ConfigurationSpace +from ConfigSpace.forbidden import (ForbiddenAndConjunction, + ForbiddenEqualsClause, + ForbiddenEqualsRelation, + ForbiddenGreaterThanRelation, + ForbiddenInClause, + ForbiddenLessThanRelation) +from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, + BetaIntegerHyperparameter, + CategoricalHyperparameter, Constant, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, + UnParametrizedHyperparameter) -__all__ = ["__version__", "Configuration", "ConfigurationSpace", - "CategoricalHyperparameter", "UniformFloatHyperparameter", - "UniformIntegerHyperparameter", "Constant", - "UnParametrizedHyperparameter", "OrdinalHyperparameter", - "AndConjunction", "OrConjunction", - "EqualsCondition", "NotEqualsCondition", - "InCondition", "GreaterThanCondition", - "LessThanCondition", "ForbiddenAndConjunction", - "ForbiddenEqualsClause", "ForbiddenInClause", - "ForbiddenLessThanRelation", "ForbiddenEqualsRelation", - "ForbiddenGreaterThanRelation"] +__all__ = [ + "__authors__", + "__version__", + "Configuration", + "ConfigurationSpace", + "CategoricalHyperparameter", + "UniformFloatHyperparameter", + "UniformIntegerHyperparameter", + "BetaFloatHyperparameter", + "BetaIntegerHyperparameter", + "NormalFloatHyperparameter", + "NormalIntegerHyperparameter", + "Constant", + "UnParametrizedHyperparameter", + "OrdinalHyperparameter", + "AndConjunction", + "OrConjunction", + "EqualsCondition", + "NotEqualsCondition", + "InCondition", + "GreaterThanCondition", + "LessThanCondition", + "ForbiddenAndConjunction", + "ForbiddenEqualsClause", + "ForbiddenInClause", + "ForbiddenLessThanRelation", + "ForbiddenEqualsRelation", + "ForbiddenGreaterThanRelation", + "Beta", + "Categorical", + "Distribution", + "Float", + "Integer", + "Normal", + "Uniform", + "distributions", + "types", +] diff --git a/ConfigSpace/__version__.py b/ConfigSpace/__version__.py index a3e938c6..ee3313ca 100644 --- a/ConfigSpace/__version__.py +++ b/ConfigSpace/__version__.py @@ -1,4 +1,4 @@ """Version information.""" # The following line *must* be the last in the module, exactly as formatted: -__version__ = "0.5.0" +__version__ = "0.6.0" diff --git a/ConfigSpace/api/__init__.py b/ConfigSpace/api/__init__.py new file mode 100644 index 00000000..fd1b6507 --- /dev/null +++ b/ConfigSpace/api/__init__.py @@ -0,0 +1,16 @@ +import ConfigSpace.api.distributions as distributions +import ConfigSpace.api.types as types +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.api.types import Categorical, Float, Integer + +__all__ = [ + "types", + "distributions", + "Beta", + "Distribution", + "Normal", + "Uniform", + "Categorical", + "Float", + "Integer", +] diff --git a/ConfigSpace/api/distributions.py b/ConfigSpace/api/distributions.py new file mode 100644 index 00000000..028c8a5a --- /dev/null +++ b/ConfigSpace/api/distributions.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass + + +@dataclass +class Distribution: + """Base distribution type""" + + pass + + +@dataclass +class Uniform(Distribution): + """A uniform distribution""" + + pass + + +@dataclass +class Normal(Distribution): + """Represents a normal distribution. + + Parameters + ---------- + mu: float + The mean of the distribution + + sigma: float + The standard deviation of the float + """ + + mu: float + sigma: float + + +@dataclass +class Beta(Distribution): + """Represents a beta distribution. + + Parameters + ---------- + alpha: float + The alpha parameter of a beta distribution + + beta: float + The beta parameter of a beta distribution + """ + + alpha: float + beta: float diff --git a/ConfigSpace/api/types/__init__.py b/ConfigSpace/api/types/__init__.py new file mode 100644 index 00000000..28259e38 --- /dev/null +++ b/ConfigSpace/api/types/__init__.py @@ -0,0 +1,5 @@ +from ConfigSpace.api.types.categorical import Categorical +from ConfigSpace.api.types.float import Float +from ConfigSpace.api.types.integer import Integer + +__all__ = ["Categorical", "Float", "Integer"] diff --git a/ConfigSpace/api/types/categorical.py b/ConfigSpace/api/types/categorical.py new file mode 100644 index 00000000..da095eef --- /dev/null +++ b/ConfigSpace/api/types/categorical.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from typing import Sequence, Union, overload + +from typing_extensions import (Literal, # Move to `typing` when 3.8 minimum + TypeAlias) + +from ConfigSpace.hyperparameters import (CategoricalHyperparameter, + OrdinalHyperparameter) + +# We only accept these types in `items` +T: TypeAlias = Union[str, int, float] + + +# ordered False -> CategoricalHyperparameter +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: Literal[False], + meta: dict | None = None, +) -> CategoricalHyperparameter: + ... + + +# ordered True -> OrdinalHyperparameter +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: Literal[True], + meta: dict | None = None, +) -> OrdinalHyperparameter: + ... + + +# ordered bool (unknown) -> Either +@overload +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: bool = ..., + meta: dict | None = None, +) -> CategoricalHyperparameter | OrdinalHyperparameter: + ... + + +def Categorical( + name: str, + items: Sequence[T], + *, + default: T | None = None, + weights: Sequence[float] | None = None, + ordered: bool = False, + meta: dict | None = None, +) -> CategoricalHyperparameter | OrdinalHyperparameter: + """Creates a Categorical Hyperparameter. + + CategoricalHyperparameter's can be used to represent a discrete + choice. Optionally, you can specify that these values are also ordered in + some manner, e.g. ``["small", "medium", "large"]``. + + .. code:: python + + # A simple categorical hyperparameter + c = Categorical("animals", ["cat", "dog", "mouse"]) + + # With a default + c = Categorical("animals", ["cat", "dog", "mouse"], default="mouse") + + # Make them weighted + c = Categorical("animals", ["cat", "dog", "mouse"], weights=[0.1, 0.8, 3.14]) + + # Specify it's an OrdinalHyperparameter (ordered categories) + # ... note that you can't apply weights to an Ordinal + o = Categorical("size", ["small", "medium", "large"], ordered=True) + + # Add some meta information for your own tracking + c = Categorical("animals", ["cat", "dog", "mouse"], meta={"use": "Favourite Animal"}) + + Note + ---- + ``Categorical`` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check with either + :py:class:`~ConfigSpace.hyperparameters.CategoricalHyperparameter` + and/or :py:class:`~ConfigSpace.hyperparameters.OrdinalHyperparameter`. + + Parameters + ---------- + name: str + The name of the hyperparameter + + items: Sequence[T], + A list of items to put in the category. Note that there are limitations: + + * Can't use `None`, use a string "None" instead and convert as required. + * Can't have duplicate categories, use weights if required. + + default: T | None = None + The default value of the categorical hyperparameter + + weights: Sequence[float] | None = None + The weights to apply to each categorical. Each item will be sampled according + to these weights. + + ordered: bool = False + Whether the categorical is ordered or not. If True, this will return an + :py:class:`OrdinalHyperparameter`, otherwise it remain a + :py:class:`CategoricalHyperparameter`. + + meta: dict | None = None + Any additional meta information you would like to store along with the hyperparamter. + """ + if ordered and weights is not None: + raise ValueError("Can't apply `weights` to `ordered` Categorical") + + if ordered: + return OrdinalHyperparameter( + name=name, + sequence=items, + default_value=default, + meta=meta, + ) + else: + return CategoricalHyperparameter( + name=name, + choices=items, + default_value=default, + weights=weights, + meta=meta, + ) diff --git a/ConfigSpace/api/types/float.py b/ConfigSpace/api/types/float.py new file mode 100644 index 00000000..31b5117e --- /dev/null +++ b/ConfigSpace/api/types/float.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +from typing import overload + +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, + NormalFloatHyperparameter, + UniformFloatHyperparameter) + + +# Uniform | None -> UniformFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Uniform | None = ..., + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> UniformFloatHyperparameter: + ... + + +# Normal -> NormalFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Normal, + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> NormalFloatHyperparameter: + ... + + +# Beta -> BetaFloatHyperparameter +@overload +def Float( + name: str, + bounds: tuple[float, float] | None = ..., + *, + distribution: Beta, + default: float | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> BetaFloatHyperparameter: + ... + + +def Float( + name: str, + bounds: tuple[float, float] | None = None, + *, + distribution: Distribution | None = None, + default: float | None = None, + q: int | None = None, + log: bool = False, + meta: dict | None = None, +) -> UniformFloatHyperparameter | NormalFloatHyperparameter | BetaFloatHyperparameter: + """Create a FloatHyperparameter. + + .. code:: python + + # Uniformly distributed + Float("a", (1, 10)) + Float("a", (1, 10), distribution=Uniform()) + + # Normally distributed at 2 with std 3 + Float("b", distribution=Normal(2, 3)) + Float("b", (0, 5), distribution=Normal(2, 3)) # ... bounded + + # Beta distributed with alpha 1 and beta 2 + Float("c", distribution=Beta(1, 2)) + Float("c", (0, 3), distribution=Beta(1, 2)) # ... bounded + + # Give it a default value + Float("a", (1, 10), default=4.3) + + # Sample on a log scale + Float("a", (1, 100), log=True) + + # Quantized into three brackets + Float("a", (1, 10), q=3) + + # Add meta info to the param + Float("a", (1.0, 10), meta={"use": "For counting chickens"}) + + Note + ---- + `Float` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check and not `Float`. + + Parameters + ---------- + name : str + The name to give to this hyperparameter + + bounds : tuple[float, float] | None = None + The bounds to give to the float. Note that by default, this is required + for Uniform distribution, which is the default distribution + + distribution : Uniform | Normal | Beta, = Uniform + The distribution to use for the hyperparameter. See above + + default : float | None = None + The default value to give to the hyperparameter. + + q : float | None = None + The quantization factor, must evenly divide the boundaries. + + Note + ---- + Quantization points act are not equal and require experimentation + to be certain about + + * https://github.com/automl/ConfigSpace/issues/264 + + log : bool = False + Whether to this parameter lives on a log scale + + meta : dict | None = None + Any meta information you want to associate with this parameter + + Returns + ------- + UniformFloatHyperparameter | NormalFloatHyperparameter | BetaFloatHyperparameter + Returns the corresponding hyperparameter type + """ + if distribution is None: + distribution = Uniform() + + if bounds is None and isinstance(distribution, Uniform): + raise ValueError("`bounds` must be specifed for Uniform distribution") + + if bounds is None: + lower, upper = (None, None) + else: + lower, upper = bounds + + if isinstance(distribution, Uniform): + return UniformFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + default_value=default, + q=q, + log=log, + meta=meta, + ) + elif isinstance(distribution, Normal): + return NormalFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + default_value=default, + mu=distribution.mu, + sigma=distribution.sigma, + q=q, + log=log, + meta=meta, + ) + elif isinstance(distribution, Beta): + return BetaFloatHyperparameter( + name=name, + lower=lower, + upper=upper, + alpha=distribution.alpha, + beta=distribution.beta, + default_value=default, + q=q, + log=log, + meta=meta, + ) + else: + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/api/types/integer.py b/ConfigSpace/api/types/integer.py new file mode 100644 index 00000000..539c6650 --- /dev/null +++ b/ConfigSpace/api/types/integer.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +from typing import overload + +from ConfigSpace.api.distributions import Beta, Distribution, Normal, Uniform +from ConfigSpace.hyperparameters import ( + BetaIntegerHyperparameter, + NormalIntegerHyperparameter, + UniformIntegerHyperparameter, +) + + +# Uniform | None -> UniformIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Uniform | None = ..., + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> UniformIntegerHyperparameter: + ... + + +# Normal -> NormalIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Normal, + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> NormalIntegerHyperparameter: + ... + + +# Beta -> BetaIntegerHyperparameter +@overload +def Integer( + name: str, + bounds: tuple[int, int] | None = ..., + *, + distribution: Beta, + default: int | None = ..., + q: int | None = ..., + log: bool = ..., + meta: dict | None = ..., +) -> BetaIntegerHyperparameter: + ... + + +def Integer( + name: str, + bounds: tuple[int, int] | None = None, + *, + distribution: Distribution | None = None, + default: int | None = None, + q: int | None = None, + log: bool = False, + meta: dict | None = None, +) -> UniformIntegerHyperparameter | NormalIntegerHyperparameter | BetaIntegerHyperparameter: + """Create an IntegerHyperparameter. + + .. code:: python + + # Uniformly distributed + Integer("a", (1, 10)) + Integer("a", (1, 10), distribution=Uniform()) + + # Normally distributed at 2 with std 3 + Integer("b", distribution=Normal(2, 3)) + Integer("b", (0, 5), distribution=Normal(2, 3)) # ... bounded + + # Beta distributed with alpha 1 and beta 2 + Integer("c", distribution=Beta(1, 2)) + Integer("c", (0, 3), distribution=Beta(1, 2)) # ... bounded + + # Give it a default value + Integer("a", (1, 10), default=4) + + # Sample on a log scale + Integer("a", (1, 100), log=True) + + # Quantized into three brackets + Integer("a", (1, 10), q=3) + + # Add meta info to the param + Integer("a", (1, 10), meta={"use": "For counting chickens"}) + + Note + ---- + `Integer` is actually a function, please use the corresponding return types if + doing an `isinstance(param, type)` check and not `Integer`. + + Parameters + ---------- + name : str + The name to give to this hyperparameter + + bounds : tuple[int, int] | None = None + The bounds to give to the integer. Note that by default, this is required + for Uniform distribution, which is the default distribution + + distribution : Uniform | Normal | Beta, = Uniform + The distribution to use for the hyperparameter. See above + + default : int | None = None + The default value to give to the hyperparameter. + + q : int | None = None + The quantization factor, must evenly divide the boundaries. + Sampled values will be + + .. code:: + + full range + 1 4 7 10 + |--------------| + | | | | q = 3 + + All samples here will then be in {1, 4, 7, 10} + + Note + ---- + Quantization points act are not equal and require experimentation + to be certain about + + * https://github.com/automl/ConfigSpace/issues/264 + + log : bool = False + Whether to this parameter lives on a log scale + + meta : dict | None = None + Any meta information you want to associate with this parameter + + Returns + ------- + UniformIntegerHyperparameter | NormalIntegerHyperparameter | BetaIntegerHyperparameter + Returns the corresponding hyperparameter type + """ + if distribution is None: + distribution = Uniform() + + if bounds is None and isinstance(distribution, Uniform): + raise ValueError("`bounds` must be specifed for Uniform distribution") + + if bounds is None: + lower, upper = (None, None) + else: + lower, upper = bounds + + if isinstance(distribution, Uniform): + return UniformIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + ) + elif isinstance(distribution, Normal): + return NormalIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + mu=distribution.mu, + sigma=distribution.sigma, + ) + elif isinstance(distribution, Beta): + return BetaIntegerHyperparameter( + name=name, + lower=lower, + upper=upper, + q=q, + log=log, + default_value=default, + meta=meta, + alpha=distribution.alpha, + beta=distribution.beta, + ) + else: + raise ValueError(f"Unknown distribution type {type(distribution)}") diff --git a/ConfigSpace/conditions.pyx b/ConfigSpace/conditions.pyx index 24e266fb..08f7add2 100644 --- a/ConfigSpace/conditions.pyx +++ b/ConfigSpace/conditions.pyx @@ -40,7 +40,6 @@ from libc.stdlib cimport malloc, free import numpy as np -from ConfigSpace.hyperparameters import NumericalHyperparameter, OrdinalHyperparameter from ConfigSpace.hyperparameters cimport Hyperparameter cimport numpy as np @@ -114,9 +113,9 @@ cdef class AbstractCondition(ConditionComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -169,24 +168,18 @@ cdef class EqualsCondition(AbstractCondition): def __init__(self, child: Hyperparameter, parent: Hyperparameter, value: Union[str, float, int]) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *equal* to ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', choices=[1, 2, 3]) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] + Make *b* an active hyperparameter if *a* has the value 1 - make *b* an active hyperparameter if *a* has the value 1 - - >>> cond = CS.EqualsCondition(b, a, 1) + >>> from ConfigSpace import ConfigurationSpace, EqualsCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": [1, 2, 3], + ... "b": (1.0, 8.0) + ... }) + >>> cond = EqualsCondition(cs['b'], cs['a'], 1) >>> cs.add_condition(cond) b | a == 1 @@ -245,24 +238,18 @@ cdef class EqualsCondition(AbstractCondition): cdef class NotEqualsCondition(AbstractCondition): def __init__(self, child: Hyperparameter, parent: Hyperparameter, value: Union[str, float, int]) -> None: - """ - Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter + """Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *not equal* to ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', choices=[1, 2, 3]) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - make *b* an active hyperparameter if *a* has **not** the value 1 + Make *b* an active hyperparameter if *a* has **not** the value 1 - >>> cond = CS.NotEqualsCondition(b, a, 1) + >>> from ConfigSpace import ConfigurationSpace, NotEqualsCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": [1, 2, 3], + ... "b": (1.0, 8.0) + ... }) + >>> cond = NotEqualsCondition(cs['b'], cs['a'], 1) >>> cs.add_condition(cond) b | a != 1 @@ -276,7 +263,6 @@ cdef class NotEqualsCondition(AbstractCondition): *not equal condition* value : str, float, int Value, which the parent is compared to - """ super(NotEqualsCondition, self).__init__(child, parent) if not parent.is_legal(value): @@ -326,22 +312,17 @@ cdef class LessThanCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *less than* ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.UniformFloatHyperparameter('a', lower=0., upper=10.) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, b, Type: ...] + Make *b* an active hyperparameter if *a* is less than 5 - make *b* an active hyperparameter if *a* is less than 5 - - >>> cond = CS.LessThanCondition(b, a, 5.) + >>> from ConfigSpace import ConfigurationSpace, LessThanCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = LessThanCondition(cs['b'], cs['a'], 5) >>> cs.add_condition(cond) - b | a < 5.0 + b | a < 5 Parameters ---------- @@ -352,9 +333,7 @@ cdef class LessThanCondition(AbstractCondition): The hyperparameter, which has to satisfy the *LessThanCondition* value : str, float, int Value, which the parent is compared to - """ - super(LessThanCondition, self).__init__(child, parent) self.parent.allow_greater_less_comparison() if not parent.is_legal(value): @@ -404,22 +383,17 @@ cdef class GreaterThanCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *greater than* ``value``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformFloatHyperparameter('a', lower=0., upper=10.) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformFloat, Range: [0.0, 10.0], Default: 5.0, b, Type: ...] - - make *b* an active hyperparameter if *a* is greater than 5 + Make *b* an active hyperparameter if *a* is greater than 5 - >>> cond = CS.GreaterThanCondition(b, a, 5.) + >>> from ConfigSpace import ConfigurationSpace, GreaterThanCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = GreaterThanCondition(cs['b'], cs['a'], 5) >>> cs.add_condition(cond) - b | a > 5.0 + b | a > 5 Parameters ---------- @@ -430,7 +404,6 @@ cdef class GreaterThanCondition(AbstractCondition): The hyperparameter, which has to satisfy the *GreaterThanCondition* value : str, float, int Value, which the parent is compared to - """ super(GreaterThanCondition, self).__init__(child, parent) @@ -484,20 +457,15 @@ cdef class InCondition(AbstractCondition): Hyperparameter ``child`` is conditional on the ``parent`` hyperparameter being *in* a set of ``values``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=0, upper=10) - >>> b = CSH.UniformFloatHyperparameter('b', lower=1., upper=8., log=False) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformInteger, Range: [0, 10], Default: 5, b, Type: ...] - make *b* an active hyperparameter if *a* is in the set [1, 2, 3, 4] - >>> cond = CS.InCondition(b, a, [1, 2, 3, 4]) + >>> from ConfigSpace import ConfigurationSpace, InCondition + >>> + >>> cs = ConfigurationSpace({ + ... "a": (0, 10), + ... "b": (1.0, 8.0) + ... }) + >>> cond = InCondition(cs['b'], cs['a'], [1, 2, 3, 4]) >>> cs.add_condition(cond) b | a in {1, 2, 3, 4} @@ -567,9 +535,9 @@ cdef class AbstractConjunction(ConditionComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -676,37 +644,35 @@ cdef class AndConjunction(AbstractConjunction): # TODO: test if an AndConjunction results in an illegal state or a # Tautology! -> SAT solver def __init__(self, *args: AbstractCondition) -> None: - """ - By using the *AndConjunction*, constraints can easily be connected. - - Example - ------- - The following example shows how two constraints with an - *AndConjunction* can be combined. - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=5, upper=15) - >>> b = CSH.UniformIntegerHyperparameter('b', lower=0, upper=10) - >>> c = CSH.UniformFloatHyperparameter('c', lower=0., upper=1.) - >>> cs.add_hyperparameters([a, b, c]) - [a, Type: UniformInteger, Range: [5, 15], Default: 10, b, Type: ...] - - >>> less_cond = CS.LessThanCondition(c, a, 10) - >>> greater_cond = CS.GreaterThanCondition(c, b, 5) - >>> cs.add_condition(CS.AndConjunction(less_cond, greater_cond)) + """By using the *AndConjunction*, constraints can easily be connected. + + The following example shows how two constraints with an *AndConjunction* + can be combined. + + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... LessThanCondition, + ... GreaterThanCondition, + ... AndConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({ + ... "a": (5, 15), + ... "b": (0, 10), + ... "c": (0.0, 1.0) + ... }) + >>> less_cond = LessThanCondition(cs['c'], cs['a'], 10) + >>> greater_cond = GreaterThanCondition(cs['c'], cs['b'], 5) + >>> cs.add_condition(AndConjunction(less_cond, greater_cond)) (c | a < 10 && c | b > 5) Parameters ---------- *args : :ref:`Conditions` conditions, which will be combined with an *AndConjunction* - """ if len(args) < 2: - raise ValueError("AndConjunction must at least have two " - "Conditions.") + raise ValueError("AndConjunction must at least have two Conditions.") super(AndConjunction, self).__init__(*args) def __repr__(self) -> str: @@ -744,21 +710,21 @@ cdef class OrConjunction(AbstractConjunction): Similar to the *AndConjunction*, constraints can be combined by using the *OrConjunction*. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=5, upper=15) - >>> b = CSH.UniformIntegerHyperparameter('b', lower=0, upper=10) - >>> c = CSH.UniformFloatHyperparameter('c', lower=0., upper=1.) - >>> cs.add_hyperparameters([a, b, c]) - [a, Type: UniformInteger, Range: [5, 15], Default: 10, b, Type: ...] - - >>> less_cond = CS.LessThanCondition(c, a, 10) - >>> greater_cond = CS.GreaterThanCondition(c, b, 5) - >>> cs.add_condition(CS.OrConjunction(less_cond, greater_cond)) + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... LessThanCondition, + ... GreaterThanCondition, + ... OrConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({ + ... "a": (5, 15), + ... "b": (0, 10), + ... "c": (0.0, 1.0) + ... }) + >>> less_cond = LessThanCondition(cs['c'], cs['a'], 10) + >>> greater_cond = GreaterThanCondition(cs['c'], cs['b'], 5) + >>> cs.add_condition(OrConjunction(less_cond, greater_cond)) (c | a < 10 || c | b > 5) Parameters @@ -767,8 +733,7 @@ cdef class OrConjunction(AbstractConjunction): conditions, which will be combined with an *OrConjunction* """ if len(args) < 2: - raise ValueError("OrConjunction must at least have two " - "Conditions.") + raise ValueError("OrConjunction must at least have two Conditions.") super(OrConjunction, self).__init__(*args) def __repr__(self) -> str: diff --git a/ConfigSpace/configuration_space.pyx b/ConfigSpace/configuration_space.pyx index bd1e806d..fcfc62d0 100644 --- a/ConfigSpace/configuration_space.pyx +++ b/ConfigSpace/configuration_space.pyx @@ -43,7 +43,8 @@ from ConfigSpace.hyperparameters import ( FloatHyperparameter, UniformFloatHyperparameter, UniformIntegerHyperparameter, - OrdinalHyperparameter + CategoricalHyperparameter, + OrdinalHyperparameter, ) from ConfigSpace.conditions import ( ConditionComponent, @@ -69,13 +70,14 @@ class ConfigurationSpace(collections.abc.Mapping): # TODO add a method to add whole configuration spaces as a child "tree" def __init__( - self, - name: Union[str, None] = None, - seed: Union[int, None] = None, - meta: Optional[Dict] = None, + self, + name: Union[str, Dict, None] = None, + seed: Union[int, None] = None, + meta: Optional[Dict] = None, + *, + space: Optional[Dict[str, Union[Tuple[int, int], Tuple[float, float], List[Union[int, float, str]], int, float, str]]] = None ) -> None: - """ - A collection-like object containing a set of hyperparameter definitions and conditions. + """A collection-like object containing a set of hyperparameter definitions and conditions. A configuration space organizes all hyperparameters and its conditions as well as its forbidden clauses. Configurations can be sampled from @@ -85,14 +87,36 @@ class ConfigurationSpace(collections.abc.Mapping): Parameters ---------- - name : str, optional - Name of the configuration space + name : str | Dict, optional + Name of the configuration space. If a dict is passed, this is considered the same + as the `space` arg. + seed : int, optional random seed meta : dict, optional Field for holding meta data provided by the user. Not used by the configuration space. + + space: Dict[str, Tuple[int, int] | Tuple[float, float] | List[str] | int | float | str] | None = None + A simple configuration space to use: + + .. code:: python + + ConfigurationSpace( + name="myspace", + space={ + "uniform_integer": (1, 10), + "uniform_float": (1.0, 10.0), + "categorical": ["a", "b", "c"], + "constant": 1337, + } + """ + # If first arg is a dict, we assume this to be `space` + if isinstance(name, Dict): + space = name + name = None + self.name = name self.meta = meta @@ -125,6 +149,56 @@ class ConfigurationSpace(collections.abc.Mapping): self._parents_of = dict() self._children_of = dict() + # User provided a basic configspace + if space is not None: + + # We store and do in one go due to caching mechanisms + hps = [] + for name, hp in space.items(): + + # Anything that is a Hyperparameter already is good + # Note that we discard the key name in this case in favour + # of the name given in the dictionary + if isinstance(hp, Hyperparameter): + hps.append(hp) + + # Tuples are bounds, check if float or int + elif isinstance(hp, Tuple): + if len(hp) != 2: + raise ValueError( + "'%s' must be (lower, upper) bound, got %s" + % (name, hp) + ) + lower, upper = hp + if isinstance(lower, float): + real_hp = UniformFloatHyperparameter(name, lower, upper) + else: + real_hp = UniformIntegerHyperparameter(name, lower, upper) + + hps.append(real_hp) + + # Lists are categoricals + elif isinstance(hp, List): + if len(hp) == 0: + raise ValueError( + "Can't have empty list for categorical '%s'" % name + ) + + real_hp = CategoricalHyperparameter(name, hp) + hps.append(real_hp) + + # If it's an allowed type, it's a constant + elif isinstance(hp, (int, str, float)): + real_hp = Constant(name, hp) + hps.append(real_hp) + + else: + raise ValueError("Unknown value '%s' for '%s'" % (hp, name)) + + # Finally, add them in + self.add_hyperparameters(hps) + + def generate_all_continuous_from_bounds(self, bounds: List[List[Any]]) -> None: """ Generate :class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` @@ -626,7 +700,7 @@ class ConfigurationSpace(collections.abc.Mapping): prefix: str, configuration_space: 'ConfigurationSpace', delimiter: str = ":", - parent_hyperparameter: Hyperparameter = None + parent_hyperparameter: dict = None ) -> 'ConfigurationSpace': """ Combine two configuration space by adding one the other configuration @@ -642,9 +716,11 @@ class ConfigurationSpace(collections.abc.Mapping): The configuration space which should be added delimiter : str, optional Defaults to ':' - parent_hyperparameter : :ref:`Hyperparameters`, optional + parent_hyperparameter : dict | None = None Adds for each new hyperparameter the condition, that - ``parent_hyperparameter`` is active + ``parent_hyperparameter`` is active. Must be a dictionary with two keys + "parent" and "value", meaning that the added configuration space is active + when `parent` is equal to `value` Returns ------- @@ -1370,9 +1446,9 @@ class ConfigurationSpace(collections.abc.Mapping): def remove_hyperparameter_priors(self) -> 'ConfigurationSpace': """ Produces a new ConfigurationSpace where all priors on parameters are removed. - Non-uniform hyperpararmeters are replaced with uniform ones, and - CategoricalHyperparameters with weights have their weights removed. - + Non-uniform hyperpararmeters are replaced with uniform ones, and + CategoricalHyperparameters with weights have their weights removed. + Returns ------- :class:`~ConfigSpace.configuration_space.ConfigurationSpace` @@ -1384,12 +1460,12 @@ class ConfigurationSpace(collections.abc.Mapping): uniform_config_space.add_hyperparameter(parameter.to_uniform()) else: uniform_config_space.add_hyperparameter(copy.copy(parameter)) - + new_conditions = self.substitute_hyperparameters_in_conditions(self.get_conditions(), uniform_config_space) new_forbiddens = self.substitute_hyperparameters_in_forbiddens(self.get_forbiddens(), uniform_config_space) uniform_config_space.add_conditions(new_conditions) uniform_config_space.add_forbidden_clauses(new_forbiddens) - + return uniform_config_space def estimate_size(self) -> Union[float, int]: @@ -1421,21 +1497,21 @@ class ConfigurationSpace(collections.abc.Mapping): @staticmethod def substitute_hyperparameters_in_conditions(conditions, new_configspace) -> List['ConditionComponent']: """ - Takes a set of conditions and generates a new set of conditions with the same structure, where + Takes a set of conditions and generates a new set of conditions with the same structure, where each hyperparameter is replaced with its namesake in new_configspace. As such, the set of conditions remain unchanged, but the included hyperparameters are changed to match those types that exist in new_configspace. Parameters ---------- - new_configspace: ConfigurationSpace + new_configspace: ConfigurationSpace A ConfigurationSpace containing hyperparameters with the same names as those in the conditions. Returns ------- - List[ConditionComponent]: + List[ConditionComponent]: The list of conditions, adjusted to fit the new ConfigurationSpace - """ + """ new_conditions = [] for condition in conditions: if isinstance(condition, AbstractConjunction): @@ -1444,14 +1520,14 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_children = ConfigurationSpace.substitute_hyperparameters_in_conditions(children, new_configspace) substituted_conjunction = conjunction_type(*substituted_children) new_conditions.append(substituted_conjunction) - + elif isinstance(condition, AbstractCondition): condition_type = type(condition) child_name = getattr(condition.get_children()[0], 'name') parent_name = getattr(condition.get_parents()[0], 'name') new_child = new_configspace[child_name] new_parent = new_configspace[parent_name] - + if hasattr(condition, 'value'): condition_arg = getattr(condition, 'value') substituted_condition = condition_type(child=new_child, parent=new_parent, value=condition_arg) @@ -1460,31 +1536,31 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_condition = condition_type(child=new_child, parent=new_parent, values=condition_arg) else: raise AttributeError(f'Did not find the expected attribute in condition {type(condition)}.') - + new_conditions.append(substituted_condition) else: raise TypeError(f'Did not expect the supplied condition type {type(condition)}.') - + return new_conditions @staticmethod def substitute_hyperparameters_in_forbiddens(forbiddens, new_configspace) -> List['ConditionComponent']: """ - Takes a set of forbidden clauses and generates a new set of forbidden clauses with the same structure, - where each hyperparameter is replaced with its namesake in new_configspace. As such, the set of forbidden + Takes a set of forbidden clauses and generates a new set of forbidden clauses with the same structure, + where each hyperparameter is replaced with its namesake in new_configspace. As such, the set of forbidden clauses remain unchanged, but the included hyperparameters are changed to match those types that exist in new_configspace. Parameters ---------- - new_configspace: ConfigurationSpace + new_configspace: ConfigurationSpace A ConfigurationSpace containing hyperparameters with the same names as those in the forbidden clauses. Returns ------- - List[AbstractForbiddenComponent]: + List[AbstractForbiddenComponent]: The list of forbidden clauses, adjusted to fit the new ConfigurationSpace - """ + """ new_forbiddens = [] for forbidden in forbiddens: if isinstance(forbidden, AbstractForbiddenConjunction): @@ -1493,12 +1569,12 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_children = ConfigurationSpace.substitute_hyperparameters_in_forbiddens(children, new_configspace) substituted_conjunction = conjunction_type(*substituted_children) new_forbiddens.append(substituted_conjunction) - + elif isinstance(forbidden, AbstractForbiddenClause): forbidden_type = type(forbidden) hyperparameter_name = getattr(forbidden.hyperparameter, 'name') new_hyperparameter = new_configspace[hyperparameter_name] - + if hasattr(forbidden, 'value'): forbidden_arg = getattr(forbidden, 'value') substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, value=forbidden_arg) @@ -1507,11 +1583,11 @@ class ConfigurationSpace(collections.abc.Mapping): substituted_forbidden = forbidden_type(hyperparameter=new_hyperparameter, values=forbidden_arg) else: raise AttributeError(f'Did not find the expected attribute in forbidden {type(forbidden)}.') - + new_forbiddens.append(substituted_forbidden) else: raise TypeError(f'Did not expect the supplied forbidden type {type(forbidden)}.') - + return new_forbiddens diff --git a/ConfigSpace/forbidden.pyx b/ConfigSpace/forbidden.pyx index 559877fa..ec1e75f5 100644 --- a/ConfigSpace/forbidden.pyx +++ b/ConfigSpace/forbidden.pyx @@ -56,9 +56,9 @@ cdef class AbstractForbiddenComponent(object): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -218,23 +218,17 @@ cdef class MultipleValueForbiddenClause(AbstractForbiddenClause): cdef class ForbiddenEqualsClause(SingleValueForbiddenClause): - """ - A ForbiddenEqualsClause + """A ForbiddenEqualsClause It forbids a value from the value range of a hyperparameter to be *equal to* ``value``. - Example - ------- - - >>> cs = CS.ConfigurationSpace() - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> cs.add_hyperparameters([a]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1] + Forbids the value 2 for the hyperparameter *a* - It forbids the value 2 for the hyperparameter *a* - - >>> forbidden_clause_a = CS.ForbiddenEqualsClause(a, 2) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenEqualsClause + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3]}) + >>> forbidden_clause_a = ForbiddenEqualsClause(cs["a"], 2) >>> cs.add_forbidden_clause(forbidden_clause_a) Forbidden: a == 2 @@ -260,35 +254,29 @@ cdef class ForbiddenEqualsClause(SingleValueForbiddenClause): cdef class ForbiddenInClause(MultipleValueForbiddenClause): def __init__(self, hyperparameter: Dict[str, Union[None, str, float, int]], values: Any) -> None: - """ - A ForbiddenInClause. + """A ForbiddenInClause. It forbids a value from the value range of a hyperparameter to be *in* a collection of ``values``. - Note - ---- - - The forbidden values have to be a subset of the hyperparameter's values. + Forbids the values 2, 3 for the hyperparameter *a* - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> cs.add_hyperparameters([a]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1] - - It forbids the values 2, 3, 4 for the hyperparameter *a* - - >>> forbidden_clause_a = CS.ForbiddenInClause(a, [2, 3]) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenInClause + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3]}) + >>> forbidden_clause_a = ForbiddenInClause(cs['a'], [2, 3]) >>> cs.add_forbidden_clause(forbidden_clause_a) Forbidden: a in {2, 3} + Note + ---- + The forbidden values have to be a subset of the hyperparameter's values. + Parameters ---------- hyperparameter : (:ref:`Hyperparameters`, dict) Hyperparameter on which a restriction will be made + values : Any Collection of forbidden values """ @@ -342,9 +330,9 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent): Additionally, it defines the __ne__() as stated in the documentation from python: - By default, object implements __eq__() by using is, returning NotImplemented - in the case of a false comparison: True if x is y else NotImplemented. - For __ne__(), by default it delegates to __eq__() and inverts the result + By default, object implements __eq__() by using is, returning NotImplemented + in the case of a false comparison: True if x is y else NotImplemented. + For __ne__(), by default it delegates to __eq__() and inverts the result unless it is NotImplemented. """ @@ -428,24 +416,25 @@ cdef class AbstractForbiddenConjunction(AbstractForbiddenComponent): cdef class ForbiddenAndConjunction(AbstractForbiddenConjunction): - """ - A ForbiddenAndConjunction. + """A ForbiddenAndConjunction. The ForbiddenAndConjunction combines forbidden-clauses, which allows to build powerful constraints. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause_a = CS.ForbiddenEqualsClause(a, 2) - >>> forbidden_clause_b = CS.ForbiddenInClause(b, [2]) - >>> forbidden_clause = CS.ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_b) + >>> from ConfigSpace import ( + ... ConfigurationSpace, + ... ForbiddenEqualsClause, + ... ForbiddenInClause, + ... ForbiddenAndConjunction + ... ) + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause_a = ForbiddenEqualsClause(cs["a"], 2) + >>> forbidden_clause_b = ForbiddenInClause(cs["b"], [2]) + >>> + >>> forbidden_clause = ForbiddenAndConjunction(forbidden_clause_a, forbidden_clause_b) + >>> >>> cs.add_forbidden_clause(forbidden_clause) (Forbidden: a == 2 && Forbidden: b in {2}) @@ -583,21 +572,15 @@ cdef class ForbiddenRelation(AbstractForbiddenComponent): cdef class ForbiddenLessThanRelation(ForbiddenRelation): - """ - A ForbiddenLessThan relation between two hyperparameters. + """A ForbiddenLessThan relation between two hyperparameters. The ForbiddenLessThan compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenLessThanRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenLessThanRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause = ForbiddenLessThanRelation(cs['a'], cs['b']) >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a < b @@ -611,6 +594,7 @@ cdef class ForbiddenLessThanRelation(ForbiddenRelation): ---------- left : :ref:`Hyperparameters` left side of the comparison + right : :ref:`Hyperparameters` right side of the comparison """ @@ -626,21 +610,15 @@ cdef class ForbiddenLessThanRelation(ForbiddenRelation): cdef class ForbiddenEqualsRelation(ForbiddenRelation): - """ - A ForbiddenEquals relation between two hyperparameters. + """A ForbiddenEquals relation between two hyperparameters. The ForbiddenEquals compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenEqualsRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenEqualsRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> + >>> forbidden_clause = ForbiddenEqualsRelation(cs['a'], cs['b']) >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a == b @@ -669,21 +647,15 @@ cdef class ForbiddenEqualsRelation(ForbiddenRelation): cdef class ForbiddenGreaterThanRelation(ForbiddenRelation): - """ - A ForbiddenGreaterThan relation between two hyperparameters. + """A ForbiddenGreaterThan relation between two hyperparameters. The ForbiddenGreaterThan compares the values of two hyperparameters. - Example - ------- - - >>> cs = CS.ConfigurationSpace(seed=1) - >>> a = CSH.CategoricalHyperparameter('a', [1,2,3]) - >>> b = CSH.CategoricalHyperparameter('b', [2,5,6]) - >>> cs.add_hyperparameters([a, b]) - [a, Type: Categorical, Choices: {1, 2, 3}, Default: 1, b, Type: ...] - - >>> forbidden_clause = CS.ForbiddenGreaterThanRelation(a, b) + >>> from ConfigSpace import ConfigurationSpace, ForbiddenGreaterThanRelation + >>> + >>> cs = ConfigurationSpace({"a": [1, 2, 3], "b": [2, 5, 6]}) + >>> forbidden_clause = ForbiddenGreaterThanRelation(cs['a'], cs['b']) + >>> >>> cs.add_forbidden_clause(forbidden_clause) Forbidden: a > b diff --git a/ConfigSpace/hyperparameters.pyx b/ConfigSpace/hyperparameters.pyx index 347e397c..f6347ecf 100644 --- a/ConfigSpace/hyperparameters.pyx +++ b/ConfigSpace/hyperparameters.pyx @@ -25,14 +25,13 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - import copy import io # cython: language_level=3 import math import warnings from collections import OrderedDict, Counter -from typing import List, Any, Dict, Union, Set, Tuple, Optional +from typing import List, Any, Dict, Union, Set, Tuple, Optional, Sequence import numpy as np from scipy.stats import truncnorm, beta as spbeta, norm @@ -153,40 +152,40 @@ cdef class Hyperparameter(object): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector """ raise NotImplementedError() - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the hyperparameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -307,19 +306,19 @@ cdef class Constant(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -331,18 +330,18 @@ cdef class Constant(Hyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -471,19 +470,19 @@ cdef class FloatHyperparameter(NumericalHyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -493,21 +492,21 @@ cdef class FloatHyperparameter(NumericalHyperparameter): raise ValueError("Method pdf expects a one-dimensional numpy array") vector = self._inverse_transform(vector) return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -517,7 +516,7 @@ cdef class FloatHyperparameter(NumericalHyperparameter): def get_max_density(self) -> float: """ - Returns the maximal density on the pdf for the parameter (so not + Returns the maximal density on the pdf for the parameter (so not the mode, but the value of the pdf on the mode). """ raise NotImplementedError() @@ -536,7 +535,7 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): def check_default(self, default_value) -> int: raise NotImplemented - + def check_int(self, parameter: int, name: str) -> int: if abs(int(parameter) - parameter) > 0.00000001 and \ type(parameter) is not int: @@ -559,22 +558,22 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): cpdef np.ndarray _transform_vector(self, np.ndarray vector): raise NotImplementedError() - + def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the hyperparameter space (the one specified by the user). - For each hyperparameter type, there is also a method _pdf which + For each hyperparameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -585,33 +584,33 @@ cdef class IntegerHyperparameter(NumericalHyperparameter): is_integer = (np.round(vector) == vector).astype(int) vector = self._inverse_transform(vector) return self._pdf(vector) * is_integer - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector """ raise NotImplementedError() - + def get_max_density(self) -> float: """ - Returns the maximal density on the pdf for the parameter (so not + Returns the maximal density on the pdf for the parameter (so not the mode, but the value of the pdf on the mode). """ raise NotImplementedError() @@ -628,16 +627,10 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): Its values are sampled from a uniform distribution with values from ``lower`` to ``upper``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> uniform_float_hp = CSH.UniformFloatHyperparameter('uni_float', lower=10, - ... upper=100, log = False) - >>> cs.add_hyperparameter(uniform_float_hp) - uni_float, Type: UniformFloat, Range: [10.0, 100.0], Default: 55.0 + >>> from ConfigSpace import UniformFloatHyperparameter + >>> + >>> UniformFloatHyperparameter('u', lower=10, upper=100, log = False) + u, Type: UniformFloat, Range: [10.0, 100.0], Default: 55.0 Parameters ---------- @@ -815,18 +808,18 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -842,7 +835,7 @@ cdef class UniformFloatHyperparameter(FloatHyperparameter): def get_max_density(self) -> float: return 1 / (self.upper - self.lower) - + def get_size(self) -> float: if self.q is None: return np.inf @@ -865,16 +858,10 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): Its values are sampled from a normal distribution :math:`\mathcal{N}(\mu, \sigma^2)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> normal_float_hp = CSH.NormalFloatHyperparameter('normal_float', mu=0, - ... sigma=1, log=False) - >>> cs.add_hyperparameter(normal_float_hp) - normal_float, Type: NormalFloat, Mu: 0.0 Sigma: 1.0, Default: 0.0 + >>> from ConfigSpace import NormalFloatHyperparameter + >>> + >>> NormalFloatHyperparameter('n', mu=0, sigma=1, log=False) + n, Type: NormalFloat, Mu: 0.0 Sigma: 1.0, Default: 0.0 Parameters ---------- @@ -1046,7 +1033,7 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): else: lower=np.ceil(self.lower) upper=np.floor(self.upper) - + return NormalIntegerHyperparameter(self.name, int(np.rint(self.mu)), self.sigma, lower=lower, upper=upper, default_value=int(np.rint(self.default_value)), @@ -1123,23 +1110,23 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) Probability density values of the input vector - """ + """ mu = self.mu sigma = self.sigma if self.lower == None: @@ -1151,7 +1138,7 @@ cdef class NormalFloatHyperparameter(FloatHyperparameter): upper = self._upper a = (lower - mu) / sigma b = (upper - mu) / sigma - + return truncnorm(a, b, loc=mu, scale=sigma).pdf(vector) def get_max_density(self) -> float: @@ -1184,16 +1171,10 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> beta_float_hp = CSH.BetaFloatHyperparameter('beta_float', alpha=3, - ... beta=2, lower=1, upper=4, log=False) - >>> cs.add_hyperparameter(beta_float_hp) - beta_float, Type: BetaFloat, Alpha: 3.0 Beta: 2.0, Range: [1.0, 4.0], Default: 3.0 + >>> from ConfigSpace import BetaFloatHyperparameter + >>> + >>> BetaFloatHyperparameter('b', alpha=3, beta=2, lower=1, upper=4, log=False) + b, Type: BetaFloat, Alpha: 3.0 Beta: 2.0, Range: [1.0, 4.0], Default: 3.0 Parameters ---------- @@ -1223,7 +1204,7 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): # TODO - we cannot use the check_default of UniformFloat (but everything else), # but we still need to overwrite it. Thus, we first just need it not to raise an # error, which we do by setting default_value = upper - lower / 2 to not raise an error, - # then actually call check_default once we have alpha and beta, and are not inside + # then actually call check_default once we have alpha and beta, and are not inside # UniformFloatHP. super(BetaFloatHyperparameter, self).__init__( name, lower, upper, (upper + lower) / 2, q, log, meta) @@ -1232,14 +1213,14 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): if (alpha < 1) or (beta < 1): raise ValueError("Please provide values of alpha and beta larger than or equal to\ 1 so that the probability density is finite.") - - if (self.q is not None) and (self.log is not None) and (default_value is None): + + if (self.q is not None) and (self.log is not None) and (default_value is None): warnings.warn('Logscale and quantization together results in incorrect default values. ' 'We recommend specifying a default value manually for this specific case.') - + self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) - + def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write("%s, Type: BetaFloat, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) @@ -1304,20 +1285,20 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): def check_default(self, default_value: Union[int, float, None]) -> Union[int, float]: # return mode as default # TODO - for log AND quantization together specifially, this does not give the exact right - # value, due to the bounds _lower and _upper being adjusted when quantizing in + # value, due to the bounds _lower and _upper being adjusted when quantizing in # UniformFloat. if default_value is None: if (self.alpha > 1) or (self.beta > 1): normalized_mode = (self.alpha - 1) / (self.alpha + self.beta - 2) - else: + else: # If both alpha and beta are 1, we have a uniform distribution. normalized_mode = 0.5 - + ub = self._inverse_transform(self.upper) lb = self._inverse_transform(self.lower) scaled_mode = normalized_mode * (ub - lb) + lb return self._transform_scalar(scaled_mode) - + elif self.is_legal(default_value): return default_value else: @@ -1328,7 +1309,7 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): q_int = None else: q_int = int(np.rint(self.q)) - + lower = int(np.ceil(self.lower)) upper = int(np.floor(self.upper)) default_value = int(np.rint(self.default_value)) @@ -1353,18 +1334,18 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -1384,17 +1365,17 @@ cdef class BetaFloatHyperparameter(UniformFloatHyperparameter): normalized_mode = 0 elif self.alpha > self.beta: normalized_mode = 1 - else: + else: normalized_mode = 0.5 ub = self._inverse_transform(self.upper) lb = self._inverse_transform(self.lower) scaled_mode = normalized_mode * (ub - lb) + lb - # Since _pdf takes only a numpy array, we have to create the array, + # Since _pdf takes only a numpy array, we have to create the array, # and retrieve the element in the first (and only) spot in the array return self._pdf(np.array([scaled_mode]))[0] - + cdef class UniformIntegerHyperparameter(IntegerHyperparameter): def __init__(self, name: str, lower: int, upper: int, default_value: Union[int, None] = None, q: Union[int, None] = None, log: bool = False, @@ -1405,16 +1386,10 @@ cdef class UniformIntegerHyperparameter(IntegerHyperparameter): Its values are sampled from a uniform distribution with bounds ``lower`` and ``upper``. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> uniform_integer_hp = CSH.UniformIntegerHyperparameter(name='uni_int', lower=10, - ... upper=100, log=False) - >>> cs.add_hyperparameter(uniform_integer_hp) - uni_int, Type: UniformInteger, Range: [10, 100], Default: 55 + >>> from ConfigSpace import UniformIntegerHyperparameter + >>> + >>> UniformIntegerHyperparameter(name='u', lower=10, upper=100, log=False) + u, Type: UniformInteger, Range: [10, 100], Default: 55 Parameters ---------- @@ -1648,20 +1623,20 @@ cdef class UniformIntegerHyperparameter(IntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -1701,16 +1676,10 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): Its values are sampled from a normal distribution :math:`\mathcal{N}(\mu, \sigma^2)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> normal_int_hp = CSH.NormalIntegerHyperparameter(name='normal_int', mu=0, - ... sigma=1, log=False) - >>> cs.add_hyperparameter(normal_int_hp) - normal_int, Type: NormalInteger, Mu: 0 Sigma: 1, Default: 0 + >>> from ConfigSpace import NormalIntegerHyperparameter + >>> + >>> NormalIntegerHyperparameter(name='n', mu=0, sigma=1, log=False) + n, Type: NormalInteger, Mu: 0 Sigma: 1, Default: 0 Parameters ---------- @@ -1785,7 +1754,7 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): self.default_value = self.check_default(default_value) self.normalized_default_value = self._inverse_transform(self.default_value) - + if (self.lower is None) or (self.upper is None): # Since a bound is missing, the pdf cannot be normalized. Working with the unnormalized variant) self.normalization_constant = 1 @@ -1938,7 +1907,7 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): else: neighbors.append(new_value) return neighbors - + def _compute_normalization(self): if self.lower is None: warnings.warn('Cannot normalize the pdf exactly for a NormalIntegerHyperparameter' @@ -1952,20 +1921,20 @@ cdef class NormalIntegerHyperparameter(IntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2010,16 +1979,10 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): Its values are sampled from a beta distribution :math:`Beta(\alpha, \beta)`. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> beta_int_hp = CSH.BetaIntegerHyperparameter('beta_int', alpha=3, - ... beta=2, lower=1, upper=4, log=False) - >>> cs.add_hyperparameter(beta_int_hp) - beta_int, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 4], Default: 3 + >>> from ConfigSpace import BetaIntegerHyperparameter + >>> + >>> BetaIntegerHyperparameter('b', alpha=3, beta=2, lower=1, upper=4, log=False) + b, Type: BetaInteger, Alpha: 3.0 Beta: 2.0, Range: [1, 4], Default: 3 Parameters @@ -2074,7 +2037,7 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): def __repr__(self) -> str: repr_str = io.StringIO() repr_str.write("%s, Type: BetaInteger, Alpha: %s Beta: %s, Range: [%s, %s], Default: %s" % (self.name, repr(self.alpha), repr(self.beta), repr(self.lower), repr(self.upper), repr(self.default_value))) - + if self.log: repr_str.write(", on log-scale") if self.q is not None: @@ -2136,16 +2099,16 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): if default_value is None: # Here, we just let the BetaFloat take care of the default value # computation, and just tansform it accordingly - value = self.bfhp.check_default(None) + value = self.bfhp.check_default(None) value = self._inverse_transform(value) value = self._transform(value) return value - + if self.is_legal(default_value): return default_value else: raise ValueError('Illegal default value {}'.format(default_value)) - + def _sample(self, rs: np.random.RandomState, size: Optional[int] = None ) -> Union[np.ndarray, float]: value = self.bfhp._sample(rs, size=size) @@ -2163,20 +2126,20 @@ cdef class BetaIntegerHyperparameter(UniformIntegerHyperparameter): def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). Optimally, an IntegerHyperparameter should have a corresponding float, which can be utlized for the calls to the probability density function (see e.g. NormalIntegerHyperparameter) - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2197,7 +2160,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): cdef public tuple probabilities cdef list choices_vector cdef set _choices_set - + # TODO add more magic for automated type recognition # TODO move from list to tuple for choices argument def __init__( @@ -2206,7 +2169,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): choices: Union[List[Union[str, float, int]], Tuple[Union[float, int, str]]], default_value: Union[int, float, str, None] = None, meta: Optional[Dict] = None, - weights: Union[List[float], Tuple[float]] = None + weights: Optional[Sequence[Union[int, float]]] = None ) -> None: """ A categorical hyperparameter. @@ -2217,15 +2180,10 @@ cdef class CategoricalHyperparameter(Hyperparameter): it in your own code, see `here _` for further details. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> cat_hp = CSH.CategoricalHyperparameter('cat_hp', choices=['red', 'green', 'blue']) - >>> cs.add_hyperparameter(cat_hp) - cat_hp, Type: Categorical, Choices: {red, green, blue}, Default: red + >>> from ConfigSpace import CategoricalHyperparameter + >>> + >>> CategoricalHyperparameter('c', choices=['red', 'green', 'blue']) + c, Type: Categorical, Choices: {red, green, blue}, Default: red Parameters ---------- @@ -2238,7 +2196,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): meta : Dict, optional Field for holding meta data provided by the user. Not used by the configuration space. - weights: (list[float], optional) + weights: Sequence[int | float] | None = None List of weights for the choices to be used (after normalization) as probabilities during sampling, no negative values allowed """ @@ -2356,7 +2314,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): Creates a categorical parameter with equal weights for all choices This is used for the uniform configspace when sampling configurations in the local search in PiBO: https://openreview.net/forum?id=MMAeCXIa89 - + Returns ---------- CategoricalHyperparameter @@ -2452,7 +2410,7 @@ cdef class CategoricalHyperparameter(Hyperparameter): return self._transform_scalar(vector) except ValueError: return None - + def _inverse_transform(self, vector: Union[None, str, float, int]) -> Union[int, float]: if vector is None: return np.NaN @@ -2509,19 +2467,19 @@ cdef class CategoricalHyperparameter(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in + Computes the probability density function of the parameter in the original parameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) parameter space. Only legal values return a positive probability density, otherwise zero. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2532,24 +2490,24 @@ cdef class CategoricalHyperparameter(Hyperparameter): raise ValueError("Method pdf expects a one-dimensional numpy array") vector = np.array(self._inverse_transform(vector)) return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the parameter in - the transformed (and possibly normalized, depends on the parameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the parameter in + the transformed (and possibly normalized, depends on the parameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform - in the pdf method handles these). For categoricals, each vector gets - transformed to its corresponding index (but in float form). To be - able to retrieve the element corresponding to the index, the float - must be cast to int. - + in the pdf method handles these). For categoricals, each vector gets + transformed to its corresponding index (but in float form). To be + able to retrieve the element corresponding to the index, the float + must be cast to int. + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2591,15 +2549,10 @@ cdef class OrdinalHyperparameter(Hyperparameter): it in your own code, see `here _` for further details. - Example - ------- - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1) - >>> ord_hp = CSH.OrdinalHyperparameter('ordinal_hp', sequence=['10', '20', '30']) - >>> cs.add_hyperparameter(ord_hp) - ordinal_hp, Type: Ordinal, Sequence: {10, 20, 30}, Default: 10 + >>> from ConfigSpace import OrdinalHyperparameter + >>> + >>> OrdinalHyperparameter('o', sequence=['10', '20', '30']) + o, Type: Ordinal, Sequence: {10, 20, 30}, Default: 10 Parameters ---------- @@ -2856,20 +2809,20 @@ cdef class OrdinalHyperparameter(Hyperparameter): def pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in + Computes the probability density function of the hyperparameter in the original hyperparameter space (the one specified by the user). - For each parameter type, there is also a method _pdf which + For each parameter type, there is also a method _pdf which operates on the transformed (and possibly normalized) hyperparameter space. Only legal values return a positive probability density, otherwise zero. The OrdinalHyperparameter is treated as a UniformHyperparameter with regard to its probability density. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) @@ -2878,22 +2831,22 @@ cdef class OrdinalHyperparameter(Hyperparameter): if vector.ndim != 1: raise ValueError("Method pdf expects a one-dimensional numpy array") return self._pdf(vector) - + def _pdf(self, vector: np.ndarray) -> np.ndarray: """ - Computes the probability density function of the hyperparameter in - the transformed (and possibly normalized, depends on the hyperparameter - type) space. As such, one never has to worry about log-normal + Computes the probability density function of the hyperparameter in + the transformed (and possibly normalized, depends on the hyperparameter + type) space. As such, one never has to worry about log-normal distributions, only normal distributions (as the inverse_transform in the pdf method handles these). The OrdinalHyperparameter is treated as a UniformHyperparameter with regard to its probability density. - + Parameters ---------- vector: np.ndarray the (N, ) vector of inputs for which the probability density function is to be computed. - + Returns ---------- np.ndarray(N, ) diff --git a/ConfigSpace/nx/algorithms/dag.py b/ConfigSpace/nx/algorithms/dag.py index 691121c3..0871158d 100644 --- a/ConfigSpace/nx/algorithms/dag.py +++ b/ConfigSpace/nx/algorithms/dag.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- try: - from math import gcd # >= Python 3.9 + # >= Python 3.9 + from math import gcd # type: ignore except ImportError: - from fractions import gcd # < Python 3.9 + # < Python 3.9 + from fractions import gcd # type: ignore import ConfigSpace.nx diff --git a/docs/source/_templates/navbarsearchbox.html b/ConfigSpace/py.typed similarity index 100% rename from docs/source/_templates/navbarsearchbox.html rename to ConfigSpace/py.typed diff --git a/ConfigSpace/read_and_write/json.py b/ConfigSpace/read_and_write/json.py index fcbfed34..b79f26a7 100644 --- a/ConfigSpace/read_and_write/json.py +++ b/ConfigSpace/read_and_write/json.py @@ -319,18 +319,15 @@ def write(configuration_space, indent=2): :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in json format. This string can be written to file. - Example: - - ..code:: python + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.read_and_write import json - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + from ConfigSpace.read_and_write import json as cs_json + + cs = ConfigurationSpace({"a": [1, 2, 3]}) with open('configspace.json', 'w') as f: - f.write(json.write(cs)) + f.write(cs_json.write(cs)) Parameters ---------- @@ -408,25 +405,21 @@ def read(jason_string): """ Create a configuration space definition from a json string. - Example - ------- - - .. testsetup:: json_test + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.read_and_write import json - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + from ConfigSpace.read_and_write import json as cs_json + + cs = ConfigurationSpace({"a": [1, 2, 3]}) + + cs_string = cs_json.write(cs) with open('configspace.json', 'w') as f: - f.write(json.write(cs)) + f.write(cs_string) - .. doctest:: json_test + with open('configspace.json', 'r') as f: + json_string = f.read() + config = cs_json.read(json_string) - >>> from ConfigSpace.read_and_write import json - >>> with open('configspace.json', 'r') as f: - ... jason_string = f.read() - ... config = json.read(jason_string) Parameters ---------- diff --git a/ConfigSpace/read_and_write/pcs.py b/ConfigSpace/read_and_write/pcs.py index 0016650d..3abfa2f9 100644 --- a/ConfigSpace/read_and_write/pcs.py +++ b/ConfigSpace/read_and_write/pcs.py @@ -173,37 +173,31 @@ def read(pcs_string, debug=False): Read in a :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` definition from a pcs file. - Example - ------- - .. testsetup:: pcs_test + .. code:: python from ConfigSpace import ConfigurationSpace - import ConfigSpace.hyperparameters as CSH from ConfigSpace.read_and_write import pcs - cs = ConfigurationSpace() - cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) + + cs = ConfigurationSpace({"a": [1, 2, 3]}) with open('configspace.pcs', 'w') as f: f.write(pcs.write(cs)) - .. doctest:: pcs_test - - >>> from ConfigSpace.read_and_write import pcs - >>> with open('configspace.pcs', 'r') as fh: - ... deserialized_conf = pcs.read(fh) + with open('configspace.pcs', 'r') as f: + deserialized_conf = pcs.read(f) Parameters ---------- pcs_string : str ConfigSpace definition in pcs format - debug : bool + + debug : bool = False Provides debug information. Defaults to False. Returns ------- :py:class:`~ConfigSpace.configuration_space.ConfigurationSpace` The deserialized ConfigurationSpace object - """ configuration_space = ConfigurationSpace() conditions = [] @@ -351,22 +345,15 @@ def write(configuration_space): :class:`~ConfigSpace.configuration_space.ConfigurationSpace` in pcs format. This string can be written to file. - Example - ------- + .. code:: python - .. doctest:: + from ConfigSpace import ConfigurationSpace + from ConfigSpace.read_and_write import pcs - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> from ConfigSpace.read_and_write import pcs - >>> cs = CS.ConfigurationSpace() - >>> cs.add_hyperparameter(CSH.CategoricalHyperparameter('a', choices=[1, 2, 3])) - a, Type: Categorical, Choices: {1, 2, 3}, Default: 1 + cs = ConfigurationSpace({"a": [1, 2, 3]}) - - >>> with open('configspace.pcs', 'w') as fh: - ... fh.write(pcs.write(cs)) - 15 + with open('configspace.pcs', 'w') as fh: + fh.write(pcs.write(cs)) Parameters ---------- diff --git a/MANIFEST.in b/MANIFEST.in index 839b3733..83859389 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include *.md recursive-include ConfigSpace *.pyx *.pxd include LICENSE include pyproject.toml +include ConfigSpace/py.typed diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..bde61c2b --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +# NOTE: Used on linux, limited support outside of Linux +# +# A simple makefile to help with small tasks related to development of ConfigSpace +# These have been configured to only really run short tasks. Longer form tasks +# are usually completed in github actions. + +.PHONY: help install-dev pre-commit clean clean-doc clean-build build docs links publish test + +help: + @echo "Makefile ConfigSpace" + @echo "* install-dev to install all dev requirements and install pre-commit" + @echo "* pre-commit to run the pre-commit check" + @echo "* docs to generate and view the html files" + @echo "* linkcheck to check the documentation links" + @echo "* publish to help publish the current branch to pypi" + @echo "* test to run the tests" + +PYTHON ?= python +CYTHON ?= cython +PYTEST ?= python -m pytest +CTAGS ?= ctags +PRECOMMIT ?= pre-commit +PIP ?= python -m pip +MAKE ?= make + +DIR := "${CURDIR}" +DIST := "${DIR}/dist"" +DOCDIR := "${DIR}/docs" +BUILD := "${DIR}/build" +INDEX_HTML := "file://${DOCDIR}/build/html/index.html" + +install-dev: + $(PIP) install -e ".[dev]" + pre-commit install + +pre-commit: + $(PRECOMMIT) run --all-files + +clean-build: + rm -rf ${BUILD} + +clean-docs: + $(MAKE) -C ${DOCDIR} clean + +clean: clean-build clean-docs + +build: + python setup.py develop + +# Running build before making docs is needed all be it very slow. +# Without doing a full build, the doctests seem to use docstrings from the last compiled build +docs: clean build + $(MAKE) -C ${DOCDIR} html + @echo + @echo "View docs at:" + @echo ${INDEX_HTML} + +links: + $(MAKE) -C ${DOCDIR} linkcheck + +# Publish to testpypi +# Will echo the commands to actually publish to be run to publish to actual PyPi +# This is done to prevent accidental publishing but provide the same conveniences +publish: + $(PIP) install twine + $(PYTHON) -m twine upload --repository testpypi ${DIST}/* + @echo + @echo "Test with the following:" + @echo "* Create a new virtual environment to install the uplaoded distribution into" + @echo "* Run the following:" + @echo + @echo " pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ ConfigSpace" + @echo + @echo "* Run this to make sure it can import correctly, plus whatever else you'd like to test:" + @echo + @echo " python -c 'import ConfigSpace'" + @echo + @echo "Once you have decided it works, publish to actual pypi with" + @echo + @echo " python -m twine upload dist/*" + +test: + $(PYTEST) test diff --git a/README.md b/README.md index 894abfaa..09ed85f0 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,27 @@ Distributed under BSD 3-clause, see LICENSE except all files in the directory ConfigSpace.nx, which are copied from the networkx package and licensed under a BSD license. -The documentation can be found at [https://automl.github.io/ConfigSpace/master/](https://automl.github.io/ConfigSpace/master/). -Further examples can be found in the [SMAC documentation](https://automl.github.io/SMAC3/master/pages/examples/index.html). +The documentation can be found at [https://automl.github.io/ConfigSpace/main/](https://automl.github.io/ConfigSpace/main/). +Further examples can be found in the [SMAC documentation](https://automl.github.io/SMAC3/main/pages/examples/index.html). + + +## Minimum Example + +```python +from ConfigSpace import ConfigurationSpace + +cs = ConfigurationSpace( + name="myspace", + space={ + "a": (0.1, 1.5), # UniformFloat + "b": (2, 10), # UniformInt + "c": ["mouse", "cat", "dog"], # Categorical + }, +) + +configs = cs.sample_configuration(2) +``` + ## Citing the ConfigSpace diff --git a/changelog.md b/changelog.md index df969ebd..e328fc1f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,14 +1,23 @@ +# 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 +* 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` +* FIX #219: Hyperparamters correctly active with diamond-or conditions + # Version 0.5.0 -* Fix #231: Links to the pcs formats. -* Fix #230: Allow Forbidden Clauses with non-numeric values. -* Fix #232: Equality `==` between hyperparameters now considers default values. -* 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 #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/master/User-Guide.html#th-example-placing-priors-on-the-hyperparameters). +* FIX #231: Links to the pcs formats. +* FIX #230: Allow Forbidden Clauses with non-numeric values. +* FIX #232: Equality `==` between hyperparameters now considers default values. +* 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 #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 * Hyperparameters now have a `get_max_density() -> float` to get the greatest value in it's probability distribution function, the probability of the mode of the distriubtion. * `ConfigurationSpace` objects now have a `remove_parameter_priors() -> ConfigurationSpace` to remove any priors diff --git a/docs/Makefile b/docs/Makefile index fd7cff1c..bbeebd67 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,32 +1,24 @@ -# Minimal makefile for Sphinx documentation -# +SPHINXBUILD = sphinx-build +BUILDDIR = build +SPHINXOPTS = +ALLSPHINXOPTS = $(SPHINXOPTS) . -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = ConfigSpace -SOURCEDIR = source -BUILDDIR = build +.PHONY: clean buildapi linkcheck html docs html-noexamples -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +clean: + rm -rf $(BUILDDIR)/* + rm -rf ../build/ -.PHONY: help Makefile +linkcheck: + SPHINX_GALLERY_PLOT=False $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." -buildapi: - sphinx-apidoc -fePTMo apidoc ../ConfigSpace/ - for f in apidoc/*; \ - do \ - sed -i '/Submodules/d' $$f; \ - sed -i '/----------/d' $$f; \ - sed -i 's/ :show-inheritance:/ :show-inheritance:\n :inherited-members:/g' $$f; \ - done; +html: clean linkcheck + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -M doctest $(ALLSPHINXOPTS) $(BUILDDIR)/html || true @echo - @echo "Auto-generation of API documentation finished. " \ - "The generated files are in 'apidoc/'" + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +docs: html linkcheck diff --git a/docs/api/conditions.rst b/docs/api/conditions.rst new file mode 100644 index 00000000..0d068ccf --- /dev/null +++ b/docs/api/conditions.rst @@ -0,0 +1,53 @@ +.. _Conditions: + +Conditions +========== + +ConfigSpace can realize *equal*, *not equal*, *less than*, *greater than* and +*in conditions*. Conditions can be combined by using the conjunctions *and* and +*or*. To see how to use conditions, please take a look at the +:doc:`user guide <../guide>`. + +EqualsCondition +------------------- + +.. autoclass:: ConfigSpace.conditions.EqualsCondition + +.. _NotEqualsCondition: + +NotEqualsCondition +------------------ + +.. autoclass:: ConfigSpace.conditions.NotEqualsCondition + +.. _LessThanCondition: + +LessThanCondition +----------------- + +.. autoclass:: ConfigSpace.conditions.LessThanCondition + + + +GreaterThanCondition +-------------------- + +.. autoclass:: ConfigSpace.conditions.GreaterThanCondition + + +InCondition +----------- + +.. autoclass:: ConfigSpace.conditions.InCondition + + +AndConjunction +-------------- + +.. autoclass:: ConfigSpace.conditions.AndConjunction + + +OrConjunction +------------- + +.. autoclass:: ConfigSpace.conditions.OrConjunction \ No newline at end of file diff --git a/docs/api/configuration.rst b/docs/api/configuration.rst new file mode 100644 index 00000000..d291d0e7 --- /dev/null +++ b/docs/api/configuration.rst @@ -0,0 +1,5 @@ +Configuration +============= + +.. autoclass:: ConfigSpace.configuration_space.Configuration + :members: \ No newline at end of file diff --git a/docs/api/configurationspace.rst b/docs/api/configurationspace.rst new file mode 100644 index 00000000..6b16b0cf --- /dev/null +++ b/docs/api/configurationspace.rst @@ -0,0 +1,5 @@ +ConfigurationSpace +================== + +.. autoclass:: ConfigSpace.configuration_space.ConfigurationSpace + :members: \ No newline at end of file diff --git a/docs/api/forbidden_clauses.rst b/docs/api/forbidden_clauses.rst new file mode 100644 index 00000000..f4be9c80 --- /dev/null +++ b/docs/api/forbidden_clauses.rst @@ -0,0 +1,26 @@ +.. _Forbidden clauses: + +Forbidden Clauses +================= + +ConfigSpace contains *forbidden equal* and *forbidden in clauses*. +The *ForbiddenEqualsClause* and the *ForbiddenInClause* can forbid values to be +sampled from a configuration space if a certain condition is met. The +*ForbiddenAndConjunction* can be used to combine *ForbiddenEqualsClauses* and +the *ForbiddenInClauses*. + +For a further example, please take a look in the :doc:`user guide <../guide>`. + +ForbiddenEqualsClause +--------------------- +.. autoclass:: ConfigSpace.ForbiddenEqualsClause(hyperparameter, value) + + +ForbiddenInClause +----------------- +.. autoclass:: ConfigSpace.ForbiddenInClause(hyperparameter, values) + + +ForbiddenAndConjunction +----------------------- +.. autoclass:: ConfigSpace.ForbiddenAndConjunction(*args) \ No newline at end of file diff --git a/docs/api/hyperparameters.rst b/docs/api/hyperparameters.rst new file mode 100644 index 00000000..e44a485e --- /dev/null +++ b/docs/api/hyperparameters.rst @@ -0,0 +1,115 @@ +.. _Hyperparameters: + +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. + +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 ` and parameters. + +A :func:`~ConfigSpace.api.types.categorical.Categorical` can optionally take weights to define your own custom distribution over the discrete **un-ordered** choices +or you can pass ``ordered=True`` to make it an :class:`~ConfigSpace.hyperparameters.OrdinalHyperparameter`. + +These are all **convenience** functions that construct the more complex :ref:`hyperparameter classes `, *e.g.* :class:`~ConfigSpace.hyperparameters.UniformIntegerHyperparameter`, +which are the underlying complex types which make up the backbone of what's possible. +You may still use these complex classes without any functional difference. + +.. note:: + + The Simple types, `Integer`, `Float` and `Categorical` are just simple functions that construct the more complex underlying types. + +Example usages are shown below each. + +Simple Types +------------ + +Float +^^^^^ + +.. automodule:: ConfigSpace.api.types.float + +Integer +^^^^^^^ + +.. automodule:: ConfigSpace.api.types.integer + +Categorical +^^^^^^^^^^^ + +.. automodule:: ConfigSpace.api.types.categorical + + +.. _Distributions: + +Distributions +------------- +These can be used as part of the ``distribution`` parameter for the basic +:func:`~ConfigSpace.api.types.integer.Integer` and :func:`~ConfigSpace.api.types.float.Float` functions. + +.. automodule:: ConfigSpace.api.distributions + :exclude-members: Distribution + +.. _Advanced_Hyperparameters: + +Advanced Types +-------------- +The full hyperparameters are exposed through the following API points. + + +Integer hyperparameters +^^^^^^^^^^^^^^^^^^^^^^^ + +These can all be constructed with the simple :func:`~ConfigSpace.api.types.integer.Integer` function and +passing the corresponding :ref:`distribution `. + +.. autoclass:: ConfigSpace.hyperparameters.UniformIntegerHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.NormalIntegerHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.BetaIntegerHyperparameter + + + +.. _advanced_float: + +Float hyperparameters +^^^^^^^^^^^^^^^^^^^^^ + +These can all be constructed with the simple :func:`~ConfigSpace.api.types.float` function and +passing the corresponding :ref:`distribution `. + +.. autoclass:: ConfigSpace.hyperparameters.UniformFloatHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.NormalFloatHyperparameter + +.. autoclass:: ConfigSpace.hyperparameters.BetaFloatHyperparameter + + + +.. _advanced_categorical: + +Categorical Hyperparameter +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This can be constructed with the simple form :func:`~ConfigSpace.api.types.categorical` and setting +``ordered=False`` which is the default. + +.. autoclass:: ConfigSpace.hyperparameters.CategoricalHyperparameter + + +Ordinal Hyperparameter +^^^^^^^^^^^^^^^^^^^^^^ +This can be constructed with the simple form :func:`~ConfigSpace.api.types.categorical` and setting +``ordered=True``. + +.. autoclass:: ConfigSpace.hyperparameters.OrdinalHyperparameter + +.. _Other hyperparameters: + +Constant +^^^^^^^^ + +.. autoclass:: ConfigSpace.hyperparameters.Constant diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 00000000..adf4c7f3 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,14 @@ +API ++++ + + +.. toctree:: + :maxdepth: 2 + + configurationspace + configuration + hyperparameters + conditions + forbidden_clauses + serialization + utils \ No newline at end of file diff --git a/docs/api/serialization.rst b/docs/api/serialization.rst new file mode 100644 index 00000000..f57d08e8 --- /dev/null +++ b/docs/api/serialization.rst @@ -0,0 +1,32 @@ +.. _Serialization: + +Serialization +============= + +ConfigSpace offers *json*, *pcs* and *pcs_new* writers/readers. +These classes can serialize and deserialize configuration spaces. +Serializing configuration spaces is useful to share configuration spaces across +experiments, or use them in other tools, for example, to analyze hyperparameter +importance with `CAVE `_. + +.. _json: + +Serialization to JSON +--------------------- + +.. automodule:: ConfigSpace.read_and_write.json + :members: read, write + +.. _pcs_new: + +Serialization with pcs-new (new format) +--------------------------------------- + +.. automodule:: ConfigSpace.read_and_write.pcs_new + :members: read, write + +Serialization with pcs (old format) +----------------------------------- + +.. automodule:: ConfigSpace.read_and_write.pcs + :members: read, write diff --git a/docs/api/utils.rst b/docs/api/utils.rst new file mode 100644 index 00000000..361565ed --- /dev/null +++ b/docs/api/utils.rst @@ -0,0 +1,10 @@ +Utils +===== + +Functions defined in the utils module can be helpful to +develop custom tools that create configurations from a given configuration +space or modify a given configuration space. + +.. automodule:: ConfigSpace.util + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..2dbe1b6c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,39 @@ +import datetime + +import automl_sphinx_theme +from ConfigSpace import __authors__, __version__ + +authors = ", ".join(__authors__) + + +options = { + "copyright": f"""Copyright {datetime.date.today().strftime('%Y')}, {authors}""", + "author": authors, + "version": __version__, + "name": "ConfigSpace", + "html_theme_options": { + "github_url": "https://github.com/automl/automl_sphinx_theme", + "twitter_url": "https://twitter.com/automl_org?lang=de", + }, +} + +# Import conf.py from the automl theme +automl_sphinx_theme.set_options(globals(), options) + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx.ext.githubpages', + 'sphinx.ext.doctest', +] + +autodoc_typehints = "description" +autoclass_content = "both" +autodoc_default_options = { + "inherited-members": True, +} diff --git a/docs/source/User-Guide.rst b/docs/guide.rst similarity index 60% rename from docs/source/User-Guide.rst rename to docs/guide.rst index edf50c6d..f5f37a10 100644 --- a/docs/source/User-Guide.rst +++ b/docs/guide.rst @@ -15,7 +15,7 @@ Assume that we want to use a support vector machine (=SVM) for classification tasks and therefore, we want to optimize its hyperparameters: - :math:`\mathcal{C}`: regularization constant with :math:`\mathcal{C} \in \mathbb{R}` -- ``max_iter``: the maximum number of iterations within the solver with :math:`max_iter \in \mathbb{N}` +- ``max_iter``: the maximum number of iterations within the solver with :math:`max\_iter \in \mathbb{N}` The implementation of the classifier is out of scope and thus not shown. But for further reading about @@ -24,37 +24,31 @@ reading `here `_ or in the `scikit-learn documentation `_. The first step is always to create a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the -hyperparameters and constraints will be added to this object. +:class:`~ConfigSpace.configuration_space.ConfigurationSpace` with the +hyperparameters :math:`\mathcal{C}` and ``max_iter``. ->>> import ConfigSpace as CS ->>> cs = CS.ConfigurationSpace(seed=1234) - -Now, we have to define the hyperparameters :math:`\mathcal{C}` and ``max_iter``. To restrict the search space, we choose :math:`\mathcal{C}` to be a -:class:`~ConfigSpace.hyperparameters.UniformFloatHyperparameter` between -1 and 1. -Furthermore, we choose ``max_iter`` to be an -:class:`~ConfigSpace.hyperparameters.UniformIntegerHyperparameter` . - ->>> import ConfigSpace.hyperparameters as CSH ->>> c = CSH.UniformFloatHyperparameter(name='C', lower=-1, upper=1) ->>> max_iter = CSH.UniformIntegerHyperparameter(name='max_iter', lower=10, upper=100) +:class:`~ConfigSpace.api.types.float` between -1 and 1. +Furthermore, we choose ``max_iter`` to be an :class:`~ConfigSpace.api.types.integer.Integer` . + +>>> from ConfigSpace import ConfigurationSpace +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ +... "C": (-1.0, 1.0), # Note the decimal to make it a float +... "max_iter": (10, 100), +... } +... ) -As last step, we need to add them to the -:class:`~ConfigSpace.configuration_space.ConfigurationSpace`. For demonstration purpose, we sample a configuration from it. -.. doctest:: - - >>> cs.add_hyperparameters([c, max_iter]) - [C, Type: UniformFloat, Range: [-1.0, 1.0], Default: 0.0, max_iter, Type: ...] - >>> cs.sample_configuration() - Configuration(values={ - 'C': -0.6169610992422154, - 'max_iter': 66, - }) - - +>>> cs.sample_configuration() +Configuration(values={ + 'C': -0.6169610992422154, + 'max_iter': 66, +}) + Now, the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object *cs* contains definitions of the hyperparameters :math:`\mathcal{C}` and ``max_iter`` with their @@ -69,16 +63,17 @@ of a parameter can be accessed or modified similar to a python dictionary. >>> conf = cs.sample_configuration() >>> conf['max_iter'] = 42 ->>> conf['max_iter'] +>>> print(conf['max_iter']) 42 + 2nd Example: Categorical hyperparameters and conditions ------------------------------------------------------- The scikit-learn SVM supports different kernels, such as an RBF, a sigmoid, a linear or a polynomial kernel. We want to include them in the configuration space. Since this new hyperparameter has a finite number of values, we use a -:class:`~ConfigSpace.hyperparameters.CategoricalHyperparameter`. +:class:`~ConfigSpace.api.types.categorical`. - ``kernel_type``: with values 'linear', 'poly', 'rbf', 'sigmoid'. @@ -106,43 +101,56 @@ To add conditions on hyperparameters to the configuration space, we first have to insert the new hyperparameters in the ``ConfigSpace`` and in a second step, the conditions on them. ->>> kernel_type = CSH.CategoricalHyperparameter( -... name='kernel_type', choices=['linear', 'poly', 'rbf', 'sigmoid']) ->>> degree = CSH.UniformIntegerHyperparameter( -... 'degree', lower=2, upper=4, default_value=2) ->>> coef0 = CSH.UniformFloatHyperparameter( -... name='coef0', lower=0, upper=1, default_value=0.0) ->>> gamma = CSH.UniformFloatHyperparameter( -... name='gamma', lower=1e-5, upper=1e2, default_value=1, log=True) - +>>> from ConfigSpace import ConfigurationSpace, Categorical, Float, Integer +>>> +>>> kernel_type = Categorical('kernel_type', ['linear', 'poly', 'rbf', 'sigmoid']) +>>> degree = Integer('degree', bounds=(2, 4), default=2) +>>> coef0 = Float('coef0', bounds=(0, 1), default=0.0) +>>> gamma = Float('gamma', bounds=(1e-5, 1e2), default=1, log=True) +>>> +>>> cs = ConfigurationSpace() >>> cs.add_hyperparameters([kernel_type, degree, coef0, gamma]) [kernel_type, Type: Categorical, Choices: {linear, poly, rbf, sigmoid}, ...] First, we define the conditions. Conditions work by constraining a child hyperparameter (the first argument) on its parent hyperparameter (the second argument) being in a certain relation to a value (the third argument). -``CS.EqualsCondition(degree, kernel_type, 'poly')`` expresses that ``degree`` is +``EqualsCondition(degree, kernel_type, 'poly')`` expresses that ``degree`` is constrained on ``kernel_type`` being equal to the value 'poly'. To express constraints involving multiple parameters or values, we can use conjunctions. In the following example, ``cond_2`` describes that ``coef0`` is a valid hyperparameter, if the ``kernel_type`` has either the value 'poly' or 'sigmoid'. ->>> cond_1 = CS.EqualsCondition(degree, kernel_type, 'poly') - ->>> cond_2 = CS.OrConjunction(CS.EqualsCondition(coef0, kernel_type, 'poly'), -... CS.EqualsCondition(coef0, kernel_type, 'sigmoid')) - ->>> cond_3 = CS.OrConjunction(CS.EqualsCondition(gamma, kernel_type, 'rbf'), -... CS.EqualsCondition(gamma, kernel_type, 'poly'), -... CS.EqualsCondition(gamma, kernel_type, 'sigmoid')) - -Again, we add the conditions to the configuration space +>>> from ConfigSpace import EqualsCondition, OrConjunction +>>> +>>> cond_1 = EqualsCondition(degree, kernel_type, 'poly') +>>> +>>> cond_2 = OrConjunction( +... EqualsCondition(coef0, kernel_type, 'poly'), +... EqualsCondition(coef0, kernel_type, 'sigmoid') +... ) +>>> +>>> cond_3 = OrConjunction( +... EqualsCondition(gamma, kernel_type, 'rbf'), +... EqualsCondition(gamma, kernel_type, 'poly'), +... EqualsCondition(gamma, kernel_type, 'sigmoid') +... ) + +In this specific example, you may wish to use the :class:`~ConfigSpace.conditions.InCondition` to express +that ``gamma`` is valid if ``kernel_type in ["rbf", "poly", "sigmoid"]`` which we show for completness + +>>> from ConfigSpace import InCondition +>>> +>>> cond_3 = InCondition(gamma, kernel_type, ["rbf", "poly", "sigmoid"]) + +Finally, we add the conditions to the configuration space >>> cs.add_conditions([cond_1, cond_2, cond_3]) [degree | kernel_type == 'poly', (coef0 | kernel_type == 'poly' || coef0 | ...), ...] .. note:: + ConfigSpace offers a lot of different condition types. For example the :class:`~ConfigSpace.conditions.NotEqualsCondition`, :class:`~ConfigSpace.conditions.LessThanCondition`, @@ -178,11 +186,11 @@ configuration space. First, we add these three new hyperparameters to the configuration space. ->>> penalty = CSH.CategoricalHyperparameter( -... name="penalty", choices=["l1", "l2"], default_value="l2") ->>> loss = CSH.CategoricalHyperparameter( -... name="loss", choices=["hinge", "squared_hinge"], default_value="squared_hinge") ->>> dual = CSH.Constant("dual", "False") +>>> from ConfigSpace import ConfigurationSpace, Categorical, Constant +>>> +>>> penalty = Categorical("penalty", ["l1", "l2"], default="l2") +>>> loss = Categorical("loss", ["hinge", "squared_hinge"], default="squared_hinge") +>>> dual = Constant("dual", "False") >>> cs.add_hyperparameters([penalty, loss, dual]) [penalty, Type: Categorical, Choices: {l1, l2}, Default: l2, ...] @@ -192,27 +200,28 @@ Now, we want to forbid the following hyperparameter combinations: - ``dual`` is False and ``penalty`` is 'l2' and ``loss`` is 'hinge' - ``dual`` is False and ``penalty`` is 'l1' ->>> penalty_and_loss = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(penalty, "l1"), -... CS.ForbiddenEqualsClause(loss, "hinge") -... ) ->>> constant_penalty_and_loss = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(dual, "False"), -... CS.ForbiddenEqualsClause(penalty, "l2"), -... CS.ForbiddenEqualsClause(loss, "hinge") -... ) ->>> penalty_and_dual = CS.ForbiddenAndConjunction( -... CS.ForbiddenEqualsClause(dual, "False"), -... CS.ForbiddenEqualsClause(penalty, "l1") -... ) +>>> from ConfigSpace import ForbiddenEqualsClause, ForbiddenAndConjunction +>>> +>>> penalty_and_loss = ForbiddenAndConjunction( +... ForbiddenEqualsClause(penalty, "l1"), +... ForbiddenEqualsClause(loss, "hinge") +... ) +>>> constant_penalty_and_loss = ForbiddenAndConjunction( +... ForbiddenEqualsClause(dual, "False"), +... ForbiddenEqualsClause(penalty, "l2"), +... ForbiddenEqualsClause(loss, "hinge") +... ) +>>> penalty_and_dual = ForbiddenAndConjunction( +... ForbiddenEqualsClause(dual, "False"), +... ForbiddenEqualsClause(penalty, "l1") +... ) In the last step, we add them to the configuration space object: ->>> cs.add_forbidden_clauses([penalty_and_loss, -... constant_penalty_and_loss, -... penalty_and_dual]) +>>> cs.add_forbidden_clauses([penalty_and_loss, constant_penalty_and_loss, penalty_and_dual]) [(Forbidden: penalty == 'l1' && Forbidden: loss == 'hinge'), ...] + 4th Example Serialization ------------------------- @@ -225,24 +234,16 @@ we can choose between different output formats, such as In this example, we want to store the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object as json file -.. testcode:: - - from ConfigSpace.read_and_write import json - with open('configspace.json', 'w') as fh: - fh.write(json.write(cs)) +>>> from ConfigSpace.read_and_write import json +>>> with open('configspace.json', 'w') as fh: +... fh.write(json.write(cs)) +2828 To read it from file -.. testsetup:: json_block - - from ConfigSpace.read_and_write import json - -.. doctest:: json_block - - >>> with open('configspace.json', 'r') as fh: - ... json_string = fh.read() - ... restored_conf = json.read(json_string) - +>>> with open('configspace.json', 'r') as fh: +... json_string = fh.read() +>>> restored_conf = json.read(json_string) 5th Example: Placing priors on the hyperparameters @@ -252,43 +253,37 @@ If you want to conduct black-box optimization in SMAC (https://arxiv.org/abs/210 Consider the case of optimizing the accuracy of an MLP with three hyperparameters: learning rate [1e-5, 1e-1], dropout [0, 0.99] and activation {Tanh, ReLU}. From prior experience, you believe the optimal learning rate to be around 1e-3, a good dropout to be around 0.25, and the optimal activation function to be ReLU about 80% of the time. This can be represented accordingly: -.. code-block:: python - - import numpy as np - import ConfigSpace.hyperparameters as CSH - from ConfigSpace.configuration_space import ConfigurationSpace - - # convert 10 log to natural log for learning rate, mean 1e-3 - logmean = np.log(1e-3) - # two standard deviations on either side of the mean to cover the search space - logstd = np.log(10.0) - - learning_rate = CSH.NormalFloatHyperparameter(name='learning_rate', lower=1e-5, upper=1e-1, default_value=1e-3, mu=logmean, sigma=logstd, log=True) - dropout = CSH.BetaFloatHyperparameter(name='dropout', lower=0, upper=0.99, default_value=0.25, alpha=2, beta=4, log=False) - activation = CSH.CategoricalHyperparameter(name='activation', choices=['tanh', 'relu'], weights=[0.2, 0.8]) - - cs = ConfigurationSpace() - - cs.add_hyperparameters([learning_rate, dropout, activation]) - # [learning_rate, Type: NormalFloat, Mu: -6.907755278982137 Sigma: 2.302585092994046, Range: [1e-05, 0.1], Default: 0.001, on log-scale, dropout, Type: BetaFloat, Alpha: 2.0 Beta: 4.0, Range: [0.0, 0.99], Default: 0.25, activation, Type: Categorical, Choices: {tanh, relu}, Default: tanh, Probabilities: (0.2, 0.8)] - -To check that your prior makes sense for each hyperparameter, you can easily do so with the __pdf__ method. There, you will see that the probability of the optimal learning rate peaks at 10^-3, and decays as we go further away from it: - -.. code-block:: python - - test_points = np.logspace(-5, -1, 5) - - print(test_points) - # array([1.e-05, 1.e-04, 1.e-03, 1.e-02, 1.e-01]) +>>> import numpy as np +>>> from ConfigSpace import ConfigurationSpace, Float, Categorical, Beta, Normal +>>> +>>> # convert 10 log to natural log for learning rate, mean 1e-3 +>>> # with two standard deviations on either side of the mean to cover the search space +>>> logmean = np.log(1e-3) +>>> logstd = np.log(10.0) +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ +... "lr": Float('lr', bounds=(1e-5, 1e-1), default=1e-3, log=True, distribution=Normal(logmean, logstd)), +... "dropout": Float('dropout', bounds=(0, 0.99), default=0.25, distribution=Beta(alpha=2, beta=4)), +... "activation": Categorical('activation', ['tanh', 'relu'], weights=[0.2, 0.8]), +... } +... ) +>>> print(cs) +Configuration space object: + Hyperparameters: + activation, Type: Categorical, Choices: {tanh, relu}, Default: tanh, Probabilities: (0.2, 0.8) + dropout, Type: BetaFloat, Alpha: 2.0 Beta: 4.0, Range: [0.0, 0.99], Default: 0.25 + lr, Type: NormalFloat, Mu: -6.907755278982137 Sigma: 2.302585092994046, Range: [1e-05, 0.1], Default: 0.001, on log-scale + + +To check that your prior makes sense for each hyperparameter, you can easily do so with the ``__pdf__`` method. There, you will see that the probability of the optimal learning rate peaks at 10^-3, and decays as we go further away from it: + +>>> test_points = np.logspace(-5, -1, 5) +>>> print(test_points) +[1.e-05 1.e-04 1.e-03 1.e-02 1.e-01] The pdf function accepts an (N, ) numpy array as input. -.. code-block:: python - - test_points_pdf = learning_rate.pdf(test_points) - print(test_points_pdf) - # array([0.02456573, 0.11009594, 0.18151753, 0.11009594, 0.02456573]) - - - - +>>> cs['lr'].pdf(test_points) +array([0.02456573, 0.11009594, 0.18151753, 0.11009594, 0.02456573]) diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 00000000..e8c0f3ac Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index ede161bd..00000000 --- a/docs/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..fff21dac --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,137 @@ +Welcome to ConfigSpace's documentation! +======================================= + +.. toctree:: + :hidden: + :maxdepth: 2 + + quickstart + guide + api/index + +ConfigSpace is a simple python package to manage configuration spaces for +`algorithm configuration `_ and +`hyperparameter optimization `_ tasks. +It includes various modules to translate between different text formats for +configuration space descriptions. + +ConfigSpace is often used in AutoML tools such as `SMAC3`_, `BOHB`_ or +`auto-sklearn`_. To read more about our group and projects, visit our homepage +`AutoML.org `_. + +This documentation explains how to use ConfigSpace and demonstrates its features. +In the :doc:`quickstart`, you will see how to set up a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +and add hyperparameters of different types to it. +Besides containing hyperparameters, a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` can contain constraints such as conditions and forbidden clauses. +Those are introduced in the :doc:`user guide `. + +Furthermore, in the :ref:`serialization section `, it will be +explained how to serialize a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` for later usage. + +.. _SMAC3: https://github.com/automl/SMAC3 +.. _BOHB: https://github.com/automl/HpBandSter +.. _auto-sklearn: https://github.com/automl/auto-sklearn + + + +Get Started +----------- + +Create a simple :class:`~ConfigSpace.configuration_space.ConfigurationSpace` and then sample a :class:`~ConfigSpace.configuration_space.Configuration` from it! + +>>> from ConfigSpace import ConfigurationSpace +>>> +>>> cs = ConfigurationSpace({ +... "myfloat": (0.1, 1.5), # Uniform Float +... "myint": (2, 10), # Uniform Integer +... "species": ["mouse", "cat", "dog"], # Categorical +... }) +>>> configs = cs.sample_configuration(2) + + +Use :mod:`~ConfigSpace.api.types.float`, :mod:`~ConfigSpace.api.types.integer` +or :mod:`~ConfigSpace.api.types.categorical` to customize how sampling is done! + +>>> from ConfigSpace import ConfigurationSpace, Integer, Float, Categorical, Normal +>>> cs = ConfigurationSpace( +... name="myspace", +... seed=1234, +... space={ +... "a": Float("a", bounds=(0.1, 1.5), distribution=Normal(1, 10), log=True), +... "b": Integer("b", bounds=(2, 10)), +... "c": Categorical("c", ["mouse", "cat", "dog"], weights=[2, 1, 1]), +... }, +... ) +>>> cs.sample_configuration(2) +[Configuration(values={ + 'a': 0.17013149799713567, + 'b': 5, + 'c': 'dog', +}) +, Configuration(values={ + 'a': 0.5476203000512754, + 'b': 9, + 'c': 'mouse', +}) +] + +Maximum flexibility with conditionals, see :ref:`forbidden clauses ` and :ref:`conditionals ` for more info. + +>>> from ConfigSpace import Categorical, ConfigurationSpace, EqualsCondition, Float +... +>>> cs = ConfigurationSpace(seed=1234) +... +>>> c = Categorical("c1", items=["a", "b"]) +>>> f = Float("f1", bounds=(1.0, 10.0)) +... +>>> # A condition where `f` is only active if `c` is equal to `a` when sampled +>>> cond = EqualsCondition(f, c, "a") +... +>>> # Add them explicitly to the configuration space +>>> cs.add_hyperparameters([c, f]) +[c1, Type: Categorical, Choices: {a, b}, Default: a, f1, Type: UniformFloat, Range: [1.0, 10.0], Default: 5.5] + +>>> cs.add_condition(cond) +f1 | c1 == 'a' + + + +Installation +============ + +*ConfigSpace* requires Python 3.7 or higher. + +*ConfigSpace* can be installed with *pip*: + +.. code:: bash + + pip install ConfigSpace + +If installing from source, the *ConfigSpace* package requires *numpy*, *cython* +and *pyparsing*. Additionally, a functioning C compiler is required. + +On Ubuntu, the required compiler tools and Python headers can be installed with: + +.. code:: bash + + sudo apt-get install build-essential python3 python3-dev + +When using Anaconda/Miniconda, the compiler has to be installed with: + +.. code:: bash + + conda install gxx_linux-64 gcc_linux-64 + + +Citing the ConfigSpace +====================== + +.. code:: + + @article{ + title = {BOAH: A Tool Suite for Multi-Fidelity Bayesian Optimization & Analysis of Hyperparameters}, + author = {M. Lindauer and K. Eggensperger and M. Feurer and A. Biedenkapp and J. Marben and P. Müller and F. Hutter}, + journal = {arXiv:1908.06756 {[cs.LG]}}, + date = {2019}, + } + diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 05d6e0ab..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=ConfigSpace - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..bad7efa8 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,228 @@ +Quickstart +========== + +A :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +is a data structure to describe the configuration space of an algorithm to tune. +Possible hyperparameter types are numerical, categorical, conditional and ordinal hyperparameters. + +AutoML tools, such as `SMAC3`_ and `BOHB`_ are using the configuration space +module to sample hyperparameter configurations. +Also, `auto-sklearn`_, an automated machine learning toolkit, which frees the +machine learning user from algorithm selection and hyperparameter tuning, +makes heavy use of the ConfigSpace package. + +This simple quickstart tutorial will show you, how to set up your own +:class:`~ConfigSpace.configuration_space.ConfigurationSpace`, and will demonstrate +what you can realize with it. This :ref:`Basic Usage` will include the following: + +- Create a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- Define a simple :ref:`hyperparameter ` and its range +- Change its :ref:`distributions `. + +The :ref:`Advanced Usage` will cover: + +- Creating two sets of possible model configs, using :ref:`Conditions` +- Create two subspaces from these and add them to a parent :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- Turn these configs into actual models! + +These will not show the following and you should refer to the :doc:`user guide ` for more: + +- Add :ref:`Forbidden clauses` +- Add :ref:`Conditions` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` +- :ref:`Serialize ` the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` + + +.. _Basic Usage: + +Basic Usage +----------- + +We take a look at a simple +`ridge regression `_, +which has only one floating hyperparameter :math:`\alpha`. + +The first step is always to create a +:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the +hyperparameters and constraints will be added to this object. + +>>> from ConfigSpace import ConfigurationSpace, Float +>>> +>>> cs = ConfigurationSpace( +... seed=1234, +... space={ "alpha": (0.0, 1.0) } +... ) + +The hyperparameter :math:`\alpha` is chosen to have floating point values from 0 to 1. +For demonstration purpose, we sample a configuration from the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. + +>>> config = cs.sample_configuration() +>>> print(config) +Configuration(values={ + 'alpha': 0.1915194503788923, +}) + + +You can use this configuration just like you would a regular old python dictionary! + +>>> for key, value in config.items(): +... print(key, value) +alpha 0.1915194503788923 + +And that's it! + + +.. _Advanced Usage: + +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. + + +We're going to create a config space that will let us correctly build a randomly selected model. + +.. code:: python + + class ModelA: + + def __init__(self, alpha: float): + """ + Parameters + ---------- + alpha: float + Some value between 0 and 1 + """ + ... + + class ModelB: + + def __init__(self, kernel: str, kernel_floops: int | None = None): + """ + Parameters + ---------- + kernel: "rbf" or "flooper" + If the kernel is set to "flooper", kernel_floops must be set. + + kernel_floops: int | None = None + Floop factor of the kernel + """ + ... + + +First, lets start with building the two individual subspaces where for ``A``, we want to sample alpha from a normal distribution and for ``B`` we have the conditioned parameter and we slightly weight one kernel over another. + +.. code:: python + + from ConfigSpace import ConfigSpace, Categorical, Integer, Float, Normal + + class ModelA: + + def __init__(self, alpha: float): + ... + + @staticmethod + def space(self) -> ConfigSpace: + return ConfigurationSpace({ + "alpha": Float("alpha", bounds=(0, 1), distribution=Normal(mu=0.5, sigma=0.2) + }) + + class ModelB: + + def __init__(self, kernel: str, kernel_floops: int | None = None): + ... + + @staticmethod + def space(self) -> ConfigSpace: + cs = ConfigurationSpace( + { + "kernel": Categorical("kernel", ["rbf", "flooper"], default="rbf", weights=[.75, .25]), + "kernel_floops": Integer("kernel_floops", bounds=(1, 10)), + } + ) + + # We have to make sure "kernel_floops" is only active when the kernel is "floops" + cs.add_condition(EqualsCondition(cs_B["kernel_floops"], cs_B["kernel"], "flooper")) + + return cs + + +Finally, we need add these two a parent space where we condition each subspace to only be active depending on a **parent**. +We'll have the default configuration be ``A`` but we put more emphasis when sampling on ``B`` + +.. code:: python + + cs = ConfigurationSpace( + seed=1234, + space={ + "model": Categorical("model", ["A", "B"], default="A", weights=[1, 2]), + } + ) + + # We set the prefix and delimiter to be empty string "" so that we don't have to do + # any extra parsing once sampling + cs.add_configuration_space( + prefix="", + delimiter="", + configuration_space=ModelA.space(), + parent_hyperparameter={"parent": cs["model"], "value": "A"}, + ) + + cs.add_configuration_space( + prefix="", + delimiter="", + configuration_space=modelB.space(), + parent_hyperparameter={"parent": cs["model"], "value": "B"} + ) + +And that's it! + +However for completness, lets examine how this works by first sampling from our config space. + +.. code:: python + + configs = cs.sample_configuration(4) + print(configs) + + # [Configuration(values={ + # 'model': 'A', + # 'alpha': 0.7799758081188035, + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'flooper', + # 'kernel_floops': 8, + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'rbf', + # }) + # , Configuration(values={ + # 'model': 'B', + # 'kernel': 'rbf', + # }) + # ] + +We can see the three different kinds of models we have, our basic ``A`` model as well as our ``B`` model +with the two kernels. + +Next, we do some processing of these configs to generate valid params to pass to these models + +.. code:: python + + models = [] + + for config in configs: + model_type = config.pop("model") + + model = ModelA(**config) if model_type == "A" else ModelB(**config) + + models.append(model) + + +To continue reading, visit the :doc:`user guide ` section. There are +more information about hyperparameters, as well as an introduction to the +powerful concepts of :ref:`Conditions` and :ref:`Forbidden clauses`. + +.. _SMAC3: https://github.com/automl/SMAC3 +.. _BOHB: https://github.com/automl/HpBandSter +.. _auto-sklearn: https://github.com/automl/auto-sklearn diff --git a/docs/source/API-Doc.rst b/docs/source/API-Doc.rst deleted file mode 100644 index 130e3080..00000000 --- a/docs/source/API-Doc.rst +++ /dev/null @@ -1,193 +0,0 @@ -API-Documentation -+++++++++++++++++ - -ConfigurationSpace -================== - -.. autoclass:: ConfigSpace.configuration_space.ConfigurationSpace - :members: - -Configuration -============= - -.. autoclass:: ConfigSpace.configuration_space.Configuration - :members: - -.. _Hyperparameters: - -Hyperparameters -=============== - -ConfigSpace contains integer, float, categorical, as well as ordinal -hyperparameters. Integer and float hyperparameter can be sampled from a uniform -or normal distribution. Example usages are shown in the -:doc:`quickstart `. - -3.1 Integer hyperparameters ---------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.UniformIntegerHyperparameter - -.. autoclass:: ConfigSpace.hyperparameters.NormalIntegerHyperparameter - - - -3.2 Float hyperparameters -------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.UniformFloatHyperparameter - -.. autoclass:: ConfigSpace.hyperparameters.NormalFloatHyperparameter - - - -.. _Categorical hyperparameters: - -3.3 Categorical hyperparameters -------------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.CategoricalHyperparameter - - -3.4 OrdinalHyperparameters --------------------------- - -.. autoclass:: ConfigSpace.hyperparameters.OrdinalHyperparameter - -.. _Other hyperparameters: - -3.5 Constant ------------- - -.. autoclass:: ConfigSpace.hyperparameters.Constant - - -.. _Conditions: - -Conditions -========== - -ConfigSpace can realize *equal*, *not equal*, *less than*, *greater than* and -*in conditions*. Conditions can be combined by using the conjunctions *and* and -*or*. To see how to use conditions, please take a look at the -:doc:`user guide `. - -4.1 EqualsCondition -------------------- - -.. autoclass:: ConfigSpace.conditions.EqualsCondition - -.. _NotEqualsCondition: - -4.2 NotEqualsCondition ----------------------- - -.. autoclass:: ConfigSpace.conditions.NotEqualsCondition - -.. _LessThanCondition: - -4.3 LessThanCondition ---------------------- - -.. autoclass:: ConfigSpace.conditions.LessThanCondition - - - -4.4 GreaterThanCondition ------------------------- - -.. autoclass:: ConfigSpace.conditions.GreaterThanCondition - - -4.5 InCondition ---------------- - -.. autoclass:: ConfigSpace.conditions.InCondition - - -4.6 AndConjunction ------------------- - -.. autoclass:: ConfigSpace.conditions.AndConjunction - - -4.7 OrConjunction ------------------ - -.. autoclass:: ConfigSpace.conditions.OrConjunction - - -.. _Forbidden clauses: - -Forbidden Clauses -================= - -ConfigSpace contains *forbidden equal* and *forbidden in clauses*. -The *ForbiddenEqualsClause* and the *ForbiddenInClause* can forbid values to be -sampled from a configuration space if a certain condition is met. The -*ForbiddenAndConjunction* can be used to combine *ForbiddenEqualsClauses* and -the *ForbiddenInClauses*. - -For a further example, please take a look in the :doc:`user guide `. - -5.1 ForbiddenEqualsClause -------------------------- -.. autoclass:: ConfigSpace.ForbiddenEqualsClause(hyperparameter, value) - - -5.2 ForbiddenInClause ---------------------- -.. autoclass:: ConfigSpace.ForbiddenInClause(hyperparameter, values) - - -5.3 ForbiddenAndConjunction ---------------------------- -.. autoclass:: ConfigSpace.ForbiddenAndConjunction(*args) - - -.. _Serialization: - -Serialization -============= - -ConfigSpace offers *json*, *pcs* and *pcs_new* writers/readers. -These classes can serialize and deserialize configuration spaces. -Serializing configuration spaces is useful to share configuration spaces across -experiments, or use them in other tools, for example, to analyze hyperparameter -importance with `CAVE `_. - -.. _json: - -6.1 Serialization to JSON -------------------------- - -.. automodule:: ConfigSpace.read_and_write.json - :members: read, write - :undoc-members: - -.. _pcs_new: - -6.2 Serialization with pcs-new (new format) -------------------------------------------- - -.. automodule:: ConfigSpace.read_and_write.pcs_new - :members: read, write - :undoc-members: - -6.3 Serialization with pcs (old format) ---------------------------------------- - -.. automodule:: ConfigSpace.read_and_write.pcs - :members: read, write - :undoc-members: - -Utils -===== - -Functions defined in the utils module can be helpful to -develop custom tools that create configurations from a given configuration -space or modify a given configuration space. - -.. automodule:: ConfigSpace.util - :members: - :undoc-members: diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html deleted file mode 100644 index 244d7a5d..00000000 --- a/docs/source/_templates/layout.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "!layout.html" %} - -{# Custom CSS overrides #} -{# set bootswatch_css_custom = ['_static/my-styles.css'] #} - -{# Add github banner (from: https://github.com/blog/273-github-ribbons). #} -{% block header %} - {{ super() }} - - - {% endblock %} - diff --git a/docs/source/_templates/navbar.html b/docs/source/_templates/navbar.html deleted file mode 100644 index 48f75c56..00000000 --- a/docs/source/_templates/navbar.html +++ /dev/null @@ -1,51 +0,0 @@ - diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 53e47365..00000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -import sphinx_bootstrap_theme -import ConfigSpace -from datetime import datetime - - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('../..')) - -# -- Project information ----------------------------------------------------- - - -project = 'ConfigSpace' -copyright = "2014-{}, ".format(datetime.now().year) + ", ".join(ConfigSpace.__authors__) -author = ', '.join(ConfigSpace.__authors__) - -# The short X.Y version -version = ConfigSpace.__version__ -# The full version, including alpha/beta/rc tags -release = '' - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.githubpages', - 'sphinx.ext.doctest', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'bootstrap' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - # Insert options - # Navigation bar title. (Default: ``project`` value) - # 'navbar_title': "Title", - - # Tab name for entire site. (Default: "Site") - 'navbar_site_name': "Site", - - # A list of tuples containting pages to link to. The value should - # be in the form [(name, page), ..] - 'navbar_links': [ - ('Start', 'index'), - ('Quickstart', 'quickstart'), - ('User Guide', 'User-Guide'), - ('API', 'API-Doc'), - ], - # Render the next and previous page links in navbar. (Default: true) - 'navbar_sidebarrel': False, - - # Render the current pages TOC in the navbar. (Default: true) - 'navbar_pagenav': False, - - # Tab name for the current pages TOC. (Default: "Page") - 'navbar_pagenav_name': "On this page", - - # Global TOC depth for "site" navbar tab. (Default: 1) - # Switching to -1 shows all levels. - 'globaltoc_depth': 2, - - # Include hidden TOCs in Site navbar? - # - # Note: If this is "false", you cannot have mixed ``:hidden:`` and - # non-hidden ``toctree`` directives in the same page, or else the build - # will break. - # - # Values: "true" (default) or "false" - 'globaltoc_includehidden': "True", - - # HTML navbar class (Default: "navbar") to attach to
element. - # For black navbar, do "navbar navbar-inverse" - 'navbar_class': "navbar", - - # Fix navigation bar to top of page? - # Values: "true" (default) or "false" - 'navbar_fixed_top': "true", - - # Location of link to source. - # Options are "nav" (default), "footer" or anything else to exclude. - 'source_link_position': "footer", - - # Bootswatch (http://bootswatch.com/) theme. - # - # Options are nothing with "" (default) or the name of a valid theme - # such as "amelia" or "cosmo". - 'bootswatch_theme': "cosmo", - - # Choose Bootstrap version. - # Values: "3" (default) or "2" (in quotes) - 'bootstrap_version': "3", -} - -html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. - - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -html_sidebars = {'**': ['localtoc.html', 'searchbox.html']} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'ConfigSpacedoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'ConfigSpace.tex', 'ConfigSpace Documentation', - [author], 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'configspace', 'ConfigSpace Documentation', - ', '.join(author), 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'ConfigSpace', 'ConfigSpace Documentation', - ', '.join(author), 'ConfigSpace', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Extension configuration ------------------------------------------------- -# Show init as well as moduledoc -autoclass_content = 'both' diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 389fe60a..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,101 +0,0 @@ -.. ConfigSpace documentation master file, created by - sphinx-quickstart on Mon Jul 23 18:06:55 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to ConfigSpace's documentation! -======================================= - -ConfigSpace is a simple python package to manage configuration spaces for -`algorithm configuration `_ and -`hyperparameter optimization `_ tasks. -It includes various modules to translate between different text formats for -configuration space descriptions. - -ConfigSpace is often used in AutoML tools such as `SMAC3`_, `BOHB`_ or -`auto-sklearn`_. To read more about our group and projects, visit our homepage -`AutoML.org `_. - -This documentation explains how to use ConfigSpace and demonstrates its features. -In the :doc:`quickstart`, you will see how to set up a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` -and add hyperparameters of different types to it. -Besides containing hyperparameters, a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -can contain constraints such as conditions and forbidden clauses. -Those are introduced in the :doc:`user guide `. - -Furthermore, in the :ref:`serialization section `, it will be -explained how to serialize a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` for later usage. - -.. _SMAC3: https://github.com/automl/SMAC3 -.. _BOHB: https://github.com/automl/HpBandSter -.. _auto-sklearn: https://github.com/automl/auto-sklearn - -Basic usage - -.. doctest:: - - >>> import ConfigSpace as CS - >>> import ConfigSpace.hyperparameters as CSH - >>> cs = CS.ConfigurationSpace(seed=1234) - >>> a = CSH.UniformIntegerHyperparameter('a', lower=10, upper=100, log=False) - >>> b = CSH.CategoricalHyperparameter('b', choices=['red', 'green', 'blue']) - >>> cs.add_hyperparameters([a, b]) - [a, Type: UniformInteger, Range: [10, 100], Default: 55,...] - >>> cs.sample_configuration() - Configuration(values={ - 'a': 27, - 'b': 'green', - }) - - -Installation -============ - -*ConfigSpace* requires Python 3.7 or higher. - -*ConfigSpace* can be installed with *pip*: - -.. code:: bash - - pip install ConfigSpace - -The *ConfigSpace* package requires *numpy*, *cython* and *pyparsing*. -Additionally, a functioning C compiler is required. - -On Ubuntu, the required compiler tools and Python headers can be installed with: - -.. code:: bash - - sudo apt-get install build-essential python3 python3-dev - -When using Anaconda/Miniconda, the compiler has to be installed with: - -.. code:: bash - - conda install gxx_linux-64 gcc_linux-64 - - -Citing the ConfigSpace -====================== - -.. code:: bibtex - - @article{ - title = {BOAH: A Tool Suite for Multi-Fidelity Bayesian Optimization & Analysis of Hyperparameters}, - author = {M. Lindauer and K. Eggensperger and M. Feurer and A. Biedenkapp and J. Marben and P. Müller and F. Hutter}, - journal = {arXiv:1908.06756 {[cs.LG]}}, - date = {2019}, - } - - -Contents -======== - -.. toctree:: - :maxdepth: 2 - - quickstart.rst - User-Guide.rst - API-Doc.rst diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst deleted file mode 100644 index b704e93d..00000000 --- a/docs/source/quickstart.rst +++ /dev/null @@ -1,73 +0,0 @@ -Quickstart -========== - -A :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -is a data structure to describe the configuration space of an algorithm to tune. -Possible hyperparameter types are numerical, categorical, conditional and ordinal hyperparameters. - -AutoML tools, such as `SMAC3`_ and `BOHB`_ are using the configuration space -module to sample hyperparameter configurations. -Also, `auto-sklearn`_, an automated machine learning toolkit, which frees the -machine learning user from algorithm selection and hyperparameter tuning, -makes heavy use of the ConfigSpace package. - -This simple quickstart tutorial will show you, how to set up your own -:class:`~ConfigSpace.configuration_space.ConfigurationSpace`, and will demonstrate -what you can realize with it. This tutorial will include the following steps: - -- Create a :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- Define :ref:`hyperparameters ` and their value ranges -- Add the :ref:`hyperparameters ` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- (Optional) Add :ref:`Conditions` to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` -- (Optional) Add :ref:`Forbidden clauses` -- (Optional) :ref:`Serialize ` the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` - -We will show those steps in an exemplary way by creating a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` for ridge regression. -Note that the topics adding constraints, adding forbidden clauses and -serialization are explained in the :doc:`user guide `. - - -Basic Usage ------------ - -We take a look at a simple -`ridge regression `_, -which has only one floating hyperparameter :math:`\alpha`. - -The first step is always to create a -:class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. All the -hyperparameters and constraints will be added to this object. - ->>> import ConfigSpace as CS ->>> cs = CS.ConfigurationSpace(seed=1234) - -The hyperparameter :math:`\alpha` is chosen to have floating point values from 0 to 1. - ->>> import ConfigSpace.hyperparameters as CSH ->>> alpha = CSH.UniformFloatHyperparameter(name='alpha', lower=0, upper=1) - -We add it to the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. - ->>> cs.add_hyperparameter(alpha) -alpha, Type: UniformFloat, Range: [0.0, 1.0], Default: 0.5 - -For demonstration purpose, we sample a configuration from the :class:`~ConfigSpace.configuration_space.ConfigurationSpace` object. - -.. doctest:: - - >>> cs.sample_configuration() - Configuration(values={ - 'alpha': 0.1915194503788923, - }) - - -And that's it. - -To continue reading, visit the :doc:`user guide ` section. There are -more information about hyperparameters, as well as an introduction to the -powerful concepts of :ref:`Conditions` and :ref:`Forbidden clauses`. - -.. _SMAC3: https://github.com/automl/SMAC3 -.. _BOHB: https://github.com/automl/HpBandSter -.. _auto-sklearn: https://github.com/automl/auto-sklearn diff --git a/setup.py b/setup.py index d477cabf..15773e0b 100644 --- a/setup.py +++ b/setup.py @@ -9,17 +9,29 @@ # Helper functions def read_file(fname): """Get contents of file from the modules directory""" - return open(os.path.join(os.path.dirname(__file__), fname), encoding='utf-8').read() + return open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8").read() def get_version(fname): """Get the module version""" - with open(fname, encoding='utf-8') as file_handle: + with open(fname, encoding="utf-8") as file_handle: return file_handle.readlines()[-1].split()[-1].strip("\"'") +def get_authors(fname): + """Get the authors""" + with open(fname, "r") as f: + content = f.read() + + return [ + line.replace(",", "").replace('"', "").replace(" ", "") # Remove noise + for line in content.split("\n") + if line.startswith(" ") # Lines with space + ] + + class BuildExt(build_ext): - """ build_ext command for use when numpy headers are needed. + """build_ext command for use when numpy headers are needed. SEE tutorial: https://stackoverflow.com/questions/2379898 SEE fix: https://stackoverflow.com/questions/19919905 """ @@ -27,90 +39,94 @@ class BuildExt(build_ext): def finalize_options(self): build_ext.finalize_options(self) import numpy + self.include_dirs.append(numpy.get_include()) # Configure setup parameters -MODULE_NAME = 'ConfigSpace' -MODULE_URL = 'https://github.com/automl/ConfigSpace' +MODULE_NAME = "ConfigSpace" +MODULE_URL = "https://github.com/automl/ConfigSpace" SHORT_DESCRIPTION = ( - 'Creation and manipulation of parameter configuration spaces for ' - 'automated algorithm configuration and hyperparameter tuning.' + "Creation and manipulation of parameter configuration spaces for " + "automated algorithm configuration and hyperparameter tuning." ) KEYWORDS = ( - 'algorithm configuration hyperparameter optimization empirical ' - 'evaluation black box' + "algorithm configuration hyperparameter optimization empirical " + "evaluation black box" ) -LICENSE = 'BSD 3-clause' -PLATS = ['Linux'] -AUTHORS = ', '.join(["Matthias Feurer", "Katharina Eggensperger", - "Syed Mohsin Ali", "Christina Hernandez Wunsch", - "Julien-Charles Levesque", "Jost Tobias Springenberg", "Philipp Mueller" - "Marius Lindauer", "Jorn Tuyls"]), -AUTHOR_EMAIL = 'feurerm@informatik.uni-freiburg.de' +LICENSE = "BSD 3-clause" +PLATS = ["Linux", "Windows", "Mac"] + +AUTHOR_EMAIL = "feurerm@informatik.uni-freiburg.de" TEST_SUITE = "pytest" -SETUP_REQS = ['numpy', 'cython'] -INSTALL_REQS = ['numpy', 'cython', 'pyparsing', 'scipy'] -MIN_PYTHON_VERSION = '>=3.7' -CLASSIFIERS = ['Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Development Status :: 4 - Beta', - 'Natural Language :: English', - 'Intended Audience :: Developers', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: POSIX :: Linux', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - 'Topic :: Scientific/Engineering', - 'Topic :: Software Development'] +SETUP_REQS = ["numpy", "cython"] +INSTALL_REQS = ["numpy", "cython", "pyparsing", "scipy", "typing_extensions"] +MIN_PYTHON_VERSION = ">=3.7" +CLASSIFIERS = [ + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Development Status :: 4 - Beta", + "Natural Language :: English", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering", + "Topic :: Software Development", +] # These do not really change the speed of the benchmarks COMPILER_DIRECTIVES = { - 'boundscheck': False, - 'wraparound': False, + "boundscheck": False, + "wraparound": False, } -EXTENSIONS = [Extension('ConfigSpace.hyperparameters', - sources=['ConfigSpace/hyperparameters.pyx']), - Extension('ConfigSpace.forbidden', - sources=['ConfigSpace/forbidden.pyx']), - Extension('ConfigSpace.conditions', - sources=['ConfigSpace/conditions.pyx']), - Extension('ConfigSpace.c_util', - sources=['ConfigSpace/c_util.pyx']), - Extension('ConfigSpace.util', - sources=['ConfigSpace/util.pyx']), - Extension('ConfigSpace.configuration_space', - sources=['ConfigSpace/configuration_space.pyx'])] +EXTENSIONS = [ + Extension( + "ConfigSpace.hyperparameters", sources=["ConfigSpace/hyperparameters.pyx"] + ), + Extension("ConfigSpace.forbidden", sources=["ConfigSpace/forbidden.pyx"]), + Extension("ConfigSpace.conditions", sources=["ConfigSpace/conditions.pyx"]), + Extension("ConfigSpace.c_util", sources=["ConfigSpace/c_util.pyx"]), + Extension("ConfigSpace.util", sources=["ConfigSpace/util.pyx"]), + Extension( + "ConfigSpace.configuration_space", + sources=["ConfigSpace/configuration_space.pyx"], + ), +] for e in EXTENSIONS: e.cython_directives = COMPILER_DIRECTIVES extras_reqs = { - "test": [ + "dev": [ "pytest>=4.6", "mypy", "pre-commit", "pytest-cov", + "automl_sphinx_theme>=0.1.11", ], - "docs": ["sphinx", "sphinx-gallery", "sphinx_bootstrap_theme", "numpydoc"], } setup( name=MODULE_NAME, - version=get_version('ConfigSpace/__version__.py'), - cmdclass={'build_ext': BuildExt}, + version=get_version("ConfigSpace/__version__.py"), + cmdclass={"build_ext": BuildExt}, url=MODULE_URL, description=SHORT_DESCRIPTION, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", ext_modules=EXTENSIONS, long_description=read_file("README.md"), license=LICENSE, platforms=PLATS, - author=AUTHORS, + author=get_authors("ConfigSpace/__authors__.py"), author_email=AUTHOR_EMAIL, test_suite=TEST_SUITE, setup_requires=SETUP_REQS, diff --git a/docs/source/_templates/searchbox.html b/test/test_api/__init__.py similarity index 100% rename from docs/source/_templates/searchbox.html rename to test/test_api/__init__.py diff --git a/test/test_api/test_hp_construction.py b/test/test_api/test_hp_construction.py new file mode 100644 index 00000000..862033eb --- /dev/null +++ b/test/test_api/test_hp_construction.py @@ -0,0 +1,252 @@ +"""Testing the API for creating the different hyperparameters avialable. + +These are intentionally verbose and using all parameters to ensure they maintain equality. +""" +from __future__ import annotations + +from ConfigSpace import Beta, Categorical, Float, Integer, Normal, Uniform +from ConfigSpace.hyperparameters import (BetaFloatHyperparameter, + BetaIntegerHyperparameter, + CategoricalHyperparameter, + NormalFloatHyperparameter, + NormalIntegerHyperparameter, + OrdinalHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter) + + +def test_uniform_int() -> None: + """ + Expects + ------- + * Should create an identical UniformIntegerHyperparameter + """ + expected = UniformIntegerHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + default=5, + distribution=Uniform(), + q=2, + log=True, + meta={"a": "b"}, + ) + assert a == expected + assert a.meta == expected.meta + + +def test_normal_int() -> None: + """ + Expects + ------- + * Should create an identical NormalIntegerHyperparameter with Normal distribution + """ + expected = NormalIntegerHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + mu=5, + sigma=1, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + distribution=Normal(mu=5, sigma=1), + default=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_beta_int() -> None: + """ + Expects + ------- + * Should create an identical BetaIntegerHyperparameter with a BetaDistribution + """ + expected = BetaIntegerHyperparameter( + "hp", + lower=2, + upper=10, + alpha=1, + beta=2, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Integer( + "hp", + bounds=(2, 10), + distribution=Beta(alpha=1, beta=2), + default=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_uniform_float() -> None: + """ + Expects + ------- + * Should create an identical UniformFloatHyperparameter with a UniformDistribution + """ + expected = UniformFloatHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Uniform(), + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_normal_float() -> None: + """ + Expects + ------- + * Should create an identical NormalFloatHyperparameter with a Normal distribution + """ + expected = NormalFloatHyperparameter( + "hp", + lower=2, + upper=10, + mu=5, + sigma=2, + default_value=5, + q=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Normal(mu=5, sigma=2), + q=2, + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_beta_float() -> None: + """ + Expects + ------- + * Should create an identical BetaFloatHyperparameter with a BetaDistribution + """ + expected = BetaFloatHyperparameter( + "hp", + lower=2, + upper=10, + default_value=5, + alpha=1, + beta=2, + log=True, + meta={"a": "b"}, + ) + + a = Float( + "hp", + bounds=(2, 10), + default=5, + distribution=Beta(alpha=1, beta=2), + log=True, + meta={"a": "b"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_categorical() -> None: + """ + Expects + ------- + * Should create an identical CategoricalHyperparameter + """ + expected = CategoricalHyperparameter( + "hp", + choices=["a", "b", "c"], + default_value="a", + weights=[1, 2, 3], + meta={"hello": "world"}, + ) + + a = Categorical( + "hp", + items=["a", "b", "c"], + default="a", + weights=[1, 2, 3], + ordered=False, + meta={"hello": "world"}, + ) + + assert a == expected + assert a.meta == expected.meta + + +def test_ordinal() -> None: + """ + Expects + ------- + * Should create an identical CategoricalHyperparameter + """ + expected = OrdinalHyperparameter( + "hp", + sequence=["a", "b", "c"], + default_value="a", + meta={"hello": "world"}, + ) + + a = Categorical( + "hp", + items=["a", "b", "c"], + default="a", + ordered=True, + meta={"hello": "world"}, + ) + + assert a == expected + assert a.meta == expected.meta diff --git a/test/test_configspace_from_dict.py b/test/test_configspace_from_dict.py new file mode 100644 index 00000000..6784117f --- /dev/null +++ b/test/test_configspace_from_dict.py @@ -0,0 +1,115 @@ +"""This file tests the easy api to create configspaces +Configspace({ + "constant": "hello", + "depth": (1, 10), + "lr": (0.1, 1.0), + "categorical": ["a", "b"], +}) +""" +from __future__ import annotations + +from typing import Any + +import pytest + +from ConfigSpace import ConfigurationSpace +from ConfigSpace.hyperparameters import ( + CategoricalHyperparameter, + Constant, + Hyperparameter, + NormalFloatHyperparameter, + UniformFloatHyperparameter, + UniformIntegerHyperparameter, +) + + +@pytest.mark.parametrize( + "value, expected", + [ + # Constant is just a value + ( + "a", + Constant("hp", "a"), + ), + ( + 1337, + Constant("hp", 1337), + ), + ( + 1, + Constant("hp", 1), + ), + ( + 1.0, + Constant("hp", 1.0), + ), + # Boundaries are tuples of length 2, int for Integer + ( + (1, 10), + UniformIntegerHyperparameter("hp", 1, 10), + ), + ( + (-5, 5), + UniformIntegerHyperparameter("hp", -5, 5), + ), + # Boundaries are tuples of length 2, float for Float + ( + (1.0, 10.0), + UniformFloatHyperparameter("hp", 1.0, 10.0), + ), + ( + (-5.5, 5.5), + UniformFloatHyperparameter("hp", -5.5, 5.5), + ), + # Lists are categorical + ( + ["a"], + CategoricalHyperparameter("hp", ["a"]), + ), + ( + ["a", "b"], + CategoricalHyperparameter("hp", ["a", "b"]), + ), + # Something that is already a hyperparameter will stay a hyperparameter + ( + NormalFloatHyperparameter("hp", mu=1, sigma=10), + NormalFloatHyperparameter("hp", mu=1, sigma=10), + ) + # We can't use {} for categoricals as it becomes undeterministic + # Hence we give Categorical the tuple() syntax and not support + # Ordinal + ], +) +def test_individual_hyperparameters(value: Any, expected: Hyperparameter) -> None: + """ + Expects + ------- + * Creating a constant with the dictionary easy api will insert a Constant + into it's hyperparameters + """ + cs = ConfigurationSpace({"hp": value}) + assert cs["hp"] == expected + + +@pytest.mark.parametrize( + "value", [(1, 10, 999), (10,), (1.0, 10.0, 999.0), (1.0,), tuple()] +) +def test_bad_tuple_in_dict(value: tuple[int, ...]) -> None: + """ + Expects + ------- + * Using a tuple that doesn't have 2 values will raise an error + """ + with pytest.raises(ValueError): + ConfigurationSpace({"hp": value}) + + +@pytest.mark.parametrize("value", [[]]) +def test_bad_categorical(value: list) -> None: + """ + Expects + ------- + * Using an empty list will raise an error + """ + with pytest.raises(ValueError): + ConfigurationSpace({"hp": value}) diff --git a/test/test_hyperparameters.py b/test/test_hyperparameters.py index b53cd6e1..ca8186f9 100644 --- a/test/test_hyperparameters.py +++ b/test/test_hyperparameters.py @@ -667,9 +667,9 @@ def test_normalfloat_get_max_density(self): c2 = NormalFloatHyperparameter("logparam", lower=np.exp( 0), upper=np.exp(10), mu=3, sigma=2, log=True) c3 = NormalFloatHyperparameter("param", lower=0, upper=0.5, mu=-1, sigma=0.2) - self.assertEqual(c1.get_max_density(), 0.2138045617479014) - self.assertAlmostEqual(c2.get_max_density(), 0.2138045617479014) - self.assertAlmostEqual(c3.get_max_density(), 25.932522722334905) + self.assertAlmostEqual(c1.get_max_density(), 0.2138045617479014, places=9) + self.assertAlmostEqual(c2.get_max_density(), 0.2138045617479014, places=9) + self.assertAlmostEqual(c3.get_max_density(), 25.932522722334905, places=9) def test_betafloat(self): # TODO test non-equality