Skip to content

Holistic CRAFT

View source | 📰 CRAFT Paper | 📰 Holistic Paper

Holistic CRAFT (Concept Recursive Activation FacTorization) is a variant of the CRAFT method designed to extract concepts from full activation maps rather than image patches.

This approach preserves the global spatial context and is particularly suitable for object detection models and other tasks where spatial structure across the entire image is important.

The crop-based approach works well for classification because images of classification datasets are typically dominated by a single, well-centred object: random crops are therefore likely to contain parts of the object of interest and carry relevant signal for concept extraction. In Object Detection, the scenes generally contain multiple objects of varying sizes, often occupying only a small fraction of the image. Random crops drawn from such images are mostly background; the target objects are absent or heavily under-represented in the resulting crop dataset, making the NMF factorization blind to the very patterns it should capture.

Supported Object Detection Models

Holistic CRAFT works with various object detection architectures through specialized latent extractors provided by the xplique-adapters package:

PyTorch (torchvision & ultralytics): - RetinaNet - RetinanetExtractorBuilder - Faster R-CNN - FasterRcnnExtractorBuilder - FCOS - FcosExtractorBuilder - SSD - SSDExtractorBuilder - YOLO (v11) - YoloExtractorBuilder - DETR - DetrExtractorBuilder

TensorFlow: - RetinaNet - RetinaNetExtractorBuilder

Each extractor handles the model-specific architecture to split it into the required g(.) and h(.) functions.

Supported Classification Models

For standard classification models, Holistic CRAFT does not require a custom extractor per architecture. Instead, the built-in LayeredModelExtractorBuilder can split any layered model at a chosen intermediate layer:

PyTorch: - Any torch.nn.Module — LayeredModelExtractorBuilder (from xplique.concepts.torch.layered_model_latent_extractor)

TensorFlow: - Any tf.keras.Model — LayeredModelExtractorBuilder (from xplique.concepts.tf.layered_model_latent_extractor)

The builder takes the model and a layer index to define the split point. Everything before that layer becomes g(.), and everything after becomes h(.).

Key Differences from Regular CRAFT

Aspect Regular CRAFT Holistic CRAFT
Input Image patches/crops Full activation maps
Use Case Classification tasks Object detection, Classification
Spatial Context Local (patch-level) Global (full image)
Concepts Visual patterns in patches Spatial activation patterns
Performance Extracts many crops per image Processes full feature maps directly

Workflow

Holistic CRAFT follows the same core principle as CRAFT but operates on full images instead of patches:

  1. Extract Activations: Pass input images through the model's encoder (g) to obtain spatial activation maps from an intermediate layer
  2. Factorize Concepts: Apply Non-negative Matrix Factorization (NMF) to these activation maps to discover recurring spatial patterns (concepts)

Warning

Activations must be non-negative to use the standard NMF. Ensure a ReLU or similar activation function is applied before the extraction layer. Third-party NMF implementations may not have this limitation (e.g., the Semi-NMF from the Overcomplete library).

  1. Estimate Importance: Use any attribution methods available in Xplique (gradient-based, perturbation-based) to rank concept importance
  2. Visualize: Generate concept heatmaps overlaid on images to show "what" and "where"

Like regular CRAFT, Holistic CRAFT requires splitting the model into two parts: \((g, h)\) such that \(f(x) = (g \cdot h)(x)\). The model \(g\) maps input to latent space (activation maps), and \(h\) maps latent space to predictions. Concepts are extracted from these activation maps in latent space.

This split is implemented through three abstractions:

  • LatentData: A container that holds the intermediate activations produced by \(g\). It abstracts away framework-specific tensor formats, providing a unified interface for reading (get_activations) and writing (set_activations) activations, with the necessary shape conversions (e.g., channel-first to channel-last).

  • LatentExtractor: Wraps both \(g\) (input_to_latent_model) and \(h\) (latent_to_logit_model). It orchestrates the full forward pass, batching, device management, and output formatting. The TorchLatentExtractor and TfLatentExtractor subclasses provide framework-specific implementations.

  • LatentExtractorBuilder: A factory that constructs a LatentExtractor for a specific model architecture. It handles all the architecture-specific wiring (defining how to split the model, which layer to extract from, and how to format outputs) so that the rest of the CRAFT pipeline remains model-agnostic.

