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

[DO NOT MERGE] Experiment with even more generic mutations/recombinations #612

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions nevergrad/optimization/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,12 @@ def ask(self) -> p.Parameter:
# only register actual asked points
if candidate.satisfies_constraints():
break # good to go!
else:
if self._penalize_cheap_violations or k == MAX_TENTATIVES - 2: # a tell may help before last tentative
self._internal_tell_candidate(candidate, float("Inf"))
self._num_ask += 1 # this is necessary for some algorithms which need new num to ask another point
if k == MAX_TENTATIVES - 1:
warnings.warn(f"Could not bypass the constraint after {MAX_TENTATIVES} tentatives, sending candidate anyway.")
# retry otherwise
if self._penalize_cheap_violations or k == MAX_TENTATIVES - 2: # a tell may help before last tentative
self._internal_tell_candidate(candidate, float("Inf"))
self._num_ask += 1 # this is necessary for some algorithms which need new num to ask another point
if k == MAX_TENTATIVES - 1:
warnings.warn(f"Could not bypass the constraint after {MAX_TENTATIVES} tentatives, sending candidate anyway.")
if not is_suggestion:
if candidate.uid in self._asked:
raise RuntimeError(
Expand Down
75 changes: 75 additions & 0 deletions nevergrad/optimization/es.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,78 @@ def __init__(
ES = EvolutionStrategy(recombination_ratio=0, only_offsprings=True, offsprings=60).set_name("ES", register=True)
MixES = EvolutionStrategy(recombination_ratio=0, only_offsprings=False, offsprings=20).set_name("MixES", register=True)
MutDE = EvolutionStrategy(recombination_ratio=0, only_offsprings=False, offsprings=None).set_name("MutDE", register=True)


class _PoolOp(base.Optimizer):
"""Experimental evolution-strategy-like algorithm
The behavior is going to evolve
"""

def __init__(
self,
parametrization: base.IntOrParameter,
budget: tp.Optional[int] = None,
num_workers: int = 1,
*,
config: tp.Optional["EvolutionStrategy"] = None
) -> None:
if budget is not None and budget < 60:
warnings.warn("ES algorithms are inefficient with budget < 60", base.InefficientSettingsWarning)
super().__init__(parametrization, budget=budget, num_workers=num_workers)
self._population: tp.Dict[str, p.Parameter] = {}
self._uid_queue = UidQueue()
self._waiting: tp.Dict[str, tp.List[p.Parameter]] = {}
# configuration
self._config = EvolutionStrategy() if config is None else config

def _internal_ask_candidate(self) -> p.Parameter:
if self.num_ask < self._config.popsize:
param = self.parametrization.sample()
assert param.uid == param.heritage["lineage"] # this is an assumption used below
self._uid_queue.asked.add(param.uid)
self._population[param.uid] = param
return param
uid = self._uid_queue.ask()
param = self._population[uid].spawn_child()
if self._rng.randint(5): # TODO adaptative
param.mutate()
else:
pool = [param_ for uid_, param_ in self._population.items() if uid_ != uid]
param.recombine(*pool, best=self.current_bests["average"].parameter)
return param

def _internal_tell_candidate(self, candidate: p.Parameter, value: float) -> None:
uid = candidate.heritage["lineage"]
self._uid_queue.tell(uid)
# past initialization
if self._config.offsprings is None:
if value < self._population[uid].loss: # type: ignore
self._population[uid] = candidate
else:
waiting = self._waiting.setdefault(uid, [])
waiting.append(candidate)
if len(waiting) >= self._config.offsprings:
waiting.sort(key=lambda x: x.loss)
self._population[uid] = waiting[0]
waiting.clear()


class PoolOp(base.ConfiguredOptimizer):
"""Experimental evolution-strategy-like algorithm
The API is going to evolve
"""

# pylint: disable=unused-argument
def __init__(
self,
*,
popsize: int = 40,
offsprings: tp.Optional[int] = None,
) -> None:
super().__init__(_PoolOp, locals(), as_config=True)
self.popsize = popsize
self.offsprings = offsprings


PoolOpPair = PoolOp().set_name("PoolOpPair", register=True)
PoolOp5 = PoolOp(popsize=10, offsprings=5).set_name("PoolOp5", register=True)
1 change: 1 addition & 0 deletions nevergrad/optimization/recorded_recommendations.csv
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ PCEDA,0.0,0.0,0.0,0.0,,,,,,,,,,,,
PSO,-0.276796063,-0.0964417266,0.4172787793,-1.0479873834,0.1282735211,3.095083081,-0.3336569052,-1.5281635527,-3.6761931612,-0.3160238771,0.7625955023,1.0816505694,-2.0040693902,-0.9629981075,,
ParaPortfolio,0.0,0.0,0.0,0.0,,,,,,,,,,,,
ParametrizationDE,0.6007366746,0.1949881274,0.1103879146,3.4094354968,1.2921548573,1.3492279731,-0.3681201995,-1.540716692,,,,,,,,
PoolOpPair,1.1400386808,0.3380024444,0.4755144618,2.6390460807,0.6911075733,1.111235567,-0.2576843178,-1.1959512855,,,,,,,,
Portfolio,1.3829941271,-0.318639364,-1.2206403488,1.7506860713,,,,,,,,,,,,
PortfolioDiscreteOnePlusOne,0.0,0.2169245995,-0.4007924638,1.4805504707,,,,,,,,,,,,
PortfolioNoisyDiscreteOnePlusOne,-0.581080115,0.8731348442,-0.4007924638,0.9105652814,,,,,,,,,,,,
Expand Down
3 changes: 2 additions & 1 deletion nevergrad/optimization/test_optimizerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def check_optimizer(
"MEDA",
"MicroCMA",
"ES",
"PoolOpPair",
]
DISCRETE = ["PBIL", "cGA"]
UNSEEDABLE: tp.List[str] = []
Expand Down Expand Up @@ -187,7 +188,7 @@ def test_optimizers_recommendation(name: str, recomkeeper: RecommendationKeeper)
random.seed(12) # may depend on non numpy generator
# budget=6 by default, larger for special cases needing more
budget = {"WidePSO": 100, "PSO": 200, "MEDA": 100, "EDA": 100, "MPCEDA": 100, "TBPSA": 100}.get(name, 6)
if isinstance(optimizer_cls, (optlib.DifferentialEvolution, optlib.EvolutionStrategy)):
if isinstance(optimizer_cls, (optlib.DifferentialEvolution, optlib.EvolutionStrategy, optlib.PoolOp)):
budget = 80
dimension = min(16, max(4, int(np.sqrt(budget))))
# set up problem
Expand Down
1 change: 0 additions & 1 deletion nevergrad/optimization/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from nevergrad.parametrization import parameter as p
from nevergrad.common.tools import OrderedSet
from nevergrad.common.typetools import ArrayLike
from nevergrad.parametrization import parameter as p


class MultiValue:
Expand Down
25 changes: 21 additions & 4 deletions nevergrad/parametrization/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ def sample(self: P) -> P:
child.heritage["lineage"] = child.uid
return child

def recombine(self: P, *others: P) -> None:
def recombine(
self: P,
*others: P,
auto: bool = False,
best: tp.Optional[P] = None,

) -> None:
"""Update value and parameters of this instance by combining it with
other instances.

Expand Down Expand Up @@ -419,7 +425,12 @@ def spawn_child(self: P, new_value: tp.Optional[tp.Any] = None) -> P:
self.value = new_value # check that it is equal
return self # no need to create another instance for a constant

def recombine(self: P, *others: P) -> None:
def recombine(
self: P,
*others: P,
auto: bool = False,
best: tp.Optional[P] = None,
) -> None:
pass

def mutate(self) -> None:
Expand Down Expand Up @@ -541,14 +552,20 @@ def sample(self: D) -> D:
child.heritage["lineage"] = child.uid
return child

def recombine(self, *others: "Dict") -> None:
def recombine(
self: D,
*others: D,
auto: bool = False,
best: tp.Optional[D] = None,
) -> None:
if not others:
return
# pylint: disable=pointless-statement
self.random_state # make sure to create one before using
assert all(isinstance(o, self.__class__) for o in others)
for k, param in self._content.items():
param.recombine(*[o[k] for o in others])
kbest = None if best is None else best[k]
param.recombine(*[o[k] for o in others], auto=auto, best=kbest)

def _internal_spawn_child(self: D) -> D:
child = self.__class__()
Expand Down
30 changes: 28 additions & 2 deletions nevergrad/parametrization/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ class Mutation(core.Parameter):
Recombinations should take several
"""

order = -1

def __init__(self, **kwargs: tp.Any):
if self.order < 1:
raise RuntimeError("Mutation order should have been specified in the implementation")
super().__init__(**kwargs)

@property
def value(self) -> tp.Callable[[tp.Sequence["Array"]], None]:
return self.apply
Expand All @@ -77,7 +84,18 @@ def value(self) -> tp.Callable[[tp.Sequence["Array"]], None]:
def value(self, value: tp.Any) -> None: # pylint: disable=unused-argument
raise RuntimeError("Mutation cannot be set.")

def apply_auto(self, arrays: tp.Sequence["Array"], best: tp.Optional["Array"] = None) -> None: # pylint: disable=unused-argument
selected_arrays = [arrays[0]]
if self.order > 1:
selected_arrays += self.random_state.choice(arrays[1:], self.order - 1, replace=False)
return self.apply(selected_arrays)

def apply(self, arrays: tp.Sequence["Array"]) -> None:
if len(arrays) != self.order:
raise Exception(f"{self.__class__.__name__} can only be applied between {self.order} individuals")
return self._apply(arrays)

def _apply(self, arrays: tp.Sequence["Array"]) -> None:
new_value = self._apply_array([a._value for a in arrays])
arrays[0]._value = new_value

Expand Down Expand Up @@ -400,7 +418,12 @@ def _to_reduced_space(self, value: np.ndarray) -> np.ndarray:
reduced = distribval / sigma
return reduced.ravel() # type: ignore

def recombine(self: A, *others: A) -> None:
def recombine(
self: A,
*others: A,
auto: bool = False,
best: tp.Optional[A] = None,
) -> None:
if not others:
return
recomb = self.parameters["recombination"].value
Expand All @@ -411,7 +434,10 @@ def recombine(self: A, *others: A) -> None:
all_arrays = [p.get_standardized_data(reference=self) for p in all_params]
self.set_standardized_data(np.mean(all_arrays, axis=0), deterministic=False)
elif isinstance(recomb, Mutation):
recomb.apply(all_params)
if auto:
recomb.apply_auto(all_params, best=best)
else:
recomb.apply(all_params)
elif callable(recomb):
recomb(all_params)
else:
Expand Down
Loading