From 1880ea291146e35fc5afc96a745d96f509987d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Rapin?= Date: Thu, 11 Feb 2021 00:41:23 +0100 Subject: [PATCH] Group Parameter containers in container module (#1044) --- CHANGELOG.md | 1 + nevergrad/optimization/es.py | 3 +- nevergrad/parametrization/choice.py | 24 ++-- nevergrad/parametrization/container.py | 156 ++++++++++++++++++++- nevergrad/parametrization/core.py | 179 ++++--------------------- nevergrad/parametrization/data.py | 8 +- nevergrad/parametrization/helpers.py | 12 +- nevergrad/parametrization/parameter.py | 24 ++-- 8 files changed, 222 insertions(+), 185 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76cdbca8..9d4085bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ [#1036](https://github.com/facebookresearch/nevergrad/pull/1036) [#1038](https://github.com/facebookresearch/nevergrad/pull/1038) [#1043](https://github.com/facebookresearch/nevergrad/pull/1043) + [#1044](https://github.com/facebookresearch/nevergrad/pull/1044) and more to come), please open an issue if you encounter any problem. The midterm aim is to allow for simpler constraint management. ## 0.4.3 (2021-01-28) diff --git a/nevergrad/optimization/es.py b/nevergrad/optimization/es.py index b3dba09e2..7d433dc29 100644 --- a/nevergrad/optimization/es.py +++ b/nevergrad/optimization/es.py @@ -95,7 +95,8 @@ class EvolutionStrategy(base.ConfiguredOptimizer): """Experimental evolution-strategy-like algorithm The API is going to evolve - Parameters: + Parameters + ---------- recombination_ratio: float probability of using a recombination (after the mutation) for generating new offsprings popsize: int diff --git a/nevergrad/parametrization/choice.py b/nevergrad/parametrization/choice.py index f37cd736c..d523ea932 100644 --- a/nevergrad/parametrization/choice.py +++ b/nevergrad/parametrization/choice.py @@ -8,7 +8,7 @@ from . import discretization from . import utils from . import core -from .container import Tuple +from . import container from .data import Array # weird pylint issue on "Descriptors" @@ -34,7 +34,7 @@ def as_tag(cls, param: core.Parameter) -> "ChoiceTag": return cls(type(param), arity) -class BaseChoice(core.Container): +class BaseChoice(container.Container): ChoiceTag = ChoiceTag @@ -46,7 +46,7 @@ def __init__( lchoices = list(choices) # unroll iterables (includig Tuple instances if not lchoices: raise ValueError("{self._class__.__name__} received an empty list of options.") - super().__init__(choices=Tuple(*lchoices), **kwargs) + super().__init__(choices=container.Tuple(*lchoices), **kwargs) def _compute_descriptors(self) -> utils.Descriptors: deterministic = getattr(self, "_deterministic", True) @@ -80,7 +80,7 @@ def indices(self) -> np.ndarray: raise NotImplementedError # TODO remove index? @property - def choices(self) -> Tuple: + def choices(self) -> container.Tuple: """The different options, as a Tuple Parameter""" return self["choices"] # type: ignore @@ -89,18 +89,18 @@ def _get_value(self) -> tp.Any: return core.as_parameter(self.choices[self.index]).value return tuple(core.as_parameter(self.choices[ind]).value for ind in self.indices) - def _set_value(self, values: tp.List[tp.Any]) -> np.ndarray: + def _set_value(self, value: tp.List[tp.Any]) -> np.ndarray: """Must be adapted to each class This handles a list of values, not just one """ # TODO this is currenlty very messy, may need some improvement - values = [values] if self._repetitions is None else values + values = [value] if self._repetitions is None else value self._check_frozen() indices: np.ndarray = -1 * np.ones(len(values), dtype=int) # try to find where to put this - for i, value in enumerate(values): + for i, val in enumerate(values): for k, choice in enumerate(self.choices): try: - choice.value = value + choice.value = val indices[i] = k break except Exception: # pylint: disable=broad-except @@ -197,8 +197,8 @@ def probabilities(self) -> np.ndarray: exp = np.exp(self.weights.value) return exp / np.sum(exp) # type: ignore - def _set_value(self, values: tp.Any) -> np.ndarray: - indices = super()._set_value(values) + def _set_value(self, value: tp.Any) -> np.ndarray: + indices = super()._set_value(value) self._indices = indices # force new probabilities arity = self.weights.value.shape[1] @@ -274,8 +274,8 @@ def __init__( def indices(self) -> np.ndarray: return np.minimum(len(self) - 1e-9, self.positions.value).astype(int) # type: ignore - def _set_value(self, values: tp.Any) -> np.ndarray: - indices = super()._set_value(values) # only one value for this class + def _set_value(self, value: tp.Any) -> np.ndarray: + indices = super()._set_value(value) # only one value for this class self._set_index(indices) return indices diff --git a/nevergrad/parametrization/container.py b/nevergrad/parametrization/container.py index ad4703597..c2e297669 100644 --- a/nevergrad/parametrization/container.py +++ b/nevergrad/parametrization/container.py @@ -3,16 +3,164 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. +import operator +import functools +from collections import OrderedDict import nevergrad.common.typing as tp -from .core import Dict as Dict # Dict needs to be implemented in core since it's used in the base class +import numpy as np +from . import utils from . import core -Ins = tp.TypeVar("Ins", bound="Instrumentation") -ArgsKwargs = tp.Tuple[tp.Tuple[tp.Any, ...], tp.Dict[str, tp.Any]] +D = tp.TypeVar("D", bound="Container") -class Tuple(core.Container): +class Container(core.Parameter): + """Parameter which can hold other parameters. + This abstract implementation is based on a dictionary. + + Parameters + ---------- + **parameters: Any + the objects or Parameter which will provide values for the dict + + Note + ---- + This is the base structure for all container Parameters, and it is + used to hold the internal/model parameters for all Parameter classes. + """ + + def __init__(self, **parameters: tp.Any) -> None: + super().__init__() + self._subobjects = utils.Subobjects(self, base=core.Parameter, attribute="_content") + self._content: tp.Dict[tp.Any, core.Parameter] = { + k: core.as_parameter(p) for k, p in parameters.items() + } + self._sizes: tp.Optional[tp.Dict[str, int]] = None + self._sanity_check(list(self._content.values())) + self._ignore_in_repr: tp.Dict[ + str, str + ] = {} # hacky undocumented way to bypass boring representations + + def _sanity_check(self, parameters: tp.List[core.Parameter]) -> None: + """Check that all parameters are different""" + # TODO: this is first order, in practice we would need to test all the different + # parameter levels together + if parameters: + assert all(isinstance(p, core.Parameter) for p in parameters) + ids = {id(p) for p in parameters} + if len(ids) != len(parameters): + raise ValueError("Don't repeat twice the same parameter") + + def _compute_descriptors(self) -> utils.Descriptors: + init = utils.Descriptors() + return functools.reduce(operator.and_, [p.descriptors for p in self._content.values()], init) + + def __getitem__(self, name: tp.Any) -> core.Parameter: + return self._content[name] + + def __len__(self) -> int: + return len(self._content) + + def _get_parameters_str(self) -> str: + raise NotImplementedError + + def _get_name(self) -> str: + return f"{self.__class__.__name__}({self._get_parameters_str()})" + + def get_value_hash(self) -> tp.Hashable: + return tuple(sorted((x, y.get_value_hash()) for x, y in self._content.items())) + + def _internal_get_standardized_data(self: D, reference: D) -> np.ndarray: + data = {k: self[k].get_standardized_data(reference=p) for k, p in reference._content.items()} + if self._sizes is None: + self._sizes = OrderedDict(sorted((x, y.size) for x, y in data.items())) + assert self._sizes is not None + data_list = [data[k] for k in self._sizes] + if not data_list: + return np.array([]) + return data_list[0] if len(data_list) == 1 else np.concatenate(data_list) # type: ignore + + def _internal_set_standardized_data( + self: D, data: np.ndarray, reference: D, deterministic: bool = False + ) -> None: + if self._sizes is None: + self.get_standardized_data(reference=self) + assert self._sizes is not None + if data.size != sum(v for v in self._sizes.values()): + raise ValueError( + f"Unexpected shape {data.shape} for {self} with dimension {self.dimension}:\n{data}" + ) + data = data.ravel() + start, end = 0, 0 + for name, size in self._sizes.items(): + end = start + size + self._content[name].set_standardized_data( + data[start:end], reference=reference[name], deterministic=deterministic + ) + start = end + assert end == len(data), f"Finished at {end} but expected {len(data)}" + + def sample(self: D) -> D: + child = self.spawn_child() + child._content = {k: p.sample() for k, p in self._content.items()} + child.heritage["lineage"] = child.uid + return child + + +class Dict(Container): + """Dictionary-valued parameter. This Parameter can contain other Parameters, + its value is a dict, with keys the ones provided as input, and corresponding values are + either directly the provided values if they are not Parameter instances, or the value of those + Parameters. It also implements a getter to access the Parameters directly if need be. + + Parameters + ---------- + **parameters: Any + the objects or Parameter which will provide values for the dict + + Note + ---- + This is the base structure for all container Parameters, and it is + used to hold the internal/model parameters for all Parameter classes. + """ + + value: core.ValueProperty[tp.Dict[str, tp.Any]] = core.ValueProperty() + + def __iter__(self) -> tp.Iterator[str]: + return iter(self.keys()) + + def keys(self) -> tp.KeysView[str]: + return self._content.keys() + + def items(self) -> tp.ItemsView[str, core.Parameter]: + return self._content.items() + + def values(self) -> tp.ValuesView[core.Parameter]: + return self._content.values() + + def _get_value(self) -> tp.Dict[str, tp.Any]: + return {k: p.value for k, p in self.items()} + + def _set_value(self, value: tp.Dict[str, tp.Any]) -> None: + cls = self.__class__.__name__ + if not isinstance(value, dict): + raise TypeError(f"{cls} value must be a dict, got: {value}\nCurrent value: {self.value}") + if set(value) != set(self): + raise ValueError( + f"Got input keys {set(value)} for {cls} but expected {set(self._content)}\nCurrent value: {self.value}" + ) + for key, val in value.items(): + self._content[key].value = val + + def _get_parameters_str(self) -> str: + params = sorted( + (k, p.name) for k, p in self.items() if p.name != self._ignore_in_repr.get(k, "#ignoredrepr#") + ) + return ",".join(f"{k}={n}" for k, n in params) + + +class Tuple(Container): """Tuple-valued parameter. This Parameter can contain other Parameters, its value is tuple which values are either directly the provided values if they are not Parameter instances, or the value of those Parameters. diff --git a/nevergrad/parametrization/core.py b/nevergrad/parametrization/core.py index 035fd68fe..dd3cddad7 100644 --- a/nevergrad/parametrization/core.py +++ b/nevergrad/parametrization/core.py @@ -6,9 +6,6 @@ import uuid import copy import warnings -import operator -import functools -from collections import OrderedDict import numpy as np import nevergrad.common.typing as tp from nevergrad.common import errors @@ -18,7 +15,6 @@ P = tp.TypeVar("P", bound="Parameter") -D = tp.TypeVar("D", bound="Container") X = tp.TypeVar("X") @@ -27,6 +23,21 @@ class ValueProperty(tp.Generic[X]): Parameter objects fetches _get_value and _set_value methods """ + # This uses the descriptor protocol, like a property: + # See https://docs.python.org/3/howto/descriptor.html + # + # Basically parameter.value calls parameter.value.__get__ + # and then parameter._get_value + def __init__(self) -> None: + self.__doc__ = """Value of the Parameter, which should be sent to the function + to optimize. + + Example + ------- + >>> ng.p.Array(shape=(2,)).value + array([0., 0.]) + """ + def __get__(self, obj: "Parameter", objtype: tp.Optional[tp.Type[object]] = None) -> X: return obj._get_value() # type: ignore @@ -36,16 +47,22 @@ def __set__(self, obj: "Parameter", value: X) -> None: # pylint: disable=too-many-instance-attributes,too-many-public-methods class Parameter: - """Abstract class providing the core functionality of a parameter, aka + """Class providing the core functionality of a parameter, aka value, internal/model parameters, mutation, recombination and additional features such as shared random state, constraint check, hashes, generation and naming. + The value field should sent to the function to optimize. - By default, all Parameter attributes of this Parameter are considered as - sub-parameters. - Spawning a child creates a shallow copy. + Example + ------- + >>> ng.p.Array(shape=(2,)).value + array([0., 0.]) """ + # By default, all Parameter attributes of this Parameter are considered as + # sub-parameters. + # Spawning a child creates a shallow copy. + value: ValueProperty[tp.Any] = ValueProperty() def __init__(self) -> None: @@ -422,6 +439,9 @@ def descriptors(self) -> utils.Descriptors: return self._descriptors +# Basic types and helpers # + + class Constant(Parameter): """Parameter-like object for simplifying management of constant parameters: mutation/recombination do nothing, value cannot be changed, standardize data is an empty array, @@ -499,146 +519,3 @@ def __init__(self, parameter: tp.Optional[Parameter] = None) -> None: f"be used by the optimizer.\n(received {parameter} of type {type(parameter)})" ) super().__init__(parameter) - - -class Container(Parameter): - """Parameter which can hold other parameters. - This abstract implementation is based on a dictionary. - - Parameters - ---------- - **parameters: Any - the objects or Parameter which will provide values for the dict - - Note - ---- - This is the base structure for all container Parameters, and it is - used to hold the internal/model parameters for all Parameter classes. - """ - - def __init__(self, **parameters: tp.Any) -> None: - super().__init__() - self._subobjects = utils.Subobjects(self, base=Parameter, attribute="_content") - self._content: tp.Dict[tp.Any, Parameter] = {k: as_parameter(p) for k, p in parameters.items()} - self._sizes: tp.Optional[tp.Dict[str, int]] = None - self._sanity_check(list(self._content.values())) - self._ignore_in_repr: tp.Dict[ - str, str - ] = {} # hacky undocumented way to bypass boring representations - - def _sanity_check(self, parameters: tp.List[Parameter]) -> None: - """Check that all parameters are different""" - # TODO: this is first order, in practice we would need to test all the different - # parameter levels together - if parameters: - assert all(isinstance(p, Parameter) for p in parameters) - ids = {id(p) for p in parameters} - if len(ids) != len(parameters): - raise ValueError("Don't repeat twice the same parameter") - - def _compute_descriptors(self) -> utils.Descriptors: - init = utils.Descriptors() - return functools.reduce(operator.and_, [p.descriptors for p in self._content.values()], init) - - def __getitem__(self, name: tp.Any) -> Parameter: - return self._content[name] - - def __len__(self) -> int: - return len(self._content) - - def _get_parameters_str(self) -> str: - raise NotImplementedError - - def _get_name(self) -> str: - return f"{self.__class__.__name__}({self._get_parameters_str()})" - - def get_value_hash(self) -> tp.Hashable: - return tuple(sorted((x, y.get_value_hash()) for x, y in self._content.items())) - - def _internal_get_standardized_data(self: D, reference: D) -> np.ndarray: - data = {k: self[k].get_standardized_data(reference=p) for k, p in reference._content.items()} - if self._sizes is None: - self._sizes = OrderedDict(sorted((x, y.size) for x, y in data.items())) - assert self._sizes is not None - data_list = [data[k] for k in self._sizes] - if not data_list: - return np.array([]) - return data_list[0] if len(data_list) == 1 else np.concatenate(data_list) # type: ignore - - def _internal_set_standardized_data( - self: D, data: np.ndarray, reference: D, deterministic: bool = False - ) -> None: - if self._sizes is None: - self.get_standardized_data(reference=self) - assert self._sizes is not None - if data.size != sum(v for v in self._sizes.values()): - raise ValueError( - f"Unexpected shape {data.shape} for {self} with dimension {self.dimension}:\n{data}" - ) - data = data.ravel() - start, end = 0, 0 - for name, size in self._sizes.items(): - end = start + size - self._content[name].set_standardized_data( - data[start:end], reference=reference[name], deterministic=deterministic - ) - start = end - assert end == len(data), f"Finished at {end} but expected {len(data)}" - - def sample(self: D) -> D: - child = self.spawn_child() - child._content = {k: p.sample() for k, p in self._content.items()} - child.heritage["lineage"] = child.uid - return child - - -class Dict(Container): - """Dictionary-valued parameter. This Parameter can contain other Parameters, - its value is a dict, with keys the ones provided as input, and corresponding values are - either directly the provided values if they are not Parameter instances, or the value of those - Parameters. It also implements a getter to access the Parameters directly if need be. - - Parameters - ---------- - **parameters: Any - the objects or Parameter which will provide values for the dict - - Note - ---- - This is the base structure for all container Parameters, and it is - used to hold the internal/model parameters for all Parameter classes. - """ - - def __iter__(self) -> tp.Iterator[str]: - return iter(self.keys()) - - def keys(self) -> tp.KeysView[str]: - return self._content.keys() - - def items(self) -> tp.ItemsView[str, Parameter]: - return self._content.items() - - def values(self) -> tp.ValuesView[Parameter]: - return self._content.values() - - def _get_value(self) -> tp.Dict[str, tp.Any]: - return {k: p.value for k, p in self.items()} - - value: ValueProperty[tp.Dict[str, tp.Any]] = ValueProperty() - - def _set_value(self, value: tp.Dict[str, tp.Any]) -> None: - cls = self.__class__.__name__ - if not isinstance(value, dict): - raise TypeError(f"{cls} value must be a dict, got: {value}\nCurrent value: {self.value}") - if set(value) != set(self): - raise ValueError( - f"Got input keys {set(value)} for {cls} but expected {set(self._content)}\nCurrent value: {self.value}" - ) - for key, val in value.items(): - self._content[key].value = val - - def _get_parameters_str(self) -> str: - params = sorted( - (k, p.name) for k, p in self.items() if p.name != self._ignore_in_repr.get(k, "#ignoredrepr#") - ) - return ",".join(f"{k}={n}" for k, n in params) diff --git a/nevergrad/parametrization/data.py b/nevergrad/parametrization/data.py index 96287df35..ccdc1e906 100644 --- a/nevergrad/parametrization/data.py +++ b/nevergrad/parametrization/data.py @@ -8,9 +8,11 @@ import numpy as np import nevergrad.common.typing as tp from . import core +from .container import Dict from . import utils from . import transforms as trans + # pylint: disable=no-value-for-parameter @@ -18,7 +20,7 @@ P = tp.TypeVar("P", bound=core.Parameter) -def _param_string(parameters: core.Dict) -> str: +def _param_string(parameters: Dict) -> str: """Hacky helper for nice-visualizatioon""" substr = f"[{parameters._get_parameters_str()}]" if substr == "[]": @@ -43,7 +45,7 @@ class Mutation(core.Parameter): def __init__(self, **kwargs: tp.Any) -> None: super().__init__() - self.parameters = core.Dict(**kwargs) + self.parameters = Dict(**kwargs) def _get_value(self) -> tp.Callable[[tp.Sequence[D]], None]: return self.apply @@ -96,7 +98,7 @@ def __init__( ) -> None: sigma = Log(init=1.0, exponent=2.0, mutable_sigma=False) if mutable_sigma else 1.0 super().__init__() - self.parameters = core.Dict(sigma=sigma, recombination="average", mutation="gaussian") + self.parameters = Dict(sigma=sigma, recombination="average", mutation="gaussian") err_msg = 'Exactly one of "init" or "shape" must be provided' self.parameters._ignore_in_repr = dict(sigma="1.0", recombination="average", mutation="gaussian") if init is not None: diff --git a/nevergrad/parametrization/helpers.py b/nevergrad/parametrization/helpers.py index f49ff71ff..9ea44c9f9 100644 --- a/nevergrad/parametrization/helpers.py +++ b/nevergrad/parametrization/helpers.py @@ -42,8 +42,8 @@ def flatten_parameter( This function is experimental, its output will probably evolve before converging. """ flat = {"": parameter} - if isinstance(parameter, core.Container): - content_to_add: tp.List[core.Container] = [parameter] + if isinstance(parameter, container.Container): + content_to_add: tp.List[container.Container] = [parameter] if isinstance(parameter, container.Instrumentation): # special case: skip internal Tuple and Dict content_to_add = [parameter[0], parameter[1]] # type: ignore for c in content_to_add: @@ -55,12 +55,12 @@ def flatten_parameter( for x, y in content.items() } ) - if order > 0 and not isinstance(parameter, core.Container): + if order > 0 and not isinstance(parameter, container.Container): content = dict(parameter._subobjects.items()) - param = core.Dict(**content) + param = container.Dict(**content) if len(content) == 1: lone_content = next(iter(content.values())) - if isinstance(lone_content, core.Dict): + if isinstance(lone_content, container.Dict): param = lone_content # shorcut subparameters subparams = flatten_parameter(param, with_containers=False, order=order - 1) flat.update({"#" + str(x): y for x, y in subparams.items()}) @@ -68,7 +68,7 @@ def flatten_parameter( flat = { x: y for x, y in flat.items() - if not isinstance(y, (core.Container, core.Constant)) or isinstance(y, choice.BaseChoice) + if not isinstance(y, (container.Container, core.Constant)) or isinstance(y, choice.BaseChoice) } return flat diff --git a/nevergrad/parametrization/parameter.py b/nevergrad/parametrization/parameter.py index f2fc5a15a..087345920 100644 --- a/nevergrad/parametrization/parameter.py +++ b/nevergrad/parametrization/parameter.py @@ -5,20 +5,28 @@ # pylint: disable=unused-import # import with "as" to explicitely allow reexport (mypy) + +# abstract types from .core import Parameter as Parameter -from .core import Container as Container # abstract -from .core import Dict as Dict +from .container import Container as Container +from .choice import BaseChoice as BaseChoice +from .data import Data as Data + +# special types from .core import Constant as Constant # avoid using except for checks -from .core import ( - MultiobjectiveReference as MultiobjectiveReference, -) # special case for multiobjective optimization +from .core import MultiobjectiveReference as MultiobjectiveReference # multiobjective optimization + +# containers +from .container import Dict as Dict from .container import Tuple as Tuple from .container import Instrumentation as Instrumentation -from .data import Data as Data # abstract + +# data from .data import Array as Array from .data import Scalar as Scalar from .data import Log as Log -from .choice import BaseChoice as BaseChoice +from . import mutation + +# choices from .choice import Choice as Choice from .choice import TransitionChoice as TransitionChoice -from . import mutation