Example

Basic Usage with Object Detection

import xplique
from xplique.concepts import HolisticCraftTorch as Craft
from xplique_adapters.concepts.torch.latent_data_retinanet import RetinanetExtractorBuilder

# Build a latent extractor that splits the model into g(.) and h(.)
# This provides the input_to_latent (g) and latent_to_logit (h) functions
latent_extractor = RetinanetExtractorBuilder.build(
    model, 
    device="cuda", 
    nb_classes=91,
    extraction_location='resnet', # Choose 'resnet' or 'fpn'
    extraction_layer=-1  # Extract from last ResNet feature layer
)

# Create Holistic CRAFT instance
craft = Craft(
    latent_extractor=latent_extractor,
    number_of_concepts=10,
    device="cuda"
)

# Fit CRAFT on input images to discover concepts
craft.fit(input_images, class_id=class_id)

# Display discovered concepts as heatmaps overlaid on images
craft.display_images_per_concept(images=input_images[:5])

# Display top 3 images for each concept ranked by activation
craft.display_top_images_per_concept(images=input_images, topk=3)

# Estimate concept importance on the 20 first images using Gradient×Input method
# (GradientxInput is the default method)
importances_gi = craft.estimate_importance(
    images=input_images[:20],
    operator=xplique.Tasks.OBJECT_DETECTION,
    class_id=class_id,
    confidence=0.8
)

# Estimate concept importance on the 20 first images using Sobol method
importances_sobol = craft.estimate_importance(
    images=input_images[:20],
    operator=xplique.Tasks.OBJECT_DETECTION,
    class_id=class_id,
    confidence=0.8,
    # Use Sobol method & its arguments
    method="sobol",
    grid_size=4,
    nb_design=8,
    perturbation_function="amplitude",
)

Using Different Attribution Methods to Compute the Concept Importances

Holistic CRAFT supports various attribution methods for concept importance estimation:

import xplique
from xplique.concepts import PartialExplainer
from xplique.attributions import VarGrad

# Use VarGrad for robust importance estimation
vargrad_explainer = PartialExplainer(
    explainer_class=VarGrad,
    operator=xplique.Tasks.OBJECT_DETECTION,
    nb_samples=20,
    noise=0.15
)

# Compute VarGrad explanation for each concept
explanation_vargrad = craft.compute_explanation_per_concept(
    partial_explainer=vargrad_explainer,
    images=input_images,
    class_id=class_id,
    confidence=0.3,
)

# Reduce the spatial dimension of the explanation 
# to compute the final concepts importances
importances_vargrad = craft.reduce_to_importance(
    explanation=explanation_vargrad,
)

Using a Different NMF Factorizer

By default, the standard Sklearn NMF is used to factorize the concepts. But other types of factorizers are supported, such as the ones provided by the Overcomplete project.

from overcomplete.optimization import SemiNMF
from xplique.concepts.torch.factorizer import OvercompleteFactorizer

nb_concepts=10

# Create a SemiNMF factorizer which allows negative activations
factorizer = OvercompleteFactorizer(
    optimizer_class=SemiNMF,
    nb_concepts=nb_concepts,
    device=device
)

# Setup Craft to use this factorizer
craft = Craft(
    latent_extractor=latent_extractor,
    number_of_concepts=nb_concepts,
    device=device,
    factorizer=factorizer,
)

craft.fit(input_images)

Implementing Your Own Latent Extractor

If you're working with a model architecture that isn't supported out-of-the-box, you can implement your own latent extractor by following these steps:

1. Create a Custom LatentData Class

First, create a class that stores the intermediate activations from your model:

from xplique.concepts.latent_extractor import LatentData
import torch

