Skip to content

Interpretability methods

Class activation map

The class activation map gives you the importance of each region of a feature map on a model's output. More specifically, a class activation map is relative to:

  • the layer at which it is computed (e.g. the N-th layer of your model)
  • the model's classification output (e.g. the raw logits of the model)
  • the class index to focus on

With TorchCAM, the target layer is selected when you create your CAM extractor. You will need to pass the model logits to the extractor and a class index for it to do its magic!

Activation-based methods

Methods related to activation-based class activation maps.

torchcam.methods.CAM

CAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, fc_layer: Module | str | None = None, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Learning Deep Features for Discriminative Localization".

The Class Activation Map (CAM) is defined for image classification models that have global pooling at the end of the visual feature extraction block. The localization map is computed as follows:

\[ L^{(c)}_{CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), and \(w_k^{(c)}\) is the weight corresponding to class \(c\) for unit \(k\) in the fully connected layer.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import CAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with CAM(model, 'layer4', 'fc') as cam_extractor:
    with torch.inference_mode(): out = model(input_tensor)
    cam = cam_extractor(class_idx=100)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

fc_layer

either the fully connected layer itself or its name

TYPE: Module | str | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

RAISES DESCRIPTION
ValueError

if the argument is invalid

TypeError

if the argument type is invalid

Source code in torchcam/methods/activation.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    fc_layer: nn.Module | str | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    if isinstance(target_layer, list) and len(target_layer) > 1:
        raise ValueError("base CAM does not support multiple target layers")

    super().__init__(model, target_layer, input_shape, **kwargs)

    if isinstance(fc_layer, str):
        fc_name = fc_layer
    # Find the location of the module
    elif isinstance(fc_layer, nn.Module):
        fc_name = self._resolve_layer_name(fc_layer)
    # If the layer is not specified, try automatic resolution
    elif fc_layer is None:
        lin_layers = [layer_name for layer_name, m in model.named_modules() if isinstance(m, nn.Linear)]
        # Warn the user of the choice
        if len(lin_layers) == 0:
            raise ValueError("unable to resolve `fc_layer` automatically, please specify its value.")
        if len(lin_layers) > 1:
            raise ValueError("This CAM method does not support multiple fully connected layers.")
        fc_name = lin_layers[0]
        logger.warning(f"no value was provided for `fc_layer`, thus set to '{fc_name}'.")
    else:
        raise TypeError("invalid argument type for `fc_layer`")
    # Softmax weight
    self._fc_weights = self.submodule_dict[fc_name].weight.data
    # squeeze to accomodate replacement by Conv1x1
    if self._fc_weights.ndim > 2:
        self._fc_weights = self._fc_weights.view(*self._fc_weights.shape[:2])

torchcam.methods.ScoreCAM

ScoreCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, batch_size: int = 32, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Score-CAM: Score-Weighted Visual Explanations for Convolutional Neural Networks".

The localization map is computed as follows:

\[ L^{(c)}_{Score-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = softmax\Big(Y^{(c)}(M_k) - Y^{(c)}(X_b)\Big)_k \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), \(Y^{(c)}(X)\) is the model output score for class \(c\) before softmax for input \(X\), \(X_b\) is a baseline image, and \(M_k\) is defined as follows:

\[ M_k = \frac{U(A_k) - \min\limits_m U(A_m)}{\max\limits_m U(A_m) - \min\limits_m U(A_m)}) \odot X_b \]

where \(\odot\) refers to the element-wise multiplication and \(U\) is the upsampling operation.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import ScoreCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with ScoreCAM(model, 'layer4') as cam_extractor:
    with torch.inference_mode(): out = model(input_tensor)
    cam = cam_extractor(class_idx=100)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

batch_size

batch size used to upsample masks and forward masked inputs

TYPE: int DEFAULT: 32

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/activation.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    batch_size: int = 32,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)

    # Input hook
    self.hook_handles.append(model.register_forward_pre_hook(self._store_input))
    self.bs = batch_size
    # Ensure ReLU is applied to CAM before normalization
    self._relu = True
    self._targets_supported = True

torchcam.methods.SSCAM

SSCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, batch_size: int = 32, num_samples: int = 35, std: float = 2.0, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "SS-CAM: Smoothed Score-CAM for Sharper Visual Feature Localization".

The localization map is computed as follows:

\[ L^{(c)}_{SS-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = softmax\Big(\frac{1}{N} \sum\limits_{i=1}^N (Y^{(c)}(\hat{M_k}) - Y^{(c)}(X_b))\Big)_k \]

where \(N\) is the number of samples used to smooth the weights, \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), \(Y^{(c)}(X)\) is the model output score for class \(c\) before softmax for input \(X\), \(X_b\) is a baseline image, and \(M_k\) is defined as follows:

\[ \hat{M_k} = \Bigg(\frac{U(A_k) - \min\limits_m U(A_m)}{\max\limits_m U(A_m) - \min\limits_m U(A_m)} + \delta\Bigg) \odot X_b \]

where \(\odot\) refers to the element-wise multiplication, \(U\) is the upsampling operation, \(\delta \sim \mathcal{N}(0, \sigma^2)\) is the random noise that follows a 0-mean gaussian distribution with a standard deviation of \(\sigma\).

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import SSCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with SSCAM(model, 'layer4') as cam_extractor:
    with torch.inference_mode(): out = model(input_tensor)
    cam = cam_extractor(class_idx=100)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

batch_size

batch size used to forward masked inputs

TYPE: int DEFAULT: 32

num_samples

number of noisy samples used for weight computation

TYPE: int DEFAULT: 35

std

standard deviation of the noise added to the normalized activation

TYPE: float DEFAULT: 2.0

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/activation.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    batch_size: int = 32,
    num_samples: int = 35,
    std: float = 2.0,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, batch_size, input_shape, **kwargs)

    self.num_samples = num_samples
    self.std = std
    self._distrib = torch.distributions.normal.Normal(0, self.std)

torchcam.methods.ISCAM

ISCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, batch_size: int = 32, num_samples: int = 10, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "IS-CAM: Integrated Score-CAM for axiomatic-based explanations".

The localization map is computed as follows:

\[ L^{(c)}_{ISS-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = softmax\Bigg(\frac{1}{N} \sum\limits_{i=1}^N \Big(Y^{(c)}(M_i) - Y^{(c)}(X_b)\Big)\Bigg)_k \]

where \(N\) is the number of samples used to smooth the weights, \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), \(Y^{(c)}(X)\) is the model output score for class \(c\) before softmax for input \(X\), \(X_b\) is a baseline image, and \(M_i\) is defined as follows:

\[ M_i = \sum\limits_{j=0}^{i-1} \frac{j}{N} \frac{U(A_k) - \min\limits_m U(A_m)}{\max\limits_m U(A_m) - \min\limits_m U(A_m)} \odot X_b \]

where \(\odot\) refers to the element-wise multiplication, \(U\) is the upsampling operation.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import ISCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with ISCAM(model, 'layer4') as cam_extractor:
    with torch.inference_mode(): out = model(input_tensor)
    cam = cam_extractor(class_idx=100)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

batch_size

batch size used to forward masked inputs

