Skip to content

Prediction explanations

explain

High-level prediction debugging for one image.

torchcam.explain.PredictionExplanation dataclass

PredictionExplanation(logits: Tensor, predicted_class_idx: int, expected_class_idx: int | None, cams: Mapping[int, tuple[Tensor, ...]], method: str, target_layers: tuple[str, ...], model: str, input_shape: tuple[int, ...], versions: Mapping[str, str], class_names: tuple[str, ...] | None = None)

Prediction, class activation maps, and reproducibility metadata for one image.

torchcam.explain.PredictionExplanation.save

save(directory: str | Path, image: Image, alpha: float = 0.5, *, context: Mapping[str, str] | None = None) -> Path

Save NumPy maps, heatmaps, overlays, and a completion manifest to a new directory.

PARAMETER DESCRIPTION
directory

new output directory

TYPE: str | Path

image

RGB or grayscale model-view image after spatial transforms, before normalization

TYPE: Image

alpha

source-image opacity in the overlay

TYPE: float DEFAULT: 0.5

context

caller-supplied identifiers, such as checkpoint, preprocessing, sample, split, and group

TYPE: Mapping[str, str] | None DEFAULT: None

RETURNS DESCRIPTION
Path

output directory

RAISES DESCRIPTION
TypeError

if the image is not a PIL image or context does not map strings to strings

ValueError

if the image mode is unsupported or dimensions differ from the model input

FileExistsError

if the output directory already exists

Source code in torchcam/explain.py
def save(
    self, directory: str | Path, image: Image, alpha: float = 0.5, *, context: Mapping[str, str] | None = None
) -> Path:
    """Save NumPy maps, heatmaps, overlays, and a completion manifest to a new directory.

    Args:
        directory: new output directory
        image: RGB or grayscale model-view image after spatial transforms, before normalization
        alpha: source-image opacity in the overlay
        context: caller-supplied identifiers, such as checkpoint, preprocessing, sample, split, and group

    Returns:
        output directory

    Raises:
        TypeError: if the image is not a PIL image or context does not map strings to strings
        ValueError: if the image mode is unsupported or dimensions differ from the model input
        FileExistsError: if the output directory already exists
    """
    if not isinstance(image, Image):
        raise TypeError("`image` must be a PIL image")
    if image.mode not in {"1", "L", "I", "F", "I;16", "I;16B", "RGB"}:
        raise ValueError(f"unsupported image mode {image.mode!r}; use RGB or grayscale (1, L, I, F, I;16, I;16B)")
    if image.size != (self.input_shape[-1], self.input_shape[-2]):
        raise ValueError("`image` must match the model input size; pass the resized/cropped model-view image")
    if context is not None and (
        not isinstance(context, Mapping)
        or any(not isinstance(key, str) or not isinstance(value, str) for key, value in context.items())
    ):
        raise TypeError("`context` must map strings to strings")

    output_dir = Path(directory)
    if output_dir.exists():
        raise FileExistsError(f"output directory already exists: {output_dir}")
    output_dir.mkdir(parents=True)
    with ExitStack() as cleanup:
        cleanup.callback(shutil.rmtree, output_dir)
        input_image = "input.png" if image.mode in {"L", "RGB"} else "input.tiff"
        image.save(output_dir / input_image)
        probabilities = self.logits.softmax(dim=1)[0]
        classes: dict[str, dict[str, Any]] = {}

        for class_idx, maps in sorted(self.cams.items()):
            artifacts = []
            artifact_layers = (
                (self.target_layers,) if len(maps) == 1 else tuple((name,) for name in self.target_layers)
            )

            for layer_idx, (target_layers, cam) in enumerate(zip(artifact_layers, maps, strict=True)):
                stem = f"class-{class_idx}-layer-{layer_idx}"
                array = cam.numpy()
                npy_path = output_dir / f"{stem}.npy"
                heatmap_path = output_dir / f"{stem}-heatmap.png"
                overlay_path = output_dir / f"{stem}-overlay.png"

                np.save(npy_path, array, allow_pickle=False)
                heatmap = fromarray((255 * np.clip(array, 0, 1)).round().astype(np.uint8))
                heatmap.save(heatmap_path)
                overlay_mask(image, fromarray(array), alpha=alpha).save(overlay_path)
                artifacts.append({
                    "target_layers": list(target_layers),
                    "map": npy_path.name,
                    "heatmap": heatmap_path.name,
                    "overlay": overlay_path.name,
                })

            classes[str(class_idx)] = {
                "class_idx": class_idx,
                "class_name": self._class_name(class_idx),
                "logit": self.logits[0, class_idx].item(),
                "probability": probabilities[class_idx].item(),
                "artifacts": artifacts,
            }

        manifest = {
            "schema_version": 1,
            "prediction": self._class_reference(self.predicted_class_idx),
            "expected": self._class_reference(self.expected_class_idx),
            "classes": classes,
            "method": self.method,
            "target_layers": list(self.target_layers),
            "model": self.model,
            "input_shape": list(self.input_shape),
            "versions": dict(self.versions),
            "image_size": list(image.size),
            "input_image": input_image,
            "alpha": alpha,
        }
        if context:
            manifest["context"] = dict(context)
        (output_dir / "manifest.json").write_text(
            json.dumps(manifest, indent=2, sort_keys=True, allow_nan=False) + "\n", encoding="utf-8"
        )
        cleanup.pop_all()
    return output_dir