class CustomLatentData(LatentData):
    def __init__(self, fpn_outs: list, extraction_layer: int = 0):
        super().__init__()
        self.fpn_outs = fpn_outs
        self.extraction_layer = extraction_layer

    def get_activations(self, as_numpy: bool = True, keep_gradients: bool = False):
        """Extract activations from the specified layer."""
        activations = self.fpn_outs[self.extraction_layer]

        if not keep_gradients:
            activations = activations.detach()

        # Convert from (N, C, H, W) to (N, H, W, C) for Xplique
        if len(activations.shape) == 4:
            activations = activations.permute(0, 2, 3, 1)

        if as_numpy:
            activations = activations.cpu().numpy()

        return activations

    def set_activations(self, values: torch.Tensor) -> None:
        """Set activations back into the latent data structure."""
        # Convert from (N, H, W, C) to (N, C, H, W)
        if len(values.shape) == 4:
            values = values.permute(0, 3, 1, 2)
        self.fpn_outs[self.extraction_layer] = values

    def to(self, device: torch.device) -> 'CustomLatentData':
        """Move latent data to specified device."""
        self.fpn_outs = [fpn_out.to(device) for fpn_out in self.fpn_outs]
        return CustomLatentData(self.fpn_outs, self.extraction_layer)

2. Create a Custom ExtractorBuilder

Next, implement a builder that splits your model into g(.) and h(.) functions:

import types
from xplique.concepts.latent_extractor import LatentExtractorBuilder
from xplique.concepts.torch.latent_extractor import TorchLatentExtractor

class CustomExtractorBuilder(LatentExtractorBuilder):
    @classmethod
    def build(
        cls,
        model,
        device: str = 'cuda',
        extraction_layer: int = -1,
        batch_size: int = 1
    ) -> TorchLatentExtractor:

        # Define g(.) function: input → latent activations
        def g(self, x):
            # Example: extract from backbone/feature pyramid
            fpn_outs = self.backbone(x)
            return CustomLatentData(
                fpn_outs=list(fpn_outs),
                extraction_layer=latent_extractor.extraction_layer
            )

        # Define h(.) function: latent activations → predictions
        def h(self, latent_data: CustomLatentData):
            fpn_outs = latent_data.fpn_outs
            outputs = self.head(fpn_outs)
            return outputs

        # Bind g and h methods to the model
        model.g = types.MethodType(g, model)
        model.h = types.MethodType(h, model)

        # Create output formatter (converts raw predictions to MultiBoxTensor)
        output_formatter = CustomBoxFormatter()

        # Build the latent extractor
        latent_extractor = TorchLatentExtractor(
            model,
            model.g,
            model.h,
            latent_data_class=CustomLatentData,
            output_formatter=output_formatter,
            batch_size=batch_size,
            device=device
        )

        # Store extraction layer for later use
        latent_extractor.extraction_layer = extraction_layer
        return latent_extractor

3. Use Your Custom Extractor with CRAFT

Once you have your custom extractor, you can use it just like the built-in ones:

from xplique.concepts import HolisticCraftTorch as Craft

# Build your custom latent extractor
latent_extractor = CustomExtractorBuilder.build(
    model,
    device="cuda",
    extraction_layer=-1,
    batch_size=16
)

# Use it with CRAFT
craft = Craft(
    latent_extractor=latent_extractor,
    number_of_concepts=10,
    device="cuda"
)

# Fit and visualize concepts
craft.fit(input_images)
craft.display_images_per_concept(input_images[:5])

Key Points

  • g(.) function: Maps input images to intermediate activations at a chosen layer
  • h(.) function: Maps latent activations back to final predictions
  • LatentData: Handles activation extraction with proper shape conversions (PyTorch uses channel-first, Xplique expects channel-last)
  • Output formatter: Converts model predictions to MultiBoxTensor format for compatibility with Xplique

API Reference

HolisticCraft

Framework-agnostic CRAFT implementation for holistic model explanations.

__init__(self,
         latent_extractor: xplique.concepts.latent_extractor.LatentExtractor,
         number_of_concepts: int = 20,
         device: str = None,
         factorizer: Optional[Any] = None)

