explainiverse 0.1.1a1__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- explainiverse/__init__.py +45 -1
- explainiverse/adapters/__init__.py +9 -0
- explainiverse/adapters/base_adapter.py +25 -25
- explainiverse/adapters/sklearn_adapter.py +32 -32
- explainiverse/core/__init__.py +22 -0
- explainiverse/core/explainer.py +31 -31
- explainiverse/core/explanation.py +24 -24
- explainiverse/core/registry.py +563 -0
- explainiverse/engine/__init__.py +8 -0
- explainiverse/engine/suite.py +142 -142
- explainiverse/evaluation/__init__.py +8 -0
- explainiverse/evaluation/metrics.py +232 -232
- explainiverse/explainers/__init__.py +41 -0
- explainiverse/explainers/attribution/__init__.py +10 -0
- explainiverse/explainers/attribution/lime_wrapper.py +90 -63
- explainiverse/explainers/attribution/shap_wrapper.py +89 -66
- explainiverse/explainers/attribution/treeshap_wrapper.py +434 -0
- explainiverse/explainers/counterfactual/__init__.py +8 -0
- explainiverse/explainers/counterfactual/dice_wrapper.py +302 -0
- explainiverse/explainers/global_explainers/__init__.py +23 -0
- explainiverse/explainers/global_explainers/ale.py +191 -0
- explainiverse/explainers/global_explainers/partial_dependence.py +192 -0
- explainiverse/explainers/global_explainers/permutation_importance.py +123 -0
- explainiverse/explainers/global_explainers/sage.py +164 -0
- explainiverse/explainers/rule_based/__init__.py +8 -0
- explainiverse/explainers/rule_based/anchors_wrapper.py +350 -0
- explainiverse-0.2.1.dist-info/METADATA +264 -0
- explainiverse-0.2.1.dist-info/RECORD +30 -0
- explainiverse-0.1.1a1.dist-info/METADATA +0 -128
- explainiverse-0.1.1a1.dist-info/RECORD +0 -19
- {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.1.dist-info}/LICENSE +0 -0
- {explainiverse-0.1.1a1.dist-info → explainiverse-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# src/explainiverse/core/registry.py
|
|
2
|
+
"""
|
|
3
|
+
ExplainerRegistry - A plugin system for XAI methods.
|
|
4
|
+
|
|
5
|
+
This module provides a flexible registry that allows:
|
|
6
|
+
- Registration of explainers with rich metadata
|
|
7
|
+
- Filtering/discovery by scope, model type, data type, task type
|
|
8
|
+
- Easy instantiation with dependency injection
|
|
9
|
+
- Decorator-based registration for clean syntax
|
|
10
|
+
- Recommendations based on use case
|
|
11
|
+
|
|
12
|
+
Example usage:
|
|
13
|
+
from explainiverse.core.registry import default_registry, ExplainerMeta
|
|
14
|
+
|
|
15
|
+
# List available explainers
|
|
16
|
+
print(default_registry.list_explainers())
|
|
17
|
+
|
|
18
|
+
# Filter by criteria
|
|
19
|
+
local_tabular = default_registry.filter(scope="local", data_type="tabular")
|
|
20
|
+
|
|
21
|
+
# Create an explainer
|
|
22
|
+
explainer = default_registry.create("lime", model=adapter, training_data=X, ...)
|
|
23
|
+
|
|
24
|
+
# Register a custom explainer
|
|
25
|
+
@default_registry.register_decorator(
|
|
26
|
+
name="my_explainer",
|
|
27
|
+
meta=ExplainerMeta(scope="local", description="My custom explainer")
|
|
28
|
+
)
|
|
29
|
+
class MyExplainer(BaseExplainer):
|
|
30
|
+
...
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Dict, List, Optional, Any, Type, Callable
|
|
35
|
+
from explainiverse.core.explainer import BaseExplainer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ExplainerMeta:
|
|
40
|
+
"""
|
|
41
|
+
Metadata for an explainer, used for discovery and recommendations.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
scope: "local" (instance-level) or "global" (model-level)
|
|
45
|
+
model_types: List of compatible model types ("any", "tree", "linear", "neural", "ensemble")
|
|
46
|
+
data_types: List of compatible data types ("tabular", "image", "text", "time_series")
|
|
47
|
+
task_types: List of compatible tasks ("classification", "regression")
|
|
48
|
+
description: Human-readable description of the explainer
|
|
49
|
+
paper_reference: Citation for the original paper
|
|
50
|
+
complexity: Computational complexity (e.g., "O(n)", "O(n^2)")
|
|
51
|
+
requires_training_data: Whether the explainer needs training data
|
|
52
|
+
supports_batching: Whether the explainer can process batches efficiently
|
|
53
|
+
"""
|
|
54
|
+
scope: str # "local" or "global"
|
|
55
|
+
model_types: List[str] = field(default_factory=lambda: ["any"])
|
|
56
|
+
data_types: List[str] = field(default_factory=lambda: ["tabular"])
|
|
57
|
+
task_types: List[str] = field(default_factory=lambda: ["classification", "regression"])
|
|
58
|
+
description: str = ""
|
|
59
|
+
paper_reference: Optional[str] = None
|
|
60
|
+
complexity: Optional[str] = None
|
|
61
|
+
requires_training_data: bool = False
|
|
62
|
+
supports_batching: bool = False
|
|
63
|
+
|
|
64
|
+
def matches(
|
|
65
|
+
self,
|
|
66
|
+
scope: Optional[str] = None,
|
|
67
|
+
model_type: Optional[str] = None,
|
|
68
|
+
data_type: Optional[str] = None,
|
|
69
|
+
task_type: Optional[str] = None
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""Check if this metadata matches the given criteria."""
|
|
72
|
+
if scope is not None and self.scope != scope:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
if model_type is not None:
|
|
76
|
+
if "any" not in self.model_types and model_type not in self.model_types:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
if data_type is not None:
|
|
80
|
+
if data_type not in self.data_types:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
if task_type is not None:
|
|
84
|
+
if task_type not in self.task_types:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ExplainerRegistry:
|
|
91
|
+
"""
|
|
92
|
+
Central registry for all explainers in Explainiverse.
|
|
93
|
+
|
|
94
|
+
Provides:
|
|
95
|
+
- Registration (programmatic and decorator-based)
|
|
96
|
+
- Discovery and filtering
|
|
97
|
+
- Instantiation with dependency injection
|
|
98
|
+
- Recommendations based on use case
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self._registry: Dict[str, Dict[str, Any]] = {}
|
|
103
|
+
|
|
104
|
+
def register(
|
|
105
|
+
self,
|
|
106
|
+
name: str,
|
|
107
|
+
explainer_class: Type[BaseExplainer],
|
|
108
|
+
meta: ExplainerMeta,
|
|
109
|
+
override: bool = False
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Register an explainer class with metadata.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
name: Unique identifier for the explainer (e.g., "lime", "shap")
|
|
116
|
+
explainer_class: The explainer class (must inherit from BaseExplainer)
|
|
117
|
+
meta: Metadata describing the explainer's capabilities
|
|
118
|
+
override: If True, allows overwriting existing registration
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: If name is already registered and override=False
|
|
122
|
+
"""
|
|
123
|
+
if name in self._registry and not override:
|
|
124
|
+
raise ValueError(f"Explainer '{name}' is already registered. Use override=True to replace.")
|
|
125
|
+
|
|
126
|
+
self._registry[name] = {
|
|
127
|
+
"class": explainer_class,
|
|
128
|
+
"meta": meta
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def unregister(self, name: str) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Remove an explainer from the registry.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: The explainer name to remove
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
KeyError: If the explainer is not registered
|
|
140
|
+
"""
|
|
141
|
+
if name not in self._registry:
|
|
142
|
+
raise KeyError(f"Explainer '{name}' is not registered.")
|
|
143
|
+
del self._registry[name]
|
|
144
|
+
|
|
145
|
+
def get(self, name: str) -> Dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Get the explainer class and metadata by name.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: The explainer name
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Dict with "class" and "meta" keys
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
KeyError: If the explainer is not registered
|
|
157
|
+
"""
|
|
158
|
+
if name not in self._registry:
|
|
159
|
+
raise KeyError(f"Explainer '{name}' is not registered. Available: {list(self._registry.keys())}")
|
|
160
|
+
return self._registry[name]
|
|
161
|
+
|
|
162
|
+
def get_meta(self, name: str) -> ExplainerMeta:
|
|
163
|
+
"""
|
|
164
|
+
Get just the metadata for an explainer.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: The explainer name
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
ExplainerMeta instance
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
KeyError: If the explainer is not registered
|
|
174
|
+
"""
|
|
175
|
+
return self.get(name)["meta"]
|
|
176
|
+
|
|
177
|
+
def list_explainers(self, with_meta: bool = False) -> Any:
|
|
178
|
+
"""
|
|
179
|
+
List all registered explainers.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
with_meta: If True, return dict with metadata; if False, return list of names
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of names or dict of {name: {"class": ..., "meta": ...}}
|
|
186
|
+
"""
|
|
187
|
+
if with_meta:
|
|
188
|
+
return dict(self._registry)
|
|
189
|
+
return list(self._registry.keys())
|
|
190
|
+
|
|
191
|
+
def filter(
|
|
192
|
+
self,
|
|
193
|
+
scope: Optional[str] = None,
|
|
194
|
+
model_type: Optional[str] = None,
|
|
195
|
+
data_type: Optional[str] = None,
|
|
196
|
+
task_type: Optional[str] = None
|
|
197
|
+
) -> List[str]:
|
|
198
|
+
"""
|
|
199
|
+
Filter explainers by criteria.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
scope: "local" or "global"
|
|
203
|
+
model_type: "any", "tree", "linear", "neural", "ensemble"
|
|
204
|
+
data_type: "tabular", "image", "text", "time_series"
|
|
205
|
+
task_type: "classification" or "regression"
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List of matching explainer names
|
|
209
|
+
"""
|
|
210
|
+
results = []
|
|
211
|
+
for name, entry in self._registry.items():
|
|
212
|
+
meta: ExplainerMeta = entry["meta"]
|
|
213
|
+
if meta.matches(scope, model_type, data_type, task_type):
|
|
214
|
+
results.append(name)
|
|
215
|
+
return results
|
|
216
|
+
|
|
217
|
+
def create(self, name: str, **kwargs) -> BaseExplainer:
|
|
218
|
+
"""
|
|
219
|
+
Instantiate an explainer by name with the given arguments.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
name: The explainer name
|
|
223
|
+
**kwargs: Arguments to pass to the explainer constructor
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Instantiated explainer
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
KeyError: If the explainer is not registered
|
|
230
|
+
"""
|
|
231
|
+
entry = self.get(name)
|
|
232
|
+
explainer_class = entry["class"]
|
|
233
|
+
return explainer_class(**kwargs)
|
|
234
|
+
|
|
235
|
+
def register_decorator(
|
|
236
|
+
self,
|
|
237
|
+
name: str,
|
|
238
|
+
meta: ExplainerMeta
|
|
239
|
+
) -> Callable[[Type[BaseExplainer]], Type[BaseExplainer]]:
|
|
240
|
+
"""
|
|
241
|
+
Decorator for registering an explainer class.
|
|
242
|
+
|
|
243
|
+
Usage:
|
|
244
|
+
@registry.register_decorator(
|
|
245
|
+
name="my_explainer",
|
|
246
|
+
meta=ExplainerMeta(scope="local")
|
|
247
|
+
)
|
|
248
|
+
class MyExplainer(BaseExplainer):
|
|
249
|
+
...
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
name: Unique identifier for the explainer
|
|
253
|
+
meta: Metadata describing the explainer
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Decorator function that registers the class and returns it unchanged
|
|
257
|
+
"""
|
|
258
|
+
def decorator(cls: Type[BaseExplainer]) -> Type[BaseExplainer]:
|
|
259
|
+
self.register(name, cls, meta)
|
|
260
|
+
return cls
|
|
261
|
+
return decorator
|
|
262
|
+
|
|
263
|
+
def summary(self) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Generate a human-readable summary of all registered explainers.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Formatted string summary
|
|
269
|
+
"""
|
|
270
|
+
lines = ["=" * 60, "Explainiverse - Registered Explainers", "=" * 60, ""]
|
|
271
|
+
|
|
272
|
+
# Group by scope
|
|
273
|
+
local = []
|
|
274
|
+
global_ = []
|
|
275
|
+
|
|
276
|
+
for name, entry in self._registry.items():
|
|
277
|
+
meta: ExplainerMeta = entry["meta"]
|
|
278
|
+
info = f" {name}: {meta.description or '(no description)'}"
|
|
279
|
+
if meta.scope == "local":
|
|
280
|
+
local.append(info)
|
|
281
|
+
else:
|
|
282
|
+
global_.append(info)
|
|
283
|
+
|
|
284
|
+
if local:
|
|
285
|
+
lines.append("LOCAL EXPLAINERS (instance-level):")
|
|
286
|
+
lines.extend(local)
|
|
287
|
+
lines.append("")
|
|
288
|
+
|
|
289
|
+
if global_:
|
|
290
|
+
lines.append("GLOBAL EXPLAINERS (model-level):")
|
|
291
|
+
lines.extend(global_)
|
|
292
|
+
lines.append("")
|
|
293
|
+
|
|
294
|
+
lines.append(f"Total: {len(self._registry)} explainers")
|
|
295
|
+
lines.append("=" * 60)
|
|
296
|
+
|
|
297
|
+
return "\n".join(lines)
|
|
298
|
+
|
|
299
|
+
def recommend(
|
|
300
|
+
self,
|
|
301
|
+
model_type: Optional[str] = None,
|
|
302
|
+
data_type: Optional[str] = None,
|
|
303
|
+
task_type: Optional[str] = None,
|
|
304
|
+
scope_preference: Optional[str] = None,
|
|
305
|
+
max_results: int = 5
|
|
306
|
+
) -> List[str]:
|
|
307
|
+
"""
|
|
308
|
+
Recommend explainers based on criteria.
|
|
309
|
+
|
|
310
|
+
This is a smarter version of filter() that ranks results
|
|
311
|
+
by compatibility and preference.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
model_type: The type of model being explained
|
|
315
|
+
data_type: The type of data
|
|
316
|
+
task_type: The ML task type
|
|
317
|
+
scope_preference: Preferred scope ("local" or "global")
|
|
318
|
+
max_results: Maximum number of recommendations
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of recommended explainer names, ranked by relevance
|
|
322
|
+
"""
|
|
323
|
+
candidates = self.filter(
|
|
324
|
+
model_type=model_type,
|
|
325
|
+
data_type=data_type,
|
|
326
|
+
task_type=task_type
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Score candidates
|
|
330
|
+
scored = []
|
|
331
|
+
for name in candidates:
|
|
332
|
+
meta = self.get_meta(name)
|
|
333
|
+
score = 0
|
|
334
|
+
|
|
335
|
+
# Prefer matching scope
|
|
336
|
+
if scope_preference and meta.scope == scope_preference:
|
|
337
|
+
score += 10
|
|
338
|
+
|
|
339
|
+
# Prefer specific model types over "any"
|
|
340
|
+
if model_type and model_type in meta.model_types:
|
|
341
|
+
score += 5
|
|
342
|
+
|
|
343
|
+
# Prefer explainers with documentation
|
|
344
|
+
if meta.description:
|
|
345
|
+
score += 1
|
|
346
|
+
if meta.paper_reference:
|
|
347
|
+
score += 2
|
|
348
|
+
|
|
349
|
+
scored.append((name, score))
|
|
350
|
+
|
|
351
|
+
# Sort by score descending
|
|
352
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
353
|
+
|
|
354
|
+
return [name for name, _ in scored[:max_results]]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# =============================================================================
|
|
358
|
+
# Default Global Registry
|
|
359
|
+
# =============================================================================
|
|
360
|
+
|
|
361
|
+
def _create_default_registry() -> ExplainerRegistry:
|
|
362
|
+
"""Create and populate the default global registry."""
|
|
363
|
+
from explainiverse.explainers.attribution.lime_wrapper import LimeExplainer
|
|
364
|
+
from explainiverse.explainers.attribution.shap_wrapper import ShapExplainer
|
|
365
|
+
from explainiverse.explainers.attribution.treeshap_wrapper import TreeShapExplainer
|
|
366
|
+
from explainiverse.explainers.rule_based.anchors_wrapper import AnchorsExplainer
|
|
367
|
+
from explainiverse.explainers.global_explainers.permutation_importance import PermutationImportanceExplainer
|
|
368
|
+
from explainiverse.explainers.global_explainers.partial_dependence import PartialDependenceExplainer
|
|
369
|
+
from explainiverse.explainers.global_explainers.ale import ALEExplainer
|
|
370
|
+
from explainiverse.explainers.global_explainers.sage import SAGEExplainer
|
|
371
|
+
from explainiverse.explainers.counterfactual.dice_wrapper import CounterfactualExplainer
|
|
372
|
+
|
|
373
|
+
registry = ExplainerRegistry()
|
|
374
|
+
|
|
375
|
+
# =========================================================================
|
|
376
|
+
# Local Explainers (instance-level)
|
|
377
|
+
# =========================================================================
|
|
378
|
+
|
|
379
|
+
# Register LIME
|
|
380
|
+
registry.register(
|
|
381
|
+
name="lime",
|
|
382
|
+
explainer_class=LimeExplainer,
|
|
383
|
+
meta=ExplainerMeta(
|
|
384
|
+
scope="local",
|
|
385
|
+
model_types=["any"],
|
|
386
|
+
data_types=["tabular", "text", "image"],
|
|
387
|
+
task_types=["classification", "regression"],
|
|
388
|
+
description="Local Interpretable Model-agnostic Explanations",
|
|
389
|
+
paper_reference="Ribeiro et al., 2016 - 'Why Should I Trust You?'",
|
|
390
|
+
complexity="O(n_samples * n_features)",
|
|
391
|
+
requires_training_data=True,
|
|
392
|
+
supports_batching=False
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Register SHAP (KernelSHAP)
|
|
397
|
+
registry.register(
|
|
398
|
+
name="shap",
|
|
399
|
+
explainer_class=ShapExplainer,
|
|
400
|
+
meta=ExplainerMeta(
|
|
401
|
+
scope="local",
|
|
402
|
+
model_types=["any"],
|
|
403
|
+
data_types=["tabular"],
|
|
404
|
+
task_types=["classification", "regression"],
|
|
405
|
+
description="SHapley Additive exPlanations (KernelSHAP)",
|
|
406
|
+
paper_reference="Lundberg & Lee, 2017 - 'A Unified Approach to Interpreting Model Predictions'",
|
|
407
|
+
complexity="O(2^n_features) approximated",
|
|
408
|
+
requires_training_data=True,
|
|
409
|
+
supports_batching=True
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Register TreeSHAP (optimized for tree models)
|
|
414
|
+
registry.register(
|
|
415
|
+
name="treeshap",
|
|
416
|
+
explainer_class=TreeShapExplainer,
|
|
417
|
+
meta=ExplainerMeta(
|
|
418
|
+
scope="local",
|
|
419
|
+
model_types=["tree", "ensemble"],
|
|
420
|
+
data_types=["tabular"],
|
|
421
|
+
task_types=["classification", "regression"],
|
|
422
|
+
description="TreeSHAP - exact SHAP values for tree-based models (RandomForest, XGBoost, etc.)",
|
|
423
|
+
paper_reference="Lundberg et al., 2018 - 'Consistent Individualized Feature Attribution for Tree Ensembles'",
|
|
424
|
+
complexity="O(TLD^2) - polynomial in tree depth",
|
|
425
|
+
requires_training_data=False,
|
|
426
|
+
supports_batching=True
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Register Anchors
|
|
431
|
+
registry.register(
|
|
432
|
+
name="anchors",
|
|
433
|
+
explainer_class=AnchorsExplainer,
|
|
434
|
+
meta=ExplainerMeta(
|
|
435
|
+
scope="local",
|
|
436
|
+
model_types=["any"],
|
|
437
|
+
data_types=["tabular"],
|
|
438
|
+
task_types=["classification"],
|
|
439
|
+
description="High-precision rule-based explanations using beam search",
|
|
440
|
+
paper_reference="Ribeiro et al., 2018 - 'Anchors: High-Precision Model-Agnostic Explanations' (AAAI)",
|
|
441
|
+
complexity="O(beam_size * n_features * n_samples)",
|
|
442
|
+
requires_training_data=True,
|
|
443
|
+
supports_batching=False
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Register Counterfactual (DiCE-style)
|
|
448
|
+
registry.register(
|
|
449
|
+
name="counterfactual",
|
|
450
|
+
explainer_class=CounterfactualExplainer,
|
|
451
|
+
meta=ExplainerMeta(
|
|
452
|
+
scope="local",
|
|
453
|
+
model_types=["any"],
|
|
454
|
+
data_types=["tabular"],
|
|
455
|
+
task_types=["classification"],
|
|
456
|
+
description="Diverse counterfactual explanations via gradient-free optimization",
|
|
457
|
+
paper_reference="Mothilal et al., 2020 - 'Explaining ML Classifiers through Diverse Counterfactual Explanations' (FAT*)",
|
|
458
|
+
complexity="O(n_counterfactuals * optimization_steps)",
|
|
459
|
+
requires_training_data=True,
|
|
460
|
+
supports_batching=False
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# =========================================================================
|
|
465
|
+
# Global Explainers (model-level)
|
|
466
|
+
# =========================================================================
|
|
467
|
+
|
|
468
|
+
# Register Permutation Importance
|
|
469
|
+
registry.register(
|
|
470
|
+
name="permutation_importance",
|
|
471
|
+
explainer_class=PermutationImportanceExplainer,
|
|
472
|
+
meta=ExplainerMeta(
|
|
473
|
+
scope="global",
|
|
474
|
+
model_types=["any"],
|
|
475
|
+
data_types=["tabular"],
|
|
476
|
+
task_types=["classification", "regression"],
|
|
477
|
+
description="Feature importance via permutation-based performance degradation",
|
|
478
|
+
paper_reference="Breiman, 2001 - 'Random Forests' (Machine Learning)",
|
|
479
|
+
complexity="O(n_features * n_repeats * n_samples)",
|
|
480
|
+
requires_training_data=True,
|
|
481
|
+
supports_batching=False
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Register Partial Dependence
|
|
486
|
+
registry.register(
|
|
487
|
+
name="partial_dependence",
|
|
488
|
+
explainer_class=PartialDependenceExplainer,
|
|
489
|
+
meta=ExplainerMeta(
|
|
490
|
+
scope="global",
|
|
491
|
+
model_types=["any"],
|
|
492
|
+
data_types=["tabular"],
|
|
493
|
+
task_types=["classification", "regression"],
|
|
494
|
+
description="Marginal effect of features on predictions (PDP)",
|
|
495
|
+
paper_reference="Friedman, 2001 - 'Greedy Function Approximation' (Annals of Statistics)",
|
|
496
|
+
complexity="O(grid_resolution * n_samples)",
|
|
497
|
+
requires_training_data=True,
|
|
498
|
+
supports_batching=True
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Register ALE
|
|
503
|
+
registry.register(
|
|
504
|
+
name="ale",
|
|
505
|
+
explainer_class=ALEExplainer,
|
|
506
|
+
meta=ExplainerMeta(
|
|
507
|
+
scope="global",
|
|
508
|
+
model_types=["any"],
|
|
509
|
+
data_types=["tabular"],
|
|
510
|
+
task_types=["classification", "regression"],
|
|
511
|
+
description="Accumulated Local Effects - unbiased alternative to PDP for correlated features",
|
|
512
|
+
paper_reference="Apley & Zhu, 2020 - 'Visualizing the Effects of Predictor Variables' (JRSS-B)",
|
|
513
|
+
complexity="O(n_bins * n_samples)",
|
|
514
|
+
requires_training_data=True,
|
|
515
|
+
supports_batching=True
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Register SAGE
|
|
520
|
+
registry.register(
|
|
521
|
+
name="sage",
|
|
522
|
+
explainer_class=SAGEExplainer,
|
|
523
|
+
meta=ExplainerMeta(
|
|
524
|
+
scope="global",
|
|
525
|
+
model_types=["any"],
|
|
526
|
+
data_types=["tabular"],
|
|
527
|
+
task_types=["classification", "regression"],
|
|
528
|
+
description="Shapley Additive Global importancE - global feature importance via Shapley values",
|
|
529
|
+
paper_reference="Covert et al., 2020 - 'Understanding Global Feature Contributions' (NeurIPS)",
|
|
530
|
+
complexity="O(n_permutations * n_features * n_samples)",
|
|
531
|
+
requires_training_data=True,
|
|
532
|
+
supports_batching=False
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
return registry
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# Lazy initialization to avoid circular imports
|
|
540
|
+
_default_registry: Optional[ExplainerRegistry] = None
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def get_default_registry() -> ExplainerRegistry:
|
|
544
|
+
"""Get the default global registry (lazy initialization)."""
|
|
545
|
+
global _default_registry
|
|
546
|
+
if _default_registry is None:
|
|
547
|
+
_default_registry = _create_default_registry()
|
|
548
|
+
return _default_registry
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# For convenience, expose as module-level variable
|
|
552
|
+
# This will be initialized on first access
|
|
553
|
+
class _LazyRegistry:
|
|
554
|
+
"""Lazy proxy for the default registry."""
|
|
555
|
+
|
|
556
|
+
def __getattr__(self, name):
|
|
557
|
+
return getattr(get_default_registry(), name)
|
|
558
|
+
|
|
559
|
+
def __contains__(self, item):
|
|
560
|
+
return item in get_default_registry().list_explainers()
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
default_registry = _LazyRegistry()
|