Coverage for mlos_bench/mlos_bench/optimizers/convert_configspace.py: 98%

86 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-07 01:52 +0000

1# 

2# Copyright (c) Microsoft Corporation. 

3# Licensed under the MIT License. 

4# 

5"""Functions to convert TunableGroups to ConfigSpace for use with the mlos_core 

6optimizers. 

7""" 

8 

9import logging 

10from typing import Dict, Hashable, List, Optional, Tuple, Union 

11 

12from ConfigSpace import ( 

13 Beta, 

14 CategoricalHyperparameter, 

15 Configuration, 

16 ConfigurationSpace, 

17 EqualsCondition, 

18 Float, 

19 Integer, 

20 Normal, 

21 Uniform, 

22) 

23from ConfigSpace.hyperparameters import NumericalHyperparameter 

24from ConfigSpace.types import NotSet 

25 

26from mlos_bench.tunables.tunable import Tunable, TunableValue 

27from mlos_bench.tunables.tunable_groups import TunableGroups 

28from mlos_bench.util import try_parse_val 

29from mlos_core.spaces.converters.util import ( 

30 QUANTIZATION_BINS_META_KEY, 

31 monkey_patch_hp_quantization, 

32) 

33 

34_LOG = logging.getLogger(__name__) 

35 

36 

37class TunableValueKind: 

38 """ 

39 Enum for the kind of the tunable value (special or not). 

40 

41 It is not a true enum because ConfigSpace wants string values. 

42 """ 

43 

44 # pylint: disable=too-few-public-methods 

45 SPECIAL = "special" 

46 RANGE = "range" 

47 

48 

49def _normalize_weights(weights: List[float]) -> List[float]: 

50 """Helper function for normalizing weights to probabilities.""" 

51 total = sum(weights) 

52 return [w / total for w in weights] 

53 

54 

55def _tunable_to_configspace( 

56 tunable: Tunable, 

57 group_name: Optional[str] = None, 

58 cost: int = 0, 

59) -> ConfigurationSpace: 

60 """ 

61 Convert a single Tunable to an equivalent set of ConfigSpace Hyperparameter objects, 

62 wrapped in a ConfigurationSpace for composability. 

63 Note: this may be more than one Hyperparameter in the case of special value handling. 

64 

65 Parameters 

66 ---------- 

67 tunable : Tunable 

68 An mlos_bench Tunable object. 

69 group_name : str 

70 Human-readable id of the CovariantTunableGroup this Tunable belongs to. 

71 cost : int 

72 Cost to change this parameter (comes from the corresponding CovariantTunableGroup). 

73 

74 Returns 

75 ------- 

76 cs : ConfigurationSpace 

77 A ConfigurationSpace object that corresponds to the Tunable. 

78 """ 

79 # pylint: disable=too-complex 

80 meta: Dict[Hashable, TunableValue] = {"cost": cost} 

81 if group_name is not None: 

82 meta["group"] = group_name 

83 if tunable.is_numerical and tunable.quantization_bins: 

84 # Temporary workaround to dropped quantization support in ConfigSpace 1.0 

85 # See Also: https://github.com/automl/ConfigSpace/issues/390 

86 meta[QUANTIZATION_BINS_META_KEY] = tunable.quantization_bins 

87 

88 if tunable.type == "categorical": 

89 return ConfigurationSpace( 

90 { 

91 tunable.name: CategoricalHyperparameter( 

92 name=tunable.name, 

93 choices=tunable.categories, 

94 weights=_normalize_weights(tunable.weights) if tunable.weights else None, 

95 default_value=tunable.default, 

96 meta=meta, 

97 ) 

98 } 

99 ) 

100 

101 distribution: Union[Uniform, Normal, Beta, None] = None 

102 if tunable.distribution == "uniform": 

103 distribution = Uniform() 

104 elif tunable.distribution == "normal": 

105 distribution = Normal( 

106 mu=tunable.distribution_params["mu"], 

107 sigma=tunable.distribution_params["sigma"], 

108 ) 

109 elif tunable.distribution == "beta": 

110 distribution = Beta( 

111 alpha=tunable.distribution_params["alpha"], 

112 beta=tunable.distribution_params["beta"], 

113 ) 

114 elif tunable.distribution is not None: 

115 raise TypeError(f"Invalid Distribution Type: {tunable.distribution}") 

116 

117 range_hp: NumericalHyperparameter 

118 if tunable.type == "int": 

119 range_hp = Integer( 

120 name=tunable.name, 

121 bounds=(int(tunable.range[0]), int(tunable.range[1])), 

122 log=bool(tunable.is_log), 

123 distribution=distribution, 

124 default=( 

125 int(tunable.default) 

126 if tunable.in_range(tunable.default) and tunable.default is not None 

127 else None 

128 ), 

129 meta=meta, 

130 ) 

131 elif tunable.type == "float": 

132 range_hp = Float( 

133 name=tunable.name, 

134 bounds=tunable.range, 

135 log=bool(tunable.is_log), 

136 distribution=distribution, 

137 default=( 

138 float(tunable.default) 

139 if tunable.in_range(tunable.default) and tunable.default is not None 

140 else None 

141 ), 

142 meta=meta, 

143 ) 

144 else: 

145 raise TypeError(f"Invalid Parameter Type: {tunable.type}") 

146 

147 monkey_patch_hp_quantization(range_hp) 

148 if not tunable.special: 

149 return ConfigurationSpace(space=[range_hp]) 

150 

151 # Compute the probabilities of switching between regular and special values. 

152 special_weights: Optional[List[float]] = None 

153 switch_weights = [0.5, 0.5] # FLAML requires uniform weights. 

154 if tunable.weights and tunable.range_weight is not None: 

155 special_weights = _normalize_weights(tunable.weights) 

