DebasishDhal99 commited on
Commit
0342a67
·
1 Parent(s): ba618db

feat: enhance attention visualization with token context and improve markdown explanations

Browse files
Files changed (3) hide show
  1. app.py +43 -3
  2. src/extract.py +4 -1
  3. src/plots.py +21 -3
app.py CHANGED
@@ -2,6 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import numpy as np
6
  import plotly.graph_objects as go
7
  import gradio as gr
@@ -319,9 +321,21 @@ def update_individual(data, which, head, token, pair, sweep):
319
  return table, rot, sweep_fig
320
 
321
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  def update_attention(data, head, query_token):
323
  if not data:
324
- return PLACEHOLDER, PLACEHOLDER, ""
325
  q_b = select_head(data["q_before"], int(head))
326
  q_a = select_head(data["q_after"], int(head))
327
  k_b_all = expand_kv_heads(data["k_before"], data["n_q_heads"])
@@ -336,7 +350,12 @@ def update_attention(data, head, query_token):
336
  "because `R(m)^T R(n) = R(n−m)`: the score depends on the position difference, "
337
  "not on absolute indices alone."
338
  )
339
- return attention_heatmaps(sb, sa), attention_bars(sb[qt], sa[qt], qt), note
 
 
 
 
 
340
 
341
 
342
  def update_compare(data):
@@ -440,10 +459,31 @@ with gr.Blocks(title="RoPE Explorer") as demo:
440
  sweep_plot = gr.Plot()
441
 
442
  with gr.Tab("Attention effect"):
 
 
 
443
  query_token = gr.Slider(0, 15, step=1, value=0, label="Query token")
444
  attn_heat = gr.Plot()
445
  attn_bar = gr.Plot()
446
  attn_note = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
  with gr.Tab("Compare to additive PE"):
449
  pe_heat = gr.Plot()
@@ -502,7 +542,7 @@ with gr.Blocks(title="RoPE Explorer") as demo:
502
  ctrl.change(update_individual, inputs=ind_inputs, outputs=ind_outputs)
503
 
504
  attn_inputs = [state, head, query_token]
505
- attn_outputs = [attn_heat, attn_bar, attn_note]
506
  for ctrl in attn_inputs:
507
  ctrl.change(update_attention, inputs=attn_inputs, outputs=attn_outputs)
508
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ from html import escape
6
+
7
  import numpy as np
8
  import plotly.graph_objects as go
9
  import gradio as gr
 
321
  return table, rot, sweep_fig
322
 
323
 
324
+ def _attention_context_markdown(data: dict | None) -> str:
325
+ if not data:
326
+ return "Compute on the Setup tab to see the sentence and tokenization."
327
+ text = data.get("text") or "Random matrix mode does not use a sentence."
328
+ token_lines = " | ".join(f"{i}: {token}" for i, token in enumerate(data["tokens"]))
329
+ return (
330
+ "### Input sentence and tokens\n\n"
331
+ f"**Sentence:** <code>{escape(str(text))}</code>\n\n"
332
+ f"**Tokenized form (index: token):** <code>{escape(token_lines)}</code>"
333
+ )
334
+
335
+
336
  def update_attention(data, head, query_token):
337
  if not data:
338
+ return PLACEHOLDER, PLACEHOLDER, "", _attention_context_markdown(None)
339
  q_b = select_head(data["q_before"], int(head))
340
  q_a = select_head(data["q_after"], int(head))
341
  k_b_all = expand_kv_heads(data["k_before"], data["n_q_heads"])
 
350
  "because `R(m)^T R(n) = R(n−m)`: the score depends on the position difference, "
351
  "not on absolute indices alone."
352
  )
353
+ return (
354
+ attention_heatmaps(sb, sa, tokens=data["tokens"]),
355
+ attention_bars(sb[qt], sa[qt], qt),
356
+ note,
357
+ _attention_context_markdown(data),
358
+ )
359
 
360
 
361
  def update_compare(data):
 
459
  sweep_plot = gr.Plot()
460
 
461
  with gr.Tab("Attention effect"):
462
+ attention_context = gr.Markdown(
463
+ "Compute on the Setup tab to see the sentence and tokenization."
464
+ )
465
  query_token = gr.Slider(0, 15, step=1, value=0, label="Query token")
466
  attn_heat = gr.Plot()
467
  attn_bar = gr.Plot()
468
  attn_note = gr.Markdown()
469
+ gr.Markdown(
470
+ """
471
+ ### How to read an attention score
472
+
473
+ Each heatmap cell is the raw dot product `Q_query · K_key` for the query token on
474
+ the y-axis and key token on the x-axis:
475
+
476
+ - **Zero** means the two vectors are orthogonal, so this query/key pair has no directional match.
477
+ - **Positive** means the vectors point partly in the same direction, indicating a compatible match.
478
+ - **Negative** means the vectors point partly in opposite directions, indicating an incompatible match.
479
+ - **Magnitude** shows how strong the alignment or opposition is. Larger absolute values mean a stronger raw signal.
480
+
481
+ These are raw, unnormalized scores, not probabilities. Compare scores within the
482
+ same query row; the model would apply softmax across that row to turn them into
483
+ relative attention weights. Vector lengths also affect the magnitude, so a larger
484
+ score does not represent a universal threshold of importance.
485
+ """
486
+ )
487
 
