{"metadata":{"kernelspec":{"display_name":"Python 3 (ipykernel)","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.8.4"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431}],"dockerImageVersionId":31401,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"72cd130d","cell_type":"code","source":"import os\nimport kagglehub\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:14.03473Z","iopub.execute_input":"2026-05-27T16:56:14.035331Z","iopub.status.idle":"2026-05-27T16:56:16.433094Z","shell.execute_reply.started":"2026-05-27T16:56:14.035305Z","shell.execute_reply":"2026-05-27T16:56:16.432451Z"}},"outputs":[],"execution_count":null},{"id":"0077f5b3","cell_type":"code","source":"import os, warnings, random\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as models\nfrom torch.optim.lr_scheduler import OneCycleLR, CosineAnnealingWarmRestarts\n\nfrom sklearn.svm import SVC\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    accuracy_score, precision_score, recall_score,\n    f1_score, confusion_matrix, classification_report\n)\nfrom sklearn.utils.class_weight import compute_class_weight\nimport joblib\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED)\ntorch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark     = False\n\nDEVICE      = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nIMG_SIZE    = 224\nBATCH_SIZE  = 32\nMEAN        = [0.485, 0.456, 0.406]\nSTD         = [0.229, 0.224, 0.225]\nBASE        = '/kaggle/input/competitions/aptos2019-blindness-detection'\nCLASS_NAMES = ['No DR','Mild','Moderate','Severe','Proliferative']\nNUM_CLASSES = 5\n\nprint(f'Device : {DEVICE}')\nprint('All imports done.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:16.434337Z","iopub.execute_input":"2026-05-27T16:56:16.434617Z","iopub.status.idle":"2026-05-27T16:56:16.444264Z","shell.execute_reply.started":"2026-05-27T16:56:16.434595Z","shell.execute_reply":"2026-05-27T16:56:16.443527Z"}},"outputs":[],"execution_count":null},{"id":"5c588aa3","cell_type":"code","source":"def apply_clahe(image_path, img_size=224):\n    try:\n        img = cv2.imread(image_path)\n        if img is None: raise ValueError\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (img_size, img_size))\n        lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n        l, a, b = cv2.split(lab)\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))\n        l_c = clahe.apply(l)\n        img_clahe = cv2.cvtColor(cv2.merge([l_c, a, b]), cv2.COLOR_LAB2RGB)\n        return Image.fromarray(img_clahe)\n    except:\n        return Image.open(image_path).convert('RGB').resize((img_size, img_size))\n\ndef find_image(id_code, folders):\n    for folder in folders:\n        for ext in ['.png', '.jpeg', '.jpg']:\n            p = os.path.join(BASE, folder, str(id_code) + ext)\n            if os.path.exists(p): return p\n    return None\n\ntrain_tfm = T.Compose([\n    T.RandomHorizontalFlip(),\n    T.RandomVerticalFlip(),\n    T.RandomRotation(20),\n    T.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05),\n    T.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n])\n\nval_tfm = T.Compose([\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n])\n\nclass FundusDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = apply_clahe(row['filepath'], IMG_SIZE)\n        if self.transform: img = self.transform(img)\n        return img, int(row['label'])\n\nprint('Functions and transforms ready.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:16.445184Z","iopub.execute_input":"2026-05-27T16:56:16.445445Z","iopub.status.idle":"2026-05-27T16:56:16.463014Z","shell.execute_reply.started":"2026-05-27T16:56:16.445418Z","shell.execute_reply":"2026-05-27T16:56:16.462334Z"}},"outputs":[],"execution_count":null},{"id":"7d950b23","cell_type":"code","source":"# ── Data loading: ONLY training images, 80/20 split (no test set leakage) ────\nTRAIN_FOLDER = ['train_images']\n\ndf_train_raw = pd.read_csv(f'{BASE}/train.csv')\ndf_train_raw['label']    = df_train_raw['diagnosis']\ndf_train_raw['filepath'] = df_train_raw['id_code'].apply(\n    lambda x: find_image(x, TRAIN_FOLDER))\ndf_train_raw = df_train_raw[df_train_raw['filepath'].notna()].reset_index(drop=True)\n\n# ── 80 / 20 split (train images only — no test images touched) ────────────────\ndf_tr, df_te = train_test_split(df_train_raw, test_size=0.20,\n                                 stratify=df_train_raw['label'], random_state=SEED)\ndf_tr, df_va = train_test_split(df_tr,        test_size=0.10,\n                                 stratify=df_tr['label'],        random_state=SEED)\n\nprint(f'Total images  : {len(df_train_raw)}')\nprint(f'Train         : {len(df_tr)}')\nprint(f'Val           : {len(df_va)}')\nprint(f'Test (held-out from train only): {len(df_te)}')\nprint('\\nClass distribution in TEST split:')\nfor i, name in enumerate(CLASS_NAMES):\n    c = len(df_te[df_te['label'] == i])\n    print(f'  {name:15s}: {c:4d}  {\"█\" * (c // 5)}')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:16.464745Z","iopub.execute_input":"2026-05-27T16:56:16.465037Z","iopub.status.idle":"2026-05-27T16:56:16.5378Z","shell.execute_reply.started":"2026-05-27T16:56:16.465017Z","shell.execute_reply":"2026-05-27T16:56:16.537197Z"}},"outputs":[],"execution_count":null},{"id":"9ac55ae2","cell_type":"code","source":"# ── Model: EfficientNetV2-S (stronger pretrained backbone, 1280-d features) ──\nclass EfficientNetV2S_Classifier(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n        base = models.efficientnet_v2_s(\n            weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1)\n        self.features   = base.features\n        self.avgpool    = base.avgpool\n        self.classifier = nn.Sequential(\n            nn.Dropout(p=0.4),\n            nn.Linear(1280, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(p=0.3),\n            nn.Linear(512, num_classes),\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.avgpool(x)\n        x = x.flatten(1)\n        return self.classifier(x)\n\n    @torch.no_grad()\n    def get_features(self, x):\n        self.eval()\n        x = self.features(x)\n        x = self.avgpool(x)\n        return x.flatten(1)\n\n\ndef get_param_groups(model):\n    blocks = list(model.features.children())\n    n      = len(blocks)\n    early  = blocks[:n // 3]\n    mid    = blocks[n // 3: 2 * n // 3]\n    late   = blocks[2 * n // 3:]\n    def params(layers):\n        p = []\n        for l in layers: p += list(l.parameters())\n        return p\n    return [\n        {'params': params(early),                       'lr_mult': 0.01},\n        {'params': params(mid),                         'lr_mult': 0.1 },\n        {'params': params(late),                        'lr_mult': 0.5 },\n        {'params': list(model.classifier.parameters()), 'lr_mult': 1.0 },\n    ]\n\n\nmodel = EfficientNetV2S_Classifier(NUM_CLASSES).to(DEVICE)\nfor p in model.parameters():\n    p.requires_grad = True\n\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f'Trainable params : {trainable:,}  (EfficientNetV2-S full fine-tune)')\nprint('Model ready.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:16.538609Z","iopub.execute_input":"2026-05-27T16:56:16.538942Z","iopub.status.idle":"2026-05-27T16:56:17.80314Z","shell.execute_reply.started":"2026-05-27T16:56:16.538906Z","shell.execute_reply":"2026-05-27T16:56:17.802521Z"}},"outputs":[],"execution_count":null},{"id":"8aa8be87","cell_type":"code","source":"EPOCHS      = 30\nBASE_LR     = 5e-4\nMODEL_PATH  = '/kaggle/working/efficientnetv2s_finetuned.pth'\nSVM_PATH    = '/kaggle/working/svm_model.pkl'\nSCALER_PATH = '/kaggle/working/scaler.pkl'\n\n# Stronger augmentation for training\ntrain_tfm_strong = T.Compose([\n    T.RandomHorizontalFlip(),\n    T.RandomVerticalFlip(),\n    T.RandomRotation(30),\n    T.ColorJitter(brightness=0.4, contrast=0.4, saturation=0.3, hue=0.05),\n    T.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.85, 1.15)),\n    T.RandomGrayscale(p=0.05),\n    T.ToTensor(),\n    T.Normalize(MEAN, STD),\n])\n\ntrain_dl = DataLoader(FundusDataset(df_tr, train_tfm_strong),\n                      batch_size=BATCH_SIZE, shuffle=True,\n                      num_workers=2, pin_memory=True)\nval_dl   = DataLoader(FundusDataset(df_va, val_tfm),\n                      batch_size=BATCH_SIZE, shuffle=False,\n                      num_workers=2, pin_memory=True)\n\nif os.path.exists(MODEL_PATH):\n    print('Loading saved fine-tuned EfficientNetV2-S model ...')\n    model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))\nelse:\n    cw = compute_class_weight('balanced', classes=np.arange(NUM_CLASSES),\n                               y=df_tr['label'].values)\n    criterion = nn.CrossEntropyLoss(\n        weight=torch.tensor(cw, dtype=torch.float).to(DEVICE),\n        label_smoothing=0.1          # helps generalisation\n    )\n\n    param_groups = get_param_groups(model)\n    optimizer = optim.AdamW(\n        [{'params': g['params'], 'lr': BASE_LR * g['lr_mult']} for g in param_groups],\n        weight_decay=2e-4\n    )\n    scheduler = OneCycleLR(\n        optimizer,\n        max_lr=[BASE_LR * g['lr_mult'] for g in param_groups],\n        steps_per_epoch=len(train_dl),\n        epochs=EPOCHS,\n        pct_start=0.2,\n        anneal_strategy='cos',\n    )\n\n    best_val_acc = 0.0\n    patience, patience_ctr = 8, 0    # more patience for deeper model\n\n    print(f'Fine-tuning EfficientNetV2-S for up to {EPOCHS} epochs (patience=8)...\\n')\n\n    for epoch in range(1, EPOCHS + 1):\n\n        # ── TRAIN ──────────────────────────────────────────────────────────\n        model.train()\n        tr_loss, tr_correct = 0.0, 0\n        for imgs, lbls in train_dl:\n            imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)\n            optimizer.zero_grad()\n            out  = model(imgs)\n            loss = criterion(out, lbls)\n            loss.backward()\n            nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            optimizer.step()\n            scheduler.step()\n            tr_loss    += loss.item() * imgs.size(0)\n            tr_correct += (out.argmax(1) == lbls).sum().item()\n\n        # ── VALIDATE ───────────────────────────────────────────────────────\n        model.eval()\n        val_correct = 0\n        with torch.no_grad():\n            for imgs, lbls in val_dl:\n                imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)\n                val_correct += (model(imgs).argmax(1) == lbls).sum().item()\n\n        t_acc = tr_correct  / len(df_tr) * 100\n        v_acc = val_correct / len(df_va) * 100\n        print(f'Epoch {epoch:2d}/{EPOCHS} | Train: {t_acc:.2f}% | Val: {v_acc:.2f}%', end='')\n\n        if v_acc > best_val_acc:\n            best_val_acc = v_acc\n            patience_ctr = 0\n            torch.save(model.state_dict(), MODEL_PATH)\n            print(f'  ✓ saved (best val {v_acc:.2f}%)')\n        else:\n            patience_ctr += 1\n            print(f'  (patience {patience_ctr}/{patience})')\n            if patience_ctr >= patience:\n                print('Early stopping.')\n                break\n\n    model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))\n    print(f'\\nBest Val Accuracy : {best_val_acc:.2f}%')\n\nprint('Done.')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T16:56:17.804192Z","iopub.execute_input":"2026-05-27T16:56:17.804502Z","iopub.status.idle":"2026-05-27T17:54:22.645543Z","shell.execute_reply.started":"2026-05-27T16:56:17.804454Z","shell.execute_reply":"2026-05-27T17:54:22.644516Z"}},"outputs":[],"execution_count":null},{"id":"e204583f","cell_type":"code","source":"@torch.no_grad()\ndef extract_features(mdl, loader):\n    mdl.eval()\n    feats, labels = [], []\n    for imgs, lbls in loader:\n        feats.append(mdl.get_features(imgs.to(DEVICE)).cpu().numpy())\n        labels.append(lbls.numpy())\n    return np.vstack(feats), np.concatenate(labels)\n\ntrain_feat_dl = DataLoader(FundusDataset(df_tr, val_tfm),\n                            batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\nval_feat_dl   = DataLoader(FundusDataset(df_va, val_tfm),\n                            batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\ntest_dl       = DataLoader(FundusDataset(df_te, val_tfm),\n                            batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nprint('Extracting features ...')\nX_train, y_train = extract_features(model, train_feat_dl)\nX_val,   y_val   = extract_features(model, val_feat_dl)\nX_test,  y_test  = extract_features(model, test_dl)\nprint(f'Shape: {X_train.shape} | Train {len(y_train)} | Val {len(y_val)} | Test {len(y_test)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T17:54:22.647118Z","iopub.execute_input":"2026-05-27T17:54:22.647454Z","iopub.status.idle":"2026-05-27T17:57:26.245045Z","shell.execute_reply.started":"2026-05-27T17:54:22.647413Z","shell.execute_reply":"2026-05-27T17:57:26.244261Z"}},"outputs":[],"execution_count":null},{"id":"de0d4ec9","cell_type":"code","source":"if os.path.exists(SVM_PATH) and os.path.exists(SCALER_PATH):\n    print('Loading saved scaler + SVM ...')\n    scaler = joblib.load(SCALER_PATH)\n    svm    = joblib.load(SVM_PATH)\n    X_train_sc = scaler.transform(X_train)\n    X_val_sc   = scaler.transform(X_val)\n    X_test_sc  = scaler.transform(X_test)\nelse:\n    scaler     = StandardScaler()\n    X_train_sc = scaler.fit_transform(X_train)\n    X_val_sc   = scaler.transform(X_val)\n    X_test_sc  = scaler.transform(X_test)\n\n    cw      = compute_class_weight('balanced', classes=np.arange(NUM_CLASSES), y=y_train)\n    cw_dict = {i: w for i, w in enumerate(cw)}\n\n    print('Class weights:')\n    for i, (n, w) in enumerate(zip(CLASS_NAMES, cw)):\n        print(f'  {n:15s}: {w:.4f}')\n\n    print('\\nFitting SVM (C=10, gamma=scale) ...')\n    svm = SVC(kernel='rbf', C=10, gamma='scale',\n              probability=True, class_weight=cw_dict, random_state=SEED)\n    svm.fit(X_train_sc, y_train)\n\n    joblib.dump(svm,    SVM_PATH)\n    joblib.dump(scaler, SCALER_PATH)\n    print('Saved scaler + SVM.')\n\nprint(f'Val accuracy : {accuracy_score(y_val, svm.predict(X_val_sc))*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T17:57:26.24626Z","iopub.execute_input":"2026-05-27T17:57:26.246579Z","iopub.status.idle":"2026-05-27T17:57:29.83178Z","shell.execute_reply.started":"2026-05-27T17:57:26.24655Z","shell.execute_reply":"2026-05-27T17:57:29.831066Z"}},"outputs":[],"execution_count":null},{"id":"26e6920a","cell_type":"code","source":"y_pred = svm.predict(X_test_sc)\n\nacc  = accuracy_score(y_test, y_pred)\nprec = precision_score(y_test, y_pred, average='weighted', zero_division=0)\nrec  = recall_score(y_test,   y_pred, average='weighted', zero_division=0)\nf1   = f1_score(y_test,       y_pred, average='weighted', zero_division=0)\n\nprint('=' * 62)\nprint('   EfficientNetV2-S (Full Fine-Tune) + SVM  —  5-CLASS DR')\nprint('=' * 62)\nprint(f'  Accuracy  : {acc*100:.2f}%')\nprint(f'  Precision : {prec:.4f}')\nprint(f'  Recall    : {rec:.4f}')\nprint(f'  F1-Score  : {f1:.4f}')\nprint('=' * 62)\nprint(classification_report(y_test, y_pred,\n                             target_names=CLASS_NAMES, zero_division=0))\ndelta = acc * 100 - 92.60\nprint(f'Wiratama et al.  : 92.60%')\nprint(f'Our Model        : {acc*100:.2f}%')\nprint(f'Improvement      : {delta:+.2f}%')\nif acc * 100 > 93:\n    print('TARGET ACHIEVED — Above 93%! ✅')\nelif acc * 100 > 90:\n    print('Above 90% — publishable! ✅')\nelif acc * 100 > 92.60:\n    print('Beats Wiratama et al.! ✅')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T17:57:29.832715Z","iopub.execute_input":"2026-05-27T17:57:29.833Z","iopub.status.idle":"2026-05-27T17:57:30.188943Z","shell.execute_reply.started":"2026-05-27T17:57:29.83297Z","shell.execute_reply":"2026-05-27T17:57:30.188285Z"}},"outputs":[],"execution_count":null},{"id":"adbc2ee2","cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(16, 5))\nfig.suptitle('EfficientNetV2-S (Full Fine-Tune) + SVM — 5-Class DR',\n             fontsize=13, fontweight='bold')\n\ncm = confusion_matrix(y_test, y_pred)\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0],\n            xticklabels=CLASS_NAMES, yticklabels=CLASS_NAMES)\naxes[0].set_title('Confusion Matrix')\naxes[0].set_ylabel('True'); axes[0].set_xlabel('Predicted')\n\nper_class = [cm[i][i] / max(cm[i].sum(), 1) * 100 for i in range(5)]\ncolors    = ['#2ecc71','#3498db','#f39c12','#e74c3c','#9b59b6']\nbars = axes[1].bar(CLASS_NAMES, per_class, color=colors, width=0.6)\naxes[1].set_ylim(0, 115)\naxes[1].axhline(92.60, color='red',   linestyle='--', lw=2, label='Wiratama 92.60%')\naxes[1].axhline(93.00, color='green', linestyle='--', lw=2, label='Target 93%')\naxes[1].set_title('Per-Class Accuracy (%)'); axes[1].legend(fontsize=8)\nfor b, v in zip(bars, per_class):\n    axes[1].text(b.get_x() + b.get_width()/2, b.get_height() + 1,\n                 f'{v:.1f}%', ha='center', fontsize=9, fontweight='bold')\n\nnames = ['Accuracy','Precision','Recall','F1']\nvals  = [acc, prec, rec, f1]\nbars2 = axes[2].bar(names, vals,\n                    color=['#4C72B0','#DD8452','#55A868','#C44E52'], width=0.5)\naxes[2].set_ylim(0, 1.15); axes[2].set_title('Overall Metrics')\nfor b, v in zip(bars2, vals):\n    axes[2].text(b.get_x() + b.get_width()/2, b.get_height() + 0.02,\n                 f'{v:.4f}', ha='center', fontsize=10)\n\nplt.tight_layout()\nplt.savefig('/kaggle/working/results.png', dpi=150, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-27T17:57:30.191265Z","iopub.execute_input":"2026-05-27T17:57:30.191957Z","iopub.status.idle":"2026-05-27T17:57:31.165825Z","shell.execute_reply.started":"2026-05-27T17:57:30.191922Z","shell.execute_reply":"2026-05-27T17:57:31.16518Z"}},"outputs":[],"execution_count":null}]}