-
Notifications
You must be signed in to change notification settings - Fork 50
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool stuff!
Can you try using singledispatch callables for the to/from conversion functions?
It would be a lot more intuitive than using the Visitor pattern, since your visitor doesn't have any state anyway:
from ConfigSpace import ConfigurationSpace
from orion.algo.space import Space
from functools import singledispatch
# ConfigSpace -> Orion
@singledispatch
def to_orion(config_space: ConfigSpace) -> Space:
""" converts the given config space to an Orion space. """
raise NotImplementedError(f"Don't have a handler for converting spaces of type {type(config_space)}.")
@to_orion.register(UniformFloatHyperparameter)
@to_orion.register(NormalFloatHyperparameter)
def _convert_uniform_float(config_space: UniformFloatHyperparameter) -> Real:
prior_type = "normal" if isinstance(config_space, NormalFloatHyperParameter) else "uniform"
... # (shared logic for both classes)
@to_orion.register
def _convert_foobar(config_space: FooBarHyperParameter) -> Dimension:
...
... # other handlers
# Same thing for Orion -> ConfigSpace
@singledispatch
def to_configspace(orion_space: Space) -> ConfigurationSpace:
""" Converts the given Orion thingy to a ConfigSpace thingy. """
raise NotImplementedError(f"Don't know how to convert {orion_space} to an equivalent from ConfigSpace")
@to_configspace.register
def _real_to_uniform(dim: Real) -> UniformFloatHyperparameter:
... # convert from Orion to ConfigSpace
Another major benefit is that we could then extend the conversions externally (even from within ConfigSpace if you want!) by just registering new handlers for new dimensions when they are added.
That specific visitor does not have a states, future one could have. |
What kind of state could it have?
I'm curious: having considered both, you say that you prefer the Visitor to singledispatch, correct? So what advantages do you see in the Visitor pattern vs dispatching? From my experience, this kind of |
The visitor does not use
NB: Visitor are more than just for the conversion they can be used to implement any new features outside of |
I forgot to mention in my initial example that you can use the same handler for more than one type, by cascading the I like how you made the conversion functions singledispatch callables, that might come in handy in the future! |
Co-authored-by: Fabrice Normandin <fabrice.normandin@gmail.com>
c3eecbb
to
adc6bdc
Compare
weights=dim._probs, | ||
) | ||
|
||
def fidelity(self, dim: Fidelity) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incoherent with the type signature of the SpaceConverter[HyperParameter]
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump
T = TypeVar("T") | ||
|
||
|
||
class SpaceConverter(Generic[T]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really see the point of having a SpaceConverter
. Is it to force an external library to provide conversion functions functions for the given types of spaces?
Also, this generic signature isn't really respected by the ToConfigSpace
class, right? Since it doesn't support Fidelity
-type spaces, it returns None
in that case, and so it becomes a SpaceConverter[Optional[HyperParameter]]
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bump
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users can look at the visitor and see clearly all the methods he is expected to override to implement a valid feature.
I guess this answers my first question. However, it's clear from the fidelity
example that you actually don't want to enforce that every type of dimension has to be supported by subclasses. Therefore, it's not actually an interface, if all its members are optional. It's only use, as far as I can see, is to be a list of names of methods that "can" be implemented by subclasses. It also doesn't have any state.
NB: Visitor are more than just for the conversion they can be used to implement any new features outside of Dimensions
Users could implement their own sample/serialization/... using a Visitor.
At this point, why not just let the user code create a function that does what they want to do, e.g. serialize / print / do something for the types of Dimension
of Orion they want to support?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair the fidelity is the only exception. I don't see how the conversions could be usable if they do not support all other type of dimensions. I think the abstract class is the clearest way to express the interface expected for contributors. The only thing needed would be to clarify the exception for the fidelity.
Co-authored-by: Fabrice Normandin <fabrice.normandin@gmail.com>
Co-authored-by: Fabrice Normandin <fabrice.normandin@gmail.com>
Co-authored-by: Fabrice Normandin <fabrice.normandin@gmail.com>
Co-authored-by: Xavier Bouthillier <xavier.bouthillier@gmail.com>
def convert_dimension(self, dimension: Dimension) -> T: | ||
"""Call the dimension conversion handler""" | ||
return getattr(self, _to_snake_case(type(dimension).__name__))(dimension) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is essentially a singledispatch callable in disguise: You have handlers for each type below, and you dispatch based on the "type" of the argument, indirectly.
However, having an implicit dependency on the name of the dimension classes is very brittle, and hard to keep track of. Could you make this more explicit somehow?
I also don't think this should be inside a class, since there isn't (and most likely won't be) any state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what happens if a new type of dimension is added, and there is no method with that name? This will raise an AttributeError
, but you'd ideally like a NotImplementedError
with an informative message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we make it an abstract class then non-implemented methods would already raise an informative message during code development. It's true there is no state so making it a class isn't super useful for functionality's sake but I think it is nevertheless useful to make the interface more clear. The other solution would be to have a module for these functions only (without the methods for the conversion the other way around).
In any case, we should make it such that if the dev forgets to write some methods it fails with a clear message at execution or some tests automatically detects this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this should use the property type
to work with transformed dimensions. Looks like we should also have tests for converting transformed spaces.
def convert_dimension(self, dimension: Dimension) -> T: | |
"""Call the dimension conversion handler""" | |
return getattr(self, _to_snake_case(type(dimension).__name__))(dimension) | |
def convert_dimension(self, dimension: Dimension) -> T: | |
"""Call the dimension conversion handler""" | |
return getattr(self, _to_snake_case(dimension.type))(dimension) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as:
- we don't dispatch based on the names of classes, and
- the solution is consistent with itself and the base class,
then I don't have a problem with using an ABC and marking all the methods with@abstractmethod
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true there is no state so making it a class isn't super useful for functionality's sake but I think it is nevertheless useful to make the interface more clear. The other solution would be to have a module for these functions only (without the methods for the conversion the other way around).
Since the abstract base class would only have static methods, this is actually a good use-case for Protocols:
from typing_extensions import Protocol
T = TypeVar("T")
class SpaceConverter(Protocol[T]):
integer: Callable[[Integer], T]
real: Callable[[Real], T]
fidelity: Callable[[Fidelity], T | None]
This way, if you have a module like so:
# nevergrad/conversion.py (or something like that)
def integer(dim: Integer) -> NeverGradStuff:
...
def real(dim: Real) -> NeverGradReal:
...
def fidelity(dim: Fidelity -> None:
return None # Nevergrad doesn't support Fidelity dimensions.
The procol can be used just like the abstract base class, and has the benefit of allowing type checkers to also check against a module:
from nevergrad import conversion
space_converter: SpaceConverter[NeverGradStuff] = conversion # Passes, since all the required methods are defined.
"""Ignores fidelity dimension as configspace does not have an equivalent""" | ||
return None | ||
|
||
def space(self, space: Space) -> ConfigurationSpace: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest moving this logic to a Space
handler of to_configspace
:
@to_configspace.register(Space)
<this code>
if not set inside ``Space`` | ||
|
||
""" | ||
conversion = ToConfigSpace() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be the "root" of the singledispatch callable, and just return a NotImplementedError
for the given argument.
cspace.add_hyperparameters(dims) | ||
return cspace | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@singledispatch |
space.register(Real("n3", "norm", 0.9, 0.1)) | ||
space.register(Integer("n4", "norm", 1, 2)) | ||
|
||
newspace = to_configspace(space) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This to me is a very clear sign that you only need a function.
return None | ||
|
||
|
||
class ToConfigSpace(SpaceConverter[Hyperparameter]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should be removed, see below.
cspace = ConfigurationSpace() | ||
dims = [] | ||
|
||
for _, dim in space.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for _, dim in space.items(): | |
for dim in space.values(): |
@@ -0,0 +1,69 @@ | |||
import pytest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed: #832 (comment)
It actually does, indirectly, since it dispatches based on the name of the type. This is equivalent to doing |
Are we still working on this? I think HPOBench integration as Orion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some issues will pop up with transformed spaces, it would be a good idea to add some tests for those.
Feel free to disregard my other comments about the design. If something comes up, I'll make a PR.
This version of the conversion would be sufficient for HPOBench that @donglinjy is working on. I will clean up this PR and merge, and then tackle the conversion of transformed space in another PR. |
There is a great opportunity here to improve the transformation process and uniformize it with the conversion of spaces. This will take a bit more work, so I will merge this PR in it's current form to allow @donglinjy progress meanwhile on the integration of HPOBench. The interface of the conversion class will likely change but that will be simple to adjust to. |
Hi there! Thank you for contributing. Feel free to use this pull request template; It helps us reviewing your work at its true value.
Please remove the instructions in italics before posting the pull request :).
Description
Describe what is the purpose of this pull request and why it should be integrated to the repository.
When your changes modify the current behavior, explain why your solution is better.
If it solves a GitHub issue, be sure to link it.
Changes
Give an overview of the suggested solution.
Checklist
This is simply a reminder of what we are going to look for before merging your code.
Add an
x
in the boxes that apply.If you're unsure about any of them, don't hesitate to ask. We're here to help!
You can also fill these out after creating the PR if it's a work in progress (be sure to publish the PR as a draft then)
Tests
$ tox -e py38
; replace38
by your Python version if necessary)Documentation
Quality
$ tox -e lint
)Further comments
Please include any additional information or comment that you feel will be helpful to the review of this pull request.