Skip to content

Commit ed4ccf2

Browse files
authored
Merge pull request #19 from automl/dev
Version 0.0.3
2 parents 23dced0 + fd83f98 commit ed4ccf2

22 files changed

Lines changed: 3881 additions & 194 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# v0.0.3
2+
- Added multi-baseline ablation game. This game computes ablation paths with respect to multiple baseline configurations and aggregates values for different paths via mean, min, max or variance.
3+
- Added waterfall plots to the HyperSHAP interface.
4+
- Added support for multi-data settings
5+
- Enabled approximation of Shapley values and interactions for settings exceeding a certain number of hyperparameters.
6+
17
# v0.0.2
28
- Added parallelization for faster analysis
39

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ The example demonstrates how to:
7474

7575
## API Overview
7676

77-
| Method | Purpose | Key Arguments |
78-
|--------|---------|---------------|
79-
| `HyperSHAP(explanation_task)` | Initialise the explainer with a generic `ExplanationTask`. |
77+
| Method | Purpose | Key Arguments |
78+
|--------|-----------------------------------------------------------------------------------------------------------------------------------|---------------|
79+
| `HyperSHAP(explanation_task)` | Initialize the explainer with a generic `ExplanationTask`. |
8080
| `ablation(config_of_interest, baseline_config, index="FSII", order=2)` | Explain the contribution of each hyperparameter value (and interactions) when moving from a baseline to a specific configuration. |
81-
| `tunability(baseline_config=None, index="FSII", order=2, n_samples=10_000)` | Quantify how much performance can be gained by tuning subsets of hyper‑parameters. |
82-
| `optimizer_bias(optimizer_of_interest, optimizer_ensemble, index="FSII", order=2)` | Attribute performance differences to a particular optimizer vs. an ensemble of optimizers. |
83-
| `plot_si_graph(interaction_values=None, save_path=None)` | Plot the Shapley Interaction (SI) graph; uses the most recent interaction values if none are supplied. |
84-
| `ExplanationTask.get_hyperparameter_names()` | Helper to retrieve ordered hyper‑parameter names (used for visualisation). |
81+
| `tunability(baseline_config=None, index="FSII", order=2, n_samples=10_000)` | Quantify how much performance can be gained by tuning subsets of hyper‑parameters. |
82+
| `optimizer_bias(optimizer_of_interest, optimizer_ensemble, index="FSII", order=2)` | Attribute performance differences to a particular optimizer vs. an ensemble of optimizers. |
83+
| `plot_si_graph(interaction_values=None, save_path=None)` | Plot the Shapley Interaction (SI) graph; uses the most recent interaction values if none are supplied. |
84+
| `ExplanationTask.get_hyperparameter_names()` | Helper to retrieve ordered hyper‑parameter names (used for visualisation). |
8585

8686
All methods return an `InteractionValues` object (from **shapiq**) that can be inspected, saved, or passed to the visualisation routine.
8787

@@ -121,11 +121,11 @@ The paper introduces the underlying game-theoretic framework and demonstrates it
121121

122122
Contributions are welcome! Please follow these steps:
123123

124-
Fork the repo and create a feature branch (git checkout -b feat/your-feature).
125-
Write tests (the project uses pytest).
126-
Ensure all tests pass (pytest).
127-
Update documentation if you add new functionality.
128-
Submit a Pull Request with a clear description of the changes.
124+
1. Fork the repo and create a feature branch (git checkout -b feat/your-feature).
125+
2. Write tests (the project uses pytest).
126+
3. Ensure all tests pass (pytest).
127+
4. Update documentation if you add new functionality.
128+
5. Submit a Pull Request with a clear description of the changes.
129129

130130

131131
See CONTRIBUTING.md for detailed guidelines.

docs/modules.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

examples/carps-benchmark-example.ipynb

Lines changed: 308 additions & 0 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dependencies = [
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"
2525
]
2626

2727
[project.urls]
@@ -60,6 +60,10 @@ dev = [
6060
{include-group = "docs"}
6161
]
6262

63+
carps = [
64+
"carps>=1.0.4"
65+
]
66+
6367
[build-system]
6468
requires = ["hatchling"]
6569
build-backend = "hatchling.build"

src/hypershap/games/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
from __future__ import annotations
44

5-
from .ablation import AblationGame
5+
from .ablation import AblationGame, MultiBaselineAblationGame
66
from .abstract import AbstractHPIGame
7+
from .multi_data import MultiDataHPIGame
78
from .optimizerbias import OptimizerBiasGame
89
from .tunability import MistunabilityGame, SearchBasedGame, SensitivityGame, TunabilityGame
910