TYPE: int DEFAULT: 32

num_samples

number of noisy samples used for weight computation

TYPE: int DEFAULT: 10

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/activation.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    batch_size: int = 32,
    num_samples: int = 10,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, batch_size, input_shape, **kwargs)

    self.num_samples = num_samples

Gradient-based methods

Methods related to gradient-based class activation maps.

torchcam.methods.FinerCAM

FinerCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, input_shape: tuple[int, ...] = (3, 224, 224), *, base_method: type[GradCAM] | type[GradCAMpp] | type[LayerCAM] = GradCAM, gamma: float = 0.6, num_references: int = 3, **base_kwargs: Any)

Implements "Finer-CAM: Spotting the Difference Reveals Finer Details for Visual Explanation".

Finer-CAM changes the base extractor objective from the target score \(y_c\) to the contrastive objective

\[ y_c - \gamma \frac{1}{T} \sum\limits_{t=1}^{T} y_{d_t}, \]

so comparisons are aggregated before the base method's final CAM ReLU. Automatic references are the classes with scores closest to the target score. The target is always excluded, and the requested count is capped by the available classes. Only GradCAM, GradCAMpp, and LayerCAM are supported initially.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import FinerCAM, LayerCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with FinerCAM(model, "layer4", base_method=LayerCAM) as cam_extractor:
    scores = model(input_tensor)
    cams = cam_extractor(class_idx=100, scores=scores, comparison_idx=[101, 102, 103])
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

base_method

gradient CAM extractor used to produce the maps

TYPE: type[GradCAM] | type[GradCAMpp] | type[LayerCAM] DEFAULT: GradCAM

gamma

comparison strength applied to the mean reference score

TYPE: float DEFAULT: 0.6

num_references

number of automatic references to select, capped by the available non-target classes

TYPE: int DEFAULT: 3

base_kwargs

keyword arguments forwarded to base_method

TYPE: Any DEFAULT: {}

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    *,
    base_method: type[GradCAM] | type[GradCAMpp] | type[LayerCAM] = GradCAM,
    gamma: float = 0.6,
    num_references: int = 3,
    **base_kwargs: Any,
) -> None:
    if base_method not in {GradCAM, GradCAMpp, LayerCAM}:
        raise TypeError("base_method must be GradCAM, GradCAMpp, or LayerCAM")
    if isinstance(gamma, bool) or not isinstance(gamma, int | float):
        raise TypeError("gamma must be a real number")
    if not isfinite(gamma) or gamma < 0:
        raise ValueError("gamma must be finite and non-negative")
    if isinstance(num_references, bool) or not isinstance(num_references, int):
        raise TypeError("num_references must be an integer")
    if num_references < 1:
        raise ValueError("num_references must be positive")

    self.gamma = float(gamma)
    self.num_references = num_references
    super().__init__(base_method(model, target_layer, input_shape=input_shape, **base_kwargs))

torchcam.methods.FinerCAM.compute_cams

compute_cams(class_idx: int | list[int], scores: Tensor | None = None, comparison_idx: int | list[int] | list[list[int]] | None = None, normalized: bool = True, **kwargs: Any) -> list[Tensor]

Compute Finer-CAMs without the base extractor precheck.

RAISES DESCRIPTION
ValueError

if arbitrary output targets are provided

Source code in torchcam/methods/gradient.py
def compute_cams(
    self,
    class_idx: int | list[int],
    scores: Tensor | None = None,
    comparison_idx: int | list[int] | list[list[int]] | None = None,
    normalized: bool = True,
    **kwargs: Any,
) -> list[Tensor]:
    """Compute Finer-CAMs without the base extractor precheck.

    Raises:
        ValueError: if arbitrary output targets are provided
    """  # noqa: DOC201
    if kwargs.get("targets") is not None:
        raise ValueError("FinerCAM does not support arbitrary output targets")
    contrastive_scores = self._contrastive_scores(class_idx, scores, comparison_idx)
    return self.base_cam.compute_cams([0] * contrastive_scores.shape[0], contrastive_scores, normalized, **kwargs)

torchcam.methods.FinerCAM.fuse_cams

fuse_cams(cams: list[Tensor], target_shape: tuple[int, int] | None = None) -> Tensor

Fuse maps using the selected base extractor.

Source code in torchcam/methods/gradient.py
def fuse_cams(self, cams: list[Tensor], target_shape: tuple[int, int] | None = None) -> Tensor:
    """Fuse maps using the selected base extractor."""  # noqa: DOC201
    return self.base_cam.fuse_cams(cams, target_shape)

torchcam.methods.GradCAM

GradCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization".

The localization map is computed as follows:

\[ L^{(c)}_{Grad-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = \frac{1}{H \cdot W} \sum\limits_{i=1}^H \sum\limits_{j=1}^W \frac{\partial Y^{(c)}}{\partial A_k(i, j)} \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), and \(Y^{(c)}\) is the model output score for class \(c\) before softmax.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import GradCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with GradCAM(model, 'layer4') as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(class_idx=100, scores=scores)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)
    # Ensure ReLU is applied before normalization
    self._relu = True
    # Model output is used by the extractor
    self._score_used = True
    self._targets_supported = True
    for idx, name in enumerate(self.target_names):
        # Trick to avoid issues with inplace operations cf. https://github.com/pytorch/pytorch/issues/61519
        self.hook_handles.append(self.submodule_dict[name].register_forward_hook(partial(self._hook_g, idx=idx)))

torchcam.methods.GradCAMpp

GradCAMpp(model: Module, target_layer: Module | str | list[Module | str] | None = None, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Grad-CAM++: Improved Visual Explanations for Deep Convolutional Networks".

The localization map is computed as follows:

\[ L^{(c)}_{Grad-CAM++}(x, y) = \sum\limits_k w_k^{(c)} A_k(x, y) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = \sum\limits_{i=1}^H \sum\limits_{j=1}^W \alpha_k^{(c)}(i, j) \cdot ReLU\Big(\frac{\partial Y^{(c)}}{\partial A_k(i, j)}\Big) \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), \(Y^{(c)}\) is the model output score for class \(c\) before softmax, and \(g_k^{(c)}(i,j) = \frac{\partial Y^{(c)}}{\partial A_k(i,j)}\). This implementation uses the square and cube of the first derivative for the higher-order terms:

\[ \alpha_k^{(c)}(i, j) = \frac{g_k^{(c)}(i,j)^2}{ 2 g_k^{(c)}(i,j)^2 + g_k^{(c)}(i,j)^3 \sum\limits_{a,b} A_k(a,b) + \varepsilon} \]

The coefficient is zero when the squared gradient is zero. A small \(\varepsilon\) stabilizes the denominator. For float16 and bfloat16 inputs, gradient powers and activation sums use float32 to avoid underflow and overflow. Float64 inputs retain their precision.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import GradCAMpp
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with GradCAMpp(model, 'layer4') as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(class_idx=100, scores=scores)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)
    # Ensure ReLU is applied before normalization
    self._relu = True
    # Model output is used by the extractor
    self._score_used = True
    self._targets_supported = True
    for idx, name in enumerate(self.target_names):
        # Trick to avoid issues with inplace operations cf. https://github.com/pytorch/pytorch/issues/61519
        self.hook_handles.append(self.submodule_dict[name].register_forward_hook(partial(self._hook_g, idx=idx)))