Parameters

  • latent_extractor : xplique.concepts.latent_extractor.LatentExtractor

    • Extractor that splits the model into encoder (input to activations) and decoder (activations to predictions) for concept extraction

  • number_of_concepts : int = 20

    • Number of concepts to extract via NMF decomposition

  • device : str = None

    • Device specification for tensor operations (framework-specific)

  • factorizer : Optional[Any] = None

    • Optional factorizer instance implementing the ConceptFactorizer protocol.

      If None, creates a default SklearnNMFFactorizer with alpha_W=1e-2 and max_iter=200

check_if_fitted(self)

Checks if the factorization model has been fitted to input data.


compute_explanation_per_concept(self,
                                images: numpy.ndarray,
                                partial_explainer: xplique.concepts.holistic_craft.PartialExplainer,
                                class_id: Optional[int] = None,
                                confidence: Optional[float] = None,
                                verbose: bool = False) -> numpy.ndarray

Compute explanations per concept using the provided explainer.

Parameters

  • images : numpy.ndarray

    • Input images as numpy arrays

  • partial_explainer : xplique.concepts.holistic_craft.PartialExplainer

    • PartialExplainer instance that creates an attribution explainer when called with model and batch_size arguments

  • class_id : Optional[int] = None

    • Target class ID for filtering detections

  • confidence : Optional[float] = None

    • Confidence threshold for filtering detections

  • verbose : bool = False

    • If True, prints progress information during processing

Return

  • explanations : numpy.ndarray

    • Concatenated explanations for all images, shape (N, H, W, n_concepts)


decode(self,
       latent_data: xplique.concepts.latent_extractor.LatentData,
       coeffs_u: Union[numpy.ndarray, Any]) -> xplique.commons.prediction_types.StructuredPrediction

Decode concept coefficients back to predictions.

Parameters

  • latent_data : xplique.concepts.latent_extractor.LatentData

    • Single image's latent representation (not batched)

  • coeffs_u : Union[numpy.ndarray, Any]

    • Concept coefficients for reconstruction

Return

  • predictions : xplique.commons.prediction_types.StructuredPrediction

    • Predictions implementing StructuredPrediction protocol (has filter() and to_batched_tensor() methods). Concrete types are MultiBoxTensor for object detection or ClassifierTensor for classification.


display_concept_heatmap(self,
                        image: numpy.ndarray,
                        concept_heatmap: numpy.ndarray,
                        concept_idx: int,
                        ax: Any,
                        filter_percentile: int = 80,
                        clip_percentile: int = 5) -> None

Overlay a single concept heatmap on a single image.

Parameters

  • image : numpy.ndarray

    • Single image as HWC numpy array, shape (H, W, C)

  • concept_heatmap : numpy.ndarray

    • Raw concept activation map, shape (H', W')

  • concept_idx : int

    • Index of the concept, used to select the colormap

  • ax : Any

    • Matplotlib axis on which to draw

  • filter_percentile : int = 80

    • Percentile used to filter the concept heatmap (only show concept if excess N-th percentile). Defaults to 80.

  • clip_percentile : int = 5

    • Percentile value to use if clipping is needed when drawing the concept, e.g a value of 1 will perform a clipping between percentile 1 and 99.

      This parameter allows to avoid outliers in case of too extreme values.

      Default to 5.


display_images_per_concept(self,
                           images: numpy.ndarray,
                           coeffs_u: Optional[numpy.ndarray] = None,
                           filter_percentile: int = 80,
                           clip_percentile: int = 5,
                           order: Optional[List[int]] = None) -> matplotlib.figure.Figure

Display concept heatmaps overlaid on images.

Parameters

  • images : numpy.ndarray

    • Input images to visualize (array of shape (N, H, W, C) for tensorflow or (N, C, H, W) for pytorch)

  • coeffs_u : Optional[numpy.ndarray] = None

    • Optional pre-computed coefficients, shape (N, H, W, C) or (N, Tokens, C).

      If None, coefficients will be computed via transform(images).

  • filter_percentile : int = 80

    • Percentile used to filter the concept heatmap (only show concept if excess N-th percentile). Defaults to 80.

  • clip_percentile : int = 5

    • Percentile value to use if clipping is needed when drawing the concept, e.g a value of 1 will perform a clipping between percentile 1 and 99.

      This parameter allows to avoid outliers in case of too extreme values.

      Default to 5.

  • order : Optional[List[int]] = None

    • Optional list of concept IDs to specify display order. If None, concepts are shown in sequential order

