subirmansukhani commited on
Commit
a30a48a
Β·
1 Parent(s): 91b2dc8

Improve attention visualization: cyan co-attention, clearer labels, quieter loading

Browse files

- Replace inferno colormap with custom black→cyan→white for co-attention
overlay (row 5), semantically matching blue (self) + green (cross)
- Switch cross-attention heatmap from "hot" to "Greens" colormap
- Replace additive dual-color overlay with multiplicative co-attention
(self-attn Γ— cross-attn) to highlight jointly salient regions
- Update row labels for clarity (SigLIP self-attn, Action cross-attn)
- Move legend into suptitle for cleaner layout
- Suppress noisy HF/lerobot warnings during model loading
- Preload Homebrew FFmpeg 6 to avoid duplicate symbol warnings on macOS
- Default config: method=rollout, cross_attention=true, show_heads=true
- Remove verbose interpretation block from CLI output

Files changed (2) hide show
  1. configs/defaults.yaml +3 -3
  2. inspect_attention.py +67 -51
configs/defaults.yaml CHANGED
@@ -9,7 +9,7 @@ output_dir: ./outputs
9
  save_individual: true # true to also save each frame separately
10
 
11
  # Attention analysis options
12
- method: last-layer # last-layer | rollout | all-layers
13
- cross_attention: false # true to capture action-expert β†’ vision cross-attention (slower)
14
- show_heads: false # true to save a per-head attention grid for the first frame
15
  raw_attention: false # true to skip positional baseline subtraction
 
9
  save_individual: true # true to also save each frame separately
10
 
11
  # Attention analysis options
