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.

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.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 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

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.

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."""  # noqa: DOC201
    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
    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)))
    self._grad_hook_handles: list[torch.utils.hooks.RemovableHandle | None] = [None] * len(self.target_names)

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 \(\alpha_k^{(c)}(i, j)\) being defined as:

\[ \alpha_k^{(c)}(i, j) = \frac{1}{\sum\limits_{i, j} \frac{\partial Y^{(c)}}{\partial A_k(i, j)}} = \frac{\frac{\partial^2 Y^{(c)}}{(\partial A_k(i,j))^2}}{2 \cdot \frac{\partial^2 Y^{(c)}}{(\partial A_k(i,j))^2} + \sum\limits_{a,b} A_k (a,b) \cdot \frac{\partial^3 Y^{(c)}}{(\partial A_k(i,j))^3}} \]

if \(\frac{\partial Y^{(c)}}{\partial A_k(i, j)} = 1\) else \(0\).

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
    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)))
    self._grad_hook_handles: list[torch.utils.hooks.RemovableHandle | None] = [None] * len(self.target_names)

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(\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 \(\alpha_k^{(c)}(i, j)\) being defined as:

\[ \alpha_k^{(c)}(i, j) = \frac{\frac{\partial^2 Y^{(c)}}{(\partial A_k(i,j))^2}}{2 \cdot \frac{\partial^2 Y^{(c)}}{(\partial A_k(i,j))^2} + \sum\limits_{a,b} A_k (a,b) \cdot \frac{\partial^3 Y^{(c)}}{(\partial A_k(i,j))^3}} = \frac{\frac{1}{n} \sum\limits_{m=1}^n D^{(c, 2)}_k(i, j)}{ \frac{2}{n} \sum\limits_{m=1}^n D^{(c, 2)}_k(i, j) + \sum\limits_{a,b} A_k (a,b) \cdot \frac{1}{n} \sum\limits_{m=1}^n D^{(c, 3)}_k(i, j)} \]

if \(\frac{\partial Y^{(c)}}{\partial A_k(i, j)} = 1\) else \(0\). Here \(D^{(c, p)}_k(i, j)\) refers to the p-th partial derivative of the class score of class \(c\) relatively to the activation in layer \(k\) at position \((i, j)\), and \(n\) is the number of samples used to get the gradient estimate.

Please note the difference in the numerator of \(\alpha_k^{(c)}(i, j)\), which is actually \(\frac{1}{n} \sum\limits_{k=1}^n D^{(c, 1)}_k(i,j)\) in the paper.

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
    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)))
    self._grad_hook_handles: list[torch.utils.hooks.RemovableHandle | None] = [None] * len(self.target_names)

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
    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)))
    self._grad_hook_handles: list[torch.utils.hooks.RemovableHandle | None] = [None] * len(self.target_names)

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], scores: Tensor | None = None, normalized: bool = True, retain_graph: bool = False, **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],
    scores: Tensor | None = None,
    normalized: bool = True,
    retain_graph: bool = False,
    **kwargs: Any,
) -> list[Tensor]:
    """Compute and average layerwise positive attention-gradient maps.

    Raises:
        ValueError: if attention and token shapes are incompatible
    """  # noqa: DOC201
    relevances = self._get_weights(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], scores: Tensor | None = None, normalized: bool = True, target_shape: tuple[int, ...] | 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],
    scores: Tensor | None = None,
    normalized: bool = True,
    target_shape: tuple[int, ...] | 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, **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