Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add configspace conversion #832

Merged
merged 52 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
3883fc2
Add configspace conversion
Delaunay Mar 16, 2022
e2dbae9
Add precision
Delaunay Mar 17, 2022
fa2663d
Isort tweak
Delaunay Mar 17, 2022
4c6811d
Remove S type from visitor
Delaunay Mar 17, 2022
442c95e
Fix typo
Delaunay Mar 17, 2022
061708d
Add default_value case
Delaunay Mar 17, 2022
acd5a46
Add ConfigSpace as an optional dependency
Delaunay Mar 17, 2022
12c656b
Add singledispatch to make to_orionspace work for any registered types
Delaunay Mar 18, 2022
746970b
Add docstrings
Delaunay Mar 18, 2022
3f77f71
-
Delaunay Mar 18, 2022
674c84e
Document typevar
Delaunay Mar 18, 2022
6bf7518
__all__ without T
Delaunay Mar 18, 2022
a14d8c6
Revert
Delaunay Mar 18, 2022
64555ed
Update src/orion/algo/space/configspace.py
Delaunay Mar 19, 2022
c7b5a46
Try to fix doc generation bypassing typing
Delaunay Mar 21, 2022
c9136fd
Merge branch 'issue_586' of github.com:Delaunay/orion into issue_586
Delaunay Mar 21, 2022
18f7395
Mock uninstalled imports, handle py3.6 for configspace
Delaunay Mar 22, 2022
72002cc
Fix tests for 3.6
Delaunay Mar 24, 2022
c083bff
Merge branch 'develop' of github.com:Epistimio/orion into issue_586
Delaunay Mar 24, 2022
8c7dac9
Remove python3.6 hacks
Delaunay Mar 24, 2022
a5a4ac6
Use single dispatch
Delaunay Mar 24, 2022
e4f6c56
Missing return
Delaunay Mar 24, 2022
5b8a2c0
Update src/orion/algo/space/configspace.py
Delaunay Mar 25, 2022
107e5bd
Add unsupported ConfigSpace priors
Delaunay Mar 25, 2022
47216cc
Merge branch 'issue_586' of github.com:Delaunay/orion into issue_586
Delaunay Mar 25, 2022
fd58aa1
Add unsupported prior
Delaunay Mar 28, 2022
8e8b47b
-
Delaunay Mar 28, 2022
a8f0425
-
Delaunay Mar 28, 2022
8abd4fb
-
Delaunay Mar 28, 2022
eb57c58
Update src/orion/algo/space/configspace.py
Delaunay Mar 28, 2022
fec66c3
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
ebc9886
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
0087ef8
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
016a6d2
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
5ff9d8e
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
59bdad2
Update src/orion/algo/space/configspace.py
Delaunay Mar 28, 2022
0709f8d
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
17af4ce
Update src/orion/algo/space/__init__.py
Delaunay Mar 28, 2022
38c1ae5
-
Delaunay Mar 28, 2022
d818079
-
Delaunay Mar 28, 2022
534ab5e
-
Delaunay Mar 28, 2022
192f31c
Update src/orion/algo/space/configspace.py
Delaunay Mar 28, 2022
a104255
Update src/orion/algo/space/configspace.py
Delaunay Mar 28, 2022
f60858a
Update src/orion/algo/space/configspace.py
Delaunay Mar 28, 2022
37c580e
Merge branch 'develop' of github.com:Epistimio/orion into issue_586
Delaunay Mar 28, 2022
65b6f7d
-
Delaunay Mar 28, 2022
a14a47b
Update tests/unittests/algo/test_configspace.py
Delaunay Apr 2, 2022
0a99146
Merge branch 'develop' of github.com:Epistimio/orion into issue_586
Delaunay Apr 11, 2022
9de07ad
-
Delaunay Apr 12, 2022
5648f03
-
Delaunay Apr 12, 2022
06adba3
Merge branch 'develop' into issue_586
bouthilx Jul 27, 2022
bd9680f
black
bouthilx Jul 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"dask": ["dask[complete]"],
"track": ["track @ git+https://github.com/Delaunay/track"],
"profet": ["emukit", "GPy", "torch", "pybnn"],
"configspace": ["ConfigSpace"]
}
extras_require["all"] = list(set(sum(extras_require.values(), [])))

Expand Down
46 changes: 46 additions & 0 deletions src/orion/algo/space.py → src/orion/algo/space/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import copy
Delaunay marked this conversation as resolved.
Show resolved Hide resolved
import logging
import numbers
from typing import Generic, TypeVar

import numpy
from scipy.stats import distributions
Expand Down Expand Up @@ -67,6 +68,47 @@ def __repr__(self):
return "..."


def _to_snake_case(name: str) -> str:
"""Transform a class name ``MyClassName`` to snakecase ``my_class_name``"""
frags = []

frag = []
for c in name:
if c.isupper() and frag:
frags.append("".join(frag).lower())
frag = []

frag.append(c)

if frag:
frags.append("".join(frag).lower())

return "_".join(frags)


T = TypeVar("T")


