Skip to content

Commit

Permalink
Plug new optimized Archive (facebookresearch#129)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrapin authored Mar 1, 2019
1 parent c527d67 commit 62d3353
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 38 deletions.
7 changes: 3 additions & 4 deletions docs/adding_an_algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ Still, this structure is not final, it is bound to evolve and you are welcome to
All algorithms derive from a base class named `Optimizer` and are registered through a decorator. The implementation of the base class is [here](../nevergrad/optimization/base.py).
This base class implements the `ask` and `tell` interface.

It records all evaluated points through the `archive` attribute, which is of type:
```
Dict[Tuple[float,...], Value]
```
It records all evaluated points through the `archive` attribute of class `Archive`. It can be seen be used as if it was of type `Dict[np.ndarray, Value]`, but since `np.ndarray` are not hashable, the underlying implementation converts arrays into bytes and register them into the `archive.bytesdict` dictionary. `Archive` however does not implement `keys` and `items` methods because converting from bytes to array is not very efficient, one should therefore interate on `bytesdict` and the keys can then be transformed back to arrays using `np.frombuffer(key)`. See [OnePlusOne implementation](../nevergrad/optimization/optimizerlib.py) for an example.


The key tuple if the point location, and `Value` is a class with attributes:
- `count`: number of evaluations at this point.
- `mean`: mean value of the evaluations at this point.
Expand Down
14 changes: 7 additions & 7 deletions nevergrad/optimization/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ class InefficientSettingsWarning(RuntimeWarning):

class Optimizer(abc.ABC): # pylint: disable=too-many-instance-attributes
"""Algorithm framework with 3 main functions:
- suggest_exploration which provides points on which to evaluate the function to optimize
- update_with_fitness_value which lets you provide the values associated to points
- provide_recommendation which provides the best final value
Typically, one would call suggest_exploration num_workers times, evaluate the
- "ask()" which provides points on which to evaluate the function to optimize
- "tell(x, value)" which lets you provide the values associated to points
- "provide_recommendation()" which provides the best final value
Typically, one would call "ask()" num_workers times, evaluate the
function on these num_workers points in parallel, update with the fitness value when the
evaluations is finished, and iterate until the budget is over. At the very end,
one would call provide_recommendation for the estimated optimum.
This class is abstract, it provides _internal equivalents for the 3 main functions,
among which at least _internal_suggest_exploration must be overridden.
among which at least _internal_ask has to be overridden.
Each optimizer instance should be used only once, with the initial provided budget
Expand Down Expand Up @@ -69,7 +69,7 @@ def __init__(self, dimension: int, budget: Optional[int] = None, num_workers: in
self.dimension = dimension
self.name = self.__class__.__name__ # printed name in repr
# keep a record of evaluations, and current bests which are updated at each new evaluation
self.archive: Union[utils.Archive, Dict[Tuple[float, ...], utils.Value]] = {}
self.archive = utils.Archive() # dict like structure taking np.ndarray as keys and Value as values
self.current_bests = {x: utils.Point(tuple(0. for _ in range(dimension)), utils.Value(np.inf))
for x in ["optimistic", "pessimistic", "average"]}
# instance state
Expand Down Expand Up @@ -346,7 +346,7 @@ class ParametrizedFamily(OptimizerFamily):
_optimizer_class: Optional[Type[Optimizer]] = None

def __init__(self) -> None:
defaults = {x: y.default for x, y in inspect.signature(self.__class__.__init__).parameters.items()
defaults = {x: y.default for x, y in inspect.signature(self.__class__.__init__).parameters.items() # type: ignore
if x not in ["self", "__class__"]}
diff = set(defaults.keys()).symmetric_difference(self.__dict__.keys())
if diff: # this is to help durring development
Expand Down
9 changes: 5 additions & 4 deletions nevergrad/optimization/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from typing import Optional, Any, Dict
from typing import Optional, Any
import numpy as np
from ..common.typetools import ArrayLike
from . import utils


def doerr_discrete_mutation(parent: ArrayLike) -> ArrayLike:
Expand Down Expand Up @@ -65,14 +66,14 @@ def crossover(parent: ArrayLike, donor: ArrayLike) -> ArrayLike:
return discrete_mutation(mix)


def get_roulette(archive: Dict[Any, Any], num: Optional[int] = None) -> Any:
def get_roulette(archive: utils.Archive, num: Optional[int] = None) -> Any:
"""Apply a roulette tournament selection.
"""
if num is None:
num = int(.999 + np.sqrt(len(archive)))
# the following sort makes the line deterministic, and function seedable, at the cost of complexity!
my_keys = sorted(archive.keys())
my_keys = sorted(archive.bytesdict.keys())
my_keys_indices = np.random.choice(len(my_keys), size=min(num, len(my_keys)), replace=False)
my_keys = [my_keys[i] for i in my_keys_indices]
# best pessimistic value in a random set of keys
return min(my_keys, key=lambda x: archive[x].pessimistic_confidence_bound)
return np.frombuffer(min(my_keys, key=lambda x: archive.bytesdict[x].pessimistic_confidence_bound))
1 change: 1 addition & 0 deletions nevergrad/optimization/oneshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def sampler(self) -> sequences.Sampler:
"LHS": sequences.LHSSampler,
}
self._sampler_instance = samplers[self._parameters.sampler](self.dimension, budget, scrambling=self._parameters.scrambled)
assert self._sampler_instance is not None
if self._parameters.rescaled:
self._rescaler = sequences.Rescaler(self.sampler)
self._sampler_instance.reinitialize() # sampler was consumed by the scaler
Expand Down
8 changes: 3 additions & 5 deletions nevergrad/optimization/optimizerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def __init__(self, dimension: int, budget: Optional[int] = None, num_workers: in

def _internal_ask(self) -> base.ArrayLike:
# pylint: disable=too-many-return-statements, too-many-branches
assert isinstance(self.archive, dict) # for typing, because following operations need changes for Archive
noise_handling = self._parameters.noise_handling
if not self._num_ask:
return np.zeros(self.dimension)
Expand All @@ -53,7 +52,7 @@ def _internal_ask(self) -> base.ArrayLike:
if self._num_ask <= limit:
if strategy in ["cubic", "random"]:
idx = np.random.choice(len(self.archive))
return list(self.archive.keys())[idx]
return np.frombuffer(list(self.archive.bytesdict.keys())[idx])
elif strategy == "optimistic":
return self.current_bests["optimistic"].x
# crossover
Expand Down Expand Up @@ -115,7 +114,7 @@ class ParametrizedOnePlusOne(base.ParametrizedFamily):
_optimizer_class = _OnePlusOne

def __init__(self, *, noise_handling: Optional[Union[str, Tuple[str, float]]] = None,
mutation: str = "gaussian", crossover: bool = False):
mutation: str = "gaussian", crossover: bool = False) -> None:
if noise_handling is not None:
if isinstance(noise_handling, str):
assert noise_handling in ["random", "optimistic"], f"Unkwnown noise handling: '{noise_handling}'"
Expand Down Expand Up @@ -524,8 +523,7 @@ def _internal_ask(self) -> base.ArrayLike:
if np.random.choice([True, False]):
# numpy does not accept choice on list of tuples, must choose index instead
idx = np.random.choice(len(self.archive))
assert isinstance(self.archive, dict) # for typing, because following operation need changes for Archive
return list(self.archive.keys())[idx]
return np.frombuffer(list(self.archive.bytesdict.keys())[idx])
return self.current_bests["optimistic"].x


Expand Down
8 changes: 4 additions & 4 deletions nevergrad/optimization/recastlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class ScipyOptimizer(base.ParametrizedFamily):
recast = True
_optimizer_class = _ScipyMinimizeBase

def __init__(self, *, method: str = "Nelder-Mead", random_restart: bool = False):
def __init__(self, *, method: str = "Nelder-Mead", random_restart: bool = False) -> None:
assert method in ["Nelder-Mead", "COBYLA", "SLSQP", "Powell"], f"Unknown method '{method}'"
self.method = method
self.random_restart = random_restart
Expand Down Expand Up @@ -95,7 +95,7 @@ def _optimization_function(self, objective_function: Callable[[base.ArrayLike],

def my_obj(**kwargs: Any) -> float:
v = [stats.norm.ppf(kwargs[str(i)]) for i in range(self.dimension)]
v = [min(max(v_, -100), 100) for v_ in v]
v = [min(max(v_, -100.), 100.) for v_ in v]
return -objective_function(v) # We minimize!

bounds = {}
Expand Down Expand Up @@ -132,7 +132,7 @@ def my_obj(**kwargs: Any) -> float:
bo.maximize(n_iter=budget - ip, init_points=ip)
# print [bo.res['max']['max_params'][str(i)] for i in xrange(self.dimension)]
v = [stats.norm.ppf(bo.res['max']['max_params'][str(i)]) for i in range(self.dimension)]
v = [min(max(v_, -100), 100) for v_ in v]
v = [min(max(v_, -100.), 100.) for v_ in v]
return v


Expand All @@ -146,7 +146,7 @@ class ParametrizedBO(base.ParametrizedFamily):
recast = True
_optimizer_class = _BO

def __init__(self, *, qr: str = "none"):
def __init__(self, *, qr: str = "none") -> None:
assert qr in ["r", "qr", "mqr", "lhs", "none"]
self.qr = qr
super().__init__()
Expand Down
12 changes: 6 additions & 6 deletions nevergrad/optimization/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ def __init__(self, num_workers: int = 1) -> None:

def _internal_ask(self) -> base.ArrayLike:
self.logs.append(f"s{self._num_ask}") # s for suggest
return np.array((self._num_ask,))
return np.array((float(self._num_ask),))

def _internal_tell(self, x: base.ArrayLike, value: float) -> None:
self.logs.append(f"u{x[0]}") # u for update
self.logs.append(f"u{int(x[0])}") # u for update


@genty.genty
Expand Down Expand Up @@ -87,12 +87,12 @@ def test_base_optimizer() -> None:
representation = repr(zeroptim)
assert "dimension=2" in representation, f"Unexpected representation: {representation}"
np.testing.assert_equal(zeroptim.ask(), [0, 0])
zeroptim.tell([0, 0], 0)
zeroptim.tell([1, 1], 1)
zeroptim.tell([0., 0], 0)
zeroptim.tell([1., 1], 1)
np.testing.assert_equal(zeroptim.provide_recommendation(), [0, 0])
# check that the best value is updated if a second evaluation is not as good
zeroptim.tell([0, 0], 10)
zeroptim.tell([1, 1], 1)
zeroptim.tell([0., 0], 10)
zeroptim.tell([1., 1], 1)
np.testing.assert_equal(zeroptim.provide_recommendation(), [1, 1])
np.testing.assert_equal(zeroptim._num_ask, 1)

Expand Down
10 changes: 6 additions & 4 deletions nevergrad/optimization/test_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Callable, Any
import numpy as np
import genty
from .utils import Value
from . import utils
from . import mutations


Expand Down Expand Up @@ -43,11 +43,13 @@ def test_run_with_array(self, func: Callable[..., Any]) -> None:
np.testing.assert_equal(output1, output2)

@genty.genty_dataset( # type: ignore
only_2=(2, "b"),
all_4=(4, "a"),
only_2=(2, 1.5),
all_4=(4, 0.5),
)
def test_get_roulette(self, num: int, expected: str) -> None:
np.random.seed(24)
archive = {"a": Value(0), "b": Value(1), "c": Value(2), "d": Value(3)}
archive = utils.Archive()
for k in range(4):
archive[np.array([k + .5])] = utils.Value(k)
output = mutations.get_roulette(archive, num)
np.testing.assert_equal(output, expected)
7 changes: 5 additions & 2 deletions nevergrad/optimization/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ def test_sequential_executor() -> None:
def test_get_nash() -> None:
zeroptim = Zero(dimension=1, budget=4, num_workers=1)
for k in range(4):
zeroptim.archive[(k,)] = utils.Value(k)
zeroptim.archive[(k,)].count += (4 - k)
array = (float(k),)
zeroptim.archive[array] = utils.Value(k)
zeroptim.archive[array].count += (4 - k)
nash = utils._get_nash(zeroptim)
testing.printed_assert_equal(nash, [((2,), 3), ((1,), 4), ((0,), 5)])
np.random.seed(12)
Expand All @@ -70,6 +71,8 @@ def test_archive() -> None:
assert isinstance(items[0][0], np.ndarray)
items = list(archive.keys_as_array())
assert isinstance(items[0], np.ndarray)
repr(archive)
str(archive)


def test_archive_errors() -> None:
Expand Down
12 changes: 10 additions & 2 deletions nevergrad/optimization/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def _get_nash(optimizer: Any) -> List[Tuple[Tuple[float, ...], int]]:
if threshold <= np.power(sum_num_trial, .25):
return [(optimizer.provide_recommendation(), 1)]
# make deterministic at the price of sort complexity
return sorted(((k, p.count) for k, p in optimizer.archive.items() if p.count >= threshold),
return sorted(((np.frombuffer(k), p.count) for k, p in optimizer.archive.bytesdict.items() if p.count >= threshold),
key=operator.itemgetter(1))


Expand Down Expand Up @@ -168,7 +168,9 @@ def _tobytes(x: ArrayLike) -> bytes:


class Archive:
"""A dict like object with numpy arrays as keys
"""A dict-like object with numpy arrays as keys.
The underlying `bytesdict` dict stores the arrays as bytes since arrays are not hashable.
Keys can be converted back with np.frombuffer(key)
"""

def __init__(self) -> None:
Expand Down Expand Up @@ -215,3 +217,9 @@ def keys_as_array(self) -> Iterator[np.ndarray]:
to np.ndarray using np.frombuffer(b)
"""
return (np.frombuffer(b) for b in self.bytesdict)

def __repr__(self):
return f"Archive with bytesdict: {self.bytesdict!r}"

def __str__(self):
return f"Archive with bytesdict: {self.bytesdict}"

0 comments on commit 62d3353

Please sign in to comment.