Spaces:
Sleeping
Sleeping
Commit ·
707f439
1
Parent(s): 202cb33
Crop padding patches from SigLIP attention heatmaps
Browse filesresize_with_pad pads on top/left, creating pure-padding patch rows/cols
that distort attention heatmaps after baseline subtraction. This adds a
post-processing crop that removes padding patches before upsampling:
- Add compute_padding_patches() helper to calculate top/left pad counts
- Add content_crop param to attention_to_heatmap() to slice off padding
- Compute crop from vision encoder config and thread through self-attn
and per-head grid paths (cross-attention uses post-connector grid, unaffected)
- Fix preprocessing to use resize_with_pad + [-1,1] normalization matching
the real SigLIP pipeline, including for positional baseline computation
- inspect_attention.py +86 -20
inspect_attention.py
CHANGED
|
@@ -75,6 +75,25 @@ import matplotlib.gridspec as gridspec
|
|
| 75 |
from PIL import Image
|
| 76 |
from matplotlib.colors import LinearSegmentedColormap
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
_CYAN_CMAP = LinearSegmentedColormap.from_list("cyan", ["black", "cyan", "white"])
|
| 79 |
|
| 80 |
|
|
@@ -534,7 +553,21 @@ class GradCAMFallback:
|
|
| 534 |
# 3. Attention-to-heatmap conversion
|
| 535 |
# ---------------------------------------------------------------------------
|
| 536 |
|
| 537 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
"""
|
| 539 |
Convert attention weights from patch-space to pixel-space heatmap.
|
| 540 |
|
|
@@ -565,7 +598,13 @@ def attention_to_heatmap(attn_weights, grid_size, image_size):
|
|
| 565 |
attn_2d = attn_weights.reshape(h_patches, w_patches)
|
| 566 |
else:
|
| 567 |
attn_2d = attn_weights
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
# Upsample to image resolution using bilinear interpolation
|
| 570 |
attn_2d = attn_2d.float().unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
|
| 571 |
heatmap = F.interpolate(attn_2d, size=(h_img, w_img), mode="bilinear", align_corners=False)
|
|
@@ -658,7 +697,8 @@ def compute_attention_rollout(all_layer_attentions):
|
|
| 658 |
return rollout
|
| 659 |
|
| 660 |
|
| 661 |
-
def compute_positional_baseline(vision_encoder, attn_capture, device, method
|
|
|
|
| 662 |
"""
|
| 663 |
Compute the attention pattern produced by a content-free (mean-gray)
|
| 664 |
image. This captures the fixed positional component of attention so
|
|
@@ -672,6 +712,10 @@ def compute_positional_baseline(vision_encoder, attn_capture, device, method):
|
|
| 672 |
device: Torch device.
|
| 673 |
method: ``"last-layer"`` or ``"rollout"`` — same aggregation used
|
| 674 |
for real frames so the baseline is comparable.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
|
| 676 |
Returns:
|
| 677 |
Tensor of shape ``(num_patches,)`` — per-patch baseline scores.
|
|
@@ -681,7 +725,15 @@ def compute_positional_baseline(vision_encoder, attn_capture, device, method):
|
|
| 681 |
getattr(vision_encoder, "config", None), "image_size", None,
|
| 682 |
) or getattr(vision_encoder, "image_size", 384)
|
| 683 |
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
try:
|
| 686 |
enc_dtype = next(vision_encoder.parameters()).dtype
|
| 687 |
gray = gray.to(enc_dtype)
|
|
@@ -928,7 +980,7 @@ def save_individual_frames(frames, heatmaps, output_dir, episode_idx=0):
|
|
| 928 |
|
| 929 |
|
| 930 |
def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
| 931 |
-
output_path="per_head_attention.png"):
|
| 932 |
"""
|
| 933 |
Visualise each attention head's pattern individually for a single
|
| 934 |
frame. Useful for identifying specialised heads (e.g. one tracking
|
|
@@ -965,7 +1017,7 @@ def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
|
| 965 |
r, c = divmod(h, cols)
|
| 966 |
head_attn = attn_weights[h] # (patches, patches)
|
| 967 |
scores = head_attn.mean(dim=0) # per-patch importance
|
| 968 |
-
hmap = attention_to_heatmap(scores, grid_size, image_size)
|
| 969 |
|
| 970 |
blended = overlay_heatmap(frame_np.copy(), hmap, alpha=0.5)
|
| 971 |
axes[r, c].imshow(blended)
|
|
@@ -1328,17 +1380,6 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1328 |
print(f" WARNING: Could not set up cross-attention capture: {e}")
|
| 1329 |
cross_capture = None
|
| 1330 |
|
| 1331 |
-
# --- Compute positional baseline (once) ---
|
| 1332 |
-
baseline_scores = None
|
| 1333 |
-
if not raw_attention:
|
| 1334 |
-
baseline_scores = compute_positional_baseline(
|
| 1335 |
-
vision_encoder, attn_capture, device, method,
|
| 1336 |
-
)
|
| 1337 |
-
if baseline_scores is not None:
|
| 1338 |
-
print(" Positional baseline computed (subtracting to reveal content-dependent attention)")
|
| 1339 |
-
else:
|
| 1340 |
-
print(" Could not compute positional baseline, using raw attention")
|
| 1341 |
-
|
| 1342 |
# --- Find image key in dataset ---
|
| 1343 |
if image_key is None:
|
| 1344 |
image_keys = find_image_keys(dataset)
|
|
@@ -1359,6 +1400,29 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1359 |
task_str = _resolve_task_string(first_sample, dataset)
|
| 1360 |
print(f" Task string: \"{task_str}\"")
|
| 1361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1362 |
# --- Run inference and collect attention ---
|
| 1363 |
print(f"\n[4/4] Running forward passes and extracting attention (method={method})...")
|
| 1364 |
frames = []
|
|
@@ -1411,10 +1475,10 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1411 |
getattr(vision_encoder, "config", None), "image_size", None,
|
| 1412 |
) or getattr(vision_encoder, "image_size", 384)
|
| 1413 |
if img.shape[-1] != target_size or img.shape[-2] != target_size:
|
| 1414 |
-
img_resized =
|
| 1415 |
-
mode="bilinear", align_corners=False)
|
| 1416 |
else:
|
| 1417 |
img_resized = img
|
|
|
|
| 1418 |
|
| 1419 |
try:
|
| 1420 |
enc_dtype = next(vision_encoder.parameters()).dtype
|
|
@@ -1507,7 +1571,8 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1507 |
grid_h = grid_w = grid_side
|
| 1508 |
|
| 1509 |
img_h, img_w = img_tensor.shape[1], img_tensor.shape[2]
|
| 1510 |
-
heatmap = attention_to_heatmap(patch_scores, (grid_h, grid_w), (img_h, img_w)
|
|
|
|
| 1511 |
heatmaps.append(heatmap)
|
| 1512 |
|
| 1513 |
print(f" Frame {i}: {n_patches} patches → "
|
|
@@ -1520,6 +1585,7 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1520 |
img_tensor, raw_heads_attn,
|
| 1521 |
(grid_h, grid_w), (img_h, img_w),
|
| 1522 |
output_path=head_path,
|
|
|
|
| 1523 |
)
|
| 1524 |
raw_heads_attn = None # only once
|
| 1525 |
else:
|
|
|
|
| 75 |
from PIL import Image
|
| 76 |
from matplotlib.colors import LinearSegmentedColormap
|
| 77 |
|
| 78 |
+
try:
|
| 79 |
+
from lerobot.policies.smolvla.modeling_smolvla import resize_with_pad
|
| 80 |
+
except ImportError:
|
| 81 |
+
def resize_with_pad(img, width, height, pad_value=-1):
|
| 82 |
+
"""Aspect-ratio-preserving resize with top/left padding."""
|
| 83 |
+
if img.ndim != 4:
|
| 84 |
+
raise ValueError(f"(b,c,h,w) expected, but {img.shape}")
|
| 85 |
+
cur_height, cur_width = img.shape[2:]
|
| 86 |
+
ratio = max(cur_width / width, cur_height / height)
|
| 87 |
+
resized_height = int(cur_height / ratio)
|
| 88 |
+
resized_width = int(cur_width / ratio)
|
| 89 |
+
resized_img = F.interpolate(
|
| 90 |
+
img, size=(resized_height, resized_width), mode="bilinear", align_corners=False,
|
| 91 |
+
)
|
| 92 |
+
pad_height = max(0, int(height - resized_height))
|
| 93 |
+
pad_width = max(0, int(width - resized_width))
|
| 94 |
+
padded_img = F.pad(resized_img, (pad_width, 0, pad_height, 0), value=pad_value)
|
| 95 |
+
return padded_img
|
| 96 |
+
|
| 97 |
_CYAN_CMAP = LinearSegmentedColormap.from_list("cyan", ["black", "cyan", "white"])
|
| 98 |
|
| 99 |
|
|
|
|
| 553 |
# 3. Attention-to-heatmap conversion
|
| 554 |
# ---------------------------------------------------------------------------
|
| 555 |
|
| 556 |
+
def compute_padding_patches(input_hw, target_size, patch_size):
|
| 557 |
+
"""Number of pure-padding patch rows (top) and columns (left)
|
| 558 |
+
produced by resize_with_pad for the given input dimensions."""
|
| 559 |
+
if input_hw is None:
|
| 560 |
+
return (0, 0)
|
| 561 |
+
in_h, in_w = input_hw
|
| 562 |
+
ratio = max(in_w / target_size, in_h / target_size)
|
| 563 |
+
resized_h = int(in_h / ratio)
|
| 564 |
+
resized_w = int(in_w / ratio)
|
| 565 |
+
pad_h = max(0, target_size - resized_h)
|
| 566 |
+
pad_w = max(0, target_size - resized_w)
|
| 567 |
+
return (pad_h // patch_size, pad_w // patch_size)
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def attention_to_heatmap(attn_weights, grid_size, image_size, content_crop=None):
|
| 571 |
"""
|
| 572 |
Convert attention weights from patch-space to pixel-space heatmap.
|
| 573 |
|
|
|
|
| 598 |
attn_2d = attn_weights.reshape(h_patches, w_patches)
|
| 599 |
else:
|
| 600 |
attn_2d = attn_weights
|
| 601 |
+
|
| 602 |
+
# Crop out pure-padding patches (top rows, left columns) before upsampling
|
| 603 |
+
if content_crop is not None:
|
| 604 |
+
crop_h, crop_w = content_crop
|
| 605 |
+
if crop_h > 0 or crop_w > 0:
|
| 606 |
+
attn_2d = attn_2d[crop_h:, crop_w:]
|
| 607 |
+
|
| 608 |
# Upsample to image resolution using bilinear interpolation
|
| 609 |
attn_2d = attn_2d.float().unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
|
| 610 |
heatmap = F.interpolate(attn_2d, size=(h_img, w_img), mode="bilinear", align_corners=False)
|
|
|
|
| 697 |
return rollout
|
| 698 |
|
| 699 |
|
| 700 |
+
def compute_positional_baseline(vision_encoder, attn_capture, device, method,
|
| 701 |
+
input_hw=None):
|
| 702 |
"""
|
| 703 |
Compute the attention pattern produced by a content-free (mean-gray)
|
| 704 |
image. This captures the fixed positional component of attention so
|
|
|
|
| 712 |
device: Torch device.
|
| 713 |
method: ``"last-layer"`` or ``"rollout"`` — same aggregation used
|
| 714 |
for real frames so the baseline is comparable.
|
| 715 |
+
input_hw: Optional ``(H, W)`` of the original input images. When
|
| 716 |
+
provided the baseline gray image is built at this aspect ratio
|
| 717 |
+
then preprocessed with ``resize_with_pad`` so padding patches
|
| 718 |
+
match those in real frames.
|
| 719 |
|
| 720 |
Returns:
|
| 721 |
Tensor of shape ``(num_patches,)`` — per-patch baseline scores.
|
|
|
|
| 725 |
getattr(vision_encoder, "config", None), "image_size", None,
|
| 726 |
) or getattr(vision_encoder, "image_size", 384)
|
| 727 |
|
| 728 |
+
if input_hw is not None:
|
| 729 |
+
# Build gray at original aspect ratio, then resize_with_pad
|
| 730 |
+
# so padding patches match the real frames exactly.
|
| 731 |
+
in_h, in_w = input_hw
|
| 732 |
+
gray_content = torch.full((1, 3, in_h, in_w), 0.5, device=device)
|
| 733 |
+
gray = resize_with_pad(gray_content, img_size, img_size, pad_value=0)
|
| 734 |
+
gray = gray * 2.0 - 1.0 # normalize to [-1, 1] matching SigLIP
|
| 735 |
+
else:
|
| 736 |
+
gray = torch.full((1, 3, img_size, img_size), 0.5, device=device)
|
| 737 |
try:
|
| 738 |
enc_dtype = next(vision_encoder.parameters()).dtype
|
| 739 |
gray = gray.to(enc_dtype)
|
|
|
|
| 980 |
|
| 981 |
|
| 982 |
def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
| 983 |
+
output_path="per_head_attention.png", content_crop=None):
|
| 984 |
"""
|
| 985 |
Visualise each attention head's pattern individually for a single
|
| 986 |
frame. Useful for identifying specialised heads (e.g. one tracking
|
|
|
|
| 1017 |
r, c = divmod(h, cols)
|
| 1018 |
head_attn = attn_weights[h] # (patches, patches)
|
| 1019 |
scores = head_attn.mean(dim=0) # per-patch importance
|
| 1020 |
+
hmap = attention_to_heatmap(scores, grid_size, image_size, content_crop=content_crop)
|
| 1021 |
|
| 1022 |
blended = overlay_heatmap(frame_np.copy(), hmap, alpha=0.5)
|
| 1023 |
axes[r, c].imshow(blended)
|
|
|
|
| 1380 |
print(f" WARNING: Could not set up cross-attention capture: {e}")
|
| 1381 |
cross_capture = None
|
| 1382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1383 |
# --- Find image key in dataset ---
|
| 1384 |
if image_key is None:
|
| 1385 |
image_keys = find_image_keys(dataset)
|
|
|
|
| 1400 |
task_str = _resolve_task_string(first_sample, dataset)
|
| 1401 |
print(f" Task string: \"{task_str}\"")
|
| 1402 |
|
| 1403 |
+
# --- Compute positional baseline (once, after frames are loaded so we
|
| 1404 |
+
# know the input aspect ratio for a properly padded baseline) ---
|
| 1405 |
+
first_img = frame_pairs[0][1] # (C, H, W)
|
| 1406 |
+
input_hw = (first_img.shape[1], first_img.shape[2])
|
| 1407 |
+
|
| 1408 |
+
baseline_scores = None
|
| 1409 |
+
if not raw_attention:
|
| 1410 |
+
baseline_scores = compute_positional_baseline(
|
| 1411 |
+
vision_encoder, attn_capture, device, method, input_hw=input_hw,
|
| 1412 |
+
)
|
| 1413 |
+
if baseline_scores is not None:
|
| 1414 |
+
print(" Positional baseline computed (subtracting to reveal content-dependent attention)")
|
| 1415 |
+
else:
|
| 1416 |
+
print(" Could not compute positional baseline, using raw attention")
|
| 1417 |
+
|
| 1418 |
+
# --- Compute padding-patch crop so heatmaps exclude pad regions ---
|
| 1419 |
+
target_size = getattr(getattr(vision_encoder, "config", None), "image_size", None) or 384
|
| 1420 |
+
patch_size_cfg = getattr(vision_encoder, "patch_size", None) or getattr(
|
| 1421 |
+
getattr(vision_encoder, "config", None), "patch_size", 14)
|
| 1422 |
+
content_crop = compute_padding_patches(input_hw, target_size, patch_size_cfg)
|
| 1423 |
+
if content_crop != (0, 0):
|
| 1424 |
+
print(f" Padding crop: {content_crop[0]} top rows, {content_crop[1]} left cols of patches")
|
| 1425 |
+
|
| 1426 |
# --- Run inference and collect attention ---
|
| 1427 |
print(f"\n[4/4] Running forward passes and extracting attention (method={method})...")
|
| 1428 |
frames = []
|
|
|
|
| 1475 |
getattr(vision_encoder, "config", None), "image_size", None,
|
| 1476 |
) or getattr(vision_encoder, "image_size", 384)
|
| 1477 |
if img.shape[-1] != target_size or img.shape[-2] != target_size:
|
| 1478 |
+
img_resized = resize_with_pad(img, target_size, target_size, pad_value=0)
|
|
|
|
| 1479 |
else:
|
| 1480 |
img_resized = img
|
| 1481 |
+
img_resized = img_resized * 2.0 - 1.0 # normalize to [-1, 1] matching SigLIP
|
| 1482 |
|
| 1483 |
try:
|
| 1484 |
enc_dtype = next(vision_encoder.parameters()).dtype
|
|
|
|
| 1571 |
grid_h = grid_w = grid_side
|
| 1572 |
|
| 1573 |
img_h, img_w = img_tensor.shape[1], img_tensor.shape[2]
|
| 1574 |
+
heatmap = attention_to_heatmap(patch_scores, (grid_h, grid_w), (img_h, img_w),
|
| 1575 |
+
content_crop=content_crop)
|
| 1576 |
heatmaps.append(heatmap)
|
| 1577 |
|
| 1578 |
print(f" Frame {i}: {n_patches} patches → "
|
|
|
|
| 1585 |
img_tensor, raw_heads_attn,
|
| 1586 |
(grid_h, grid_w), (img_h, img_w),
|
| 1587 |
output_path=head_path,
|
| 1588 |
+
content_crop=content_crop,
|
| 1589 |
)
|
| 1590 |
raw_heads_attn = None # only once
|
| 1591 |
else:
|