Coverage for mlos_bench/mlos_bench/optimizers/convert_configspace.py: 98%
80 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-05 00:36 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-05 00:36 +0000
1#
2# Copyright (c) Microsoft Corporation.
3# Licensed under the MIT License.
4#
5"""
6Functions to convert TunableGroups to ConfigSpace for use with the mlos_core optimizers.
7"""
9import logging
11from typing import Dict, List, Optional, Tuple, Union
13from ConfigSpace import (
14 Beta,
15 CategoricalHyperparameter,
16 Configuration,
17 ConfigurationSpace,
18 EqualsCondition,
19 Float,
20 Integer,
21 Normal,
22 Uniform,
23)
24from mlos_bench.tunables.tunable import Tunable, TunableValue
25from mlos_bench.tunables.tunable_groups import TunableGroups
26from mlos_bench.util import try_parse_val, nullable
28_LOG = logging.getLogger(__name__)
31class TunableValueKind:
32 """
33 Enum for the kind of the tunable value (special or not).
34 It is not a true enum because ConfigSpace wants string values.
35 """
37 # pylint: disable=too-few-public-methods
38 SPECIAL = "special"
39 RANGE = "range"
42def _normalize_weights(weights: List[float]) -> List[float]:
43 """
44 Helper function for normalizing weights to probabilities.
45 """
46 total = sum(weights)
47 return [w / total for w in weights]
50def _tunable_to_configspace(
51 tunable: Tunable, group_name: Optional[str] = None, cost: int = 0) -> ConfigurationSpace:
52 """
53 Convert a single Tunable to an equivalent set of ConfigSpace Hyperparameter objects,
54 wrapped in a ConfigurationSpace for composability.
55 Note: this may be more than one Hyperparameter in the case of special value handling.
57 Parameters
58 ----------
59 tunable : Tunable
60 An mlos_bench Tunable object.
61 group_name : str
62 Human-readable id of the CovariantTunableGroup this Tunable belongs to.
63 cost : int
64 Cost to change this parameter (comes from the corresponding CovariantTunableGroup).
66 Returns
67 -------
68 cs : ConfigurationSpace
69 A ConfigurationSpace object that corresponds to the Tunable.
70 """
71 meta = {"group": group_name, "cost": cost} # {"scaling": ""}
73 if tunable.type == "categorical":
74 return ConfigurationSpace({
75 tunable.name: CategoricalHyperparameter(
76 name=tunable.name,
77 choices=tunable.categories,
78 weights=_normalize_weights(tunable.weights) if tunable.weights else None,
79 default_value=tunable.default,
80 meta=meta)
81 })
83 distribution: Union[Uniform, Normal, Beta, None] = None
84 if tunable.distribution == "uniform":
85 distribution = Uniform()
86 elif tunable.distribution == "normal":
87 distribution = Normal(
88 mu=tunable.distribution_params["mu"],
89 sigma=tunable.distribution_params["sigma"]
90 )
91 elif tunable.distribution == "beta":
92 distribution = Beta(
93 alpha=tunable.distribution_params["alpha"],
94 beta=tunable.distribution_params["beta"]
95 )
96 elif tunable.distribution is not None:
97 raise TypeError(f"Invalid Distribution Type: {tunable.distribution}")
99 if tunable.type == "int":
100 range_hp = Integer(
101 name=tunable.name,
102 bounds=(int(tunable.range[0]), int(tunable.range[1])),
103 log=bool(tunable.is_log),
104 q=nullable(int, tunable.quantization),
105 distribution=distribution,
106 default=(int(tunable.default)
107 if tunable.in_range(tunable.default) and tunable.default is not None
108 else None),
109 meta=meta
110 )
111 elif tunable.type == "float":
112 range_hp = Float(
113 name=tunable.name,
114 bounds=tunable.range,
115 log=bool(tunable.is_log),
116 q=tunable.quantization, # type: ignore[arg-type]
117 distribution=distribution, # type: ignore[arg-type]
118 default=(float(tunable.default)
119 if tunable.in_range(tunable.default) and tunable.default is not None
120 else None),
121 meta=meta
122 )
123 else:
124 raise TypeError(f"Invalid Parameter Type: {tunable.type}")
126 if not tunable.special:
127 return ConfigurationSpace({tunable.name: range_hp})
129 # Compute the probabilities of switching between regular and special values.
130 special_weights: Optional[List[float]] = None
131 switch_weights = [0.5, 0.5] # FLAML requires uniform weights.
132 if tunable.weights and tunable.range_weight is not None:
133 special_weights = _normalize_weights(tunable.weights)
134 switch_weights = _normalize_weights([sum(tunable.weights), tunable.range_weight])
136 # Create three hyperparameters: one for regular values,
137 # one for special values, and one to choose between the two.
138 (special_name, type_name) = special_param_names(tunable.name)
139 conf_space = ConfigurationSpace({
140 tunable.name: range_hp,
141 special_name: CategoricalHyperparameter(
142 name=special_name,
143 choices=tunable.special,
144 weights=special_weights,
145 default_value=tunable.default if tunable.default in tunable.special else None,
146 meta=meta
147 ),
148 type_name: CategoricalHyperparameter(
149 name=type_name,
150 choices=[TunableValueKind.SPECIAL, TunableValueKind.RANGE],
151 weights=switch_weights,
152 default_value=TunableValueKind.SPECIAL,
153 ),
154 })
155 conf_space.add_condition(EqualsCondition(
156 conf_space[special_name], conf_space[type_name], TunableValueKind.SPECIAL))
157 conf_space.add_condition(EqualsCondition(
158 conf_space[tunable.name], conf_space[type_name], TunableValueKind.RANGE))
160 return conf_space
163def tunable_groups_to_configspace(tunables: TunableGroups, seed: Optional[int] = None) -> ConfigurationSpace:
164 """
165 Convert TunableGroups to hyperparameters in ConfigurationSpace.
167 Parameters
168 ----------
169 tunables : TunableGroups
170 A collection of tunable parameters.
172 seed : Optional[int]
173 Random seed to use.
175 Returns
176 -------
177 configspace : ConfigurationSpace
178 A new ConfigurationSpace instance that corresponds to the input TunableGroups.
179 """
180 space = ConfigurationSpace(seed=seed)
181 for (tunable, group) in tunables:
182 space.add_configuration_space(
183 prefix="", delimiter="",
184 configuration_space=_tunable_to_configspace(
185 tunable, group.name, group.get_current_cost()))
186 return space
189def tunable_values_to_configuration(tunables: TunableGroups) -> Configuration:
190 """
191 Converts a TunableGroups current values to a ConfigSpace Configuration.
193 Parameters
194 ----------
195 tunables : TunableGroups
196 The TunableGroups to take the current value from.
198 Returns
199 -------
200 Configuration
201 A ConfigSpace Configuration.
202 """
203 values: Dict[str, TunableValue] = {}
204 for (tunable, _group) in tunables:
205 if tunable.special:
206 (special_name, type_name) = special_param_names(tunable.name)
207 if tunable.value in tunable.special:
208 values[type_name] = TunableValueKind.SPECIAL
209 values[special_name] = tunable.value
210 else:
211 values[type_name] = TunableValueKind.RANGE
212 values[tunable.name] = tunable.value
213 else:
214 values[tunable.name] = tunable.value
215 configspace = tunable_groups_to_configspace(tunables)
216 return Configuration(configspace, values=values)
219def configspace_data_to_tunable_values(data: dict) -> Dict[str, TunableValue]:
220 """
221 Remove the fields that correspond to special values in ConfigSpace.
222 In particular, remove and keys suffixes added by `special_param_names`.
223 """
224 data = data.copy()
225 specials = [
226 special_param_name_strip(k)
227 for k in data.keys() if special_param_name_is_temp(k)
228 ]
229 for k in specials:
230 (special_name, type_name) = special_param_names(k)
231 if data[type_name] == TunableValueKind.SPECIAL:
232 data[k] = data[special_name]
233 if special_name in data:
234 del data[special_name]
235 del data[type_name]
236 # May need to convert numpy values to regular types.
237 data = {k: try_parse_val(v) for k, v in data.items()}
238 return data
241def special_param_names(name: str) -> Tuple[str, str]:
242 """
243 Generate the names of the auxiliary hyperparameters that correspond
244 to a tunable that can have special values.
246 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic.
248 Parameters
249 ----------
250 name : str
251 The name of the tunable parameter.
253 Returns
254 -------
255 special_name : str
256 The name of the hyperparameter that corresponds to the special value.
257 type_name : str
258 The name of the hyperparameter that chooses between the regular and the special values.
259 """
260 return (name + "!special", name + "!type")
263def special_param_name_is_temp(name: str) -> bool:
264 """
265 Check if name corresponds to a temporary ConfigSpace parameter.
267 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic.
269 Parameters
270 ----------
271 name : str
272 The name of the hyperparameter.
274 Returns
275 -------
276 is_special : bool
277 True if the name corresponds to a temporary ConfigSpace hyperparameter.
278 """
279 return name.endswith("!type")
282def special_param_name_strip(name: str) -> str:
283 """
284 Remove the temporary suffix from a special parameter name.
286 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic.
288 Parameters
289 ----------
290 name : str
291 The name of the hyperparameter.
293 Returns
294 -------
295 stripped_name : str
296 The name of the hyperparameter without the temporary suffix.
297 """
298 return name.split("!", 1)[0]