from __future__ import annotations from html import escape from pathlib import Path import torch from tiny_transformer.model import TinyTransformer from tiny_transformer.tokenizer import Tokenizer @torch.no_grad() def save_attention_heatmap( model: TinyTransformer, tokenizer: Tokenizer, idx: torch.Tensor, output_path: str, layer: int = -1, head: int = 0, ) -> None: attentions = model.attention_maps(idx) if not attentions: raise ValueError("Model did not return attention maps") selected = attentions[layer][0] if head < 0 or head >= selected.shape[0]: raise ValueError(f"head must be between 0 and {selected.shape[0] - 1}") weights = selected[head].detach().cpu() token_ids = idx[0].detach().cpu().tolist() labels = [_display_token(tokenizer.id_to_token(token_id)) for token_id in token_ids] svg = _attention_svg(weights, labels, layer=layer, head=head) output = Path(output_path) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(svg, encoding="utf-8") def _attention_svg(weights: torch.Tensor, labels: list[str], layer: int, head: int) -> str: cell = 24 margin_left = 120 margin_top = 96 size = len(labels) width = margin_left + size * cell + 24 height = margin_top + size * cell + 40 max_weight = max(float(weights.max()), 1e-9) cells = [] for row in range(size): for col in range(size): value = float(weights[row, col]) / max_weight color = 255 - int(value * 210) cells.append( f'' f"{escape(labels[row])} attends to {escape(labels[col])}: " f"{float(weights[row, col]):.3f}" ) x_labels = [ f'' f"{escape(label)}" for idx, label in enumerate(labels) ] y_labels = [ f'' f"{escape(label)}" for idx, label in enumerate(labels) ] return "\n".join( [ f'', "", f'' f"Attention heatmap: layer {layer}, head {head}", *x_labels, *y_labels, *cells, "", ] ) def _display_token(token: str) -> str: return token.replace("\n", "\\n").replace("\t", "\\t").replace(" ", "space")