torchcam.methods.SmoothGradCAMpp

SmoothGradCAMpp(model: Module, target_layer: Module | str | list[Module | str] | None = None, num_samples: int = 4, std: float = 0.3, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Smooth Grad-CAM++: An Enhanced Inference Level Visualization Technique for Deep Convolutional Neural Network Models" with a personal correction to the paper (alpha coefficient numerator).

The localization map is computed as follows:

\[ L^{(c)}_{Smooth Grad-CAM++}(x, y) = \sum\limits_k w_k^{(c)} A_k(x, y) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = \sum\limits_{i=1}^H \sum\limits_{j=1}^W \alpha_k^{(c)}(i, j) \cdot ReLU\Big(\overline{g^{(1)}}_k^{(c)}(i,j)\Big) \]

where \(A_k(x,y)\) is the original input's activation in the target layer, and \(g_{k,m}^{(c)}(i,j)\) is the first derivative of the pre-softmax class score with respect to the activation for noisy sample \(m\). For \(p \in \{1,2,3\}\), the same \(n\) noisy samples estimate each gradient moment:

\[ \overline{g^{(p)}}_k^{(c)}(i,j) = \frac{1}{n} \sum\limits_{m=1}^n g_{k,m}^{(c)}(i,j)^p \]

As in :class:GradCAMpp, powers of the first derivative approximate the higher-order terms:

\[ \alpha_k^{(c)}(i,j) = \frac{\overline{g^{(2)}}_k^{(c)}(i,j)}{ 2\overline{g^{(2)}}_k^{(c)}(i,j) + \overline{g^{(3)}}_k^{(c)}(i,j) \sum\limits_{a,b} A_k(a,b) + \varepsilon} \]

The first-order gradients are averaged before applying ReLU. The resulting weights are combined with the original input's activations, so permuting the same noisy samples does not change the estimator apart from floating-point rounding. Zero squared-gradient moments give zero coefficients.

The corrected alpha numerator uses the mean squared gradient, rather than the mean first derivative in the paper. For float16 and bfloat16 inputs, gradient moments and activation sums use float32 to avoid underflow and overflow. Float64 inputs retain their precision.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import SmoothGradCAMpp
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with SmoothGradCAMpp(model, 'layer4') as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(class_idx=100)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

num_samples

number of samples to use for smoothing

TYPE: int DEFAULT: 4

std

standard deviation of the noise

TYPE: float DEFAULT: 0.3

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    num_samples: int = 4,
    std: float = 0.3,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)
    # Model scores is not used by the extractor
    self._score_used = False

    # Input hook
    self.hook_handles.append(model.register_forward_pre_hook(self._store_input))
    # Noise distribution
    self.num_samples = num_samples
    self.std = std
    self._distrib = torch.distributions.normal.Normal(0, self.std)
    # Specific input hook updater
    self._ihook_enabled = True

torchcam.methods.XGradCAM

XGradCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "Axiom-based Grad-CAM: Towards Accurate Visualization and Explanation of CNNs".

The localization map is computed as follows:

\[ L^{(c)}_{XGrad-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)} A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}\) being defined as:

\[ w_k^{(c)} = \sum\limits_{i=1}^H \sum\limits_{j=1}^W \Big( \frac{\partial Y^{(c)}}{\partial A_k(i, j)} \cdot \frac{A_k(i, j)}{\sum\limits_{m=1}^H \sum\limits_{n=1}^W A_k(m, n)} \Big) \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), and \(Y^{(c)}\) is the model output score for class \(c\) before softmax.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import XGradCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with XGradCAM(model, 'layer4') as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(class_idx=100, scores=scores)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)
    # Ensure ReLU is applied before normalization
    self._relu = True
    # Model output is used by the extractor
    self._score_used = True
    self._targets_supported = True
    for idx, name in enumerate(self.target_names):
        # Trick to avoid issues with inplace operations cf. https://github.com/pytorch/pytorch/issues/61519
        self.hook_handles.append(self.submodule_dict[name].register_forward_hook(partial(self._hook_g, idx=idx)))

torchcam.methods.LayerCAM

LayerCAM(model: Module, target_layer: Module | str | list[Module | str] | None = None, input_shape: tuple[int, ...] = (3, 224, 224), **kwargs: Any)

Implements a class activation map extractor as described in "LayerCAM: Exploring Hierarchical Class Activation Maps for Localization".

The localization map is computed as follows:

\[ L^{(c)}_{Layer-CAM}(x, y) = ReLU\Big(\sum\limits_k w_k^{(c)}(x, y) \cdot A_k(x, y)\Big) \]

with the coefficient \(w_k^{(c)}(x, y)\) being defined as:

\[ w_k^{(c)}(x, y) = ReLU\Big(\frac{\partial Y^{(c)}}{\partial A_k(i, j)}(x, y)\Big) \]

where \(A_k(x, y)\) is the activation of node \(k\) in the target layer of the model at position \((x, y)\), and \(Y^{(c)}\) is the model output score for class \(c\) before softmax.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import LayerCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with LayerCAM(model, 'layer4') as cam_extractor:
    scores = model(input_tensor)
    cams = cam_extractor(class_idx=100, scores=scores)
    fused_cam = cam_extractor.fuse_cams(cams)
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

either the target layer itself or its name, or a list of those

TYPE: Module | str | list[Module | str] | None DEFAULT: None

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    input_shape: tuple[int, ...] = (3, 224, 224),
    **kwargs: Any,
) -> None:
    super().__init__(model, target_layer, input_shape, **kwargs)
    # Ensure ReLU is applied before normalization
    self._relu = True
    # Model output is used by the extractor
    self._score_used = True
    self._targets_supported = True
    for idx, name in enumerate(self.target_names):
        # Trick to avoid issues with inplace operations cf. https://github.com/pytorch/pytorch/issues/61519
        self.hook_handles.append(self.submodule_dict[name].register_forward_hook(partial(self._hook_g, idx=idx)))

torchcam.methods.LeGrad

LeGrad(model: Module, target_layer: Module | str | list[Module | str], *, score_projection: Callable[[Tensor], Tensor] | None = None, prefix_tokens: int = 1, grid_shape: tuple[int, int] | None = None, enable_hooks: bool = True)

Implements LeGrad as described in "LeGrad: An Explainability Method for Vision Transformers via Feature Formation Sensitivity".

For every selected transformer block :math:l, LeGrad differentiates that block's class score :math:s^l with respect to its post-softmax attention probabilities :math:A^l. Positive gradients are averaged over heads and query tokens, prefix keys are removed, and the resulting patch maps are averaged over layers before normalization. Attention values are not multiplied into the gradients.

Target layers must return tokens shaped (batch, tokens, embedding) and expose a direct self_attention child implemented by batch-first :class:torch.nn.MultiheadAttention. The built-in score projection supports torchvision VisionTransformer models; other matching blocks require score_projection to map intermediate tokens to class logits.