12
+ 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
inspect_attention.py CHANGED
@@ -46,11 +46,25 @@ Interpreting results:
46
  """
47
 
48
  import argparse
 
49
  import math
50
  import os
51
  import sys
 
52
  from pathlib import Path
53
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  import numpy as np
55
  import torch
56
  import torch.nn.functional as F
@@ -59,6 +73,9 @@ matplotlib.use("Agg") # Non-interactive backend for saving files
59
  import matplotlib.pyplot as plt
60
  import matplotlib.gridspec as gridspec
61
  from PIL import Image
 
 
 
62
 
63
 
64
  # ---------------------------------------------------------------------------
@@ -689,7 +706,7 @@ def create_visualization_grid(frames, heatmaps, actions=None,
689
  ax2.imshow(heatmap_resized, cmap="jet", vmin=0, vmax=1)
690
  ax2.axis("off")
691
  if i == 0:
692
- ax2.set_ylabel("Self-attn", fontsize=11, rotation=0, labelpad=60, va="center")
693
 
694
  # Row 3: Self-attention overlay
695
  ax3 = fig.add_subplot(gs[2, i])
@@ -703,39 +720,45 @@ def create_visualization_grid(frames, heatmaps, actions=None,
703
 
704
  # Row 4: Cross-attention heatmap
705
  ax4 = fig.add_subplot(gs[3, i])
706
- ax4.imshow(cross_hm, cmap="hot", vmin=0, vmax=1)
707
  ax4.axis("off")
708
  if i == 0:
709
- ax4.set_ylabel("Cross-attn", fontsize=11, rotation=0, labelpad=60, va="center")
710
 
711
- # Row 5: Dual-color overlay (self-attn=blue, cross-attn=red)
712
- dual = frame_np.astype(np.float32).copy()
713
- blue_layer = np.zeros_like(dual)
714
- blue_layer[:, :, 2] = heatmap_resized * 255
715
- red_layer = np.zeros_like(dual)
716
- red_layer[:, :, 0] = cross_hm * 255
717
- dual = (0.5 * dual + 0.25 * blue_layer + 0.25 * red_layer)
718
- dual = np.clip(dual, 0, 255).astype(np.uint8)
719
 
720
  ax5 = fig.add_subplot(gs[4, i])
721
- ax5.imshow(dual)
722
  ax5.axis("off")
723
  if i == 0:
724
- ax5.set_ylabel("Dual overlay\n(blue=self, red=cross)", fontsize=9, rotation=0, labelpad=80, va="center")
725
-
726
- title = (
727
- f"SmolVLA Attention β€” Episode {episode_idx}\n"
728
- f"Bright regions = where the model focuses"
729
- )
730
- fig.suptitle(title, fontsize=14, fontweight="bold", y=0.98)
731
 
732
  if has_cross:
733
- footer = ("Row 1: Original | Row 2: Self-attn heatmap | Row 3: Self-attn overlay | "
734
- "Row 4: Cross-attn heatmap | Row 5: Dual overlay")
 
 
 
 
 
 
 
 
 
 
 
735
  else:
736
- footer = "Row 1: Original | Row 2: Heatmap (attention only) | Row 3: Overlay (heatmap on frame)"
737
- fig.subplots_adjust(bottom=0.04)
738
- fig.text(0.5, 0.01, footer, ha="center", fontsize=9, style="italic")
 
 
739
 
740
  plt.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
741
  plt.close()
@@ -831,7 +854,7 @@ def create_per_head_grid(frame, attn_weights, grid_size, image_size,
831
  r, c = divmod(idx, cols)
832
  axes[r, c].axis("off")
833
 
834
- fig.suptitle("Per-head attention patterns", fontsize=13, fontweight="bold")
835
  plt.savefig(output_path, dpi=120, bbox_inches="tight", facecolor="white")
836
  plt.close()
837
  print(f" Saved per-head grid: {output_path}")
@@ -1565,7 +1588,24 @@ Examples:
1565
 
1566
  try:
1567
  from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy
1568
- policy = SmolVLAPolicy.from_pretrained(args.model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1569
  policy.to(device)
1570
  policy.eval()
1571
  print(f" Model loaded successfully ({sum(p.numel() for p in policy.parameters()) / 1e6:.1f}M params)")
@@ -1672,31 +1712,7 @@ Examples:
1672
  if args.save_individual:
1673
  print(f" Individual frames: {args.output_dir}/episode_{args.episode:03d}/")
1674
 
1675
- print(f"""
1676
- How to interpret the results:
1677
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
1678
- β”‚ SELF-ATTENTION (vision encoder, rows 2-3): β”‚
1679
- β”‚ Healthy: bright on gripper, object, goal; dark background. β”‚
1680
- β”‚ Overfitting: bright on shelves, cables, table texture. β”‚
1681
- β”‚ β”‚
1682
- β”‚ CROSS-ATTENTION (action expert β†’ vision, rows 4-5, if enabled): β”‚
1683
- β”‚ Healthy: tight focus on the regions the action decoder uses β”‚
1684
- β”‚ to predict actions (gripper tip, target object). β”‚
1685
- β”‚ Diffuse: expert reads all tokens equally β€” weak specialisation.β”‚
1686
- β”‚ β”‚
1687
- β”‚ Compare self-attn vs cross-attn: if self-attn is diffuse but β”‚
1688
- β”‚ cross-attn is focused, the decoder has learned to select useful β”‚
1689
- β”‚ tokens despite a noisy encoder. β”‚
1690
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1691
-
1692
- Next steps:
1693
- β€’ Compare attention before/after applying the cropping pipeline
1694
- β€’ Compare base model vs fine-tuned model attention
1695
- β€’ If background is highlighted β†’ confirms distribution shift hypothesis
1696
- β€’ Use --cross-attention to see what the action decoder actually reads
1697
- β€’ Use --method rollout for accumulated information flow across layers
1698
- β€’ Use --show-heads to find specialised attention heads
1699
- """)
1700
 
1701
 
1702
  if __name__ == "__main__":
 
46
  """
47
 
48
  import argparse
49
+ import logging
50
  import math
51
  import os
52
  import sys
53
+ import warnings
54
  from pathlib import Path
55
 
56
+ # Preload Homebrew FFmpeg 6 libavdevice so PyAV/av doesn't load its bundled copy;
57
+ # avoids "Class AVFFrameReceiver is implemented in both..." duplicate symbol warning.
58
+ _ffmpeg6_lib = "/opt/homebrew/opt/ffmpeg@6/lib"
59
+ if os.path.isdir(_ffmpeg6_lib):
60
+ _libavdevice = os.path.join(_ffmpeg6_lib, "libavdevice.60.dylib")
61
+ if os.path.isfile(_libavdevice):
62
+ try:
63
+ import ctypes
64
+ ctypes.CDLL(_libavdevice)
65
+ except OSError:
66
+ pass
67
+
68
  import numpy as np
69
  import torch
70
  import torch.nn.functional as F
 
73
  import matplotlib.pyplot as plt
74
  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
 
81
  # ---------------------------------------------------------------------------
 
706
  ax2.imshow(heatmap_resized, cmap="jet", vmin=0, vmax=1)
707
  ax2.axis("off")
708
  if i == 0:
709
+ ax2.set_ylabel("SigLIP\nself-attn", fontsize=11, rotation=0, labelpad=60, va="center")
710
 