Return

  • fig : matplotlib.figure.Figure

    • matplotlib figure with len(images) rows and number_of_concepts columns


display_top_images_per_concept(self,
                               images: Union[numpy.ndarray, List[Any]],
                               topk: int = 3,
                               filter_percentile: int = 80,
                               clip_percentile: int = 5,
                               order: Optional[List[int]] = None,
                               coeffs_u: Optional[numpy.ndarray] = None) -> matplotlib.figure.Figure

Display top N images per concept ranked by average activation.

Parameters

  • images : Union[numpy.ndarray, List[Any]]

    • Input images (as framework tensors or numpy arrays)

  • topk : int = 3

    • Number of top images to display per concept (default: 3)

  • filter_percentile : int = 80

    • Percentile threshold for filtering heatmaps (default: 80)

  • clip_percentile : int = 5

    • Percentile for clipping heatmap values (default: 5)

  • order : Optional[List[int]] = None

    • Optional list of concept IDs to specify display order

  • coeffs_u : Optional[numpy.ndarray] = None

    • Optional pre-computed concept coefficients. If None, will call self.transform(images) to compute them. Use this to pass the coefficients stored in factorization.coeffs_u after fit().

Return

  • fig : matplotlib.figure.Figure

    • matplotlib figure with topk rows and number_of_concepts columns


encode(self,
       inputs: Union[numpy.ndarray, Any],
       resize: Optional[Tuple[int, int]] = None,
       differentiable: bool = False) -> List[xplique.concepts.latent_extractor.EncodedData]

Encode inputs to latent data and concept coefficients.

Parameters

  • inputs : Union[numpy.ndarray, Any]

    • Input images to encode

  • resize : Optional[Tuple[int, int]] = None

    • Target size for resizing images

  • differentiable : bool = False

    • If True, preserves gradients for backpropagation using differentiable non-negative optimization. If False (default), uses standard NMF transform which is faster but does not preserve gradients.

Return

  • encoded_data : List[xplique.concepts.latent_extractor.EncodedData]

    • List of EncodedData named tuples, each containing: - latent_data: LatentData object with intermediate activations - coeffs_u: Concept coefficients (numpy array or tensor with gradients) When differentiable=False, coeffs_u are numpy arrays.

      When differentiable=True, coeffs_u are framework tensors (torch.Tensor or tf.Tensor) with gradients preserved.


fit(self,
    inputs,
    class_id: int = 0)

Fit NMF to extract concepts from latent activations.

Parameters

  • inputs : inputs

    • Input images to extract concepts from, as framework tensors or arrays

  • class_id : int = 0

    • Target class ID for object detection (used in factorization metadata)


get_topk_images_per_concept(self,
                            coeffs_u: numpy.ndarray,
                            topk: int = 3) -> numpy.ndarray

Return the indices of the top images for each concept, ranked by mean activation.

Parameters

  • coeffs_u : numpy.ndarray

    • Concept coefficients, shape (N, H, W, n_concepts)

  • topk : int = 3

    • Number of top images to return per concept (default: 3)

Return

  • top_image_ids : numpy.ndarray

    • Array of shape (n_concepts, topk) containing the indices of the top images for each concept, ranked by descending mean activation


latent_to_concept(self,
                  latent_data: xplique.concepts.latent_extractor.LatentData) -> numpy.ndarray

Transform latent data to concept coefficients.

Parameters

  • latent_data : xplique.concepts.latent_extractor.LatentData

    • Single image's latent representation containing activations

Return

  • coeffs_u : numpy.ndarray

    • Concept coefficients, shape (H, W, n_concepts)


latent_to_concept_differentiable(self,
                                 latent_data: xplique.concepts.latent_extractor.LatentData) -> Any

