Skip to content

Advanced usage

The quick start uses a torchvision classifier, but TorchCAM works with many PyTorch classifiers. This guide covers the questions that come up most often once you move past the basic example. Hitting an error rather than a usage question? Jump to Troubleshooting.

Model and task compatibility

TorchCAM needs a spatial feature tensor and a scalar output target to explain. The integration path depends on what your model accepts and returns:

Model or task Support What TorchCAM needs
CNN classifier Native Class logits shaped (N, num_classes) and a spatial target layer.
Batched, 3D, or video classifier Native One class index per sample and the correct input_shape.
Multi-input model Adapter required Wrap the inputs into one tensor argument while preserving the hooked path.
Tensor or per-sample list output Native with targets Reduce each sample output to one scalar tensor.
torchvision VisionTransformer Native with LeGrad Target supported encoder blocks; other CAM methods need reshape_transform.
Other ViT or Swin classifier Adapter required Set target_layer and reshape tokens with reshape_transform; LeGrad only supports the contract below.
Detection, segmentation, embedding, or other output Native with targets Define a scalar target; gradient methods require it to remain differentiable.

Use your own model

TorchCAM's class-index API works with an nn.Module whose forward returns logits of shape (N, num_classes) — it is not limited to torchvision. The targets API below also supports other batched outputs. In both cases, tell the extractor which layer to read the activations from.

List the candidate layers by name:

for name, module in model.named_modules():
    print(name, "->", type(module).__name__)

Then pass the name or the module itself as target_layer:

from torchcam.methods import SmoothGradCAMpp

cam_extractor = SmoothGradCAMpp(model, target_layer="features.7")        # by name
# equivalently
cam_extractor = SmoothGradCAMpp(model, target_layer=model.features[7])   # by module

If you omit target_layer, TorchCAM runs a dummy forward of shape (1, *input_shape) (default (3, 224, 224)), picks the last layer whose output still has spatial dimensions, and logs the choice. If your model expects a different input, set input_shape accordingly — otherwise the dummy forward will fail or pick the wrong layer:

cam_extractor = LayerCAM(model, input_shape=(3, 384, 384))

Choosing the target layer

A CAM is computed on the activation map of a convolutional (spatial) layer. The default — the last convolutional layer before global pooling — is the most class-discriminative but also the coarsest. Earlier layers give finer, less semantic maps. Rules of thumb for common torchvision backbones:

The target-layer trade-off between spatial detail and class specificity across a convolutional network.

Architecture Typical target_layer fc_layer for CAM
ResNet / ResNeXt "layer4" "fc"
DenseNet "features" "classifier"
MobileNet v2 "features" "classifier.1"
EfficientNet "features" "classifier.1"
MobileNet v3 "features" two Linear layers — CAM n/a
VGG "features" three Linear layers — CAM n/a
SqueezeNet "features" no Linear head — CAM n/a

When does the base CAM work?

CAM needs exactly one nn.Linear classification head fed by global pooling, and resolves it automatically. It therefore works for ResNet, DenseNet, MobileNet v2, EfficientNet, etc., but not for models with several linear layers (VGG, MobileNet v3) or none (SqueezeNet) — there, use a gradient- or score-based method, or pass a compatible fc_layer explicitly. All the other methods have no such requirement.

You can also pass a list of layers and fuse them — LayerCAM benefits a lot from this:

from torchcam.methods import LayerCAM

with LayerCAM(model, ["layer2", "layer3", "layer4"]) as cam_extractor:
    out = model(input_tensor)
    class_idx = out.squeeze(0).argmax().item()
    cams = cam_extractor(class_idx, out)        # one map per layer
    fused = cam_extractor.fuse_cams(cams)       # single fused map

RefineCAM formalizes multi-layer fusion by normalizing and multiplying the maps. It requires at least two target layers and uses GradCAMpp as its base method by default. Pass another extractor class to reuse its weighting:

from torchcam.methods import LayerCAM, RefineCAM

with RefineCAM(model, ["layer2", "layer3", "layer4"], base_method=LayerCAM) as cam_extractor:
    out = model(input_tensor)
    refined = cam_extractor(out.squeeze(0).argmax().item(), out)[0]