Example
from torchvision.models import ViT_B_16_Weights, vit_b_16
from torchcam.methods import LeGrad

model = vit_b_16(weights=ViT_B_16_Weights.DEFAULT).eval()
with LeGrad(model, list(model.encoder.layers)[-4:]) as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(scores[0].argmax().item())[0]
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

transformer block or blocks, specified as modules or their names

TYPE: Module | str | list[Module | str]

score_projection

optional function mapping intermediate tokens to class logits shaped (N, C)

TYPE: Callable[[Tensor], Tensor] | None DEFAULT: None

prefix_tokens

number of non-spatial key tokens to remove before reshaping

TYPE: int DEFAULT: 1

grid_shape

patch-grid (height, width); inferred when the patch count is a perfect square

TYPE: tuple[int, int] | None DEFAULT: None

enable_hooks

whether hooks should be enabled by default

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
TypeError

if an argument has an invalid type

ValueError

if the model, target blocks, attention modules, or grid are unsupported

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: nn.Module | str | list[nn.Module | str],
    *,
    score_projection: Callable[[Tensor], Tensor] | None = None,
    prefix_tokens: int = 1,
    grid_shape: tuple[int, int] | None = None,
    enable_hooks: bool = True,
) -> None:
    if target_layer is None or (isinstance(target_layer, list) and not target_layer):
        raise ValueError("LeGrad requires at least one explicit target block")
    self._validate_init_args(prefix_tokens, grid_shape, score_projection)

    if score_projection is None:
        encoder = getattr(model, "encoder", None)
        norm = getattr(encoder, "ln", None)
        heads = getattr(model, "heads", None)
        if not isinstance(norm, nn.Module) or not isinstance(heads, nn.Module):
            raise ValueError("`score_projection` is required for models other than torchvision VisionTransformer")

        def project_tokens(tokens: Tensor) -> Tensor:
            return heads(norm(tokens.mean(dim=1)))

        score_projection = project_tokens

    self._score_projection = score_projection
    self.prefix_tokens = prefix_tokens
    self.grid_shape = grid_shape
    super().__init__(model, target_layer, enable_hooks=enable_hooks)

    try:
        self._register_attention_hooks()
    except Exception:
        self.remove_hooks()
        raise

torchcam.methods.LeGrad.reset_hooks

reset_hooks() -> None

Clear stored layer scores and attention probabilities.

Source code in torchcam/methods/gradient.py
def reset_hooks(self) -> None:
    """Clear stored layer scores and attention probabilities."""
    super().reset_hooks()
    self.hook_attn: list[Tensor | None] = [None] * len(self.target_names)
    self._token_counts: list[int | None] = [None] * len(self.target_names)
    self._weight_options: list[tuple[bool, bool] | None] = [None] * len(self.target_names)

torchcam.methods.LeGrad.compute_cams

compute_cams(class_idx: int | list[int] | None = None, scores: Any = None, normalized: bool = True, retain_graph: bool = False, *, targets: OutputTarget | list[OutputTarget] | None = None, **kwargs: Any) -> list[Tensor]

Compute and average layerwise positive attention-gradient maps.

RAISES DESCRIPTION
ValueError

if attention and token shapes are incompatible

Source code in torchcam/methods/gradient.py
def compute_cams(
    self,
    class_idx: int | list[int] | None = None,
    scores: Any = None,
    normalized: bool = True,
    retain_graph: bool = False,
    *,
    targets: OutputTarget | list[OutputTarget] | None = None,
    **kwargs: Any,
) -> list[Tensor]:
    """Compute and average layerwise positive attention-gradient maps.

    Raises:
        ValueError: if attention and token shapes are incompatible
    """  # noqa: DOC201
    if targets is not None:
        raise ValueError("LeGrad does not support arbitrary output targets")
    relevances = self._get_weights(cast(int | list[int], class_idx), scores, retain_graph=retain_graph, **kwargs)
    maps: list[Tensor] = []
    for idx, relevance in enumerate(relevances):
        token_count = self._token_counts[idx]
        if token_count is None or token_count != relevance.shape[-1] + self.prefix_tokens:
            raise ValueError("attention keys and target-block tokens must have matching lengths")
        height, width = self._resolve_grid_shape(relevance.shape[-1])
        maps.append(relevance.reshape(relevance.shape[0], height, width))

    with torch.no_grad():
        cam = torch.stack(maps).mean(dim=0)
        return [self._normalize(cam) if normalized else cam]

torchcam.methods.RefineCAM

RefineCAM(model: Module, target_layer: list[Module | str], input_shape: tuple[int, ...] = (3, 224, 224), *, base_method: type[_CAM] = GradCAMpp, **base_kwargs: Any)

Implements the multi-layer refinement described in "How to Evaluate and Refine your CAM".

RefineCAM normalizes class activation maps from multiple layers, resizes them to a common spatial shape, and multiplies them element-wise. Grad-CAM++ is used by default, but any CAM extractor supporting multiple target layers can be passed as base_method.

Example
from torchvision.models import get_model, get_model_weights
from torchcam.methods import LayerCAM, RefineCAM
model = get_model("resnet18", weights=get_model_weights("resnet18").DEFAULT).eval()
with RefineCAM(model, ["layer2", "layer3", "layer4"], base_method=LayerCAM) as cam_extractor:
    scores = model(input_tensor)
    cam = cam_extractor(class_idx=100, scores=scores)[0]
PARAMETER DESCRIPTION
model

input model

TYPE: Module

target_layer

target layers, specified as modules or their names

TYPE: list[Module | str]

input_shape

shape of the expected input tensor excluding the batch dimension

TYPE: tuple[int, ...] DEFAULT: (3, 224, 224)

base_method

CAM extractor used to produce the per-layer maps

TYPE: type[_CAM] DEFAULT: GradCAMpp

base_kwargs

keyword arguments forwarded to base_method

TYPE: Any DEFAULT: {}

Source code in torchcam/methods/gradient.py
def __init__(
    self,
    model: nn.Module,
    target_layer: list[nn.Module | str],
    input_shape: tuple[int, ...] = (3, 224, 224),
    *,
    base_method: type[_CAM] = GradCAMpp,
    **base_kwargs: Any,
) -> None:
    if not isinstance(target_layer, list) or len(target_layer) < 2:
        raise ValueError("RefineCAM requires at least two target layers")
    if not isinstance(base_method, type) or not issubclass(base_method, _CAM):
        raise TypeError("base_method must be a CAM extractor class")

    super().__init__(base_method(model, target_layer, input_shape=input_shape, **base_kwargs))

torchcam.methods.RefineCAM.compute_cams

compute_cams(class_idx: int | list[int] | None = None, scores: Any = None, normalized: bool = True, target_shape: tuple[int, ...] | None = None, *, targets: OutputTarget | list[OutputTarget] | None = None, **kwargs: Any) -> list[Tensor]

Compute and refine CAMs without the base extractor precheck.