156 switch_weights = _normalize_weights([sum(tunable.weights), tunable.range_weight]) 

157 

158 # Create three hyperparameters: one for regular values, 

159 # one for special values, and one to choose between the two. 

160 (special_name, type_name) = special_param_names(tunable.name) 

161 conf_space = ConfigurationSpace( 

162 space=[ 

163 range_hp, 

164 CategoricalHyperparameter( 

165 name=special_name, 

166 choices=tunable.special, 

167 weights=special_weights, 

168 default_value=tunable.default if tunable.default in tunable.special else NotSet, 

169 meta=meta, 

170 ), 

171 CategoricalHyperparameter( 

172 name=type_name, 

173 choices=[TunableValueKind.SPECIAL, TunableValueKind.RANGE], 

174 weights=switch_weights, 

175 default_value=TunableValueKind.SPECIAL, 

176 ), 

177 ] 

178 ) 

179 conf_space.add( 

180 [ 

181 EqualsCondition( 

182 conf_space[special_name], conf_space[type_name], TunableValueKind.SPECIAL 

183 ), 

184 EqualsCondition( 

185 conf_space[tunable.name], conf_space[type_name], TunableValueKind.RANGE 

186 ), 

187 ] 

188 ) 

189 return conf_space 

190 

191 

192def tunable_groups_to_configspace( 

193 tunables: TunableGroups, 

194 seed: Optional[int] = None, 

195) -> ConfigurationSpace: 

196 """ 

197 Convert TunableGroups to hyperparameters in ConfigurationSpace. 

198 

199 Parameters 

200 ---------- 

201 tunables : TunableGroups 

202 A collection of tunable parameters. 

203 

204 seed : Optional[int] 

205 Random seed to use. 

206 

207 Returns 

208 ------- 

209 configspace : ConfigurationSpace 

210 A new ConfigurationSpace instance that corresponds to the input TunableGroups. 

211 """ 

212 space = ConfigurationSpace(seed=seed) 

213 for tunable, group in tunables: 

214 space.add_configuration_space( 

215 prefix="", 

216 delimiter="", 

217 configuration_space=_tunable_to_configspace( 

218 tunable, 

219 group.name, 

220 group.get_current_cost(), 

221 ), 

222 ) 

223 return space 

224 

225 

226def tunable_values_to_configuration(tunables: TunableGroups) -> Configuration: 

227 """ 

228 Converts a TunableGroups current values to a ConfigSpace Configuration. 

229 

230 Parameters 

231 ---------- 

232 tunables : TunableGroups 

233 The TunableGroups to take the current value from. 

234 

235 Returns 

236 ------- 

237 Configuration 

238 A ConfigSpace Configuration. 

239 """ 

240 values: Dict[str, TunableValue] = {} 

241 for tunable, _group in tunables: 

242 if tunable.special: 

243 (special_name, type_name) = special_param_names(tunable.name) 

244 if tunable.value in tunable.special: 

245 values[type_name] = TunableValueKind.SPECIAL 

246 values[special_name] = tunable.value 

247 else: 

248 values[type_name] = TunableValueKind.RANGE 

249 values[tunable.name] = tunable.value 

250 else: 

251 values[tunable.name] = tunable.value 

252 configspace = tunable_groups_to_configspace(tunables) 

253 return Configuration(configspace, values=values) 

254 

255 

256def configspace_data_to_tunable_values(data: dict) -> Dict[str, TunableValue]: 

257 """ 

258 Remove the fields that correspond to special values in ConfigSpace. 

259 

260 In particular, remove and keys suffixes added by `special_param_names`. 

261 """ 

262 data = data.copy() 

263 specials = [special_param_name_strip(k) for k in data.keys() if special_param_name_is_temp(k)] 

264 for k in specials: 

265 (special_name, type_name) = special_param_names(k) 

266 if data[type_name] == TunableValueKind.SPECIAL: 

267 data[k] = data[special_name] 

268 if special_name in data: 

269 del data[special_name] 

270 del data[type_name] 

271 # May need to convert numpy values to regular types. 

272 data = {k: try_parse_val(v) for k, v in data.items()} 

273 return data 

274 

275 

276def special_param_names(name: str) -> Tuple[str, str]: 

277 """ 

278 Generate the names of the auxiliary hyperparameters that correspond to a tunable 

279 that can have special values. 

280 

281 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic. 

282 

283 Parameters 

284 ---------- 

285 name : str 

286 The name of the tunable parameter. 

287 

288 Returns 

289 ------- 

290 special_name : str 

291 The name of the hyperparameter that corresponds to the special value. 

292 type_name : str 

293 The name of the hyperparameter that chooses between the regular and the special values. 

294 """ 

295 return (name + "!special", name + "!type") 

296 

297 

298def special_param_name_is_temp(name: str) -> bool: 

299 """ 

300 Check if name corresponds to a temporary ConfigSpace parameter. 

301 

302 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic. 

303 

304 Parameters 

305 ---------- 

306 name : str 

307 The name of the hyperparameter. 

308 

309 Returns 

310 ------- 

311 is_special : bool 

312 True if the name corresponds to a temporary ConfigSpace hyperparameter. 

313 """ 

314 return name.endswith("!type") 

315 

316 

317def special_param_name_strip(name: str) -> str: 

318 """ 

319 Remove the temporary suffix from a special parameter name. 

320 

321 NOTE: `!` characters are currently disallowed in Tunable names in order handle this logic. 

322 

323 Parameters 

324 ---------- 

325 name : str 

326 The name of the hyperparameter. 

327 

328 Returns 

329 ------- 

330 stripped_name : str 

331 The name of the hyperparameter without the temporary suffix. 

332 """ 

333 return name.split("!", 1)[0]