Transform latent data to concept coefficients with gradient preservation.

Parameters

  • latent_data : xplique.concepts.latent_extractor.LatentData

    • Single image's latent representation containing activations

Return

  • coeffs_u : Any

    • Concept coefficients as framework tensor with gradients


make_concept_decoder(self,
                     latent_data: xplique.concepts.latent_extractor.LatentData) -> Any

Creates a concept decoder for gradient-based attribution.

Parameters

  • latent_data : xplique.concepts.latent_extractor.LatentData

    • Image-specific latent representation

Return

  • decoder : Any

    • ConceptDecoder instance with signature: (coeffs_u) -> predictions


reduce_to_importance(self,
                     explanation: numpy.ndarray,
                     spatial_reducer: Optional[str] = 'max',
                     abs_before_reduce: bool = True,
                     aggregation_reducer: Optional[str] = 'mean') -> numpy.ndarray

Reduce pre-computed concept explanations to global importance scores.

Parameters

  • explanation : numpy.ndarray

    • Per-concept explanations, shape (N, H, W, n_concepts), as returned by :meth:compute_explanation_per_concept.

  • spatial_reducer : Optional[str] = 'max'

    • Reducer applied over the spatial dimensions (H, W) to collapse each image to a per-concept score vector. Either "min", "mean", "max", "sum", "median" or None to skip. Default is "max".

  • abs_before_reduce : bool = True

    • Whether to take the absolute value of explanations before spatial reduction.

      Default is True.

  • aggregation_reducer : Optional[str] = 'mean'

    • Reducer applied over the image dimension after spatial reduction to produce a single score per concept. Either "min", "mean", "max", "sum", "median" or None to skip (returns per-image scores). Default is "mean".

Return

  • importances : numpy.ndarray

    • Importance scores for each concept, shape (n_concepts,).


reduce_to_prevalence(self,
                     explanation: numpy.ndarray) -> numpy.ndarray

Compute concept prevalence from pre-computed explanations.

Parameters

  • explanation : numpy.ndarray

    • Per-concept explanations, shape (N, H, W, n_concepts), as returned by :meth:compute_explanation_per_concept.

Return

  • prevalence : numpy.ndarray

    • Fraction of images for which each concept is dominant, shape (n_concepts,).

      Values sum to 1.


reduce_to_reliability(self,
                      explanation: numpy.ndarray,
                      accuracy: numpy.ndarray) -> numpy.ndarray

Compute concept reliability from pre-computed explanations and per-image accuracy.

Parameters

  • explanation : numpy.ndarray

    • Per-concept explanations, shape (N, H, W, n_concepts), as returned by :meth:compute_explanation_per_concept.

  • accuracy : numpy.ndarray

    • Per-image accuracy scores, shape (N,). For classification: 0.0 or 1.0.

      For object detection: per-image IoU, AP, or any scalar correctness metric computed externally by the user.

Return

  • reliability : numpy.ndarray

    • Mean accuracy per dominant-concept group, shape (n_concepts,).

      Concepts with no dominant image get a reliability of 0.0.


transform(self,
          inputs=None,
          resize=None) -> numpy.ndarray

Transform inputs to concept coefficients.

Parameters

  • inputs : inputs=None

    • Input images to transform. If None, returns stored coefficients from fit().

  • resize : resize=None

    • Target size for resizing images

Return

  • coeffs_u : numpy.ndarray

    • Concept coefficients for the inputs (or stored coefficients if inputs=None)


PartialExplainer

Wrapper for explainer classes to enable deferred instantiation.

__init__(self,
         explainer_class,
         **kwargs)

Parameters

  • explainer_class : explainer_class

    • The explainer class to instantiate (e.g., GradientInput, SobolAttributionMethod).

      Must be callable and accept 'model' and 'batch_size' as keyword arguments.

  • kwargs : **kwargs

    • Configuration arguments for the explainer (e.g., operator, reducer, grid_size).

      Should NOT include 'model' or 'batch_size' as these will be provided during instantiation.

References