Skip to content

Commit 5d196c9

Browse files
authored
Merge pull request #31 from automl/dev
v0.0.5
2 parents aa5d27e + c767532 commit 5d196c9

File tree

8 files changed

+244
-39
lines changed

8 files changed

+244
-39
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# v0.0.5
2+
3+
## Features
4+
- Added seed parameter for controlled pseudo-randomization
5+
- Dealing with conditions on hyperparameters of configuration spaces
6+
7+
## Improvements
8+
- Bumped shapiq dependency to most recent version (v1.4.1)
9+
- Automatically configure approximator with the help of shapiq's helper function.
10+
11+
## Documentation
12+
- Added installation instructions to README.md for pip install.
13+
114
# v0.0.4
215
- Added pseudorandomization
316
- Added index-specific approximation

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,21 @@ HyperSHAP – a game‑theoretic Python library for explaining Hyperparameter Op
3535

3636
## Installation
3737

38+
First, create a virtual environment, e.g., via `conda`:
3839
```sh
40+
$ conda create -n hypershap python=3.10
41+
$ conda activate hypershap
42+
```
43+
44+
Now, you can just pip install HyperSHAP as follows:
45+
```sh
46+
$ pip install hypershap
47+
```
48+
49+
Or, clone the git repository and install hypershap via the Makefile:
50+
```sh
51+
$ git clone https://github.com/automl/hypershap
52+
$ cd hypershap
3953
$ make install
4054
```
4155

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "hypershap"
3-
version = "0.0.4"
3+
version = "0.0.5"
44
description = "HyperSHAP is a post-hoc explanation method for hyperparameter optimization."
55
authors = [{ name = "Marcel Wever", email = "m.wever@ai.uni-hannover.de" }]
66
readme = "README.md"
@@ -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: 34 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,9 +245,10 @@ 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,
251+
seed: int | None = 0,
246252
) -> InteractionValues:
247253
"""Compute and return the interaction values for tunability analysis.
248254
@@ -251,6 +257,7 @@ def tunability(
251257
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
252258
order (int, optional): The order of the interaction values. Defaults to 2.
253259
n_samples (int, optional): The number of samples to use for simulating HPO. Defaults to 10_000.
260+
seed (int, optiona): The random seed for simulating HPO. Defaults to 0.
254261
255262
Returns:
256263
InteractionValues: The computed interaction values.
@@ -278,6 +285,7 @@ def tunability(
278285
explanation_task=tunability_task,
279286
n_samples=n_samples,
280287
mode=Aggregation.MAX,
288+
seed=seed,
281289
),
282290
n_workers=self.n_workers,
283291
verbose=self.verbose,
@@ -295,9 +303,10 @@ def tunability(
295303
def sensitivity(
296304
self,
297305
baseline_config: Configuration | None = None,
298-
index: str = "FSII",
306+
index: ValidApproximationIndices = "FSII",
299307
order: int = 2,
300308
n_samples: int = 10_000,
309+
seed: int | None = 0,
301310
) -> InteractionValues:
302311
"""Compute and return the interaction values for sensitivity analysis.
303312
@@ -306,6 +315,7 @@ def sensitivity(
306315
index (str, optional): The index to use for computing interaction values. Defaults to "FSII".
307316
order (int, optional): The order of the interaction values. Defaults to 2.
308317
n_samples (int, optional): The number of samples to use for simulating HPO. Defaults to 10_000.
318+
seed (int, optiona): The random seed for simulating HPO. Defaults to 0.
309319
310320
Returns:
311321
InteractionValues: The computed interaction values.
@@ -333,6 +343,7 @@ def sensitivity(
333343
explanation_task=sensitivity_task,
334344
n_samples=n_samples,
335345
mode=Aggregation.VAR,
346+
seed=seed,
336347
),
337348
n_workers=self.n_workers,
338349
verbose=self.verbose,
@@ -350,17 +361,19 @@ def sensitivity(
350361
def mistunability(
351362
self,
352363
baseline_config: Configuration | None = None,
353-
index: str = "FSII",
364+
index: ValidApproximationIndices = "FSII",
354365
order: int = 2,
355366
n_samples: int = 10_000,
367+
seed: int | None = 0,
356368
) -> InteractionValues:
357369
"""Compute and return the interaction values for mistunability analysis.
358370
359371
Args:
360372
baseline_config (Configuration | None, optional): The baseline configuration. Defaults to None.
361-
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".
362374
order (int, optional): The order of the interaction values. Defaults to 2.
363375
n_samples (int, optional): The number of samples to use for simulating HPO. Defaults to 10_000.
376+
seed (int, optiona): The random seed for simulating HPO. Defaults to 0.
364377
365378
Returns:
366379
InteractionValues: The computed interaction values.
@@ -388,6 +401,7 @@ def mistunability(
388401
explanation_task=mistunability_task,
389402
n_samples=n_samples,
390403
mode=Aggregation.MIN,
404+
seed=seed,
391405
),
392406
n_workers=self.n_workers,
393407
verbose=self.verbose,
@@ -405,15 +419,15 @@ def optimizer_bias(
405419
self,
406420
optimizer_of_interest: ConfigSpaceSearcher,
407421
optimizer_ensemble: list[ConfigSpaceSearcher],
408-
index: str = "FSII",
422+
index: ValidApproximationIndices = "FSII",
409423
order: int = 2,
410424
) -> InteractionValues:
411425
"""Compute and return the interaction values for optimizer bias analysis.
412426
413427
Args:
414428
optimizer_of_interest (ConfigSpaceSearcher): The optimizer of interest.
415429
optimizer_ensemble (list[ConfigSpaceSearcher]): The ensemble of optimizers.
416-
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".
417431
order (int, optional): The order of the interaction values. Defaults to 2.
418432
419433
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)

0 commit comments

Comments
 (0)