torchcam.explain.explain

explain(model: Module, input_tensor: Tensor, *, expected_class_idx: int | None = None, class_names: Sequence[str] | None = None, method: type[_CAM] = GradCAM, target_layer: Module | str | list[Module | str] | None = None, method_kwargs: Mapping[str, Any] | None = None) -> PredictionExplanation

Explain the predicted and optional expected class for one 2D image.

RETURNS DESCRIPTION
PredictionExplanation

detached prediction evidence and CAMs

RAISES DESCRIPTION
RuntimeError

if called from inference mode

ValueError

if an argument, model output, or CAM has an unsupported value

Source code in torchcam/explain.py
def explain(
    model: nn.Module,
    input_tensor: Tensor,
    *,
    expected_class_idx: int | None = None,
    class_names: Sequence[str] | None = None,
    method: type[_CAM] = GradCAM,
    target_layer: nn.Module | str | list[nn.Module | str] | None = None,
    method_kwargs: Mapping[str, Any] | None = None,
) -> PredictionExplanation:
    """Explain the predicted and optional expected class for one 2D image.

    Returns:
        detached prediction evidence and CAMs

    Raises:
        RuntimeError: if called from inference mode
        ValueError: if an argument, model output, or CAM has an unsupported value
    """
    if torch.is_inference_mode_enabled():
        raise RuntimeError("`explain` cannot run inside torch.inference_mode() because CAM extraction needs gradients")
    _validate_request(model, input_tensor, expected_class_idx, method, method_kwargs)

    kwargs = dict(method_kwargs or {})
    if "target_layer" in kwargs:
        raise ValueError("pass `target_layer` directly, not through `method_kwargs`")
    if target_layer is not None:
        kwargs["target_layer"] = target_layer
    elif method is GradCAM:
        kwargs.setdefault("input_shape", tuple(input_tensor.shape[1:]))

    working_input = input_tensor.detach().requires_grad_(True)
    parameters = tuple(model.parameters())
    gradients = tuple(parameter.grad for parameter in parameters)

    try:
        with torch.enable_grad(), method(model, **kwargs) as extractor:
            logits = _validate_logits(model(working_input))
            predicted_class_idx = int(logits[0].argmax().item())
            validated_class_names = _validate_classes(expected_class_idx, class_names, logits.shape[1])

            cams = {predicted_class_idx: _prepare_maps(extractor(predicted_class_idx, logits))}
            if expected_class_idx is not None and expected_class_idx != predicted_class_idx:
                expected_logits = _validate_logits(model(working_input))
                if expected_logits.shape != logits.shape:
                    raise ValueError("model output shape changed between explanation forwards")
                cams[expected_class_idx] = _prepare_maps(extractor(expected_class_idx, expected_logits))

            result = PredictionExplanation(
                logits=logits.detach().cpu(),
                predicted_class_idx=predicted_class_idx,
                expected_class_idx=expected_class_idx,
                cams=MappingProxyType(cams),
                method=method.__name__,
                target_layers=tuple(extractor.target_names),
                model=f"{model.__class__.__module__}.{model.__class__.__qualname__}",
                input_shape=tuple(input_tensor.shape),
                versions=MappingProxyType({
                    "python": platform.python_version(),
                    "torch": str(torch.__version__),
                    "torchcam": version("torchcam"),
                }),
                class_names=validated_class_names,
            )
    finally:
        for parameter, gradient in zip(parameters, gradients, strict=True):
            parameter.grad = gradient

    return result