Spaces:
Sleeping
Sleeping
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
- configs/defaults.yaml +3 -3
- 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:
|
| 13 |
-
cross_attention:
|
| 14 |
-
show_heads:
|
| 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("
|
| 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="
|
| 707 |
ax4.axis("off")
|
| 708 |
if i == 0:
|
| 709 |
-
ax4.set_ylabel("
|
| 710 |
|
| 711 |
-
# Row 5:
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
dual = np.clip(dual, 0, 255).astype(np.uint8)
|
| 719 |
|
| 720 |
ax5 = fig.add_subplot(gs[4, i])
|
| 721 |
-
ax5.imshow(
|
| 722 |
ax5.axis("off")
|
| 723 |
if i == 0:
|
| 724 |
-
ax5.set_ylabel("
|
| 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 |
-
|
| 734 |
-
"Row 4:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
else:
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
|
|
|
|
|
|
| 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("
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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__":
|