{"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":31400,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"colab":{"provenance":[],"name":"ConvNeXtV2_WithPreprocessing"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ConvNeXt-V2 — APTOS 2019 Diabetic Retinopathy Classification\n## Dengan Preprocessing (Crop + CLAHE)\n\n**Model:** ConvNeXt-V2 (Fully Convolutional Masked Autoencoder)  \n**Dataset:** APTOS 2019 Blindness Detection  \n**Skenario:** With Preprocessing (Crop + CLAHE)  \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!pip install opencv-python-headless -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-26T02:21:23.568262Z","iopub.execute_input":"2026-06-26T02:21:23.568671Z","iopub.status.idle":"2026-06-26T02:21:52.620412Z","shell.execute_reply.started":"2026-06-26T02:21:23.568641Z","shell.execute_reply":"2026-06-26T02:21:52.619581Z"},"id":"HRuO9vRRHsUt","outputId":"6df4c293-5957-49c2-a435-25bcfd44d4c1"},"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-26T02:21:52.622127Z","iopub.execute_input":"2026-06-26T02:21:52.622522Z","iopub.status.idle":"2026-06-26T02:21:52.667034Z","shell.execute_reply.started":"2026-06-26T02:21:52.622496Z","shell.execute_reply":"2026-06-26T02:21:52.666376Z"},"id":"0iptrgiXHsUu","outputId":"4fc78d76-22dd-49c9-a809-18ca1f7698f1"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Contoh Gambar per Kelas (Dengan 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 - Dengan Preprocessing (Crop + CLAHE)', fontsize=14)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T02:21:52.667819Z","iopub.execute_input":"2026-06-26T02:21:52.668358Z","iopub.status.idle":"2026-06-26T02:21:55.282065Z","shell.execute_reply.started":"2026-06-26T02:21:52.668326Z","shell.execute_reply":"2026-06-26T02:21:55.281032Z"},"id":"jM2DMBiiHsUv","outputId":"ddcef7e9-1acf-463f-d48d-f5f0109c65d4"},"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-26T02:21:55.283635Z","iopub.execute_input":"2026-06-26T02:21:55.284048Z","iopub.status.idle":"2026-06-26T02:21:55.304154Z","shell.execute_reply.started":"2026-06-26T02:21:55.284009Z","shell.execute_reply":"2026-06-26T02:21:55.303189Z"},"id":"4cL9SzF-HsUw","outputId":"b9212286-c64d-46f8-9c0e-7bd0a90fe9f0"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Dataset & Transforms (Dengan Preprocessing: Crop + CLAHE)","metadata":{"id":"gOPj6H7WHsUx"}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n\nIMG_SIZE   = 224\nBATCH_SIZE = 16\n\n# =====================================================\n# Preprocessing: Crop area hitam + CLAHE\n# =====================================================\n\ndef crop_black_border(img_np, threshold=10):\n    \"\"\"\n    Hapus area hitam di pinggir gambar retina.\n    img_np : numpy array (H, W, 3) RGB uint8\n    Returns: cropped numpy array\n    \"\"\"\n    gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)\n    _, mask = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)\n    coords = cv2.findNonZero(mask)\n    if coords is None:\n        return img_np\n    x, y, w, h = cv2.boundingRect(coords)\n    return img_np[y:y+h, x:x+w]\n\ndef apply_clahe(img_np, clip_limit=2.0, tile_grid=(8, 8)):\n    \"\"\"\n    Terapkan CLAHE di channel L (LAB color space).\n    img_np : numpy array (H, W, 3) RGB uint8\n    Returns: numpy array RGB uint8\n    \"\"\"\n    lab = cv2.cvtColor(img_np, cv2.COLOR_RGB2LAB)\n    clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid)\n    lab[:, :, 0] = clahe.apply(lab[:, :, 0])\n    return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)\n\ndef preprocess_retinal(pil_image):\n    \"\"\"\n    Pipeline preprocessing:\n    PIL RGB → Crop black border → CLAHE → PIL RGB\n    \"\"\"\n    img_np = np.array(pil_image)          # PIL → numpy\n    img_np = crop_black_border(img_np)    # 1. Crop area hitam\n    img_np = apply_clahe(img_np)          # 2. CLAHE\n    return Image.fromarray(img_np)        # numpy → PIL\n\n# =====================================================\n# Dataset Transforms (dengan preprocessing)\n# =====================================================\n\ntrain_transform = transforms.Compose([\n    transforms.Lambda(preprocess_retinal),   # Crop + CLAHE\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n\nval_transform = transforms.Compose([\n    transforms.Lambda(preprocess_retinal),   # Crop + CLAHE\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T02:21:55.306380Z","iopub.execute_input":"2026-06-26T02:21:55.306965Z","iopub.status.idle":"2026-06-26T02:21:55.337088Z","shell.execute_reply.started":"2026-06-26T02:21:55.306925Z","shell.execute_reply":"2026-06-26T02:21:55.335979Z"},"id":"leRrutxDHsUx","outputId":"dee82693-6ed2-4df5-9790-61ec758c47e3"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = APTOSDataset(train_df, transform=train_transform)\nval_dataset   = APTOSDataset(val_df,   transform=val_transform)\ntest_dataset  = APTOSDataset(test_df,  transform=val_transform)\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)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T02:21:55.338148Z","iopub.execute_input":"2026-06-26T02:21:55.339361Z","iopub.status.idle":"2026-06-26T02:21:55.353585Z","shell.execute_reply.started":"2026-06-26T02:21:55.339337Z","shell.execute_reply":"2026-06-26T02:21:55.352935Z"}},"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-26T02:21:55.354526Z","iopub.execute_input":"2026-06-26T02:21:55.354888Z","iopub.status.idle":"2026-06-26T02:22:00.222915Z","shell.execute_reply.started":"2026-06-26T02:21:55.354835Z","shell.execute_reply":"2026-06-26T02:22:00.222232Z"},"id":"AXGJiI3mHsUy","outputId":"13f70ee5-76c0-4735-a641-686c5802de44"},"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-26T02:22:00.223704Z","iopub.execute_input":"2026-06-26T02:22:00.224031Z","iopub.status.idle":"2026-06-26T02:22:00.243565Z","shell.execute_reply.started":"2026-06-26T02:22:00.223998Z","shell.execute_reply":"2026-06-26T02:22:00.242676Z"},"id":"srzoa67lHsUy","outputId":"a55b7910-5281-429e-b850-cf43e5d640f1"},"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-26T02:22:00.244501Z","iopub.execute_input":"2026-06-26T02:22:00.244798Z","iopub.status.idle":"2026-06-26T02:22:00.684691Z","shell.execute_reply.started":"2026-06-26T02:22:00.244775Z","shell.execute_reply":"2026-06-26T02:22:00.683953Z"},"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_prep.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-26T02:22:00.686017Z","iopub.execute_input":"2026-06-26T02:22:00.686396Z","iopub.status.idle":"2026-06-26T05:11:02.821840Z","shell.execute_reply.started":"2026-06-26T02:22:00.686362Z","shell.execute_reply":"2026-06-26T05:11:02.821048Z"},"id":"V2mgJDZCHsUz","outputId":"d91ab07a-1317-47ea-b489-5660f01ba429"},"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 — Dengan Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.savefig('training_history_with_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T05:11:02.823272Z","iopub.execute_input":"2026-06-26T05:11:02.823667Z","iopub.status.idle":"2026-06-26T05:11:03.867218Z","shell.execute_reply.started":"2026-06-26T05:11:02.823633Z","shell.execute_reply":"2026-06-26T05:11:03.866570Z"},"id":"9pXYS2RYHsUz","outputId":"468b8d3d-77fb-4250-cef7-704e1b8b4855"},"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_prep.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_with_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T05:11:03.868164Z","iopub.execute_input":"2026-06-26T05:11:03.868687Z","iopub.status.idle":"2026-06-26T05:12:01.998568Z","shell.execute_reply.started":"2026-06-26T05:11:03.868660Z","shell.execute_reply":"2026-06-26T05:12:01.997747Z"},"id":"GLMW80d_HsU0","outputId":"681fe75c-4f2e-43f1-fcb5-c3a0dba1f565"},"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 Dengan Preprocessing (Test Set)')\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.tight_layout()\nplt.savefig('confusion_matrix_with_preprocessing.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T05:12:02.000316Z","iopub.execute_input":"2026-06-26T05:12:02.000605Z","iopub.status.idle":"2026-06-26T05:12:02.419067Z","shell.execute_reply.started":"2026-06-26T05:12:02.000574Z","shell.execute_reply":"2026-06-26T05:12:02.418416Z"},"id":"mONCBX-uHsU0","outputId":"5afb5c7a-dcad-4d9e-eb92-8ed0d4ceda98"},"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 Dengan Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.savefig('gradcam_with_preprocessing.png', dpi=150, bbox_inches='tight')\nplt.show()\nprint('Grad-CAM tersimpan!')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T05:12:02.421673Z","iopub.execute_input":"2026-06-26T05:12:02.422274Z","iopub.status.idle":"2026-06-26T05:12:12.162261Z","shell.execute_reply.started":"2026-06-26T05:12:02.422214Z","shell.execute_reply":"2026-06-26T05:12:12.161205Z"},"id":"SJ532L3vHsU1","outputId":"2078784e-e69a-407b-dad9-e1c3c6ce1adc"},"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 DENGAN PREPROCESSING')\nprint('=' * 60)\nprint(f'Model      : ConvNeXt-V2-Tiny (timm)')\nprint(f'Dataset    : APTOS 2019')\nprint(f'Skenario   : Dengan Preprocessing (Crop + CLAHE)')\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_prep.pth')\nprint('  training_history_with_preprocessing.png')\nprint('  confusion_matrix_with_preprocessing.png')\nprint('  roc_curve_with_preprocessing.png')\nprint('  gradcam_with_preprocessing.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-26T05:12:12.163536Z","iopub.execute_input":"2026-06-26T05:12:12.163845Z","iopub.status.idle":"2026-06-26T05:12:12.171440Z","shell.execute_reply.started":"2026-06-26T05:12:12.163814Z","shell.execute_reply":"2026-06-26T05:12:12.170585Z"},"id":"bzqRzIwaHsU1","outputId":"0f7c551f-e28d-4785-f6bb-53d4afd639b4"},"outputs":[],"execution_count":null}]}