Source code in torchcam/methods/gradient.py
def compute_cams(
    self,
    class_idx: int | list[int] | None = None,
    scores: Any = None,
    normalized: bool = True,
    target_shape: tuple[int, ...] | None = None,
    *,
    targets: OutputTarget | list[OutputTarget] | None = None,
    **kwargs: Any,
) -> list[Tensor]:
    """Compute and refine CAMs without the base extractor precheck."""  # noqa: DOC201
    cams = self.base_cam.compute_cams(class_idx, scores, normalized=True, targets=targets, **kwargs)
    return [self.fuse_cams(cams, target_shape, normalized)]

torchcam.methods.RefineCAM.fuse_cams staticmethod

fuse_cams(cams: list[Tensor], target_shape: tuple[int, ...] | None = None, normalized: bool = True) -> Tensor

Normalize, resize, and multiply maps from multiple layers.

RAISES DESCRIPTION
TypeError

if cams is not a list of tensors

ValueError

if cams is empty

Source code in torchcam/methods/gradient.py
@staticmethod
@torch.no_grad()
def fuse_cams(
    cams: list[Tensor],
    target_shape: tuple[int, ...] | None = None,
    normalized: bool = True,
) -> Tensor:
    """Normalize, resize, and multiply maps from multiple layers.

    Raises:
        TypeError: if ``cams`` is not a list of tensors
        ValueError: if ``cams`` is empty
    """  # noqa: DOC201
    if not isinstance(cams, list) or any(not isinstance(cam, Tensor) for cam in cams):
        raise TypeError("invalid argument type for `cams`")
    if not cams:
        raise ValueError("argument `cams` cannot be an empty list")

    shape = target_shape or tuple(map(max, zip(*[tuple(cam.shape[1:]) for cam in cams], strict=True)))
    interpolation_mode = "bilinear" if cams[0].ndim == 3 else "trilinear" if cams[0].ndim == 4 else "nearest"
    resize_kwargs = {} if interpolation_mode == "nearest" else {"align_corners": False}
    resized_cams = [
        F.interpolate(
            _CAM._normalize(cam.clone()).unsqueeze(1),  # noqa: SLF001
            shape,
            mode=interpolation_mode,
            **resize_kwargs,
        )
        for cam in cams
    ]
    refined_cam = torch.stack(resized_cams).prod(dim=0).squeeze(1)
    return _CAM._normalize(refined_cam) if normalized else refined_cam  # noqa: SLF001

Entropy-gradient maps for VLMs

EntropyGradient implements the paper's map primitive: next-token Shannon entropy, its gradient with respect to projected visual input embeddings, then the channel L2 norm. Bright patches indicate sensitivity of uncertainty, not support for a generated word. Region selection and iterative cropping are excluded; the full pipeline's answer-accuracy gains are not claimed.

from torchcam.methods import EntropyGradient

# embeddings: (batch, tokens, channels), requiring gradients BEFORE this forward
output = language_model(inputs_embeds=embeddings, use_cache=False)
maps = EntropyGradient((height, width))(
    output.logits[:, -1], embeddings, visual_tokens=image_positions,
)  # (batch, height, width), detached and normalized per image

Supply the exact embeddings entering the language model, not final hidden states or post-forward copies/slices. visual_tokens is an optional boolean mask or ordered integer indices; omit it for an all-visual input. Samples must be independent and share a row-major grid and selection. Both forward and attribution need autograd. Checkpointed forwards require use_reentrant=False. retain_graph=True permits graph reuse; normalized=False retains raw norms. Parameter gradients are preserved. Entropy and norms accumulate in float32/float64; they cannot recover gradients lost inside a low-precision model. The caller owns model extraction and spatial layout; no automatic VLM adapter or Transformers dependency is added.

EntropyGradient

EntropyGradient(grid_shape: tuple[int, int])

Extract the map primitive from "Entropy-Gradient Grounding", equations (1)-(4).

Take the channel L2 norm of next-token Shannon entropy gradients with respect to projected visual embeddings entering the language model. This measures uncertainty sensitivity, not word attribution. Region selection and crop-and-refine are excluded. The caller supplies tensors from an existing differentiable forward and their spatial order; this extractor installs no hooks.

PARAMETER DESCRIPTION
grid_shape

explicit visual grid (height, width), including rectangular grids

TYPE: tuple[int, int]

RAISES DESCRIPTION
ValueError

if the grid is not a pair of positive integers

Source code in torchcam/methods/entropy.py
def __init__(self, grid_shape: tuple[int, int]) -> None:
    if (
        not isinstance(grid_shape, tuple)
        or len(grid_shape) != 2
        or any(not isinstance(dim, int) or isinstance(dim, bool) or dim <= 0 for dim in grid_shape)
    ):
        raise ValueError("`grid_shape` must be a tuple of two positive integers")
    self.grid_shape = grid_shape

torchcam.methods.EntropyGradient.__call__

__call__(logits: Tensor, embeddings: Tensor, visual_tokens: Tensor | None = None, *, normalized: bool = True, retain_graph: bool = False) -> Tensor

Map uncertainty sensitivity at one caller-selected decoding step.

PARAMETER DESCRIPTION
logits

full vocabulary logits (N, vocabulary) at the chosen query, e.g. output.logits[:, -1] for an unpadded prefix; no temperature or sampling filters are applied

TYPE: Tensor

embeddings

exact projected or mixed visual/text input embeddings (N, tokens, channels) used by the forward; final hidden states and post-forward slices/copies are not valid substitutes

TYPE: Tensor

visual_tokens

optional boolean mask (tokens,) or unique int32/int64 indices in row-major order, shared across the batch; omitted means all tokens are visual

TYPE: Tensor | None DEFAULT: None

normalized

min-max normalize each image to [0, 1]; constant maps become zero

TYPE: bool DEFAULT: True

retain_graph

keep the graph for another attribution/backward; default releases it

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Tensor

detached (N, height, width) maps on the embeddings' device; accumulation uses float32 or

Tensor

float64 if either input is double precision. Entropy uses natural logs.

RAISES DESCRIPTION
ValueError

if shapes, devices, dtypes, finite values or token selection are incompatible

RuntimeError

if autograd is disabled, inputs are disconnected/non-differentiable, or the graph is freed

Note

Sample entropies are summed, assuming independent samples. Parameter gradients are preserved. Accumulation cannot recover gradients already lost inside a low-precision model. Checkpointed forwards must use use_reentrant=False (required by autograd.grad).