1011
__all__ = [
1112
"AblationGame",
1213
"AbstractHPIGame",
1314
"MistunabilityGame",
15+
"MultiBaselineAblationGame",
16+
"MultiDataHPIGame",
1417
"OptimizerBiasGame",
1518
"SearchBasedGame",
1619
"SensitivityGame",

src/hypershap/games/ablation.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
import numpy as np
2828

2929
from hypershap.games.abstract import AbstractHPIGame
30-
from hypershap.task import AblationExplanationTask
30+
from hypershap.task import AblationExplanationTask, MultiBaselineAblationExplanationTask
31+
from hypershap.utils import Aggregation, evaluate_aggregation
3132

3233

3334
class AblationGame(AbstractHPIGame):
@@ -78,7 +79,8 @@ def evaluate_single_coalition(self, coalition: np.ndarray) -> float:
7879
baseline_cfg = self._get_explanation_task().baseline_config.get_array()
7980
cfg_of_interest = self._get_explanation_task().config_of_interest.get_array()
8081
blend = np.where(coalition == 0, baseline_cfg, cfg_of_interest)
81-
res = self._get_explanation_task().surrogate_model.evaluate(blend)
82+
83+
res = self._get_explanation_task().get_single_surrogate_model().evaluate(blend)
8284

8385
# validate that we do not get a list of floats by accident
8486
if isinstance(res, list): # pragma: no cover
@@ -100,3 +102,48 @@ def _get_explanation_task(self) -> AblationExplanationTask:
100102
return self.explanation_task
101103

102104
raise ValueError # pragma: no cover
105+
106+
107+
class MultiBaselineAblationGame(AbstractHPIGame):
108+
"""The multi-baseline ablation game generates local explanations for hyperparameter configurations.
109+
110+
It does so by considering multiple baseline configurations as starting points.
111+
"""
112+
113+
def __init__(
114+
self,
115+
explanation_task: MultiBaselineAblationExplanationTask,
116+
aggregation: Aggregation = Aggregation.AVG,
117+
n_workers: int | None = None,
118+
verbose: bool | None = None,
119+
) -> None:
120+
"""Initialize a MultiBaselineAblationGame.
121+
122+
n_workers (int | None): The number of worker threads to use for parallel
123+
evaluation of coalitions. Defaults to None, which disables parallelization.
124+
Using more workers can significantly speed up the computation of Shapley values.
125+
The maximum number of workers is capped by the number of coalitions.
126+
verbose (bool | None): A boolean indicating whether to print verbose messages
127+
during computation. Defaults to None. When set to True, the method prints
128+
debugging information and progress updates.
129+
"""
130+
self.aggregation = aggregation
131+
self.ablation_games = [
132+
AblationGame(
133+
AblationExplanationTask(
134+
config_space=explanation_task.config_space,
135+
surrogate_model=explanation_task.surrogate_model,
136+
baseline_config=baseline_config,
137+
config_of_interest=explanation_task.config_of_interest,
138+
),
139+
n_workers=n_workers,
140+
verbose=verbose,
141+
)
142+
for baseline_config in explanation_task.baseline_configs
143+
]
144+
super().__init__(explanation_task, n_workers, verbose)
145+
146+
def evaluate_single_coalition(self, coalition: np.ndarray) -> float:
147+
"""Evaluate a single coalition (combination of baseline and optimized hyperparameters)."""
148+
vals = np.array([ag.evaluate_single_coalition(coalition) for ag in self.ablation_games])
149+
return evaluate_aggregation(self.aggregation, vals)

src/hypershap/games/multi_data.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""The multi-data module provides a wrapper for extending explanation games to a multi-data setting."""
2+
3+
from __future__ import annotations
4+
5+
import copy
6+
from typing import TYPE_CHECKING
7+
8+
import numpy as np
9+
10+
from hypershap.games.ablation import MultiBaselineAblationGame
11+
from hypershap.games.abstract import AbstractHPIGame
12+
from hypershap.games.tunability import SearchBasedGame
13+
14+
if TYPE_CHECKING:
15+
from hypershap.task import ExplanationTask
16+
17+
from hypershap.utils import Aggregation, evaluate_aggregation
18+
19+
20+
class MultiDataHPIGame(AbstractHPIGame):
21+
"""The multi-data game generalizes an explanation game to multiple datasets."""
22+
23+
def __init__(
24+
self,
25+
explanation_task: ExplanationTask,
26+
base_game: AbstractHPIGame,
27+
aggregation: Aggregation,
28+
) -> None:
29+
"""Initialize the multi-data game wrapper.
30+
31+
Args:
32+
explanation_task: The explanation task containing the configuration space and surrogate model.
33+
base_game: The base game instance.
34+
aggregation: The aggregation method to use.
35+
36+
"""
37+
self.aggregation = aggregation
38+
self.base_game = base_game
39+
40+
self.sub_games = []
41+
for surrogate_model in explanation_task.get_surrogate_model_list():
42+
game_copy = copy.deepcopy(base_game)
43+
game_copy.explanation_task.surrogate_model = surrogate_model
44+
if isinstance(game_copy, SearchBasedGame):
45+
game_copy.cs_searcher.explanation_task.surrogate_model = surrogate_model
46+
if isinstance(game_copy, MultiBaselineAblationGame):
47+
for ag in game_copy.ablation_games:
48+
ag.explanation_task.surrogate_model = surrogate_model
49+
self.sub_games.append(game_copy)
50+
51+
super().__init__(explanation_task)
52+
53+
def evaluate_single_coalition(self, coalition: np.ndarray) -> float:
54+
"""Evaluate the multi-data game on the coalition.
55+
56+
Args:
57+
coalition: The coalition to evaluate.
58+
59+
Returns: The value of the multi-data game on the coalition.
60+
61+
"""
62+
vals = np.array([game.evaluate_single_coalition(coalition) for game in self.sub_games])
63+
return evaluate_aggregation(self.aggregation, vals)

src/hypershap/games/tunability.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333

3434
from hypershap.games.abstract import AbstractHPIGame
35-
from hypershap.utils import ConfigSpaceSearcher, RandomConfigSpaceSearcher
35+
from hypershap.utils import Aggregation, ConfigSpaceSearcher, RandomConfigSpaceSearcher
3636

3737
logger = logging.getLogger(__name__)
3838

@@ -106,10 +106,10 @@ def __init__(
106106
"""
107107
# set cs searcher if not given by default to a random config space searcher.
108108
if cs_searcher is None:
109-
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode="max")
110-
elif cs_searcher.mode != "max": # ensure that cs_searcher is maximizing
109+
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode=Aggregation.MAX)
110+
elif cs_searcher.mode != Aggregation.MAX: # ensure that cs_searcher is maximizing
111111
logger.warning("WARN: Tunability game set mode of given ConfigSpaceSearcher to maximize.")
112-
cs_searcher.mode = "max"
112+
cs_searcher.mode = Aggregation.MAX
113113
super().__init__(explanation_task, cs_searcher, n_workers=n_workers, verbose=verbose)
114114