711
  # Row 3: Self-attention overlay
712
  ax3 = fig.add_subplot(gs[2, i])
 
720
 
721
  # Row 4: Cross-attention heatmap
722
  ax4 = fig.add_subplot(gs[3, i])
723
+ ax4.imshow(cross_hm, cmap="Greens", vmin=0, vmax=1)
724
  ax4.axis("off")
725
  if i == 0:
726
+ ax4.set_ylabel("Action\ncross-attn", fontsize=11, rotation=0, labelpad=60, va="center")
727
 
728
+ # Row 5: Co-attention overlay (self-attn Γ— cross-attn)
729
+ co_attn = heatmap_resized * cross_hm # element-wise product
730
+ co_attn = co_attn / (co_attn.max() + 1e-8) # renormalize to [0, 1]
731
+ co_overlay = frame_np.copy()
732
+ co_overlay = (0.5 * co_overlay.astype(np.float32)
733
+ + 0.5 * _CYAN_CMAP(co_attn)[:, :, :3] * 255)
734
+ co_overlay = np.clip(co_overlay, 0, 255).astype(np.uint8)
 
735
 
736
  ax5 = fig.add_subplot(gs[4, i])
737
+ ax5.imshow(co_overlay)
738
  ax5.axis("off")
739
  if i == 0:
740
+ ax5.set_ylabel("Co-attention\noverlay", fontsize=11, rotation=0, labelpad=60, va="center")
 
 
 
 
 
 
741
 
742
  if has_cross:
743
+ legend = ("Row 1: Original | Row 2: SigLIP self-attn heatmap | Row 3: Self-attn overlay | "
744
+ "Row 4: Action cross-attn heatmap | Row 5: Co-attention (self Γ— cross)")
745
+ dual_legend = "Co-attention: self-attn Γ— cross-attn β€” bright regions are both visually salient and action-relevant"
746
+ else:
747
+ legend = "Row 1: Original | Row 2: SigLIP self-attn heatmap | Row 3: Overlay (heatmap on frame)"
748
+ dual_legend = None
749
+
750
+ if dual_legend:
751
+ title = (
752
+ f"SmolVLA Attention β€” Episode {episode_idx}\n\n"
753
+ f"{legend}\n\n"
754
+ f"{dual_legend}"
755
+ )
756
  else:
757
+ title = (
758
+ f"SmolVLA Attention β€” Episode {episode_idx}\n\n"
759
+ f"{legend}"
760
+ )
761
+ fig.suptitle(title, fontsize=14, fontweight="bold", y=0.98)
762
 
763
  plt.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
764
  plt.close()
 
854
  r, c = divmod(idx, cols)
855
  axes[r, c].axis("off")
856
 
857
+ fig.suptitle("SigLIP vision encoder per-head self-attention", fontsize=13, fontweight="bold")
858
  plt.savefig(output_path, dpi=120, bbox_inches="tight", facecolor="white")
859
  plt.close()
860
  print(f" Saved per-head grid: {output_path}")
 
1588
 
1589
  try:
1590
  from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy
1591
+ # Suppress noisy warnings from HF/lerobot during model loading:
1592
+ # - "Device 'cuda' is not available. Switching to 'mps'"
1593
+ # - "`torch_dtype` is deprecated! Use `dtype` instead!"
1594
+ # - "Loading ... weights ..."
1595
+ _suppressed_loggers = {
1596
+ name: logging.getLogger(name)
1597
+ for name in ("lerobot.configs.policies", "lerobot", "transformers")
1598
+ }
1599
+ _saved_levels = {name: lg.level for name, lg in _suppressed_loggers.items()}
1600
+ for lg in _suppressed_loggers.values():
1601
+ lg.setLevel(logging.ERROR)
1602
+ try:
1603
+ with warnings.catch_warnings():
1604
+ warnings.filterwarnings("ignore", message=".*torch_dtype.*deprecated.*")
1605
+ policy = SmolVLAPolicy.from_pretrained(args.model)
1606
+ finally:
1607
+ for name, lg in _suppressed_loggers.items():
1608
+ lg.setLevel(_saved_levels[name])
1609
  policy.to(device)
1610
  policy.eval()
1611
  print(f" Model loaded successfully ({sum(p.numel() for p in policy.parameters()) / 1e6:.1f}M params)")
 
1712
  if args.save_individual:
1713
  print(f" Individual frames: {args.output_dir}/episode_{args.episode:03d}/")
1714
 
1715
+ print()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1716
 
1717
 
1718
  if __name__ == "__main__":