shapiq.vision Sub-package for Image Classification - #549
Conversation
* ADD: image imputer implementation for resnet with superpixel players and zero / mean color masking * added image explainer and game execution to image mvp notebook * Merge remote-tracking branch 'vision/vit-prototype' * ADD: initialized package structure for shapiq.vision * ADD: entry point to Image Explainer * ADD: players and masking strategies copied from resnet prototype * ADD: ImageImputer copied from resnet prototype * ADD: ImageExplainer only for resnet, structur similar to TabularExplainer * ADD: init file for vision subpackage * ADD: Patchstrategy for ViT & value function in imputer * ADD: default masking and player strategy decision in explainer * ADD: bug fixes to run explainer for both resnet and vit * ADD: testing notebook to visualize how to interact with the explainer * ADD: moved prototype notebooks to specific folder * ADD: cat image from current frontend testing * REFACTOR: decouple vision package via ModelArchitectureStrategy Replaces model_type if/elif branches with ResNetArchitecture and ViTArchitecture. Player and masking strategies split into Pixel/Latent subtypes. Update test notebook with current implementation * ADD: - dynamic dispatch of model architecture based on the given model - refactor some variable names, cleaned imputer - model architecture interaction * REMOVE: debugging prints * REFACTOR: testing notebook * ADD: plotting function for heatmap Taking image (as numpy array), the explainer and label type as input and outputting one plot made up of 2 subplots: First plot is image with alpha overlay of first order interaction values. Second plot is actual values in bar chart. * ADD: Heatmap only argument, so barchart not plotted * Merge branch 'superpixel-improvement' --------- Co-authored-by: Tamara Muras <Tamara.Muras> Co-authored-by: Alexander Feix <alexander.feix03@gmail.com> Co-authored-by: S2k-1 <219272227+S2k-1@users.noreply.github.com>
* REFACTOR: Restnet/Pixel -> CNN and ViT/latent -> Transformer. For player and masking strategies aswell as architecture * REFACTOR: split player definition and masking into respective classes for Transformers * REFACTOR: rename masking strategy function, move logit call to architecture and adjust arguments for function call. * ADD: torch import only where necessary and ensure annotations are evaluated lazy * REFACTOR: move build pixel mask to patch strategy and introduce player mask property in architecture for visualizations * REFACTOR: change order of player strategies in file --------- Co-authored-by: Tamara Muras <Tamara.Muras> Co-authored-by: S2k-1 <219272227+S2k-1@users.noreply.github.com>
…and internal handling between torch and numpy (#34) * ADD: image conversion methods in utils * REFACTOR: change internal handling to torch mainly, except for players * REFACTOR: convert player masks to torch in architecture instead of masking * ADD: ImageLike typy to also support input of torch and pil images * Fix: wrong typing in architecture * remove debug print and unnecessary method * ADD: appropirate batching in the value function of imputer * REFACTOR: comment on masking strategies * comment on players * REFACTOR: removed model auto dispatch and require to input model architecture * ADD: improved docstring on imputer * ADD: player documentation --------- Co-authored-by: Tamara Muras <Tamara.Muras>
* ADD: imputer fit method and image property in imputer * fix: explainer only updates imputer when x is not None being passed to explain * remove unintended import --------- Co-authored-by: Tamara Muras <Tamara.Muras>
…tom masks from Superpixels (#37) * ADD: Custom Player strategy and gridstrategy for CNN architectures * Refactor: exclude custom masks from superpixels and add to customplayer strategy * Refactor: improve grid strategy to take patch or grid size instead of row and cols --------- Co-authored-by: Tamara Muras <Tamara.Muras>
* ADD: vision tests * ADD & FIX: added readl model tests and fixed tensor channel bug * ADD: unit test cases for custom players * Fix: custom player mask not casted to bool * ADD: unit tests for grid strategy * ADD: tests for imputer fit method * Fix: imputer not returning self * Add: Adjust explainer tests due to changed implementation of explain function and renamed batch size arg in imputer * fix: failing tests due to inconsistent renaming of batch size arg --------- Co-authored-by: Tamara Muras <Tamara.Muras> Co-authored-by: Alexander Feix <alexander.feix03@gmail.com>
* Remove: notebooks used for quick testing * fix: apply pre-commit auto-fixes for vision package * Refactor: improve code quality and add docstrings * Refactor: fix all code quality issues from main files of vision package - did not improve plot functions - did not remove lazy import statements as recommended * fix: explainer couldnt be initialized with transformer model --------- Co-authored-by: Tamara Muras <Tamara.Muras>
|
Hello, thank you for your PR already! Please ping me here as soon as you want a PR review. :) Note however, that for a review, the CI pipeline should really be green first (all tests pass, the code-quality checks are okay, and the docs building pipeline compiles). Otherwise it's hard for me to give you good feedback. :) I can also approve the workflows from time to time if you ping me. |
|
@mmschlk ty/ruff errors fixed. CI workflow on our fork only got errors for codecov upload. (expected due to missing token) |
…s for install (#40) * ADD: example notebooks for quickstart and on defining players * Fix: docstring inaccuracies for sphinx doc and finalize image explanation examples * fix: code quality * remove code blocks from example * ADD: proper shapiq[vision] package to install and add tests to ensure import errors appear * fix: test for import error when running in a row with the other tests * fix: code quality * fix: imputer torch import and CI pipeline shapiq import * ADD: imputer to framework import testing --------- Co-authored-by: Tamara Muras <Tamara.Muras>
|
We also added two examples to the documentation to showcase how to interact with the vision subpackage and how to define different player strategies. These are kept rather simple until now, we'll extend them after we decided how to refactor the architectures (after the feedback). We also now copied the behavior from ShaplEIG and SPEX approximators in defining optional dependencies for the vision package. |
|
@mmschlk Could you approve the workflow, so we can verify the full pipeline runs successfully? |
|
@Advueu963 could you please approve the workflow or ping @mmschlk |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
mmschlk
left a comment
There was a problem hiding this comment.
Thank you already for your pull request and all of your work already on this matter!! :) I only had some minor comments (see individually). It's already pretty nice!
The only "major-ish" thing I have is also a bit of a question. Did we decided not to wirre ImageExplainer into the Explainer dispatching logic? So, we said we do not expect users to do
explainer = Explainer(image_model, ...)
and expect explainer to be ImageExplainer? Because I think this is not possible, yet. I would however, like to know weather it would be easy to allow users to call
explainer = Explainer(CNNArchitecture(model), ...)
and then get ImageExplainer. But then only these special architectures would work in the dispatching. Is this doable?
Thank you already!
| data: ImageLike, | ||
| *, | ||
| imputer: ImageImputer | None = None, | ||
| index: ExplainerIndices = "k-SII", |
There was a problem hiding this comment.
we actually went to "SV" as default in v1.5.0 and order 1.
| Returns: | ||
| InteractionValues: The interaction values of the prediction. | ||
| """ | ||
| budget: int = kwargs.get("budget", 64) |
There was a problem hiding this comment.
A few parameters are out of step with our other explainers and I'd like parity before we can merge:
- budget is read from kwargs here instead of being an explicit argument (compare TabularExplainer.explain_function(x, budget=...)). Please make it a real signature parameter so it shows up in the docs/IDE.
- no random_state is this not also necessary here?
| def default_player_strategy(self) -> PatchStrategy: | ||
| """Return a patch player strategy with a 3x3 grid.""" | ||
| grid_size = self._model.config.image_size // self._model.config.patch_size | ||
| return PatchStrategy(grid_size=grid_size, n_players=9) |
There was a problem hiding this comment.
This hard-codes a 3x3 grid, but PatchStrategy.init requires grid_size % sqrt(n_players) == 0. For one of the most common ViT, vit-base-patch16-224, the grid_size is 224/16 = 14 and 14 % 3 != 0, so this raises ValueError on construction and the default path crashes for the standard model. The examples only work because they use patch32-384 (grid 12). Please make the default adapt to grid_size (pick a perfect square whose root divides the grid), and add a regression test against a 14x14 grid so this can't silently come back.
| """Vision-based explanation methods for image models.""" | ||
|
|
||
| try: | ||
| from .architecture import CNNArchitecture, ModelArchitectureStrategy, TransformerArchitecture |
There was a problem hiding this comment.
Heads-up, and this one isn't really your fault as we already have the same pattern in ShaplEIG. This eager from .architecture import ... pulls architecture.py (and its top-level import torch) at import-shapiq time, so even a tabular-only user pays ~0.5s of torch import. The try/except guards graceful failure but not laziness. Please defer this with a module-level getattr (PEP 562) so torch is only imported when ImageExplainer is actually used. Soon I will open a matching issue against ShaplEIG so we fix both consistently then.
shapiq.vision Sub-package for Image Classification
* fix: default values for explainer init * fix: method signature of explain function and missing arguments in init * Add: class id argument to explainer init and inside architecture / imputer init to hand over * Add: tests for requested changes in particular setting class index * Refactor: model architecture argument of explainer init to model (to make suitable for later protocol implementations and to allow dispatch) * Add: dispatch for image explainer and test function * Refactor: set slic to default superpixel algorithm * FIX: decouple plot_image_attributions form vision subpackage * FIX: adapt default ViT patch grid to model grid_size * FIX: Defer vision torch import via PEP 562 * FIX: rebuild approximator when player count changes * FIX: cover image_attribution_plot in tests and change called colormap api * FIX: handle pre commit errors and change auto fix for subpackage import * FIX: add blank line to fix ruff auto-fix --------- Co-authored-by: Tamara Muras <Tamara.Muras> Co-authored-by: S2k-1 <219272227+S2k-1@users.noreply.github.com>
|
@mmschlk: Thank you for your feedback! We have implemented the requested changes. Could you approve the workflow again and check if the changes match your expectation? |
|
Got it! I approved the runs. There are now some issues with the current main branch after I merged a few things in. Mea culpa 😇 |
|
@mmschlk: Resolved already 😊, thanks! |
mmschlk
left a comment
There was a problem hiding this comment.
I already went through the current state and my comments seemed to be all resolved now, apart from the "typing" model definition in the explainer (such that ImageExplainer can be also called without a specific strategy in mind). I think you awyways are still cooking something up there, right? :)
CNNMaskingStrategy._build_pixel_mask matmuls the coalition matrix against the player masks. The player masks follow the model onto the GPU, while the coalition sampler emits coalitions on the CPU, so the matmul raised `RuntimeError: Expected all tensors to be on the same device ... mat2 is on cuda:0, different from other tensors on cpu` and ImageExplainer could not run on a CUDA model. Move the coalitions onto the masks' device before the matmul (a no-op on CPU).
FIX: device mismatch in CNN vision masking on GPU
* ADD: dummy typing protocols for model detection and enum to check masking and player scope
* ADD: validation logic for model, masking and player compatibility
* fix: ruff errors
* fix: more pre commit fixes
* fix: runtime errors
* fix: masking validation
* REFACTOR: Remove useless protocol ViTLikeModel
* FIX: ruff error by adding docstring
* REFACTOR: CNNArchitecture and TransformerArchitecture to ClassificationArchitecture and ViTClassificationArchitecture
* REFACTOR: CNN Player and Masking Strategy to PixelBased and Transformer Player and Masking to LatentBased rename. And add guards for checking compatibility between architectures and masking and player strategies aswell as between them
* ADD: validation for BoolMaskedPos
* Merge vision main into pr_final
* FIX: device mismatch in CNN vision masking on GPU
CNNMaskingStrategy._build_pixel_mask matmuls the coalition matrix against the
player masks. The player masks follow the model onto the GPU, while the
coalition sampler emits coalitions on the CPU, so the matmul raised
`RuntimeError: Expected all tensors to be on the same device ... mat2 is on
cuda:0, different from other tensors on cpu` and ImageExplainer could not run on
a CUDA model. Move the coalitions onto the masks' device before the matmul
(a no-op on CPU).
---------
Co-authored-by: vladikko <vladikko@users.noreply.github.com>
* Refactor: Rename ModelArchitectureStrategy to ModelArchitecture; adjust imputer model input to match explainer naming
* Remove model compatible from players
* Refactor: clean comments in masking
* ADD: improved docstrings and error messages
* Add: shared error message for model validation
* ADD: check architecture against protocol to ensure model is callable
* Add: error handling and refactoring of architecture, especially model calls
* Delete testing notebook
* fix: typing issues
* fix: ruff and formatting
* FIX: GPU support and error attribution in the vision architectures
The ViT token path was unusable on GPU: value_function never moved
coalitions to the model's device, and MaskTokenStrategy built the zero mask
token on the CPU. Both are fixed, and the mask token is no longer rebuilt on
every coalition batch.
Both _forward methods also caught every exception and blamed the model's
interface -- which is how the device bugs above stayed hidden. Only
TypeError/ValueError are wrapped now (with the underlying error included);
everything else propagates. Preprocessing moved out of the try block so
processor failures are not re-labelled as model failures.
* ADD & FIX: updated tests, now with 100% vision code coverage, and one bugfix in architecture.py
* FIX: type errors and dead branch in ClassificationArchitecture._forward
_preprocess_batch was only reachable with a processor configured, so its
`if self._processor is None` pass-through branch was dead code. Removing it
also removed the narrowing that kept ty quiet, so the processor is now passed
in explicitly.
This also fixes the pre-existing ty error where pixel_values was typed
`None | Tensor` and handed to a `pixel_values: Tensor` parameter. It is now
always a tensor, and preprocessing still happens outside the try block so
processor failures keep their own error message.
* FEATURE: add four pixel-space masking strategies
Ports the maskers from the abandoned feature/updated-vision-package branch
to the current API. That branch predates the numpy -> torch move in the
masking layer, so these are reimplementations rather than copies, and
masking.py stays numpy-free: MarginalSampling takes numpy/PIL references but
converts them through utils.to_tensor_chw, and the inpainter contract is
torch in, torch out.
BlurMasking(sigma) Gaussian-blurred fill. A separable
convolution in torch, so it runs on GPU
and needs no new dependency. Matches
scipy.ndimage.gaussian_filter to 2e-7
(against scipy's 'mirror' mode; torch
and scipy disagree on what 'reflect'
means at the border).
DatasetMeanMasking(mean_color) Fixed dataset-wide baseline. Unlike
MeanColorMasking the fill does not
BlurMasking(sigma) Gaussian-blurred fill. A separable
convolution in torch, so it runs on GPU
and needs no new dependency. Matches
scipy.ndimage.gaussian_filter to 2e-7
(against scipy's 'mirror' mode; torch
and scipy disagree on what 'reflect'
means at the border).
DatasetMeanMasking(mean_color) Fixed dataset-wide baseline. Unlike
MeanColorMasking the fill does not
depend on the image, so values stay
comparable across a dataset.
MarginalSampling(references, seed) Picks a reference image per coalition:
the marginal removal function of
Covert, Lundberg & Lee (JMLR 2021).
InpaintingMasking(inpainter) Delegates to a user callable: the
conditional removal function. Keeps
diffusers/cv2/skimage out of shapiq.
* ADD: vision readme
* ADD: shapiq vision in changelog
* fix: inconsistency in changelog
* FIX: size image_attribution_plot correctly in heatmap-only mode
The figsize (14, 5) is meant for the two-panel layout (heatmap + bar chart); with heatmap_only=True it produced a single square axes floating in a mostly empty wide canvas. Heatmap-only figures now use (6, 5).
* DOCS: rebuild the vision example gallery for the pr_final API
Replaces the three outdated vision examples with seven sphinx-gallery scripts in reading order: CNN quickstart, ViT quickstart, architectures, player strategies, masking strategies, target class, Shapley interactions. All target the current API (ClassificationArchitecture / ViTClassificationArchitecture, explicit architecture construction). Numeric filename prefixes plus within_subsection_order=FileNameSortKey encode the reading order. Adds torch to intersphinx and the Covert, Lundberg and Lee 'Explaining by Removing' (JMLR 2021) reference.
* FIX: stop the docs sidebar from nesting all galleries under Visualization
With sphinx-gallery's default nested_sections=True, the generated gallery root index carries a single hidden toctree at the end of the file, which RST places inside the last section (Visualization). Section-aware themes like furo therefore attached every gallery as a child of that section in the sidebar. nested_sections=False emits one toctree per section header instead, so each gallery nests under its own heading.
* Add: guard to model input in imputer
* Add: quick test notebook
* FIX: run framework-agnostic tests in subprocesses to stop suite pollution
test_framework_agnostic simulated missing torch/skimage by removing them from sys.modules and reloading the vision modules in-process. Reloading a module rebinds its classes to new objects, desynchronising the 'from x import Y' references other modules hold, so every test running after this file (test_imputer, test_integration_real_models) failed with spurious isinstance / default-strategy errors -- 30 failures in the full suite that all passed in isolation.
Each dependency-removal test now runs its snippet in a fresh interpreter via subprocess (the pattern the file already used for the lazy-import test), so the module-graph surgery never touches the parent process. Full vision suite: 336 passed (was 306 passed / 30 failed); the ImportError behaviour is still verified, just isolated.
* Remove test notebook again
* fix: ruff
* FIX: block torch via meta_path instead of sys.modules=None in framework tests
The framework-agnostic subprocess tests simulated a missing torch with sys.modules['torch'] = None. In a fresh subprocess, importing any shapiq.vision.* module pulls in the full stack (shapiq -> approximator -> sklearn -> scipy.stats), and scipy's array-API torch detection runs at import time: getattr(sys.modules['torch'], 'Tensor'). With torch set to None that is getattr(None, 'Tensor') -> AttributeError, crashing the subprocess and failing 5 tests on CI's newer scipy; local runs with older scipy skipped that path and passed.
Simulate 'not installed' by blocking the import with a meta_path finder that raises ModuleNotFoundError, leaving sys.modules untouched. scipy's sys.modules['torch'] lookup then raises KeyError and returns False gracefully, while the vision module's import torch still raises a clean ModuleNotFoundError -> the friendly install hint. Full vision suite: 336 passed sequentially and under --cov -n logical (the CI invocation).
* Refactor: final comment and docstring fixes
* Update README with ViT compatibility details
Clarify compatibility requirements for ViT models and update installation instructions.
* Refactor: readme
* fix: ruff
---------
Co-authored-by: Tamara Muras <Tamara.Muras>
Co-authored-by: S2k-1 <219272227+S2k-1@users.noreply.github.com>
Co-authored-by: vladikko <vladikko@users.noreply.github.com>
Co-authored-by: Alexander Feix <alexander.feix03@gmail.com>
|
@mmschlk: We just submitted our final version for the practical, pipeline ran without errors in our forked repo ! If we need to make further changes so that we can merge this, just ping us :) |
Motivation and Context
The current
shapiqpackage does not support explaining image models, except for code inside benchmark games, where logic is hardcoded per model, which is not flexible for users who whant to explain their own models.This PR introduces a new subpackage
shapiq.visionfor explaining image classification models with Shapley values and interactions. Itsshapiq.vision.ImageExplainerandshapiq.vision.ImageImputerwork with provided model callables like CNNs and vision transformers, and any supported approximator fromshapiq.approximator, covering first-order and higher-order interaction indices. The package fits into the existingshapiqframework of using Explainer, Game and Imputer classes, and adds new functionality for image players and masking strategies.In general, we provide the following new modules and functionalities:
shapiq.vision.architecture: Model-architecture-specific logic for image models, including default player/masking strategies and value functions.ClassifierArchitecture(flexible, runs arbitrary callables with players/masks defined in pixel space) andViTArchitecture(Hugging Face ViTs only, supports masking in latent/token space).shapiq.vision.custom_types: Custom protocols and type aliases for image inputs and model callables.shapiq.vision.explainer: Explains image classification models by passing a model callable, and configure the explanation as usual in the shapiq packageshapiq.vision.imputer: Imputes absent players in a coalition, providing batched evaluation of coalitionsshapiq.vision.masking: Implements different masking strategies for image models depending on a domain (pixel space or latent space)shapiq.vision.players: Implements different player strategies for image models, depending on a domain (pixel space or latent space)shapiq.vision.utils: Utility functions for image models, including e.g. transforming images to and from torch tensors, extracting logits.shapiq.vision.validation: Validation functions for e.g. checking model is callable or has certain attributes.Additionally:
shapiq.Explainertoshapiq.vision.ImageExplainerif ashapiq.vision.ModelArchitectureis passed in as themodelargument.shapiq.vision.plot_image_attributions(), as a heatmap plot and optionally interaction values next to it.The design of
shapiq.visionfollows the pattern of existing shapiq subpackages likeshapiq.tree, separating the concerns of image players, masking strategies, and model-callable logic into dedicated modules distinct from shapiq's core logic, since these concerns are specific to image models. This isolation also makes it easier to define distinct supported model types and behaviors without needing to provide an universal extension to the core package. Keeping the image logic contained in a single subpackage also keeps the core shapiq package lightweight and avoids introducing optional dependencies like torch or scikit-image into the base install.We therefore introduce a new optional dependency group
visionfor the new subpackage, which can be installed withpip install shapiq[vision].Public API Changes
We add a new subpackage
shapiq.visionwith its own public API, documented and provided with examples inexamples/vision. We thus also added dispatching toshapiq.vision.ImageExplainerif ashapiq.vision.ModelArchitectureis passed in as themodelargument toshapiq.Explainer. Apart from that we did not change the public API ofshapiqand only added new functionality for image models.To showcase the new functionality, see the following simple example:
Notes: We explicitly choose to use a
modelargument inImageExplainerthat currently only takes subclasses ofshapiq.vision.ModelArchitectureinstead of a callable model. We did not rename that to architecture, to be consistent with the existingshapiq.ExplainerAPI and to allow for future dispatching of a callable model to ashapiq.vision.ModelArchitectureif the model is recognized as a supported image model. We explored on that in our own repository using forward passes to determine the model type, because there is no distinct way to determine if a model is a vision transformer or CNN from protocols or attributes. We decided to not include that in the final PR, as this might require additional refactoring and enforces a model call at every instantiation of theImageExplainer, which is not necessary for all use cases. We thus leave it to the user to pass in ashapiq.vision.ModelArchitecturesubclass, which is also more explicit, but still allows for more flexibility in the future. We have also added image support for the upset plot in our repository.How Has This Been Tested?
We provide a test suite for our new shapiq.vision subpackage covering unit tests for individual components and integration tests for the end-to-end explanation pipeline.
All tests are included in
tests/test_unit/tests_vision, covering the individual player and masking strategies, batching, explainer and imputer logic, and validation logic for model callables and configuration of players, masking, and model input.Not covered in the test suite:
Benchmarking against real pretrained models is done separately in the efficiency benchmarks (not included in this PR)
Benchmarking the performance of shapiq.vision against commonly used methods for explaining image models (like captum) is not included in this PR
GPU-specific device-handling paths are tested only on CPU in CI; manual GPU testing was done locally.
Checklist
CHANGELOG.md(if relevant for users).