nevergrad_search.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import inspect
  2. import logging
  3. import math
  4. import pickle
  5. from typing import Dict, List, Optional, Sequence, Type, Union
  6. from ray.tune.result import DEFAULT_METRIC
  7. from ray.tune.search import (
  8. UNDEFINED_METRIC_MODE,
  9. UNDEFINED_SEARCH_SPACE,
  10. UNRESOLVED_SEARCH_SPACE,
  11. Searcher,
  12. )
  13. from ray.tune.search.sample import (
  14. Categorical,
  15. Domain,
  16. Float,
  17. Integer,
  18. LogUniform,
  19. Quantized,
  20. )
  21. from ray.tune.search.variant_generator import parse_spec_vars
  22. from ray.tune.utils.util import flatten_dict, unflatten_dict
  23. try:
  24. import nevergrad as ng
  25. from nevergrad.optimization import Optimizer
  26. from nevergrad.optimization.base import ConfiguredOptimizer
  27. Parameter = ng.p.Parameter
  28. except ImportError:
  29. ng = None
  30. Optimizer = None
  31. ConfiguredOptimizer = None
  32. Parameter = None
  33. logger = logging.getLogger(__name__)
  34. class NevergradSearch(Searcher):
  35. """Uses Nevergrad to optimize hyperparameters.
  36. Nevergrad is an open source tool from Facebook for derivative free
  37. optimization. More info can be found at:
  38. https://github.com/facebookresearch/nevergrad.
  39. You will need to install Nevergrad via the following command:
  40. .. code-block:: bash
  41. $ pip install nevergrad
  42. Parameters:
  43. optimizer: Optimizer class provided from Nevergrad.
  44. See here for available optimizers:
  45. https://facebookresearch.github.io/nevergrad/optimizers_ref.html#optimizers
  46. This can also be an instance of a `ConfiguredOptimizer`. See the
  47. section on configured optimizers in the above link.
  48. optimizer_kwargs: Kwargs passed in when instantiating the `optimizer`
  49. space: Nevergrad parametrization
  50. to be passed to optimizer on instantiation, or list of parameter
  51. names if you passed an optimizer object.
  52. metric: The training result objective value attribute. If None
  53. but a mode was passed, the anonymous metric `_metric` will be used
  54. per default.
  55. mode: One of {min, max}. Determines whether objective is
  56. minimizing or maximizing the metric attribute.
  57. points_to_evaluate: Initial parameter suggestions to be run
  58. first. This is for when you already have some good parameters
  59. you want to run first to help the algorithm make better suggestions
  60. for future parameters. Needs to be a list of dicts containing the
  61. configurations.
  62. Tune automatically converts search spaces to Nevergrad's format:
  63. .. code-block:: python
  64. import nevergrad as ng
  65. config = {
  66. "width": tune.uniform(0, 20),
  67. "height": tune.uniform(-100, 100),
  68. "activation": tune.choice(["relu", "tanh"])
  69. }
  70. current_best_params = [{
  71. "width": 10,
  72. "height": 0,
  73. "activation": relu",
  74. }]
  75. ng_search = NevergradSearch(
  76. optimizer=ng.optimizers.OnePlusOne,
  77. metric="mean_loss",
  78. mode="min",
  79. points_to_evaluate=current_best_params)
  80. run(my_trainable, config=config, search_alg=ng_search)
  81. If you would like to pass the search space manually, the code would
  82. look like this:
  83. .. code-block:: python
  84. import nevergrad as ng
  85. space = ng.p.Dict(
  86. width=ng.p.Scalar(lower=0, upper=20),
  87. height=ng.p.Scalar(lower=-100, upper=100),
  88. activation=ng.p.Choice(choices=["relu", "tanh"])
  89. )
  90. ng_search = NevergradSearch(
  91. optimizer=ng.optimizers.OnePlusOne,
  92. space=space,
  93. metric="mean_loss",
  94. mode="min")
  95. run(my_trainable, search_alg=ng_search)
  96. """
  97. def __init__(
  98. self,
  99. optimizer: Optional[
  100. Union[Optimizer, Type[Optimizer], ConfiguredOptimizer]
  101. ] = None,
  102. optimizer_kwargs: Optional[Dict] = None,
  103. space: Optional[Union[Dict, Parameter]] = None,
  104. metric: Optional[str] = None,
  105. mode: Optional[str] = None,
  106. points_to_evaluate: Optional[List[Dict]] = None,
  107. ):
  108. assert (
  109. ng is not None
  110. ), """Nevergrad must be installed!
  111. You can install Nevergrad with the command:
  112. `pip install nevergrad`."""
  113. if mode:
  114. assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
  115. super(NevergradSearch, self).__init__(metric=metric, mode=mode)
  116. self._space = None
  117. self._opt_factory = None
  118. self._nevergrad_opt = None
  119. self._optimizer_kwargs = optimizer_kwargs or {}
  120. if points_to_evaluate is None:
  121. self._points_to_evaluate = None
  122. elif not isinstance(points_to_evaluate, Sequence):
  123. raise ValueError(
  124. "Invalid object type passed for `points_to_evaluate`: "
  125. f"{type(points_to_evaluate)}. "
  126. "Please pass a list of points (dictionaries) instead."
  127. )
  128. else:
  129. self._points_to_evaluate = list(points_to_evaluate)
  130. if isinstance(space, dict) and space:
  131. resolved_vars, domain_vars, grid_vars = parse_spec_vars(space)
  132. if domain_vars or grid_vars:
  133. logger.warning(
  134. UNRESOLVED_SEARCH_SPACE.format(par="space", cls=type(self))
  135. )
  136. space = self.convert_search_space(space)
  137. if isinstance(optimizer, Optimizer):
  138. if space is not None and not isinstance(space, list):
  139. raise ValueError(
  140. "If you pass a configured optimizer to Nevergrad, either "
  141. "pass a list of parameter names or None as the `space` "
  142. "parameter."
  143. )
  144. if self._optimizer_kwargs:
  145. raise ValueError(
  146. "If you pass in optimizer kwargs, either pass "
  147. "an `Optimizer` subclass or an instance of "
  148. "`ConfiguredOptimizer`."
  149. )
  150. self._parameters = space
  151. self._nevergrad_opt = optimizer
  152. elif (
  153. inspect.isclass(optimizer) and issubclass(optimizer, Optimizer)
  154. ) or isinstance(optimizer, ConfiguredOptimizer):
  155. self._opt_factory = optimizer
  156. self._parameters = None
  157. self._space = space
  158. else:
  159. raise ValueError(
  160. "The `optimizer` argument passed to NevergradSearch must be "
  161. "either an `Optimizer` or a `ConfiguredOptimizer`."
  162. )
  163. self._live_trial_mapping = {}
  164. if self._nevergrad_opt or self._space:
  165. self._setup_nevergrad()
  166. def _setup_nevergrad(self):
  167. if self._opt_factory:
  168. self._nevergrad_opt = self._opt_factory(
  169. self._space, **self._optimizer_kwargs
  170. )
  171. # nevergrad.tell internally minimizes, so "max" => -1
  172. if self._mode == "max":
  173. self._metric_op = -1.0
  174. elif self._mode == "min":
  175. self._metric_op = 1.0
  176. if self._metric is None and self._mode:
  177. # If only a mode was passed, use anonymous metric
  178. self._metric = DEFAULT_METRIC
  179. if hasattr(self._nevergrad_opt, "instrumentation"): # added in v0.2.0
  180. if self._nevergrad_opt.instrumentation.kwargs:
  181. if self._nevergrad_opt.instrumentation.args:
  182. raise ValueError("Instrumented optimizers should use kwargs only")
  183. if self._parameters is not None:
  184. raise ValueError(
  185. "Instrumented optimizers should provide "
  186. "None as parameter_names"
  187. )
  188. else:
  189. if self._parameters is None:
  190. raise ValueError(
  191. "Non-instrumented optimizers should have "
  192. "a list of parameter_names"
  193. )
  194. if len(self._nevergrad_opt.instrumentation.args) != 1:
  195. raise ValueError("Instrumented optimizers should use kwargs only")
  196. if self._parameters is not None and self._nevergrad_opt.dimension != len(
  197. self._parameters
  198. ):
  199. raise ValueError(
  200. "len(parameters_names) must match optimizer "
  201. "dimension for non-instrumented optimizers"
  202. )
  203. if self._points_to_evaluate:
  204. # Nevergrad is LIFO, so we add the points to evaluate in reverse
  205. # order.
  206. for i in range(len(self._points_to_evaluate) - 1, -1, -1):
  207. self._nevergrad_opt.suggest(self._points_to_evaluate[i])
  208. def set_search_properties(
  209. self, metric: Optional[str], mode: Optional[str], config: Dict, **spec
  210. ) -> bool:
  211. if self._nevergrad_opt or self._space:
  212. return False
  213. space = self.convert_search_space(config)
  214. self._space = space
  215. if metric:
  216. self._metric = metric
  217. if mode:
  218. self._mode = mode
  219. self._setup_nevergrad()
  220. return True
  221. def suggest(self, trial_id: str) -> Optional[Dict]:
  222. if not self._nevergrad_opt:
  223. raise RuntimeError(
  224. UNDEFINED_SEARCH_SPACE.format(
  225. cls=self.__class__.__name__, space="space"
  226. )
  227. )
  228. if not self._metric or not self._mode:
  229. raise RuntimeError(
  230. UNDEFINED_METRIC_MODE.format(
  231. cls=self.__class__.__name__, metric=self._metric, mode=self._mode
  232. )
  233. )
  234. suggested_config = self._nevergrad_opt.ask()
  235. self._live_trial_mapping[trial_id] = suggested_config
  236. # in v0.2.0+, output of ask() is a Candidate,
  237. # with fields args and kwargs
  238. if not suggested_config.kwargs:
  239. if self._parameters:
  240. return unflatten_dict(
  241. dict(zip(self._parameters, suggested_config.args[0]))
  242. )
  243. return unflatten_dict(suggested_config.value)
  244. else:
  245. return unflatten_dict(suggested_config.kwargs)
  246. def on_trial_complete(
  247. self, trial_id: str, result: Optional[Dict] = None, error: bool = False
  248. ):
  249. """Notification for the completion of trial.
  250. The result is internally negated when interacting with Nevergrad
  251. so that Nevergrad Optimizers can "maximize" this value,
  252. as it minimizes on default.
  253. """
  254. if result:
  255. self._process_result(trial_id, result)
  256. self._live_trial_mapping.pop(trial_id)
  257. def _process_result(self, trial_id: str, result: Dict):
  258. ng_trial_info = self._live_trial_mapping[trial_id]
  259. self._nevergrad_opt.tell(ng_trial_info, self._metric_op * result[self._metric])
  260. def save(self, checkpoint_path: str):
  261. save_object = self.__dict__
  262. with open(checkpoint_path, "wb") as outputFile:
  263. pickle.dump(save_object, outputFile)
  264. def restore(self, checkpoint_path: str):
  265. with open(checkpoint_path, "rb") as inputFile:
  266. save_object = pickle.load(inputFile)
  267. self.__dict__.update(save_object)
  268. @staticmethod
  269. def convert_search_space(spec: Dict) -> Parameter:
  270. resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
  271. if grid_vars:
  272. raise ValueError(
  273. "Grid search parameters cannot be automatically converted "
  274. "to a Nevergrad search space."
  275. )
  276. # Flatten and resolve again after checking for grid search.
  277. spec = flatten_dict(spec, prevent_delimiter=True)
  278. resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
  279. def resolve_value(domain: Domain) -> Parameter:
  280. sampler = domain.get_sampler()
  281. if isinstance(sampler, Quantized):
  282. logger.warning(
  283. "Nevergrad does not support quantization. Dropped quantization."
  284. )
  285. sampler = sampler.get_sampler()
  286. if isinstance(domain, Float):
  287. if isinstance(sampler, LogUniform):
  288. return ng.p.Log(
  289. lower=domain.lower, upper=domain.upper, exponent=math.e
  290. )
  291. return ng.p.Scalar(lower=domain.lower, upper=domain.upper)
  292. elif isinstance(domain, Integer):
  293. if isinstance(sampler, LogUniform):
  294. return ng.p.Log(
  295. lower=domain.lower,
  296. upper=domain.upper - 1, # Upper bound exclusive
  297. exponent=math.e,
  298. ).set_integer_casting()
  299. return ng.p.Scalar(
  300. lower=domain.lower,
  301. upper=domain.upper - 1, # Upper bound exclusive
  302. ).set_integer_casting()
  303. elif isinstance(domain, Categorical):
  304. return ng.p.Choice(choices=domain.categories)
  305. raise ValueError(
  306. "Nevergrad does not support parameters of type "
  307. "`{}` with samplers of type `{}`".format(
  308. type(domain).__name__, type(domain.sampler).__name__
  309. )
  310. )
  311. # Parameter name is e.g. "a/b/c" for nested dicts
  312. space = {"/".join(path): resolve_value(domain) for path, domain in domain_vars}
  313. return ng.p.Dict(**space)