result_grid.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. from typing import Optional, Union
  2. import pandas as pd
  3. import pyarrow
  4. from ray.air.result import Result
  5. from ray.exceptions import RayTaskError
  6. from ray.tune.analysis import ExperimentAnalysis
  7. from ray.tune.error import TuneError
  8. from ray.tune.experiment import Trial
  9. from ray.util import PublicAPI
  10. @PublicAPI(stability="beta")
  11. class ResultGrid:
  12. """A set of ``Result`` objects for interacting with Ray Tune results.
  13. You can use it to inspect the trials and obtain the best result.
  14. The constructor is a private API. This object can only be created as a result of
  15. ``Tuner.fit()``.
  16. Example:
  17. .. testcode::
  18. import random
  19. from ray import tune
  20. def random_error_trainable(config):
  21. if random.random() < 0.5:
  22. return {"loss": 0.0}
  23. else:
  24. raise ValueError("This is an error")
  25. tuner = tune.Tuner(
  26. random_error_trainable,
  27. run_config=tune.RunConfig(name="example-experiment"),
  28. tune_config=tune.TuneConfig(num_samples=10),
  29. )
  30. try:
  31. result_grid = tuner.fit()
  32. except ValueError:
  33. pass
  34. for i in range(len(result_grid)):
  35. result = result_grid[i]
  36. if not result.error:
  37. print(f"Trial finishes successfully with metrics"
  38. f"{result.metrics}.")
  39. else:
  40. print(f"Trial failed with error {result.error}.")
  41. .. testoutput::
  42. :hide:
  43. ...
  44. You can also use ``result_grid`` for more advanced analysis.
  45. >>> # Get the best result based on a particular metric.
  46. >>> best_result = result_grid.get_best_result( # doctest: +SKIP
  47. ... metric="loss", mode="min")
  48. >>> # Get the best checkpoint corresponding to the best result.
  49. >>> best_checkpoint = best_result.checkpoint # doctest: +SKIP
  50. >>> # Get a dataframe for the last reported results of all of the trials
  51. >>> df = result_grid.get_dataframe() # doctest: +SKIP
  52. >>> # Get a dataframe for the minimum loss seen for each trial
  53. >>> df = result_grid.get_dataframe(metric="loss", mode="min") # doctest: +SKIP
  54. Note that trials of all statuses are included in the final result grid.
  55. If a trial is not in terminated state, its latest result and checkpoint as
  56. seen by Tune will be provided.
  57. See :doc:`/tune/examples/tune_analyze_results` for more usage examples.
  58. """
  59. def __init__(
  60. self,
  61. experiment_analysis: ExperimentAnalysis,
  62. ):
  63. self._experiment_analysis = experiment_analysis
  64. self._results = [
  65. self._trial_to_result(trial) for trial in self._experiment_analysis.trials
  66. ]
  67. @property
  68. def experiment_path(self) -> str:
  69. """Path pointing to the experiment directory on persistent storage.
  70. This can point to a remote storage location (e.g. S3) or to a local
  71. location (path on the head node)."""
  72. return self._experiment_analysis.experiment_path
  73. @property
  74. def filesystem(self) -> pyarrow.fs.FileSystem:
  75. """Return the filesystem that can be used to access the experiment path.
  76. Returns:
  77. pyarrow.fs.FileSystem implementation.
  78. """
  79. return self._experiment_analysis._fs
  80. def get_best_result(
  81. self,
  82. metric: Optional[str] = None,
  83. mode: Optional[str] = None,
  84. scope: str = "last",
  85. filter_nan_and_inf: bool = True,
  86. ) -> Result:
  87. """Get the best result from all the trials run.
  88. Args:
  89. metric: Key for trial info to order on. Defaults to
  90. the metric specified in your Tuner's ``TuneConfig``.
  91. mode: One of [min, max]. Defaults to the mode specified
  92. in your Tuner's ``TuneConfig``.
  93. scope: One of [all, last, avg, last-5-avg, last-10-avg].
  94. If `scope=last`, only look at each trial's final step for
  95. `metric`, and compare across trials based on `mode=[min,max]`.
  96. If `scope=avg`, consider the simple average over all steps
  97. for `metric` and compare across trials based on
  98. `mode=[min,max]`. If `scope=last-5-avg` or `scope=last-10-avg`,
  99. consider the simple average over the last 5 or 10 steps for
  100. `metric` and compare across trials based on `mode=[min,max]`.
  101. If `scope=all`, find each trial's min/max score for `metric`
  102. based on `mode`, and compare trials based on `mode=[min,max]`.
  103. filter_nan_and_inf: If True (default), NaN or infinite
  104. values are disregarded and these trials are never selected as
  105. the best trial.
  106. """
  107. if len(self._experiment_analysis.trials) == 1:
  108. return self._trial_to_result(self._experiment_analysis.trials[0])
  109. if not metric and not self._experiment_analysis.default_metric:
  110. raise ValueError(
  111. "No metric is provided. Either pass in a `metric` arg to "
  112. "`get_best_result` or specify a metric in the "
  113. "`TuneConfig` of your `Tuner`."
  114. )
  115. if not mode and not self._experiment_analysis.default_mode:
  116. raise ValueError(
  117. "No mode is provided. Either pass in a `mode` arg to "
  118. "`get_best_result` or specify a mode in the "
  119. "`TuneConfig` of your `Tuner`."
  120. )
  121. best_trial = self._experiment_analysis.get_best_trial(
  122. metric=metric,
  123. mode=mode,
  124. scope=scope,
  125. filter_nan_and_inf=filter_nan_and_inf,
  126. )
  127. if not best_trial:
  128. error_msg = (
  129. "No best trial found for the given metric: "
  130. f"{metric or self._experiment_analysis.default_metric}. "
  131. "This means that no trial has reported this metric"
  132. )
  133. error_msg += (
  134. ", or all values reported for this metric are NaN. To not ignore NaN "
  135. "values, you can set the `filter_nan_and_inf` arg to False."
  136. if filter_nan_and_inf
  137. else "."
  138. )
  139. raise RuntimeError(error_msg)
  140. return self._trial_to_result(best_trial)
  141. def get_dataframe(
  142. self,
  143. filter_metric: Optional[str] = None,
  144. filter_mode: Optional[str] = None,
  145. ) -> pd.DataFrame:
  146. """Return dataframe of all trials with their configs and reported results.
  147. Per default, this returns the last reported results for each trial.
  148. If ``filter_metric`` and ``filter_mode`` are set, the results from each
  149. trial are filtered for this metric and mode. For example, if
  150. ``filter_metric="some_metric"`` and ``filter_mode="max"``, for each trial,
  151. every received result is checked, and the one where ``some_metric`` is
  152. maximal is returned.
  153. Example:
  154. .. testcode::
  155. import ray.tune
  156. def training_loop_per_worker(config):
  157. ray.tune.report({"accuracy": 0.8})
  158. result_grid = ray.tune.Tuner(
  159. trainable=training_loop_per_worker,
  160. run_config=ray.tune.RunConfig(name="my_tune_run")
  161. ).fit()
  162. # Get last reported results per trial
  163. df = result_grid.get_dataframe()
  164. # Get best ever reported accuracy per trial
  165. df = result_grid.get_dataframe(
  166. filter_metric="accuracy", filter_mode="max"
  167. )
  168. .. testoutput::
  169. :hide:
  170. ...
  171. Args:
  172. filter_metric: Metric to filter best result for.
  173. filter_mode: If ``filter_metric`` is given, one of ``["min", "max"]``
  174. to specify if we should find the minimum or maximum result.
  175. Returns:
  176. Pandas DataFrame with each trial as a row and their results as columns.
  177. """
  178. return self._experiment_analysis.dataframe(
  179. metric=filter_metric, mode=filter_mode
  180. )
  181. def __len__(self) -> int:
  182. return len(self._results)
  183. def __getitem__(self, i: int) -> Result:
  184. """Returns the i'th result in the grid."""
  185. return self._results[i]
  186. @property
  187. def errors(self):
  188. """Returns the exceptions of errored trials."""
  189. return [result.error for result in self if result.error]
  190. @property
  191. def num_errors(self):
  192. """Returns the number of errored trials."""
  193. return len(
  194. [t for t in self._experiment_analysis.trials if t.status == Trial.ERROR]
  195. )
  196. @property
  197. def num_terminated(self):
  198. """Returns the number of terminated (but not errored) trials."""
  199. return len(
  200. [
  201. t
  202. for t in self._experiment_analysis.trials
  203. if t.status == Trial.TERMINATED
  204. ]
  205. )
  206. @staticmethod
  207. def _populate_exception(trial: Trial) -> Optional[Union[TuneError, RayTaskError]]:
  208. if trial.status == Trial.TERMINATED:
  209. return None
  210. return trial.get_pickled_error() or trial.get_error()
  211. def _trial_to_result(self, trial: Trial) -> Result:
  212. cpm = trial.run_metadata.checkpoint_manager
  213. checkpoint = None
  214. if cpm.latest_checkpoint_result:
  215. checkpoint = cpm.latest_checkpoint_result.checkpoint
  216. best_checkpoint_results = cpm.best_checkpoint_results
  217. best_checkpoints = [
  218. (checkpoint_result.checkpoint, checkpoint_result.metrics)
  219. for checkpoint_result in best_checkpoint_results
  220. ]
  221. metrics_df = self._experiment_analysis.trial_dataframes.get(trial.trial_id)
  222. result = Result(
  223. checkpoint=checkpoint,
  224. metrics=trial.last_result.copy(),
  225. error=self._populate_exception(trial),
  226. path=trial.path,
  227. _storage_filesystem=self._experiment_analysis._fs,
  228. metrics_dataframe=metrics_df,
  229. best_checkpoints=best_checkpoints,
  230. )
  231. return result
  232. def __repr__(self) -> str:
  233. all_results_repr = [result._repr(indent=2) for result in self]
  234. all_results_repr = ",\n".join(all_results_repr)
  235. return f"ResultGrid<[\n{all_results_repr}\n]>"