Closed charlesjhill closed 2 months ago
Hi there,
Yeah unfortunatly json doesn't keep tuple
types in serialization. I'm kind of surprised that you could use tuples in a Categorical in the first place.
Some updates from #346 which might be relevant:
I've upgraded the json read/write to accepted arbitrary user defined encoder/decoders (now accessible through space.to_json(path)
and ConfigurationSpace.from_json(path)
). You can overwrite a decoder for a specific "type"
that is written into the json output. I've provided an example based on your helpful reproducible example.
from typing import Any
from ConfigSpace import CategoricalHyperparameter, ConfigurationSpace from ConfigSpace.read_and_write.dictionary import _decode_categorical
cs = ConfigurationSpace( { "dims": [(1e-1, 1e-2), 1e-2, (1e-2, 1e-4), 1e-4], }, ) cs.to_json("k.json")
def my_categorical_decoder( item: dict[str, Any], cs: ConfigurationSpace, decode, ) -> CategoricalHyperparameter: if item["name"] == "dims":
item["choices"] = [
tuple(x) if isinstance(x, list) else x for x in item["choices"]
]
dv = item["default_value"]
item["default_value"] = tuple(dv) if isinstance(dv, list) else dv
return _decode_categorical(item, cs, decode)
custom_decoders = {"hyperparameters": {"categorical": my_categorical_decoder}} space = ConfigurationSpace.from_json("k.json", decoders=custom_decoders) print(space)
We hope to release this sometime next week :)
Awesome, the ability to pass a decoder that targets a particular key especially will make the process less prone to side-effects compared to using a JSONDecoder
which must decide which transformations to apply based on type of input alone.
Version & copy-pastable script
> Version: > ```python > from importlib.metadata import version > import ConfigSpace > version("ConfigSpace") > # '0.7.1' > ConfigSpace.__version__ > # '0.6.1' > ``` > Copy-pastable script: > ```python > from ConfigSpace import ConfigurationSpace, Categorical > from ConfigSpace.read_and_write.json import read, write > > cs = ConfigurationSpace() > cs.add_hyperparameter(Categorical("dims", [(1e-1, 1e-2), 1e-2, (1e-2, 1e-4), 1e-4])) > json_repr = write(cs) > cs_2 = read(json_repr) # Crashes > ```
This works:
But a round-trip to JSON doesn't.
The stacktrace is:
In short, the json write converts the tuples to lists, but this isn't restored on the read. The CategoricalHyperparameter constructor requires that choices be hashable, and lists are not. I can thing of a few possible solutions; none would be very intensive.
cls
kwarg fromjson.{dumps,loads}
inread_and_write.json.{read,write}
so users can pass a customJSONEncoder
orJSONDecoder
, respectively. This would also allow for serialization/deserialization of other types of interest.ConfigSpace.read_and_write.json._construct_hyperparameter
to convert thelist
-typed elements ofhyperparameter[{"choices", "sequence"}]
andhyperparameter["default"]
to atuple
, if needed, for categorical and ordinal hyperparameters.The third option is the hack I'm doing for my use-case, but the second seems more robust and forward-looking. I'm filing the issue because I don't like option 1, of course :)
I can make a PR if there's interest. Cheers~