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

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""" 

8 

9import logging 

10 

11from typing import Dict, List, Optional, Tuple, Union 

12 

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 

27 

28_LOG = logging.getLogger(__name__) 

29 

30 

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 """ 

36 

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

38 SPECIAL = "special" 

39 RANGE = "range" 

40 

41 

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] 

48 

49 

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. 

56 

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). 

65 

66 Returns 

67 ------- 

68 cs : ConfigurationSpace 

69 A ConfigurationSpace object that corresponds to the Tunable. 

70 """ 

71 meta = {"group": group_name, "cost": cost} # {"scaling": ""} 

72 

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 }) 

82 

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}") 

98 

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}") 

125 

126 if not tunable.special: 

127 return ConfigurationSpace({tunable.name: range_hp}) 

128 

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]) 

135 

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)) 

159 

160 return conf_space 

161 

162 

163def tunable_groups_to_configspace(tunables: TunableGroups, seed: Optional[int] = None) -> ConfigurationSpace: 

164 """ 

165 Convert TunableGroups to hyperparameters in ConfigurationSpace. 

166 

167 Parameters 

168 ---------- 

169 tunables : TunableGroups 

170 A collection of tunable parameters. 

171 

172 seed : Optional[int] 

173 Random seed to use. 

174 

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 

187 

188 

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

190 """ 

191 Converts a TunableGroups current values to a ConfigSpace Configuration. 

192 

193 Parameters 

194 ---------- 

195 tunables : TunableGroups 

196 The TunableGroups to take the current value from. 

197 

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) 

217 

218 

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 

239 

240 

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. 

245 

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

247 

248 Parameters 

249 ---------- 

250 name : str 

251 The name of the tunable parameter. 

252 

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") 

261 

262 

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

264 """ 

265 Check if name corresponds to a temporary ConfigSpace parameter. 

266 

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

268 

269 Parameters 

270 ---------- 

271 name : str 

272 The name of the hyperparameter. 

273 

274 Returns 

275 ------- 

276 is_special : bool 

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

278 """ 

279 return name.endswith("!type") 

280 

281 

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

283 """ 

284 Remove the temporary suffix from a special parameter name. 

285 

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

287 

288 Parameters 

289 ---------- 

290 name : str 

291 The name of the hyperparameter. 

292 

293 Returns 

294 ------- 

295 stripped_name : str 

296 The name of the hyperparameter without the temporary suffix. 

297 """ 

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