Download xai_engine/chefer.py from LPX55/DeepfakeDetection-Explainability: direct link, hf CLI and curl.
- Browser
- Download file 4.26 kB
-
https://huggingface.co/spaces/LPX55/DeepfakeDetection-Explainability/resolve/main/xai_engine/chefer.py
- Command line
-
hf download hf://spaces/LPX55/DeepfakeDetection-Explainability/xai_engine/chefer.py
-
curl -L -o chefer.py https://huggingface.co/spaces/LPX55/DeepfakeDetection-Explainability/resolve/main/xai_engine/chefer.py
4.26 kB
| """ | |
| Gradient-weighted Attention Rollout for Vision Transformers (Chefer et al., CVPR 2021). | |
| Calculates class-discriminative attention maps by weighting attention probabilities with backward gradients. | |
| """ | |
| import torch | |
| import numpy as np | |
| class ViTCheferRollout: | |
| def __init__(self, model): | |
| self.model = model | |
| self.attentions = [] | |
| self.attention_gradients = [] | |
| self.hooks = [] | |
| def _register_hooks(self): | |
| self._remove_hooks() | |
| self.attentions = [] | |
| self.attention_gradients = [] | |
| for name, module in self.model.named_modules(): | |
| if "attention.attention" in name and not hasattr(module, "_chefer_hooked"): | |
| def get_attn_hook(module, input, output): | |
| # output in HF ViT self-attention is (context_layer, attn_probs) | |
| if isinstance(output, tuple) and len(output) > 1: | |
| self.attentions.append(output[1]) | |
| elif hasattr(output, "attentions"): | |
| self.attentions.append(output.attentions) | |
| def get_attn_grad_hook(module, grad_input, grad_output): | |
| # grad_output[1] is gradient w.r.t attention probabilities | |
| if len(grad_output) > 1 and grad_output[1] is not None: | |
| self.attention_gradients.append(grad_output[1]) | |
| h1 = module.register_forward_hook(get_attn_hook) | |
| h2 = module.register_full_backward_hook(get_attn_grad_hook) | |
| self.hooks.extend([h1, h2]) | |
| def _remove_hooks(self): | |
| for h in self.hooks: | |
| h.remove() | |
| self.hooks = [] | |
| def attribute(self, input_tensor, target_class=0): | |
| """ | |
| Compute Chefer Gradient-Weighted Attention Rollout. | |
| Args: | |
| input_tensor: (1, 3, 384, 384) preprocessed input tensor | |
| target_class: output index (0 for single binary logit) | |
| Returns: | |
| 24x24 numpy array of patch attributions | |
| """ | |
| self.model.eval() | |
| self._register_hooks() | |
| device = input_tensor.device | |
| # Enable gradients for backward pass | |
| input_tensor.requires_grad_(True) | |
| self.model.zero_grad() | |
| # Forward pass with output_attentions=True | |
| outputs = self.model(input_tensor, output_attentions=True) | |
| logits = outputs.logits # (1, 1) | |
| # Backward pass on logit | |
| if logits.dim() > 1 and logits.size(1) > target_class: | |
| score = logits[:, target_class] | |
| else: | |
| score = logits.squeeze() | |
| score.backward() | |
| attentions = outputs.attentions # Tuple of 12 (B, heads, 577, 577) tensors | |
| # Cleanup hooks | |
| self._remove_hooks() | |
| if not attentions: | |
| # Fallback if hooks failed to catch: return flat matrix | |
| return np.zeros((24, 24)) | |
| seq_len = attentions[0].size(-1) # 577 | |
| result = torch.eye(seq_len, device=device) | |
| # Reverse gradient order because backward hook captures in reverse | |
| gradients = self.attention_gradients[::-1] if self.attention_gradients else [None] * len(attentions) | |
| for i, attn in enumerate(attentions): | |
| # attn: (1, heads, 577, 577) | |
| if i < len(gradients) and gradients[i] is not None: | |
| grad = gradients[i] | |
| # Elementwise product: gradient * attention | |
| cam = (grad * attn).clamp(min=0).mean(dim=1) # (1, 577, 577) | |
| else: | |
| # Fallback to plain attention if gradient missing | |
| cam = attn.mean(dim=1) | |
| cam = cam[0] # (577, 577) | |
| cam = cam + torch.eye(seq_len, device=device) | |
| cam = cam / cam.sum(dim=-1, keepdim=True) | |
| result = torch.matmul(cam, result) | |
| # Extract CLS token -> patch tokens | |
| patch_attn = result[0, 1:].reshape(24, 24).detach().cpu().numpy() | |
| return patch_attn | |
| def chefer_gradient_rollout(wrapper, input_tensor) -> np.ndarray: | |
| """Convenience function for running Chefer rollout on ViTWrapper.""" | |
| explainer = ViTCheferRollout(wrapper.model) | |
| return explainer.attribute(input_tensor, target_class=0) | |