{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774}],"dockerImageVersionId":31401,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"colab":{"name":"DR_TanpaPre-Processing","provenance":[]}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# IMPORTANT: RUN THIS CELL IN ORDER TO IMPORT YOUR KAGGLE DATA SOURCES,\n# THEN FEEL FREE TO DELETE THIS CELL.\n# NOTE: THIS NOTEBOOK ENVIRONMENT DIFFERS FROM KAGGLE'S PYTHON\n# ENVIRONMENT SO THERE MAY BE MISSING LIBRARIES USED BY YOUR\n# NOTEBOOK.\n\naptos2019_blindness_detection_path = kagglehub.competition_download('aptos2019-blindness-detection')\n\nprint('Data source import complete.')\n","metadata":{"id":"QNaOFo1IHsUo","trusted":true,"execution":{"iopub.status.busy":"2026-06-23T15:55:33.413636Z","iopub.execute_input":"2026-06-23T15:55:33.414150Z","iopub.status.idle":"2026-06-23T15:55:34.200790Z","shell.execute_reply.started":"2026-06-23T15:55:33.414116Z","shell.execute_reply":"2026-06-23T15:55:34.199808Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MaxViT — APTOS 2019 Diabetic Retinopathy Classification\n## Tanpa Preprocessing (Raw Images)\n\n**Model:** MaxViT (Multi-Axis Vision Transformer)  \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-23T15:55:34.201834Z","iopub.execute_input":"2026-06-23T15:55:34.202135Z","iopub.status.idle":"2026-06-23T15:56:01.052462Z","shell.execute_reply.started":"2026-06-23T15:55:34.202098Z","shell.execute_reply":"2026-06-23T15:56:01.051495Z"},"id":"HRuO9vRRHsUt","outputId":"f13cd006-5380-47e7-f6ff-db010cf17942"},"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-23T15:56:01.054468Z","iopub.execute_input":"2026-06-23T15:56:01.054975Z","iopub.status.idle":"2026-06-23T15:56:01.099804Z","shell.execute_reply.started":"2026-06-23T15:56:01.054946Z","shell.execute_reply":"2026-06-23T15:56:01.099144Z"},"id":"0iptrgiXHsUu","outputId":"c18aee14-29ce-4854-a160-a7081b413de0"},"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-23T15:56:01.100629Z","iopub.execute_input":"2026-06-23T15:56:01.100936Z","iopub.status.idle":"2026-06-23T15:56:03.753429Z","shell.execute_reply.started":"2026-06-23T15:56:01.100913Z","shell.execute_reply":"2026-06-23T15:56:03.752451Z"},"id":"jM2DMBiiHsUv","outputId":"11646f7d-b970-486d-a0ca-2d1d322dde44"},"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-23T15:56:03.754412Z","iopub.execute_input":"2026-06-23T15:56:03.754697Z","iopub.status.idle":"2026-06-23T15:56:03.772990Z","shell.execute_reply.started":"2026-06-23T15:56:03.754674Z","shell.execute_reply":"2026-06-23T15:56:03.772370Z"},"id":"4cL9SzF-HsUw","outputId":"d006c6e9-a956-4fde-e582-9959eeed3275"},"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-23T15:56:03.773825Z","iopub.execute_input":"2026-06-23T15:56:03.774084Z","iopub.status.idle":"2026-06-23T15:56:03.786449Z","shell.execute_reply.started":"2026-06-23T15:56:03.774065Z","shell.execute_reply":"2026-06-23T15:56:03.785613Z"},"id":"leRrutxDHsUx","outputId":"aa1dfbb4-cf0c-45f0-a5b1-1674d60ffff0"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Build MaxViT Model","metadata":{"id":"j94_vBaeHsUy"}},{"cell_type":"code","source":"NUM_CLASSES = 5\n\nmodel = timm.create_model(\n    'maxvit_tiny_tf_224.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-23T15:56:03.787273Z","iopub.execute_input":"2026-06-23T15:56:03.787494Z","iopub.status.idle":"2026-06-23T15:56:09.844717Z","shell.execute_reply.started":"2026-06-23T15:56:03.787475Z","shell.execute_reply":"2026-06-23T15:56:09.843944Z"},"id":"AXGJiI3mHsUy","outputId":"0b6b0c3a-0b05-48f1-f656-0d57d33b5e48"},"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-23T15:56:09.845712Z","iopub.execute_input":"2026-06-23T15:56:09.846070Z","iopub.status.idle":"2026-06-23T15:56:09.876218Z","shell.execute_reply.started":"2026-06-23T15:56:09.846044Z","shell.execute_reply":"2026-06-23T15:56:09.875353Z"},"id":"srzoa67lHsUy","outputId":"b0b75232-5f82-4e63-968c-00c829b7d9c3"},"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\nfrom tqdm import tqdm\n\ndef train_one_epoch(model, loader, criterion, optimizer):\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        outputs = model(imgs)\n\n        loss = criterion(outputs, labels)\n\n        loss.backward()\n        optimizer.step()\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\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            outputs = model(imgs)\n\n            loss = criterion(outputs, labels)\n\n            total_loss += loss.item()\n\n            preds = outputs.argmax(dim=1)\n\n            all_preds.extend(preds.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, all_labels, all_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-23T15:56:09.878442Z","iopub.execute_input":"2026-06-23T15:56:09.878707Z","iopub.status.idle":"2026-06-23T15:56:10.296786Z","shell.execute_reply.started":"2026-06-23T15:56:09.878687Z","shell.execute_reply":"2026-06-23T15:56:10.296005Z"},"id":"Lxy-Z7s_HsUy"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==================================================\n# Training Loop + Early Stopping\n# ==================================================\n\nPATIENCE = 7\n\nhistory = {\n    'train_loss': [],\n    'val_loss': [],\n    'train_acc': [],\n    'val_acc': [],\n    'train_qwk': [],\n    'val_qwk': []\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    )\n\n    # -------------------------\n    # Validation\n    # -------------------------\n    vl_loss, vl_acc, vl_qwk, val_labels, val_preds = 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\n    history['train_acc'].append(tr_acc)\n    history['val_acc'].append(vl_acc)\n\n    history['train_qwk'].append(tr_qwk)\n    history['val_qwk'].append(vl_qwk)\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    )\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_maxvit_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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-23T15:56:10.297771Z","iopub.execute_input":"2026-06-23T15:56:10.298023Z","iopub.status.idle":"2026-06-23T16:31:52.148009Z","shell.execute_reply.started":"2026-06-23T15:56:10.298001Z","shell.execute_reply":"2026-06-23T16:31:52.147178Z"},"id":"V2mgJDZCHsUz","outputId":"90bded7a-e68f-4573-86c9-ec7c68151559"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Plot Training History","metadata":{"id":"rj3Bn4pFHsUz"}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfor ax, metric, title in zip(axes,\n    [('train_loss','val_loss'), ('train_acc','val_acc'), ('train_qwk','val_qwk')],\n    ['Loss', 'Accuracy', 'QWK']):\n    ax.plot(history[metric[0]], label=f'Train {title}', color='blue')\n    ax.plot(history[metric[1]], label=f'Val {title}',   color='orange')\n    ax.set_title(f'{title} per Epoch')\n    ax.set_xlabel('Epoch')\n    ax.legend()\nplt.suptitle('MaxViT Training History — Tanpa Preprocessing', fontsize=14)\nplt.tight_layout()\nplt.savefig('training_history_no_preprocessing.png', dpi=150)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-23T16:31:52.149441Z","iopub.execute_input":"2026-06-23T16:31:52.150307Z","iopub.status.idle":"2026-06-23T16:31:52.933258Z","shell.execute_reply.started":"2026-06-23T16:31:52.150266Z","shell.execute_reply":"2026-06-23T16:31:52.932631Z"},"id":"9pXYS2RYHsUz","outputId":"7fa225f2-895b-4b78-b4dc-65b925b8acdc"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Evaluasi Final (Test Set)\n","metadata":{"id":"leP7UeaiHsU0"}},{"cell_type":"code","source":"model.load_state_dict(\n    torch.load('best_maxvit_aptos2019.pth', map_location=DEVICE)\n)\n\nmodel.eval()\n\n# Evaluasi akhir memakai TEST SET (bukan val_loader) -- ini set\n# yang sama sekali belum pernah dilihat model maupun proses\n# early stopping, jadi mencerminkan performa generalisasi yang\n# lebih jujur dibanding mengevaluasi ulang di val_loader.\n_, final_acc, final_qwk, true_labels, pred_labels = validate(\n    model,\n    test_loader,\n    criterion\n)\n\nprint('=' * 50)\nprint('HASIL EVALUASI — MaxViT (Test Set)')\nprint('=' * 50)\nprint(f'Accuracy : {final_acc:.4f} ({final_acc*100:.2f}%)')\nprint(f'QWK      : {final_qwk:.4f}')\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-23T16:31:52.934210Z","iopub.execute_input":"2026-06-23T16:31:52.934490Z","iopub.status.idle":"2026-06-23T16:32:20.694817Z","shell.execute_reply.started":"2026-06-23T16:31:52.934468Z","shell.execute_reply":"2026-06-23T16:32:20.693943Z"},"id":"GLMW80d_HsU0","outputId":"7249ba3c-0934-4404-8ae1-49fa2c55ac4a"},"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 — MaxViT 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-23T16:32:20.696162Z","iopub.execute_input":"2026-06-23T16:32:20.696596Z","iopub.status.idle":"2026-06-23T16:32:21.099317Z","shell.execute_reply.started":"2026-06-23T16:32:20.696527Z","shell.execute_reply":"2026-06-23T16:32:21.098631Z"},"id":"mONCBX-uHsU0","outputId":"b997a4ca-ddd5-4b55-ce92-6de86ebc2b9c"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. XAI — Grad-CAM Visualization","metadata":{"id":"jfEIYOpsHsU0"}},{"cell_type":"code","source":"# Target layer Grad-CAM: conv block terakhir di stage terakhir.\n# (Catatan: attn_grid/attn_block TIDAK dipakai sebagai target layer\n# karena outputnya channel-last (B,H,W,C) -- pytorch_grad_cam\n# mengasumsikan channel-first (B,C,H,W), sehingga tanpa\n# reshape_transform khusus, axis H akan tertukar dengan axis\n# channel dan menghasilkan heatmap yang salah secara diam-diam.)\ntarget_layers = [model.stages[-1].blocks[-1].conv]\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 — MaxViT 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-23T16:32:21.100267Z","iopub.execute_input":"2026-06-23T16:32:21.100618Z","iopub.status.idle":"2026-06-23T16:32:27.661932Z","shell.execute_reply.started":"2026-06-23T16:32:21.100586Z","shell.execute_reply":"2026-06-23T16:32:27.660937Z"},"id":"SJ532L3vHsU1","outputId":"0f1f7c42-79c4-4441-c965-f5cfc2466c7f"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Ringkasan Hasil","metadata":{"id":"PA-C6WktHsU1"}},{"cell_type":"code","source":"print('=' * 60)\nprint('RINGKASAN — MaxViT TANPA PREPROCESSING')\nprint('=' * 60)\nprint(f'Model      : MaxViT-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('=' * 60)\nprint('File output:')\nprint('  best_maxvit_aptos2019.pth')\nprint('  training_history_no_preprocessing.png')\nprint('  confusion_matrix_no_preprocessing.png')\nprint('  gradcam_no_preprocessing.png')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-23T16:32:27.663445Z","iopub.execute_input":"2026-06-23T16:32:27.663835Z","iopub.status.idle":"2026-06-23T16:32:27.674967Z","shell.execute_reply.started":"2026-06-23T16:32:27.663798Z","shell.execute_reply":"2026-06-23T16:32:27.674093Z"},"id":"bzqRzIwaHsU1","outputId":"1926e133-6d04-4a91-f6c5-869e26632839"},"outputs":[],"execution_count":null}]}