bohb_search.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. """BOHB (Bayesian Optimization with HyperBand)"""
  2. import copy
  3. import logging
  4. import math
  5. from typing import Dict, List, Optional, Union
  6. # use cloudpickle instead of pickle to make BOHB obj
  7. # pickleable
  8. from ray import cloudpickle
  9. from ray.tune.result import DEFAULT_METRIC
  10. from ray.tune.search import (
  11. UNDEFINED_METRIC_MODE,
  12. UNDEFINED_SEARCH_SPACE,
  13. UNRESOLVED_SEARCH_SPACE,
  14. Searcher,
  15. )
  16. from ray.tune.search.sample import (
  17. Categorical,
  18. Domain,
  19. Float,
  20. Integer,
  21. LogUniform,
  22. Normal,
  23. Quantized,
  24. Uniform,
  25. )
  26. from ray.tune.search.variant_generator import parse_spec_vars
  27. from ray.tune.utils.util import flatten_dict, unflatten_list_dict
  28. try:
  29. import ConfigSpace
  30. from hpbandster.optimizers.config_generators.bohb import BOHB
  31. except ImportError:
  32. BOHB = ConfigSpace = None
  33. logger = logging.getLogger(__name__)
  34. class _BOHBJobWrapper:
  35. """Mock object for HpBandSter to process."""
  36. def __init__(self, loss: float, budget: float, config: Dict):
  37. self.result = {"loss": loss}
  38. self.kwargs = {"budget": budget, "config": config.copy()}
  39. self.exception = None
  40. class TuneBOHB(Searcher):
  41. """BOHB suggestion component.
  42. Requires HpBandSter and ConfigSpace to be installed. You can install
  43. HpBandSter and ConfigSpace with: ``pip install hpbandster ConfigSpace``.
  44. This should be used in conjunction with HyperBandForBOHB.
  45. Args:
  46. space: Continuous ConfigSpace search space.
  47. Parameters will be sampled from this space which will be used
  48. to run trials.
  49. bohb_config: configuration for HpBandSter BOHB algorithm
  50. metric: The training result objective value attribute. If None
  51. but a mode was passed, the anonymous metric `_metric` will be used
  52. per default.
  53. mode: One of {min, max}. Determines whether objective is
  54. minimizing or maximizing the metric attribute.
  55. points_to_evaluate: Initial parameter suggestions to be run
  56. first. This is for when you already have some good parameters
  57. you want to run first to help the algorithm make better suggestions
  58. for future parameters. Needs to be a list of dicts containing the
  59. configurations.
  60. seed: Optional random seed to initialize the random number
  61. generator. Setting this should lead to identical initial
  62. configurations at each run.
  63. max_concurrent: Number of maximum concurrent trials.
  64. If this Searcher is used in a ``ConcurrencyLimiter``, the
  65. ``max_concurrent`` value passed to it will override the
  66. value passed here. Set to <= 0 for no limit on concurrency.
  67. Tune automatically converts search spaces to TuneBOHB's format:
  68. .. code-block:: python
  69. config = {
  70. "width": tune.uniform(0, 20),
  71. "height": tune.uniform(-100, 100),
  72. "activation": tune.choice(["relu", "tanh"])
  73. }
  74. algo = TuneBOHB(metric="mean_loss", mode="min")
  75. bohb = HyperBandForBOHB(
  76. time_attr="training_iteration",
  77. metric="mean_loss",
  78. mode="min",
  79. max_t=100)
  80. run(my_trainable, config=config, scheduler=bohb, search_alg=algo)
  81. If you would like to pass the search space manually, the code would
  82. look like this:
  83. .. code-block:: python
  84. import ConfigSpace as CS
  85. config_space = CS.ConfigurationSpace()
  86. config_space.add_hyperparameter(
  87. CS.UniformFloatHyperparameter("width", lower=0, upper=20))
  88. config_space.add_hyperparameter(
  89. CS.UniformFloatHyperparameter("height", lower=-100, upper=100))
  90. config_space.add_hyperparameter(
  91. CS.CategoricalHyperparameter(
  92. name="activation", choices=["relu", "tanh"]))
  93. algo = TuneBOHB(
  94. config_space, metric="mean_loss", mode="min")
  95. bohb = HyperBandForBOHB(
  96. time_attr="training_iteration",
  97. metric="mean_loss",
  98. mode="min",
  99. max_t=100)
  100. run(my_trainable, scheduler=bohb, search_alg=algo)
  101. """
  102. def __init__(
  103. self,
  104. space: Optional[Union[Dict, "ConfigSpace.ConfigurationSpace"]] = None,
  105. bohb_config: Optional[Dict] = None,
  106. metric: Optional[str] = None,
  107. mode: Optional[str] = None,
  108. points_to_evaluate: Optional[List[Dict]] = None,
  109. seed: Optional[int] = None,
  110. max_concurrent: int = 0,
  111. ):
  112. assert (
  113. BOHB is not None
  114. ), """HpBandSter must be installed!
  115. You can install HpBandSter with the command:
  116. `pip install hpbandster ConfigSpace`."""
  117. if mode:
  118. assert mode in ["min", "max"], "`mode` must be 'min' or 'max'."
  119. self.trial_to_params = {}
  120. self._metric = metric
  121. self._bohb_config = bohb_config
  122. if isinstance(space, dict) and space:
  123. resolved_vars, domain_vars, grid_vars = parse_spec_vars(space)
  124. if domain_vars or grid_vars:
  125. logger.warning(
  126. UNRESOLVED_SEARCH_SPACE.format(par="space", cls=type(self))
  127. )
  128. space = self.convert_search_space(space)
  129. self._space = space
  130. self._seed = seed
  131. self.running = set()
  132. self.paused = set()
  133. self._max_concurrent = max_concurrent
  134. self._points_to_evaluate = points_to_evaluate
  135. super(TuneBOHB, self).__init__(
  136. metric=self._metric,
  137. mode=mode,
  138. )
  139. if self._space:
  140. self._setup_bohb()
  141. def set_max_concurrency(self, max_concurrent: int) -> bool:
  142. self._max_concurrent = max_concurrent
  143. return True
  144. def _setup_bohb(self):
  145. from hpbandster.optimizers.config_generators.bohb import BOHB
  146. if self._metric is None and self._mode:
  147. # If only a mode was passed, use anonymous metric
  148. self._metric = DEFAULT_METRIC
  149. if self._mode == "max":
  150. self._metric_op = -1.0
  151. elif self._mode == "min":
  152. self._metric_op = 1.0
  153. if self._seed is not None:
  154. self._space.seed(self._seed)
  155. self.running = set()
  156. self.paused = set()
  157. bohb_config = self._bohb_config or {}
  158. self.bohber = BOHB(self._space, **bohb_config)
  159. def set_search_properties(
  160. self, metric: Optional[str], mode: Optional[str], config: Dict, **spec
  161. ) -> bool:
  162. if self._space:
  163. return False
  164. space = self.convert_search_space(config)
  165. self._space = space
  166. if metric:
  167. self._metric = metric
  168. if mode:
  169. self._mode = mode
  170. self._setup_bohb()
  171. return True
  172. def suggest(self, trial_id: str) -> Optional[Dict]:
  173. if not self._space:
  174. raise RuntimeError(
  175. UNDEFINED_SEARCH_SPACE.format(
  176. cls=self.__class__.__name__, space="space"
  177. )
  178. )
  179. if not self._metric or not self._mode:
  180. raise RuntimeError(
  181. UNDEFINED_METRIC_MODE.format(
  182. cls=self.__class__.__name__, metric=self._metric, mode=self._mode
  183. )
  184. )
  185. max_concurrent = (
  186. self._max_concurrent if self._max_concurrent > 0 else float("inf")
  187. )
  188. if len(self.running) >= max_concurrent:
  189. return None
  190. if self._points_to_evaluate:
  191. config = self._points_to_evaluate.pop(0)
  192. else:
  193. # This parameter is not used in hpbandster implementation.
  194. config, _ = self.bohber.get_config(None)
  195. self.trial_to_params[trial_id] = copy.deepcopy(config)
  196. self.running.add(trial_id)
  197. return unflatten_list_dict(config)
  198. def on_trial_result(self, trial_id: str, result: Dict):
  199. if trial_id not in self.paused:
  200. self.running.add(trial_id)
  201. if "hyperband_info" not in result:
  202. logger.warning(
  203. "BOHB Info not detected in result. Are you using "
  204. "HyperBandForBOHB as a scheduler?"
  205. )
  206. elif "budget" in result.get("hyperband_info", {}):
  207. hbs_wrapper = self.to_wrapper(trial_id, result)
  208. self.bohber.new_result(hbs_wrapper)
  209. def on_trial_complete(
  210. self, trial_id: str, result: Optional[Dict] = None, error: bool = False
  211. ):
  212. del self.trial_to_params[trial_id]
  213. self.paused.discard(trial_id)
  214. self.running.discard(trial_id)
  215. def to_wrapper(self, trial_id: str, result: Dict) -> _BOHBJobWrapper:
  216. return _BOHBJobWrapper(
  217. self._metric_op * result[self.metric],
  218. result["hyperband_info"]["budget"],
  219. self.trial_to_params[trial_id],
  220. )
  221. # BOHB Specific.
  222. # TODO(team-ml): Refactor alongside HyperBandForBOHB
  223. def on_pause(self, trial_id: str):
  224. self.paused.add(trial_id)
  225. self.running.discard(trial_id)
  226. def on_unpause(self, trial_id: str):
  227. self.paused.discard(trial_id)
  228. self.running.add(trial_id)
  229. @staticmethod
  230. def convert_search_space(spec: Dict) -> "ConfigSpace.ConfigurationSpace":
  231. resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
  232. if grid_vars:
  233. raise ValueError(
  234. "Grid search parameters cannot be automatically converted "
  235. "to a TuneBOHB search space."
  236. )
  237. # Flatten and resolve again after checking for grid search.
  238. spec = flatten_dict(spec, prevent_delimiter=True)
  239. resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)
  240. def resolve_value(
  241. par: str, domain: Domain
  242. ) -> ConfigSpace.hyperparameters.Hyperparameter:
  243. quantize = None
  244. sampler = domain.get_sampler()
  245. if isinstance(sampler, Quantized):
  246. quantize = sampler.q
  247. sampler = sampler.sampler
  248. if isinstance(domain, Float):
  249. if isinstance(sampler, LogUniform):
  250. lower = domain.lower
  251. upper = domain.upper
  252. if quantize:
  253. lower = math.ceil(domain.lower / quantize) * quantize
  254. upper = math.floor(domain.upper / quantize) * quantize
  255. return ConfigSpace.UniformFloatHyperparameter(
  256. par, lower=lower, upper=upper, q=quantize, log=True
  257. )
  258. elif isinstance(sampler, Uniform):
  259. lower = domain.lower
  260. upper = domain.upper
  261. if quantize:
  262. lower = math.ceil(domain.lower / quantize) * quantize
  263. upper = math.floor(domain.upper / quantize) * quantize
  264. return ConfigSpace.UniformFloatHyperparameter(
  265. par, lower=lower, upper=upper, q=quantize, log=False
  266. )
  267. elif isinstance(sampler, Normal):
  268. return ConfigSpace.hyperparameters.NormalFloatHyperparameter(
  269. par, mu=sampler.mean, sigma=sampler.sd, q=quantize, log=False
  270. )
  271. elif isinstance(domain, Integer):
  272. if isinstance(sampler, LogUniform):
  273. lower = domain.lower
  274. upper = domain.upper
  275. if quantize:
  276. lower = math.ceil(domain.lower / quantize) * quantize
  277. upper = math.floor(domain.upper / quantize) * quantize
  278. else:
  279. # Tune search space integers are exclusive
  280. upper -= 1
  281. return ConfigSpace.UniformIntegerHyperparameter(
  282. par, lower=lower, upper=upper, q=quantize, log=True
  283. )
  284. elif isinstance(sampler, Uniform):
  285. lower = domain.lower
  286. upper = domain.upper
  287. if quantize:
  288. lower = math.ceil(domain.lower / quantize) * quantize
  289. upper = math.floor(domain.upper / quantize) * quantize
  290. else:
  291. # Tune search space integers are exclusive
  292. upper -= 1
  293. return ConfigSpace.UniformIntegerHyperparameter(
  294. par, lower=lower, upper=upper, q=quantize, log=False
  295. )
  296. elif isinstance(domain, Categorical):
  297. if isinstance(sampler, Uniform):
  298. return ConfigSpace.CategoricalHyperparameter(
  299. par, choices=domain.categories
  300. )
  301. raise ValueError(
  302. "TuneBOHB does not support parameters of type "
  303. "`{}` with samplers of type `{}`".format(
  304. type(domain).__name__, type(domain.sampler).__name__
  305. )
  306. )
  307. cs = ConfigSpace.ConfigurationSpace()
  308. for path, domain in domain_vars:
  309. par = "/".join(str(p) for p in path)
  310. value = resolve_value(par, domain)
  311. cs.add_hyperparameter(value)
  312. return cs
  313. def save(self, checkpoint_path: str):
  314. save_object = self.__dict__
  315. with open(checkpoint_path, "wb") as outputFile:
  316. cloudpickle.dump(save_object, outputFile)
  317. def restore(self, checkpoint_path: str):
  318. with open(checkpoint_path, "rb") as inputFile:
  319. save_object = cloudpickle.load(inputFile)
  320. self.__dict__.update(save_object)