f2hf-live / vit_explainer.py
imrnh's picture
Force CPU execution for ViT service to avoid ZeroGPU token intercept
55751f5
Raw History Blame Contribute Delete
34.1 kB
# ==============================================================================
# Vision Transformer Interpretability Service
# Functions below are directly adapted line-for-line from:
# draft/f2hf_ablation_study.py (ICCIT paper codebase)
# ==============================================================================
import io
import json
import os
import math
import warnings
import base64
import collections.abc
from itertools import repeat
from functools import partial
from typing import Dict, List, Optional, Tuple, Any
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from PIL import Image
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import torchvision.transforms as transforms
# Optional einops with native PyTorch fallback
try:
from einops import rearrange
except ImportError:
def rearrange(tensor, pattern, **kwargs):
if pattern == 'b n (qkv h d) -> qkv b h n d':
h = kwargs['h']
qkv = kwargs['qkv']
b, n, total_dim = tensor.shape
d = total_dim // (qkv * h)
return tensor.reshape(b, n, qkv, h, d).permute(2, 0, 3, 1, 4)
elif pattern == 'b h n d -> b n (h d)':
b, h, n, d = tensor.shape
return tensor.permute(0, 2, 1, 3).reshape(b, n, h * d)
raise NotImplementedError(f"Pattern {pattern} not implemented in fallback")
# ------------------------------------------------------------------------------
# Lines 467-476 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
def _ntuple(n):
def parse(x):
if isinstance(x, collections.abc.Iterable):
return x
return tuple(repeat(x, n))
return parse
to_2tuple = _ntuple(2)
# ------------------------------------------------------------------------------
# Lines 622-676 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
def _no_grad_trunc_normal_(tensor, mean, std, a, b):
def norm_cdf(x):
return (1. + math.erf(x / math.sqrt(2.))) / 2.
if (mean < a - 2 * std) or (mean > b + 2 * std):
warnings.warn("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
"The distribution of values may be incorrect.",
stacklevel=2)
with torch.no_grad():
l = norm_cdf((a - mean) / std)
u = norm_cdf((b - mean) / std)
tensor.uniform_(2 * l - 1, 2 * u - 1)
tensor.erfinv_()
tensor.mul_(std * math.sqrt(2.))
tensor.add_(mean)
tensor.clamp_(min=a, max=b)
return tensor
def trunc_normal_(tensor, mean=0., std=1., a=-2., b=2.):
return _no_grad_trunc_normal_(tensor, mean, std, a, b)
# ------------------------------------------------------------------------------
# Lines 322-329 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
def _conv_filter(state_dict, patch_size=16):
""" convert patch embedding weight from manual patchify + linear proj to conv"""
out_dict = {}
for k, v in state_dict.items():
if 'patch_embed.proj.weight' in k:
if len(v.shape) == 2:
v = v.reshape((v.shape[0], 3, patch_size, patch_size))
out_dict[k] = v
return out_dict
# ------------------------------------------------------------------------------
# Lines 680-701 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
class PatchEmbed(nn.Module):
""" Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768):
super().__init__()
img_size = to_2tuple(img_size)
patch_size = to_2tuple(patch_size)
num_patches = (img_size[1] // patch_size[1]) * (img_size[0] // patch_size[0])
self.img_size = img_size
self.patch_size = patch_size
self.num_patches = num_patches
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
def forward(self, x):
B, C, H, W = x.shape
assert H == self.img_size[0] and W == self.img_size[1], \
f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
x = self.proj(x).flatten(2).transpose(1, 2)
return x
# ------------------------------------------------------------------------------
# Lines 705-746 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Linear(in_features, hidden_features)
self.act = act_layer()
self.fc2 = nn.Linear(hidden_features, out_features)
self.drop = nn.Dropout(drop)
def save_fc1_gradient(self, fc1_grad):
self.fc1_gradient = fc1_grad
def get_fc1_gradient(self):
return self.fc1_gradient
def save_fc2_gradient(self, fc2_grad):
self.fc2_gradient = fc2_grad
def get_fc2_gradient(self):
return self.fc2_gradient
def save_act_gradient(self, act_grad):
self.act_gradient = act_grad
def get_act_gradient(self):
return self.act_gradient
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
# ------------------------------------------------------------------------------
# Lines 750-835 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
class Attention(nn.Module):
def __init__(self, dim, num_heads=8, qkv_bias=False, attn_drop=0., proj_drop=0., verbose=False):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim ** -0.5
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
self.attn_drop = nn.Dropout(attn_drop)
self.proj = nn.Linear(dim, dim)
self.proj_drop = nn.Dropout(proj_drop)
self.attn_gradients = None
self.attention_map = None
self.vproj = None
self.input = None
def save_input(self, z):
self.input = z
def get_input(self):
return self.input
def save_attn_gradients(self, attn_gradients):
self.attn_gradients = attn_gradients
def get_attn_gradients(self):
return self.attn_gradients
def save_attention_map(self, attention_map):
self.attention_map = attention_map
def get_attention_map(self):
return self.attention_map
def forward(self, x, register_hook=False):
b, n, _ = x.shape
h = self.num_heads
self.save_input(x)
qkv = self.qkv(x)
q, k, v = rearrange(qkv, 'b n (qkv h d) -> qkv b h n d', qkv=3, h=h)
W_qkv = self.qkv.weight
dim = x.shape[-1]
W_v = W_qkv[2 * dim : 3 * dim, :]
self.W_v = W_v
self.W_proj = self.proj.weight
dots = torch.einsum('bhid,bhjd->bhij', q, k) * self.scale
attn = dots.softmax(dim=-1)
attn = self.attn_drop(attn)
attention_score = torch.einsum('bhij,bhjd->bhid', attn, v)
self.save_attention_map(attn)
if register_hook:
attn.register_hook(self.save_attn_gradients)
attention_score_rearranged = rearrange(attention_score, 'b h n d -> b n (h d)')
out = self.proj(attention_score_rearranged)
out = self.proj_drop(out)
self.vproj = torch.matmul(rearrange(v, 'b h n d -> b n (h d)'), self.proj.weight.t())
return out
# ------------------------------------------------------------------------------
# Lines 840-892 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
class Block(nn.Module):
def __init__(self, dim, num_heads, mlp_ratio=4., qkv_bias=False, drop=0., attn_drop=0., act_layer=nn.GELU, norm_layer=nn.LayerNorm, block_index=None, verbose=False):
super().__init__()
self.norm1 = norm_layer(dim)
self.attn = Attention(dim, num_heads=num_heads, qkv_bias=qkv_bias, attn_drop=attn_drop, proj_drop=drop, verbose=verbose)
self.norm2 = norm_layer(dim)
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
self.input = None
self.output = None
self.tilde = None
self.block_index = block_index
self.verbose = verbose
def save_input(self, z):
self.input = z
def get_input(self):
return self.input
def save_tilde(self, z):
self.tilde = z
def get_tilde(self):
return self.tilde
def save_output(self, z):
self.output = z
def get_output(self):
return self.output
def forward(self, x, register_hook=False):
self.save_input(x)
attn_out = self.attn(self.norm1(x), register_hook=register_hook)
out1 = x + attn_out
self.save_tilde(out1)
mlp_out = self.mlp(self.norm2(out1))
out = out1 + mlp_out
self.save_output(out)
return out
# ------------------------------------------------------------------------------
# Lines 896-997 of draft/f2hf_ablation_study.py
# ------------------------------------------------------------------------------
class VisionTransformer(nn.Module):
""" Vision Transformer
"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, num_classes=1000, embed_dim=768, depth=12,
num_heads=12, mlp_ratio=4., qkv_bias=False, drop_rate=0., attn_drop_rate=0., norm_layer=nn.LayerNorm,
global_pooling=False, dino=False, verbose=False):
super().__init__()
self.global_pooling = global_pooling
self.num_classes = num_classes
self.num_features = self.embed_dim = embed_dim
self.patch_embed = PatchEmbed(
img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim)
num_patches = self.patch_embed.num_patches
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
self.pos_drop = nn.Dropout(p=drop_rate)
self.blocks = nn.ModuleList([
Block(
dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias,
drop=drop_rate, attn_drop=attn_drop_rate, norm_layer=norm_layer, block_index=i, verbose=verbose)
for i in range(depth)])
if global_pooling:
self.fc_norm = norm_layer(embed_dim)
else:
self.norm = norm_layer(embed_dim)
if dino:
self.head = nn.Linear(embed_dim * 2, num_classes) if num_classes > 0 else nn.Identity()
else:
self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity()
self.cls_gradients = None
self.input_grad = None
self.dino = dino
trunc_normal_(self.pos_embed, std=.02)
trunc_normal_(self.cls_token, std=.02)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
def forward(self, x, register_hook=False):
B = x.shape[0]
x = self.patch_embed(x)
cls_tokens = self.cls_token.expand(B, -1, -1)
x = torch.cat((cls_tokens, x), dim=1)
x = x + self.pos_embed
x = self.pos_drop(x)
for blk in self.blocks[:-1]:
x = blk(x, register_hook=register_hook)
x = self.blocks[-1](x, register_hook=register_hook)
if self.global_pooling:
x = x[:, 1:, :].mean(axis=1)
x = self.fc_norm(x)
elif self.dino:
x = self.norm(x)
x = torch.cat((x[:, 0].unsqueeze(-1), x[:, 1:, :].mean(axis=1).unsqueeze(-1)), dim=-1)
x = x.reshape(x.shape[0], -1)
else:
x = self.norm(x)
x = x[:, 0]
x = self.head(x)
return x
# ------------------------------------------------------------------------------
# Lines 1145-1187 of draft/f2hf_ablation_study.py (Attention Rollout)
# ------------------------------------------------------------------------------
def compute_rollout_attention(all_layer_matrices, start_layer=0):
num_tokens = all_layer_matrices[0].shape[1]
batch_size = all_layer_matrices[0].shape[0]
eye = torch.eye(num_tokens).expand(batch_size, num_tokens, num_tokens).to(all_layer_matrices[0].device)
all_layer_matrices = [all_layer_matrices[i] + eye for i in range(len(all_layer_matrices))]
matrices_aug = [all_layer_matrices[i] / all_layer_matrices[i].sum(dim=-1, keepdim=True)
for i in range(len(all_layer_matrices))]
joint_attention = matrices_aug[start_layer]
for i in range(start_layer+1, len(matrices_aug)):
joint_attention = matrices_aug[i].bmm(joint_attention)
return joint_attention
def generate_rollout(model, input_tensor, device=None, start_layer=0, mae=False):
input_tensor = input_tensor.to(device)
out = model(input_tensor)
all_layer_attentions = []
for blk in model.blocks:
attn = blk.attn.get_attention_map()
attn_avg = attn.mean(dim=1).detach()
all_layer_attentions.append(attn_avg)
rollout = compute_rollout_attention(all_layer_attentions, start_layer=start_layer)
if mae:
return rollout[:, 1:, 1:].mean(1)
else:
return rollout[:, 0, 1:]
# ------------------------------------------------------------------------------
# Lines 1050-1141 of draft/f2hf_ablation_study.py (F2HF & BeyondIntuition)
# ------------------------------------------------------------------------------
def beyond_intuition_tokenwise(model, x, device, onehot=None, ffd_consideration=False, index=None, steps=20, start_layer=1, samples=20, noise=0.1, mae=False, ssl=False, dino=False, taken_head_idx = None):
b = x.shape[0]
batch_size = b
x = x.to(device)
output = model(x)
logit_score = output
if index is None:
index = np.argmax(output.cpu().data.numpy(), axis=-1)
elif isinstance(index, int):
index = np.array([index])
out_sum = torch.sum(output)
one_hot = np.zeros((batch_size, output.size()[-1]), dtype=np.float32)
one_hot[np.arange(batch_size), index] = 1
one_hot = torch.from_numpy(one_hot).requires_grad_(True).to(device)
one_hot = torch.sum(one_hot)
model.zero_grad()
one_hot.backward(retain_graph=True)
_, num_head, num_tokens, _ = model.blocks[-1].attn.get_attention_map().shape
R = torch.eye(num_tokens, num_tokens).expand(b, num_tokens, num_tokens).to(device)
for blk_idx, blk in enumerate(model.blocks):
if blk_idx < start_layer - 1:
continue
# Calculate alpha in the paper
fc2_w = blk.mlp.fc2.weight
h_dim = fc2_w.shape[1] // num_head
impw = torch.abs(fc2_w.reshape((num_head, h_dim, -1))).mean(2).mean(1).unsqueeze(1).unsqueeze(2)
z = blk.get_input()
vproj = blk.attn.vproj
order = torch.linalg.norm(vproj, dim=-1).squeeze() / (torch.linalg.norm(z, dim=-1).squeeze() + 1e-8)
m = torch.diag_embed(order)
cam = blk.attn.get_attention_map()
cam = cam.reshape(-1, cam.shape[-2], cam.shape[-1])
if taken_head_idx is None:
if ffd_consideration:
cam = cam * impw
cam = cam.mean(0)
else:
cam = cam[taken_head_idx]
O_t = torch.matmul(cam.to(device), m.to(device))
R = R + torch.matmul(O_t, R.to(device))
if ssl:
if mae:
return R[:, 1:, 1:].abs().mean(axis=1), logit_score
elif dino:
return R[:, 1:, 1:].abs().mean(axis=1) + R[:, 0, 1:].abs(), logit_score
else:
return R[:, 0, 1:].abs(), logit_score
total_gradients = torch.zeros(b, num_head, num_tokens, num_tokens, device=device)
for alpha in np.linspace(0, 1, steps):
data_scaled = x * alpha
output = model(data_scaled, register_hook=True)
one_hot = np.zeros((b, output.size()[-1]), dtype=np.float32)
one_hot[np.arange(b), index] = 1
one_hot = torch.from_numpy(one_hot).requires_grad_(True).to(device)
one_hot = torch.sum(one_hot * output)
model.zero_grad()
one_hot.backward(retain_graph=True)
gradients = model.blocks[-1].attn.get_attn_gradients()
if gradients is not None:
total_gradients += gradients
W_state = (total_gradients / steps).clamp(min=0).mean(1).reshape(b, num_tokens, num_tokens)
R = W_state * R.abs()
if mae:
R_MAE = R[:, 1:, 1:].mean(axis=1)
return (R_MAE, logit_score)
elif dino:
R_DINO = (R[:, 1:, 1:].mean(axis=1) + R[:, 0, 1:])
return (R_DINO, logit_score)
else:
R_general = R[:, 0, 1:]
return (R_general, logit_score)
# ------------------------------------------------------------------------------
# Lines 1316-1364 of draft/f2hf_ablation_study.py (Integrated Gradients)
# ------------------------------------------------------------------------------
def integrated_gradient(model, x, device, index=None, steps=20, mae=False, dino=False, ssl=False):
b = x.shape[0]
batch_size = b
x = x.to(device)
output = model(x)
if index is None:
index = np.argmax(output.cpu().data.numpy(), axis=-1)
elif isinstance(index, int):
index = np.array([index])
one_hot = np.zeros((batch_size, output.size()[-1]), dtype=np.float32)
one_hot[np.arange(batch_size), index] = 1
one_hot = torch.from_numpy(one_hot).requires_grad_(True).to(device)
one_hot = torch.sum(one_hot)
model.zero_grad()
one_hot.backward(retain_graph=True)
_, num_head, num_tokens, _ = model.blocks[-1].attn.get_attention_map().shape
total_gradients = torch.zeros(b, num_head, num_tokens, num_tokens, device=device)
for alpha in np.linspace(0, 1, steps):
data_scaled = x * alpha
output = model(data_scaled, register_hook=True)
one_hot = np.zeros((b, output.size()[-1]), dtype=np.float32)
one_hot[np.arange(b), index] = 1
one_hot = torch.from_numpy(one_hot).requires_grad_(True).to(device)
one_hot = torch.sum(one_hot * output)
model.zero_grad()
one_hot.backward(retain_graph=True)
gradients = model.blocks[-1].attn.get_attn_gradients()
if gradients is not None:
total_gradients += gradients
W_state = (total_gradients / steps).clamp(min=0).mean(1).reshape(b, num_tokens, num_tokens)
R = W_state
if mae:
R_MAE = R[:, 1:, 1:].mean(axis=1)
return R_MAE
elif dino:
R_DINO = (R[:, 1:, 1:].mean(axis=1) + R[:, 0, 1:])
return R_DINO
else:
R_general = R[:, 0, 1:]
return R_general
# ------------------------------------------------------------------------------
# Lines 1455-1487 of draft/f2hf_ablation_study.py (Grad-CAM)
# ------------------------------------------------------------------------------
def gradcam(model, x, device, target_cls_idx=None, mae=False):
x = x.to(device)
output = model(x, register_hook=True)
if target_cls_idx is None:
target_cls_idx = np.argmax(output.cpu().data.numpy())
one_hot = np.zeros((1, output.size()[-1]), dtype=np.float32)
one_hot[0][target_cls_idx] = 1
one_hot = torch.from_numpy(one_hot).requires_grad_(True).to(device)
one_hot = torch.sum(one_hot * output)
model.zero_grad()
one_hot.backward(retain_graph=True)
grad = model.blocks[-1].attn.get_attn_gradients().to(device)
cam = model.blocks[-1].attn.get_attention_map().to(device)
if mae:
cam = cam[0, :, 1:, 1:]
grad = grad[0, :, 1:, 1:]
grad = grad.mean(dim=[0, 2], keepdim=True)
cam = (cam * grad).mean(0).mean(1).clamp(min=0)
cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)
else:
cam = cam[0, :, 0, 1:].reshape(-1, 14, 14)
grad = grad[0, :, 0, 1:].reshape(-1, 14, 14)
grad = grad.mean(dim=[1, 2], keepdim=True)
cam = (cam * grad).mean(0).clamp(min=0)
cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)
return cam
def normalize_class_name(s: str) -> str:
import re
# Remove WordNet synset prefix if present (e.g., "n01818515_ruffed_grouse" -> "ruffed_grouse")
s = re.sub(r'^n\d{8}[_\s]+', '', str(s).strip().lower())
# Replace underscores, hyphens, and multiple spaces with a single space
return re.sub(r'[\s_\-]+', ' ', s).strip()
def compact_class_name(s: str) -> str:
import re
return re.sub(r'[^a-z0-9]', '', str(s).lower())
# ==============================================================================
# Service Wrapper for Local Pretrained Weights & Web API
# ==============================================================================
class ViTExplanationService:
def __init__(self, base_dir: str = "."):
self.base_dir = base_dir
# In Hugging Face ZeroGPU, PyTorch CUDA is mocked. Run inference on CPU to avoid ZeroGPU token intercept.
if os.environ.get("SPACES_ZERO_GPU") or os.environ.get("FORCE_CPU"):
self.device = torch.device("cpu")
else:
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.models: Dict[str, VisionTransformer] = {}
self.id2label: Dict[int, str] = {}
self.label2id: Dict[str, int] = {}
self.norm_label2id: Dict[str, int] = {}
self.compact_label2id: Dict[str, int] = {}
self._load_classes()
# Image transform from line 1023-1027 of draft/f2hf_ablation_study.py
self.transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
def _index_label(self, idx: int, label_str: str):
self.id2label[idx] = label_str
candidates = [label_str] + [p.strip() for p in label_str.split(",") if p.strip()]
for cand in candidates:
raw_l = cand.lower()
self.label2id[raw_l] = idx
self.label2id[cand.replace(" ", "_").lower()] = idx
norm = normalize_class_name(cand)
if norm:
self.norm_label2id[norm] = idx
comp = compact_class_name(cand)
if comp:
self.compact_label2id[comp] = idx
def _load_classes(self):
possible_paths = [
os.path.join(self.base_dir, "draft", "vit_config.json"),
os.path.join(self.base_dir, "vit_config.json"),
os.path.join(self.base_dir, "static", "classes.json")
]
loaded = False
for p in possible_paths:
if os.path.exists(p):
try:
with open(p, "r") as f:
data = json.load(f)
if "id2label" in data:
for k, v in data["id2label"].items():
self._index_label(int(k), v)
loaded = True
break
elif isinstance(data, list):
for item in data:
self._index_label(int(item["index"]), item["name"])
loaded = True
break
except Exception as e:
print(f"Notice: error reading {p}: {e}")
if not loaded:
for i in range(1000):
self._index_label(i, f"Class {i}")
def get_classes_list(self) -> List[Dict[str, Any]]:
return [{"index": i, "name": self.id2label.get(i, f"Class {i}")} for i in range(1000)]
def resolve_class_index(self, query: str, query_type: str = "auto") -> Tuple[int, str]:
query_clean = str(query).strip()
if not query_clean:
return 0, self.id2label.get(0, "tench")
if query_type == "number" or (query_type == "auto" and query_clean.isdigit()):
idx = int(query_clean)
idx = max(0, min(999, idx))
return idx, self.id2label.get(idx, f"Class {idx}")
q_lower = query_clean.lower()
q_norm = normalize_class_name(query_clean)
q_comp = compact_class_name(query_clean)
# 1. Exact match in raw, normalized, or compact lookup
if q_lower in self.label2id:
idx = self.label2id[q_lower]
return idx, self.id2label[idx]
if q_norm in self.norm_label2id:
idx = self.norm_label2id[q_norm]
return idx, self.id2label[idx]
if q_comp in self.compact_label2id:
idx = self.compact_label2id[q_comp]
return idx, self.id2label[idx]
# 2. Substring matching across normalized dictionary
for name, idx in self.norm_label2id.items():
if q_norm in name or name in q_norm:
return idx, self.id2label[idx]
# 3. Substring matching across compact dictionary
for comp, idx in self.compact_label2id.items():
if q_comp in comp or comp in q_comp:
return idx, self.id2label[idx]
return 0, self.id2label.get(0, "tench")
def get_model(self, model_key: str = "vit_base_patch16_224") -> VisionTransformer:
if model_key in self.models:
return self.models[model_key]
models_dir = os.path.join(self.base_dir, "models")
if model_key == "vit_small_patch16_224":
model = VisionTransformer(
patch_size=16, embed_dim=768, depth=8, num_classes=1000, num_heads=6,
mlp_ratio=3, qkv_bias=False, norm_layer=partial(nn.LayerNorm, eps=1e-6)
)
ckpt_path = os.path.join(models_dir, "vit_small_p16_224-15ec54c9.pth")
else:
model = VisionTransformer(
patch_size=16, embed_dim=768, depth=12, num_classes=1000, num_heads=12,
mlp_ratio=4, qkv_bias=True, norm_layer=partial(nn.LayerNorm, eps=1e-6)
)
ckpt_path = os.path.join(models_dir, "jx_vit_base_p16_224-80ecf9dd.pth")
if os.path.exists(ckpt_path):
state = torch.load(ckpt_path, map_location="cpu")
state = _conv_filter(state)
model.load_state_dict(state, strict=False)
model.to(self.device)
model.eval()
self.models[model_key] = model
return model
def _render_heatmaps(self, raw_map: torch.Tensor, original_pil: Image.Image) -> Tuple[str, str]:
arr = raw_map.detach().cpu().numpy()
if arr.size == 196:
arr = arr.reshape(14, 14)
elif arr.shape[-2:] != (14, 14):
arr = arr.reshape(14, 14)
# 1. Raw saliency map with Reds colormap (matching draft/f2hf_ablation_study.py)
# Directly streamed from an in-memory RAM buffer — never written to disk
fig, ax = plt.subplots(figsize=(3, 3))
ax.imshow(arr, cmap="Reds")
ax.axis("off")
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
raw_buf = io.BytesIO()
fig.savefig(raw_buf, format="png", bbox_inches="tight", pad_inches=0)
plt.close(fig)
saliency_base64 = "data:image/png;base64," + base64.b64encode(raw_buf.getvalue()).decode("utf-8")
# 2. Superimposed heatmap on top of original image (seismic overlay)
h_min, h_max = float(arr.min()), float(arr.max())
if h_max > h_min:
norm_map = (arr - h_min) / (h_max - h_min)
else:
norm_map = np.zeros_like(arr)
norm_tensor = torch.from_numpy(norm_map).unsqueeze(0).unsqueeze(0).float()
upsampled = F.interpolate(norm_tensor, size=(224, 224), mode='bilinear', align_corners=False).squeeze().numpy()
seismic_rgba = cm.seismic(upsampled)
seismic_rgb = (seismic_rgba[:, :, :3] * 255).astype(np.uint8)
orig_resized = original_pil.resize((224, 224)).convert("RGB")
orig_arr = np.array(orig_resized)
blended = (0.55 * orig_arr + 0.45 * seismic_rgb).astype(np.uint8)
overlay_buf = io.BytesIO()
Image.fromarray(blended).save(overlay_buf, format="PNG")
overlay_base64 = "data:image/png;base64," + base64.b64encode(overlay_buf.getvalue()).decode("utf-8")
return saliency_base64, overlay_base64
def run_simulation(
self,
pil_image: Image.Image,
class_query: str,
query_type: str = "auto",
model_name: str = "vit_base_patch16_224",
selected_methods: Optional[List[str]] = None
) -> Dict[str, Any]:
if selected_methods is None or len(selected_methods) == 0:
selected_methods = ["f2hf", "beyond_intuition", "integrated_gradients", "gradcam", "rollout"]
model = self.get_model(model_name)
input_tensor = self.transform(pil_image).unsqueeze(0).to(self.device)
with torch.no_grad():
logits = model(input_tensor)
probs = F.softmax(logits, dim=-1).squeeze().cpu().numpy()
top5_indices = np.argsort(probs)[::-1][:5]
top5 = [
{"index": int(i), "name": self.id2label.get(int(i), f"Class {i}"), "score": float(probs[i])}
for i in top5_indices
]
pred_top1 = top5[0]
target_idx, target_name = self.resolve_class_index(class_query, query_type)
heatmaps: Dict[str, Any] = {}
# 1. F2HF: beyond_intuition_tokenwise with ffd_consideration=True
if "f2hf" in selected_methods:
h_f2hf, _ = beyond_intuition_tokenwise(model, input_tensor, self.device, index=target_idx, ffd_consideration=True)
sal_b64, over_b64 = self._render_heatmaps(h_f2hf, pil_image)
heatmaps["f2hf"] = {
"id": "f2hf",
"name": "F2HF",
"badge": "Proposed Method",
"description": "Feed-Forward Network Filtered Head Attention",
"image": sal_b64,
"saliency_image": sal_b64,
"overlay_image": over_b64
}
# 2. BeyondIntuition: beyond_intuition_tokenwise with ffd_consideration=False, start_layer=1
if "beyond_intuition" in selected_methods:
h_bi, _ = beyond_intuition_tokenwise(model, input_tensor, self.device, index=target_idx, ffd_consideration=False, start_layer=1)
sal_b64, over_b64 = self._render_heatmaps(h_bi, pil_image)
heatmaps["beyond_intuition"] = {
"id": "beyond_intuition",
"name": "BeyondIntuition",
"badge": "Baseline",
"description": "Tokenwise Self-Attention Attribution",
"image": sal_b64,
"saliency_image": sal_b64,
"overlay_image": over_b64
}
# 3. Integrated Gradients
if "integrated_gradients" in selected_methods:
h_ig = integrated_gradient(model, input_tensor, self.device, index=target_idx, steps=10)
sal_b64, over_b64 = self._render_heatmaps(h_ig, pil_image)
heatmaps["integrated_gradients"] = {
"id": "integrated_gradients",
"name": "Integrated Gradients",
"badge": "Baseline",
"description": "Path-Integrated Head Attentions",
"image": sal_b64,
"saliency_image": sal_b64,
"overlay_image": over_b64
}
# 4. Grad-CAM
if "gradcam" in selected_methods:
h_gcam = gradcam(model, input_tensor, self.device, target_cls_idx=target_idx)
sal_b64, over_b64 = self._render_heatmaps(h_gcam, pil_image)
heatmaps["gradcam"] = {
"id": "gradcam",
"name": "Grad-CAM",
"badge": "Baseline",
"description": "Attention Map Weighted by Target Gradients",
"image": sal_b64,
"saliency_image": sal_b64,
"overlay_image": over_b64
}
# 5. Attention Rollout
if "rollout" in selected_methods:
h_roll = generate_rollout(model, input_tensor, device=self.device, start_layer=1)
sal_b64, over_b64 = self._render_heatmaps(h_roll, pil_image)
heatmaps["rollout"] = {
"id": "rollout",
"name": "Attention Rollout",
"badge": "Baseline",
"description": "Joint Inter-Layer Attention Flow",
"image": sal_b64,
"saliency_image": sal_b64,
"overlay_image": over_b64
}
thumb = pil_image.copy()
thumb.thumbnail((300, 300))
orig_buf = io.BytesIO()
thumb.save(orig_buf, format="PNG")
orig_base64 = "data:image/png;base64," + base64.b64encode(orig_buf.getvalue()).decode("utf-8")
return {
"prediction": pred_top1,
"top5": top5,
"target": {"index": target_idx, "name": target_name},
"model_name": model_name,
"original_image": orig_base64,
"heatmaps": heatmaps
}