Spaces:
Running on Zero
Running on Zero
Commit ·
0342a67
1
Parent(s): ba618db
feat: enhance attention visualization with token context and improve markdown explanations
Browse files- app.py +43 -3
- src/extract.py +4 -1
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
|
|
|
|
|
|
|
|
|
| 248 |
showscale=(i == 2),
|
| 249 |
-
hovertemplate=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|