Skip to content

Commit f32bfdc

Browse files
authored
Merge pull request #30 from automl/feature/constraints
Constraints and Auto-Configure Approximators
2 parents 52b1f14 + c5f28ef commit f32bfdc

File tree

6 files changed

+206
-37
lines changed

6 files changed

+206
-37
lines changed

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ classifiers = [
1717
"Topic :: Software Development :: Libraries :: Python Modules",
1818
]
1919
dependencies = [
20-
"shapiq>=1.2.3",
20+
"shapiq==1.4.1",
2121
"numpy>=2.2.6",
2222
"scikit-learn>=1.7.1",
2323
"matplotlib>=3.10.5",
24-
"networkx>=3.4.2"
24+
"networkx>=3.4.2",
25+
"ConfigSpace>=1.2.1",
2526
]
2627

2728
[project.urls]
@@ -50,8 +51,6 @@ docs = [
5051
]
5152

5253
dev = [
53-
"shapiq>=1.2.0",
54-
"ConfigSpace>=1.2.1",
5554
"numpy>=1.26.4",
5655
"tox-uv>=1.11.3",
5756
"deptry>=0.23.0",

src/hypershap/hypershap.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
if TYPE_CHECKING:
1414
from ConfigSpace import Configuration
15+
from shapiq import ValidApproximationIndices
1516

1617
from hypershap.utils import ConfigSpaceSearcher
1718

@@ -20,7 +21,8 @@
2021
import matplotlib.pyplot as plt
2122
import networkx as nx
2223
import numpy as np
23-
from shapiq import SHAPIQ, ExactComputer, InteractionValues, KernelSHAPIQ
24+
from shapiq import ExactComputer, InteractionValues
25+
from shapiq.explainer.configuration import setup_approximator_automatically
2426

2527
from hypershap.games import (
2628
AblationGame,
@@ -66,13 +68,13 @@ class HyperSHAP:
6668
__init__(explanation_task: ExplanationTask):
6769
Initializes the HyperSHAP instance with an explanation task.
6870
69-
ablation(config_of_interest: Configuration, baseline_config: Configuration, index: str = "FSII", order: int = 2) -> InteractionValues:
71+
ablation(config_of_interest: Configuration, baseline_config: Configuration, index: ValidApproximationIndices = "FSII", order: int = 2) -> InteractionValues:
7072
Computes and returns the interaction values for ablation analysis.
7173
72-
tunability(baseline_config: Configuration | None, index: str = "FSII", order: int = 2) -> InteractionValues:
74+
tunability(baseline_config: Configuration | None, index: ValidApproximationIndices = "FSII", order: int = 2) -> InteractionValues:
7375
Computes and returns the interaction values for tunability analysis.
7476
75-
optimizer_bias(optimizer_of_interest: ConfigSpaceSearcher, optimizer_ensemble: list[ConfigSpaceSearcher], index: str = "FSII", order: int = 2) -> InteractionValues:
77+
optimizer_bias(optimizer_of_interest: ConfigSpaceSearcher, optimizer_ensemble: list[ConfigSpaceSearcher], index: ValidApproximationIndices = "FSII", order: int = 2) -> InteractionValues:
7678
Computes and returns the interaction values for optimizer bias analysis.
7779
7880
plot_si_graph(interaction_values: InteractionValues | None = None, save_path: str | None = None):
@@ -116,19 +118,22 @@ def __init__(
116118
)
117119
self.verbose = verbose
118120

119-
def __get_interaction_values(self, game: AbstractHPIGame, index: str = "FSII", order: int = 2) -> InteractionValues:
121+
def __get_interaction_values(
122+
self,
123+
game: AbstractHPIGame,
124+
index: ValidApproximationIndices = "FSII",
125+
order: int = 2,
126+
seed: int | None = 0,
127+
) -> InteractionValues:
120128
if game.n_players <= EXACT_MAX_HYPERPARAMETERS:
121129
# instantiate exact computer if number of hyperparameters is small enough
122130
ec = ExactComputer(n_players=game.get_num_hyperparameters(), game=game) # pyright: ignore
123131

124132
# compute interaction values with the given index and order
125133
interaction_values = ec(index=index, order=order)
126134
else:
127-
# instantiate kernel
128-
if index == "FSII":
129-
approx = SHAPIQ(n=game.n_players, max_order=2, index=index)
130-
else:
131-
approx = KernelSHAPIQ(n=game.n_players, max_order=2, index=index)
135+
# instantiate approximator
136+
approx = setup_approximator_automatically(index, order, game.n_players, seed)
132137

133138
# approximate interaction values with the given index and order
134139
interaction_values = approx(budget=self.approximation_budget, game=game)
@@ -142,15 +147,15 @@ def ablation(
142147
self,
143148
config_of_interest: Configuration,
144149
baseline_config: Configuration,
145-
index: str = "FSII",
150+
index: ValidApproximationIndices = "FSII",
146151
order: int = 2,
147152
) -> InteractionValues:
148153
"""Compute and return the interaction values for ablation analysis.
149154
150155
Args:
151156
config_of_interest (Configuration): The configuration of interest.
152157
baseline_config (Configuration): The baseline configuration.
153-
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
158+
index (ValidApproximationIndices, optional): The index to use for computing interaction values. Defaults to "FSII".
154159
order (int, optional): The order of the interaction values. Defaults to 2.
155160
156161
Returns:
@@ -191,7 +196,7 @@ def ablation_multibaseline(
191196
config_of_interest: Configuration,
192197
baseline_configs: list[Configuration],
193198
aggregation: Aggregation = Aggregation.AVG,
194-
index: str = "FSII",
199+
index: ValidApproximationIndices = "FSII",
195200
order: int = 2,
196201
) -> InteractionValues:
197202
"""Compute and return the interaction values for multi-baseline ablation analysis.
@@ -200,7 +205,7 @@ def ablation_multibaseline(
200205
config_of_interest (Configuration): The configuration of interest.
201206
baseline_configs (list[Configuration]): The list of baseline configurations.
202207
aggregation (Aggregation): The aggregation method to use for computing interaction values.
203-
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
208+
index (ValidApproximationIndices, optional): The index to use for computing interaction values. Defaults to "FSII".
204209
order (int, optional): The order of the interaction values. Defaults to 2.
205210
206211
Returns:
@@ -240,7 +245,7 @@ def ablation_multibaseline(
240245
def tunability(
241246
self,
242247
baseline_config: Configuration | None = None,
243-
index: str = "FSII",
248+
index: ValidApproximationIndices = "FSII",
244249
order: int = 2,
245250
n_samples: int = 10_000,
246251
seed: int | None = 0,
@@ -298,7 +303,7 @@ def tunability(
298303
def sensitivity(
299304
self,
300305
baseline_config: Configuration | None = None,
301-
index: str = "FSII",
306+
index: ValidApproximationIndices = "FSII",
302307
order: int = 2,
303308
n_samples: int = 10_000,
304309
seed: int | None = 0,
@@ -356,7 +361,7 @@ def sensitivity(
356361
def mistunability(
357362
self,
358363
baseline_config: Configuration | None = None,
359-
index: str = "FSII",
364+
index: ValidApproximationIndices = "FSII",
360365
order: int = 2,
361366
n_samples: int = 10_000,
362367
seed: int | None = 0,
@@ -365,7 +370,7 @@ def mistunability(
365370
366371
Args:
367372
baseline_config (Configuration | None, optional): The baseline configuration. Defaults to None.
368-
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
373+
index (ValidApproximationIndices, optional): The index to use for computing interaction values. Defaults to "FSII".
369374
order (int, optional): The order of the interaction values. Defaults to 2.
370375
n_samples (int, optional): The number of samples to use for simulating HPO. Defaults to 10_000.
371376
seed (int, optiona): The random seed for simulating HPO. Defaults to 0.
@@ -414,15 +419,15 @@ def optimizer_bias(
414419
self,
415420
optimizer_of_interest: ConfigSpaceSearcher,
416421
optimizer_ensemble: list[ConfigSpaceSearcher],
417-
index: str = "FSII",
422+
index: ValidApproximationIndices = "FSII",
418423
order: int = 2,
419424
) -> InteractionValues:
420425
"""Compute and return the interaction values for optimizer bias analysis.
421426
422427
Args:
423428
optimizer_of_interest (ConfigSpaceSearcher): The optimizer of interest.
424429
optimizer_ensemble (list[ConfigSpaceSearcher]): The ensemble of optimizers.
425-
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
430+
index (ValidApproximationIndices, optional): The index to use for computing interaction values. Defaults to "FSII".
426431
order (int, optional): The order of the interaction values. Defaults to 2.
427432
428433
Returns:

src/hypershap/utils.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,23 @@
55

66
from __future__ import annotations
77

8+
import logging
89
from abc import ABC, abstractmethod
910
from copy import deepcopy
1011
from enum import Enum
1112
from typing import TYPE_CHECKING
1213

1314
if TYPE_CHECKING:
1415
from hypershap.task import BaselineExplanationTask
15-
1616
import numpy as np
17+
from ConfigSpace.exceptions import (
18+
ActiveHyperparameterNotSetError,
19+
ForbiddenValueError,
20+
IllegalVectorizedValueError,
21+
InactiveHyperparameterSetError,
22+
)
23+
24+
logger = logging.getLogger(__name__)
1725

1826

1927
class Aggregation(Enum):
@@ -106,6 +114,20 @@ def __init__(
106114
# cache coalition values to ensure monotonicity for min/max
107115
self.coalition_cache = {}
108116

117+
def _is_valid(self, config: np.ndarray) -> bool:
118+
"""Check whether a configuration is valid with respect to conditions of the configuration space."""
119+
try:
120+
self.explanation_task.config_space.check_configuration_vector_representation(config)
121+
except (
122+
ActiveHyperparameterNotSetError,
123+
IllegalVectorizedValueError,
124+
InactiveHyperparameterSetError,
125+
ForbiddenValueError,
126+
):
127+
return False
128+
else:
129+
return True
130+
109131
def search(self, coalition: np.ndarray) -> float:
110132
"""Search the configuration space based on the coalition.
111133
@@ -125,6 +147,22 @@ def search(self, coalition: np.ndarray) -> float:
125147
column_index = np.where(blind_coalition)
126148
temp_random_sample[:, column_index] = self.explanation_task.baseline_config.get_array()[column_index]
127149

128-
# predict performance values with the help of the surrogate model
129-
vals: np.ndarray = np.array(self.explanation_task.get_single_surrogate_model().evaluate(temp_random_sample))
150+
# in case of conditions in the config space, it might happen that through blinding hyperparameter values
151+
# configurations might become invalid and those should not be considered for calculating vals
152+
if len(self.explanation_task.config_space.conditions) > 0:
153+
# filter invalid configurations
154+
validity = np.apply_along_axis(self._is_valid, axis=1, arr=temp_random_sample)
155+
filtered_samples = temp_random_sample[validity]
156+
157+
if len(filtered_samples) < 0.05 * len(temp_random_sample): # pragma: no cover
158+
logger.warning(
159+
"WARNING: Due to blinding less than 5% of the samples in the random search remain valid. "
160+
"Consider increasing the sampling budget of the random search.",
161+
)
162+
163+
# predict performance values with the help of the surrogate model for the filtered configurations
164+
vals: np.ndarray = np.array(self.explanation_task.get_single_surrogate_model().evaluate(filtered_samples))
165+
else:
166+
vals: np.ndarray = np.array(self.explanation_task.get_single_surrogate_model().evaluate(temp_random_sample))
167+
130168
return evaluate_aggregation(self.mode, vals)

tests/fixtures/simple_setup.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import pytest
6-
from ConfigSpace import Configuration, ConfigurationSpace, UniformFloatHyperparameter
6+
from ConfigSpace import Configuration, ConfigurationSpace, LessThanCondition, UniformFloatHyperparameter
77

88
from hypershap import ExplanationTask
99

@@ -41,7 +41,7 @@ def evaluate(self, x: Configuration) -> float:
4141
Returns: The value of the configuration.
4242
4343
"""
44-
return self.value(x["a"], x["b"])
44+
return self.value(x["a"], x.get("b", 0))
4545

4646
def value(self, a: float, b: float) -> float:
4747
"""Evaluate the value of a configuration.
@@ -71,3 +71,27 @@ def simple_base_et(
7171
) -> ExplanationTask:
7272
"""Return a base explanation task for the simple setup."""
7373
return ExplanationTask.from_function(simple_config_space, simple_blackbox_function.evaluate)
74+
75+
76+
@pytest.fixture(scope="session")
77+
def simple_cond_config_space() -> ConfigurationSpace:
78+
"""Return a simple config space with conditions for testing."""
79+
config_space = ConfigurationSpace()
80+
config_space.seed(42)
81+
82+
a = UniformFloatHyperparameter("a", 0, 1, 0)
83+
b = UniformFloatHyperparameter("b", 0, 1, 0)
84+
config_space.add(a)
85+
config_space.add(b)
86+
87+
config_space.add(LessThanCondition(b, a, 0.3))
88+
return config_space
89+
90+
91+
@pytest.fixture(scope="session")
92+
def simple_cond_base_et(
93+
simple_cond_config_space: ConfigurationSpace,
94+
simple_blackbox_function: SimpleBlackboxFunction,
95+
) -> ExplanationTask:
96+
"""Return a base explanation task for the simple setup with conditions."""
97+
return ExplanationTask.from_function(simple_cond_config_space, simple_blackbox_function.evaluate)

tests/test_extended_settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,10 @@ def test_multi_data_sensitivity(multi_data_baseline_config: Configuration, hyper
7272
"""Test the multi-data sesntivity task."""
7373
iv = hypershap_inst.sensitivity(baseline_config=multi_data_baseline_config)
7474
assert iv is not None, "Interaction values should not be none."
75+
76+
77+
def test_tunability_with_conditions(simple_cond_base_et: ExplanationTask) -> None:
78+
"""Test the tunability task with a configuration space that has conditions."""
79+
hypershap = HyperSHAP(simple_cond_base_et)
80+
iv = hypershap.tunability(simple_cond_base_et.config_space.get_default_configuration())
81+
assert iv is not None, "Interaction values should not be none."

0 commit comments

Comments
 (0)