Skip to content

Commit 63e99cb

Browse files
SimonBlankefkiraly
andauthored
Fix selection direction, scorer handling, and fit kwargs; resolve sktime doctest (#182)
Problems - Grid/Random search picked the wrong best `params` (argmin on signed scores) and didn’t consistently set best_* attributes. - Sklearn fit `kwargs` were silently dropped by the verify_fit decorator. - Doctest failed in sktime classification by accessing private `scorer._score_func` (not present for _PassthroughScorer). - “mixed” experiment objectives had undefined score sign behavior. - _score_params returned `experiment(**params)`, which calls `score(). score()` applies a sign flip based on the experiment tag, so Grid/Random search were selecting on “signed” scores and then also making assumptions about direction (previously hardcoded argmin). Solutions - Grid/Random search: select min/max based on experiment tag using raw `evaluate()` values; set `best_params_`, `best_index_`, and compute signed `best_score_` via `experiment.score(...)`. - Centralized scorer handling: `_coerce_to_scorer` now attaches a `safe ._metric_func` fallback (e.g., accuracy/r2) and robust sign inference. - Sklearn decorator: `verify_fit` now preserves *args, **kwargs and marks fit success. - BaseExperiment: `score()` raises on "mixed" to avoid undefined behavior (users should define a concrete direction or override). - `_score_params` now returns the raw `evaluate()` value (float), not the signed `score()`. Selecting the best config should use raw objective values and then choose min or max based on the tag (higher/lower). This removes ambiguity, avoids double sign logic, and makes selection correct and explicit. We still compute the public `best_score_` via `experiment.score(best_params)` so external consumers see the standardized “higher-is-better” `score_`. --------- Co-authored-by: Franz Király <fkiraly@gcos.ai>
1 parent 79168ac commit 63e99cb

13 files changed

Lines changed: 301 additions & 305 deletions

File tree

examples/hyperactive_intro.ipynb

Lines changed: 25 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,11 @@
5151
},
5252
{
5353
"cell_type": "code",
54-
"execution_count": 1,
54+
"execution_count": null,
5555
"id": "8c428229",
5656
"metadata": {},
5757
"outputs": [],
58-
"source": [
59-
"def sphere(opt):\n",
60-
" x = opt[\"x\"]\n",
61-
" y = opt[\"y\"]\n",
62-
"\n",
63-
" return -x**2 - y**2"
64-
]
58+
"source": "\"\"\"Hyperactive optimization library introduction notebook.\n\nThis notebook demonstrates unified interfaces for optimizers and experiments\nusing the Hyperactive optimization library.\n\"\"\"\n\n\ndef sphere(opt):\n \"\"\"Evaluate sphere function for optimization.\n\n Parameters\n ----------\n opt : dict\n Dictionary with 'x' and 'y' keys containing numeric values.\n\n Returns\n -------\n float\n Negative sum of squares (for maximization).\n \"\"\"\n x = opt[\"x\"]\n y = opt[\"y\"]\n\n return -(x**2) - y**2"
6559
},
6660
{
6761
"cell_type": "markdown",
@@ -139,7 +133,7 @@
139133
"source": [
140134
"from hyperactive.experiment.bench import Parabola\n",
141135
"\n",
142-
"?Parabola"
136+
"Parabola?"
143137
]
144138
},
145139
{
@@ -319,27 +313,11 @@
319313
},
320314
{
321315
"cell_type": "code",
322-
"execution_count": 10,
316+
"execution_count": null,
323317
"id": "57110e86",
324318
"metadata": {},
325319
"outputs": [],
326-
"source": [
327-
"from hyperactive.experiment.integrations import SklearnCvExperiment\n",
328-
"from sklearn.datasets import load_iris\n",
329-
"from sklearn.svm import SVC\n",
330-
"from sklearn.metrics import accuracy_score\n",
331-
"from sklearn.model_selection import KFold\n",
332-
"\n",
333-
"X, y = load_iris(return_X_y=True)\n",
334-
"\n",
335-
"sklearn_exp = SklearnCvExperiment(\n",
336-
" estimator=SVC(),\n",
337-
" scoring=accuracy_score,\n",
338-
" cv=KFold(n_splits=3, shuffle=True),\n",
339-
" X=X,\n",
340-
" y=y,\n",
341-
")"
342-
]
320+
"source": "from sklearn.datasets import load_iris\nfrom sklearn.metrics import accuracy_score\nfrom sklearn.model_selection import KFold\nfrom sklearn.svm import SVC\n\nfrom hyperactive.experiment.integrations import SklearnCvExperiment\n\nX, y = load_iris(return_X_y=True)\n\nsklearn_exp = SklearnCvExperiment(\n estimator=SVC(),\n scoring=accuracy_score,\n cv=KFold(n_splits=3, shuffle=True),\n X=X,\n y=y,\n)"
343321
},
344322
{
345323
"cell_type": "markdown",
@@ -500,57 +478,19 @@
500478
},
501479
{
502480
"cell_type": "code",
503-
"execution_count": 15,
481+
"execution_count": null,
504482
"id": "ab78b796",
505483
"metadata": {},
506484
"outputs": [],
507-
"source": [
508-
"def sphere(opt):\n",
509-
" x = opt[\"x\"]\n",
510-
" y = opt[\"y\"]\n",
511-
"\n",
512-
" return -x**2 - y**2"
513-
]
485+
"source": "def sphere(opt):\n \"\"\"Evaluate sphere function for optimization.\n\n Parameters\n ----------\n opt : dict\n Dictionary with 'x' and 'y' keys containing numeric values.\n\n Returns\n -------\n float\n Negative sum of squares (for maximization).\n \"\"\"\n x = opt[\"x\"]\n y = opt[\"y\"]\n\n return -(x**2) - y**2"
514486
},
515487
{
516488
"cell_type": "code",
517-
"execution_count": 16,
489+
"execution_count": null,
518490
"id": "7104e5ec",
519491
"metadata": {},
520-
"outputs": [
521-
{
522-
"name": "stderr",
523-
"output_type": "stream",
524-
"text": [
525-
" \r"
526-
]
527-
},
528-
{
529-
"data": {
530-
"text/plain": [
531-
"{'x': np.float64(0.10101010101010033), 'y': np.float64(0.10101010101010033)}"
532-
]
533-
},
534-
"execution_count": 16,
535-
"metadata": {},
536-
"output_type": "execute_result"
537-
}
538-
],
539-
"source": [
540-
"import numpy as np\n",
541-
"from hyperactive.opt import HillClimbing\n",
542-
"\n",
543-
"hillclimbing_config = {\n",
544-
" \"search_space\": {\n",
545-
" \"x\": np.linspace(-10, 10, 100),\n",
546-
" \"y\": np.linspace(-10, 10, 100),\n",
547-
" },\n",
548-
" \"n_iter\": 1000,\n",
549-
"}\n",
550-
"hill_climbing = HillClimbing(**hillclimbing_config, experiment=sphere)\n",
551-
"\n",
552-
"hill_climbing.solve()"
553-
]
492+
"outputs": [],
493+
"source": "import numpy as np\n\nfrom hyperactive.opt import HillClimbing\n\nhillclimbing_config = {\n \"search_space\": {\n \"x\": np.linspace(-10, 10, 100),\n \"y\": np.linspace(-10, 10, 100),\n },\n \"n_iter\": 1000,\n}\nhill_climbing = HillClimbing(**hillclimbing_config, experiment=sphere)\n\nhill_climbing.solve()"
554494
},
555495
{
556496
"cell_type": "markdown",
@@ -562,56 +502,19 @@
562502
},
563503
{
564504
"cell_type": "code",
565-
"execution_count": 17,
505+
"execution_count": null,
566506
"id": "5e2328c9",
567507
"metadata": {},
568508
"outputs": [],
569-
"source": [
570-
"from hyperactive.experiment.integrations import SklearnCvExperiment\n",
571-
"from sklearn.datasets import load_iris\n",
572-
"from sklearn.svm import SVC\n",
573-
"from sklearn.metrics import accuracy_score\n",
574-
"from sklearn.model_selection import KFold\n",
575-
"\n",
576-
"X, y = load_iris(return_X_y=True)\n",
577-
"\n",
578-
"sklearn_exp = SklearnCvExperiment(\n",
579-
" estimator=SVC(),\n",
580-
" scoring=accuracy_score,\n",
581-
" cv=KFold(n_splits=3, shuffle=True),\n",
582-
" X=X,\n",
583-
" y=y,\n",
584-
")"
585-
]
509+
"source": "from sklearn.datasets import load_iris\nfrom sklearn.metrics import accuracy_score\nfrom sklearn.model_selection import KFold\nfrom sklearn.svm import SVC\n\nfrom hyperactive.experiment.integrations import SklearnCvExperiment\n\nX, y = load_iris(return_X_y=True)\n\nsklearn_exp = SklearnCvExperiment(\n estimator=SVC(),\n scoring=accuracy_score,\n cv=KFold(n_splits=3, shuffle=True),\n X=X,\n y=y,\n)"
586510
},
587511
{
588512
"cell_type": "code",
589-
"execution_count": 18,
513+
"execution_count": null,
590514
"id": "e9a07a73",
591515
"metadata": {},
592-
"outputs": [
593-
{
594-
"data": {
595-
"text/plain": [
596-
"{'C': 0.01, 'gamma': 1}"
597-
]
598-
},
599-
"execution_count": 18,
600-
"metadata": {},
601-
"output_type": "execute_result"
602-
}
603-
],
604-
"source": [
605-
"from hyperactive.opt import GridSearchSk as GridSearch\n",
606-
"\n",
607-
"param_grid = {\n",
608-
" \"C\": [0.01, 0.1, 1, 10],\n",
609-
" \"gamma\": [0.0001, 0.01, 0.1, 1, 10],\n",
610-
"}\n",
611-
"grid_search = GridSearch(param_grid=param_grid, experiment=sklearn_exp)\n",
612-
"\n",
613-
"grid_search.solve()"
614-
]
516+
"outputs": [],
517+
"source": "from hyperactive.opt import GridSearchSk as GridSearch\n\nparam_grid = {\n \"C\": [0.01, 0.1, 1, 10],\n \"gamma\": [0.0001, 0.01, 0.1, 1, 10],\n}\ngrid_search = GridSearch(param_grid=param_grid, experiment=sklearn_exp)\n\ngrid_search.solve()"
615518
},
616519
{
617520
"cell_type": "markdown",
@@ -623,67 +526,19 @@
623526
},
624527
{
625528
"cell_type": "code",
626-
"execution_count": 19,
529+
"execution_count": null,
627530
"id": "f9a4d922",
628531
"metadata": {},
629532
"outputs": [],
630-
"source": [
631-
"from hyperactive.experiment.integrations import SklearnCvExperiment\n",
632-
"from sklearn.datasets import load_iris\n",
633-
"from sklearn.svm import SVC\n",
634-
"from sklearn.metrics import accuracy_score\n",
635-
"from sklearn.model_selection import KFold\n",
636-
"\n",
637-
"X, y = load_iris(return_X_y=True)\n",
638-
"\n",
639-
"sklearn_exp = SklearnCvExperiment(\n",
640-
" estimator=SVC(),\n",
641-
" scoring=accuracy_score,\n",
642-
" cv=KFold(n_splits=3, shuffle=True),\n",
643-
" X=X,\n",
644-
" y=y,\n",
645-
")"
646-
]
533+
"source": "from sklearn.datasets import load_iris\nfrom sklearn.metrics import accuracy_score\nfrom sklearn.model_selection import KFold\nfrom sklearn.svm import SVC\n\nfrom hyperactive.experiment.integrations import SklearnCvExperiment\n\nX, y = load_iris(return_X_y=True)\n\nsklearn_exp = SklearnCvExperiment(\n estimator=SVC(),\n scoring=accuracy_score,\n cv=KFold(n_splits=3, shuffle=True),\n X=X,\n y=y,\n)"
647534
},
648535
{
649536
"cell_type": "code",
650-
"execution_count": 20,
537+
"execution_count": null,
651538
"id": "9a13b4f3",
652539
"metadata": {},
653-
"outputs": [
654-
{
655-
"name": "stderr",
656-
"output_type": "stream",
657-
"text": [
658-
" \r"
659-
]
660-
},
661-
{
662-
"data": {
663-
"text/plain": [
664-
"{'C': np.float64(10.0), 'gamma': np.float64(0.1)}"
665-
]
666-
},
667-
"execution_count": 20,
668-
"metadata": {},
669-
"output_type": "execute_result"
670-
}
671-
],
672-
"source": [
673-
"import numpy as np\n",
674-
"from hyperactive.opt import HillClimbing\n",
675-
"\n",
676-
"hillclimbing_config = {\n",
677-
" \"search_space\": {\n",
678-
" \"C\": np.array([0.01, 0.1, 1, 10]),\n",
679-
" \"gamma\": np.array([0.0001, 0.01, 0.1, 1, 10]),\n",
680-
" },\n",
681-
" \"n_iter\": 100,\n",
682-
"}\n",
683-
"hill_climbing = HillClimbing(**hillclimbing_config, experiment=sklearn_exp)\n",
684-
"\n",
685-
"hill_climbing.solve()"
686-
]
540+
"outputs": [],
541+
"source": "import numpy as np\n\nfrom hyperactive.opt import HillClimbing\n\nhillclimbing_config = {\n \"search_space\": {\n \"C\": np.array([0.01, 0.1, 1, 10]),\n \"gamma\": np.array([0.0001, 0.01, 0.1, 1, 10]),\n },\n \"n_iter\": 100,\n}\nhill_climbing = HillClimbing(**hillclimbing_config, experiment=sklearn_exp)\n\nhill_climbing.solve()"
687542
},
688543
{
689544
"cell_type": "markdown",
@@ -716,31 +571,11 @@
716571
},
717572
{
718573
"cell_type": "code",
719-
"execution_count": 21,
574+
"execution_count": null,
720575
"id": "4bdf2d49",
721576
"metadata": {},
722577
"outputs": [],
723-
"source": [
724-
"# 1. defining the tuned estimator\n",
725-
"from sklearn.svm import SVC\n",
726-
"from hyperactive.integrations.sklearn import OptCV\n",
727-
"from hyperactive.opt import GridSearchSk as GridSearch\n",
728-
"\n",
729-
"param_grid = {\"kernel\": [\"linear\", \"rbf\"], \"C\": [1, 10]}\n",
730-
"tuned_svc = OptCV(SVC(), optimizer=GridSearch(param_grid))\n",
731-
"\n",
732-
"# 2. fitting the tuned estimator = tuning the hyperparameters\n",
733-
"from sklearn.datasets import load_iris\n",
734-
"from sklearn.model_selection import train_test_split\n",
735-
"\n",
736-
"X, y = load_iris(return_X_y=True)\n",
737-
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n",
738-
"\n",
739-
"tuned_svc.fit(X_train, y_train)\n",
740-
"\n",
741-
"# 3. making predictions with the tuned estimator\n",
742-
"y_pred = tuned_svc.predict(X_test)"
743-
]
578+
"source": "# 1. defining the tuned estimator\nfrom sklearn.datasets import load_iris\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.svm import SVC\n\nfrom hyperactive.integrations.sklearn import OptCV\nfrom hyperactive.opt import GridSearchSk as GridSearch\n\nparam_grid = {\"kernel\": [\"linear\", \"rbf\"], \"C\": [1, 10]}\ntuned_svc = OptCV(SVC(), optimizer=GridSearch(param_grid))\n\n# 2. fitting the tuned estimator = tuning the hyperparameters\nX, y = load_iris(return_X_y=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n\ntuned_svc.fit(X_train, y_train)\n\n# 3. making predictions with the tuned estimator\ny_pred = tuned_svc.predict(X_test)"
744579
},
745580
{
746581
"cell_type": "markdown",
@@ -1198,48 +1033,11 @@
11981033
},
11991034
{
12001035
"cell_type": "code",
1201-
"execution_count": 24,
1036+
"execution_count": null,
12021037
"id": "f606284b",
12031038
"metadata": {},
1204-
"outputs": [
1205-
{
1206-
"name": "stderr",
1207-
"output_type": "stream",
1208-
"text": [
1209-
" \r"
1210-
]
1211-
}
1212-
],
1213-
"source": [
1214-
"# 1. defining the tuned estimator\n",
1215-
"from sklearn.svm import SVC\n",
1216-
"from hyperactive.integrations.sklearn import OptCV\n",
1217-
"from hyperactive.opt import HillClimbing\n",
1218-
"\n",
1219-
"# picking the optimizer is the only part that changes!\n",
1220-
"hill_climbing_config = {\n",
1221-
" \"search_space\": {\n",
1222-
" \"C\": np.array([0.01, 0.1, 1, 10]),\n",
1223-
" \"gamma\": np.array([0.0001, 0.01, 0.1, 1, 10]),\n",
1224-
" },\n",
1225-
" \"n_iter\": 100,\n",
1226-
"}\n",
1227-
"hill_climbing = HillClimbing(**hill_climbing_config)\n",
1228-
"\n",
1229-
"tuned_svc = OptCV(SVC(), optimizer=hill_climbing)\n",
1230-
"\n",
1231-
"# 2. fitting the tuned estimator = tuning the hyperparameters\n",
1232-
"from sklearn.datasets import load_iris\n",
1233-
"from sklearn.model_selection import train_test_split\n",
1234-
"\n",
1235-
"X, y = load_iris(return_X_y=True)\n",
1236-
"X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n",
1237-
"\n",
1238-
"tuned_svc.fit(X_train, y_train)\n",
1239-
"\n",
1240-
"# 3. making predictions with the tuned estimator\n",
1241-
"y_pred = tuned_svc.predict(X_test)"
1242-
]
1039+
"outputs": [],
1040+
"source": "# 1. defining the tuned estimator\nfrom sklearn.datasets import load_iris\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.svm import SVC\n\nfrom hyperactive.integrations.sklearn import OptCV\nfrom hyperactive.opt import HillClimbing\n\n# picking the optimizer is the only part that changes!\nhill_climbing_config = {\n \"search_space\": {\n \"C\": np.array([0.01, 0.1, 1, 10]),\n \"gamma\": np.array([0.0001, 0.01, 0.1, 1, 10]),\n },\n \"n_iter\": 100,\n}\nhill_climbing = HillClimbing(**hill_climbing_config)\n\ntuned_svc = OptCV(SVC(), optimizer=hill_climbing)\n\n# 2. fitting the tuned estimator = tuning the hyperparameters\nX, y = load_iris(return_X_y=True)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)\n\ntuned_svc.fit(X_train, y_train)\n\n# 3. making predictions with the tuned estimator\ny_pred = tuned_svc.predict(X_test)"
12431041
},
12441042
{
12451043
"cell_type": "markdown",

examples/test_examples.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
don't break the examples.
1111
"""
1212

13-
import os
1413
import sys
1514
import subprocess
16-
import tempfile
1715
from pathlib import Path
1816
import pytest
1917

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ where = ["src"]
77

88
[project]
99
name = "hyperactive"
10-
version = "4.8.1"
10+
version = "5.0.0"
1111
description = "An optimization and data collection toolbox for convenient and fast prototyping of computationally expensive models."
1212
readme = "README.md"
1313
requires-python = ">=3.9"

src/hyperactive/base/_experiment.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Base class for experiment."""
2+
23
# copyright: hyperactive developers, MIT License (see LICENSE file)
34

45
import numpy as np
@@ -22,7 +23,7 @@ def __init__(self):
2223
super().__init__()
2324

2425
def __call__(self, params):
25-
"""Score parameters. Same as score call, returns only only a first element."""
26+
"""Score parameters. Same as score call, returns only a first element."""
2627
score, _ = self.score(params)
2728
return score
2829

@@ -125,6 +126,15 @@ def score(self, params):
125126
sign = 1
126127
elif hib == "lower":
127128
sign = -1
129+
elif hib == "mixed":
130+
raise NotImplementedError(
131+
"Score is undefined for mixed objectives. Override `score` or "
132+
"set a concrete objective where higher or lower is better."
133+
)
134+
else:
135+
raise ValueError(
136+
f"Unknown value for tag 'property:higher_or_lower_is_better': {hib}"
137+
)
128138

129139
eval_res = self.evaluate(params)
130140
value = eval_res[0]

0 commit comments

Comments
 (0)