{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# GroundingDINO: Attention Visualization & Cross-Modal Attention\n", "\n", "## Notebook 12 of 14\n", "\n", "This notebook visualizes all attention patterns in GroundingDINO:\n", "- Swin window attention maps\n", "- BERT self-attention patterns\n", "- Feature Enhancer cross-modal attention (image↔text)\n", "- Decoder text cross-attention (query→text)\n", "- Phrase grounding visualization\n", "\n", "---" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os, sys\n", "import torch\n", "import torch.nn as nn\n", "import torch.nn.functional as F\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import matplotlib.patches as patches\n", "from PIL import Image\n", "from transformers import BertTokenizer, BertModel\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "MODEL_DIR = \"/mnt/mydata/huggingface\"\n", "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", "print(f\"Device: {device}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. BERT Self-Attention Visualization" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')\n", "bert = BertModel.from_pretrained('bert-base-uncased', output_attentions=True)\n", "bert.eval()\n", "\n", "# Test with GroundingDINO-style queries\n", "queries = [\n", " \"cat . dog . bird\",\n", " \"person wearing a red hat\",\n", " \"the large brown dog sitting on the grass\",\n", "]\n", "\n", "query = queries[2]\n", "inputs = tokenizer(query, return_tensors='pt')\n", "tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])\n", "\n", "with torch.no_grad():\n", " outputs = bert(**inputs)\n", "\n", "attentions = outputs.attentions # 12 layers\n", "\n", "# Visualize attention from last 4 layers\n", "fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n", "\n", "for row, layer_idx in enumerate([8, 11]):\n", " attn = attentions[layer_idx][0].detach().numpy() # 12, L, L\n", " \n", " # Average attention\n", " ax = axes[row, 0]\n", " avg_attn = attn.mean(0)\n", " im = ax.imshow(avg_attn, cmap='Blues', aspect='equal', vmin=0)\n", " ax.set_xticks(range(len(tokens)))\n", " ax.set_yticks(range(len(tokens)))\n", " ax.set_xticklabels(tokens, rotation=45, ha='right', fontsize=7)\n", " ax.set_yticklabels(tokens, fontsize=7)\n", " ax.set_title(f'Layer {layer_idx+1}: Avg Attention', fontsize=9, fontweight='bold')\n", " \n", " # 3 individual heads\n", " for col, head_idx in enumerate([0, 4, 8]):\n", " ax = axes[row, col+1]\n", " im = ax.imshow(attn[head_idx], cmap='hot', aspect='equal', vmin=0)\n", " ax.set_xticks(range(len(tokens)))\n", " ax.set_yticks(range(len(tokens)))\n", " ax.set_xticklabels(tokens, rotation=45, ha='right', fontsize=7)\n", " ax.set_yticklabels(tokens, fontsize=7)\n", " ax.set_title(f'Layer {layer_idx+1}, Head {head_idx+1}', fontsize=9, fontweight='bold')\n", "\n", "plt.suptitle(f'BERT Attention Patterns\\nQuery: \"{query}\"', fontsize=13, fontweight='bold')\n", "plt.tight_layout()\n", "plt.savefig('bert_attention_patterns.png', dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Cross-Modal Attention Visualization (Feature Enhancer)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Simulate cross-modal attention from Feature Enhancer\n", "# This shows how image regions attend to text tokens\n", "\n", "# Create a structured scenario\n", "H, W = 20, 30 # feature map size\n", "Nv = H * W\n", "tokens_vis = ['[CLS]', 'cat', '.', 'dog', '.', 'remote', '[SEP]']\n", "L = len(tokens_vis)\n", "\n", "# Simulate cross-modal attention after Feature Enhancer\n", "np.random.seed(42)\n", "img2txt_attn = np.random.rand(Nv, L) * 0.05\n", "\n", "# Cat region (top-left quadrant) → attends to 'cat'\n", "cat_region = np.array([i*W + j for i in range(H//2) for j in range(W//2)])\n", "img2txt_attn[cat_region, 1] = np.random.rand(len(cat_region)) * 0.6 + 0.4\n", "\n", "# Dog region (bottom-right quadrant) → attends to 'dog'\n", "dog_region = np.array([i*W + j for i in range(H//2, H) for j in range(W//2, W)])\n", "img2txt_attn[dog_region, 3] = np.random.rand(len(dog_region)) * 0.6 + 0.4\n", "\n", "# Remote region (top-right) → attends to 'remote'\n", "remote_region = np.array([i*W + j for i in range(H//3) for j in range(2*W//3, W)])\n", "img2txt_attn[remote_region, 5] = np.random.rand(len(remote_region)) * 0.5 + 0.3\n", "\n", "# Normalize\n", "img2txt_attn /= (img2txt_attn.sum(axis=-1, keepdims=True) + 1e-8)\n", "\n", "fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n", "\n", "# Full attention matrix\n", "ax = axes[0, 0]\n", "im = ax.imshow(img2txt_attn, cmap='hot', aspect='auto', vmin=0)\n", "ax.set_xticks(range(L))\n", "ax.set_xticklabels(tokens_vis, rotation=45, ha='right', fontsize=9)\n", "ax.set_ylabel('Image region (flattened)')\n", "ax.set_title('Image→Text Attention\\n(Full matrix)', fontsize=10, fontweight='bold')\n", "plt.colorbar(im, ax=ax)\n", "\n", "# Per-token attention maps (reshaped to 2D)\n", "for col, (tok_idx, tok_name) in enumerate([(1, 'cat'), (3, 'dog'), (5, 'remote')]):\n", " ax = axes[0, col+1]\n", " attn_map = img2txt_attn[:, tok_idx].reshape(H, W)\n", " im = ax.imshow(attn_map, cmap='hot', aspect='equal', vmin=0)\n", " ax.set_title(f'Attention to \"{tok_name}\"\\n(Image regions)', fontsize=10, fontweight='bold')\n", " ax.axis('off')\n", " plt.colorbar(im, ax=ax)\n", "\n", "# Text-to-image attention\n", "txt2img_attn = img2txt_attn.T # L, Nv\n", "txt2img_attn /= (txt2img_attn.sum(axis=-1, keepdims=True) + 1e-8)\n", "\n", "ax = axes[1, 0]\n", "im = ax.imshow(txt2img_attn, cmap='Blues', aspect='auto', vmin=0)\n", "ax.set_yticks(range(L))\n", "ax.set_yticklabels(tokens_vis, fontsize=9)\n", "ax.set_xlabel('Image region (flattened)')\n", "ax.set_title('Text→Image Attention\\n(Full matrix)', fontsize=10, fontweight='bold')\n", "plt.colorbar(im, ax=ax)\n", "\n", "# Per-token text→image attention maps\n", "for col, (tok_idx, tok_name) in enumerate([(1, 'cat'), (3, 'dog'), (5, 'remote')]):\n", " ax = axes[1, col+1]\n", " attn_map = txt2img_attn[tok_idx].reshape(H, W)\n", " im = ax.imshow(attn_map, cmap='Blues', aspect='equal', vmin=0)\n", " ax.set_title(f'\"{tok_name}\" attends to\\nimage regions', fontsize=10, fontweight='bold')\n", " ax.axis('off')\n", " plt.colorbar(im, ax=ax)\n", "\n", "plt.suptitle('Cross-Modal Attention: Feature Enhancer\\n(Image↔Text Bidirectional Attention)', \n", " fontsize=13, fontweight='bold')\n", "plt.tight_layout()\n", "plt.savefig('cross_modal_attention_viz.png', dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Decoder Text Cross-Attention (Phrase Grounding)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Simulate decoder text cross-attention\n", "# Shows which text tokens each detected box corresponds to\n", "\n", "K = 10 # detected boxes\n", "tokens_dec = ['[CLS]', 'cat', '.', 'dog', '.', 'remote', '[SEP]']\n", "L_dec = len(tokens_dec)\n", "\n", "# Simulate text cross-attention weights from decoder\n", "np.random.seed(42)\n", "dec_txt_attn = np.random.rand(K, L_dec) * 0.1\n", "\n", "# Boxes 0-3: cats\n", "dec_txt_attn[:4, 1] = np.random.rand(4) * 0.6 + 0.4\n", "# Boxes 4-6: dogs\n", "dec_txt_attn[4:7, 3] = np.random.rand(3) * 0.6 + 0.4\n", "# Boxes 7-9: remotes\n", "dec_txt_attn[7:, 5] = np.random.rand(3) * 0.5 + 0.3\n", "\n", "# Normalize\n", "dec_txt_attn /= (dec_txt_attn.sum(axis=-1, keepdims=True) + 1e-8)\n", "\n", "# Contrastive scores (before sigmoid)\n", "cont_scores = dec_txt_attn * 5 - 2 # scale to reasonable range\n", "conf_scores = 1 / (1 + np.exp(-cont_scores.max(axis=-1))) # sigmoid of max\n", "\n", "# Phrase assignment\n", "best_token = dec_txt_attn.argmax(axis=-1)\n", "assigned_phrases = [tokens_dec[t] for t in best_token]\n", "\n", "fig, axes = plt.subplots(1, 3, figsize=(18, 6))\n", "\n", "# Attention matrix\n", "ax1 = axes[0]\n", "im = ax1.imshow(dec_txt_attn, cmap='hot', aspect='auto', vmin=0)\n", "ax1.set_xticks(range(L_dec))\n", "ax1.set_xticklabels(tokens_dec, rotation=45, ha='right', fontsize=10)\n", "ax1.set_yticks(range(K))\n", "ax1.set_yticklabels([f'Box {i}' for i in range(K)], fontsize=9)\n", "ax1.set_title('Decoder Text Cross-Attention\\n(Query→Text)', fontsize=11, fontweight='bold')\n", "plt.colorbar(im, ax=ax1)\n", "\n", "# Phrase grounding\n", "ax2 = axes[1]\n", "phrase_colors = {'cat': 'red', 'dog': 'blue', 'remote': 'green', '.': 'gray', '[CLS]': 'gray', '[SEP]': 'gray'}\n", "bar_colors = [phrase_colors.get(p, 'gray') for p in assigned_phrases]\n", "ax2.barh(range(K), conf_scores, color=bar_colors, alpha=0.8)\n", "ax2.set_yticks(range(K))\n", "ax2.set_yticklabels([f'Box {i}: \"{assigned_phrases[i]}\"' for i in range(K)], fontsize=9)\n", "ax2.set_xlabel('Detection Confidence')\n", "ax2.set_title('Phrase Grounding Result\\n(Color = matched phrase)', fontsize=11, fontweight='bold')\n", "ax2.axvline(x=0.5, color='black', linestyle='--', linewidth=2, label='Threshold')\n", "ax2.legend()\n", "ax2.grid(True, alpha=0.3)\n", "\n", "from matplotlib.patches import Patch\n", "legend_elements = [Patch(facecolor='red', label='cat'),\n", " Patch(facecolor='blue', label='dog'),\n", " Patch(facecolor='green', label='remote')]\n", "ax2.legend(handles=legend_elements, loc='lower right')\n", "\n", "# Simulated image with detections\n", "ax3 = axes[2]\n", "img_sim = np.ones((300, 400, 3)) * 0.9\n", "# Add colored regions\n", "img_sim[50:150, 30:150] = [0.8, 0.6, 0.6] # cat region\n", "img_sim[150:250, 250:380] = [0.6, 0.6, 0.8] # dog region\n", "img_sim[20:80, 300:380] = [0.6, 0.8, 0.6] # remote region\n", "ax3.imshow(img_sim)\n", "\n", "# Draw boxes with phrase labels\n", "sim_boxes = [\n", " (30, 50, 120, 100, 'cat', 0.89, 'red'),\n", " (250, 150, 130, 100, 'dog', 0.82, 'blue'),\n", " (300, 20, 80, 60, 'remote', 0.75, 'green'),\n", "]\n", "for x, y, w, h, phrase, score, color in sim_boxes:\n", " rect = patches.Rectangle((x, y), w, h, linewidth=2.5,\n", " edgecolor=color, facecolor='none')\n", " ax3.add_patch(rect)\n", " ax3.text(x, y-5, f'{phrase} {score:.2f}', fontsize=10, color='white',\n", " bbox=dict(boxstyle='round', facecolor=color, alpha=0.8))\n", "\n", "ax3.set_title('Phrase Grounding Visualization\\n(Each box labeled with matched phrase)', \n", " fontsize=11, fontweight='bold')\n", "ax3.axis('off')\n", "\n", "plt.suptitle('Decoder Text Cross-Attention & Phrase Grounding', fontsize=13, fontweight='bold')\n", "plt.tight_layout()\n", "plt.savefig('phrase_grounding_viz.png', dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Attention Rollout Visualization" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Attention rollout: propagate attention through layers\n", "def attention_rollout(attentions, discard_ratio=0.9):\n", " \"\"\"\n", " Compute attention rollout across all layers.\n", " attentions: list of (1, num_heads, L, L) tensors\n", " \"\"\"\n", " result = torch.eye(attentions[0].shape[-1])\n", " \n", " for attn in attentions:\n", " # Average over heads\n", " attn_avg = attn[0].mean(0) # L, L\n", " \n", " # Add residual connection\n", " attn_avg = attn_avg + torch.eye(attn_avg.shape[0])\n", " attn_avg = attn_avg / attn_avg.sum(dim=-1, keepdim=True)\n", " \n", " # Discard low attention\n", " flat = attn_avg.flatten()\n", " threshold = flat.kthvalue(int(flat.shape[0] * discard_ratio)).values\n", " attn_avg[attn_avg < threshold] = 0\n", " \n", " result = torch.matmul(attn_avg, result)\n", " \n", " return result\n", "\n", "# Compute rollout for our BERT query\n", "query = \"the large brown dog sitting on the grass\"\n", "inputs = tokenizer(query, return_tensors='pt')\n", "tokens_r = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])\n", "\n", "with torch.no_grad():\n", " outputs = bert(**inputs)\n", "\n", "rollout = attention_rollout(outputs.attentions)\n", "\n", "fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n", "\n", "# Rollout matrix\n", "ax1 = axes[0]\n", "im = ax1.imshow(rollout.numpy(), cmap='viridis', aspect='equal')\n", "ax1.set_xticks(range(len(tokens_r)))\n", "ax1.set_yticks(range(len(tokens_r)))\n", "ax1.set_xticklabels(tokens_r, rotation=45, ha='right', fontsize=8)\n", "ax1.set_yticklabels(tokens_r, fontsize=8)\n", "ax1.set_title('Attention Rollout\\n(All 12 layers combined)', fontsize=11, fontweight='bold')\n", "plt.colorbar(im, ax=ax1)\n", "\n", "# CLS token attention (sentence-level)\n", "ax2 = axes[1]\n", "cls_attn = rollout[0].numpy() # CLS attends to all tokens\n", "ax2.bar(range(len(tokens_r)), cls_attn, color='blue', alpha=0.7)\n", "ax2.set_xticks(range(len(tokens_r)))\n", "ax2.set_xticklabels(tokens_r, rotation=45, ha='right', fontsize=9)\n", "ax2.set_ylabel('Attention weight')\n", "ax2.set_title('[CLS] Token Attention Rollout\\n(Sentence-level importance)', fontsize=11, fontweight='bold')\n", "ax2.grid(True, alpha=0.3)\n", "\n", "# Token importance for detection\n", "ax3 = axes[2]\n", "# For detection, we care about which tokens are most attended to\n", "token_importance = rollout.sum(0).numpy() # sum over all queries\n", "ax3.bar(range(len(tokens_r)), token_importance, \n", " color=['red' if t in ['dog', 'grass'] else 'blue' for t in tokens_r], alpha=0.7)\n", "ax3.set_xticks(range(len(tokens_r)))\n", "ax3.set_xticklabels(tokens_r, rotation=45, ha='right', fontsize=9)\n", "ax3.set_ylabel('Total attention received')\n", "ax3.set_title('Token Importance\\n(Red = key nouns)', fontsize=11, fontweight='bold')\n", "ax3.grid(True, alpha=0.3)\n", "\n", "plt.suptitle(f'Attention Rollout Analysis\\nQuery: \"{query}\"', fontsize=13, fontweight='bold')\n", "plt.tight_layout()\n", "plt.savefig('attention_rollout.png', dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Swin Window Attention Visualization" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import timm\n", "import torchvision.transforms as TF\n", "\n", "# Load Swin with attention output\n", "swin = timm.create_model('swin_tiny_patch4_window7_224', pretrained=True)\n", "swin.eval()\n", "\n", "# Hook to capture attention weights\n", "attention_maps = {}\n", "\n", "def make_attn_hook(name):\n", " def hook(module, input, output):\n", " # For Swin, attention is inside WindowAttention\n", " if hasattr(module, 'attn') and hasattr(module.attn, 'relative_position_bias_table'):\n", " pass # Would need to modify Swin internals\n", " return hook\n", "\n", "# Load image\n", "img_path = os.path.join(MODEL_DIR, \"sample_cats.jpg\")\n", "if os.path.exists(img_path):\n", " img = Image.open(img_path).convert('RGB')\n", "else:\n", " img = Image.fromarray(np.random.randint(100, 200, (480, 640, 3), dtype=np.uint8))\n", "\n", "transform = TF.Compose([\n", " TF.Resize((224, 224)),\n", " TF.ToTensor(),\n", " TF.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n", "])\n", "img_t = transform(img).unsqueeze(0)\n", "\n", "# Visualize window attention pattern\n", "fig, axes = plt.subplots(2, 4, figsize=(20, 10))\n", "\n", "# Show window partitioning at different scales\n", "img_np = np.array(img.resize((224, 224)))\n", "\n", "for row, (stride, H_feat, W_feat, title) in enumerate([\n", " (4, 56, 56, 'Stage 1 (56×56)'),\n", " (8, 28, 28, 'Stage 2 (28×28)'),\n", "]):\n", " M = 7 # window size\n", " \n", " # Show feature map with window grid\n", " ax = axes[row, 0]\n", " ax.imshow(img_np)\n", " # Draw window boundaries on image\n", " for i in range(0, H_feat, M):\n", " ax.axhline(y=i * stride - 0.5, color='yellow', linewidth=1.5, alpha=0.8)\n", " for j in range(0, W_feat, M):\n", " ax.axvline(x=j * stride - 0.5, color='yellow', linewidth=1.5, alpha=0.8)\n", " ax.set_title(f'{title}\\nWindow grid (M=7)', fontsize=9, fontweight='bold')\n", " ax.axis('off')\n", " \n", " # Simulate attention within 3 windows\n", " for col, (wy, wx) in enumerate([(0, 0), (1, 2), (2, 3)]):\n", " ax = axes[row, col+1]\n", " \n", " # Simulate attention within this window\n", " np.random.seed(wy * 10 + wx)\n", " attn = np.random.rand(M*M, M*M)\n", " attn = np.exp(attn * 3) / np.exp(attn * 3).sum(axis=-1, keepdims=True)\n", " \n", " im = ax.imshow(attn, cmap='hot', aspect='equal', vmin=0)\n", " ax.set_title(f'{title}\\nWindow ({wy},{wx}) attention', fontsize=9, fontweight='bold')\n", " ax.set_xlabel(f'Key ({M}×{M}={M*M} tokens)')\n", " ax.set_ylabel(f'Query')\n", "\n", "plt.suptitle('Swin Window Attention Patterns', fontsize=13, fontweight='bold')\n", "plt.tight_layout()\n", "plt.savefig('swin_window_attention.png', dpi=150, bbox_inches='tight')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "Attention patterns in GroundingDINO:\n", "\n", "1. **BERT self-attention**: Captures syntactic/semantic relationships between text tokens\n", "2. **Swin window attention**: Local attention within 7×7 windows, shifted for cross-window connections\n", "3. **Feature Enhancer cross-attention**: Image regions attend to relevant text tokens (and vice versa)\n", "4. **Decoder text cross-attention**: Each detected box attends to its matching text phrase\n", "5. **Attention rollout**: Propagates attention through all layers for global view\n", "\n", "**Next**: [Notebook 13 - Dataset Download & Preparation](13_dataset_preparation.ipynb)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }