Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- [**Evaluator**](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/evaluator/metrics/): The evaluator module provides various ways to evaluate and compare the performance and explainability of AI models with the categorized evaluation properties of correctness ([fidelity](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/evaluator/metrics/mu_fidelity.py), [area between perturbation curves](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/evaluator/metrics/pixel_flipping.py)), continuity ([sensitivity](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/evaluator/metrics/sensitivity.py)), and compactness ([complexity](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/evaluator/metrics/complexity.py)).
- **Explainers**: The explainers module contains a collection of state-of-the-art XAI methods that can generate global or local explanations for any AI model, such as:
- Perturbation-based ([SHAP](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/kernel_shap.py), [LIME](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/lime.py))
- Relevance-based ([IG](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/integrated_gradients.py), [LRP](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/lrp.py), [RAP](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/rap), [GuidedBackprop](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/guided_backprop.py))
- Relevance-based ([IG](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/integrated_gradients.py), [MA-GIG](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/magig.py), [LRP](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/lrp.py), [RAP](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/rap), [GuidedBackprop](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/guided_backprop.py))
- CAM-based ([GradCAM](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/grad_cam.py), [Guided GradCAM](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/guided_grad_cam.py))
- Gradient-based ([SmoothGrad](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/smooth_grad.py), [VarGrad](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/var_grad.py), [FullGrad](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/full_grad.py), [Gradient × Input](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/explainers/grad_x_input.py))
- [**Recommender**](https://github.com/OpenXAIProject/pnpxai/tree/main/pnpxai/core/recommender): The recommender module offers a recommender system that can suggest the most suitable XAI methods for a given model and dataset, based on the user’s preferences and goals.
Expand Down Expand Up @@ -125,6 +125,7 @@ This guide explains how to automatically explain your own models and datasets us
- [ImageNet Example All Explainers](https://github.com/OpenXAIProject/pnpxai/tree/main/tutorials/imagenet_example_all_explainers.md)
- [ImageNet Example All Metrics](https://github.com/OpenXAIProject/pnpxai/tree/main/tutorials/imagenet_example_all_metrics.md)
- [Free MCG](https://github.com/OpenXAIProject/pnpxai/tree/main/tutorials/gfgp_tutorial.py) [[Reference](https://arxiv.org/abs/2411.15265)]
- [MA-GIG Example All Explainers](https://github.com/OpenXAIProject/pnpxai/tree/main/tutorials/MAGIG_example_all_explainers.md) [[Reference](https://arxiv.org/abs/2605.02167)]

## Use Cases

Expand Down
1 change: 1 addition & 0 deletions docs/api/explainer/magig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: pnpxai.explainers.magig
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nav:
- SmoothGrad: api/explainer/smooth_grad.md
- VarGrad: api/explainer/var_grad.md
- IntegratedGradients: api/explainer/ig.md
- MAGIG: api/explainer/magig.md
- LRP: api/explainer/lrp.md
- RAP: api/explainer/rap.md
- KernelShap: api/explainer/kernel_shap.md
Expand Down
2 changes: 2 additions & 0 deletions pnpxai/explainers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pnpxai.explainers.lear import LEAR
from pnpxai.explainers.gfgp import Gfgp
from pnpxai.explainers.mad_xai import MadXai
from pnpxai.explainers.magig import MAGIG

CAM_BASED_EXPLAINERS = [GradCam, GuidedGradCam]
GRADIENT_BASED_EXPLAINERS = [
Expand All @@ -33,6 +34,7 @@
SmoothGrad,
VarGrad,
IntegratedGradients,
MAGIG,
LRPUniformEpsilon,
LRPEpsilonPlus,
LRPEpsilonGammaBox,
Expand Down
443 changes: 443 additions & 0 deletions pnpxai/explainers/magig.py

Large diffs are not rendered by default.

116 changes: 114 additions & 2 deletions tests/explainers/test_explainers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from abc import abstractmethod
from types import SimpleNamespace
import pytest
import torch
from torch import nn

from pnpxai.explainers import (
GradCam,
Expand All @@ -13,7 +15,8 @@
LRPEpsilonPlus,
LRPUniformEpsilon,
RAP,
Gfgp
Gfgp,
MAGIG,
)
from tests.helpers import ToyCNN, get_test_input_image

Expand Down Expand Up @@ -104,4 +107,113 @@ def test_model_loading(self):
transforms = lambda x: x
explainer = Gfgp(model, transforms)
assert explainer.diffusion_model is not None
assert explainer.diffusion is not None
assert explainer.diffusion is not None


class StubAutoencoder(nn.Module):
"""
Smallest thing shaped like a `diffusers` autoencoder.

Lets MAGIG be tested end to end -- path construction, latent gradients,
accumulation -- without pulling a real VAE off the Hub.
"""

def __init__(self, in_channels=3, latent_channels=4):
super().__init__()
self.enc = nn.Conv2d(in_channels, latent_channels, 1)
self.dec = nn.Conv2d(latent_channels, in_channels, 1)

@property
def dtype(self):
return self.enc.weight.dtype

def encode(self, x):
return SimpleNamespace(latent_dist=SimpleNamespace(mean=self.enc(x)))

def decode(self, z):
return SimpleNamespace(sample=self.dec(z))


class TestMAGIG(_TestExplainer):
@pytest.fixture
def explainer(self, model):
return MAGIG(model, vae=StubAutoencoder(), n_steps=4)

def test_path_endpoints_are_anchored(self, explainer, valid_input):
"""The path must start at the baseline and end at the input exactly."""
targets = torch.zeros(1, dtype=torch.long)
path = explainer.generate_path(valid_input, targets)
baseline = explainer.vae.normalize(torch.zeros_like(valid_input))
assert path.shape[0] == explainer.n_steps
assert torch.equal(path[0], baseline[0])
assert torch.equal(path[-1], valid_input[0])

def test_attribute_shape_matches_input(self, explainer, valid_input):
attrs = explainer.attribute(valid_input, torch.zeros(1, dtype=torch.long))
assert attrs.shape == valid_input.shape

def test_attribute_is_deterministic(self, explainer, valid_input):
targets = torch.zeros(1, dtype=torch.long)
first = explainer.attribute(valid_input, targets)
second = explainer.attribute(valid_input, targets)
assert torch.equal(first, second)

def test_batch_matches_per_sample(self, explainer):
"""Samples are explained independently, so a batch equals its parts."""
batch = torch.cat([get_test_input_image() for _ in range(3)])
targets = torch.tensor([0, 1, 0])
batched = explainer.attribute(batch, targets)
for i in range(batch.shape[0]):
alone = explainer.attribute(batch[i][None], targets[i][None])
assert torch.equal(batched[i][None], alone)

def test_rejects_unknown_objective(self, model):
with pytest.raises(ValueError):
MAGIG(model, vae=StubAutoencoder(), exp_obj="nonsense")

def test_no_determinism_warning_on_cpu(self, model):
"""The cuDNN determinism warning is CUDA-specific; CPU users shouldn't see it."""
import warnings

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
MAGIG(model, vae=StubAutoencoder(), n_steps=4)
assert not [w for w in caught if "cudnn" in str(w.message).lower()]

def test_baseline_fn_accepts_pnpxai_forms(self, model, valid_input):
"""None means the paper's black image; strings and functions go through
the base class's baseline machinery like any other explainer."""
from pnpxai.explainers.utils.baselines import ZeroBaselineFunction
from pnpxai.explainers.magig import IMAGENET_MEAN, IMAGENET_STD

black = MAGIG(model, vae=StubAutoencoder(), n_steps=4)._baselines_for(valid_input)
expected = torch.tensor([-m / s for m, s in zip(IMAGENET_MEAN, IMAGENET_STD)])
assert torch.allclose(black[0, :, 0, 0], expected)

for baseline_fn in ("zeros", ZeroBaselineFunction()):
explainer = MAGIG(model, vae=StubAutoencoder(), n_steps=4,
baseline_fn=baseline_fn)
assert torch.equal(explainer._baselines_for(valid_input),
torch.zeros_like(valid_input))
assert explainer.attribute(
valid_input, torch.zeros(1, dtype=torch.long)
).shape == valid_input.shape

def test_tunables_are_suggestable(self, explainer):
"""Every tunable must be a type the optimizer's suggestor can sample."""
import optuna
from pnpxai.core.modality.modality import ImageModality
from pnpxai.evaluator.optimizer.suggestor import suggest

optuna.logging.set_verbosity(optuna.logging.WARNING)
modality = ImageModality()

def objective(trial):
tuned = suggest(trial, explainer, modality)
assert isinstance(tuned.n_steps, int)
assert isinstance(tuned.use_slerp, bool)
return float(tuned.n_steps)

study = optuna.create_study(sampler=optuna.samplers.TPESampler(seed=0))
study.optimize(objective, n_trials=3)
assert len(study.trials) == 3
Loading