Skip to content

Commit a7e6cbf

Browse files
authored
Merge pull request #221 from automl/217-add-converter-for-raytune
217 add converter for raytune
2 parents c636020 + 00a37f7 commit a7e6cbf

File tree

381 files changed

+1782
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

381 files changed

+1782
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Version 1.3.6
2+
3+
## Converter
4+
- Added a new converter to handle RayTune runs
5+
- Added example RayTune runs to logs
6+
17
# Version 1.3.5
28

39
## Access Specifier

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ install-optuna:
6161
install-bohb:
6262
$(PIP) install -e ".[bohb]"
6363

64+
install-raytune:
65+
$(PIP) install -e ".[raytune]"
66+
6467
check-black:
6568
$(BLACK) ${SOURCE_DIR} --check || :
6669
$(BLACK) ${EXAMPLES_DIR} --check || :

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ conda install -c anaconda swig
2525
pip install DeepCAVE
2626
```
2727

28-
To load runs created with Optuna or the BOHB optimizer, you need to install the
28+
To load runs created with Optuna, the BOHB optimizer or RayTune, you need to install the
2929
respective packages by running:
3030
```bash
3131
pip install deepcave[optuna]
3232
pip install deepcave[bohb]
33+
pip install deepcave[raytune]
3334
```
3435

3536
To try the examples for recording your results in DeepCAVE format, run this after installing:

deepcave/config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,17 @@ def CONVERTERS(self) -> List[Type["Run"]]:
156156
from deepcave.runs.converters.dataframe import DataFrameRun
157157
from deepcave.runs.converters.deepcave import DeepCAVERun
158158
from deepcave.runs.converters.optuna import OptunaRun
159+
from deepcave.runs.converters.raytune import RayTuneRun
159160
from deepcave.runs.converters.smac3v1 import SMAC3v1Run
160161
from deepcave.runs.converters.smac3v2 import SMAC3v2Run
161162

162-
return [AMLTKRun, BOHBRun, DeepCAVERun, OptunaRun, SMAC3v1Run, SMAC3v2Run, DataFrameRun]
163+
return [
164+
AMLTKRun,
165+
BOHBRun,
166+
DeepCAVERun,
167+
OptunaRun,
168+
RayTuneRun,
169+
SMAC3v1Run,
170+
SMAC3v2Run,
171+
DataFrameRun,
172+
]

deepcave/plugins/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,6 @@ def plugin_input_update(pathname: str, *inputs_list: str) -> List[Optional[str]]
306306
if passed_inputs is not None:
307307
# First get normal inputs
308308
inputs = self.load_inputs()
309-
310309
# Overwrite/set the passed inputs
311310
update_dict(inputs, passed_inputs)
312311

@@ -1096,7 +1095,6 @@ def load_run_inputs(
10961095
run_path = run.path
10971096
if run_path is not None:
10981097
run_name = run_path.parent.name + "/" + run.name
1099-
11001098
values.append(run.id)
11011099
labels.append(run_name)
11021100
disabled.append(False)

deepcave/plugins/budget/budget_correlation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ def process(run: AbstractRun, inputs: Dict[str, int]) -> Dict[str, Any]:
198198
objective_id
199199
]
200200
]
201+
201202
c2 += [
202203
run.get_avg_costs(config_id, budget2, statuses=[Status.SUCCESS])[0][
203204
objective_id

deepcave/plugins/hyperparameter/symbolic_explanations.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ def load_dependency_inputs(self, run, previous_inputs, inputs) -> Dict[str, Any]
294294
budgets = run.get_budgets(human=True)
295295
budget_ids = run.get_budget_ids()
296296
budget_options = get_checklist_options(budgets, budget_ids)
297-
298297
hp_dict = run.configspace.get_hyperparameters_dict()
299298
hp_names_numerical = []
300299
for k, v in hp_dict.items():

deepcave/runs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ def get_budget(self, id: Union[int, str], human: bool = False) -> Union[int, flo
561561
If the budget with this id is invalid.
562562
"""
563563
budgets = self.get_budgets(human=human)
564+
564565
return budgets[int(id)] # type: ignore
565566

566567
def get_budget_ids(self, include_combined: bool = True) -> List[int]:

deepcave/runs/converters/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@
3333
This module provides utilities to create an AMLTK (AutoML Toolkit) run.
3434
optuna
3535
This module provides utilities to create an Optuna run.
36+
raytune
37+
This module provides utilities to create a Raytune run.
3638
"""
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# noqa: D400
2+
"""
3+
# RayTuneRun
4+
5+
This module provides utilities to create a RayTune run.
6+
7+
## Classes
8+
- RayTuneRun: Define an RayTune run object.
9+
"""
10+
11+
import glob
12+
import json
13+
import os
14+
from pathlib import Path
15+
16+
from ConfigSpace import Configuration, ConfigurationSpace
17+
from ray.tune import ExperimentAnalysis
18+
19+
from deepcave.runs import Status
20+
from deepcave.runs.objective import Objective
21+
from deepcave.runs.run import Run
22+
from deepcave.utils.hash import file_to_hash
23+
24+
25+
class RayTuneRun(Run):
26+
"""
27+
Define a RayTune run object.
28+
29+
Properties
30+
----------
31+
path : Path
32+
The path to the run.
33+
"""
34+
35+
prefix = "RayTune"
36+
37+
@property
38+
def hash(self) -> str:
39+
"""
40+
Hash of the current run.
41+
42+
If the hash changes, the cache has to be cleared.
43+
This ensures that the cache always holds the latest results of the run.
44+
45+
Returns
46+
-------
47+
str
48+
The hash of the run.
49+
"""
50+
if self.path is None:
51+
return ""
52+
53+
# Use hash of experiment_stat as id
54+
return file_to_hash(self.path / "results.json")
55+
56+
@property
57+
def latest_change(self) -> float:
58+
"""
59+
Get the timestamp of the latest change.
60+
61+
Returns
62+
-------
63+
Union[float, int]
64+
The latest change.
65+
"""
66+
if self.path is None:
67+
return 0
68+
69+
return Path(self.path / "results.json").stat().st_mtime
70+
71+
@classmethod
72+
def from_path(cls, path: Path) -> "RayTuneRun":
73+
"""
74+
Return a Run object from a given path.
75+
76+
Parameters
77+
----------
78+
path: Path
79+
The path where the data to the run lies.
80+
81+
Returns
82+
-------
83+
RayTuneRun
84+
The run.
85+
"""
86+
configspace_new: dict
87+
hp_names = {}
88+
analysis = None
89+
analysis = ExperimentAnalysis(str(path)).results
90+
91+
# RayTune does not provide a configspace.json
92+
if not os.path.isfile(str(path) + "/configspace.json"):
93+
print(
94+
"The configspace.json file will be auto extracted. For more "
95+
"reliable results please provide your own configspace.json file or "
96+
"ajust the one provided. Numeric values will be treated as uniform values."
97+
" Please also check if the objectives bounds as well as its goal are as wanted."
98+
)
99+
100+
# Get the information of the configspace
101+
if not os.path.isfile(str(path) + "/configspace.json"):
102+
configspace_new = {
103+
"name": None,
104+
"hyperparameters": [],
105+
"conditions": [],
106+
"forbiddens": [],
107+
"python_module_version": "1.2.0",
108+
"format_version": 0.4,
109+
"comment": "The configspace.json file has been auto extracted. For more"
110+
" reliable results please provide your own configspace.json file or "
111+
"adjust the one provided. Numeric values will be treated as uniform values.",
112+
}
113+
# Get hyperparameters as well as upper and lower bounds, types etc
114+
115+
for key in analysis.keys():
116+
for hp, value in analysis[key]["config"].items():
117+
if hp not in hp_names:
118+
hp_names[hp] = [value]
119+
else:
120+
hp_names[hp].append(value)
121+
122+
for key, values in hp_names.items():
123+
if isinstance(values[0], str):
124+
values_set = set(values)
125+
configspace_new["hyperparameters"].append(
126+
{"type": "categorical", "name": key, "choices": list(values_set)}
127+
)
128+
else:
129+
configspace_new["hyperparameters"].append(
130+
{
131+
"type": "uniform_" + type(values[0]).__name__,
132+
"name": key,
133+
"lower": min(values),
134+
"upper": max(values),
135+
"default_value": type(values[0])((min(values) + max(values)) / 2),
136+
}
137+
)
138+
139+
with open(str(path) + "/configspace.json", "w") as f:
140+
json.dump(configspace_new, f)
141+
# Convert into a Configuration Space object
142+
configspace = ConfigurationSpace.from_json(path / "configspace.json")
143+
file_path = str(path) + "/experiment_state*"
144+
for filename in glob.glob(file_path):
145+
with open(filename, "r") as f:
146+
spamreader = json.load(f)
147+
nested_json_str = spamreader["trial_data"][0][0]
148+
obj = json.loads(nested_json_str)["trainable_name"]
149+
150+
objective = Objective(obj)
151+
run = RayTuneRun(name=str(path.stem), configspace=configspace, objectives=objective)
152+
run.path = path
153+
config = None
154+
# Get all information of the run
155+
156+
for result in analysis:
157+
# ConfigSpace shortens floats to a certain length
158+
for hp in analysis[result]["config"]:
159+
if not isinstance(analysis[result]["config"][hp], str):
160+
analysis[result]["config"][hp] = round(analysis[result]["config"][hp], 13)
161+
config = Configuration(
162+
configuration_space=configspace,
163+
values=analysis[result]["config"],
164+
config_id=analysis[result]["trial_id"],
165+
)
166+
if analysis[result]["done"]:
167+
status = Status.SUCCESS
168+
else:
169+
status = Status.CRASHED
170+
start_time = analysis[result]["timestamp"]
171+
end_time = start_time + analysis[result]["time_this_iter_s"]
172+
cost = analysis[result]["score"]
173+
174+
budget = []
175+
if os.path.isfile(str(path) + "/budget.json"):
176+
with open(str(path) + "/budget.json", "r") as f:
177+
budget_name = json.load(f)
178+
budget = analysis[result][budget_name]
179+
180+
# If a budget is not provided, the budget default is set to the number of trials
181+
else:
182+
budget = len(run.history) # type: ignore
183+
184+
run.add(
185+
costs=cost,
186+
config=config,
187+
budget=budget, # type: ignore
188+
seed=42,
189+
status=status,
190+
start_time=start_time,
191+
end_time=end_time,
192+
)
193+
194+
# The configs are stored, without results
195+
if not os.path.isfile(str(path) + "/configs.json"):
196+
config_dict = {id: config["config"] for id, config in analysis.items()}
197+
with open(str(path) + "/configs.json", "w") as f:
198+
json.dump(config_dict, f, indent=4)
199+
200+
# The results with
201+
if not os.path.isfile(str(path) + "/results.json"):
202+
# The first value of the results should be the result itself
203+
results_dict = {id: list(config.items())[0] for id, config in analysis.items()}
204+
with open(str(path) + "/results.json", "w") as f:
205+
json.dump(results_dict, f, indent=4)
206+
207+
return run
208+
209+
@classmethod
210+
def is_valid_run(cls, path_name: str) -> bool:
211+
"""
212+
Check whether the path name belongs to a valid smac3v2 run.
213+
214+
Parameters
215+
----------
216+
path_name: str
217+
The path to check.
218+
219+
Returns
220+
-------
221+
bool
222+
True if path is valid run.
223+
False otherwise.
224+
"""
225+
for file in Path(path_name).iterdir():
226+
if file.is_file() and file.name.startswith("experiment_state"):
227+
return True
228+
229+
return False

0 commit comments

Comments
 (0)