Source code in torchcam/methods/entropy.py
def __call__(
    self,
    logits: Tensor,
    embeddings: Tensor,
    visual_tokens: Tensor | None = None,
    *,
    normalized: bool = True,
    retain_graph: bool = False,
) -> Tensor:
    """Map uncertainty sensitivity at one caller-selected decoding step.

    Args:
        logits: full vocabulary logits ``(N, vocabulary)`` at the chosen query, e.g. ``output.logits[:, -1]``
            for an unpadded prefix; no temperature or sampling filters are applied
        embeddings: exact projected or mixed visual/text input embeddings ``(N, tokens, channels)`` used
            by the forward; final hidden states and post-forward slices/copies are not valid substitutes
        visual_tokens: optional boolean mask ``(tokens,)`` or unique int32/int64 indices in row-major
            order, shared across the batch; omitted means all tokens are visual
        normalized: min-max normalize each image to [0, 1]; constant maps become zero
        retain_graph: keep the graph for another attribution/backward; default releases it

    Returns:
        detached ``(N, height, width)`` maps on the embeddings' device; accumulation uses float32 or
        float64 if either input is double precision. Entropy uses natural logs.

    Raises:
        ValueError: if shapes, devices, dtypes, finite values or token selection are incompatible
        RuntimeError: if autograd is disabled, inputs are disconnected/non-differentiable, or the graph is freed

    Note:
        Sample entropies are summed, assuming independent samples. Parameter gradients are preserved.
        Accumulation cannot recover gradients already lost inside a low-precision model.
        Checkpointed forwards must use ``use_reentrant=False`` (required by ``autograd.grad``).
    """
    if not torch.is_grad_enabled() or torch.is_inference_mode_enabled():
        raise RuntimeError("EntropyGradient requires a forward and attribution with gradient tracking enabled")
    indices = self._validate_inputs(logits, embeddings, visual_tokens)
    if not logits.requires_grad or not embeddings.requires_grad:
        raise RuntimeError("EntropyGradient requires differentiable logits and embeddings")
    dtype = torch.float64 if torch.float64 in {logits.dtype, embeddings.dtype} else torch.float32
    log_probs = logits.to(dtype).log_softmax(-1)
    probs = log_probs.exp()
    # For first derivatives, sum(p * d(log p)) = sum(dp) = 0. Center detached coefficients
    # so uniform logits yield exact zero gradients; mask zero probabilities for finite extreme logits.
    offset = log_probs.detach().amax(-1, keepdim=True)
    centered_log_probs = (log_probs.detach() - offset).masked_fill(probs == 0, 0)
    entropy = -(probs * centered_log_probs).sum(-1) - offset.squeeze(-1)
    # Differentiate the original tensor, then select tokens: a post-forward slice is not a graph ancestor.
    device_type = embeddings.device.type
    autocast = (
        torch.autocast(device_type=device_type, enabled=False)
        if torch.amp.is_autocast_available(device_type)
        else nullcontext()
    )
    with autocast:
        grad = torch.autograd.grad(entropy.sum(), embeddings, retain_graph=retain_graph, allow_unused=True)[0]
    if grad is None:
        raise RuntimeError("logits are disconnected from `embeddings`; pass the exact tensor used by the forward")
    grad = grad.detach().to(dtype)
    if indices is not None:
        grad = grad.index_select(1, indices)
    if not grad.isfinite().all():
        raise ValueError("selected embedding gradients must be finite")
    # Rescaling prevents the sum of squared channels from underflowing or overflowing.
    scale = grad.abs().amax(-1, keepdim=True)
    maps = (grad / scale.masked_fill(scale == 0, 1)).norm(p=2, dim=-1) * scale.squeeze(-1)
    if not maps.isfinite().all():
        raise ValueError("gradient channel norms must be finite")
    maps = maps.reshape(embeddings.shape[0], *self.grid_shape)
    if normalized:
        # A fixed epsilon would erase contrast in small uncertainty gradients.
        maps = _CAM._normalize(maps, spatial_dims=2, eps=torch.finfo(dtype).tiny)  # noqa: SLF001
    return maps

Input-token attribution for text decisions

TokenIG runs embedding Integrated Gradients for each selected classifier score. It returns signed token contributions and uncalibrated model scores, with completeness and refinement checks. TokenAblation instead measures single-token replacement effects using only forwards; effects need not add up. See Text decision models for examples and Laya integration.

TokenAblation

TokenAblation(model: Module, output_transform: Callable[[Any], Tensor] | None = None)

Explain a classifier by replacing one input token at a time, without gradients.

All classes share one forward per selected token position, plus the original forward. Attention, positions and other context stay fixed. Use a replacement ID appropriate to the model, such as its pretrained mask token. Models must be deterministic in evaluation mode, have independent batch samples, and not mutate their parameters.

PARAMETER DESCRIPTION
model

PyTorch classifier accepting keyword arguments, including input_ids

TYPE: Module

output_transform

optional function selecting floating-point (N, classes) scores from model output

TYPE: Callable[[Any], Tensor] | None DEFAULT: None

RAISES DESCRIPTION
ValueError

if an embedding renormalizes weights during forward

Source code in torchcam/methods/text.py
def __init__(self, model: nn.Module, output_transform: Callable[[Any], Tensor] | None = None) -> None:
    if any(isinstance(layer, nn.Embedding) and layer.max_norm is not None for layer in model.modules()):
        raise ValueError("embeddings with `max_norm` mutate weights during forward and are unsupported")
    self.model = model
    self.output_transform = output_transform

torchcam.methods.TokenAblation.__call__

__call__(inputs: Mapping[str, Any], replacement_id: int, *, token_mask: Tensor | None = None) -> TokenAblationResult

Return signed effects of individual token replacements.

PARAMETER DESCRIPTION
inputs

unchanged model keyword arguments; input_ids must be integer (N, tokens)

TYPE: Mapping[str, Any]

replacement_id

vocabulary ID substituted at each selected position; attention remains unchanged

TYPE: int

token_mask

boolean (N, tokens) mask on the IDs' device; omitted means every token

TYPE: Tensor | None DEFAULT: None

RETURNS DESCRIPTION
TokenAblationResult

detached original scores and signed effects. Positive values mean replacement lowers a class score.

TokenAblationResult

Effects are marginal changes with other tokens fixed, not additive contributions or token probabilities.

RAISES DESCRIPTION
ValueError

if IDs, replacement, mask or model scores are invalid

Source code in torchcam/methods/text.py
def __call__(
    self, inputs: Mapping[str, Any], replacement_id: int, *, token_mask: Tensor | None = None
) -> TokenAblationResult:
    """Return signed effects of individual token replacements.

    Args:
        inputs: unchanged model keyword arguments; ``input_ids`` must be integer ``(N, tokens)``
        replacement_id: vocabulary ID substituted at each selected position; attention remains unchanged
        token_mask: boolean ``(N, tokens)`` mask on the IDs' device; omitted means every token

    Returns:
        detached original scores and signed effects. Positive values mean replacement lowers a class score.
        Effects are marginal changes with other tokens fixed, not additive contributions or token probabilities.

    Raises:
        ValueError: if IDs, replacement, mask or model scores are invalid
    """
    ids = inputs.get("input_ids")
    if (
        not isinstance(ids, Tensor)
        or ids.ndim != 2
        or not ids.numel()
        or ids.dtype not in {torch.int32, torch.int64}
    ):
        raise ValueError("`input_ids` must be non-empty integer (N, tokens)")
    if not isinstance(replacement_id, int) or isinstance(replacement_id, bool) or replacement_id < 0:
        raise ValueError("`replacement_id` must be a nonnegative vocabulary ID")
    mask = torch.ones_like(ids, dtype=torch.bool) if token_mask is None else token_mask
    if (
        not isinstance(mask, Tensor)
        or mask.shape != ids.shape
        or mask.dtype != torch.bool
        or mask.device != ids.device
    ):
        raise ValueError("`token_mask` must be boolean (N, tokens) on the IDs' device")

    def score(batch: Mapping[str, Any]) -> Tensor:
        output = self.model(**batch)
        value = output if self.output_transform is None else self.output_transform(output)
        shape = value.shape if isinstance(value, Tensor) else ()
        if (
            len(shape) != 2
            or shape[0] != ids.shape[0]
            or not shape[1]
            or not value.is_floating_point()
            or not value.isfinite().all()
        ):
            raise ValueError("model scores must be finite floating-point (N, classes)")
        return value.detach()

    with _model_eval(self.model), torch.no_grad():
        scores = score(inputs)
        maps = scores.new_zeros((*scores.shape, ids.shape[1]))
        for position in mask.any(0).nonzero().flatten().tolist():
            replaced = ids.clone()
            replaced[mask[:, position], position] = replacement_id
            changed = score({**inputs, "input_ids": replaced})
            if changed.shape != scores.shape:
                raise ValueError("model score shape changed after token replacement")
            maps[:, :, position] = (scores - changed) * mask[:, position, None]
    return TokenAblationResult(scores, maps)