FinerCAM instead changes what a gradient method explains. For target class \(c\) and reference classes \(d_t\), it replaces the target score with the contrastive objective

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

The references are averaged before the base method's final CAM ReLU. By default, gamma=0.6 and up to three classes whose logits are closest to the target logit are selected automatically, excluding the target. Pass an integer or flat list to share explicit references across the batch, or an equal-width nested list with one row per sample. Explicit references override num_references.

from torchcam.methods import FinerCAM, LayerCAM

with FinerCAM(model, "layer4", base_method=LayerCAM) as cam_extractor:
    out = model(input_tensor)
    class_idx = out.squeeze(0).argmax().item()
    cams = cam_extractor(class_idx, out, comparison_idx=[12, 37, 84])

FinerCAM supports GradCAM, GradCAMpp, and LayerCAM, requires the original differentiable score tensor, and returns one tensor per selected layer. Its intended behavior is improved discrimination between fine-grained classes; it is not a universal guarantee of better localization. Score-based CAM methods are not approximated.

Understanding targets and the call signature

cam_extractor(class_idx=None, scores=None, normalized=True, *, targets=None)
  • class_idx (int or list[int]) — the index, in the output logits, of the class you want to explain. To explain the top prediction use the argmax (out.squeeze(0).argmax().item()), but you can pass any valid index to see where the model looks for that class. For a batch, pass one index per sample (see below).
  • scores — the raw model output. class_idx expects a tensor shaped (N, num_classes); targets accepts a batched tensor or one list item per sample. Required by gradient methods; ignored by LeGrad, SmoothGradCAMpp, and CAM. The Score-CAM family re-runs the stored input, so scores can be omitted.
  • targets — one callable shared by the batch or one callable per sample. Each callable receives one sample's model output and must return a scalar tensor. Pass exactly one of class_idx or targets.
  • normalized — when True (default) each map is min-max normalized to [0, 1], which is what you want for visualization/overlay. Pass normalized=False to get the raw weighted maps, e.g. when comparing magnitudes across layers before fusing them yourself.
  • Returns a list of activation maps, one tensor per hooked layer, each of shape (N, H, W). With a single layer and a single image, the map you want is cams[0].squeeze(0). LeGrad and RefineCAM instead return a one-element list containing their final fused map.

Gradient-based extractors also accept retain_graph=True (forwarded to the gradient computation), needed when you call the extractor several times after a single forward — see Troubleshooting.

targets opens the same extractor API to dense predictions and embeddings without a task-specific adapter. For a segmentation tensor shaped (N, classes, H, W), explain class 5 inside a region mask:

with LayerCAM(model, target_layer="backbone.layer4") as cam_extractor:
    output = model(input_batch)
    cams = cam_extractor(scores=output, targets=lambda sample: sample[5][region_mask].mean())

For an embedding tensor shaped (N, embedding_dim), the target can be its similarity to another embedding:

import torch.nn.functional as F

target = lambda embedding: F.cosine_similarity(embedding, text_embedding, dim=0)
with LayerCAM(model, target_layer="visual.layer4") as cam_extractor:
    image_embeddings = model(input_batch)
    cams = cam_extractor(scores=image_embeddings, targets=target)

Tensor outputs are split along their batch dimension. A model may instead return a list with one item per sample, such as detection dictionaries; in that case each target receives the corresponding item. GradCAM, GradCAMpp, SmoothGradCAMpp, XGradCAM, LayerCAM, ScoreCAM, SSCAM, and ISCAM support this contract, as does RefineCAM when its base method supports it. CAM, FinerCAM, and LeGrad retain their specialized class-based objectives.

Batched inputs

Batches are supported: pass a list of class indices whose length matches the batch size.

import torch
from torchcam.methods import GradCAM

input_batch = torch.stack([img1, img2, img3])   # (3, C, H, W)
with GradCAM(model) as cam_extractor:
    out = model(input_batch)                     # (3, num_classes)
    class_ids = out.argmax(dim=1).tolist()       # one class per sample
    cams = cam_extractor(class_ids, out)         # cams[0] has shape (3, H, W)

