Scoring TorchLens attributions with Quantus¶
Quantus evaluates explanation methods: faithfulness, robustness, complexity,
and more. TorchLens computes input attributions (torchlens.attribution:
saliency, input_x_grad, integrated_gradients, and others) but does not
score them, and it ships no Quantus extra. The join is Quantus's
explain_func argument: wrap a TorchLens attribution function so it takes
and returns numpy arrays the way Quantus calls it.
Minimal example on the running model¶
This page uses ResNet-18 on one image. Quantus scores an input attribution; this is TorchLens's saliency map for class 0 on the running CNN. The recipe below builds its own model for the method.
# recipe-test: tool=quantus expected=True
import torch
import torchvision
import torchlens as tl
torch.manual_seed(0)
model = torchvision.models.resnet18(weights=None).eval()
x = torch.rand(1, 3, 224, 224)
trace = tl.trace(model, x)
attribution = tl.attribution.saliency(model, x, target=0)
RESULT = tuple(attribution.values.shape) == tuple(x.shape)
Wrap an attribution function as explain_func¶
Quantus calls explain_func(model=..., inputs=..., targets=..., **kwargs)
with a numpy batch and one integer target per row, and expects a numpy array
with the batch size first and the same number of dimensions as the inputs.
TorchLens attribution functions take one target: an integer class index, or
a callable from the model output to a scalar. A callable that sums each row's own target
logit gives every row its own gradient, because the rows of an eval-mode
batch do not interact.
# recipe-test: tool=quantus expected=(4, 1, 8, 8)
import numpy as np
import torch
from torch import nn
import quantus
from torchlens.attribution import input_x_grad, saliency
torch.manual_seed(0)
model = nn.Sequential(
nn.Conv2d(1, 4, kernel_size=3, padding=1), nn.ReLU(),
nn.Flatten(), nn.Linear(4 * 8 * 8, 3),
).eval()
x_batch = np.random.default_rng(0).standard_normal((4, 1, 8, 8)).astype(np.float32)
with torch.no_grad():
y_batch = model(torch.from_numpy(x_batch)).argmax(dim=1).numpy()
def torchlens_explain(model, inputs, targets, method=saliency, **kwargs):
"""Quantus explain_func backed by a TorchLens attribution function."""
x = torch.as_tensor(inputs, dtype=torch.float32)
rows = torch.arange(x.shape[0])
labels = torch.as_tensor(targets, dtype=torch.long)
result = method(model, x, target=lambda out: out[rows, labels].sum())
return result.values.detach().cpu().numpy()
RESULT = torchlens_explain(model, x_batch, y_batch).shape
Run a Quantus metric on it¶
MaxSensitivity perturbs each input with small uniform noise, recomputes the
explanation through explain_func, and reports the largest relative change
(lower is more robust). It draws its noise from numpy's global generator, so
seed that for a repeatable score. Extra explain_func_kwargs reach the
wrapper, which is how one wrapper serves several TorchLens methods.
# recipe-test: tool=quantus expected={'input_x_grad': ((4,), True), 'saliency': ((4,), True)}
metric = quantus.MaxSensitivity(
nr_samples=5, lower_bound=0.1, disable_warnings=True, display_progressbar=False
)
scores = {}
for name, method in [("saliency", saliency), ("input_x_grad", input_x_grad)]:
np.random.seed(0)
values = np.asarray(
metric(
model=model, x_batch=x_batch, y_batch=y_batch, a_batch=None,
channel_first=True, explain_func=torchlens_explain,
explain_func_kwargs={"method": method}, device="cpu",
)
)
scores[name] = (values.shape, bool(np.isfinite(values).all() and (values >= 0).all()))
RESULT = scores
Limits¶
- The scores are Quantus's; TorchLens only produces the attributions. Read each metric's assumptions (baselines, noise scale, normalization) in the Quantus documentation before comparing methods.
- The per-row callable target relies on rows not interacting. A model with batch-dependent layers in the mode you run it (batch norm in training mode, for example) needs one attribution call per row instead.
- Quantus metrics call
explain_funcmany times; slow attribution methods such asintegrated_gradientswith many steps make metrics slow. - Quantus's own torch and tensorflow extras are not needed for this route.