cdpearlman commited on
Commit
977a3ff
·
1 Parent(s): 1221d69

Add simplified attention view with top-3 attended tokens and BertViz toggle button

Browse files
app.py CHANGED
@@ -609,6 +609,37 @@ def create_layer_accordions(activation_data, model_name):
609
  else:
610
  content_items.append(html.P("No predictions available"))
611
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  panel = html.Details([
613
  html.Summary(summary_text, className="layer-summary"),
614
  html.Div(content_items, className="layer-content")
@@ -693,6 +724,38 @@ def toggle_comparison_mode(n_clicks, is_comparing):
693
 
694
  return new_comparing, style, style, button_content, button_class
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  # Node click callback for analysis results
697
  @app.callback(
698
  Output('results-container', 'children'),
 
609
  else:
610
  content_items.append(html.P("No predictions available"))
611
 
612
+ # Add attention view (top-3 attended tokens)
613
+ top_attended = layer.get('top_attended_tokens', [])
614
+ if top_attended:
615
+ attention_items = []
616
+ for token, weight in top_attended:
617
+ attention_items.append(html.Div([
618
+ html.Span(f"'{token}'", style={'fontWeight': 'bold', 'marginRight': '8px'}),
619
+ html.Span(f"{weight:.3f}", style={'color': '#6c757d', 'fontSize': '13px'})
620
+ ], style={'marginBottom': '4px'}))
621
+
622
+ content_items.append(html.Div([
623
+ html.Hr(style={'margin': '15px 0'}),
624
+ html.H6("Attention (current position)", style={'marginBottom': '8px', 'fontSize': '14px', 'color': '#495057'}),
625
+ html.Div(attention_items, style={'marginBottom': '10px'}),
626
+ html.Button(
627
+ [html.I(className="fas fa-expand", style={'marginRight': '5px'}), "Open full interactive view"],
628
+ id={'type': 'bertviz-btn', 'layer': layer_num},
629
+ className='bertviz-button',
630
+ style={
631
+ 'padding': '6px 12px',
632
+ 'fontSize': '12px',
633
+ 'backgroundColor': '#667eea',
634
+ 'color': 'white',
635
+ 'border': 'none',
636
+ 'borderRadius': '4px',
637
+ 'cursor': 'pointer'
638
+ }
639
+ ),
640
+ html.Div(id={'type': 'bertviz-container', 'layer': layer_num}, style={'marginTop': '10px'})
641
+ ]))
642
+
643
  panel = html.Details([
644
  html.Summary(summary_text, className="layer-summary"),
645
  html.Div(content_items, className="layer-content")
 
724
 
725
  return new_comparing, style, style, button_content, button_class
726
 
727
+ # Callback to show BertViz visualization when button is clicked
728
+ @app.callback(
729
+ Output({'type': 'bertviz-container', 'layer': dash.dependencies.MATCH}, 'children'),
730
+ [Input({'type': 'bertviz-btn', 'layer': dash.dependencies.MATCH}, 'n_clicks')],
731
+ [State('session-activation-store', 'data'),
732
+ State({'type': 'bertviz-btn', 'layer': dash.dependencies.MATCH}, 'id')],
733
+ prevent_initial_call=True
734
+ )
735
+ def toggle_bertviz_view(n_clicks, activation_data, button_id):
736
+ """Show/hide BertViz full model view when button is clicked."""
737
+ if not n_clicks or not activation_data:
738
+ return None
739
+
740
+ try:
741
+ from utils import generate_bertviz_html
742
+ layer_num = button_id['layer']
743
+
744
+ # Toggle: even clicks hide, odd clicks show
745
+ if n_clicks % 2 == 0:
746
+ return None
747
+
748
+ # Generate BertViz HTML for this layer
749
+ bertviz_html = generate_bertviz_html(activation_data, layer_num, 'full')
750
+
751
+ return html.Iframe(
752
+ srcDoc=bertviz_html,
753
+ style={'width': '100%', 'height': '500px', 'border': '1px solid #ddd', 'borderRadius': '4px', 'marginTop': '10px'}
754
+ )
755
+
756
+ except Exception as e:
757
+ return html.P(f"Error loading BertViz: {str(e)}", style={'color': '#dc3545', 'fontSize': '13px'})
758
+
759
  # Node click callback for analysis results
760
  @app.callback(
761
  Output('results-container', 'children'),
components/__pycache__/main_panel.cpython-311.pyc CHANGED
Binary files a/components/__pycache__/main_panel.cpython-311.pyc and b/components/__pycache__/main_panel.cpython-311.pyc differ
 
todo.md CHANGED
@@ -24,10 +24,10 @@ Note: Minimal-change approach. Reuse existing files (`app.py`, `components/main_
24
  - [x] Add a spinning "Loading visuals..." after loading data until all the visualizations are loaded
25
 
26
  ## Feature: Simplified attention view + open full interactive view
27
- - [ ] From `activation_data['attention_outputs']`, compute top-3 attended input tokens for current position (per layer)
28
- - [ ] Render a simple list: token text + attention weight (rounded), most-to-least
29
- - [ ] Add button/link: "Open full interactive view" → shows existing BertViz `model_view` for that layer
30
- - [ ] Keep current BertViz plumbing; reuse callbacks to load the selected layer
31
 
32
  ## Feature: Tokenization example of initial prompt (top of page)
33
  - [ ] Add a new section above panels: "How the tokenizer splits your prompt"
 
24
  - [x] Add a spinning "Loading visuals..." after loading data until all the visualizations are loaded
25
 
26
  ## Feature: Simplified attention view + open full interactive view
27
+ - [x] From `activation_data['attention_outputs']`, compute top-3 attended input tokens for current position (per layer)
28
+ - [x] Render a simple list: token text + attention weight (rounded), most-to-least
29
+ - [x] Add button/link: "Open full interactive view" → shows existing BertViz `model_view` for that layer
30
+ - [x] Keep current BertViz plumbing; reuse callbacks to load the selected layer
31
 
32
  ## Feature: Tokenization example of initial prompt (top of page)
33
  - [ ] Add a new section above panels: "How the tokenizer splits your prompt"
utils/__pycache__/model_patterns.cpython-311.pyc CHANGED
Binary files a/utils/__pycache__/model_patterns.cpython-311.pyc and b/utils/__pycache__/model_patterns.cpython-311.pyc differ
 
utils/model_patterns.py CHANGED
@@ -420,12 +420,75 @@ def _compute_certainty(probs: List[float]) -> float:
420
  return max(0.0, min(1.0, certainty)) # Clamp to [0, 1]
421
 
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  def extract_layer_data(activation_data: Dict[str, Any], model, tokenizer) -> List[Dict[str, Any]]:
424
  """
425
- Extract layer-by-layer data for accordion display with top-5, deltas, and certainty.
426
 
427
  Returns:
428
- List of dicts with: layer_num, top_token, top_prob, top_5_tokens, deltas, certainty
429
  """
430
  layer_modules = activation_data.get('block_modules', [])
431
  if not layer_modules:
@@ -444,6 +507,9 @@ def extract_layer_data(activation_data: Dict[str, Any], model, tokenizer) -> Lis
444
  for layer_num, module_name in layer_info:
445
  top_tokens = _get_top_tokens(activation_data, module_name, model, tokenizer, top_k=5) if logit_lens_enabled else None
446
 
 
 
 
447
  if top_tokens:
448
  top_token, top_prob = top_tokens[0]
449
 
@@ -465,7 +531,8 @@ def extract_layer_data(activation_data: Dict[str, Any], model, tokenizer) -> Lis
465
  'top_3_tokens': top_tokens[:3], # Keep for backward compatibility
466
  'top_5_tokens': top_tokens[:5], # New: top-5 for bar chart
467
  'deltas': deltas,
468
- 'certainty': certainty
 
469
  })
470
 
471
  # Update previous layer probabilities
@@ -479,7 +546,8 @@ def extract_layer_data(activation_data: Dict[str, Any], model, tokenizer) -> Lis
479
  'top_3_tokens': [],
480
  'top_5_tokens': [],
481
  'deltas': {},
482
- 'certainty': 0.0
 
483
  })
484
 
485
  return layer_data
 
420
  return max(0.0, min(1.0, certainty)) # Clamp to [0, 1]
421
 
422
 
423
+ def _get_top_attended_tokens(activation_data: Dict[str, Any], layer_num: int, tokenizer, top_k: int = 3) -> Optional[List[Tuple[str, float]]]:
424
+ """
425
+ Get top-K attended input tokens for the current position (last token) in a layer.
426
+ Averages attention across all heads.
427
+
428
+ Args:
429
+ activation_data: Output from execute_forward_pass
430
+ layer_num: Layer number to analyze
431
+ tokenizer: Tokenizer for decoding tokens
432
+ top_k: Number of top attended tokens to return
433
+
434
+ Returns:
435
+ List of (token_string, attention_weight) tuples, sorted by weight (highest first)
436
+ """
437
+ try:
438
+ attention_outputs = activation_data.get('attention_outputs', {})
439
+ input_ids = activation_data.get('input_ids', [])
440
+
441
+ if not attention_outputs or not input_ids:
442
+ return None
443
+
444
+ # Find attention output for this layer
445
+ target_module = None
446
+ for module_name in attention_outputs.keys():
447
+ numbers = re.findall(r'\d+', module_name)
448
+ if numbers and int(numbers[0]) == layer_num:
449
+ target_module = module_name
450
+ break
451
+
452
+ if not target_module:
453
+ return None
454
+
455
+ attention_output = attention_outputs[target_module]['output']
456
+ if not isinstance(attention_output, list) or len(attention_output) < 2:
457
+ return None
458
+
459
+ # Get attention weights: [batch, heads, seq_len, seq_len]
460
+ attention_weights = torch.tensor(attention_output[1])
461
+
462
+ # Average across heads: [seq_len, seq_len]
463
+ avg_attention = attention_weights[0].mean(dim=0)
464
+
465
+ # Get attention from last position to all positions
466
+ last_pos_attention = avg_attention[-1, :] # [seq_len]
467
+
468
+ # Get top-K attended positions
469
+ top_values, top_indices = torch.topk(last_pos_attention, min(top_k, len(last_pos_attention)))
470
+
471
+ # Convert to tokens
472
+ input_ids_tensor = torch.tensor(input_ids[0]) if isinstance(input_ids[0], list) else torch.tensor(input_ids)
473
+ result = []
474
+ for idx, weight in zip(top_indices, top_values):
475
+ token_id = input_ids_tensor[idx].item()
476
+ token_str = tokenizer.decode([token_id], skip_special_tokens=False)
477
+ result.append((token_str, weight.item()))
478
+
479
+ return result
480
+
481
+ except Exception as e:
482
+ print(f"Warning: Could not compute attended tokens for layer {layer_num}: {e}")
483
+ return None
484
+
485
+
486
  def extract_layer_data(activation_data: Dict[str, Any], model, tokenizer) -> List[Dict[str, Any]]:
487
  """
488
+ Extract layer-by-layer data for accordion display with top-5, deltas, certainty, and attention.
489
 
490
  Returns:
491
+ List of dicts with: layer_num, top_token, top_prob, top_5_tokens, deltas, certainty, top_attended_tokens
492
  """
493
  layer_modules = activation_data.get('block_modules', [])
494
  if not layer_modules:
 
507
  for layer_num, module_name in layer_info:
508
  top_tokens = _get_top_tokens(activation_data, module_name, model, tokenizer, top_k=5) if logit_lens_enabled else None
509
 
510
+ # Get top-3 attended tokens for this layer
511
+ top_attended = _get_top_attended_tokens(activation_data, layer_num, tokenizer, top_k=3)
512
+
513
  if top_tokens:
514
  top_token, top_prob = top_tokens[0]
515
 
 
531
  'top_3_tokens': top_tokens[:3], # Keep for backward compatibility
532
  'top_5_tokens': top_tokens[:5], # New: top-5 for bar chart
533
  'deltas': deltas,
534
+ 'certainty': certainty,
535
+ 'top_attended_tokens': top_attended # New: attention view
536
  })
537
 
538
  # Update previous layer probabilities
 
546
  'top_3_tokens': [],
547
  'top_5_tokens': [],
548
  'deltas': {},
549
+ 'certainty': 0.0,
550
+ 'top_attended_tokens': top_attended
551
  })
552
 
553
  return layer_data