Spaces:
Sleeping
Add percentile thresholding and per-head baseline subtraction for SigLIP heatmaps
Browse filesSigLIP's learned positional embeddings create position-dependent attention
hotspots on image edges that persist across episodes. The existing gray-baseline
subtraction can't fully cancel them because softmax is nonlinear, and the
per-head view had no baseline subtraction at all.
Changes:
- Add --attn-threshold flag (default 0.5) to zero attention values below the
Nth percentile after normalization, suppressing residual positional noise
- Return per-head baseline from compute_positional_baseline() and subtract it
in create_per_head_grid() before heatmap creation
- Save positional_baseline.png diagnostic showing the gray-image attention
pattern that gets subtracted
- Thread threshold through all attention_to_heatmap() call sites (self-attn,
cross-attn, per-head grid)
- Update README with new flag, examples, and pipeline description
- README.md +11 -3
- configs/defaults.yaml +1 -0
- inspect_attention.py +81 -17
|
@@ -24,7 +24,7 @@ Extracts attention maps from two places:
|
|
| 24 |
That lets you check whether the model attends to task-relevant regions (gripper, object, goal) or background (walls, table texture) -- useful for debugging overfitting or distribution shift.
|
| 25 |
|
| 26 |
**Input:** A pretrained or fine-tuned SmolVLA policy + a LeRobot dataset (e.g. episodes of pick-and-place).
|
| 27 |
-
**Output:** A multi-row grid PNG per episode, optional per-frame PNGs,
|
| 28 |
|
| 29 |
### Model health diagnostics
|
| 30 |
|
|
@@ -61,7 +61,7 @@ For a detailed visual walkthrough of the architecture and how it maps to the rep
|
|
| 61 |
|
| 62 |
4. **Capture cross-attention** (`--cross-attention`) -- SmolVLA's VLM builds a KV cache from the prefix (vision + language + state tokens). The action expert queries that cache. The script monkey-patches `eager_attention_forward()` on the expert layers to intercept the softmax attention when expert Q attends to prefix K. Only columns corresponding to vision tokens are kept, giving a heatmap of which image regions the action decoder reads.
|
| 63 |
|
| 64 |
-
5. **Turn attention into spatial heatmaps** -- patch-level importance scores are reshaped into a 2D grid, upsampled with bilinear interpolation to image size, and normalized to [0, 1].
|
| 65 |
|
| 66 |
6. **Visualize** -- the output grid has up to 5 rows per frame:
|
| 67 |
|
|
@@ -77,7 +77,7 @@ Rows 4-5 only appear when cross-attention is enabled. The co-attention overlay m
|
|
| 77 |
|
| 78 |
### Per-head grid
|
| 79 |
|
| 80 |
-
With `--show-heads`, a separate grid shows each of the 12 SigLIP attention heads individually for the first frame
|
| 81 |
|
| 82 |

