File size: 5,926 Bytes
2aee49a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """
Trực quan hóa Ma trận Self-Attention của mô hình Transformer.
Chạy: python src/visualize_attention.py
Kết quả: Lưu file ảnh .png vào thư mục logs/
"""
import torch
import pickle
import os
import sys
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, os.path.dirname(__file__))
from model import make_model
VOCAB_PATH = 'data/processed/vocab.pkl'
MODEL_PATH = 'models/best/model.pt'
TOKENIZER_PATH = 'data/processed/bpe_tokenizer.json'
OUTPUT_DIR = 'logs'
os.makedirs(OUTPUT_DIR, exist_ok=True)
def load_bpe_tokenizer():
if os.path.exists(TOKENIZER_PATH):
from tokenizers import Tokenizer
return Tokenizer.from_file(TOKENIZER_PATH)
return None
def visualize_attention(question, model, device, word2idx, bpe_tok=None):
"""Trích xuất và vẽ ma trận Attention cho một câu hỏi."""
# Tokenize
if bpe_tok is not None:
enc = bpe_tok.encode(question.strip())
token_ids = enc.ids
token_labels = enc.tokens
else:
import re
text = str(question).lower()
text = re.sub(r'([.,!?()])', r' \1 ', text)
text = re.sub(r'\s{2,}', ' ', text)
words = text.strip().split()
token_ids = [word2idx.get(w, word2idx.get('<unk>', 3)) for w in words]
token_labels = words
sos_id = word2idx.get('<sos>', 1)
input_ids = [sos_id] + token_ids
token_labels = ['<sos>'] + token_labels
x = torch.tensor([input_ids]).to(device)
# Lấy attention maps
attn_maps = model.get_attention_maps(x)
n_layers = len(attn_maps)
n_heads = attn_maps[0].size(1)
# Vẽ heatmap cho tất cả các layer (lấy trung bình các head)
fig, axes = plt.subplots(1, n_layers, figsize=(6 * n_layers, 5))
if n_layers == 1:
axes = [axes]
fig.suptitle(f'Self-Attention Maps\nInput: "{question}"',
fontsize=14, fontweight='bold', y=1.02)
for layer_idx, (ax, attn) in enumerate(zip(axes, attn_maps)):
# Trung bình các head: (1, n_head, seq, seq) -> (seq, seq)
avg_attn = attn[0].mean(dim=0).cpu().numpy()
sns.heatmap(
avg_attn,
xticklabels=token_labels,
yticklabels=token_labels,
cmap='YlOrRd',
ax=ax,
square=True,
cbar_kws={'shrink': 0.6},
vmin=0, vmax=1,
)
ax.set_title(f'Layer {layer_idx + 1}', fontsize=12, fontweight='bold')
ax.set_xlabel('Key (Nhìn vào)')
ax.set_ylabel('Query (Từ đang xét)')
ax.tick_params(axis='x', rotation=45)
ax.tick_params(axis='y', rotation=0)
plt.tight_layout()
safe_name = question[:30].replace(' ', '_').replace('?', '').replace('/', '_')
save_path = os.path.join(OUTPUT_DIR, f'attention_{safe_name}.png')
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.close()
print(f" Đã lưu: {save_path}")
# Vẽ chi tiết từng head của Layer cuối
last_attn = attn_maps[-1][0].cpu().numpy() # (n_head, seq, seq)
fig2, axes2 = plt.subplots(2, (n_heads + 1) // 2, figsize=(4 * ((n_heads + 1) // 2), 8))
axes2 = axes2.flatten()
fig2.suptitle(f'Attention Heads (Layer {n_layers})\nInput: "{question}"',
fontsize=14, fontweight='bold')
for head_idx in range(n_heads):
ax = axes2[head_idx]
sns.heatmap(
last_attn[head_idx],
xticklabels=token_labels,
yticklabels=token_labels,
cmap='Blues',
ax=ax,
square=True,
cbar=False,
vmin=0, vmax=1,
)
ax.set_title(f'Head {head_idx + 1}', fontsize=10)
ax.tick_params(axis='x', rotation=45, labelsize=7)
ax.tick_params(axis='y', rotation=0, labelsize=7)
# Ẩn các axes thừa
for idx in range(n_heads, len(axes2)):
axes2[idx].set_visible(False)
plt.tight_layout()
save_path2 = os.path.join(OUTPUT_DIR, f'attention_heads_{safe_name}.png')
plt.savefig(save_path2, dpi=150, bbox_inches='tight')
plt.close()
print(f" Đã lưu: {save_path2}")
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if not os.path.exists(MODEL_PATH):
print(f"Không tìm thấy model: {MODEL_PATH}")
print("Hãy chạy train_gpu.py trước.")
return
with open(VOCAB_PATH, 'rb') as f:
vocab = pickle.load(f)
word2idx = vocab['word2idx']
ckpt = torch.load(MODEL_PATH, map_location=device)
model = make_model(
len(word2idx),
n_layer=ckpt['n_l'], d_model=ckpt['d_m'],
n_head=ckpt['n_h'], dropout=ckpt['dr']
)
model.load_state_dict(ckpt['model_state_dict'])
model.to(device)
model.eval()
bpe_tok = load_bpe_tokenizer()
print(f"Model: d_model={ckpt['d_m']} | layers={ckpt['n_l']} | heads={ckpt['n_h']}")
print(f"BPE Tokenizer: {'Loaded' if bpe_tok else 'Fallback'}")
print()
# Danh sách câu hỏi mẫu để vẽ biểu đồ
test_questions = [
"Luật lũy đẳng là gì?",
"Thuật toán Dijkstra hoạt động như thế nào?",
"Đường đi Euler khác đường đi Hamilton chỗ nào?",
]
print(f"{'='*55}")
print(f" VẼ BIỂU ĐỒ ATTENTION HEATMAP")
print(f"{'='*55}")
for q in test_questions:
print(f"\n Câu hỏi: \"{q}\"")
visualize_attention(q, model, device, word2idx, bpe_tok)
print(f"\n Tất cả biểu đồ đã lưu vào thư mục: {OUTPUT_DIR}/")
print(f" Bạn có thể dán ảnh này vào Slide PowerPoint để báo cáo!")
if __name__ == "__main__":
main()
|