Models with multiple inputs or batched dictionary outputs

The hooked layer must output a tensor. targets handles a batched tensor or a list containing one output per sample. Tuple outputs such as (logits, aux), one dictionary containing batched tensors, and models taking several inputs (e.g. a siamese network) need a thin wrapper around that boundary:

import torch.nn as nn
from torchcam.methods import GradCAM

class LogitsOnly(nn.Module):
    def __init__(self, model):
        super().__init__()
        self.model = model

    def forward(self, x):
        return self.model(x)[0]          # keep the logits, drop the rest

wrapped = LogitsOnly(model)
cam_extractor = GradCAM(wrapped, target_layer=wrapped.model.backbone.layer4)  # pass the module directly

Passing the module object (rather than its name) sidesteps the naming gotcha: wrapping shifts every layer name under a "model." prefix, so a hard-coded string like "backbone.layer4" would raise a ValueError. If you prefer names, discover the correct one after wrapping:

print([n for n, _ in wrapped.named_modules() if n.endswith("layer4")])
# -> ['model.backbone.layer4']

Vision Transformers and other non-CNN models

TorchCAM's methods operate on spatial feature maps of shape (N, C, H, W) (or (N, C, D, H, W) in 3D). Transformer blocks emit token sequences of shape (N, num_tokens, dim), which have no spatial grid, so CAM methods do not apply directly and automatic target_layer resolution cannot infer the token layout.

Use reshape_transform to convert the hooked tokens and their gradients back to a spatial grid. For a torchvision ViT, drop the class token, reshape the remaining patch tokens, and move the embedding dimension before the spatial dimensions:

A Vision Transformer hook and reshape transform converting patch tokens into a spatial feature tensor.

from PIL import Image
from torchvision.models import ViT_B_16_Weights, vit_b_16
from torchcam.methods import GradCAM

weights = ViT_B_16_Weights.DEFAULT
model = vit_b_16(weights=weights).eval()
grid_size = model.image_size // model.patch_size
image = Image.open("path/to/image.jpg").convert("RGB")

def reshape_transform(tensor):
    patches = tensor[:, 1:, :].reshape(tensor.size(0), grid_size, grid_size, tensor.size(-1))
    return patches.permute(0, 3, 1, 2)

input_tensor = weights.transforms()(image).unsqueeze(0)
target_layer = model.encoder.layers[-2].ln_1

with GradCAM(model, target_layer, reshape_transform=reshape_transform) as extractor:
    scores = model(input_tensor)
    cam = extractor(scores[0].argmax().item(), scores)[0]

DeiT-Tiny follows the same pattern: target model.blocks[-1].norm1, drop model.num_prefix_tokens, and reshape the remaining tokens using model.patch_embed.grid_size.

For a complete example without additional dependencies, run the official pretrained torchvision Swin-T (patch size 4, window size 7, input size 224) from the repository checkout:

uv run --extra scripts python scripts/cam_example.py \
    --arch swin_t --method GradCAM \
    --savefig swin_t_cam.png --noblock

The first run downloads the torchvision Swin-T weights. The script selects the model prediction and targets the final block's norm2. This layer retains a 7×7 channels-last spatial grid, so its transform only moves the channel axis before the spatial axes.

The exact transform is architecture-specific: models may use a different patch grid or have additional prefix tokens such as a distillation token. Always specify target_layer when using reshape_transform. For a ViT, choose a layer before the final attention operation; patch-token gradients are zero at the complete final block output because classification uses only the class token. The selected module must return a tensor, and the same transform is applied to every selected target layer. Keep the transform structural—token selection, reshaping, and axis permutation—because TorchCAM applies it identically to activation and gradient tensors.

This enables activation- and gradient-based extractors such as LayerCAM, GradCAM, and ScoreCAM. The original weight-based CAM method still requires its global-pooling and classifier-weight assumptions, which standard ViTs do not satisfy. Attention rollout or attention flow are separate transformer-specific explanation techniques.

LeGrad for torchvision Vision Transformers

LeGrad uses the positive gradient of each layer-specific class score with respect to the post-softmax attention probabilities. For layer \(l\), head \(h\), query \(q\), and key \(k\):

