Skip to content

Commit ff54b4f

Browse files
committed
sign
1 parent 964643b commit ff54b4f

5 files changed

Lines changed: 137 additions & 4 deletions

File tree

extension_templates/experiments.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class MyExperiment(BaseExperiment):
7777
# valid values: "random", "deterministic"
7878
# if "deterministic", two calls of score must result in the same value
7979
#
80+
"property:higher_or_lower_is_better": "lower",
81+
# valid values: "higher", "lower", "mixed"
82+
# whether higher or lower scores are better
83+
#
8084
# --------------
8185
# packaging info
8286
# --------------

src/hyperactive/base/_experiment.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ class BaseExperiment(BaseObject):
1414
"property:randomness": "random", # random or deterministic
1515
# if deterministic, two calls of score will result in the same value
1616
# random = two calls may result in different values; same as "stochastic"
17+
"property:higher_or_lower_is_better": "lower", # "higher", "lower", "mixed"
18+
# whether higher or lower scores are better
1719
}
1820

1921
def __init__(self):
2022
super().__init__()
2123

2224
def __call__(self, **kwargs):
23-
"""Score parameters, with kwargs call."""
24-
score, _ = self.score(kwargs)
25+
"""Score parameters, with kwargs call. Same as cost call."""
26+
score, _ = self.cost(kwargs)
2527
return score
2628

2729
@property
@@ -86,3 +88,36 @@ def _score(self, params):
8688
Additional metadata about the search.
8789
"""
8890
raise NotImplementedError
91+
92+
def cost(self, params):
93+
"""Score the parameters - with sign such that lower is better.
94+
95+
Same as ``score`` call except for the sign.
96+
97+
If the tag ``property:higher_or_lower_is_better`` is set to
98+
``"higher"``, the result is ``-self.score(params)``.
99+
100+
If the tag is set to ``"lower"``, the result is
101+
identical to ``self.score(params)``.
102+
103+
Parameters
104+
----------
105+
params : dict with string keys
106+
Parameters to score.
107+
108+
Returns
109+
-------
110+
float
111+
The score of the parameters.
112+
dict
113+
Additional metadata about the search.
114+
"""
115+
hib = self.get_tag("property:higher_or_lower_is_better", "lower")
116+
if hib == "higher":
117+
sign = -1
118+
elif hib == "lower":
119+
sign = 1
120+
121+
score_res = self.score(params)
122+
123+
return sign * score_res[0], score_res[1]

src/hyperactive/experiment/integrations/sklearn_cv.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ def __init__(self, estimator, X, y, scoring=None, cv=None):
110110
self._scoring = make_scorer(scoring)
111111
self.scorer_ = self._scoring
112112

113+
# Set the sign of the scoring function
114+
if hasattr(self._scoring, "_score"):
115+
score_func = self._scoring._score_func
116+
_sign = _guess_sign_of_sklmetric(score_func)
117+
_sign_str = "higher" if _sign == 1 else "lower"
118+
self.set_tags(**{"property:higher_or_lower_is_better": _sign_str})
119+
113120
def _paramnames(self):
114121
"""Return the parameter names of the search.
115122
@@ -235,3 +242,80 @@ def _get_score_params(self):
235242
score_params_regress = {"C": 1.0, "kernel": "linear"}
236243
score_params_defaults = {"C": 1.0, "kernel": "linear"}
237244
return [score_params_classif, score_params_regress, score_params_defaults]
245+
246+
247+
def _guess_sign_of_sklmetric(scorer):
248+
"""Guess the sign of a sklearn metric scorer.
249+
250+
Parameters
251+
----------
252+
scorer : callable
253+
The sklearn metric scorer to guess the sign for.
254+
255+
Returns
256+
-------
257+
int
258+
1 if higher scores are better, -1 if lower scores are better.
259+
"""
260+
HIGHER_IS_BETTER = {
261+
# Classification
262+
"accuracy_score": True,
263+
"auc": True,
264+
"average_precision_score": True,
265+
"balanced_accuracy_score": True,
266+
"brier_score_loss": False,
267+
"class_likelihood_ratios": False,
268+
"cohen_kappa_score": True,
269+
"d2_log_loss_score": True,
270+
"dcg_score": True,
271+
"f1_score": True,
272+
"fbeta_score": True,
273+
"hamming_loss": False,
274+
"hinge_loss": False,
275+
"jaccard_score": True,
276+
"log_loss": False,
277+
"matthews_corrcoef": True,
278+
"ndcg_score": True,
279+
"precision_score": True,
280+
"recall_score": True,
281+
"roc_auc_score": True,
282+
"top_k_accuracy_score": True,
283+
"zero_one_loss": False,
284+
285+
# Regression
286+
"d2_absolute_error_score": True,
287+
"d2_pinball_score": True,
288+
"d2_tweedie_score": True,
289+
"explained_variance_score": True,
290+
"max_error": False,
291+
"mean_absolute_error": False,
292+
"mean_absolute_percentage_error": False,
293+
"mean_gamma_deviance": False,
294+
"mean_pinball_loss": False,
295+
"mean_poisson_deviance": False,
296+
"mean_squared_error": False,
297+
"mean_squared_log_error": False,
298+
"mean_tweedie_deviance": False,
299+
"median_absolute_error": False,
300+
"r2_score": True,
301+
"root_mean_squared_error": False,
302+
"root_mean_squared_log_error": False,
303+
}
304+
305+
scorer_name = getattr(scorer, "__name__", None)
306+
307+
if hasattr(scorer, "greater_is_better"):
308+
return 1 if scorer.greater_is_better else -1
309+
elif scorer_name in HIGHER_IS_BETTER:
310+
return 1 if HIGHER_IS_BETTER[scorer_name] else -1
311+
elif scorer_name.endswith("_score"):
312+
# If the scorer name ends with "_score", we assume higher is better
313+
return 1
314+
elif scorer_name.endswith("_loss") or scorer_name.endswith("_deviance"):
315+
# If the scorer name ends with "_loss", we assume lower is better
316+
return -1
317+
elif scorer_name.endswith("_error"):
318+
return -1
319+
else:
320+
# If we cannot determine the sign, we assume lower is better
321+
return -1

