Source code for graph_explain.methods.gradient.grad_x_input
from __future__ import annotations
from typing import Any
import torch
from ...core.explanation import Explanation
from ...core.registry import register
from ..base import ExplanationAlgorithm
[docs]
@register("grad_x_input", "gradient_x_input", "gx")
class GradXInput(ExplanationAlgorithm):
"""Gradient x Input: attribution as gradient scaled by the activation.
The importance of each feature (and of each edge, if the backend supports
edge weights) is the gradient of the target-class logit multiplied by the
input-baseline difference (zero baseline by default). Node importance is the
sum of `abs(grad * Δx)` over features.
"""
graph_level = True
def __init__(
self,
baseline: str = "zero",
edge_grads: bool = True,
node_mask_type: str | None = "attributes",
**kwargs,
):
self.baseline_name = baseline
self.edge_grads = edge_grads
self.node_mask_type = node_mask_type
def explain(
self,
backend: Any,
model: Any,
data: Any,
index: int | list[int] | torch.Tensor | None = None,
target_class: int | None = None,
**kwargs,
) -> Explanation:
model.eval()
x = backend.node_features(data)
edge_index = backend.edge_index(data)
num_nodes = int(x.size(0))
nodes = self._to_node_ids(index, num_nodes)
with torch.no_grad():
logits = backend.forward(model, x, edge_index)
if logits.dim() != 2:
raise ValueError(
"GradXInput requiere predicciones node-level (logits (N, C))."
)
target = target_class
if target is None:
target = int(logits[nodes[0]].argmax().item())
target_cls = max(0, min(int(target), logits.size(1) - 1))
x_in = x.detach().clone().requires_grad_(True)
ew = None
compute_edge = self.edge_grads and backend.supports_edge_weight(model)
if compute_edge:
ew = torch.ones(
edge_index.size(1), dtype=torch.float32, device=x.device
).requires_grad_(True)
out = backend.forward(model, x_in, edge_index, edge_weight=ew)
if out.dim() != 2:
raise ValueError("GradXInput requiere predicciones node-level.")
score = out[nodes, target_cls].sum()
model.zero_grad()
score.backward()
grad_x = x_in.grad.detach()
baseline = torch.zeros_like(x_in)
contrib = grad_x * (x_in - baseline)
node_importance = contrib.abs().sum(dim=-1)
feature_importance = contrib.detach()
edge_importance = None
if compute_edge and ew is not None and ew.grad is not None:
edge_importance = ew.grad.detach().abs()
return Explanation(
node_importance=node_importance.cpu(),
edge_importance=(
edge_importance.cpu() if edge_importance is not None else None
),
feature_importance=(
feature_importance.cpu()
if self.node_mask_type == "attributes"
else None
),
prediction_original=logits[nodes[0]].detach().cpu(),
prediction_explanation=None,
node_idx=int(nodes[0]) if nodes.shape[0] == 1 else None,
target_class=target_cls,
)
@staticmethod
def _to_node_ids(index, num_nodes: int) -> torch.Tensor:
if index is None:
return torch.zeros(1, dtype=torch.long)
if isinstance(index, int):
return torch.tensor([index], dtype=torch.long)
idx = torch.as_tensor(index, dtype=torch.long)
return idx.reshape(-1) if idx.numel() else torch.zeros(1, dtype=torch.long)