class Visitor(Generic[T]):
def visit(self, dim: "Dimension") -> T:
Delaunay marked this conversation as resolved.
Show resolved Hide resolved
return dim.visit(self)

def dimension(self, dim: "Dimension") -> T:
Delaunay marked this conversation as resolved.
Show resolved Hide resolved
pass

def real(self, dim: "Dimension") -> T:
pass

def integer(self, dim: "Dimension") -> T:
pass

def categorical(self, dim: "Dimension") -> T:
pass

def fidelity(self, dim: "Dimension") -> T:
pass


class Dimension:
"""Base class for search space dimensions.

Expand Down Expand Up @@ -337,6 +379,10 @@ def cardinality(self):
"""
return numpy.inf

def visit(self, visitor: Visitor[T]) -> T:
"""Execute a visitor on the given dimension"""
return getattr(visitor, _to_snake_case(self.__class__.__name__))(self)


def _is_numeric_array(point):
"""Test whether a point is numerical object or an array containing only numerical objects"""
Expand Down
212 changes: 212 additions & 0 deletions src/orion/algo/space/configspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from math import log10
from typing import Optional

from orion.algo.space import Categorical, Dimension, Integer, Real, Space, Visitor

try:
from ConfigSpace import ConfigurationSpace
from ConfigSpace.hyperparameters import (
CategoricalHyperparameter,
FloatHyperparameter,
Hyperparameter,
IntegerHyperparameter,
NormalFloatHyperparameter,
NormalIntegerHyperparameter,
UniformFloatHyperparameter,
UniformIntegerHyperparameter,
)

IMPORT_ERROR = None

except ImportError as err:
IMPORT_ERROR = err

IntegerHyperparameter = object()
FloatHyperparameter = object()
ConfigurationSpace = object()
Hyperparameter = object()
UniformFloatHyperparameter = object()
NormalFloatHyperparameter = object()
UniformIntegerHyperparameter = object()
NormalIntegerHyperparameter = object()
CategoricalHyperparamete = object()


def _qantization(dim: Dimension) -> float:
"""Convert precision to the quantization factor"""
if dim.precision:
return 10 ** (-dim.precision)
return None


class ToConfigSpace(Visitor[Optional[Hyperparameter]]):
"""Convert an Orion space into a configspace"""

def __init__(self) -> None:
if IMPORT_ERROR is not None:
raise IMPORT_ERROR

def dimension(self, dim: Dimension) -> None:
"""Raise an error if the visitor is called on an abstract class"""
raise NotImplementedError()

def real(self, dim: Dimension) -> Optional[FloatHyperparameter]:
"""Convert a real dimension into a configspace equivalent"""
if dim.prior_name in ("reciprocal", "uniform"):
a, b = dim._args

return UniformFloatHyperparameter(
name=dim.name,
lower=a,
upper=b,
default_value=dim.default_value,
q=_qantization(dim),
log=dim.prior_name == "reciprocal",
)

if dim.prior_name in ("normal", "norm"):
a, b = dim._args

return NormalFloatHyperparameter(
name=dim.name,
mu=a,
sigma=b,
default_value=dim.default_value,
q=_qantization(dim),
log=False,
lower=dim.low if hasattr(dim, "low") else None,
upper=dim.high if hasattr(dim, "high") else None,
)

return

def integer(self, dim: Dimension) -> Optional[IntegerHyperparameter]:
"""Convert a integer dimension into a configspace equivalent"""
if dim.prior_name in ("int_uniform", "int_reciprocal"):
a, b = dim._args

return UniformIntegerHyperparameter(
name=dim.name,
lower=a,
upper=b,
default_value=dim.default_value,
q=_qantization(dim),
log=dim.prior_name == "int_reciprocal",
)

if dim.prior_name in ("norm", "normal"):
a, b = dim._args

return NormalIntegerHyperparameter(
name=dim.name,
mu=a,
sigma=b,
default_value=dim.default_value,
q=_qantization(dim),
log=False,
lower=dim.low if hasattr(dim, "low") else None,
upper=dim.high if hasattr(dim, "high") else None,
)

return None

def categorical(self, dim: Dimension) -> Optional[CategoricalHyperparameter]:
"""Convert a categorical dimension into a configspace equivalent"""
return CategoricalHyperparameter(
name=dim.name,
choices=dim.categories,
weights=dim._probs,
)

def fidelity(self, dim: Dimension) -> None:
"""Ignores fidelity dimension as configspace does not have an equivalent"""
return None

def space(self, space: Space) -> ConfigurationSpace:
Copy link
Collaborator

@lebrice lebrice Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving this logic to a Space handler of to_configspace:

@to_configspace.register(Space)
<this code>

"""Convert orion space to configspace"""
cspace = ConfigurationSpace()
dims = []

for _, dim in space.items():
bouthilx marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for _, dim in space.items():
for dim in space.values():

cdim = self.visit(dim)

if cdim:
dims.append(cdim)

cspace.add_hyperparameters(dims)
return cspace


Copy link
Collaborator

@lebrice lebrice Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly suggest making this a singedispatch callable like you did with to_oriondim, and cutting-pasting the methods of the ToConfigSpace class into handler functions for each supported dimension type below.

