Spaces:
Running
Running
Commit ·
8a1fc4b
1
Parent(s): ef749ae
Refactor analysis scope and enhance attention UI (Agents F and G)
Browse files- app.py +112 -73
- components/pipeline.py +60 -34
- todo.md +15 -0
app.py
CHANGED
|
@@ -11,7 +11,8 @@ import json
|
|
| 11 |
import torch
|
| 12 |
from utils import (load_model_and_get_patterns, execute_forward_pass, extract_layer_data,
|
| 13 |
categorize_single_layer_heads, perform_beam_search,
|
| 14 |
-
execute_forward_pass_with_head_ablation, evaluate_sequence_ablation, score_sequence
|
|
|
|
| 15 |
from utils.model_config import get_auto_selections
|
| 16 |
from utils.token_attribution import compute_integrated_gradients, compute_simple_gradient_attribution
|
| 17 |
|
|
@@ -52,6 +53,9 @@ app.layout = html.Div([
|
|
| 52 |
dcc.Store(id='generation-results-store', storage_type='session'),
|
| 53 |
dcc.Store(id='investigation-active-tab', storage_type='session', data='ablation'),
|
| 54 |
dcc.Store(id='ablation-selected-heads', storage_type='session', data=[]),
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
# Main container
|
| 57 |
html.Div([
|
|
@@ -304,7 +308,9 @@ def enable_run_button(model, prompt, block_modules, norm_params):
|
|
| 304 |
Output('generation-results-store', 'data'),
|
| 305 |
Output('pipeline-container', 'style'),
|
| 306 |
Output('investigation-panel', 'style'),
|
| 307 |
-
Output('session-activation-store', 'data', allow_duplicate=True)
|
|
|
|
|
|
|
| 308 |
[Input('generate-btn', 'n_clicks')],
|
| 309 |
[State('model-dropdown', 'value'),
|
| 310 |
State('prompt-input', 'value'),
|
|
@@ -317,8 +323,15 @@ def enable_run_button(model, prompt, block_modules, norm_params):
|
|
| 317 |
prevent_initial_call=True
|
| 318 |
)
|
| 319 |
def run_generation(n_clicks, model_name, prompt, max_new_tokens, beam_width, patterns_data, attn_patterns, block_patterns, norm_patterns):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
if not n_clicks or not model_name or not prompt:
|
| 321 |
-
return no_update, no_update, no_update, no_update, no_update
|
| 322 |
|
| 323 |
try:
|
| 324 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
@@ -326,96 +339,92 @@ def run_generation(n_clicks, model_name, prompt, max_new_tokens, beam_width, pat
|
|
| 326 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 327 |
model.eval()
|
| 328 |
|
|
|
|
| 329 |
results = perform_beam_search(model, tokenizer, prompt, beam_width, max_new_tokens)
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
results_ui = []
|
| 332 |
if max_new_tokens > 1:
|
|
|
|
| 333 |
results_ui.append(html.H4("Generated Sequences", className="section-title"))
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
| 339 |
html.Div(result['text'], style={'fontFamily': 'monospace', 'backgroundColor': '#fff', 'padding': '10px', 'borderRadius': '4px', 'border': '1px solid #dee2e6'}),
|
| 340 |
-
html.Button("
|
| 341 |
className="action-button secondary-button", style={'marginTop': '10px', 'fontSize': '12px'})
|
| 342 |
], style={'marginBottom': '15px', 'padding': '15px', 'backgroundColor': '#f8f9fa', 'borderRadius': '6px'}))
|
| 343 |
-
return results_ui, results, {'display': 'none'}, {'display': 'none'}, {}
|
| 344 |
-
|
| 345 |
-
else:
|
| 346 |
-
result = results[0]
|
| 347 |
-
module_patterns = patterns_data.get('module_patterns', {})
|
| 348 |
-
param_patterns = patterns_data.get('param_patterns', {})
|
| 349 |
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
'norm_parameters': [param for pattern in (norm_patterns or []) for param in param_patterns.get(pattern, [])]
|
| 354 |
-
}
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
|
| 362 |
except Exception as e:
|
| 363 |
import traceback
|
| 364 |
traceback.print_exc()
|
| 365 |
-
return html.Div(f"Error: {e}", style={'color': 'red'}), [],
|
|
|
|
| 366 |
|
| 367 |
|
| 368 |
@app.callback(
|
| 369 |
-
|
| 370 |
-
Output('pipeline-container', 'style', allow_duplicate=True),
|
| 371 |
-
Output('investigation-panel', 'style', allow_duplicate=True)],
|
| 372 |
Input({'type': 'result-item', 'index': ALL}, 'n_clicks'),
|
| 373 |
-
[State('generation-results-store', 'data'),
|
| 374 |
-
State('model-dropdown', 'value'),
|
| 375 |
-
State('session-patterns-store', 'data'),
|
| 376 |
-
State('attention-modules-dropdown', 'value'),
|
| 377 |
-
State('block-modules-dropdown', 'value'),
|
| 378 |
-
State('norm-params-dropdown', 'value')],
|
| 379 |
prevent_initial_call=True
|
| 380 |
)
|
| 381 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
if not any(n_clicks_list) or not results_data:
|
| 383 |
-
return no_update
|
| 384 |
|
| 385 |
ctx = dash.callback_context
|
| 386 |
if not ctx.triggered:
|
| 387 |
-
return no_update
|
| 388 |
|
| 389 |
triggered_id = json.loads(ctx.triggered[0]['prop_id'].split('.')[0])
|
| 390 |
index = triggered_id['index']
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
model = AutoModelForCausalLM.from_pretrained(model_name, attn_implementation='eager')
|
| 397 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 398 |
-
model.eval()
|
| 399 |
-
|
| 400 |
-
module_patterns = patterns_data.get('module_patterns', {})
|
| 401 |
-
param_patterns = patterns_data.get('param_patterns', {})
|
| 402 |
-
|
| 403 |
-
config = {
|
| 404 |
-
'attention_modules': [mod for pattern in (attn_patterns or []) for mod in module_patterns.get(pattern, [])],
|
| 405 |
-
'block_modules': [mod for pattern in (block_patterns or []) for mod in module_patterns.get(pattern, [])],
|
| 406 |
-
'norm_parameters': [param for pattern in (norm_patterns or []) for param in param_patterns.get(pattern, [])]
|
| 407 |
-
}
|
| 408 |
-
|
| 409 |
-
if not config['block_modules']:
|
| 410 |
-
return no_update, no_update, no_update
|
| 411 |
-
|
| 412 |
-
activation_data = execute_forward_pass(model, tokenizer, result['text'], config)
|
| 413 |
-
return activation_data, {'display': 'block'}, {'display': 'block'}
|
| 414 |
-
|
| 415 |
-
except Exception as e:
|
| 416 |
-
import traceback
|
| 417 |
-
traceback.print_exc()
|
| 418 |
-
return no_update, no_update, no_update
|
| 419 |
|
| 420 |
|
| 421 |
# ============================================================================
|
|
@@ -487,6 +496,13 @@ def update_pipeline_content(activation_data, model_name):
|
|
| 487 |
except:
|
| 488 |
pass
|
| 489 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
# Build outputs for each stage
|
| 491 |
outputs = []
|
| 492 |
|
|
@@ -498,9 +514,9 @@ def update_pipeline_content(activation_data, model_name):
|
|
| 498 |
outputs.append(f"{hidden_dim}-dim vectors")
|
| 499 |
outputs.append(create_embedding_content(hidden_dim, len(tokens)))
|
| 500 |
|
| 501 |
-
# Stage 3: Attention
|
| 502 |
outputs.append(f"{num_heads} heads × {num_layers} layers")
|
| 503 |
-
outputs.append(create_attention_content(attention_html,
|
| 504 |
|
| 505 |
# Stage 4: MLP
|
| 506 |
outputs.append(f"{num_layers} layers")
|
|
@@ -663,10 +679,17 @@ def handle_ablation_head_selection(n_clicks_list, selected_heads):
|
|
| 663 |
State('ablation-selected-heads', 'data'),
|
| 664 |
State('session-activation-store', 'data'),
|
| 665 |
State('model-dropdown', 'value'),
|
| 666 |
-
State('prompt-input', 'value')
|
|
|
|
| 667 |
prevent_initial_call=True
|
| 668 |
)
|
| 669 |
-
def run_ablation_experiment(n_clicks, layer_num, selected_heads, activation_data, model_name, prompt):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
if not n_clicks or layer_num is None or not selected_heads or not activation_data:
|
| 671 |
return no_update
|
| 672 |
|
|
@@ -676,6 +699,7 @@ def run_ablation_experiment(n_clicks, layer_num, selected_heads, activation_data
|
|
| 676 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 677 |
model.eval()
|
| 678 |
|
|
|
|
| 679 |
sequence_text = activation_data.get('prompt', prompt)
|
| 680 |
|
| 681 |
config = {
|
|
@@ -684,12 +708,12 @@ def run_ablation_experiment(n_clicks, layer_num, selected_heads, activation_data
|
|
| 684 |
'norm_parameters': activation_data.get('norm_parameters', [])
|
| 685 |
}
|
| 686 |
|
| 687 |
-
# Get original output
|
| 688 |
original_output = activation_data.get('actual_output', {})
|
| 689 |
original_token = original_output.get('token', '')
|
| 690 |
original_prob = original_output.get('probability', 0)
|
| 691 |
|
| 692 |
-
# Run ablation
|
| 693 |
ablated_data = execute_forward_pass_with_head_ablation(
|
| 694 |
model, tokenizer, sequence_text, config, layer_num, selected_heads
|
| 695 |
)
|
|
@@ -698,10 +722,25 @@ def run_ablation_experiment(n_clicks, layer_num, selected_heads, activation_data
|
|
| 698 |
ablated_token = ablated_output.get('token', '')
|
| 699 |
ablated_prob = ablated_output.get('probability', 0)
|
| 700 |
|
| 701 |
-
|
|
|
|
| 702 |
original_token, ablated_token, original_prob, ablated_prob,
|
| 703 |
layer_num, selected_heads
|
| 704 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
|
| 706 |
except Exception as e:
|
| 707 |
import traceback
|
|
|
|
| 11 |
import torch
|
| 12 |
from utils import (load_model_and_get_patterns, execute_forward_pass, extract_layer_data,
|
| 13 |
categorize_single_layer_heads, perform_beam_search,
|
| 14 |
+
execute_forward_pass_with_head_ablation, evaluate_sequence_ablation, score_sequence,
|
| 15 |
+
get_head_category_counts)
|
| 16 |
from utils.model_config import get_auto_selections
|
| 17 |
from utils.token_attribution import compute_integrated_gradients, compute_simple_gradient_attribution
|
| 18 |
|
|
|
|
| 53 |
dcc.Store(id='generation-results-store', storage_type='session'),
|
| 54 |
dcc.Store(id='investigation-active-tab', storage_type='session', data='ablation'),
|
| 55 |
dcc.Store(id='ablation-selected-heads', storage_type='session', data=[]),
|
| 56 |
+
# Agent F: Stores for separating original prompt analysis from beam generation
|
| 57 |
+
dcc.Store(id='session-original-prompt-store', storage_type='memory'), # Original user prompt
|
| 58 |
+
dcc.Store(id='session-selected-beam-store', storage_type='memory'), # Selected beam for comparison
|
| 59 |
|
| 60 |
# Main container
|
| 61 |
html.Div([
|
|
|
|
| 308 |
Output('generation-results-store', 'data'),
|
| 309 |
Output('pipeline-container', 'style'),
|
| 310 |
Output('investigation-panel', 'style'),
|
| 311 |
+
Output('session-activation-store', 'data', allow_duplicate=True),
|
| 312 |
+
Output('session-original-prompt-store', 'data'),
|
| 313 |
+
Output('session-selected-beam-store', 'data')],
|
| 314 |
[Input('generate-btn', 'n_clicks')],
|
| 315 |
[State('model-dropdown', 'value'),
|
| 316 |
State('prompt-input', 'value'),
|
|
|
|
| 323 |
prevent_initial_call=True
|
| 324 |
)
|
| 325 |
def run_generation(n_clicks, model_name, prompt, max_new_tokens, beam_width, patterns_data, attn_patterns, block_patterns, norm_patterns):
|
| 326 |
+
"""
|
| 327 |
+
Run generation and analysis.
|
| 328 |
+
|
| 329 |
+
Agent F refactor: Pipeline analysis (tokenization, embedding, attention, MLP, output)
|
| 330 |
+
always runs on the ORIGINAL PROMPT only. Beam generation results are stored separately
|
| 331 |
+
for comparison in experiments.
|
| 332 |
+
"""
|
| 333 |
if not n_clicks or not model_name or not prompt:
|
| 334 |
+
return no_update, no_update, no_update, no_update, no_update, no_update, no_update
|
| 335 |
|
| 336 |
try:
|
| 337 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
|
|
| 339 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 340 |
model.eval()
|
| 341 |
|
| 342 |
+
# Always run beam search (even with max_new_tokens=1)
|
| 343 |
results = perform_beam_search(model, tokenizer, prompt, beam_width, max_new_tokens)
|
| 344 |
|
| 345 |
+
# Store original prompt for reference
|
| 346 |
+
original_prompt_data = {'prompt': prompt, 'model': model_name}
|
| 347 |
+
|
| 348 |
+
# Build module config for analysis
|
| 349 |
+
module_patterns = patterns_data.get('module_patterns', {})
|
| 350 |
+
param_patterns = patterns_data.get('param_patterns', {})
|
| 351 |
+
|
| 352 |
+
config = {
|
| 353 |
+
'attention_modules': [mod for pattern in (attn_patterns or []) for mod in module_patterns.get(pattern, [])],
|
| 354 |
+
'block_modules': [mod for pattern in (block_patterns or []) for mod in module_patterns.get(pattern, [])],
|
| 355 |
+
'norm_parameters': [param for pattern in (norm_patterns or []) for param in param_patterns.get(pattern, [])]
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
if not config['block_modules']:
|
| 359 |
+
return (html.Div("Please select modules in the sidebar.", style={'color': 'red'}),
|
| 360 |
+
results, {'display': 'none'}, {'display': 'none'}, {}, original_prompt_data, {})
|
| 361 |
+
|
| 362 |
+
# AGENT F KEY CHANGE: Run analysis on ORIGINAL PROMPT only, not generated text
|
| 363 |
+
# This ensures pipeline stages show how the model processes the user's input,
|
| 364 |
+
# regardless of what tokens are generated.
|
| 365 |
+
activation_data = execute_forward_pass(model, tokenizer, prompt, config)
|
| 366 |
+
|
| 367 |
results_ui = []
|
| 368 |
if max_new_tokens > 1:
|
| 369 |
+
# Show generated sequences for user selection
|
| 370 |
results_ui.append(html.H4("Generated Sequences", className="section-title"))
|
| 371 |
+
results_ui.append(html.P("Select a sequence to store for comparison after experiments.",
|
| 372 |
+
style={'color': '#6c757d', 'fontSize': '13px', 'marginBottom': '12px'}))
|
| 373 |
+
for i, result in enumerate(results):
|
| 374 |
+
results_ui.append(html.Div([
|
| 375 |
+
html.Div([
|
| 376 |
+
html.Span(f"Rank {i+1}", style={'fontWeight': 'bold', 'marginRight': '10px', 'color': '#667eea'})
|
| 377 |
+
], style={'marginBottom': '5px'}),
|
| 378 |
html.Div(result['text'], style={'fontFamily': 'monospace', 'backgroundColor': '#fff', 'padding': '10px', 'borderRadius': '4px', 'border': '1px solid #dee2e6'}),
|
| 379 |
+
html.Button("Select for Comparison", id={'type': 'result-item', 'index': i}, n_clicks=0,
|
| 380 |
className="action-button secondary-button", style={'marginTop': '10px', 'fontSize': '12px'})
|
| 381 |
], style={'marginBottom': '15px', 'padding': '15px', 'backgroundColor': '#f8f9fa', 'borderRadius': '6px'}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
|
| 383 |
+
# Show pipeline immediately (analyzing original prompt)
|
| 384 |
+
return (results_ui, results, {'display': 'block'}, {'display': 'block'},
|
| 385 |
+
activation_data, original_prompt_data, {})
|
|
|
|
|
|
|
| 386 |
|
| 387 |
+
else:
|
| 388 |
+
# Single token generation - store the result as selected beam
|
| 389 |
+
selected_beam_data = {'text': results[0]['text'], 'score': results[0].get('score', 0)}
|
| 390 |
+
return (results_ui, results, {'display': 'block'}, {'display': 'block'},
|
| 391 |
+
activation_data, original_prompt_data, selected_beam_data)
|
| 392 |
|
| 393 |
except Exception as e:
|
| 394 |
import traceback
|
| 395 |
traceback.print_exc()
|
| 396 |
+
return (html.Div(f"Error: {e}", style={'color': 'red'}), [],
|
| 397 |
+
{'display': 'none'}, {'display': 'none'}, {}, {}, {})
|
| 398 |
|
| 399 |
|
| 400 |
@app.callback(
|
| 401 |
+
Output('session-selected-beam-store', 'data', allow_duplicate=True),
|
|
|
|
|
|
|
| 402 |
Input({'type': 'result-item', 'index': ALL}, 'n_clicks'),
|
| 403 |
+
[State('generation-results-store', 'data')],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
prevent_initial_call=True
|
| 405 |
)
|
| 406 |
+
def store_selected_beam(n_clicks_list, results_data):
|
| 407 |
+
"""
|
| 408 |
+
Agent F: Store selected beam for post-experiment comparison.
|
| 409 |
+
|
| 410 |
+
The pipeline analysis already runs on the original prompt. This callback
|
| 411 |
+
just stores the selected beam text so we can compare ablation/attribution
|
| 412 |
+
results against the original generation.
|
| 413 |
+
"""
|
| 414 |
if not any(n_clicks_list) or not results_data:
|
| 415 |
+
return no_update
|
| 416 |
|
| 417 |
ctx = dash.callback_context
|
| 418 |
if not ctx.triggered:
|
| 419 |
+
return no_update
|
| 420 |
|
| 421 |
triggered_id = json.loads(ctx.triggered[0]['prop_id'].split('.')[0])
|
| 422 |
index = triggered_id['index']
|
| 423 |
|
| 424 |
+
result = results_data[index]
|
| 425 |
+
|
| 426 |
+
# Store the selected beam for comparison after experiments
|
| 427 |
+
return {'text': result['text'], 'score': result.get('score', 0), 'index': index}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
|
| 430 |
# ============================================================================
|
|
|
|
| 496 |
except:
|
| 497 |
pass
|
| 498 |
|
| 499 |
+
# Agent G: Get head categorization for attention stage UI
|
| 500 |
+
head_categories = None
|
| 501 |
+
try:
|
| 502 |
+
head_categories = get_head_category_counts(activation_data)
|
| 503 |
+
except:
|
| 504 |
+
pass
|
| 505 |
+
|
| 506 |
# Build outputs for each stage
|
| 507 |
outputs = []
|
| 508 |
|
|
|
|
| 514 |
outputs.append(f"{hidden_dim}-dim vectors")
|
| 515 |
outputs.append(create_embedding_content(hidden_dim, len(tokens)))
|
| 516 |
|
| 517 |
+
# Stage 3: Attention (Agent G: now includes head_categories)
|
| 518 |
outputs.append(f"{num_heads} heads × {num_layers} layers")
|
| 519 |
+
outputs.append(create_attention_content(attention_html, None, head_categories=head_categories))
|
| 520 |
|
| 521 |
# Stage 4: MLP
|
| 522 |
outputs.append(f"{num_layers} layers")
|
|
|
|
| 679 |
State('ablation-selected-heads', 'data'),
|
| 680 |
State('session-activation-store', 'data'),
|
| 681 |
State('model-dropdown', 'value'),
|
| 682 |
+
State('prompt-input', 'value'),
|
| 683 |
+
State('session-selected-beam-store', 'data')],
|
| 684 |
prevent_initial_call=True
|
| 685 |
)
|
| 686 |
+
def run_ablation_experiment(n_clicks, layer_num, selected_heads, activation_data, model_name, prompt, selected_beam):
|
| 687 |
+
"""
|
| 688 |
+
Agent F: Run ablation on ORIGINAL PROMPT and compare results.
|
| 689 |
+
|
| 690 |
+
Shows how ablating attention heads affects the model's prediction on the
|
| 691 |
+
original input. If a beam was selected, shows it for context.
|
| 692 |
+
"""
|
| 693 |
if not n_clicks or layer_num is None or not selected_heads or not activation_data:
|
| 694 |
return no_update
|
| 695 |
|
|
|
|
| 699 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 700 |
model.eval()
|
| 701 |
|
| 702 |
+
# Agent F: Always analyze the original prompt
|
| 703 |
sequence_text = activation_data.get('prompt', prompt)
|
| 704 |
|
| 705 |
config = {
|
|
|
|
| 708 |
'norm_parameters': activation_data.get('norm_parameters', [])
|
| 709 |
}
|
| 710 |
|
| 711 |
+
# Get original output (from analysis of original prompt)
|
| 712 |
original_output = activation_data.get('actual_output', {})
|
| 713 |
original_token = original_output.get('token', '')
|
| 714 |
original_prob = original_output.get('probability', 0)
|
| 715 |
|
| 716 |
+
# Run ablation on original prompt
|
| 717 |
ablated_data = execute_forward_pass_with_head_ablation(
|
| 718 |
model, tokenizer, sequence_text, config, layer_num, selected_heads
|
| 719 |
)
|
|
|
|
| 722 |
ablated_token = ablated_output.get('token', '')
|
| 723 |
ablated_prob = ablated_output.get('probability', 0)
|
| 724 |
|
| 725 |
+
# Build results display
|
| 726 |
+
results = [create_ablation_results_display(
|
| 727 |
original_token, ablated_token, original_prob, ablated_prob,
|
| 728 |
layer_num, selected_heads
|
| 729 |
+
)]
|
| 730 |
+
|
| 731 |
+
# Agent F: If a beam was selected, show context for comparison
|
| 732 |
+
if selected_beam and selected_beam.get('text'):
|
| 733 |
+
results.append(html.Div([
|
| 734 |
+
html.Hr(style={'margin': '15px 0', 'borderColor': '#dee2e6'}),
|
| 735 |
+
html.Div([
|
| 736 |
+
html.I(className='fas fa-info-circle', style={'color': '#6c757d', 'marginRight': '8px'}),
|
| 737 |
+
html.Span("Selected generation for reference: ", style={'fontWeight': '500', 'color': '#495057'}),
|
| 738 |
+
html.Span(selected_beam['text'], style={'fontFamily': 'monospace', 'backgroundColor': '#f8f9fa',
|
| 739 |
+
'padding': '4px 8px', 'borderRadius': '4px'})
|
| 740 |
+
], style={'fontSize': '13px'})
|
| 741 |
+
]))
|
| 742 |
+
|
| 743 |
+
return html.Div(results)
|
| 744 |
|
| 745 |
except Exception as e:
|
| 746 |
import traceback
|
components/pipeline.py
CHANGED
|
@@ -345,14 +345,18 @@ def create_embedding_content(hidden_dim=None, num_tokens=None):
|
|
| 345 |
])
|
| 346 |
|
| 347 |
|
| 348 |
-
def create_attention_content(attention_html=None, top_attended=None, layer_info=None):
|
| 349 |
"""
|
| 350 |
Create content for the attention stage.
|
| 351 |
|
|
|
|
|
|
|
|
|
|
| 352 |
Args:
|
| 353 |
attention_html: BertViz HTML string for attention visualization
|
| 354 |
-
top_attended:
|
| 355 |
layer_info: Optional layer information for context
|
|
|
|
| 356 |
"""
|
| 357 |
content_items = [
|
| 358 |
html.Div([
|
|
@@ -369,55 +373,77 @@ def create_attention_content(attention_html=None, top_attended=None, layer_info=
|
|
| 369 |
])
|
| 370 |
]
|
| 371 |
|
| 372 |
-
#
|
| 373 |
-
if
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
'padding': '6px 12px',
|
| 382 |
-
'backgroundColor': '#f093fb20',
|
| 383 |
-
'border': '1px solid #f093fb40',
|
| 384 |
-
'borderRadius': '16px',
|
| 385 |
-
'marginRight': '8px',
|
| 386 |
-
'display': 'inline-block',
|
| 387 |
-
'marginBottom': '6px'
|
| 388 |
-
})
|
| 389 |
-
)
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
-
# BertViz visualization
|
| 399 |
if attention_html:
|
| 400 |
-
#
|
| 401 |
content_items.append(
|
| 402 |
html.Div([
|
| 403 |
-
html.H5("How to
|
| 404 |
html.Div([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
html.Div([
|
| 406 |
html.I(className='fas fa-arrows-alt-h', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 407 |
html.Strong("Lines show attention: "),
|
| 408 |
-
html.Span("Each line connects a token (left) to
|
| 409 |
style={'color': '#6c757d'})
|
| 410 |
], style={'marginBottom': '8px'}),
|
| 411 |
html.Div([
|
| 412 |
html.I(className='fas fa-paint-brush', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 413 |
html.Strong("Line thickness = attention strength: "),
|
| 414 |
-
html.Span("Thicker, darker lines mean stronger attention.
|
| 415 |
style={'color': '#6c757d'})
|
| 416 |
], style={'marginBottom': '8px'}),
|
| 417 |
html.Div([
|
| 418 |
-
html.I(className='fas fa-
|
| 419 |
-
html.Strong("
|
| 420 |
-
html.Span("
|
| 421 |
style={'color': '#6c757d'})
|
| 422 |
])
|
| 423 |
], style={'padding': '12px', 'backgroundColor': '#fdf4ff', 'borderRadius': '6px', 'marginBottom': '16px'})
|
|
|
|
| 345 |
])
|
| 346 |
|
| 347 |
|
| 348 |
+
def create_attention_content(attention_html=None, top_attended=None, layer_info=None, head_categories=None):
|
| 349 |
"""
|
| 350 |
Create content for the attention stage.
|
| 351 |
|
| 352 |
+
Agent G: Removed "Most attended tokens" section (deprecated). Now shows head categorization
|
| 353 |
+
to help users understand what different attention heads are doing.
|
| 354 |
+
|
| 355 |
Args:
|
| 356 |
attention_html: BertViz HTML string for attention visualization
|
| 357 |
+
top_attended: DEPRECATED - no longer used, kept for backward compatibility
|
| 358 |
layer_info: Optional layer information for context
|
| 359 |
+
head_categories: Optional dict mapping category names to counts (from get_head_category_counts)
|
| 360 |
"""
|
| 361 |
content_items = [
|
| 362 |
html.Div([
|
|
|
|
| 373 |
])
|
| 374 |
]
|
| 375 |
|
| 376 |
+
# Agent G: Head Categorization Summary (replaces deprecated "Most attended tokens")
|
| 377 |
+
if head_categories:
|
| 378 |
+
category_labels = {
|
| 379 |
+
'previous_token': ('Previous-Token', '#667eea', 'Heads that attend to the immediately preceding token'),
|
| 380 |
+
'first_token': ('First/Positional', '#764ba2', 'Heads that focus on the first token or positional patterns'),
|
| 381 |
+
'bow': ('Bag-of-Words', '#f093fb', 'Heads with diffuse attention across many tokens'),
|
| 382 |
+
'syntactic': ('Syntactic', '#4facfe', 'Heads that capture grammatical relationships'),
|
| 383 |
+
'other': ('Other', '#6c757d', 'Heads with mixed or specialized patterns')
|
| 384 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
|
| 386 |
+
category_chips = []
|
| 387 |
+
for cat_key, count in head_categories.items():
|
| 388 |
+
if count > 0 and cat_key in category_labels:
|
| 389 |
+
label, color, tooltip = category_labels[cat_key]
|
| 390 |
+
category_chips.append(
|
| 391 |
+
html.Span([
|
| 392 |
+
html.Span(label, style={'fontWeight': '500'}),
|
| 393 |
+
html.Span(f" ({count})", style={'marginLeft': '4px'})
|
| 394 |
+
], style={
|
| 395 |
+
'padding': '6px 12px',
|
| 396 |
+
'backgroundColor': f'{color}20',
|
| 397 |
+
'border': f'1px solid {color}40',
|
| 398 |
+
'borderRadius': '16px',
|
| 399 |
+
'marginRight': '8px',
|
| 400 |
+
'display': 'inline-block',
|
| 401 |
+
'marginBottom': '6px',
|
| 402 |
+
'cursor': 'help'
|
| 403 |
+
}, title=tooltip)
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
if category_chips:
|
| 407 |
+
content_items.append(
|
| 408 |
+
html.Div([
|
| 409 |
+
html.H5("Attention Head Categories:", style={'color': '#495057', 'marginBottom': '12px'}),
|
| 410 |
+
html.Div(category_chips),
|
| 411 |
+
html.P([
|
| 412 |
+
html.I(className='fas fa-info-circle', style={'color': '#6c757d', 'marginRight': '6px'}),
|
| 413 |
+
"Heads are automatically categorized based on their attention patterns. Hover for descriptions."
|
| 414 |
+
], style={'color': '#6c757d', 'fontSize': '12px', 'marginTop': '8px'})
|
| 415 |
+
], style={'marginBottom': '16px'})
|
| 416 |
+
)
|
| 417 |
|
| 418 |
+
# BertViz visualization with navigation instructions
|
| 419 |
if attention_html:
|
| 420 |
+
# Agent G: Enhanced navigation instructions for head view
|
| 421 |
content_items.append(
|
| 422 |
html.Div([
|
| 423 |
+
html.H5("How to Navigate the Attention Visualization:", style={'color': '#495057', 'marginBottom': '12px'}),
|
| 424 |
html.Div([
|
| 425 |
+
html.Div([
|
| 426 |
+
html.I(className='fas fa-mouse-pointer', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 427 |
+
html.Strong("Select heads: "),
|
| 428 |
+
html.Span("Click on layer/head numbers at the top to view specific attention heads. ",
|
| 429 |
+
style={'color': '#6c757d'})
|
| 430 |
+
], style={'marginBottom': '8px'}),
|
| 431 |
html.Div([
|
| 432 |
html.I(className='fas fa-arrows-alt-h', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 433 |
html.Strong("Lines show attention: "),
|
| 434 |
+
html.Span("Each line connects a token (left) to tokens it attends to (right). ",
|
| 435 |
style={'color': '#6c757d'})
|
| 436 |
], style={'marginBottom': '8px'}),
|
| 437 |
html.Div([
|
| 438 |
html.I(className='fas fa-paint-brush', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 439 |
html.Strong("Line thickness = attention strength: "),
|
| 440 |
+
html.Span("Thicker, darker lines mean stronger attention.",
|
| 441 |
style={'color': '#6c757d'})
|
| 442 |
], style={'marginBottom': '8px'}),
|
| 443 |
html.Div([
|
| 444 |
+
html.I(className='fas fa-search-plus', style={'color': '#f093fb', 'marginRight': '8px'}),
|
| 445 |
+
html.Strong("Hover for details: "),
|
| 446 |
+
html.Span("Hover over tokens or lines to see exact attention weights.",
|
| 447 |
style={'color': '#6c757d'})
|
| 448 |
])
|
| 449 |
], style={'padding': '12px', 'backgroundColor': '#fdf4ff', 'borderRadius': '6px', 'marginBottom': '16px'})
|
todo.md
CHANGED
|
@@ -60,3 +60,18 @@
|
|
| 60 |
- [x] Add generate_head_view_with_categories function for categorized attention heads
|
| 61 |
- [x] Add get_head_category_counts helper function for UI display
|
| 62 |
- [x] Run tests to verify no regressions (73 tests pass)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
- [x] Add generate_head_view_with_categories function for categorized attention heads
|
| 61 |
- [x] Add get_head_category_counts helper function for UI display
|
| 62 |
- [x] Run tests to verify no regressions (73 tests pass)
|
| 63 |
+
|
| 64 |
+
## Completed: Pipeline Clarity Improvements (Agent F - Analysis Scope)
|
| 65 |
+
|
| 66 |
+
- [x] Add session-original-prompt-store and session-selected-beam-store to app.py
|
| 67 |
+
- [x] Modify run_generation to analyze ORIGINAL PROMPT only (not generated beam)
|
| 68 |
+
- [x] Store beam generation results separately for post-experiment comparison
|
| 69 |
+
- [x] Update analyze_selected_sequence to store beam for comparison instead of re-analyzing
|
| 70 |
+
- [x] Update ablation experiment to show selected beam context in results
|
| 71 |
+
|
| 72 |
+
## Completed: Pipeline Clarity Improvements (Agent G - Integration & Attention UI)
|
| 73 |
+
|
| 74 |
+
- [x] Remove deprecated "Most attended tokens" section from attention stage
|
| 75 |
+
- [x] Wire head categorization into attention stage UI (shows category counts)
|
| 76 |
+
- [x] Add enhanced navigation instructions for BertViz head view
|
| 77 |
+
- [x] Verify all 73 tests pass
|