{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.0"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":4117,"databundleVersionId":46665,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":310292430,"isSourceIdPinned":false}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"8d38c7b4-8afb-4988-8499-ba3b8645faf4","cell_type":"markdown","source":"# Malware CNN Classifier + Grad-CAM\n**CE8013 AI for Security R&D | Group 5 | NCU 2026**\n\nPipeline: Load data → Train 4 backbones → Evaluate → Grad-CAM visualization","metadata":{}},{"id":"e5dd5e8a-34a6-4d3a-8f62-6a5aaa4278c9","cell_type":"markdown","source":"## Cell 1 — Install","metadata":{}},{"id":"2800a815-8511-4073-b529-8edddd4cab28","cell_type":"code","source":"!pip install -q timm grad-cam\nprint('OK')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:02.891433Z","iopub.execute_input":"2026-04-11T07:18:02.892214Z","iopub.status.idle":"2026-04-11T07:18:06.469147Z","shell.execute_reply.started":"2026-04-11T07:18:02.892176Z","shell.execute_reply":"2026-04-11T07:18:06.468129Z"}},"outputs":[],"execution_count":null},{"id":"382491b3-2a11-4dec-9d52-20ff43d8dbd8","cell_type":"markdown","source":"## Cell 2 — Config & Imports","metadata":{}},{"id":"8b1a0803-3133-45c3-9a62-84d9253bd4ef","cell_type":"code","source":"import os, json, time, random, warnings\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\nimport seaborn as sns\nfrom pathlib import Path\nfrom datetime import datetime\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nfrom torchvision import transforms\nfrom PIL import Image\nimport timm\nfrom sklearn.metrics import (\n    classification_report, confusion_matrix,\n    accuracy_score, f1_score\n)\n\n# ── Paths ──\nDATA_ROOT  = Path('/kaggle/input/notebooks/sunniepa/ai-v4/malware_images')\nTRAIN_DIR  = DATA_ROOT / 'train'\nTEST_DIR   = DATA_ROOT / 'test' / 'Unknown'\nOUT_DIR    = Path('/kaggle/working')\nOUT_DIR.mkdir(exist_ok=True)\n\n# ── Classes ──\nCLASSES = sorted([d.name for d in TRAIN_DIR.iterdir() if d.is_dir()])\nCLASS2IDX = {c: i for i, c in enumerate(CLASSES)}\nIDX2CLASS = {i: c for c, i in CLASS2IDX.items()}\nNUM_CLASSES = len(CLASSES)\n\n# ── Hyperparams ──\nIMG_SIZE   = 224\nBATCH_SIZE = 64\nNUM_EPOCHS = 30\nLR         = 1e-4\nVAL_SPLIT  = 0.15\nSEED       = 42\n\n# ── Device ──\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\n\nprint(f'Device    : {DEVICE}')\nprint(f'Classes   : {NUM_CLASSES} -> {CLASSES}')\nprint(f'Train dir : {TRAIN_DIR}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:06.471162Z","iopub.execute_input":"2026-04-11T07:18:06.471513Z","iopub.status.idle":"2026-04-11T07:18:06.491457Z","shell.execute_reply.started":"2026-04-11T07:18:06.471486Z","shell.execute_reply":"2026-04-11T07:18:06.490664Z"}},"outputs":[],"execution_count":null},{"id":"b96f7289-355f-46fb-8c1c-2f038ecabf60","cell_type":"code","source":"# Compact CSV - chay 1 lan truoc khi train\nimport pandas as pd\nfrom pathlib import Path\n\nINDEX_CSV = Path('/kaggle/input/notebooks/sunniepa/ai-v4/malware_index.csv')\n\ndf = pd.read_csv(INDEX_CSV)\nprint(f'Truoc: {len(df):,} rows')\nprint(df['status'].value_counts().to_string())\n\n# Giu dong \"done\" uu tien hon \"pending\"\ndf = df.sort_values('status', ascending=True)  # 'done' < 'pending'\ndf = df.drop_duplicates(subset=['file_id'], keep='first')\nprint(f'\\nSau: {len(df):,} rows')\nprint(df['status'].value_counts().to_string())\n\n# Kiem tra\ndone_count = (df['status'] == 'done').sum()\nprint(f'\\nFile done: {done_count:,} / {len(df):,}')\nprint('OK - san sang train' if done_count == len(df) else 'WARNING: van con pending!')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:06.492468Z","iopub.execute_input":"2026-04-11T07:18:06.492759Z","iopub.status.idle":"2026-04-11T07:18:06.630790Z","shell.execute_reply.started":"2026-04-11T07:18:06.492729Z","shell.execute_reply":"2026-04-11T07:18:06.629955Z"}},"outputs":[],"execution_count":null},{"id":"05d4aa75-2837-4d93-931a-0e56c08a9a12","cell_type":"markdown","source":"## Cell 3 — Dataset & DataLoader","metadata":{}},{"id":"3adb4d84-ce41-47fd-8086-77f4c72af7ec","cell_type":"code","source":"class MalwareDataset(Dataset):\n    def __init__(self, samples, transform=None):\n        self.samples   = samples   # list of (path, label_idx)\n        self.transform = transform\n\n    def __len__(self): return len(self.samples)\n\n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img = Image.open(path).convert('RGB')  # grayscale -> 3ch for pretrained\n        if self.transform:\n            img = self.transform(img)\n        return img, label\n\n\n# ── Transforms ──\ntrain_tf = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(10),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize([0.5]*3, [0.5]*3),\n])\nval_tf = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.5]*3, [0.5]*3),\n])\n\n\ndef build_samples():\n    \"\"\"Collect all (path, label) pairs from TRAIN_DIR.\"\"\"\n    samples = []\n    for cls in CLASSES:\n        cls_dir = TRAIN_DIR / cls\n        for img_path in cls_dir.glob('*.png'):\n            samples.append((img_path, CLASS2IDX[cls]))\n    return samples\n\n\ndef split_samples(samples, val_ratio=VAL_SPLIT, seed=SEED):\n    \"\"\"Stratified split -> (train_samples, val_samples).\"\"\"\n    from collections import defaultdict\n    by_class = defaultdict(list)\n    for s in samples:\n        by_class[s[1]].append(s)\n    train_s, val_s = [], []\n    rng = random.Random(seed)\n    for cls_idx, cls_samples in by_class.items():\n        rng.shuffle(cls_samples)\n        n_val = max(1, int(len(cls_samples) * val_ratio))\n        val_s.extend(cls_samples[:n_val])\n        train_s.extend(cls_samples[n_val:])\n    return train_s, val_s\n\n\ndef make_weighted_sampler(samples):\n    \"\"\"WeightedRandomSampler to handle class imbalance (Simda=42 vs Kelihos_ver3=2942).\"\"\"\n    labels = [s[1] for s in samples]\n    class_counts = np.bincount(labels, minlength=NUM_CLASSES).astype(float)\n    class_weights = 1.0 / np.where(class_counts == 0, 1, class_counts)\n    sample_weights = torch.tensor([class_weights[l] for l in labels], dtype=torch.float)\n    return WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True)\n\n\n# ── Build loaders ──\nall_samples = build_samples()\ntrain_samples, val_samples = split_samples(all_samples)\n\ntrain_ds = MalwareDataset(train_samples, transform=train_tf)\nval_ds   = MalwareDataset(val_samples,   transform=val_tf)\n\nsampler    = make_weighted_sampler(train_samples)\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, sampler=sampler,\n                          num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False,\n                          num_workers=2, pin_memory=True)\n\n# ── Class distribution ──\nlabels_all = [s[1] for s in all_samples]\ncounts = np.bincount(labels_all)\nprint(f'Total samples : {len(all_samples):,}')\nprint(f'Train / Val   : {len(train_samples):,} / {len(val_samples):,}')\nprint()\nprint('Class distribution:')\nfor i, cls in enumerate(CLASSES):\n    print(f'  {cls:<20} {counts[i]:>5}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:06.632857Z","iopub.execute_input":"2026-04-11T07:18:06.633084Z","iopub.status.idle":"2026-04-11T07:18:06.707872Z","shell.execute_reply.started":"2026-04-11T07:18:06.633064Z","shell.execute_reply":"2026-04-11T07:18:06.706977Z"}},"outputs":[],"execution_count":null},{"id":"98fdfd87-f6d9-432d-83b2-b60b8f5b89a0","cell_type":"markdown","source":"## Cell 4 — Model Builder (timm + custom head)","metadata":{}},{"id":"8b8f2452-67db-4eed-b96b-600da17748e3","cell_type":"code","source":"# ── Backbone configs ──\nBACKBONE_CONFIGS = {\n    'vgg16':        {'timm_name': 'vgg16',             'unfreeze_last': 2},\n    'resnet50':     {'timm_name': 'resnet50',           'unfreeze_last': 2},\n    'efficientnet': {'timm_name': 'efficientnet_b0',    'unfreeze_last': 3},\n    'convnext_v2':  {'timm_name': 'convnextv2_tiny',    'unfreeze_last': 3},\n}\n\n\ndef build_model(backbone_key: str, num_classes: int = NUM_CLASSES):\n    \"\"\"\n    Load pretrained backbone from timm.\n    Freeze all layers, then unfreeze last N blocks.\n    Replace classifier head with custom MLP.\n    \"\"\"\n    cfg = BACKBONE_CONFIGS[backbone_key]\n    model = timm.create_model(\n        cfg['timm_name'],\n        pretrained=True,\n        num_classes=0,   # remove original head\n    )\n\n    # Freeze all parameters\n    for param in model.parameters():\n        param.requires_grad = False\n\n    # Unfreeze last N children (fine-tuning)\n    children = list(model.children())\n    for child in children[-cfg['unfreeze_last']:]:\n        for param in child.parameters():\n            param.requires_grad = True\n\n    # Get feature dim\n    with torch.no_grad():\n        dummy = torch.zeros(1, 3, IMG_SIZE, IMG_SIZE)\n        feat_dim = model(dummy).shape[1]\n\n    # Custom classifier head\n    head = nn.Sequential(\n        nn.Linear(feat_dim, 512),\n        nn.BatchNorm1d(512),\n        nn.ReLU(),\n        nn.Dropout(0.4),\n        nn.Linear(512, 256),\n        nn.ReLU(),\n        nn.Dropout(0.3),\n        nn.Linear(256, num_classes),\n    )\n\n    class FullModel(nn.Module):\n        def __init__(self, backbone, head):\n            super().__init__()\n            self.backbone = backbone\n            self.head     = head\n        def forward(self, x):\n            return self.head(self.backbone(x))\n\n    full_model = FullModel(model, head).to(DEVICE)\n\n    trainable = sum(p.numel() for p in full_model.parameters() if p.requires_grad)\n    total     = sum(p.numel() for p in full_model.parameters())\n    print(f'  {backbone_key:<15} feat_dim={feat_dim}  '\n          f'trainable={trainable:,}/{total:,} ({100*trainable/total:.1f}%)')\n\n    return full_model\n\n\nprint('Building all backbones...')\nfor k in BACKBONE_CONFIGS:\n    build_model(k)\nprint('OK')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:06.708852Z","iopub.execute_input":"2026-04-11T07:18:06.709046Z","iopub.status.idle":"2026-04-11T07:18:10.641807Z","shell.execute_reply.started":"2026-04-11T07:18:06.709028Z","shell.execute_reply":"2026-04-11T07:18:10.641176Z"}},"outputs":[],"execution_count":null},{"id":"63457c8d-ec96-4f7e-bd65-61eedf4a1908","cell_type":"markdown","source":"## Cell 5 — Training Loop","metadata":{}},{"id":"9270e027-9967-4ccf-bdc3-d4e479b1cd74","cell_type":"code","source":"def compute_class_weights():\n    \"\"\"Inverse frequency weights for CrossEntropyLoss.\"\"\"\n    labels = [s[1] for s in train_samples]\n    counts = np.bincount(labels, minlength=NUM_CLASSES).astype(float)\n    weights = 1.0 / np.where(counts == 0, 1, counts)\n    weights = weights / weights.sum() * NUM_CLASSES\n    return torch.tensor(weights, dtype=torch.float).to(DEVICE)\n\n\ndef train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    total_loss = correct = total = 0\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item() * imgs.size(0)\n        preds = outputs.argmax(1)\n        correct += (preds == labels).sum().item()\n        total   += imgs.size(0)\n    return total_loss / total, correct / total\n\n\ndef evaluate(model, loader):\n    model.eval()\n    total_loss = correct = total = 0\n    all_preds, all_labels = [], []\n    criterion = nn.CrossEntropyLoss(weight=compute_class_weights())\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n            outputs = model(imgs)\n            loss    = criterion(outputs, labels)\n            total_loss += loss.item() * imgs.size(0)\n            preds = outputs.argmax(1)\n            correct += (preds == labels).sum().item()\n            total   += imgs.size(0)\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n    acc = correct / total\n    f1  = f1_score(all_labels, all_preds, average='macro')\n    return total_loss / total, acc, f1, all_preds, all_labels\n\n\ndef train_model(backbone_key, num_epochs=NUM_EPOCHS):\n    print(f'\\n{\"=\"*60}')\n    print(f'  Training: {backbone_key}  |  {datetime.now().strftime(\"%H:%M:%S\")}')\n    print(f'{\"=\"*60}')\n\n    model     = build_model(backbone_key)\n    criterion = nn.CrossEntropyLoss(weight=compute_class_weights())\n    optimizer = optim.AdamW(\n        filter(lambda p: p.requires_grad, model.parameters()),\n        lr=LR, weight_decay=1e-4\n    )\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_epochs)\n\n    best_f1   = 0.0\n    best_path = OUT_DIR / f'best_{backbone_key}.pth'\n    history   = {'train_loss':[], 'train_acc':[], 'val_loss':[], 'val_acc':[], 'val_f1':[]}\n    patience  = 10\n    no_improve = 0\n\n    for epoch in range(1, num_epochs + 1):\n        t0 = time.time()\n        tr_loss, tr_acc = train_one_epoch(model, train_loader, criterion, optimizer)\n        vl_loss, vl_acc, vl_f1, _, _ = evaluate(model, val_loader)\n        scheduler.step()\n\n        history['train_loss'].append(tr_loss)\n        history['train_acc'].append(tr_acc)\n        history['val_loss'].append(vl_loss)\n        history['val_acc'].append(vl_acc)\n        history['val_f1'].append(vl_f1)\n\n        # Save best\n        if vl_f1 > best_f1:\n            best_f1 = vl_f1\n            torch.save(model.state_dict(), best_path)\n            no_improve = 0\n            marker = ' <<< best'\n        else:\n            no_improve += 1\n            marker = ''\n\n        print(f'  Epoch {epoch:>3}/{num_epochs} | '\n              f'tr_loss={tr_loss:.4f} tr_acc={tr_acc:.4f} | '\n              f'val_loss={vl_loss:.4f} val_acc={vl_acc:.4f} val_f1={vl_f1:.4f} | '\n              f'{time.time()-t0:.1f}s{marker}')\n\n        if no_improve >= patience:\n            print(f'  Early stopping at epoch {epoch}')\n            break\n\n    # Load best weights\n    model.load_state_dict(torch.load(best_path))\n    print(f'\\n  Best val F1: {best_f1:.4f}')\n    return model, history\n\n\n# Dict to store all results\nRESULTS  = {}\nHISTORIES = {}\nprint('Train loop defined OK')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:10.642820Z","iopub.execute_input":"2026-04-11T07:18:10.643073Z","iopub.status.idle":"2026-04-11T07:18:10.659299Z","shell.execute_reply.started":"2026-04-11T07:18:10.643052Z","shell.execute_reply":"2026-04-11T07:18:10.658267Z"}},"outputs":[],"execution_count":null},{"id":"c671a2dd-e4da-4456-9285-6627bba8ec84","cell_type":"markdown","source":"## Cell 6 — Run Training (4 backbones)\n> Chạy tuần tự 4 model. Mỗi model ~10-20 phút trên GPU Kaggle.\n> Kết quả tốt nhất được lưu tự động.","metadata":{}},{"id":"95432964-2db5-4081-8684-7f54e438aa60","cell_type":"code","source":"BACKBONES_TO_TRAIN = ['vgg16', 'resnet50', 'efficientnet', 'convnext_v2']\n\nfor bk in BACKBONES_TO_TRAIN:\n    model, history = train_model(bk, num_epochs=NUM_EPOCHS)\n    # Final eval on val set\n    _, val_acc, val_f1, preds, labels_gt = evaluate(model, val_loader)\n    RESULTS[bk] = {\n        'model'     : model,\n        'val_acc'   : val_acc,\n        'val_f1'    : val_f1,\n        'preds'     : preds,\n        'labels_gt' : labels_gt,\n    }\n    HISTORIES[bk] = history\n    print(f'\\n[{bk}] DONE -> val_acc={val_acc:.4f}  val_f1={val_f1:.4f}')\n\n# Summary table\nprint('\\n' + '='*50)\nprint('  SUMMARY')\nprint('='*50)\nprint(f'  {\"Backbone\":<20} {\"Val Acc\":>10} {\"Val F1\":>10}')\nprint('  ' + '-'*42)\nfor bk, res in RESULTS.items():\n    print(f'  {bk:<20} {res[\"val_acc\"]:>10.4f} {res[\"val_f1\"]:>10.4f}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T07:18:10.660352Z","iopub.execute_input":"2026-04-11T07:18:10.661031Z","iopub.status.idle":"2026-04-11T10:29:56.318030Z","shell.execute_reply.started":"2026-04-11T07:18:10.661010Z","shell.execute_reply":"2026-04-11T10:29:56.317251Z"}},"outputs":[],"execution_count":null},{"id":"49771390-b1f4-446e-bbe6-833af165c852","cell_type":"markdown","source":"## Cell 7 — Training Curves","metadata":{}},{"id":"4eab79d5-2cf2-4c09-a764-78cab462af76","cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(16, 10))\naxes = axes.flatten()\ncolors = ['#2196F3', '#F44336', '#4CAF50', '#FF9800']\n\nfor i, (bk, hist) in enumerate(HISTORIES.items()):\n    ax = axes[i]\n    epochs = range(1, len(hist['train_acc']) + 1)\n    ax.plot(epochs, hist['train_acc'], label='Train Acc', color=colors[i], linewidth=2)\n    ax.plot(epochs, hist['val_acc'],   label='Val Acc',   color=colors[i], linewidth=2, linestyle='--')\n    ax.plot(epochs, hist['val_f1'],    label='Val F1',    color='gray',    linewidth=1.5, linestyle=':')\n    ax.set_title(f'{bk}  (best F1={max(hist[\"val_f1\"]):.4f})', fontsize=13, fontweight='bold')\n    ax.set_xlabel('Epoch')\n    ax.set_ylabel('Score')\n    ax.legend()\n    ax.grid(True, alpha=0.3)\n    ax.set_ylim(0, 1.05)\n\nplt.suptitle('Training Curves — All Backbones', fontsize=15, fontweight='bold')\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'training_curves.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Saved: training_curves.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:29:56.319421Z","iopub.execute_input":"2026-04-11T10:29:56.320061Z","iopub.status.idle":"2026-04-11T10:29:57.718561Z","shell.execute_reply.started":"2026-04-11T10:29:56.320032Z","shell.execute_reply":"2026-04-11T10:29:57.717835Z"}},"outputs":[],"execution_count":null},{"id":"ec544b12-9035-4b28-919d-3b07da14dc9c","cell_type":"markdown","source":"## Cell 8 — Confusion Matrices","metadata":{}},{"id":"6e6701ee-d199-4d09-b9de-26cc860c8fc0","cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(20, 16))\naxes = axes.flatten()\nshort_names = [c[:10] for c in CLASSES]\n\nfor i, (bk, res) in enumerate(RESULTS.items()):\n    cm = confusion_matrix(res['labels_gt'], res['preds'])\n    cm_norm = cm.astype(float) / cm.sum(axis=1, keepdims=True)\n\n    sns.heatmap(\n        cm_norm, annot=True, fmt='.2f', cmap='Blues',\n        xticklabels=short_names, yticklabels=short_names,\n        ax=axes[i], cbar=True, vmin=0, vmax=1\n    )\n    axes[i].set_title(f'{bk}  (acc={res[\"val_acc\"]:.4f})', fontsize=12, fontweight='bold')\n    axes[i].set_ylabel('True')\n    axes[i].set_xlabel('Predicted')\n    axes[i].tick_params(axis='x', rotation=45)\n\nplt.suptitle('Normalized Confusion Matrices — Validation Set', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'confusion_matrices.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Saved: confusion_matrices.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:29:57.719507Z","iopub.execute_input":"2026-04-11T10:29:57.719751Z","iopub.status.idle":"2026-04-11T10:30:00.911395Z","shell.execute_reply.started":"2026-04-11T10:29:57.719732Z","shell.execute_reply":"2026-04-11T10:30:00.910582Z"}},"outputs":[],"execution_count":null},{"id":"4959c25e-1451-4cc0-b4b1-d69ad35855fd","cell_type":"markdown","source":"## Cell 9 — Classification Report (per-class F1)","metadata":{}},{"id":"7b7dc2eb-4c51-4ca9-8fc0-62096bb53e7b","cell_type":"code","source":"for bk, res in RESULTS.items():\n    print(f'\\n{\"=\"*55}')\n    print(f'  {bk}')\n    print(f'{\"=\"*55}')\n    print(classification_report(\n        res['labels_gt'], res['preds'],\n        target_names=CLASSES, digits=4\n    ))\n\n# Comparison bar chart\nfig, ax = plt.subplots(figsize=(14, 6))\nx = np.arange(NUM_CLASSES)\nwidth = 0.2\ncolors = ['#2196F3', '#F44336', '#4CAF50', '#FF9800']\n\nfor i, (bk, res) in enumerate(RESULTS.items()):\n    report = classification_report(\n        res['labels_gt'], res['preds'],\n        target_names=CLASSES, output_dict=True\n    )\n    f1_per_class = [report[c]['f1-score'] for c in CLASSES]\n    ax.bar(x + i*width, f1_per_class, width, label=bk, color=colors[i], alpha=0.85)\n\nax.set_xticks(x + width*1.5)\nax.set_xticklabels(CLASSES, rotation=45, ha='right')\nax.set_ylabel('F1-Score')\nax.set_title('Per-class F1-Score Comparison', fontsize=13, fontweight='bold')\nax.legend()\nax.grid(True, axis='y', alpha=0.3)\nax.set_ylim(0, 1.05)\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'f1_comparison.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Saved: f1_comparison.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:30:00.913943Z","iopub.execute_input":"2026-04-11T10:30:00.914335Z","iopub.status.idle":"2026-04-11T10:30:01.510570Z","shell.execute_reply.started":"2026-04-11T10:30:00.914307Z","shell.execute_reply":"2026-04-11T10:30:01.509806Z"}},"outputs":[],"execution_count":null},{"id":"eb9bda71-7971-49be-879e-2c7a84a4f30c","cell_type":"markdown","source":"## Cell 10 — Grad-CAM Visualization\nVisualize which regions the best model focuses on per family.","metadata":{}},{"id":"0078ffae-1a1d-4d8a-865c-fcd2f7d89e7f","cell_type":"code","source":"from pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\n\n# ── Chon model tot nhat theo val_f1 ──\nbest_bk = max(RESULTS, key=lambda k: RESULTS[k]['val_f1'])\nbest_model = RESULTS[best_bk]['model'].eval()\nprint(f'Best backbone: {best_bk}  (val_f1={RESULTS[best_bk][\"val_f1\"]:.4f})')\n\n\ndef get_target_layer(model, backbone_key):\n    \"\"\"Tra ve layer cuoi cung cua backbone de Grad-CAM.\"\"\"\n    bk = model.backbone\n    if backbone_key == 'vgg16':\n        return [bk.features[-1]]\n    elif backbone_key == 'resnet50':\n        return [bk.layer4[-1]]\n    elif backbone_key == 'efficientnet':\n        return [bk.blocks[-1]]\n    elif backbone_key == 'convnext_v2':\n        return [bk.stages[-1]]\n    raise ValueError(f'Unknown backbone: {backbone_key}')\n\n\ntarget_layers = get_target_layer(best_model, best_bk)\ncam = GradCAM(model=best_model, target_layers=target_layers)\n\n# ── Lay 1 anh mau cho moi family ──\nfig, axes = plt.subplots(3, NUM_CLASSES, figsize=(NUM_CLASSES * 3, 9))\ninv_norm = transforms.Normalize(\n    mean=[-1.0, -1.0, -1.0], std=[2.0, 2.0, 2.0]\n)\n\nfor col, cls in enumerate(CLASSES):\n    cls_dir  = TRAIN_DIR / cls\n    img_path = list(cls_dir.glob('*.png'))[0]\n    cls_idx  = CLASS2IDX[cls]\n\n    # Load & preprocess\n    pil_img = Image.open(img_path).convert('RGB')\n    tensor  = val_tf(pil_img).unsqueeze(0).to(DEVICE)\n\n    # Grad-CAM\n    targets  = [ClassifierOutputTarget(cls_idx)]\n    grayscale_cam = cam(input_tensor=tensor, targets=targets)[0]\n\n    # Reconstruct RGB image for overlay\n    rgb_img = inv_norm(tensor[0]).permute(1,2,0).cpu().numpy()\n    rgb_img = np.clip(rgb_img, 0, 1)\n    cam_image = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)\n\n    # Row 0: original\n    axes[0, col].imshow(pil_img, cmap='gray')\n    axes[0, col].set_title(cls[:12], fontsize=8, fontweight='bold')\n    axes[0, col].axis('off')\n\n    # Row 1: heatmap only\n    axes[1, col].imshow(grayscale_cam, cmap='jet')\n    axes[1, col].axis('off')\n\n    # Row 2: overlay\n    axes[2, col].imshow(cam_image)\n    axes[2, col].axis('off')\n\n# Row labels\nfor row, label in enumerate(['Original', 'Grad-CAM Heatmap', 'Overlay']):\n    axes[row, 0].set_ylabel(label, fontsize=10, fontweight='bold')\n\nplt.suptitle(f'Grad-CAM Visualization — {best_bk}\\n(1 sample per malware family)',\n             fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'gradcam_visualization.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Saved: gradcam_visualization.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:30:01.511970Z","iopub.execute_input":"2026-04-11T10:30:01.512506Z","iopub.status.idle":"2026-04-11T10:30:10.043481Z","shell.execute_reply.started":"2026-04-11T10:30:01.512481Z","shell.execute_reply":"2026-04-11T10:30:10.042607Z"}},"outputs":[],"execution_count":null},{"id":"384cdc28-c809-4a95-b88d-6ccbebb46398","cell_type":"markdown","source":"## Cell 11 — Grad-CAM Intra/Inter Family Consistency\nPhân tích RQ2 của proposal: heatmap có nhất quán trong cùng family không?","metadata":{}},{"id":"68484786-adc2-4844-9773-586de971cc0a","cell_type":"code","source":"from scipy.stats import pearsonr\n\ndef get_cam_maps(model_used, bk_key, class_name, n_samples=5):\n    \"\"\"Lay n_samples Grad-CAM map cho 1 class.\"\"\"\n    tl  = get_target_layer(model_used, bk_key)\n    cam_local = GradCAM(model=model_used, target_layers=tl)\n    cls_idx = CLASS2IDX[class_name]\n    cls_dir = TRAIN_DIR / class_name\n    imgs    = list(cls_dir.glob('*.png'))[:n_samples]\n    maps = []\n    for img_path in imgs:\n        tensor = val_tf(Image.open(img_path).convert('RGB')).unsqueeze(0).to(DEVICE)\n        gc = cam_local(input_tensor=tensor,\n                       targets=[ClassifierOutputTarget(cls_idx)])[0]\n        maps.append(gc.flatten())\n    return maps\n\n\nprint(f'Computing intra-family correlation (model: {best_bk})...')\nN_SAMPLES = 5\nintra_corrs = {}\nfor cls in CLASSES:\n    maps = get_cam_maps(best_model, best_bk, cls, N_SAMPLES)\n    if len(maps) < 2: continue\n    corrs = []\n    for j in range(len(maps)):\n        for k in range(j+1, len(maps)):\n            r, _ = pearsonr(maps[j], maps[k])\n            corrs.append(r)\n    intra_corrs[cls] = np.mean(corrs)\n    print(f'  {cls:<22} intra-corr = {intra_corrs[cls]:.4f}')\n\n# Bar chart\nfig, ax = plt.subplots(figsize=(12, 5))\nclasses_sorted = sorted(intra_corrs, key=lambda c: intra_corrs[c], reverse=True)\nvals = [intra_corrs[c] for c in classes_sorted]\nbars = ax.bar(classes_sorted, vals, color='steelblue', alpha=0.85)\nax.axhline(np.mean(vals), color='red', linestyle='--', label=f'Mean={np.mean(vals):.3f}')\nax.set_ylabel('Pearson Correlation')\nax.set_title('Intra-Family Grad-CAM Consistency\\n(higher = more consistent heatmaps within same family)',\n             fontweight='bold')\nax.set_xticklabels(classes_sorted, rotation=45, ha='right')\nax.legend()\nax.grid(True, axis='y', alpha=0.3)\nax.set_ylim(-0.1, 1.05)\nplt.tight_layout()\nplt.savefig(OUT_DIR / 'intra_family_consistency.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Saved: intra_family_consistency.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:30:10.044757Z","iopub.execute_input":"2026-04-11T10:30:10.045493Z","iopub.status.idle":"2026-04-11T10:30:12.340976Z","shell.execute_reply.started":"2026-04-11T10:30:10.045448Z","shell.execute_reply":"2026-04-11T10:30:12.340089Z"}},"outputs":[],"execution_count":null},{"id":"b3ec034b-4f49-4552-a347-f0126af1a4e0","cell_type":"markdown","source":"## Cell 12 — Save All Results to CSV","metadata":{}},{"id":"9d03f1e1-5e78-4ecf-9f00-e823cd18dd3b","cell_type":"code","source":"rows = []\nfor bk, res in RESULTS.items():\n    report = classification_report(\n        res['labels_gt'], res['preds'],\n        target_names=CLASSES, output_dict=True\n    )\n    row = {\n        'backbone'       : bk,\n        'val_accuracy'   : round(res['val_acc'], 6),\n        'val_macro_f1'   : round(res['val_f1'], 6),\n        'val_macro_prec' : round(report['macro avg']['precision'], 6),\n        'val_macro_rec'  : round(report['macro avg']['recall'], 6),\n    }\n    for cls in CLASSES:\n        row[f'f1_{cls}'] = round(report[cls]['f1-score'], 6)\n    rows.append(row)\n\ndf_results = pd.DataFrame(rows)\ndf_results.to_csv(OUT_DIR / 'model_results.csv', index=False)\nprint('Saved: model_results.csv')\nprint(df_results[['backbone','val_accuracy','val_macro_f1','val_macro_prec','val_macro_rec']].to_string(index=False))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T10:30:12.341996Z","iopub.execute_input":"2026-04-11T10:30:12.342329Z","iopub.status.idle":"2026-04-11T10:30:12.396924Z","shell.execute_reply.started":"2026-04-11T10:30:12.342307Z","shell.execute_reply":"2026-04-11T10:30:12.396248Z"}},"outputs":[],"execution_count":null}]}