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()