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 TransitionChoice with repetitions #696

Merged
merged 12 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
- `recommend` now provides an evaluated candidate when possible. For non-deterministic parametrization like `Choice`, this means we won't
resample, and we will actually recommend the best past evaluated candidate [#668](https://github.com/facebookresearch/nevergrad/pull/668).
Still, some optimizers (like `TBPSA`) may recommend a non-evaluated point.
- `Choice` now takes a new `repetitions` parameters for sampling several times,
it is equivalent to :code:`Tuple(*[Choice(options) for _ in range(repetitions)])` but can be be around 30x faster for large numbers of repetitions [#670](https://github.com/facebookresearch/nevergrad/pull/670).
- `Choice` and `TransitionChoice` can now take a `repetitions` parameters for sampling several times,
it is equivalent to :code:`Tuple(*[Choice(options) for _ in range(repetitions)])` but can be be up to 30x faster for large numbers of repetitions [#670](https://github.com/facebookresearch/nevergrad/pull/670).
- Defaults for bounds in `Array` is now `bouncing`, which is a variant of `clipping` avoiding over-sompling on the bounds [#684](https://github.com/facebookresearch/nevergrad/pull/684)
and [#691](https://github.com/facebookresearch/nevergrad/pull/691).

Expand Down
102 changes: 56 additions & 46 deletions nevergrad/parametrization/choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import warnings
import typing as tp
import numpy as np
from nevergrad.common.typetools import ArrayLike
Expand All @@ -11,7 +12,6 @@
from . import core
from .container import Tuple
from .data import Array
from .data import Scalar
# weird pylint issue on "Descriptors"
# pylint: disable=no-value-for-parameter

Expand All @@ -22,7 +22,15 @@

class BaseChoice(core.Dict):

def __init__(self, *, choices: tp.Iterable[tp.Any], **kwargs: tp.Any) -> None:
def __init__(
self,
*,
choices: tp.Iterable[tp.Any],
repetitions: tp.Optional[int] = None,
**kwargs: tp.Any
) -> None:
assert repetitions is None or isinstance(repetitions, int) # avoid silent issues
self._repetitions = repetitions
assert not isinstance(choices, Tuple)
lchoices = list(choices) # for iterables
if not lchoices:
Expand All @@ -41,10 +49,11 @@ def __len__(self) -> int:
return len(self.choices)

@property
def index(self) -> int:
"""Index of the chosen option, if unique
def index(self) -> int: # delayed choice
"""Index of the chosen option
"""
raise NotImplementedError
assert self.indices.size == 1
return int(self.indices[0])

@property
def indices(self) -> np.ndarray:
Expand All @@ -67,12 +76,15 @@ def value(self, value: tp.Any) -> None:
self._find_and_set_value(value)

def _get_value(self) -> tp.Any:
return core.as_parameter(self.choices[self.index]).value
if self._repetitions is None:
return core.as_parameter(self.choices[self.index]).value
return tuple(core.as_parameter(self.choices[ind]).value for ind in self.indices)

def _find_and_set_value(self, values: 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
self._check_frozen()
indices: np.ndarray = -1 * np.ones(len(values), dtype=int)
nums = sorted(int(k) for k in self.choices._content)
Expand Down Expand Up @@ -141,11 +153,10 @@ def __init__(
deterministic: bool = False,
) -> None:
assert not isinstance(choices, Tuple)
assert repetitions is None or isinstance(repetitions, int) # avoid silent issues
lchoices = list(choices)
self._repetitions: tp.Optional[int] = repetitions
rep = 1 if self._repetitions is None else self._repetitions
super().__init__(choices=lchoices, weights=Array(shape=(rep, len(lchoices)), mutable_sigma=False))
rep = 1 if repetitions is None else repetitions
super().__init__(choices=lchoices, repetitions=repetitions,
weights=Array(shape=(rep, len(lchoices)), mutable_sigma=False))
self._deterministic = deterministic
self._indices: tp.Optional[np.ndarray] = None

Expand All @@ -166,13 +177,6 @@ def indices(self) -> np.ndarray: # delayed choice
assert self._indices is not None
return self._indices

@property
def index(self) -> int: # delayed choice
"""Index of the chosen option
"""
assert self.indices.size == 1
return int(self.indices[0])

@property
def weights(self) -> Array:
"""The weights used to draw the value
Expand All @@ -184,15 +188,10 @@ def probabilities(self) -> np.ndarray:
"""The probabilities used to draw the value
"""
exp = np.exp(self.weights.value)
return exp / np.sum(exp) # type: ignore

def _get_value(self) -> tp.Any:
if self._repetitions is None:
return super()._get_value()
return tuple(core.as_parameter(self.choices[ind]).value for ind in self.indices)
return exp / np.sum(exp)

def _find_and_set_value(self, values: tp.Any) -> np.ndarray:
indices = super()._find_and_set_value([values] if self._repetitions is None else values)
indices = super()._find_and_set_value(values)
self._indices = indices
# force new probabilities
arity = self.weights.value.shape[1]
Expand Down Expand Up @@ -254,28 +253,28 @@ def __init__(
self,
choices: tp.Iterable[tp.Any],
transitions: tp.Union[ArrayLike, Array] = (1.0, 1.0),
repetitions: tp.Optional[int] = None,
) -> None:
choices = list(choices)
positions = Array(init=len(choices) / 2.0 * np.ones((repetitions if repetitions is not None else 1,)))
positions.set_bounds(0, len(choices), method="gaussian")
super().__init__(choices=choices,
position=Scalar(),
repetitions=repetitions,
positions=positions,
transitions=transitions if isinstance(transitions, Array) else np.array(transitions, copy=False))
assert self.transitions.value.ndim == 1

@property
def index(self) -> int:
return discretization.threshold_discretization(np.array([self.position.value]), arity=len(self.choices))[0]

@property
def indices(self) -> np.ndarray:
return np.array([self.index], dtype=int)
return np.minimum(len(self) - 1e-9, self.positions.value).astype(int)

def _find_and_set_value(self, values: tp.Any) -> np.ndarray:
indices = super()._find_and_set_value([values]) # only one value for this class
self._set_index(int(indices[0]))
indices = super()._find_and_set_value(values) # only one value for this class
self._set_index(indices)
return indices

def _set_index(self, index: int) -> None:
out = discretization.inverse_threshold_discretization([index], len(self.choices))
self.position.value = out[0]
def _set_index(self, indices: np.ndarray) -> None:
self.positions.value = indices + 0.5

@property
def transitions(self) -> Array:
Expand All @@ -284,28 +283,39 @@ def transitions(self) -> Array:
return self["transitions"] # type: ignore

@property
def position(self) -> Scalar:
def position(self) -> Array:
"""The continuous version of the index (used when working with standardized space)
"""
return self["position"] # type: ignore
warnings.warn("position is replaced by positions in order to allow for repetitions", DeprecationWarning)
return self.positions

@property
def positions(self) -> Array:
"""The continuous version of the index (used when working with standardized space)
"""
return self["positions"] # type: ignore

def mutate(self) -> None:
# force random_state sync
self.random_state # pylint: disable=pointless-statement
transitions = core.as_parameter(self.transitions)
transitions.mutate()
probas = np.exp(transitions.value)
probas /= np.sum(probas) # TODO decide if softmax is the best way to go...
move = self.random_state.choice(list(range(probas.size)), p=probas)
sign = 1 if self.random_state.randint(2) else -1
new_index = max(0, min(len(self.choices), self.index + sign * move))
self._set_index(new_index)
rep = 1 if self._repetitions is None else self._repetitions
#
enc = discretization.Encoder(np.ones((rep, 1)) * np.log(self.transitions.value),
self.random_state)
moves = enc.encode()
signs = self.random_state.choice([-1, 1], size=rep)
new_index = np.clip(self.indices + signs * moves, 0, len(self) - 1)
self._set_index(new_index.ravel())
# mutate corresponding parameter
self.choices[self.index].mutate()
indices = set(self.indices)
for ind in indices:
self.choices[ind].mutate()

def _internal_spawn_child(self: T) -> T:
choices = (y for x, y in sorted(self.choices.spawn_child()._content.items()))
child = self.__class__(choices=choices)
child._content["position"] = self.position.spawn_child()
child = self.__class__(choices=choices, repetitions=self._repetitions)
child._content["positions"] = self.positions.spawn_child()
child._content["transitions"] = self.transitions.spawn_child()
return child
3 changes: 2 additions & 1 deletion nevergrad/parametrization/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ def set_bounds(
if (bounds[0] >= bounds[1]).any(): # type: ignore
raise ValueError(f"Lower bounds {lower} should be strictly smaller than upper bounds {upper}")
# update instance
transforms = dict(clipping=trans.Clipping, arctan=trans.ArctanBound, tanh=trans.TanhBound)
transforms = dict(clipping=trans.Clipping, arctan=trans.ArctanBound, tanh=trans.TanhBound,
gaussian=trans.CumulativeDensity)
transforms["bouncing"] = functools.partial(trans.Clipping, bounce=True) # type: ignore
if method in transforms:
if self.exponent is not None and method not in ("clipping", "bouncing"):
Expand Down
16 changes: 14 additions & 2 deletions nevergrad/parametrization/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _true(*args: tp.Any, **kwargs: tp.Any) -> bool: # pylint: disable=unused-ar
par.Choice([par.Array(shape=(2,)), "blublu"]),
par.Choice([1, 2], repetitions=2),
par.TransitionChoice([par.Array(shape=(2,)), par.Scalar()]),
par.TransitionChoice(["a", "b", "c"], transitions=(0, 2, 1), repetitions=4),
],
)
def test_parameters_basic_features(param: par.Parameter) -> None:
Expand Down Expand Up @@ -163,8 +164,8 @@ def check_parameter_freezable(param: par.Parameter) -> None:
"Instrumentation(Tuple(Array{(2,)}),Dict(string=blublu,truc=plop))"),
(par.Choice([1, 12]), "Choice(choices=Tuple(1,12),weights=Array{(1,2)})"),
(par.Choice([1, 12], deterministic=True), "Choice{det}(choices=Tuple(1,12),weights=Array{(1,2)})"),
(par.TransitionChoice([1, 12]), "TransitionChoice(choices=Tuple(1,12),position=Scalar["
"sigma=Log{exp=2.0}],transitions=[1. 1.])")
(par.TransitionChoice([1, 12]), "TransitionChoice(choices=Tuple(1,12),position=Array{Cd(0,2)}"
",transitions=[1. 1.])")
]
)
def test_parameter_names(param: par.Parameter, name: str) -> None:
Expand Down Expand Up @@ -314,6 +315,17 @@ def test_choice_repetitions() -> None:
choice.mutate()


def test_transition_choice_repetitions() -> None:
choice = par.TransitionChoice([0, 1, 2, 3], repetitions=2)
choice.random_state.seed(12)
assert len(choice) == 4
assert choice.value == (2, 2)
choice.value = (3, 1)
np.testing.assert_almost_equal(choice.positions.value, [3.5, 1.5], decimal=3)
choice.mutate()
assert choice.value == (3, 0)


def test_descriptors() -> None:
d1 = utils.Descriptors()
d2 = utils.Descriptors(continuous=False)
Expand Down
2 changes: 1 addition & 1 deletion nevergrad/parametrization/test_parameters_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_instrumentation() -> None:
# check naming
instru_str = ("Instrumentation(Tuple(Scalar[sigma=Log{exp=2.0}],3),"
"Dict(a=TransitionChoice(choices=Tuple(0,1,2,3),"
"position=Scalar[sigma=Log{exp=2.0}],transitions=[1. 1.]),"
"position=Array{Cd(0,4)},transitions=[1. 1.]),"
"b=Choice(choices=Tuple(0,1,2,3),weights=Array{(1,4)})))")
testing.printed_assert_equal(instru.name, instru_str)
testing.printed_assert_equal("blublu", instru.set_name("blublu").name)
Expand Down
4 changes: 3 additions & 1 deletion nevergrad/parametrization/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
exponentiate=(transforms.Exponentiate(3, 4), "Ex(3,4)"),
tanh=(transforms.TanhBound(3.0, 4.5), "Th(3,4.5)"),
arctan=(transforms.ArctanBound(3, 4), "At(3,4)"),
cumdensity=(transforms.CumulativeDensity(), "Cd()"),
cumdensity=(transforms.CumulativeDensity(), "Cd(0,1)"),
cumdensity2=(transforms.CumulativeDensity(1, 3), "Cd(1,3)"),
clipping=(transforms.Clipping(None, 1e12), "Cl(None,1000000000000)"),
bouncing=(transforms.Clipping(-12000, 12000, bounce=True), "Cl(-12000,12000,b)"),
fourrier=(transforms.Fourrier(), "F(0)"),
Expand All @@ -37,6 +38,7 @@ def test_back_and_forth(transform: transforms.Transform, string: str) -> None:
arctan=(transforms.ArctanBound(3, 5), [-100000, 100000, 0], [3, 5, 4]),
bouncing=(transforms.Clipping(0, 10, bounce=True), [-1, 22, 3], [1, 0, 3]),
cumdensity=(transforms.CumulativeDensity(), [-10, 0, 10], [0, .5, 1]),
cumdensity_bounds=(transforms.CumulativeDensity(2, 4), [-10, 0, 10], [2, 3, 4]),
)
def test_vals(transform: transforms.Transform, x: List[float], expected: List[float]) -> None:
y = transform.forward(np.array(x))
Expand Down
29 changes: 19 additions & 10 deletions nevergrad/parametrization/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,31 +242,40 @@ def __init__(

def forward(self, x: np.ndarray) -> np.ndarray:
self._check_shape(x)
return self._b + self._a * np.arctan(x) # type: ignore
return self._b + self._a * np.arctan(x)

def backward(self, y: np.ndarray) -> np.ndarray:
self._check_shape(y)
if (y > self.a_max).any() or (y < self.a_min).any():
raise ValueError(f"Only data between {self.a_min} and {self.a_max} can be transformed back.")
return np.tan((y - self._b) / self._a) # type: ignore
return np.tan((y - self._b) / self._a)


class CumulativeDensity(Transform):
class CumulativeDensity(BoundTransform):
"""Bounds all real values into [0, 1] using a gaussian cumulative density function (cdf)
Beware, cdf goes very fast to its limits.
"""

def __init__(self) -> None:
super().__init__()
self.name = "Cd()"
def __init__(
self,
lower: float = 0.0,
upper: float = 1.0,
eps: float = 1e-9
) -> None:
super().__init__(a_min=lower, a_max=upper)
self._b = lower
self._a = upper - lower
self._eps = eps
self.name = f"Cd({_f(lower)},{_f(upper)})"

def forward(self, x: np.ndarray) -> np.ndarray:
return stats.norm.cdf(x) # type: ignore
return self._a * stats.norm.cdf(x) + self._b

def backward(self, y: np.ndarray) -> np.ndarray:
if np.max(y) > 1 or np.min(y) < 0:
raise ValueError("Only data between 0 and 1 can be transformed back (bounds lead to infinity).")
return stats.norm.ppf(y) # type: ignore
if (y > self.a_max).any() or (y < self.a_min).any():
raise ValueError(f"Only data between {self.a_min} and {self.a_max} can be transformed back.\nGot: {y}")
y = np.clip((y - self._b) / self._a, self._eps, 1 - self._eps)
return stats.norm.ppf(y)


class Fourrier(Transform):
Expand Down