Skip to content

Commit

Permalink
Dedicate half constraint budget to a suboptimizer (#1047)
Browse files Browse the repository at this point in the history
* cst_solver

* warning

* warning

* fix

* black

* fix keras issue

* Update test_mlfunctionlib.py

* black

* [PR on cstsolve PR] Try to extract constraint solving (#968)

* returnpenalties

* fix

* removeprints

* extract

Co-authored-by: Jeremy Rapin <jrapin@fb.com>

* Update utils.py

* Fix a bug.

The previous code was ok only if the optimization method is not limited by the budget.

* Update base.py

* fix

* fix

* Update base.py

* Update base.py

* Update base.py

* fix

* fix

* fix

* fix_comment

* fixes

* black

* fix

* fix

* fix_typo

* Update nevergrad/optimization/base.py

Co-authored-by: Jérémy Rapin <jrapin.github@gmail.com>

* Update nevergrad/optimization/base.py

* Clarify suggest arguments (#1072)

* Skip rocket xp test for speed (#1075)

* Update parametrization flatten function (#1074)

* Add configuration for PSO + simplifications (#1073)

* Remove deprecated stuff (#1041)

* fix keras issue

* Update test_mlfunctionlib.py

* black

* remove_useless_stuff

* Update test_core.py

* fix

* Update core.py

* Morphing with Nevergrad (#1042)

* fix keras issue

* Update test_mlfunctionlib.py

* black

* WIP morphing

* fix

* Update core.py

* black

* fix

* fix

* syntax_fix

* fix

* fix

* fix

* fix

* fix

* black

* Update core.py

* Update core.py

* Add MOO xp variants (#1004)

* MOO variants

* Update experiments.py

* better_alignment

* better_doc

* fix

* minor

* fix

* CHANGELOG

* black

* fix

* [PR on PR] Fix constraints (#1079)

Co-authored-by: Jeremy Rapin <jrapin@fb.com>
Co-authored-by: Jérémy Rapin <jrapin.github@gmail.com>
  • Loading branch information
3 people authored Mar 17, 2021
1 parent bca7fa4 commit 7fadec0
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 21 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
variables since in this case `parameter.sample()` samples uniformly (unless otherwise specified).
The previous behavior can be obtained with `RandomSearchMaker(sampler="gaussian")`.
- `PSO` API has been slightly changed [#1073](https://github.com/facebookresearch/nevergrad/pull/1073)
- Half the budget alloted to solve cheap constrained is now used by a sub-optimizer
[#1047](https://github.com/facebookresearch/nevergrad/pull/1047). More changes of constraint management will land
in the near future.

### Important changes

Expand Down
59 changes: 45 additions & 14 deletions nevergrad/optimization/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,30 +432,39 @@ def ask(self) -> p.Parameter:
callback(self)
current_num_ask = self.num_ask
# tentatives if a cheap constraint is available
# TODO: this should be replaced by an optimization algorithm.
max_trials = self._constraints_manager.max_trials
for k in range(max_trials):
max_trials = max(1, self._constraints_manager.max_trials // 2)
# half will be used for sub-optimization --- if the optimization method does not need/use a budget.
# TODO(oteytaud): actually we could do this even when the budget is known, if we are sure that
# exceeding the budget is not a problem.
# Very simple constraint solver:
# - we use a simple algorithm.
# - no memory of previous iterations.
# - just projection to constraint satisfaction.
# We try using the normal tool during half constraint budget, in order to reduce the impact on the normal run.
for _ in range(max_trials):
is_suggestion = False
if self._suggestions:
if self._suggestions: # use suggestions if available
is_suggestion = True
candidate = self._suggestions.pop()
else:
candidate = self._internal_ask_candidate()
# only register actual asked points
if candidate.satisfies_constraints():
break # good to go!
if self._penalize_cheap_violations:
# TODO using a suboptimizer instead may help remove this
# Warning! This might be a tell not asked.
self._internal_tell_candidate(candidate, float("Inf")) # DE requires a tell
self._num_ask += (
1 # this is necessary for some algorithms which need new num to ask another point
# updating num_ask is necessary for some algorithms which need new num to ask another point
self._num_ask += 1
satisfies = candidate.satisfies_constraints()
if not satisfies:
# still not solving, let's run sub-optimization
candidate = _constraint_solver(candidate, budget=max_trials)
if not (satisfies or candidate.satisfies_constraints()):
warnings.warn(
f"Could not bypass the constraint after {max_trials} tentatives, "
"sending candidate anyway.",
errors.FailedConstraintWarning,
)
if k == max_trials - 1:
warnings.warn(
f"Could not bypass the constraint after {max_trials} tentatives, "
"sending candidate anyway.",
errors.FailedConstraintWarning,
)
if not is_suggestion:
if candidate.uid in self._asked:
raise RuntimeError(
Expand Down Expand Up @@ -736,3 +745,25 @@ def __eq__(self, other: tp.Any) -> tp.Any:
if self._config == other._config:
return True
return False


def _constraint_solver(parameter: p.Parameter, budget: int) -> p.Parameter:
"""Runs a suboptimization to solve the parameter constraints"""
parameter_without_constraint = parameter.copy()
parameter_without_constraint._constraint_checkers.clear()
opt = registry["OnePlusOne"](parameter_without_constraint, num_workers=1, budget=budget)
for _ in range(budget):
cand = opt.ask()
# Our objective function is minimum for the point the closest to
# the original candidate under the constraints.
penalty = sum(utils._float_penalty(func(cand.value)) for func in parameter._constraint_checkers)

# TODO: this may not scale well with dimension
distance = np.tanh(np.sum(cand.get_standardized_data(reference=parameter) ** 2))
# TODO: because of the return whenever constraints are satisfied, the first case never arises
loss = distance if penalty <= 0 else penalty + distance + 1.0
opt.tell(cand, loss)
if penalty <= 0: # constraints are satisfied
break
data = opt.recommend().get_standardized_data(reference=parameter_without_constraint)
return parameter.spawn_child().set_standardized_data(data)
9 changes: 4 additions & 5 deletions nevergrad/optimization/test_optimizerlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,24 +523,23 @@ def _ellips(x: np.ndarray) -> float:
"penalization,expected,as_layer",
[
(False, [1.005573e00, 3.965783e-04], False),
(True, [0.999987, -0.322118], False),
(True, [0.999975, -0.111235], False),
(False, [1.000760, -5.116619e-4], True),
],
)
@testing.suppress_nevergrad_warnings() # hides failed constraints
def test_constrained_optimization(penalization: bool, expected: tp.List[float], as_layer: bool) -> None:
def constraint(i: tp.Any) -> tp.Union[bool, float]:
if penalization:
return -float(abs(i[1]["x"][0] - 1))
out = i[1]["x"][0] >= 1
return out if not as_layer else float(not out)

parametrization = ng.p.Instrumentation(x=ng.p.Array(shape=(1,)), y=ng.p.Scalar())
optimizer = optlib.OnePlusOne(parametrization, budget=100)
optimizer.parametrization.random_state.seed(12)
if penalization:
optimizer._constraints_manager.update(max_trials=2, penalty_factor=10)

def constraint(i: tp.Any) -> tp.Union[bool, float]: # pylint: disable=function-redefined
return -abs(i[1]["x"][0] - 1)
optimizer._constraints_manager.update(max_trials=10, penalty_factor=10)

with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
Expand Down
2 changes: 0 additions & 2 deletions nevergrad/parametrization/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def __init__(self) -> None:
# Additional convenient features
self._random_state: tp.Optional[np.random.RandomState] = None # lazy initialization
self._generation = 0
# self._constraint_checkers: tp.List[tp.Union[tp.Callable[[tp.Any], bool], tp.Callable[[tp.Any], float]]] = []
self._constraint_checkers: tp.List[tp.Callable[[tp.Any], tp.Union[bool, float]]] = []
self._name: tp.Optional[str] = None
self._frozen = False
Expand Down Expand Up @@ -240,7 +239,6 @@ def __repr__(self) -> str:
return ":".join(strings)

# %% Constraint management

def satisfies_constraints(self) -> bool:
"""Whether the instance satisfies the constraints added through
the `register_cheap_constraint` method
Expand Down

0 comments on commit 7fadec0

Please sign in to comment.