src/hyperactive/opt/_adapters/_gfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def _run(self, experiment, **search_config):
133133

134134
with StdoutMute(active=not self.verbose):
135135
gfopt.search(
136-
objective_function=experiment.score,
136+
objective_function=experiment.cost,
137137
n_iter=n_iter,
138138
max_time=max_time,
139139
)

src/hyperactive/tests/test_all_objects.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class PackageConfig:
4545
"maintainers",
4646
# experiments
4747
"property:randomness",
48+
"property:higher_or_lower_is_better",
4849
# optimizers
4950
"info:name", # str
5051
"info:local_vs_global", # "local", "mixed", "global"
@@ -184,10 +185,19 @@ def test_score_function(self, object_class):
184185
assert isinstance(score, float), f"Score is not a float: {score}"
185186
assert isinstance(metadata, dict), f"Metadata is not a dict: {metadata}"
186187

188+
cost_res = inst.cost(obj)
189+
msg = f"Cost function did not return a length two tuple: {res}"
190+
assert isinstance(cost_res, tuple) and len(cost_res) == 2, msg
191+
c_score, c_metadata = cost_res
192+
assert isinstance(c_score, float), f"Score is not a float: {c_score}"
193+
assert isinstance(c_metadata, dict), f"Metadata is not a dict: {c_metadata}"
194+
195+
assert abs(c_score) == score
196+
187197
call_sc = inst(**obj)
188198
assert isinstance(call_sc, float), f"Score is not a float: {call_sc}"
189199
if inst.get_tag("property:randomness") == "deterministic":
190-
assert score == call_sc, f"Score does not match: {score} != {call_sc}"
200+
assert c_score == call_sc, f"Score does not match: {score} != {call_sc}"
191201

192202

193203
class OptimizerFixtureGenerator(BaseFixtureGenerator):

0 commit comments

Comments
 (0)