cdpearlman commited on
Commit
8a1fc4b
·
1 Parent(s): ef749ae

Refactor analysis scope and enhance attention UI (Agents F and G)

Browse files
Files changed (3) hide show
  1. app.py +112 -73
  2. components/pipeline.py +60 -34
  3. 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
- for i, result in enumerate(results):
335
- results_ui.append(html.Div([
336
- html.Div([
337
- html.Span(f"Rank {i+1}", style={'fontWeight': 'bold', 'marginRight': '10px', 'color': '#667eea'})
338
- ], style={'marginBottom': '5px'}),
 
 
339
  html.Div(result['text'], style={'fontFamily': 'monospace', 'backgroundColor': '#fff', 'padding': '10px', 'borderRadius': '4px', 'border': '1px solid #dee2e6'}),
340
- html.Button("Analyze", id={'type': 'result-item', 'index': i}, n_clicks=0,
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
- config = {
351
- 'attention_modules': [mod for pattern in (attn_patterns or []) for mod in module_patterns.get(pattern, [])],
352
- 'block_modules': [mod for pattern in (block_patterns or []) for mod in module_patterns.get(pattern, [])],
353
- 'norm_parameters': [param for pattern in (norm_patterns or []) for param in param_patterns.get(pattern, [])]
354
- }
355
 
356
- if not config['block_modules']:
357
- return html.Div("Please select modules in the sidebar.", style={'color': 'red'}), results, {'display': 'none'}, {'display': 'none'}, {}
358
-
359
- activation_data = execute_forward_pass(model, tokenizer, result['text'], config)
360
- return results_ui, results, {'display': 'block'}, {'display': 'block'}, activation_data
361
 
362
  except Exception as e:
363
  import traceback
364
  traceback.print_exc()
365
- return html.Div(f"Error: {e}", style={'color': 'red'}), [], {'display': 'none'}, {'display': 'none'}, {}
 
366
 
367
 
368
  @app.callback(
369
- [Output('session-activation-store', 'data', allow_duplicate=True),
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 analyze_selected_sequence(n_clicks_list, results_data, model_name, patterns_data, attn_patterns, block_patterns, norm_patterns):
 
 
 
 
 
 
 
382
  if not any(n_clicks_list) or not results_data:
383
- return no_update, no_update, no_update
384
 
385
  ctx = dash.callback_context
386
  if not ctx.triggered:
387
- return no_update, no_update, no_update
388
 
389
  triggered_id = json.loads(ctx.triggered[0]['prop_id'].split('.')[0])
390
  index = triggered_id['index']
391
 
392
- try:
393
- from transformers import AutoModelForCausalLM, AutoTokenizer
394
- result = results_data[index]
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, top_attended))
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
- return create_ablation_results_display(
 
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: List of (token, weight) tuples for top attended tokens
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
- # Top attended summary
373
- if top_attended:
374
- attended_chips = []
375
- for tok, weight in top_attended[:5]:
376
- attended_chips.append(
377
- html.Span([
378
- html.Span(tok, style={'fontWeight': '500'}),
379
- html.Span(f" ({weight:.2f})", style={'color': '#6c757d', 'fontSize': '12px'})
380
- ], style={
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
- content_items.append(
392
- html.Div([
393
- html.H5("Most attended tokens:", style={'color': '#495057', 'marginBottom': '12px'}),
394
- html.Div(attended_chips)
395
- ], style={'marginBottom': '16px'})
396
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- # BertViz visualization
399
  if attention_html:
400
- # Add explanation for how to read the visualization
401
  content_items.append(
402
  html.Div([
403
- html.H5("How to Read the Attention Visualization:", style={'color': '#495057', 'marginBottom': '12px'}),
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 the tokens it attends to (right). ",
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. The model is focusing more on those tokens.",
415
  style={'color': '#6c757d'})
416
  ], style={'marginBottom': '8px'}),
417
  html.Div([
418
- html.I(className='fas fa-layer-group', style={'color': '#f093fb', 'marginRight': '8px'}),
419
- html.Strong("Each head sees differently: "),
420
- html.Span("Click on different attention heads to see how each one captures different relationships.",
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