488
  with gr.Tab("Compare to additive PE"):
489
  pe_heat = gr.Plot()
 
542
  ctrl.change(update_individual, inputs=ind_inputs, outputs=ind_outputs)
543
 
544
  attn_inputs = [state, head, query_token]
545
+ attn_outputs = [attn_heat, attn_bar, attn_note, attention_context]
546
  for ctrl in attn_inputs:
547
  ctrl.change(update_attention, inputs=attn_inputs, outputs=attn_outputs)
548
 
src/extract.py CHANGED
@@ -20,7 +20,7 @@ MODEL_CHOICES = [
20
  "Qwen/Qwen2.5-0.5B-Instruct",
21
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
22
  ]
23
- DEFAULT_MODEL = MODEL_CHOICES[1]
24
 
25
  _cache: dict[str, Any] = {"name": None, "model": None, "tokenizer": None}
26
  _config_cache: dict[str, Any] = {}
@@ -71,6 +71,7 @@ def _pack_tensors(
71
  checksum: float | None,
72
  source: str,
73
  model_name: str | None,
 
74
  q_after_model: np.ndarray | None = None,
75
  k_after_model: np.ndarray | None = None,
76
  ) -> dict[str, Any]:
@@ -92,6 +93,7 @@ def _pack_tensors(
92
  "checksum": checksum,
93
  "source": source,
94
  "model_name": model_name,
 
95
  }
96
 
97
 
@@ -296,6 +298,7 @@ def extract_from_model(model_name: str, sentence: str) -> dict[str, Any]:
296
  checksum=checksum,
297
  source="model",
298
  model_name=model_name,
 
299
  q_after_model=q_model_np,
300
  k_after_model=k_model_np,
301
  )
 
20
  "Qwen/Qwen2.5-0.5B-Instruct",
21
  "TinyLlama/TinyLlama-1.1B-Chat-v1.0",
22
  ]
23
+ DEFAULT_MODEL = MODEL_CHOICES[0]
24
 
25
  _cache: dict[str, Any] = {"name": None, "model": None, "tokenizer": None}
26
  _config_cache: dict[str, Any] = {}
 
71
  checksum: float | None,
72
  source: str,
73
  model_name: str | None,
74
+ text: str | None = None,
75
  q_after_model: np.ndarray | None = None,
76
  k_after_model: np.ndarray | None = None,
77
  ) -> dict[str, Any]:
 
93
  "checksum": checksum,
94
  "source": source,
95
  "model_name": model_name,
96
+ "text": text,
97
  }
98
 
99
 
 
298
  checksum=checksum,
299
  source="model",
300
  model_name=model_name,
301
+ text=text,
302
  q_after_model=q_model_np,
303
  k_after_model=k_model_np,
304
  )
src/plots.py CHANGED
@@ -239,14 +239,32 @@ def position_sweep(
239
  return fig
240
 
241
 
242
- def attention_heatmaps(scores_before: np.ndarray, scores_after: np.ndarray) -> go.Figure:
 
 
 
 
243
  fig = make_subplots(rows=1, cols=2, subplot_titles=["QKᵀ without RoPE", "QKᵀ with RoPE"])
 
 
 
 
 
 
 
244
  for i, mat in enumerate([scores_before, scores_after], start=1):
245
  fig.add_trace(
246
  go.Heatmap(
247
- z=downsample(mat),
 
 
 
248
  showscale=(i == 2),
249
- hovertemplate="query=%{y}<br>key=%{x}<br>score=%{z:.4f}<extra></extra>",
 
 
 
 
250
  ),
251
  row=1,
252
  col=i,
 
239
  return fig
240
 
241
 
242
+ def attention_heatmaps(
243
+ scores_before: np.ndarray,
244
+ scores_after: np.ndarray,
245
+ tokens=None,
246
+ ) -> go.Figure:
247
  fig = make_subplots(rows=1, cols=2, subplot_titles=["QKᵀ without RoPE", "QKᵀ with RoPE"])
248
+ seq_len = scores_before.shape[0]
249
+ row_step = max(1, int(np.ceil(seq_len / 64)))
250
+ indices = np.arange(0, seq_len, row_step)
251
+ labels = [str(tokens[i]) if tokens is not None else f"token {i}" for i in indices]
252
+ customdata = np.empty((len(indices), len(indices), 2), dtype=object)
253
+ customdata[:, :, 0] = np.asarray(labels)[:, None]
254
+ customdata[:, :, 1] = np.asarray(labels)[None, :]
255
  for i, mat in enumerate([scores_before, scores_after], start=1):
256
  fig.add_trace(
257
  go.Heatmap(
258
+ z=np.asarray(mat)[::row_step, ::row_step],
259
+ x=indices,
260
+ y=indices,
261
+ customdata=customdata,
262
  showscale=(i == 2),
263
+ hovertemplate=(
264
+ "query position k=%{y}<br>query token=%{customdata[0]}<br>"
265
+ "key position k=%{x}<br>key token=%{customdata[1]}<br>"
266
+ "Q·K score=%{z:.4f}<extra></extra>"
267
+ ),
268
  ),
269
  row=1,
270
  col=i,