|
| 83 |
*Each subplot is one attention head. Look for specialization -- e.g. one head tracking the gripper, another tracking the object.*
|
|
@@ -142,6 +142,12 @@ python inspect_attention.py
|
|
| 142 |
# Raw attention without positional baseline subtraction
|
| 143 |
./run.sh --raw-attention
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
# Model health diagnostics (spectral analysis + entropy + redundancy)
|
| 146 |
./run.sh --model-health
|
| 147 |
|
|
@@ -175,6 +181,7 @@ Results land in `outputs/`.
|
|
| 175 |
| `--cross-attention` | `true` | Capture action-expert cross-attention |
|
| 176 |
| `--show-heads` | `true` | Save per-head attention grid for first frame |
|
| 177 |
| `--raw-attention` | `false` | Skip positional baseline subtraction |
|
|
|
|
| 178 |
|
| 179 |
**Model health diagnostics:**
|
| 180 |
|
|
@@ -256,6 +263,7 @@ smolvla-inspect/
|
|
| 256 |
│ ├── ELI5.md # Plain-language explanation of the interpretability approach
|
| 257 |
│ └── TESTING.md # CLI test commands and expected output
|
| 258 |
├── outputs/ # Generated images and reports (gitignored)
|
|
|
|
| 259 |
│ ├── model_health_report.md
|
| 260 |
│ └── model_health_report.png
|
| 261 |
├── run.sh # Wrapper that sets FFmpeg lib path
|
|
|
|
| 24 |
That lets you check whether the model attends to task-relevant regions (gripper, object, goal) or background (walls, table texture) -- useful for debugging overfitting or distribution shift.
|
| 25 |
|
| 26 |
**Input:** A pretrained or fine-tuned SmolVLA policy + a LeRobot dataset (e.g. episodes of pick-and-place).
|
| 27 |
+
**Output:** A multi-row grid PNG per episode, optional per-frame PNGs, an optional per-head attention grid, and a positional baseline diagnostic (`positional_baseline.png`) showing the position-dependent attention pattern that gets subtracted.
|
| 28 |
|
| 29 |
### Model health diagnostics
|
| 30 |
|
|
|
|
| 61 |
|
| 62 |
4. **Capture cross-attention** (`--cross-attention`) -- SmolVLA's VLM builds a KV cache from the prefix (vision + language + state tokens). The action expert queries that cache. The script monkey-patches `eager_attention_forward()` on the expert layers to intercept the softmax attention when expert Q attends to prefix K. Only columns corresponding to vision tokens are kept, giving a heatmap of which image regions the action decoder reads.
|
| 63 |
|
| 64 |
+
5. **Turn attention into spatial heatmaps** -- patch-level importance scores are reshaped into a 2D grid, upsampled with bilinear interpolation to image size, and normalized to [0, 1]. A percentile threshold (`--attn-threshold`, default 0.5) then zeros out low-attention values to suppress residual positional noise from SigLIP's learned position embeddings, and re-normalizes the remainder.
|
| 65 |
|
| 66 |
6. **Visualize** -- the output grid has up to 5 rows per frame:
|
| 67 |
|
|
|
|
| 77 |
|
| 78 |
### Per-head grid
|
| 79 |
|
| 80 |
+
With `--show-heads`, a separate grid shows each of the 12 SigLIP attention heads individually for the first frame. Each head has its per-head positional baseline subtracted (computed from a gray-image forward pass) so the patterns reflect content-dependent attention rather than position artifacts.
|
| 81 |
|
| 82 |

|
| 83 |
*Each subplot is one attention head. Look for specialization -- e.g. one head tracking the gripper, another tracking the object.*
|
|
|
|
| 142 |
# Raw attention without positional baseline subtraction
|
| 143 |
./run.sh --raw-attention
|
| 144 |
|
| 145 |
+
# Higher threshold to suppress more positional noise (default 0.5)
|
| 146 |
+
./run.sh --attn-threshold 0.7
|
| 147 |
+
|
| 148 |
+
# No threshold (show all baseline-subtracted values)
|
| 149 |
+
./run.sh --attn-threshold 0
|
| 150 |
+
|
| 151 |
# Model health diagnostics (spectral analysis + entropy + redundancy)
|
| 152 |
./run.sh --model-health
|
| 153 |
|
|
|
|
| 181 |
| `--cross-attention` | `true` | Capture action-expert cross-attention |
|
| 182 |
| `--show-heads` | `true` | Save per-head attention grid for first frame |
|
| 183 |
| `--raw-attention` | `false` | Skip positional baseline subtraction |
|
| 184 |
+
| `--attn-threshold` | `0.5` | Percentile (0-1) below which attention values are zeroed to suppress positional noise |
|
| 185 |
|
| 186 |
**Model health diagnostics:**
|
| 187 |
|
|
|
|
| 263 |
│ ├── ELI5.md # Plain-language explanation of the interpretability approach
|
| 264 |
│ └── TESTING.md # CLI test commands and expected output
|
| 265 |
├── outputs/ # Generated images and reports (gitignored)
|
| 266 |
+
│ ├── positional_baseline.png
|
| 267 |
│ ├── model_health_report.md
|
| 268 |
│ └── model_health_report.png
|
| 269 |
├── run.sh # Wrapper that sets FFmpeg lib path
|
|
@@ -13,6 +13,7 @@ method: rollout # last-layer | rollout | all-layers
|
|
| 13 |
cross_attention: true # true to capture action-expert → vision cross-attention (slower)
|
| 14 |
show_heads: true # true to save a per-head attention grid for the first frame
|
| 15 |
raw_attention: false # true to skip positional baseline subtraction
|
|
|
|
| 16 |
|
| 17 |
# Model health analysis
|
| 18 |
model_health: false
|
|
|
|
| 13 |
cross_attention: true # true to capture action-expert → vision cross-attention (slower)
|
| 14 |
show_heads: true # true to save a per-head attention grid for the first frame
|
| 15 |
raw_attention: false # true to skip positional baseline subtraction
|
| 16 |
+
attn_threshold: 0.5 # percentile (0-1) below which attention values are zeroed
|
| 17 |
|
| 18 |
# Model health analysis
|
| 19 |
model_health: false
|
|
@@ -567,16 +567,21 @@ def compute_padding_patches(input_hw, target_size, patch_size):
|
|
| 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 |
-
|
| 574 |
Args:
|
| 575 |
attn_weights: Tensor of shape (num_patches,) or (H_patches, W_patches)
|
| 576 |
representing per-patch attention scores
|
| 577 |
grid_size: (H_patches, W_patches) — the patch grid dimensions
|
| 578 |
image_size: (H_pixels, W_pixels) — the original image dimensions
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
Returns:
|
| 581 |
heatmap: numpy array of shape (H_pixels, W_pixels) normalized to [0, 1]
|
| 582 |
"""
|
|
@@ -614,7 +619,15 @@ def attention_to_heatmap(attn_weights, grid_size, image_size, content_crop=None)
|
|
| 614 |
hmin, hmax = heatmap.min(), heatmap.max()
|
| 615 |
if hmax > hmin:
|
| 616 |
heatmap = (heatmap - hmin) / (hmax - hmin)
|
| 617 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
return heatmap
|
| 619 |
|
| 620 |
|
|
@@ -718,7 +731,9 @@ def compute_positional_baseline(vision_encoder, attn_capture, device, method,
|
|
| 718 |
match those in real frames.
|
| 719 |
|
| 720 |
Returns:
|
| 721 |
-
|
|
|
|
|
|
|
| 722 |
"""
|
| 723 |
# Build a mean-gray image at the encoder's expected resolution
|
| 724 |
img_size = getattr(
|
|
@@ -760,20 +775,30 @@ def compute_positional_baseline(vision_encoder, attn_capture, device, method,
|
|
| 760 |
vision_encoder(pixel_values=gray, patch_attention_mask=patch_mask)
|
| 761 |
except Exception as e:
|
| 762 |
print(f" WARNING: Baseline forward pass failed ({e}), skipping correction")
|
| 763 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
|
| 765 |
# Reduce captured attention using the same method as real frames
|
| 766 |
if method == "rollout":
|
| 767 |
all_layers = attn_capture.get_all_layer_attentions()
|
| 768 |
rollout_mat = compute_attention_rollout(all_layers)
|
| 769 |
if rollout_mat is None:
|
| 770 |
-
return None
|
| 771 |
baseline_scores = rollout_mat.mean(dim=0)
|
| 772 |
else:
|
| 773 |
# "last-layer" or "all-layers" both use last-layer for the summary
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
while attn.dim() > 3:
|
| 778 |
attn = attn[0]
|
| 779 |
if attn.dim() == 3:
|
|
@@ -781,7 +806,7 @@ def compute_positional_baseline(vision_encoder, attn_capture, device, method,
|
|
| 781 |
baseline_scores = compute_patch_attention_scores(attn, method="mean")
|
| 782 |
|
| 783 |
attn_capture.reset_maps()
|
| 784 |
-
return baseline_scores
|
| 785 |
|
| 786 |
|
| 787 |
# ---------------------------------------------------------------------------
|
|
@@ -980,7 +1005,8 @@ def save_individual_frames(frames, heatmaps, output_dir, episode_idx=0):
|
|
| 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
|
|
@@ -993,6 +1019,9 @@ def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
|
| 993 |
grid_size: ``(H_patches, W_patches)``
|
| 994 |
image_size: ``(H_pixels, W_pixels)``
|
| 995 |
output_path: where to save the PNG
|
|
|
|
|
|
|
|
|
|
| 996 |
"""
|
| 997 |
if isinstance(frame, torch.Tensor):
|
| 998 |
frame_np = frame.permute(1, 2, 0).numpy()
|
|
@@ -1017,7 +1046,11 @@ def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
|
| 1022 |
blended = overlay_heatmap(frame_np.copy(), hmap, alpha=0.5)
|
| 1023 |
axes[r, c].imshow(blended)
|
|
@@ -1318,7 +1351,7 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1318 |
image_key=None, device="cpu",
|
| 1319 |
method="last-layer", cross_attention=False,
|
| 1320 |
show_heads=False, output_dir="./outputs",
|
| 1321 |
-
raw_attention=False):
|
| 1322 |
"""
|
| 1323 |
Core function: Run inference and extract attention heatmaps.
|
| 1324 |
|
|
@@ -1333,6 +1366,8 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1333 |
*show_heads* is True).
|
| 1334 |
raw_attention: If *True* skip positional baseline subtraction
|
| 1335 |
(show raw, uncorrected attention).
|
|
|
|
|
|
|
| 1336 |
|
| 1337 |
Returns:
|
| 1338 |
frames: list of image tensors (C, H, W)
|
|
@@ -1406,8 +1441,9 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 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:
|
|
@@ -1423,6 +1459,26 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 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 = []
|
|
@@ -1572,7 +1628,8 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 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 → "
|
|
@@ -1586,6 +1643,8 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 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:
|
|
@@ -1608,7 +1667,8 @@ def extract_attention_maps(policy, dataset, episode_idx=0, num_frames=8,
|
|
| 1608 |
cs_h = cs_w = cs_side
|
| 1609 |
|
| 1610 |
img_h, img_w = img_tensor.shape[1], img_tensor.shape[2]
|
| 1611 |
-
cross_hm = attention_to_heatmap(cross_scores, (cs_h, cs_w), (img_h, img_w)
|
|
|
|
| 1612 |
cross_attn_heatmaps.append(cross_hm)
|
| 1613 |
print(f" Frame {i}: Cross-attention captured ({n_vis} vision tokens)")
|
| 1614 |
else:
|
|
@@ -2602,6 +2662,9 @@ Examples:
|
|
| 2602 |
parser.add_argument("--raw-attention", action="store_true",
|
| 2603 |
default=defaults.get("raw_attention", False),
|
| 2604 |
help="Skip positional baseline subtraction (show raw attention)")
|
|
|
|
|
|
|
|
|
|
| 2605 |
|
| 2606 |
# Model health diagnostics
|
| 2607 |
parser.add_argument("--model-health", action="store_true",
|
|
@@ -2726,6 +2789,7 @@ Examples:
|
|
| 2726 |
show_heads=args.show_heads,
|
| 2727 |
output_dir=args.output_dir,
|
| 2728 |
raw_attention=args.raw_attention,
|
|
|
|
| 2729 |
)
|
| 2730 |
except Exception as e:
|
| 2731 |
print(f"\n Attention extraction failed: {e}")
|
|
|
|
| 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 |
+
threshold_pct=0.0):
|
| 572 |
"""
|
| 573 |
Convert attention weights from patch-space to pixel-space heatmap.
|
| 574 |
+
|
| 575 |
Args:
|
| 576 |
attn_weights: Tensor of shape (num_patches,) or (H_patches, W_patches)
|
| 577 |
representing per-patch attention scores
|
| 578 |
grid_size: (H_patches, W_patches) — the patch grid dimensions
|
| 579 |
image_size: (H_pixels, W_pixels) — the original image dimensions
|
| 580 |
+
content_crop: Optional (crop_h, crop_w) to remove padding patches
|
| 581 |
+
threshold_pct: Percentile threshold (0.0–1.0). Values below this
|
| 582 |
+
percentile are zeroed, then the remainder is
|
| 583 |
+
re-normalised to [0, 1]. 0.0 = no thresholding.
|
| 584 |
+
|
| 585 |
Returns:
|
| 586 |
heatmap: numpy array of shape (H_pixels, W_pixels) normalized to [0, 1]
|
| 587 |
"""
|
|
|
|
| 619 |
hmin, hmax = heatmap.min(), heatmap.max()
|
| 620 |
if hmax > hmin:
|
| 621 |
heatmap = (heatmap - hmin) / (hmax - hmin)
|
| 622 |
+
|
| 623 |
+
# Percentile thresholding: zero out diffuse low-attention noise
|
| 624 |
+
if threshold_pct > 0:
|
| 625 |
+
thresh_val = np.percentile(heatmap, threshold_pct * 100)
|
| 626 |
+
heatmap = np.where(heatmap >= thresh_val, heatmap, 0.0)
|
| 627 |
+
hmax = heatmap.max()
|
| 628 |
+
if hmax > 0:
|
| 629 |
+
heatmap = heatmap / hmax
|
| 630 |
+
|
| 631 |
return heatmap
|
| 632 |
|
| 633 |
|
|
|
|
| 731 |
match those in real frames.
|
| 732 |
|
| 733 |
Returns:
|
| 734 |
+
``(baseline_scores, per_head_baseline)`` where *baseline_scores* has
|
| 735 |
+
shape ``(num_patches,)`` and *per_head_baseline* has shape
|
| 736 |
+
``(heads, num_patches)`` (or *None* if unavailable).
|
| 737 |
"""
|
| 738 |
# Build a mean-gray image at the encoder's expected resolution
|
| 739 |
img_size = getattr(
|
|
|
|
| 775 |
vision_encoder(pixel_values=gray, patch_attention_mask=patch_mask)
|
| 776 |
except Exception as e:
|
| 777 |
print(f" WARNING: Baseline forward pass failed ({e}), skipping correction")
|
| 778 |
+
return None, None
|
| 779 |
+
|
| 780 |
+
# Capture per-head baseline from the last layer before aggregation
|
| 781 |
+
per_head_baseline = None
|
| 782 |
+
last_layer_attn = attn_capture.get_last_layer_attention()
|
| 783 |
+
if last_layer_attn is not None:
|
| 784 |
+
head_attn = last_layer_attn
|
| 785 |
+
while head_attn.dim() > 3:
|
| 786 |
+
head_attn = head_attn[0]
|
| 787 |
+
# head_attn: (heads, patches, patches)
|
| 788 |
+
per_head_baseline = head_attn.mean(dim=-2) # (heads, patches)
|
| 789 |
|
| 790 |
# Reduce captured attention using the same method as real frames
|
| 791 |
if method == "rollout":
|
| 792 |
all_layers = attn_capture.get_all_layer_attentions()
|
| 793 |
rollout_mat = compute_attention_rollout(all_layers)
|
| 794 |
if rollout_mat is None:
|
| 795 |
+
return None, per_head_baseline
|
| 796 |
baseline_scores = rollout_mat.mean(dim=0)
|
| 797 |
else:
|
| 798 |
# "last-layer" or "all-layers" both use last-layer for the summary
|
| 799 |
+
if last_layer_attn is None:
|
| 800 |
+
return None, None
|
| 801 |
+
attn = last_layer_attn
|
| 802 |
while attn.dim() > 3:
|
| 803 |
attn = attn[0]
|
| 804 |
if attn.dim() == 3:
|
|
|
|
| 806 |
baseline_scores = compute_patch_attention_scores(attn, method="mean")
|
| 807 |
|
| 808 |
attn_capture.reset_maps()
|
| 809 |
+
return baseline_scores, per_head_baseline
|
| 810 |
|
| 811 |
|
| 812 |
# ---------------------------------------------------------------------------
|
|
|
|
| 1005 |
|
| 1006 |
|
| 1007 |
def create_per_head_grid(frame, attn_weights, grid_size, image_size,
|
| 1008 |
+
output_path="per_head_attention.png", content_crop=None,
|
| 1009 |
+
baseline_per_head=None, threshold_pct=0.0):
|
| 1010 |
"""
|
| 1011 |
Visualise each attention head's pattern individually for a single
|
| 1012 |
frame. Useful for identifying specialised heads (e.g. one tracking
|
|
|
|
| 1019 |
grid_size: ``(H_patches, W_patches)``
|
| 1020 |
image_size: ``(H_pixels, W_pixels)``
|
| 1021 |
output_path: where to save the PNG
|
| 1022 |
+
baseline_per_head: Optional tensor ``(heads, patches)`` — per-head
|
| 1023 |
+
positional baseline to subtract before heatmap creation.
|
| 1024 |
+
threshold_pct: Percentile threshold passed to ``attention_to_heatmap``.
|
| 1025 |
"""
|
| 1026 |
if isinstance(frame, torch.Tensor):
|
| 1027 |
frame_np = frame.permute(1, 2, 0).numpy()
|
|
|
|
| 1046 |
r, c = divmod(h, cols)
|
| 1047 |
head_attn = attn_weights[h] # (patches, patches)
|
| 1048 |
scores = head_attn.mean(dim=0) # per-patch importance
|
| 1049 |
+
if baseline_per_head is not None and h < baseline_per_head.shape[0]:
|
| 1050 |
+
scores = torch.clamp(scores - baseline_per_head[h], min=0)
|
| 1051 |
+
hmap = attention_to_heatmap(scores, grid_size, image_size,
|
| 1052 |
+
content_crop=content_crop,
|
| 1053 |
+
threshold_pct=threshold_pct)
|
| 1054 |
|
| 1055 |
blended = overlay_heatmap(frame_np.copy(), hmap, alpha=0.5)
|
| 1056 |
axes[r, c].imshow(blended)
|
|
|
|
| 1351 |
image_key=None, device="cpu",
|
| 1352 |
method="last-layer", cross_attention=False,
|
| 1353 |
show_heads=False, output_dir="./outputs",
|
| 1354 |
+
raw_attention=False, attn_threshold=0.5):
|
| 1355 |
"""
|
| 1356 |
Core function: Run inference and extract attention heatmaps.
|
| 1357 |
|
|
|
|
| 1366 |
*show_heads* is True).
|
| 1367 |
raw_attention: If *True* skip positional baseline subtraction
|
| 1368 |
(show raw, uncorrected attention).
|
| 1369 |
+
attn_threshold: Percentile (0–1) below which attention values are
|
| 1370 |
+
zeroed to suppress residual positional noise.
|
| 1371 |
|
| 1372 |
Returns:
|
| 1373 |
frames: list of image tensors (C, H, W)
|
|
|
|
| 1441 |
input_hw = (first_img.shape[1], first_img.shape[2])
|
| 1442 |
|
| 1443 |
baseline_scores = None
|
| 1444 |
+
per_head_baseline = None
|
| 1445 |
if not raw_attention:
|
| 1446 |
+
baseline_scores, per_head_baseline = compute_positional_baseline(
|
| 1447 |
vision_encoder, attn_capture, device, method, input_hw=input_hw,
|
| 1448 |
)
|
| 1449 |
if baseline_scores is not None:
|
|
|
|
| 1459 |
if content_crop != (0, 0):
|
| 1460 |
print(f" Padding crop: {content_crop[0]} top rows, {content_crop[1]} left cols of patches")
|
| 1461 |
|
| 1462 |
+
# --- Save positional baseline diagnostic heatmap ---
|
| 1463 |
+
if baseline_scores is not None:
|
| 1464 |
+
n_bl = baseline_scores.shape[0]
|
| 1465 |
+
bl_side = int(math.sqrt(n_bl))
|
| 1466 |
+
grid_h_bl = target_size // patch_size_cfg if target_size and patch_size_cfg else bl_side
|
| 1467 |
+
grid_w_bl = grid_h_bl
|
| 1468 |
+
img_h_bl, img_w_bl = first_img.shape[1], first_img.shape[2]
|
| 1469 |
+
bl_heatmap = attention_to_heatmap(
|
| 1470 |
+
baseline_scores, (grid_h_bl, grid_w_bl), (img_h_bl, img_w_bl),
|
| 1471 |
+
content_crop=content_crop,
|
| 1472 |
+
)
|
| 1473 |
+
bl_path = os.path.join(output_dir, "positional_baseline.png")
|
| 1474 |
+
fig_bl, ax_bl = plt.subplots(figsize=(6, 6))
|
| 1475 |
+
ax_bl.imshow(bl_heatmap, cmap="jet")
|
| 1476 |
+
ax_bl.set_title("Positional baseline (gray-image attention)", fontsize=11)
|
| 1477 |
+
ax_bl.axis("off")
|
| 1478 |
+
fig_bl.savefig(bl_path, dpi=100, bbox_inches="tight")
|
| 1479 |
+
plt.close(fig_bl)
|
| 1480 |
+
print(f" Saved positional baseline diagnostic: {bl_path}")
|
| 1481 |
+
|
| 1482 |
# --- Run inference and collect attention ---
|
| 1483 |
print(f"\n[4/4] Running forward passes and extracting attention (method={method})...")
|
| 1484 |
frames = []
|
|
|
|
| 1628 |
|
| 1629 |
img_h, img_w = img_tensor.shape[1], img_tensor.shape[2]
|
| 1630 |
heatmap = attention_to_heatmap(patch_scores, (grid_h, grid_w), (img_h, img_w),
|
| 1631 |
+
content_crop=content_crop,
|
| 1632 |
+
threshold_pct=attn_threshold)
|
| 1633 |
heatmaps.append(heatmap)
|
| 1634 |
|
| 1635 |
print(f" Frame {i}: {n_patches} patches → "
|
|
|
|
| 1643 |
(grid_h, grid_w), (img_h, img_w),
|
| 1644 |
output_path=head_path,
|
| 1645 |
content_crop=content_crop,
|
| 1646 |
+
baseline_per_head=per_head_baseline if not raw_attention else None,
|
| 1647 |
+
threshold_pct=attn_threshold,
|
| 1648 |
)
|
| 1649 |
raw_heads_attn = None # only once
|
| 1650 |
else:
|
|
|
|
| 1667 |
cs_h = cs_w = cs_side
|
| 1668 |
|
| 1669 |
img_h, img_w = img_tensor.shape[1], img_tensor.shape[2]
|
| 1670 |
+
cross_hm = attention_to_heatmap(cross_scores, (cs_h, cs_w), (img_h, img_w),
|
| 1671 |
+
threshold_pct=attn_threshold)
|
| 1672 |
cross_attn_heatmaps.append(cross_hm)
|
| 1673 |
print(f" Frame {i}: Cross-attention captured ({n_vis} vision tokens)")
|
| 1674 |
else:
|
|
|
|
| 2662 |
parser.add_argument("--raw-attention", action="store_true",
|
| 2663 |
default=defaults.get("raw_attention", False),
|
| 2664 |
help="Skip positional baseline subtraction (show raw attention)")
|
| 2665 |
+
parser.add_argument("--attn-threshold", type=float,
|
| 2666 |
+
default=defaults.get("attn_threshold", 0.5),
|
| 2667 |
+
help="Percentile threshold (0-1) below which attention values are zeroed")
|
| 2668 |
|
| 2669 |
# Model health diagnostics
|
| 2670 |
parser.add_argument("--model-health", action="store_true",
|
|
|
|
| 2789 |
show_heads=args.show_heads,
|
| 2790 |
output_dir=args.output_dir,
|
| 2791 |
raw_attention=args.raw_attention,
|
| 2792 |
+
attn_threshold=args.attn_threshold,
|
| 2793 |
)
|
| 2794 |
except Exception as e:
|
| 2795 |
print(f"\n Attention extraction failed: {e}")
|