Suggested change
@singledispatch

def toconfigspace(space: Space) -> ConfigurationSpace:
"""Convert orion space to configspace

Notes
-----
``ConfigurationSpace`` will set its own default values
if not set inside ``Space``

"""
conversion = ToConfigSpace()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the "root" of the singledispatch callable, and just return a NotImplementedError for the given argument.

return conversion.space(space)


def tooriondim(dim: Hyperparameter) -> Dimension:
"""Convert a config space hyperparameter to an orion dimension"""

if isinstance(dim, CategoricalHyperparameter):
choices = {k: w for k, w in zip(dim.choices, dim.probabilities)}
return Categorical(dim.name, choices)

klass = Integer
args = []
kwargs = dict(
# NOTE: Config space always has a config value
# so orion-space would get it as well
default_value=dim.default_value
)

if isinstance(dim, (UniformFloatHyperparameter, UniformIntegerHyperparameter)):
if isinstance(dim, UniformFloatHyperparameter):
klass = Real
else:
kwargs["precision"] = int(-log10(dim.q)) if dim.q else 4

dist = "uniform"
args.append(dim.lower)
args.append(dim.upper)

if dim.log:
dist = "reciprocal"

if isinstance(dim, (NormalFloatHyperparameter, NormalIntegerHyperparameter)):
if isinstance(dim, NormalFloatHyperparameter):
klass = Real
else:
kwargs["precision"] = int(-log10(dim.q)) if dim.q else 4

dist = "norm"
args.append(dim.mu)
args.append(dim.sigma)

if dim.lower:
kwargs["low"] = dim.lower
kwargs["high"] = dim.upper

return klass(dim.name, dist, *args, **kwargs)


def toorionspace(cspace: ConfigurationSpace) -> Space:
"""Convert from orion space to configspace

Notes
-----
``ConfigurationSpace`` will set default values for each dimensions of ``Space``

"""
space = Space()

for _, cdim in cspace.get_hyperparameters_dict().items():
Delaunay marked this conversation as resolved.
Show resolved Hide resolved
odim = tooriondim(cdim)
space.register(odim)

return space
45 changes: 45 additions & 0 deletions tests/unittests/algo/test_configspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it could probably be useful to also test the transformed dimensions TransformedDimension, ReshapedDimension, etc. that are passed to the constructors of the algorithms.

If you choose to support these kind of dimensions, you can use this kind of pattern to get the delegate function based on the type of the wrapped dimension, instead of the type of the wrapper:

@to_configspace.register(TransformedDimension)
def _transformed_to_configspace(dim: TransformedDimension) -> HyperParameter:
    unwrapped_dim = dim.original_dimension
    # Get the handler for the unwrapped dim:
    handler_fn = to_configspace.dispatch(type(unwrapped_dim))    
    converted_unwrapped_dim = handler_fn(unwrapped_dim)
    # do something with it? Like re-add the transformation somehow?
    return converted_unwrapped_dim  # just drop the transformation for now.

Copy link
Member

@bouthilx bouthilx Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like most of the dispatch would be done this way since algorithms receive a transformed space. The use of strings to map the conversion methods may be more convenient than dispatch because of this.

Copy link
Collaborator

@lebrice lebrice Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of strings to map the conversion methods may be more convenient than dispatch because of this.

That's incorrect: https://github.com/Epistimio/orion/pull/832/files#diff-eec91bf300bfcaf8b606f6046f67669737948168c0e7a6b00133c91965f5ddd1R104

The name of the class is used for dispatching. The issue is the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed: #832 (comment)


from orion.algo.space import Categorical, Fidelity, Integer, Real, Space
from orion.algo.space.configspace import IMPORT_ERROR, toconfigspace, toorionspace


@pytest.mark.skipif(IMPORT_ERROR, reason="Running without ConfigSpace")
def test_orion_configspace():
space = Space()

space.register(Integer("r1i", "reciprocal", 1, 6))
space.register(Integer("u1i", "uniform", -3, 6))
space.register(Integer("u2i", "uniform", -3, 6))
space.register(Integer("u3i", "uniform", -3, 6, default_value=2))

space.register(Real("r1f", "reciprocal", 1, 6))
space.register(Real("u1f", "uniform", -3, 6))
space.register(Real("u2f", "uniform", -3, 6))

space.register(Categorical("c1", ("asdfa", 2)))
space.register(Categorical("c2", dict(a=0.2, b=0.8)))
space.register(Fidelity("f1", 1, 9, 3))

space.register(Real("n1", "norm", 0.9, 0.1, precision=6))
space.register(Real("n2", "norm", 0.9, 0.1, precision=None))
space.register(Real("n3", "norm", 0.9, 0.1))
bouthilx marked this conversation as resolved.
Show resolved Hide resolved

newspace = toconfigspace(space)

roundtrip = toorionspace(newspace)

for k, original in space.items():
# ConfigSpace does not have a fidelity dimension
if k == "f1":
continue

converted = roundtrip[k]

# Orion space did not have default values
# but ConfigSpace always set them
if not original.default_value:
converted._default_value = None

assert type(original) == type(converted)
assert original == converted