\[ E^l_k = \frac{1}{H Q} \sum_{h,q} \operatorname{ReLU}\left(\frac{\partial s^l}{\partial A^l_{h,q,k}}\right), \qquad E = \operatorname{normalize}\left(\frac{1}{L} \sum_l \operatorname{reshape}(E^l_{\text{patch keys}})\right). \]

LeGrad does not multiply gradients by attention values. That multiplication is AttentionCAM. It also differs from GradCAM-on-tokens, which weights token activations, and attention rollout, which multiplies attention matrices across layers.

For torchvision VisionTransformer, target complete encoder blocks. The default layer score averages that block's tokens, then applies model.encoder.ln and model.heads as the shared classifier projection:

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

weights = ViT_B_16_Weights.DEFAULT
model = vit_b_16(weights=weights).eval()
input_tensor = weights.transforms()(image).unsqueeze(0)
target_layers = list(model.encoder.layers)[-4:]

with LeGrad(model, target_layers, prefix_tokens=1) as extractor:
    scores = model(input_tensor)
    cam = extractor(scores[0].argmax().item())[0]

The 196 patch keys form a square 14×14 grid, so grid_shape is inferred. Pass grid_shape=(height, width) for a non-square patch layout. Custom models must provide score_projection(tokens) -> logits and meet every condition below:

Requirement Supported contract
Target output One tensor shaped (batch, tokens, embedding).
Attention Direct self_attention child using batch-first, shared-dimension nn.MultiheadAttention.
Attention mode Self-attention without added key/value tokens or active attention dropout.
Prefix/grid Configurable prefix count; square grid inferred or non-square grid specified explicitly.

Swin, timm, OpenCLIP, cross-attention, attentional poolers, and arbitrary transformer layouts are not supported by this initial implementation. Run the model with gradient tracking enabled; torch.no_grad() and torch.inference_mode() cannot produce LeGrad maps.

3D and video models

Volumetric inputs work out of the box: set input_shape to your 3D input shape as (C, D, H, W) (i.e. excluding the batch dimension) and the resulting map has shape (N, D, H, W). Visualize it slice by slice:

import matplotlib.pyplot as plt
from torchcam.methods import GradCAM

cam_extractor = GradCAM(model, target_layer="...", input_shape=(1, 64, 128, 128))
out = model(volume)                                  # volume: (N, C, D, H, W)
cam = cam_extractor(out.squeeze(0).argmax().item(), out)[0]   # (N, D, H, W)
plt.imshow(cam[0, 32].cpu().numpy())                 # one depth slice

Video models that output (N, C, T, H, W) features are handled the same way (the temporal axis behaves like an extra spatial dimension). Note that overlay_mask works on 2D PIL images, so overlay each slice/frame separately.

Choosing a CAM method

Method Needs gradients Relative cost Notes
CAM no cheapest needs global pooling + a single nn.Linear head (e.g. ResNet); fails on multi-FC heads like VGG
GradCAM yes one backward pass robust default for most CNNs
LayerCAM yes one backward pass best localization in our benchmark; ideal when fusing layers
FinerCAM yes one backward pass contrastive fine-grained cues with GradCAM, GradCAMpp, or LayerCAM
LeGrad yes one gradient per selected layer attention-gradient maps for supported torchvision-style ViTs
RefineCAM depends on base base method + cheap fusion high-resolution fusion across at least two layers; defaults to GradCAMpp
GradCAMpp / XGradCAM yes one backward pass alternative weighting schemes
SmoothGradCAMpp yes num_samples forwards sharper maps via noise averaging
ScoreCAM / SSCAM / ISCAM no many forwards (slow) gradient-free; tune batch_size; useful when gradients are unavailable

See the latency and faithfulness benchmarks in the README for concrete numbers, and the methods reference for the full API.

Using CAM during or after training

CAM methods are post-hoc: run them on a trained model in eval() mode to interpret its predictions — they are not a training objective. To quantify how faithful a method is on your own data, use the ClassificationMetric. Metrics re-run masked or perturbed inputs in batches, so the model must support batched inference and return the same output structure for those forwards.