{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431},{"sourceType":"datasetVersion","sourceId":16017101,"datasetId":10273078,"databundleVersionId":16981602},{"sourceType":"kernelVersion","sourceId":316146124}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nfrom sklearn.model_selection import train_test_split\n\nBASE_PATH  = '../input/aptos2019-blindness-detection'\nMODEL_PATH = '/kaggle/input/datasets/andimuhammadnaufall/aptos-ver10'\n\ndf_full = pd.read_csv(f'{BASE_PATH}/train.csv')\n\ntrain_val_df, test_df = train_test_split(\n    df_full, test_size=0.10,\n    stratify=df_full['diagnosis'], random_state=42\n)\ntrain_df, val_df = train_test_split(\n    train_val_df, test_size=0.111,\n    stratify=train_val_df['diagnosis'], random_state=42\n)\n\nprint(f\"Split selesai:\")\nprint(f\"  Train : {len(train_df)} sampel\")\nprint(f\"  Val   : {len(val_df)} sampel\")\nprint(f\"  Test  : {len(test_df)} sampel\")\nprint(f\"\\n{'Kelas':<8} {'Label':<20} {'Train':>8} {'Val':>8} {'Test':>8}\")\n\nCLASS_NAMES = ['No DR', 'Mild', 'Moderate', 'Severe', 'PDR']\nfor cls in range(5):\n    t  = (train_df['diagnosis'] == cls).sum()\n    v  = (val_df['diagnosis']   == cls).sum()\n    te = (test_df['diagnosis']  == cls).sum()\n    print(f\"{cls:<8} {CLASS_NAMES[cls]:<20} {t:>8} {v:>8} {te:>8}\")\n\n# Tampilkan 1 sampel gambar per kelas SEBELUM preprocessing\n# Display 1 sample image per class BEFORE preprocessing\nprint(\"\\nSample images from APTOS 2019 dataset (before preprocessing):\")\nfig, axes = plt.subplots(1, 5, figsize=(20, 4))\nfig.suptitle(\n    'APTOS 2019 Dataset — Sample Images Before Preprocessing',\n    fontsize=13, fontweight='bold'\n)\nfor cls in range(5):\n    sample   = df_full[df_full['diagnosis'] == cls].iloc[2]\n    img_path = f\"{BASE_PATH}/train_images/{sample['id_code']}.png\"\n    image    = cv2.imread(img_path)\n    image    = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    axes[cls].imshow(image)\n    axes[cls].set_title(\n        f\"Class {cls}: {CLASS_NAMES[cls]}\\n(n={df_full['diagnosis'].eq(cls).sum()})\",\n        fontsize=10\n    )\n    axes[cls].axis('off')\nplt.tight_layout()\n\nplt.savefig('dataset_samples_before_preprocessing.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"Saved: dataset_samples_before_preprocessing.png\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install coral-pytorch -q\n!pip install albumentations -q\n!pip install grad-cam -q\n\nimport random, os\nimport numpy as np\nimport torch\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark     = False\n    os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'\n\nseed_everything(42)\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ndef get_gpu_memory():\n    if torch.cuda.is_available():\n        return f\"VRAM: {torch.cuda.memory_allocated() / 1024**2:.1f} MB\"\n    return \"CPU\"\n\nprint(f\"Cell 1 Siap. Device: {device}. {get_gpu_memory()}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nfrom sklearn.model_selection import train_test_split\n\nBASE_PATH  = '../input/aptos2019-blindness-detection'\nMODEL_PATH = '/kaggle/input/datasets/andimuhammadnaufall/aptos-ver10'\n\ndf_full = pd.read_csv(f'{BASE_PATH}/train.csv')\n\ntrain_val_df, test_df = train_test_split(\n    df_full, test_size=0.10,\n    stratify=df_full['diagnosis'], random_state=42\n)\ntrain_df, val_df = train_test_split(\n    train_val_df, test_size=0.111,\n    stratify=train_val_df['diagnosis'], random_state=42\n)\n\nprint(f\"Split selesai:\")\nprint(f\"  Train : {len(train_df)} sampel\")\nprint(f\"  Val   : {len(val_df)} sampel\")\nprint(f\"  Test  : {len(test_df)} sampel\")\nprint(f\"\\n{'Kelas':<8} {'Label':<20} {'Train':>8} {'Val':>8} {'Test':>8}\")\n\nCLASS_NAMES = ['No DR', 'Mild', 'Moderate', 'Severe', 'PDR']\nfor cls in range(5):\n    t  = (train_df['diagnosis'] == cls).sum()\n    v  = (val_df['diagnosis']   == cls).sum()\n    te = (test_df['diagnosis']  == cls).sum()\n    print(f\"{cls:<8} {CLASS_NAMES[cls]:<20} {t:>8} {v:>8} {te:>8}\")\n\n# 1 sampel gambar per kelas SEBELUM preprocessing\nprint(\"\\nContoh gambar dari dataset APTOS 2019 (sebelum preprocessing):\")\nfig, axes = plt.subplots(1, 5, figsize=(20, 4))\nfig.suptitle(\n    'Contoh Gambar Dataset APTOS 2019 — Sebelum Preprocessing',\n    fontsize=13, fontweight='bold'\n)\nfor cls in range(5):\n    sample   = df_full[df_full['diagnosis'] == cls].iloc[2]\n    img_path = f\"{BASE_PATH}/train_images/{sample['id_code']}.png\"\n    image    = cv2.imread(img_path)\n    image    = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    axes[cls].imshow(image)\n    axes[cls].set_title(\n        f\"Kelas {cls}: {CLASS_NAMES[cls]}\\n(n={df_full['diagnosis'].eq(cls).sum()})\",\n        fontsize=10\n    )\n    axes[cls].axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader\n\ndef resize_with_padding(image, target_size=448):\n    h, w = image.shape[:2]\n    ratio = target_size / max(h, w)\n    new_h, new_w = int(h * ratio), int(w * ratio)\n    image = cv2.resize(image, (new_w, new_h))\n    delta_w = target_size - new_w\n    delta_h = target_size - new_h\n    top, bottom = delta_h // 2, delta_h - (delta_h // 2)\n    left, right = delta_w // 2, delta_w - (delta_w // 2)\n    return cv2.copyMakeBorder(\n        image, top, bottom, left, right,\n        cv2.BORDER_CONSTANT, value=[0, 0, 0]\n    )\n\ndef preprocess_image(image):\n    image = cv2.GaussianBlur(image, (3, 3), sigmaX=0)\n    lab   = cv2.cvtColor(image, cv2.COLOR_RGB2LAB)\n    l, a, b = cv2.split(lab)\n    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n    return cv2.cvtColor(\n        cv2.merge((clahe.apply(l), a, b)), cv2.COLOR_LAB2RGB\n    )\n\nval_transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nclass APTOSDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df        = df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row   = self.df.iloc[idx]\n        image = cv2.imread(f\"{BASE_PATH}/train_images/{row['id_code']}.png\")\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        image = resize_with_padding(image, 448)\n        image = preprocess_image(image)\n        if self.transform:\n            image = self.transform(image)\n        return image, row['diagnosis']\n\nval_loader  = DataLoader(APTOSDataset(val_df,  val_transform), batch_size=16, shuffle=False)\ntest_loader = DataLoader(APTOSDataset(test_df, val_transform), batch_size=16, shuffle=False)\n\n# sebelum vs sesudah per kelas\n# Visualize preprocessing impact: before vs after per class\nprint(\"Preprocessing impact on retinal fundus images:\")\nfig, axes = plt.subplots(2, 5, figsize=(20, 8))\nfig.suptitle(\n    'Preprocessing Comparison: Before vs. After\\n'\n    '(Resize+Padding → GaussianBlur → CLAHE)',\n    fontsize=15, fontweight='bold'\n)\nfor cls in range(5):\n    sample   = df_full[df_full['diagnosis'] == cls].iloc[2]\n    img_path = f\"{BASE_PATH}/train_images/{sample['id_code']}.png\"\n\n    img_raw = cv2.imread(img_path)\n    img_raw = cv2.cvtColor(img_raw, cv2.COLOR_BGR2RGB)\n    axes[0, cls].imshow(img_raw)\n    axes[0, cls].set_title(\n        f\"Class {cls}: {CLASS_NAMES[cls]}\\nBefore\", fontsize=15\n    )\n    axes[0, cls].axis('off')\n\n    img_proc = resize_with_padding(img_raw, 448)\n    img_proc = preprocess_image(img_proc)\n    axes[1, cls].imshow(img_proc)\n    axes[1, cls].set_title(\n        f\"Class {cls}: {CLASS_NAMES[cls]}\\nAfter\", fontsize=15\n    )\n    axes[1, cls].axis('off')\n\nplt.tight_layout()\n# [TAMBAHAN] Simpan sebagai file agar tersimpan di output notebook\nplt.savefig('preprocessing_comparison.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint(\"Saved: preprocessing_comparison.png\")\nprint(f\"Cell 3 Ready. Val: {len(val_loader)} batches, Test: {len(test_loader)} batches\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\n\nmodel_cn = timm.create_model(\n    'convnextv2_tiny', pretrained=False, num_classes=4\n).to(device)\nmodel_cn.load_state_dict(torch.load(\n    f'{MODEL_PATH}/ConvNeXtV2_best_robust.pth',\n    map_location=device, weights_only=True\n))\nmodel_cn.eval()\n\nmodel_vit = timm.create_model(\n    'vit_base_r50_s16_224', pretrained=False, num_classes=4, img_size=448\n).to(device)\nmodel_vit.load_state_dict(torch.load(\n    f'{MODEL_PATH}/ViTHybrid_best_robust.pth',\n    map_location=device, weights_only=True\n))\nmodel_vit.eval()\n\nprint(f\"Cell 4 Siap. Kedua model ver_10 berhasil dimuat. {get_gpu_memory()}\")\nprint(f\"ConvNeXtV2 params : {sum(p.numel() for p in model_cn.parameters()):,}\")\nprint(f\"ViT Hybrid params : {sum(p.numel() for p in model_vit.parameters()):,}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nfrom scipy import stats\nfrom sklearn.metrics import (\n    cohen_kappa_score, accuracy_score, confusion_matrix,\n    classification_report, roc_auc_score\n)\nfrom coral_pytorch.dataset import corn_label_from_logits\n\ndef corn_logits_to_probs(logits):\n    cum      = torch.sigmoid(logits)\n    ones     = torch.ones(cum.size(0),  1, device=logits.device)\n    zeros    = torch.zeros(cum.size(0), 1, device=logits.device)\n    extended = torch.cat([ones, cum, zeros], dim=1)\n    probs    = extended[:, :-1] - extended[:, 1:]\n    probs    = torch.clamp(probs, min=1e-8)\n    return probs / probs.sum(dim=1, keepdim=True)\n\ndef center_crop_tensor(imgs, crop_ratio=0.9):\n    B, C, H, W = imgs.shape\n    crop_h  = int(H * crop_ratio)\n    crop_w  = int(W * crop_ratio)\n    start_h = (H - crop_h) // 2\n    start_w = (W - crop_w) // 2\n    cropped = imgs[:, :, start_h:start_h+crop_h, start_w:start_w+crop_w]\n    return torch.nn.functional.interpolate(\n        cropped, size=(H, W), mode='bilinear', align_corners=False\n    )\n\ndef collect_all_voters(model_cn, model_vit, loader):\n    model_cn.eval()\n    model_vit.eval()\n    all_votes, all_probs_cn, all_probs_vit, all_labels = [], [], [], []\n\n    with torch.no_grad():\n        for imgs, lbls in loader:\n            imgs = imgs.to(device)\n            versions = [\n                imgs,\n                torch.flip(imgs, dims=[3]),\n                torch.flip(imgs, dims=[2]),\n                center_crop_tensor(imgs, crop_ratio=0.90),\n                center_crop_tensor(imgs, crop_ratio=0.85),\n            ]\n            batch_votes_cn, batch_votes_vit = [], []\n            batch_probs_cn, batch_probs_vit = [], []\n\n            for v_img in versions:\n                logits_cn  = model_cn(v_img)\n                logits_vit = model_vit(v_img)\n                batch_votes_cn.append(corn_label_from_logits(logits_cn).cpu().numpy())\n                batch_votes_vit.append(corn_label_from_logits(logits_vit).cpu().numpy())\n                batch_probs_cn.append(corn_logits_to_probs(logits_cn).cpu().numpy())\n                batch_probs_vit.append(corn_logits_to_probs(logits_vit).cpu().numpy())\n\n            votes_cn  = np.stack(batch_votes_cn,  axis=0)\n            votes_vit = np.stack(batch_votes_vit, axis=0)\n            votes_10  = np.concatenate([votes_cn, votes_vit], axis=0).T\n\n            all_votes.append(votes_10)\n            all_probs_cn.append(np.mean(batch_probs_cn,  axis=0))\n            all_probs_vit.append(np.mean(batch_probs_vit, axis=0))\n            all_labels.extend(lbls.cpu().numpy())\n\n    return (\n        np.vstack(all_votes),\n        np.vstack(all_probs_cn),\n        np.vstack(all_probs_vit),\n        all_labels\n    )\n\ndef majority_vote_10(votes, probs_cn, probs_vit, split_name=\"\"):\n    final_preds, tie_cases, vote_margins = [], 0, []\n\n    for i in range(len(votes)):\n        row         = votes[i]\n        mode_result = stats.mode(row)\n        winner      = int(mode_result.mode)\n        count       = int(mode_result.count)\n        vote_margins.append(count)\n\n        if count > 5:\n            final_preds.append(winner)\n        else:\n            tie_cases += 1\n            unique, counts_arr = np.unique(row, return_counts=True)\n            top2_idx      = np.argsort(counts_arr)[::-1][:2]\n            candidate_cls = unique[top2_idx]\n            avg_probs     = 0.5 * probs_cn[i] + 0.5 * probs_vit[i]\n            best_cls      = candidate_cls[\n                np.argmax([avg_probs[c] for c in candidate_cls])\n            ]\n            final_preds.append(int(best_cls))\n\n    margins = np.array(vote_margins)\n    print(f\"\\n  Voting Margin Statistics — {split_name}:\")\n    print(f\"  {'Margin':<30} {'Count':>8} {'Percentage':>12}\")\n    print(f\"  {'-'*52}\")\n    for m in range(6, 11):\n        cm = (margins == m).sum()\n        print(f\"  {str(m)+' votes ('+str(m*10)+'% consensus)':<30} {cm:>8} {cm/len(votes)*100:>11.1f}%\")\n    print(f\"  {'Tie (5 vs 5)':<30} {tie_cases:>8} {tie_cases/len(votes)*100:>11.1f}%\")\n    print(f\"  {'Average margin':<30} {margins.mean():>8.4f} / 10\")\n\n    return np.array(final_preds)\n\ndef compute_metrics(preds, probs, labels, split_name):\n    qwk      = cohen_kappa_score(labels, preds, weights='quadratic')\n    accuracy = accuracy_score(labels, preds)\n    auc      = roc_auc_score(labels, probs, multi_class='ovr')\n    report   = classification_report(\n        labels, preds,\n        target_names=['No DR (0)', 'Mild (1)', 'Moderate (2)', 'Severe (3)', 'PDR (4)'],\n        output_dict=True\n    )\n    macro_f1 = report['macro avg']['f1-score']\n\n    print(f\"\\n{'='*60}\")\n    print(f\"EVALUATION RESULTS — {split_name.upper()} SET\")\n    print(f\"{'='*60}\")\n    print(f\"  QWK (Quadratic Weighted Kappa) : {qwk:.4f}\")\n    print(f\"  Macro-F1                       : {macro_f1:.4f}\")\n    print(f\"  Accuracy                       : {accuracy*100:.4f}%\")\n    print(f\"  Macro-AUC (OvR)                : {auc:.4f}\")\n\n    print(f\"\\n  Precision, Recall & F1 per Class:\")\n    print(classification_report(\n        labels, preds,\n        target_names=['No DR (0)', 'Mild (1)', 'Moderate (2)', 'Severe (3)', 'PDR (4)'],\n        digits=4\n    ))\n\n    print(f\"  Per-Class Confidence (average prediction probability):\")\n    print(f\"  {'Class':<20} {'Confidence':>12} {'n samples':>10}\")\n    print(f\"  {'-'*44}\")\n    for cls_idx, cls_name in enumerate(\n        ['No DR (0)', 'Mild (1)', 'Moderate (2)', 'Severe (3)', 'PDR (4)']\n    ):\n        mask     = np.array(labels) == cls_idx\n        avg_conf = probs[mask, cls_idx].mean() if mask.sum() > 0 else 0\n        print(f\"  {cls_name:<20} {avg_conf:>12.4f} {mask.sum():>10}\")\n\n    plt.figure(figsize=(7, 5))\n    sns.heatmap(\n        confusion_matrix(labels, preds),\n        annot=True, fmt='d', cmap='Purples',\n        xticklabels=['No DR', 'Mild', 'Mod', 'Sev', 'PDR'],\n        yticklabels=['No DR', 'Mild', 'Mod', 'Sev', 'PDR']\n    )\n    plt.title(f'Confusion Matrix — {split_name} Set', fontsize=12, pad=12)\n    plt.ylabel('Actual Label')\n    plt.xlabel('Predicted Label')\n    plt.tight_layout()\n    # [TAMBAHAN] Simpan confusion matrix sebagai file\n    plt.savefig(f'confusion_matrix_{split_name.lower()}.png', dpi=150, bbox_inches='tight')\n    plt.show()\n    print(f\"Saved: confusion_matrix_{split_name.lower()}.png\")\n\n    return qwk, macro_f1, accuracy, auc\n\ndef disagreement_analysis(votes, labels, preds_ensemble):\n    labels    = np.array(labels)\n    hard_cn   = stats.mode(votes[:, :5],  axis=1).mode.flatten()\n    hard_vit  = stats.mode(votes[:, 5:],  axis=1).mode.flatten()\n\n    cn_correct  = (hard_cn  == labels)\n    vit_correct = (hard_vit == labels)\n    ens_correct = (preds_ensemble == labels)\n\n    both_correct   = (cn_correct  & vit_correct).sum()\n    cn_only        = (cn_correct  & ~vit_correct).sum()\n    vit_only       = (~cn_correct & vit_correct).sum()\n    both_wrong     = (~cn_correct & ~vit_correct).sum()\n    ens_saves      = (both_wrong  & ens_correct).sum()\n\n    total = len(labels)\n    print(f\"\\n{'='*60}\")\n    print(f\"ENSEMBLE COMPLEMENTARITY ANALYSIS\")\n    print(f\"{'='*60}\")\n    print(f\"  {'Condition':<42} {'n':>6} {'%':>8}\")\n    print(f\"  {'-'*58}\")\n    print(f\"  {'Both correct':<42} {both_correct:>6} {both_correct/total*100:>7.1f}%\")\n    print(f\"  {'Only ConvNeXtV2 correct':<42} {cn_only:>6} {cn_only/total*100:>7.1f}%\")\n    print(f\"  {'Only ViT Hybrid correct':<42} {vit_only:>6} {vit_only/total*100:>7.1f}%\")\n    print(f\"  {'Both wrong':<42} {both_wrong:>6} {both_wrong/total*100:>7.1f}%\")\n    print(f\"  {'Ensemble rescued both-wrong cases':<42} {ens_saves:>6} {ens_saves/total*100:>7.1f}%\")\n    print(f\"\\n  Interpretation: {cn_only + vit_only} cases ({(cn_only+vit_only)/total*100:.1f}%)\")\n    print(f\"  rescued by CNN vs Transformer architectural complementarity.\")\n\n\n# VALIDASI\n\nprint(\">>> Mengumpulkan prediksi Validation Set...\")\nvotes_val, probs_cn_val, probs_vit_val, val_labels = collect_all_voters(\n    model_cn, model_vit, val_loader\n)\npreds_val     = majority_vote_10(votes_val, probs_cn_val, probs_vit_val, \"Validation\")\nprobs_avg_val = 0.5 * probs_cn_val + 0.5 * probs_vit_val\nqwk_v, f1_v, acc_v, auc_v = compute_metrics(\n    preds_val, probs_avg_val, val_labels, \"Validation\"\n)\ndisagreement_analysis(votes_val, val_labels, preds_val)\n\n\n# TEST\n\nprint(\"\\n>>> Mengumpulkan prediksi Test Set...\")\nvotes_test, probs_cn_test, probs_vit_test, test_labels = collect_all_voters(\n    model_cn, model_vit, test_loader\n)\npreds_test     = majority_vote_10(votes_test, probs_cn_test, probs_vit_test, \"Test\")\nprobs_avg_test = 0.5 * probs_cn_test + 0.5 * probs_vit_test\nqwk_t, f1_t, acc_t, auc_t = compute_metrics(\n    preds_test, probs_avg_test, test_labels, \"Test\"\n)\ndisagreement_analysis(votes_test, test_labels, preds_test)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install grad-cam -q\n\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\nimport timm\n\n# ── LOAD MODEL UNTUK GRAD-CAM ─────────────────────────────────────────────────\n\nmodel_cn_cam = timm.create_model(\n    'convnextv2_tiny', pretrained=False, num_classes=4\n).to(device)\nmodel_cn_cam.load_state_dict(torch.load(\n    f'{MODEL_PATH}/ConvNeXtV2_best_robust.pth',\n    map_location=device, weights_only=True\n))\nmodel_cn_cam.eval()\n\nmodel_vit_cam = timm.create_model(\n    'vit_base_r50_s16_224', pretrained=False, num_classes=4, img_size=448\n).to(device)\nmodel_vit_cam.load_state_dict(torch.load(\n    f'{MODEL_PATH}/ViTHybrid_best_robust.pth',\n    map_location=device, weights_only=True\n))\nmodel_vit_cam.eval()\n\ntarget_layers_cn  = [model_cn_cam.stages[-1].blocks[-1]]\ntarget_layers_vit = [model_vit_cam.blocks[-1].norm1]\n\ndef vit_reshape_transform(tensor, height=28, width=28):\n    result = tensor[:, 1:, :].reshape(\n        tensor.size(0), height, width, tensor.size(2)\n    )\n    return result.transpose(2, 3).transpose(1, 2)\n\nprint(f\"Grad-CAM models loaded. {get_gpu_memory()}\")\n\n# ── HELPER FUNCTIONS ──────────────────────────────────────────────────────────\n\ndef load_image_for_cam(img_path):\n    image      = cv2.imread(img_path)\n    image      = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    image      = resize_with_padding(image, 448)\n    image      = preprocess_image(image)\n    image_f    = image.astype(np.float32) / 255.0\n    mean       = np.array([0.485, 0.456, 0.406])\n    std        = np.array([0.229, 0.224, 0.225])\n    image_norm = (image_f - mean) / std\n    tensor     = torch.from_numpy(\n        image_norm.transpose(2, 0, 1)\n    ).unsqueeze(0).float().to(device)\n    return image_f, tensor\n\ndef predict_ensemble_cam(tensor):\n    with torch.no_grad():\n        p_cn  = corn_logits_to_probs(model_cn_cam(tensor))\n        p_vit = corn_logits_to_probs(model_vit_cam(tensor))\n        return torch.argmax(0.5 * p_cn + 0.5 * p_vit, dim=1).item()\n\n# ── GRAD-CAM PER CLASS ────────────────────────────────────────────────────────\n\ndef plot_gradcam_per_class(model, target_layers, model_name, reshape_fn=None):\n    cam = GradCAM(\n        model=model, target_layers=target_layers,\n        reshape_transform=reshape_fn\n    )\n    fig = plt.figure(figsize=(22, 10))\n    fig.suptitle(\n        f'Grad-CAM — {model_name}: Activation Analysis per DR Class',\n        fontsize=19, fontweight='bold', y=1.01\n    )\n\n    for cls_idx in range(5):\n        sample        = test_df[test_df['diagnosis'] == cls_idx].iloc[0]\n        img_path      = f\"{BASE_PATH}/train_images/{sample['id_code']}.png\"\n        img_f, tensor = load_image_for_cam(img_path)\n\n        grayscale_cam = cam(input_tensor=tensor)[0]\n        visualization = show_cam_on_image(img_f, grayscale_cam, use_rgb=True)\n        pred_ensemble = predict_ensemble_cam(tensor)\n        color         = 'green' if pred_ensemble == cls_idx else 'red'\n\n        ax_top = fig.add_subplot(2, 5, cls_idx + 1)\n        ax_top.imshow(img_f)\n        ax_top.set_title(\n            f\"Original\\nClass {cls_idx}: {CLASS_NAMES[cls_idx]}\",\n            fontsize=15, pad=7\n        )\n        ax_top.axis('off')\n\n        ax_bot = fig.add_subplot(2, 5, cls_idx + 6)\n        ax_bot.imshow(visualization)\n        ax_bot.set_title(\"Grad-CAM Heatmap\", fontsize=15, pad=15)\n        ax_bot.set_xlabel(\n            f\"Ensemble Prediction: {CLASS_NAMES[pred_ensemble]}\",\n            fontsize=15, color=color, labelpad=6\n        )\n        ax_bot.tick_params(left=False, bottom=False,\n                           labelleft=False, labelbottom=False)\n\n    plt.tight_layout(rect=[0, 0, 1, 0.98])\n    plt.savefig(\n        f'gradcam_{model_name.replace(\" \", \"_\")}_per_class.png',\n        dpi=150, bbox_inches='tight'\n    )\n    plt.show()\n    print(f\"Saved: gradcam_{model_name.replace(' ', '_')}_per_class.png\")\n\n# ── GRAD-CAM ERROR ANALYSIS ───────────────────────────────────────────────────\n\ndef plot_gradcam_error(model, target_layers, model_name, reshape_fn=None):\n    cam           = GradCAM(\n        model=model, target_layers=target_layers,\n        reshape_transform=reshape_fn\n    )\n    wrong_samples = []\n\n    for _, row in test_df.sample(frac=1, random_state=42).iterrows():\n        img_path      = f\"{BASE_PATH}/train_images/{row['id_code']}.png\"\n        img_f, tensor = load_image_for_cam(img_path)\n        pred          = predict_ensemble_cam(tensor)\n        if pred != int(row['diagnosis']):\n            wrong_samples.append(\n                (img_f, tensor, int(row['diagnosis']), pred)\n            )\n        if len(wrong_samples) >= 4:\n            break\n\n    if not wrong_samples:\n        print(f\"No misclassified samples found for {model_name}.\")\n        return\n\n    n   = len(wrong_samples)\n    fig = plt.figure(figsize=(n * 4, 9))\n    fig.suptitle(\n        f'Grad-CAM Error Analysis — {model_name} (Misclassified Cases)',\n        fontsize=19, fontweight='bold', color='darkred', y=1.01\n    )\n\n    for i, (img_f, tens, true_cls, pred_cls) in enumerate(wrong_samples):\n        grayscale_cam = cam(input_tensor=tens)[0]\n        viz           = show_cam_on_image(img_f, grayscale_cam, use_rgb=True)\n\n        ax_top = fig.add_subplot(2, n, i + 1)\n        ax_top.imshow(img_f)\n        ax_top.set_title(\n            f\"Ground Truth\\n{CLASS_NAMES[true_cls]}\",\n            fontsize=15, pad=15\n        )\n        ax_top.axis('off')\n\n        ax_bot = fig.add_subplot(2, n, i + n + 1)\n        ax_bot.imshow(viz)\n        ax_bot.set_title(\"Grad-CAM Heatmap\", fontsize=15, pad=15)\n        ax_bot.set_xlabel(\n            f\"Predicted: {CLASS_NAMES[pred_cls]}\",\n            fontsize=15, color='red', labelpad=6\n        )\n        ax_bot.tick_params(left=False, bottom=False,\n                           labelleft=False, labelbottom=False)\n\n    plt.tight_layout(rect=[0, 0, 1, 0.98])\n    plt.savefig(\n        f'gradcam_{model_name.replace(\" \", \"_\")}_error_analysis.png',\n        dpi=150, bbox_inches='tight'\n    )\n    plt.show()\n    print(f\"Saved: gradcam_{model_name.replace(' ', '_')}_error_analysis.png\")\n\n# ── JALANKAN SEMUA GRAD-CAM ───────────────────────────────────────────────────\n\nprint(\"Running Grad-CAM for ConvNeXtV2...\")\nplot_gradcam_per_class(model_cn_cam,  target_layers_cn,  \"ConvNeXtV2\")\nplot_gradcam_error(model_cn_cam,      target_layers_cn,  \"ConvNeXtV2\")\n\nprint(\"\\nRunning Grad-CAM for ViT Hybrid...\")\nplot_gradcam_per_class(model_vit_cam, target_layers_vit, \"ViT Hybrid\",\n                       reshape_fn=vit_reshape_transform)\nplot_gradcam_error(model_vit_cam,     target_layers_vit, \"ViT Hybrid\",\n                   reshape_fn=vit_reshape_transform)\n\ntorch.cuda.empty_cache()\nprint(\"\\nCell 6 complete. All Grad-CAM outputs saved as PNG files.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}