Skip to content

Commit

Permalink
High-speed differential evolution (#1366)
Browse files Browse the repository at this point in the history
* High-speed differential evolution

* Update base.py

* Update optimizerlib.py

* fix

* fix

* clean

* clean

* Update differentialevolution.py

* Update nevergrad/optimization/differentialevolution.py

Co-authored-by: Jérémy Rapin <[email protected]>

* Update nevergrad/optimization/differentialevolution.py

Co-authored-by: Jérémy Rapin <[email protected]>

* fi

* fix

* fix

* fix

Co-authored-by: Jérémy Rapin <[email protected]>
  • Loading branch information
teytaud and jrapin authored Mar 13, 2022
1 parent 392e45d commit c403b08
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 71 deletions.
12 changes: 12 additions & 0 deletions nevergrad/optimization/differentialevolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np
import nevergrad.common.typing as tp
from nevergrad.parametrization import parameter as p
from . import metamodel
from . import base
from . import oneshot

Expand Down Expand Up @@ -114,6 +115,13 @@ def __init__(
self._no_hypervolume = self._config.multiobjective_adaptation

def recommend(self) -> p.Parameter: # This is NOT the naive version. We deal with noise.
sample_size = int((self.dimension * (self.dimension - 1)) / 2 + 2 * self.dimension + 1)
if self._config.high_speed and len(self.archive) >= sample_size:
try:
meta_data = metamodel.learn_on_k_best(self.archive, sample_size)
return self.parametrization.spawn_child().set_standardized_data(meta_data)
except metamodel.MetaModelFailure: # The optimum is at infinity. Shit happens.
pass # MetaModel failures are something which happens, no worries.
if self._config.recommendation != "noisy":
return self.current_bests[self._config.recommendation].parameter
med_fitness = np.median([p.loss for p in self.population.values() if p.loss is not None])
Expand Down Expand Up @@ -272,6 +280,8 @@ class DifferentialEvolution(base.ConfiguredOptimizer):
multiobjective_adaptation: bool
Automatically adapts to handle multiobjective case. This is a very basic **experimental** version,
activated by default because the non-multiobjective implementation is performing very badly.
high_speed: bool
Trying to make the optimization faster by a metamodel for the recommendation step.
"""

def __init__(
Expand All @@ -286,6 +296,7 @@ def __init__(
popsize: tp.Union[str, int] = "standard",
propagate_heritage: bool = False, # experimental
multiobjective_adaptation: bool = True,
high_speed: bool = False,
) -> None:
super().__init__(_DE, locals(), as_config=True)
assert recommendation in ["optimistic", "pessimistic", "noisy", "mean"]
Expand All @@ -303,6 +314,7 @@ def __init__(
]
self.initialization = initialization
self.scale = scale
self.high_speed = high_speed
self.recommendation = recommendation
self.propagate_heritage = propagate_heritage
self.F1 = F1
Expand Down
4 changes: 4 additions & 0 deletions nevergrad/optimization/experimentalvariants.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,7 @@
NoisyRL3 = Chaining([MixDeterministicRL, OptimisticNoisyOnePlusOne], ["half"]).set_name(
"NoisyRL3", register=True
)

# High-Speed variants
HSDE = DifferentialEvolution(high_speed=True).set_name("HSDE", register=True)
LhsHSDE = DifferentialEvolution(initialization="LHS", high_speed=True).set_name("LhsHSDE", register=True)
80 changes: 80 additions & 0 deletions nevergrad/optimization/metamodel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import numpy as np
import nevergrad.common.typing as tp
from . import utils
from .base import registry
from . import callbacks


class MetaModelFailure(ValueError):
"""Sometimes the optimum of the metamodel is at infinity."""


def learn_on_k_best(archive: utils.Archive[utils.MultiValue], k: int) -> tp.ArrayLike:
"""Approximate optimum learnt from the k best.
Parameters
----------
archive: utils.Archive[utils.Value]
"""
items = list(archive.items_as_arrays())
dimension = len(items[0][0])

# Select the k best.
first_k_individuals = sorted(items, key=lambda indiv: archive[indiv[0]].get_estimation("pessimistic"))[:k]
assert len(first_k_individuals) == k

# Recenter the best.
middle = np.array(sum(p[0] for p in first_k_individuals) / k)
normalization = 1e-15 + np.sqrt(np.sum((first_k_individuals[-1][0] - first_k_individuals[0][0]) ** 2))
y = np.asarray([archive[c[0]].get_estimation("pessimistic") for c in first_k_individuals])
X = np.asarray([(c[0] - middle) / normalization for c in first_k_individuals])

# We need SKLearn.
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures

polynomial_features = PolynomialFeatures(degree=2)
X2 = polynomial_features.fit_transform(X)

# Fit a linear model.
if not max(y) - min(y) > 1e-20: # better use "not" for dealing with nans
raise MetaModelFailure

y = (y - min(y)) / (max(y) - min(y))
model = LinearRegression()
model.fit(X2, y)

# Check model quality.
model_outputs = model.predict(X2)
indices = np.argsort(y)
ordered_model_outputs = [model_outputs[i] for i in indices]
if not np.all(np.diff(ordered_model_outputs) > 0):
raise MetaModelFailure("Unlearnable objective function.")

try:
Powell = registry["Powell"]
DE = registry["DE"]
for cls in (Powell, DE): # Powell excellent here, DE as a backup for thread safety.
optimizer = cls(parametrization=dimension, budget=45 * dimension + 30)
# limit to 20s at most
optimizer.register_callback("ask", callbacks.EarlyStopping.timer(20))
try:
minimum = optimizer.minimize(
lambda x: float(model.predict(polynomial_features.fit_transform(x[None, :])))
).value
except RuntimeError:
assert cls == Powell, "Only Powell is allowed to crash here."
else:
break
except ValueError:
raise MetaModelFailure("Infinite meta-model optimum in learn_on_k_best.")
if float(model.predict(polynomial_features.fit_transform(minimum[None, :]))) > y[0]:
raise MetaModelFailure("Not a good proposal.")
if np.sum(minimum ** 2) > 1.0:
raise MetaModelFailure("huge meta-model optimum in learn_on_k_best.")
return middle + normalization * minimum
74 changes: 3 additions & 71 deletions nevergrad/optimization/optimizerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
from nevergrad.parametrization import discretization
from nevergrad.parametrization import _layering
from nevergrad.parametrization import _datalayers
from . import callbacks
from . import oneshot
from . import base
from . import mutations
from . import utils
from .metamodel import MetaModelFailure as MetaModelFailure
from .metamodel import learn_on_k_best as learn_on_k_best
from .base import registry as registry
from .base import addCompare # pylint: disable=unused-import
from .base import IntOrParameter
Expand Down Expand Up @@ -1455,74 +1455,6 @@ def enable_pickling(self) -> None:
).set_name("MultiScaleCMA", register=True)


class MetaModelFailure(ValueError):
"""Sometimes the optimum of the metamodel is at infinity."""


def _learn_on_k_best(archive: utils.Archive[utils.MultiValue], k: int) -> tp.ArrayLike:
"""Approximate optimum learnt from the k best.
Parameters
----------
archive: utils.Archive[utils.Value]
"""
items = list(archive.items_as_arrays())
dimension = len(items[0][0])

# Select the k best.
first_k_individuals = sorted(items, key=lambda indiv: archive[indiv[0]].get_estimation("pessimistic"))[:k]
assert len(first_k_individuals) == k

# Recenter the best.
middle = np.array(sum(p[0] for p in first_k_individuals) / k)
normalization = 1e-15 + np.sqrt(np.sum((first_k_individuals[-1][0] - first_k_individuals[0][0]) ** 2))
y = np.asarray([archive[c[0]].get_estimation("pessimistic") for c in first_k_individuals])
X = np.asarray([(c[0] - middle) / normalization for c in first_k_individuals])

# We need SKLearn.
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures

polynomial_features = PolynomialFeatures(degree=2)
X2 = polynomial_features.fit_transform(X)

# Fit a linear model.
if not max(y) - min(y) > 1e-20: # better use "not" for dealing with nans
raise MetaModelFailure

y = (y - min(y)) / (max(y) - min(y))
model = LinearRegression()
model.fit(X2, y)

# Check model quality.
model_outputs = model.predict(X2)
indices = np.argsort(y)
ordered_model_outputs = [model_outputs[i] for i in indices]
if not np.all(np.diff(ordered_model_outputs) > 0):
raise MetaModelFailure("Unlearnable objective function.")

try:
for cls in (Powell, DE): # Powell excellent here, DE as a backup for thread safety.
optimizer = cls(parametrization=dimension, budget=45 * dimension + 30)
# limit to 20s at most
optimizer.register_callback("ask", callbacks.EarlyStopping.timer(20))
try:
minimum = optimizer.minimize(
lambda x: float(model.predict(polynomial_features.fit_transform(x[None, :])))
).value
except RuntimeError:
assert cls == Powell, "Only Powell is allowed to crash here."
else:
break
except ValueError:
raise MetaModelFailure("Infinite meta-model optimum in learn_on_k_best.")
if float(model.predict(polynomial_features.fit_transform(minimum[None, :]))) > y[0]:
raise MetaModelFailure("Not a good proposal.")
if np.sum(minimum ** 2) > 1.0:
raise MetaModelFailure("huge meta-model optimum in learn_on_k_best.")
return middle + normalization * minimum


class _MetaModel(base.Optimizer):
def __init__(
self,
Expand All @@ -1549,7 +1481,7 @@ def _internal_ask_candidate(self) -> p.Parameter:
freq = max(13, self.num_workers, self.dimension, int(self.frequency_ratio * sample_size))
if len(self.archive) >= sample_size and not self._num_ask % freq:
try:
data = _learn_on_k_best(self.archive, sample_size)
data = learn_on_k_best(self.archive, sample_size)
candidate = self.parametrization.spawn_child().set_standardized_data(data)
except MetaModelFailure: # The optimum is at infinity. Shit happens.
candidate = self._optim.ask()
Expand Down
2 changes: 2 additions & 0 deletions nevergrad/optimization/recorded_recommendations.csv
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ FastGADiscreteOnePlusOne,0.7531428339,1.095956118,0.0,1.3423563714,,,,,,,,,,,,
FastGANoisyDiscreteOnePlusOne,0.7531428339,1.095956118,0.0,1.3423563714,,,,,,,,,,,,
FastGAOptimisticNoisyDiscreteOnePlusOne,0.7531428339,1.095956118,0.0,1.3423563714,,,,,,,,,,,,
GeneticDE,1.012515477,-0.9138691467,-1.0295302074,1.2097964496,,,,,,,,,,,,
HSDE,0.5,-0.7999999785,-3.3e-09,4.0000000001,5.0000000231,2.7015115302,-2.080734155,-4.9499624832,,,,,,,,
HaltonSearch,-0.318639364,-0.7647096738,-0.7063025628,1.0675705239,,,,,,,,,,,,
HaltonSearchPlusMiddlePoint,0.0,0.0,0.0,0.0,,,,,,,,,,,,
HammersleySearch,0.2104283942,-1.1503493804,-0.1397102989,0.8416212336,,,,,,,,,,,,
Expand All @@ -109,6 +110,7 @@ IsoEMNATBPSA,0.0,0.0,0.0,0.0,,,,,,,,,,,,
LHSSearch,-0.3978418928,0.827925915,1.2070034191,1.3637174061,,,,,,,,,,,,
LargeHaltonSearch,-67.4489750196,43.0727299295,-25.3347103136,-56.5948821933,,,,,,,,,,,,
LhsDE,-0.8072358182,0.6354687554,1.575403308,1.1808277036,2.5888168575,-0.1627990771,-3.656466139,-1.040475202,,,,,,,,
LhsHSDE,-0.8072358182,0.6354687554,1.575403308,1.1808277036,2.5888168575,-0.1627990771,-3.656466139,-1.040475202,,,,,,,,
MetaCauchyRecentering,1.8789278226,-0.2085387973,-1.3832372686,3.9852740423,,,,,,,,,,,,
MetaModel,1.012515477,-0.9138805701,-1.029555946,1.2098418178,,,,,,,,,,,,
MetaModelDiagonalCMA,1.012515477,-0.9138691467,-1.0295302074,1.2097964496,,,,,,,,,,,,
Expand Down
1 change: 1 addition & 0 deletions nevergrad/optimization/test_optimizerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def test_infnan(name: str) -> None:
any(x == name for x in ["WidePSO", "SPSA", "NGOptBase", "Shiwa", "NGO"])
or isinstance(optim, (optlib.Portfolio, optlib._CMA, optlib.recaster.SequentialRecastOptimizer))
or "NGOpt" in name
or "HS" in name
or "MetaModelDiagonalCMA" in name
) # Second chance!
recom = optim.minimize(buggy_function)
Expand Down

0 comments on commit c403b08

Please sign in to comment.