TokenAblationResult dataclass

TokenAblationResult(scores: Tensor, attributions: Tensor)

Single-token replacement effects; interactions mean contributions need not add up.

ATTRIBUTE DESCRIPTION
scores

original scores shaped (N, classes); no calibration is applied

TYPE: Tensor

attributions

original minus replaced-token scores, shaped (N, classes, tokens); excluded tokens are zero

TYPE: Tensor

TokenIG

TokenIG(model: Module, embedding_layer: Module | str, output_transform: Callable[[Any], Tensor] | None = None)

Explain text classifiers with embedding Integrated Gradients.

Implements Integrated Gradients on a straight embedding path. Gauss-Legendre integration is refined until signed completeness and successive token maps agree. One forward per node is shared across selected classes. No tokenizer or model-specific dependency is needed. Temporary embedding hooks and evaluation mode are restored on success or failure; parameter gradients are preserved.

PARAMETER DESCRIPTION
model

differentiable PyTorch classifier; call its underlying module rather than a no-grad prediction API

TYPE: Module

embedding_layer

input word-embedding module, or its name in model; it must run exactly once per forward

TYPE: Module | str

output_transform

optional function selecting a floating-point (N, classes) score tensor from model output; for example lambda output: output.logits or lambda output: output[0]. Prefer pre-softmax logits.

TYPE: Callable[[Any], Tensor] | None DEFAULT: None

RAISES DESCRIPTION
ValueError

if the embedding layer is outside the model or renormalizes weights during forward

Source code in torchcam/methods/text.py
def __init__(
    self,
    model: nn.Module,
    embedding_layer: nn.Module | str,
    output_transform: Callable[[Any], Tensor] | None = None,
) -> None:
    self.model = model
    self.embedding_layer = (
        model.get_submodule(embedding_layer) if isinstance(embedding_layer, str) else embedding_layer
    )
    if self.embedding_layer not in model.modules():
        raise ValueError("`embedding_layer` must belong to `model`")
    if isinstance(self.embedding_layer, nn.Embedding) and self.embedding_layer.max_norm is not None:
        raise ValueError("embeddings with `max_norm` mutate weights during forward and are unsupported")
    self.output_transform = output_transform

torchcam.methods.TokenIG.__call__

__call__(inputs: Mapping[str, Any], class_idx: int | list[int] | None = None, *, token_mask: Tensor | None = None, baseline: Tensor | None = None, steps: int = 32, max_steps: int = 4096, tolerance: float = 0.0001, stability_tolerance: float = 0.01) -> TokenAttribution

Attribute selected class scores to the input tokens.

PARAMETER DESCRIPTION
inputs

keyword arguments forwarded unchanged to the model, including IDs, masks and position metadata

TYPE: Mapping[str, Any]

class_idx

one output column, an ordered list, or None for all columns; shared across the batch

TYPE: int | list[int] | None DEFAULT: None

token_mask

boolean (N, tokens) mask selecting embeddings to vary; omitted means every token

TYPE: Tensor | None DEFAULT: None

baseline

reference embeddings matching the embedding output's shape, device and dtype; defaults to zero

TYPE: Tensor | None DEFAULT: None

steps

initial quadrature nodes, doubled on refinement; at least two integration passes are required

TYPE: int DEFAULT: 32

max_steps

maximum nodes per integration pass, at least twice steps

TYPE: int DEFAULT: 4096

tolerance

maximum absolute signed completeness error for every sample and selected class

TYPE: float DEFAULT: 0.0001

stability_tolerance

maximum L1 map change divided by map L1 norm on successive passes

TYPE: float DEFAULT: 0.01

RETURNS DESCRIPTION
TokenAttribution

detached scores, signed maps and convergence information. Positive values support a class relative to

TokenAttribution

the baseline; negative values oppose it. These are score contributions, not token probabilities.

RAISES DESCRIPTION
ValueError

if inputs, scores, baseline, selection or integration settings are invalid

RuntimeError

if gradients are unavailable, the embedding module is reused, or integration fails to converge

Note

Batch samples must be independent. Use eager inference and non-reentrant gradient checkpointing. An outer torch.no_grad() is supported, but inference mode and no-grad model forwards are not. Numerical convergence does not establish causal influence or bias; compare baselines and controlled edits.

Source code in torchcam/methods/text.py
def __call__(
    self,
    inputs: Mapping[str, Any],
    class_idx: int | list[int] | None = None,
    *,
    token_mask: Tensor | None = None,
    baseline: Tensor | None = None,
    steps: int = 32,
    max_steps: int = 4096,
    tolerance: float = 1e-4,
    stability_tolerance: float = 0.01,
) -> TokenAttribution:
    """Attribute selected class scores to the input tokens.

    Args:
        inputs: keyword arguments forwarded unchanged to the model, including IDs, masks and position metadata
        class_idx: one output column, an ordered list, or ``None`` for all columns; shared across the batch
        token_mask: boolean ``(N, tokens)`` mask selecting embeddings to vary; omitted means every token
        baseline: reference embeddings matching the embedding output's shape, device and dtype; defaults to zero
        steps: initial quadrature nodes, doubled on refinement; at least two integration passes are required
        max_steps: maximum nodes per integration pass, at least twice ``steps``
        tolerance: maximum absolute signed completeness error for every sample and selected class
        stability_tolerance: maximum L1 map change divided by map L1 norm on successive passes

    Returns:
        detached scores, signed maps and convergence information. Positive values support a class relative to
        the baseline; negative values oppose it. These are score contributions, not token probabilities.

    Raises:
        ValueError: if inputs, scores, baseline, selection or integration settings are invalid
        RuntimeError: if gradients are unavailable, the embedding module is reused, or integration fails to converge

    Note:
        Batch samples must be independent. Use eager inference and non-reentrant gradient checkpointing.
        An outer ``torch.no_grad()`` is supported, but inference mode and no-grad model forwards are not.
        Numerical convergence does not establish causal influence or bias; compare baselines and controlled edits.
    """
    if torch.is_inference_mode_enabled():
        raise RuntimeError("TokenIG cannot run in inference mode; use a differentiable model forward")
    if any(not isinstance(n, int) or isinstance(n, bool) for n in (steps, max_steps)):
        raise ValueError("`steps` and `max_steps` must be integers")
    if steps < 2 or max_steps < 2 * steps:
        raise ValueError("use `steps` >= 2 and `max_steps` >= 2 * steps")
    if not 0 < tolerance < float("inf") or not 0 < stability_tolerance < float("inf"):
        raise ValueError("convergence tolerances must be finite and positive")
    with _model_eval(self.model), torch.enable_grad():
        return self._explain(
            inputs, class_idx, token_mask, baseline, steps, max_steps, tolerance, stability_tolerance
        )