115115

@@ -140,10 +140,10 @@ def __init__(
140140
"""
141141
# set cs searcher if not given by default to a random config space searcher.
142142
if cs_searcher is None:
143-
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode="var")
144-
elif cs_searcher.mode != "var": # ensure that cs_searcher is maximizing
143+
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode=Aggregation.VAR)
144+
elif cs_searcher.mode != Aggregation.VAR: # ensure that cs_searcher is maximizing
145145
logger.warning("WARN: Sensitivity game set mode of given ConfigSpaceSearcher to variance.")
146-
cs_searcher.mode = "var"
146+
cs_searcher.mode = Aggregation.VAR
147147

148148
super().__init__(explanation_task, cs_searcher, n_workers=n_workers, verbose=verbose)
149149

@@ -175,9 +175,9 @@ def __init__(
175175
"""
176176
# set cs searcher if not given by default to a random config space searcher.
177177
if cs_searcher is None:
178-
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode="min")
179-
elif cs_searcher.mode != "min": # ensure that cs_searcher is maximizing
178+
cs_searcher = RandomConfigSpaceSearcher(explanation_task, mode=Aggregation.MIN)
179+
elif cs_searcher.mode != Aggregation.MIN: # ensure that cs_searcher is maximizing
180180
logger.warning("WARN: Mistunability game set mode of given ConfigSpaceSearcher to minimize.")
181-
cs_searcher.mode = "min"
181+
cs_searcher.mode = Aggregation.MIN
182182

183183
super().__init__(explanation_task, cs_searcher, n_workers=n_workers, verbose=verbose)

0 commit comments

Comments
 (0)