{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":14774}],"dockerImageVersionId":31401,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"colab":{"provenance":[],"name":"DR_TanpaPre-Processing"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ConvNeXt-V2 — APTOS 2019 Diabetic Retinopathy Classification\n## Tanpa Preprocessing (Raw Images)\n\n**Model:** ConvNeXt-V2 (Fully Convolutional Masked Autoencoder)  \n**Dataset:** APTOS 2019 Blindness Detection  \n**Skenario:** Without Preprocessing  \n**XAI:** Grad-CAM  \n**Platform:** Kaggle GPU","metadata":{"id":"ytN_5mL1HsUp"}},{"cell_type":"markdown","source":"## 1. Install & Import Libraries","metadata":{"id":"GlroqFg3HsUs"}},{"cell_type":"code","source":"!pip install timm grad-cam -q\n\nimport os, random, numpy as np, pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport timm\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix, cohen_kappa_score\n\nfrom 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\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n\nset_seed(42)\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {DEVICE}')\nprint(f'GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"CPU\"}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:36:56.629105Z","iopub.execute_input":"2026-06-25T15:36:56.629400Z","iopub.status.idle":"2026-06-25T15:37:19.761116Z","shell.execute_reply.started":"2026-06-25T15:36:56.629365Z","shell.execute_reply":"2026-06-25T15:37:19.760081Z"},"id":"HRuO9vRRHsUt"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Load Dataset APTOS 2019","metadata":{"id":"OgZkORUMHsUu"}},{"cell_type":"code","source":"import os\nimport pandas as pd\n\nBASE_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\ndf = pd.read_csv(os.path.join(BASE_DIR, \"train.csv\"))\n\ndf[\"path\"] = df[\"id_code\"].apply(\n    lambda x: os.path.join(BASE_DIR, \"train_images\", f\"{x}.png\")\n)\n\nprint(f\"Total data: {len(df)}\")\n\n# `display()` hanya tersedia otomatis di Jupyter/Kaggle/Colab.\n# Fallback ke print agar cell ini tetap aman dijalankan di environment lain.\ntry:\n    display(df.head())\nexcept NameError:\n    print(df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:19.762728Z","iopub.execute_input":"2026-06-25T15:37:19.763195Z","iopub.status.idle":"2026-06-25T15:37:19.808373Z","shell.execute_reply.started":"2026-06-25T15:37:19.763168Z","shell.execute_reply":"2026-06-25T15:37:19.807714Z"},"id":"0iptrgiXHsUu"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Contoh Gambar per Kelas (Tanpa Preprocessing)","metadata":{"id":"lxYS6NlKHsUv"}},{"cell_type":"code","source":"from PIL import Image\nimport matplotlib.pyplot as plt\n\nlabel_names = {\n    0: 'No DR',\n    1: 'Mild',\n    2: 'Moderate',\n    3: 'Severe',\n    4: 'Proliferative'\n}\n\nfig, axes = plt.subplots(1, 5, figsize=(20, 4))\n\nfor grade in range(5):\n    sample = df[df['diagnosis'] == grade].sample(1, random_state=42).iloc[0]\n\n    img = Image.open(sample['path']).convert('RGB')\n\n    axes[grade].imshow(img)\n    axes[grade].set_title(f'Grade {grade}: {label_names[grade]}')\n    axes[grade].axis('off')\n\nplt.suptitle('Contoh Gambar Retina - Tanpa Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:19.809561Z","iopub.execute_input":"2026-06-25T15:37:19.809844Z","iopub.status.idle":"2026-06-25T15:37:22.030397Z","shell.execute_reply.started":"2026-06-25T15:37:19.809820Z","shell.execute_reply":"2026-06-25T15:37:22.029264Z"},"id":"jM2DMBiiHsUv"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Train/Validation/Test Split\n","metadata":{"id":"7k52tQ32HsUw"}},{"cell_type":"code","source":"# 80% train, 20% sementara\ntrain_df, temp_df = train_test_split(\n    df,\n    test_size=0.2,\n    random_state=42,\n    stratify=df['diagnosis']\n)\n\n# 20% tadi dibagi lagi menjadi 10% val dan 10% test\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.5,\n    random_state=42,\n    stratify=temp_df['diagnosis']\n)\n\nprint(f\"Train      : {len(train_df)}\")\nprint(f\"Validation : {len(val_df)}\")\nprint(f\"Test       : {len(test_df)}  <- dipakai khusus untuk evaluasi akhir (Bagian 10)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:22.032204Z","iopub.execute_input":"2026-06-25T15:37:22.032581Z","iopub.status.idle":"2026-06-25T15:37:22.052639Z","shell.execute_reply.started":"2026-06-25T15:37:22.032547Z","shell.execute_reply":"2026-06-25T15:37:22.051930Z"},"id":"4cL9SzF-HsUw"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Dataset & Transforms (Tanpa Preprocessing)","metadata":{"id":"gOPj6H7WHsUx"}},{"cell_type":"code","source":"IMG_SIZE = 224\nBATCH_SIZE = 16\n\n# =====================================================\n# Dataset Transforms\n# No retinal-specific preprocessing (CLAHE, cropping,\n# Ben Graham preprocessing, etc.)\n# =====================================================\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(15),\n\n    transforms.ColorJitter(\n        brightness=0.2,\n        contrast=0.2\n    ),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\n# val_transform juga dipakai untuk test set (tanpa augmentasi,\n# hanya resize + normalize, sesuai praktik evaluasi standar)\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\n\nclass APTOSDataset(Dataset):\n\n    def __init__(self, dataframe, transform=None):\n        self.df = dataframe.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n\n        row = self.df.iloc[idx]\n\n        image = Image.open(row['path']).convert('RGB')\n\n        if self.transform:\n            image = self.transform(image)\n\n        label = torch.tensor(\n            row['diagnosis'],\n            dtype=torch.long\n        )\n\n        return image, label\n\n\ntrain_dataset = APTOSDataset(\n    train_df,\n    transform=train_transform\n)\n\nval_dataset = APTOSDataset(\n    val_df,\n    transform=val_transform\n)\n\n# Test set terpisah — dipakai HANYA di evaluasi akhir (Bagian 10),\n# tidak pernah dilihat selama training maupun early stopping.\ntest_dataset = APTOSDataset(\n    test_df,\n    transform=val_transform\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(f\"Train batches : {len(train_loader)}\")\nprint(f\"Val batches   : {len(val_loader)}\")\nprint(f\"Test batches  : {len(test_loader)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:22.054615Z","iopub.execute_input":"2026-06-25T15:37:22.054874Z","iopub.status.idle":"2026-06-25T15:37:22.067707Z","shell.execute_reply.started":"2026-06-25T15:37:22.054851Z","shell.execute_reply":"2026-06-25T15:37:22.066671Z"},"id":"leRrutxDHsUx"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Build ConvNeXt-V2 Model","metadata":{"id":"j94_vBaeHsUy"}},{"cell_type":"code","source":"NUM_CLASSES = 5\n\nmodel = timm.create_model(\n    'convnextv2_tiny.fcmae_ft_in1k',\n    pretrained=True,\n    num_classes=NUM_CLASSES\n)\nmodel = model.to(DEVICE)\n\ntotal_params     = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f'Total parameters    : {total_params:,}')\nprint(f'Trainable parameters: {trainable_params:,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:22.068582Z","iopub.execute_input":"2026-06-25T15:37:22.068800Z","iopub.status.idle":"2026-06-25T15:37:30.213236Z","shell.execute_reply.started":"2026-06-25T15:37:22.068781Z","shell.execute_reply":"2026-06-25T15:37:30.212523Z"},"id":"AXGJiI3mHsUy"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Training Setup","metadata":{"id":"iC_aOnDOHsUy"}},{"cell_type":"code","source":"EPOCHS = 30\nLR     = 1e-4\n\nclass_counts  = df['diagnosis'].value_counts().sort_index().values\nclass_weights = torch.tensor(1.0 / class_counts, dtype=torch.float32)\nclass_weights = (class_weights / class_weights.sum()).to(DEVICE)\n\ncriterion = nn.CrossEntropyLoss(weight=class_weights)\noptimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)\nprint(f'Setup selesai! Epochs: {EPOCHS} | LR: {LR} | Batch: {BATCH_SIZE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:30.214110Z","iopub.execute_input":"2026-06-25T15:37:30.214440Z","iopub.status.idle":"2026-06-25T15:37:30.234554Z","shell.execute_reply.started":"2026-06-25T15:37:30.214393Z","shell.execute_reply":"2026-06-25T15:37:30.233860Z"},"id":"srzoa67lHsUy"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Training Loop","metadata":{"id":"if-N8DOSHsUy"}},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, cohen_kappa_score, roc_auc_score\nfrom sklearn.preprocessing import label_binarize\nfrom tqdm import tqdm\nimport numpy as np\n\ndef train_one_epoch(model, loader, criterion, optimizer, scaler):\n\n    model.train()\n\n    total_loss = 0\n    all_preds = []\n    all_labels = []\n\n    for imgs, labels in tqdm(loader, desc=\"Training\"):\n\n        imgs = imgs.to(DEVICE)\n        labels = labels.to(DEVICE)\n\n        optimizer.zero_grad()\n\n        with torch.amp.autocast(device_type=DEVICE.type, enabled=(DEVICE.type == 'cuda')):\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        total_loss += loss.item()\n\n        preds = outputs.argmax(dim=1)\n\n        all_preds.extend(preds.detach().cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\n    loss = total_loss / len(loader)\n\n    acc = accuracy_score(all_labels, all_preds)\n\n    qwk = cohen_kappa_score(\n        all_labels,\n        all_preds,\n        weights='quadratic'\n    )\n\n    return loss, acc, qwk\n\n\ndef validate(model, loader, criterion):\n\n    model.eval()\n\n    total_loss = 0\n    all_preds = []\n    all_labels = []\n    all_probs  = []   # probabilitas untuk AUC-ROC\n\n    with torch.no_grad():\n\n        for imgs, labels in tqdm(loader, desc=\"Validation\"):\n\n            imgs = imgs.to(DEVICE)\n            labels = labels.to(DEVICE)\n\n            with torch.amp.autocast(device_type=DEVICE.type, enabled=(DEVICE.type == 'cuda')):\n                outputs = model(imgs)\n                loss = criterion(outputs, labels)\n\n            total_loss += loss.item()\n\n            probs = torch.softmax(outputs, dim=1)\n\n            preds = probs.argmax(dim=1)\n\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    loss = total_loss / len(loader)\n\n    acc = accuracy_score(all_labels, all_preds)\n\n    qwk = cohen_kappa_score(\n        all_labels,\n        all_preds,\n        weights='quadratic'\n    )\n\n    # AUC-ROC — One-vs-Rest, macro average\n    all_labels_np = np.array(all_labels)\n    all_probs_np  = np.array(all_probs)\n    labels_bin    = label_binarize(all_labels_np, classes=[0, 1, 2, 3, 4])\n    auc = roc_auc_score(labels_bin, all_probs_np, multi_class='ovr', average='macro')\n\n    return loss, acc, qwk, auc, all_labels, all_preds, all_probs\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:30.235466Z","iopub.execute_input":"2026-06-25T15:37:30.235697Z","iopub.status.idle":"2026-06-25T15:37:30.665592Z","shell.execute_reply.started":"2026-06-25T15:37:30.235673Z","shell.execute_reply":"2026-06-25T15:37:30.664499Z"},"id":"Lxy-Z7s_HsUy"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==================================================\n# Training Loop + Early Stopping\n# ==================================================\n\nPATIENCE = 7\n\n# Mixed Precision (AMP) — mempercepat training di GPU (~2x lebih cepat)\nscaler = torch.amp.GradScaler(enabled=(DEVICE.type == 'cuda'))\n\nhistory = {\n    'train_loss': [],\n    'val_loss':   [],\n    'train_acc':  [],\n    'val_acc':    [],\n    'train_qwk':  [],\n    'val_qwk':    [],\n    'val_auc':    []   # AUC-ROC hanya dihitung di validation\n}\n\nbest_qwk = 0.0\ncounter  = 0\n\nfor epoch in range(1, EPOCHS + 1):\n\n    print(\"\\n\" + \"=\" * 60)\n    print(f\"Epoch {epoch}/{EPOCHS}\")\n    print(\"=\" * 60)\n\n    # -------------------------\n    # Training\n    # -------------------------\n    tr_loss, tr_acc, tr_qwk = train_one_epoch(\n        model=model,\n        loader=train_loader,\n        criterion=criterion,\n        optimizer=optimizer,\n        scaler=scaler\n    )\n\n    # -------------------------\n    # Validation\n    # -------------------------\n    vl_loss, vl_acc, vl_qwk, vl_auc, val_labels, val_preds, val_probs = validate(\n        model=model,\n        loader=val_loader,\n        criterion=criterion\n    )\n\n    # Update scheduler\n    scheduler.step()\n\n    # Save history\n    history['train_loss'].append(tr_loss)\n    history['val_loss'].append(vl_loss)\n    history['train_acc'].append(tr_acc)\n    history['val_acc'].append(vl_acc)\n    history['train_qwk'].append(tr_qwk)\n    history['val_qwk'].append(vl_qwk)\n    history['val_auc'].append(vl_auc)\n\n    # Print metrics\n    print(\n        f\"Train | Loss: {tr_loss:.4f} | \"\n        f\"Acc: {tr_acc:.4f} | \"\n        f\"QWK: {tr_qwk:.4f}\"\n    )\n\n    print(\n        f\"Val   | Loss: {vl_loss:.4f} | \"\n        f\"Acc: {vl_acc:.4f} | \"\n        f\"QWK: {vl_qwk:.4f} | \"\n        f\"AUC: {vl_auc:.4f}\"\n    )\n\n    # Current Learning Rate\n    current_lr = optimizer.param_groups[0]['lr']\n    print(f\"LR    | {current_lr:.8f}\")\n\n    # -------------------------\n    # Save Best Model\n    # -------------------------\n    if vl_qwk > best_qwk:\n\n        best_qwk = vl_qwk\n        counter  = 0\n\n        torch.save(\n            model.state_dict(),\n            \"best_convnextv2_aptos2019.pth\"\n        )\n\n        print(\n            f\"✅ Best Model Saved! \"\n            f\"(QWK = {best_qwk:.4f})\"\n        )\n\n    else:\n\n        counter += 1\n\n        print(\n            f\"⏳ No Improvement \"\n            f\"({counter}/{PATIENCE})\"\n        )\n\n    # -------------------------\n    # Early Stopping\n    # -------------------------\n    if counter >= PATIENCE:\n\n        print(\"\\n🛑 EARLY STOPPING TRIGGERED\")\n        print(f\"Best Validation QWK: {best_qwk:.4f}\")\n\n        break\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"🎉 TRAINING FINISHED\")\nprint(f\"Best Validation QWK: {best_qwk:.4f}\")\nprint(\"=\" * 60)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T15:37:30.666973Z","iopub.execute_input":"2026-06-25T15:37:30.667233Z","iopub.status.idle":"2026-06-25T19:16:48.470854Z","shell.execute_reply.started":"2026-06-25T15:37:30.667211Z","shell.execute_reply":"2026-06-25T19:16:48.463751Z"},"id":"V2mgJDZCHsUz"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Plot Training History","metadata":{"id":"rj3Bn4pFHsUz"}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 4, figsize=(24, 5))\n\nmetrics = [\n    ('train_loss', 'val_loss',  'Loss'),\n    ('train_acc',  'val_acc',   'Accuracy'),\n    ('train_qwk',  'val_qwk',  'QWK'),\n]\n\nfor ax, (tr_key, vl_key, title) in zip(axes[:3], metrics):\n    ax.plot(history[tr_key], label=f'Train {title}', color='blue')\n    ax.plot(history[vl_key], label=f'Val {title}',   color='orange')\n    ax.set_title(f'{title} per Epoch')\n    ax.set_xlabel('Epoch')\n    ax.legend()\n\n# Panel ke-4: Val AUC-ROC\naxes[3].plot(history['val_auc'], label='Val AUC-ROC (macro OvR)', color='green')\naxes[3].set_title('AUC-ROC per Epoch')\naxes[3].set_xlabel('Epoch')\naxes[3].legend()\n\nplt.suptitle('ConvNeXt-V2 Training History — Tanpa Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.savefig('training_history_no_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T19:16:48.478273Z","iopub.execute_input":"2026-06-25T19:16:48.478853Z","iopub.status.idle":"2026-06-25T19:16:50.510315Z","shell.execute_reply.started":"2026-06-25T19:16:48.478785Z","shell.execute_reply":"2026-06-25T19:16:50.509600Z"},"id":"9pXYS2RYHsUz"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Evaluasi Final (Test Set)\n","metadata":{"id":"leP7UeaiHsU0"}},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score, roc_curve, auc\nfrom sklearn.preprocessing import label_binarize\n\nmodel.load_state_dict(\n    torch.load('best_convnextv2_aptos2019.pth', map_location=DEVICE)\n)\n\nmodel.eval()\n\n# Evaluasi akhir memakai TEST SET\n_, final_acc, final_qwk, final_auc, true_labels, pred_labels, test_probs = validate(\n    model,\n    test_loader,\n    criterion\n)\n\nprint('=' * 50)\nprint('HASIL EVALUASI — ConvNeXt-V2 (Test Set)')\nprint('=' * 50)\nprint(f'Accuracy : {final_acc:.4f} ({final_acc*100:.2f}%)')\nprint(f'QWK      : {final_qwk:.4f}')\nprint(f'AUC-ROC  : {final_auc:.4f}  (macro OvR)')\n\nprint(\n    classification_report(\n        true_labels,\n        pred_labels,\n        target_names=[\n            'No DR',\n            'Mild',\n            'Moderate',\n            'Severe',\n            'Proliferative'\n        ]\n    )\n)\n\n# ── Plot ROC Curve per kelas ──────────────────────────────────────────────────\nimport numpy as np\n\ntest_probs_np  = np.array(test_probs)\ntrue_labels_np = np.array(true_labels)\ntrue_bin       = label_binarize(true_labels_np, classes=[0, 1, 2, 3, 4])\n\nclass_names = ['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative']\ncolors      = ['blue', 'orange', 'green', 'red', 'purple']\n\nplt.figure(figsize=(8, 6))\n\nfor i, (cname, color) in enumerate(zip(class_names, colors)):\n    fpr, tpr, _ = roc_curve(true_bin[:, i], test_probs_np[:, i])\n    roc_auc_i   = auc(fpr, tpr)\n    plt.plot(fpr, tpr, color=color, lw=2,\n             label=f'{cname} (AUC = {roc_auc_i:.3f})')\n\nplt.plot([0, 1], [0, 1], 'k--', lw=1)\nplt.xlim([0.0, 1.0])\nplt.ylim([0.0, 1.05])\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('ROC Curve per Kelas — ConvNeXt-V2 Tanpa Preprocessing (Test Set)')\nplt.legend(loc='lower right')\nplt.tight_layout()\nplt.savefig('roc_curve_no_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T19:16:50.511313Z","iopub.execute_input":"2026-06-25T19:16:50.512541Z","iopub.status.idle":"2026-06-25T19:17:33.163575Z","shell.execute_reply.started":"2026-06-25T19:16:50.512509Z","shell.execute_reply":"2026-06-25T19:17:33.162710Z"},"id":"GLMW80d_HsU0"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Confusion Matrix","metadata":{"id":"IlUzSrOgHsU0"}},{"cell_type":"code","source":"cm = confusion_matrix(true_labels, pred_labels)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=['No DR','Mild','Moderate','Severe','Proliferative'],\n            yticklabels=['No DR','Mild','Moderate','Severe','Proliferative'])\nplt.title('Confusion Matrix — ConvNeXt-V2 Tanpa Preprocessing (Test Set)')\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.tight_layout()\nplt.savefig('confusion_matrix_no_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T19:17:33.165135Z","iopub.execute_input":"2026-06-25T19:17:33.165512Z","iopub.status.idle":"2026-06-25T19:17:33.517333Z","shell.execute_reply.started":"2026-06-25T19:17:33.165467Z","shell.execute_reply":"2026-06-25T19:17:33.516508Z"},"id":"mONCBX-uHsU0"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. XAI — Grad-CAM Visualization","metadata":{"id":"jfEIYOpsHsU0"}},{"cell_type":"code","source":"# Target layer Grad-CAM: depthwise conv (dwconv) di blok terakhir\n# stage terakhir ConvNeXt-V2.\n# ConvNeXt-V2 menggunakan struktur stages -> blocks -> dwconv\n# yang outputnya sudah channel-first (B,C,H,W), kompatibel\n# langsung dengan pytorch_grad_cam tanpa reshape_transform.\ntarget_layers = [model.stages[-1].blocks[-1].conv_dw]\n\ncam = GradCAM(model=model, target_layers=target_layers)\n\ndef generate_gradcam(model, img_path):\n    img_raw = Image.open(img_path).convert('RGB').resize((IMG_SIZE, IMG_SIZE))\n    img_np  = np.array(img_raw) / 255.0\n    transform = transforms.Compose([\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n    ])\n    img_tensor = transform(img_raw).unsqueeze(0).to(DEVICE)\n    with torch.no_grad():\n        output = model(img_tensor)\n        pred_class  = output.argmax(dim=1).item()\n        confidence  = torch.softmax(output, dim=1).max().item()\n    grayscale_cam = cam(input_tensor=img_tensor, targets=[ClassifierOutputTarget(pred_class)])[0]\n    visualization = show_cam_on_image(img_np.astype(np.float32), grayscale_cam, use_rgb=True)\n    return img_np, grayscale_cam, visualization, pred_class, confidence\n\nfig, axes = plt.subplots(5, 3, figsize=(15, 25))\nfor grade in range(5):\n    # random_state=42 -> sama dengan gambar contoh di Bagian 3,\n    # dan hasilnya reproducible setiap notebook dijalankan ulang\n    # (sebelumnya .sample(1) tanpa seed -> gambar acak beda setiap run)\n    sample = df[df['diagnosis'] == grade].sample(1, random_state=42).iloc[0]\n    img_np, heatmap, cam_img, pred, conf = generate_gradcam(model, sample['path'])\n    axes[grade, 0].imshow(img_np)\n    axes[grade, 0].set_title(f'Original\\nTrue: {label_names[grade]}')\n    axes[grade, 0].axis('off')\n    axes[grade, 1].imshow(heatmap, cmap='jet')\n    axes[grade, 1].set_title('Grad-CAM Heatmap')\n    axes[grade, 1].axis('off')\n    axes[grade, 2].imshow(cam_img)\n    axes[grade, 2].set_title(f'Overlay\\nPred: {label_names[pred]} ({conf:.2%})')\n    axes[grade, 2].axis('off')\n\nplt.suptitle('Grad-CAM XAI — ConvNeXt-V2 Tanpa Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.savefig('gradcam_no_preprocessing.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Grad-CAM tersimpan!')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T19:17:33.518445Z","iopub.execute_input":"2026-06-25T19:17:33.518719Z","iopub.status.idle":"2026-06-25T19:17:40.745801Z","shell.execute_reply.started":"2026-06-25T19:17:33.518691Z","shell.execute_reply":"2026-06-25T19:17:40.744845Z"},"id":"SJ532L3vHsU1"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Ringkasan Hasil","metadata":{"id":"PA-C6WktHsU1"}},{"cell_type":"code","source":"print('=' * 60)\nprint('RINGKASAN — ConvNeXt-V2 TANPA PREPROCESSING')\nprint('=' * 60)\nprint(f'Model      : ConvNeXt-V2-Tiny (timm)')\nprint(f'Dataset    : APTOS 2019')\nprint(f'Skenario   : Tanpa Preprocessing')\nprint(f'Epochs     : {EPOCHS} | Batch: {BATCH_SIZE} | LR: {LR}')\nprint(f'Test Acc   : {final_acc:.4f} ({final_acc*100:.2f}%)')\nprint(f'Test QWK   : {final_qwk:.4f}')\nprint(f'Test AUC   : {final_auc:.4f}  (macro OvR)')\nprint('=' * 60)\nprint('File output:')\nprint('  best_convnextv2_aptos2019.pth')\nprint('  training_history_no_preprocessing.png')\nprint('  confusion_matrix_no_preprocessing.png')\nprint('  roc_curve_no_preprocessing.png')\nprint('  gradcam_no_preprocessing.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-25T19:17:40.748255Z","iopub.execute_input":"2026-06-25T19:17:40.748512Z","iopub.status.idle":"2026-06-25T19:17:40.755870Z","shell.execute_reply.started":"2026-06-25T19:17:40.748491Z","shell.execute_reply":"2026-06-25T19:17:40.754663Z"},"id":"bzqRzIwaHsU1"},"outputs":[],"execution_count":null}]}