TokenAttribution dataclass

TokenAttribution(scores: Tensor, baseline_scores: Tensor, attributions: Tensor, class_indices: tuple[int, ...], completeness_error: Tensor, steps: int)

Input-token contributions relative to an embedding baseline.

ATTRIBUTE DESCRIPTION
scores

original model scores, shaped (N, classes); no calibration is applied

TYPE: Tensor

baseline_scores

model scores with selected embeddings replaced by the baseline

TYPE: Tensor

attributions

signed contributions shaped (N, selected_classes, tokens); excluded tokens are zero

TYPE: Tensor

class_indices

output columns, in the same order as the attribution maps

TYPE: tuple[int, ...]

completeness_error

signed attribution sum minus score change, shaped (N, selected_classes)

TYPE: Tensor

steps

quadrature nodes used by the final integration; earlier refinement passes are additional

TYPE: int

Visual token activation maps

Visual explanations for selected vocabulary tokens in multimodal language models. TAM consumes final language-model states and a linear vocabulary head directly; it does not use the class CAM hook API.

TAM

TAM(token_classifier: Linear, kernel_size: int = 3)

Extract the visual Token Activation Map from "Token Activation Map to Visually Explain Multimodal LLMs".

TAM projects final language-model states onto output-head weights, removes the least-squares estimate of earlier text's visual interference, and applies the paper's rank Gaussian filter. Identical context token IDs are excluded from interference. The output-head bias is unused, following equation (1).

This extractor consumes states from an existing forward; it installs no hooks and runs no model forwards or backward passes. Unlike class CAM extractors, it returns one tensor, shaped (N, H, W). Only visual maps are returned, with optional normalization over each image rather than joint image/text normalization.

PARAMETER DESCRIPTION
token_classifier

the model's linear vocabulary output head, usually model.get_output_embeddings()

TYPE: Linear

kernel_size

positive odd rank Gaussian window size; use 1 to disable smoothing

TYPE: int DEFAULT: 3

RAISES DESCRIPTION
TypeError

if the output head or kernel size has an invalid type

ValueError

if the kernel size is not positive and odd

Source code in torchcam/methods/token.py
def __init__(self, token_classifier: nn.Linear, kernel_size: int = 3) -> None:
    if not isinstance(token_classifier, nn.Linear):
        raise TypeError("`token_classifier` must be an nn.Linear vocabulary head")
    if not isinstance(kernel_size, int) or isinstance(kernel_size, bool):
        raise TypeError("`kernel_size` must be an integer")
    if kernel_size <= 0 or kernel_size % 2 == 0:
        raise ValueError("`kernel_size` must be positive and odd")
    self.token_classifier = token_classifier
    self.kernel_size = kernel_size

torchcam.methods.TAM.__call__

__call__(token_id: int | list[int], visual_features: Tensor, context_features: Tensor, context_ids: Tensor, normalized: bool = True) -> Tensor

Explain one vocabulary token per image using final language-model states.

PARAMETER DESCRIPTION
token_id

vocabulary ID shared by the batch, or one ID per sample

TYPE: int | list[int]

visual_features

image-token states reshaped to (N, H, W, C) in spatial order

TYPE: Tensor

context_features

earlier text states shaped (N, S, C), aligned with context_ids

TYPE: Tensor

context_ids

earlier text vocabulary IDs shaped (N, S); exclude image, padding and special tokens

TYPE: Tensor

normalized

whether to normalize each visual map to [0, 1]

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
Tensor

visual activation maps shaped (N, H, W), on the visual features' device and dtype

RAISES DESCRIPTION
ValueError

if shapes, devices, feature dtypes or vocabulary IDs are incompatible

Source code in torchcam/methods/token.py
@torch.no_grad()
def __call__(
    self,
    token_id: int | list[int],
    visual_features: Tensor,
    context_features: Tensor,
    context_ids: Tensor,
    normalized: bool = True,
) -> Tensor:
    """Explain one vocabulary token per image using final language-model states.

    Args:
        token_id: vocabulary ID shared by the batch, or one ID per sample
        visual_features: image-token states reshaped to ``(N, H, W, C)`` in spatial order
        context_features: earlier text states shaped ``(N, S, C)``, aligned with ``context_ids``
        context_ids: earlier text vocabulary IDs shaped ``(N, S)``; exclude image, padding and special tokens
        normalized: whether to normalize each visual map to [0, 1]

    Returns:
        visual activation maps shaped ``(N, H, W)``, on the visual features' device and dtype

    Raises:
        ValueError: if shapes, devices, feature dtypes or vocabulary IDs are incompatible
    """  # noqa: DOC502
    self._validate_inputs(token_id, visual_features, context_features, context_ids)
    ids = [token_id] * visual_features.shape[0] if isinstance(token_id, int) else token_id
    target_ids = torch.tensor(ids, device=visual_features.device, dtype=torch.long)
    # Accumulate in float32 for half-precision models, without converting the full vocabulary head.
    dtype = torch.float64 if visual_features.dtype == torch.float64 else torch.float32
    device_type = visual_features.device.type
    autocast = (
        torch.autocast(device_type=device_type, enabled=False)
        if torch.amp.is_autocast_available(device_type)
        else nullcontext()
    )
    with autocast:
        visual = visual_features.flatten(1, 2).to(dtype)
        weights = self.token_classifier.weight[target_ids].to(dtype)
        maps = (visual * weights.unsqueeze(1)).sum(-1).relu()
        tolerance = maps.amax(-1, keepdim=True) * (16 * torch.finfo(dtype).eps)
        relevance = (context_features.to(dtype) * weights.unsqueeze(1)).sum(-1).relu()
        relevance.masked_fill_(context_ids == target_ids.unsqueeze(1), 0)
        relevance /= relevance.sum(-1, keepdim=True) + 1e-8
        context_weights = self.token_classifier.weight[context_ids].to(dtype)
        context_maps = visual.bmm(context_weights.transpose(1, 2)).relu()
        interference = context_maps.bmm(relevance.unsqueeze(-1)).squeeze(-1)
        denominator = interference.square().sum(-1, keepdim=True)
        scale = (maps * interference).sum(-1, keepdim=True) / denominator.masked_fill(denominator == 0, 1)
        maps = (maps - scale * interference).relu()
        # Avoid amplifying round-off from complete cancellation into a strong heatmap.
        maps.masked_fill_(maps.amax(-1, keepdim=True) <= tolerance, 0)
        maps = maps.reshape(visual_features.shape[:3])
        maps = self._filter(maps)
        if normalized:
            maps = _CAM._normalize(maps)  # noqa: SLF001
    return maps.to(visual_features.dtype)