{"metadata":{"kernelspec":{"display_name":"Python 3","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.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =============================================\n# Kaggle Setup - APTOS 2019 Blindness Detection\n# =============================================\n# Dataset: Add \"aptos2019-blindness-detection\" as a Kaggle dataset\n# Go to: Add Data -> Competition Data -> aptos2019-blindness-detection\n# The data will be at: /kaggle/input/aptos2019-blindness-detection/\n\nimport os\n\nDATA_DIR = '/kaggle/input/competitions/aptos2019-blindness-detection'\nSAVE_DIR = '/kaggle/working/'\n\nprint(f\"Data directory: {DATA_DIR}\")\nprint(f\"Output directory: {SAVE_DIR}\")\nprint(f\"Data files: {os.listdir(DATA_DIR)}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:28:41.325130Z","iopub.execute_input":"2026-04-07T21:28:41.325419Z","iopub.status.idle":"2026-04-07T21:28:41.335468Z","shell.execute_reply.started":"2026-04-07T21:28:41.325393Z","shell.execute_reply":"2026-04-07T21:28:41.334340Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    print(dirname)","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:28:41.336869Z","iopub.execute_input":"2026-04-07T21:28:41.337190Z","iopub.status.idle":"2026-04-07T21:28:49.680317Z","shell.execute_reply.started":"2026-04-07T21:28:41.337165Z","shell.execute_reply":"2026-04-07T21:28:49.679490Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q timm\n\nimport os\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nimport random\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report, cohen_kappa_score, f1_score, precision_score, recall_score\nfrom sklearn.utils.class_weight import compute_class_weight\n\nimport timm\nfrom tqdm import tqdm\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\n# Set seeds for reproducibility\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    torch.backends.cudnn.benchmark = False\n\nset_seed(42)","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:28:49.681193Z","iopub.execute_input":"2026-04-07T21:28:49.681426Z","iopub.status.idle":"2026-04-07T21:29:10.114352Z","shell.execute_reply.started":"2026-04-07T21:28:49.681403Z","shell.execute_reply":"2026-04-07T21:29:10.113466Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\nprint(f\"Total samples: {len(df)}\")\nprint(f\"Class distribution:\\n{df['diagnosis'].value_counts().sort_index()}\")\n\ntrain_df, temp_df = train_test_split(\n    df, test_size=0.2, stratify=df[\"diagnosis\"], random_state=42\n)\n\nval_df, test_df = train_test_split(\n    temp_df, test_size=0.5, stratify=temp_df[\"diagnosis\"], random_state=42\n)\n\nprint(f\"\\nTrain: {len(train_df)}, Val: {len(val_df)}, Test: {len(test_df)}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.116446Z","iopub.execute_input":"2026-04-07T21:29:10.116998Z","iopub.status.idle":"2026-04-07T21:29:10.163859Z","shell.execute_reply.started":"2026-04-07T21:29:10.116966Z","shell.execute_reply":"2026-04-07T21:29:10.163178Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_clahe(pil_img):\n    img = np.array(pil_img)\n    img = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)\n\n    l, a, b = cv2.split(img)\n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))\n    cl = clahe.apply(l)\n\n    merged = cv2.merge((cl,a,b))\n    img = cv2.cvtColor(merged, cv2.COLOR_LAB2RGB)\n\n    return Image.fromarray(img)","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.164853Z","iopub.execute_input":"2026-04-07T21:29:10.165197Z","iopub.status.idle":"2026-04-07T21:29:10.170739Z","shell.execute_reply.started":"2026-04-07T21:29:10.165168Z","shell.execute_reply":"2026-04-07T21:29:10.169987Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ImageNet normalization - CRITICAL for pretrained models\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\nIMG_SIZE = 456\n\ntrain_transform = transforms.Compose([\n    transforms.Lambda(apply_clahe),\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomVerticalFlip(p=0.5),\n    transforms.RandomRotation(10),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\nval_transform = transforms.Compose([\n    transforms.Lambda(apply_clahe),\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\nprint(\"Transforms configured with ImageNet normalization + reduced augmentation\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.171794Z","iopub.execute_input":"2026-04-07T21:29:10.172484Z","iopub.status.idle":"2026-04-07T21:29:10.187023Z","shell.execute_reply.started":"2026-04-07T21:29:10.172457Z","shell.execute_reply":"2026-04-07T21:29:10.186380Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class APTOSDataset(Dataset):\n    def __init__(self, df, data_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.data_dir = data_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_name = self.df.iloc[idx][\"id_code\"]\n        label = self.df.iloc[idx][\"diagnosis\"]\n\n        path = os.path.join(self.data_dir, \"train_images\", f\"{img_name}.png\")\n        image = Image.open(path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n\ndef mixup_data(x, y, alpha=0.4):\n    \"\"\"Mixup augmentation: blends pairs of images and their labels.\"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.0\n\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n\n    mixed_x = lam * x + (1 - lam) * x[index]\n    y_a, y_b = y, y[index]\n    return mixed_x, y_a, y_b, lam\n\n\ndef mixup_criterion(criterion, pred, y_a, y_b, lam):\n    \"\"\"Mixup loss: weighted combination of losses for both label sets.\"\"\"\n    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.188099Z","iopub.execute_input":"2026-04-07T21:29:10.188590Z","iopub.status.idle":"2026-04-07T21:29:10.200572Z","shell.execute_reply.started":"2026-04-07T21:29:10.188565Z","shell.execute_reply":"2026-04-07T21:29:10.199910Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 8\nNUM_WORKERS = 2\n\ntrain_loader = DataLoader(\n    APTOSDataset(train_df, DATA_DIR, train_transform),\n    batch_size=BATCH_SIZE, shuffle=True,\n    num_workers=NUM_WORKERS, pin_memory=True, drop_last=True\n)\nval_loader = DataLoader(\n    APTOSDataset(val_df, DATA_DIR, val_transform),\n    batch_size=BATCH_SIZE,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\ntest_loader = DataLoader(\n    APTOSDataset(test_df, DATA_DIR, val_transform),\n    batch_size=BATCH_SIZE,\n    num_workers=NUM_WORKERS, pin_memory=True\n)\n\nprint(f\"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}, Test batches: {len(test_loader)}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.201526Z","iopub.execute_input":"2026-04-07T21:29:10.201925Z","iopub.status.idle":"2026-04-07T21:29:10.222441Z","shell.execute_reply.started":"2026-04-07T21:29:10.201871Z","shell.execute_reply":"2026-04-07T21:29:10.221759Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================\n# EfficientNet-B5 with pretrained ImageNet weights\n# =============================================\n\nmodel = timm.create_model(\"efficientnet_b5\", pretrained=True, num_classes=5,\n                          drop_rate=0.4, drop_path_rate=0.2).to(device)\n\n# Replace classifier with a more regularized head\nin_features = model.classifier.in_features\nmodel.classifier = nn.Sequential(\n    nn.Dropout(p=0.5),\n    nn.Linear(in_features, 256),\n    nn.ReLU(inplace=True),\n    nn.BatchNorm1d(256),\n    nn.Dropout(p=0.3),\n    nn.Linear(256, 5)\n).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 params: {total_params:,}\")\nprint(f\"Trainable params: {trainable_params:,}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:10.223473Z","iopub.execute_input":"2026-04-07T21:29:10.223750Z","iopub.status.idle":"2026-04-07T21:29:13.744289Z","shell.execute_reply.started":"2026-04-07T21:29:10.223725Z","shell.execute_reply":"2026-04-07T21:29:13.743423Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_weights = compute_class_weight(\n    \"balanced\",\n    classes=np.unique(train_df[\"diagnosis\"]),\n    y=train_df[\"diagnosis\"]\n)\n\nclass_weights = torch.tensor(class_weights, dtype=torch.float).to(device)\nprint(f\"Class weights: {class_weights}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:13.746654Z","iopub.execute_input":"2026-04-07T21:29:13.746951Z","iopub.status.idle":"2026-04-07T21:29:14.035719Z","shell.execute_reply.started":"2026-04-07T21:29:13.746917Z","shell.execute_reply":"2026-04-07T21:29:14.035035Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def freeze_backbone(model):\n    \"\"\"Freeze all layers except the classifier head.\"\"\"\n    for name, param in model.named_parameters():\n        if 'classifier' not in name:\n            param.requires_grad = False\n        else:\n            param.requires_grad = True\n\ndef unfreeze_all(model):\n    \"\"\"Unfreeze all layers for fine-tuning.\"\"\"\n    for param in model.parameters():\n        param.requires_grad = True\n\nclass FocalLoss(nn.Module):\n    \"\"\"Focal Loss for handling class imbalance. Penalizes easy examples less, focuses on hard ones.\"\"\"\n    def __init__(self, weight=None, gamma=2.0, alpha=0.25, reduction='mean'):\n        super(FocalLoss, self).__init__()\n        self.weight = weight\n        self.gamma = gamma  # Focusing parameter\n        self.alpha = alpha  # Class weight factor\n        self.reduction = reduction\n        self.ce = nn.CrossEntropyLoss(reduction='none', weight=weight)\n    \n    def forward(self, inputs, targets):\n        ce_loss = self.ce(inputs, targets)\n        pt = torch.exp(-ce_loss)  # Probability of ground truth\n        focal_loss = self.alpha * (1 - pt) ** self.gamma * ce_loss\n        \n        if self.reduction == 'mean':\n            return focal_loss.mean()\n        elif self.reduction == 'sum':\n            return focal_loss.sum()\n        else:\n            return focal_loss\n\nprint(\"Helper functions defined: freeze_backbone, unfreeze_all, FocalLoss\")","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:14.036618Z","iopub.execute_input":"2026-04-07T21:29:14.036939Z","iopub.status.idle":"2026-04-07T21:29:14.044469Z","shell.execute_reply.started":"2026-04-07T21:29:14.036887Z","shell.execute_reply":"2026-04-07T21:29:14.043593Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_model(model, train_loader, val_loader, epochs=30, use_mixup=True, mixup_alpha=0.4):\n\n    # 🔥 FOCAL LOSS: Targets hard examples, better for imbalanced classes (Severe only 19 samples!)\n    criterion = FocalLoss(weight=class_weights, gamma=2.0, alpha=0.25)\n\n    # ==============================\n    # STAGE 1: Warmup (classifier only)\n    # ==============================\n    freeze_backbone(model)\n    warmup_params = [p for p in model.parameters() if p.requires_grad]\n    print(f\"Stage 1 - Trainable params (classifier only): {sum(p.numel() for p in warmup_params):,}\")\n\n    warmup_optimizer = torch.optim.Adam(warmup_params, lr=1e-3, weight_decay=1e-4)\n\n    print(\"\\n\" + \"=\"*60)\n    print(\"STAGE 1: Warmup - Training classifier head (3 epochs)\")\n    print(\"=\"*60)\n\n    for epoch in range(3):\n        model.train()\n        loop = tqdm(train_loader, desc=f\"Warmup Epoch {epoch+1}/3\")\n        for images, labels in loop:\n            images, labels = images.to(device), labels.to(device)\n            warmup_optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(warmup_params, max_norm=1.0)\n            warmup_optimizer.step()\n            loop.set_postfix(loss=loss.item())\n\n    # ==============================\n    # STAGE 2: Full Fine-tuning\n    # ==============================\n    unfreeze_all(model)\n\n    all_params = list(model.parameters())\n    print(f\"\\nStage 2 - All params unfrozen: {sum(p.numel() for p in all_params):,}\")\n\n    # Differential learning rates: backbone lower, head higher\n    backbone_params = [p for n, p in model.named_parameters() if 'classifier' not in n]\n    head_params = [p for n, p in model.named_parameters() if 'classifier' in n]\n\n    optimizer = torch.optim.Adam([\n        {'params': backbone_params, 'lr': 1e-5},\n        {'params': head_params, 'lr': 1e-4}\n    ], weight_decay=1e-4)\n\n    # Simpler cosine annealing for stable convergence\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n        optimizer, T_max=epochs, eta_min=1e-7\n    )\n\n    train_losses, val_losses = [], []\n    train_accs, val_accs = [], []\n\n    best_val_qwk = 0\n    patience = 8  # INCREASED from 5 to allow longer training with better convergence\n    patience_counter = 0\n\n    save_path = os.path.join(SAVE_DIR, 'eff_best.pth')\n\n    print(\"\\n\" + \"=\"*60)\n    print(f\"STAGE 2: Full Fine-tuning ({epochs} epochs, patience={patience})\")\n    print(\"=\"*60 + \"\\n\")\n\n    for epoch in range(epochs):\n\n        # ===== TRAIN =====\n        model.train()\n        running_loss, correct, total = 0, 0, 0\n\n        loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs}\")\n\n        for images, labels in loop:\n            images, labels = images.to(device), labels.to(device)\n\n            optimizer.zero_grad()\n\n            # Apply Mixup with 50% probability during training\n            if use_mixup and random.random() < 0.5:\n                mixed_images, y_a, y_b, lam = mixup_data(images, labels, mixup_alpha)\n                outputs = model(mixed_images)\n                loss = mixup_criterion(criterion, outputs, y_a, y_b, lam)\n                # For accuracy tracking, use original labels\n                _, preds_batch = torch.max(outputs, 1)\n                correct += (lam * (preds_batch == y_a).sum().item() +\n                           (1 - lam) * (preds_batch == y_b).sum().item())\n            else:\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n                _, preds_batch = torch.max(outputs, 1)\n                correct += (preds_batch == labels).sum().item()\n\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            optimizer.step()\n\n            running_loss += loss.item()\n            total += labels.size(0)\n\n            loop.set_postfix(loss=loss.item(), acc=correct/total)\n\n        train_loss = running_loss / len(train_loader)\n        train_acc = correct / total\n\n        # ===== VALIDATION =====\n        model.eval()\n        val_loss_sum, correct, total = 0, 0, 0\n        all_preds, all_labels = [], []\n\n        with torch.no_grad():\n            for images, labels in val_loader:\n                images, labels = images.to(device), labels.to(device)\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n\n                val_loss_sum += loss.item()\n                _, preds_batch = torch.max(outputs, 1)\n                correct += (preds_batch == labels).sum().item()\n                total += labels.size(0)\n\n                all_preds.extend(preds_batch.cpu().numpy())\n                all_labels.extend(labels.cpu().numpy())\n\n        val_loss = val_loss_sum / len(val_loader)\n        val_acc = correct / total\n        val_qwk = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\n\n        scheduler.step()\n\n        train_losses.append(train_loss)\n        val_losses.append(val_loss)\n        train_accs.append(train_acc)\n        val_accs.append(val_acc)\n\n        # Print epoch results\n        current_lr = optimizer.param_groups[0]['lr']\n        print(f\"\\nEpoch {epoch+1}/{epochs} | LR: {current_lr:.2e}\")\n        print(f\"  Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f}\")\n        print(f\"  Val   Loss: {val_loss:.4f} | Val   Acc: {val_acc:.4f} | Val QWK: {val_qwk:.4f}\")\n        print(f\"  Gap (Train-Val Acc): {(train_acc - val_acc):.4f}\")\n        print(\"-\"*60)\n\n        # Save best model based on val QWK\n        if val_qwk > best_val_qwk:\n            best_val_qwk = val_qwk\n            patience_counter = 0\n            torch.save({\n                'epoch': epoch + 1,\n                'model_state_dict': model.state_dict(),\n                'optimizer_state_dict': optimizer.state_dict(),\n                'val_acc': val_acc,\n                'val_qwk': val_qwk,\n                'train_acc': train_acc,\n            }, save_path)\n            print(f\"  >>> Best model saved! Val QWK: {val_qwk:.4f}, Val Acc: {val_acc:.4f}\")\n        else:\n            patience_counter += 1\n            print(f\"  No improvement. Patience: {patience_counter}/{patience}\")\n\n        # Early stopping\n        if patience_counter >= patience:\n            print(f\"\\nEarly stopping triggered at epoch {epoch+1}!\")\n            break\n\n    print(f\"\\nTraining complete. Best Val QWK: {best_val_qwk:.4f}\")\n    print(f\"Best model saved to: {save_path}\")\n\n    return train_losses, val_losses, train_accs, val_accs","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:14.045549Z","iopub.execute_input":"2026-04-07T21:29:14.045965Z","iopub.status.idle":"2026-04-07T21:29:14.068276Z","shell.execute_reply.started":"2026-04-07T21:29:14.045928Z","shell.execute_reply":"2026-04-07T21:29:14.067486Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"trainable = [name for name, p in model.named_parameters() if p.requires_grad]\nprint(f\"Total trainable layers: {len(trainable)}\")\nprint(\"Last 10 trainable:\", trainable[-10:])","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:14.069462Z","iopub.execute_input":"2026-04-07T21:29:14.069807Z","iopub.status.idle":"2026-04-07T21:29:14.087609Z","shell.execute_reply.started":"2026-04-07T21:29:14.069775Z","shell.execute_reply":"2026-04-07T21:29:14.086975Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = train_model(model, train_loader, val_loader, epochs=30, use_mixup=True, mixup_alpha=0.4)","metadata":{"execution":{"iopub.status.busy":"2026-04-07T21:29:14.088504Z","iopub.execute_input":"2026-04-07T21:29:14.088836Z","iopub.status.idle":"2026-04-08T00:37:02.992223Z","shell.execute_reply.started":"2026-04-07T21:29:14.088811Z","shell.execute_reply":"2026-04-08T00:37:02.991165Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_history(history):\n    train_losses, val_losses, train_accs, val_accs = history\n\n    fig, axes = plt.subplots(1, 3, figsize=(18, 5))\n\n    # Loss plot\n    axes[0].plot(train_losses, label=\"Train Loss\", linewidth=2)\n    axes[0].plot(val_losses, label=\"Validation Loss\", linewidth=2)\n    axes[0].set_xlabel(\"Epochs\")\n    axes[0].set_ylabel(\"Loss\")\n    axes[0].set_title(\"Training vs Validation Loss\")\n    axes[0].legend()\n    axes[0].grid(True, alpha=0.3)\n\n    # Accuracy plot\n    axes[1].plot(train_accs, label=\"Train Accuracy\", linewidth=2)\n    axes[1].plot(val_accs, label=\"Validation Accuracy\", linewidth=2)\n    axes[1].set_xlabel(\"Epochs\")\n    axes[1].set_ylabel(\"Accuracy\")\n    axes[1].set_title(\"Training vs Validation Accuracy\")\n    axes[1].legend()\n    axes[1].grid(True, alpha=0.3)\n\n    # Gap plot (overfitting indicator)\n    gaps = [t - v for t, v in zip(train_accs, val_accs)]\n    axes[2].plot(gaps, label=\"Train-Val Acc Gap\", linewidth=2, color='red')\n    axes[2].axhline(y=0.05, color='green', linestyle='--', label='Acceptable gap (5%)')\n    axes[2].set_xlabel(\"Epochs\")\n    axes[2].set_ylabel(\"Accuracy Gap\")\n    axes[2].set_title(\"Overfitting Gap (Lower is Better)\")\n    axes[2].legend()\n    axes[2].grid(True, alpha=0.3)\n\n    plt.tight_layout()\n    plt.savefig(os.path.join(SAVE_DIR, 'training_curves.png'), dpi=150, bbox_inches='tight')\n    plt.show()\n\nplot_history(history)","metadata":{"execution":{"iopub.status.busy":"2026-04-08T00:37:02.993779Z","iopub.execute_input":"2026-04-08T00:37:02.994089Z","iopub.status.idle":"2026-04-08T00:37:04.054650Z","shell.execute_reply.started":"2026-04-08T00:37:02.994055Z","shell.execute_reply":"2026-04-08T00:37:04.053778Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load best model for evaluation\ncheckpoint = torch.load(os.path.join(SAVE_DIR, 'eff_best.pth'), map_location=device, weights_only=False)\nmodel.load_state_dict(checkpoint['model_state_dict'])\nprint(f\"Loaded best model from epoch {checkpoint['epoch']}\")\nprint(f\"Best Val Acc: {checkpoint['val_acc']:.4f}, Best Val QWK: {checkpoint['val_qwk']:.4f}\")\n\nmodel.eval()\n\npreds, labels_all = [], []\n\nwith torch.no_grad():\n    for images, labels in tqdm(test_loader, desc=\"Testing\"):\n        images = images.to(device)\n        outputs = model(images)\n\n        preds.extend(outputs.argmax(1).cpu().numpy())\n        labels_all.extend(labels.numpy())\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"TEST SET RESULTS - EfficientNet-B5\")\nprint(\"=\"*60)\nprint(classification_report(labels_all, preds,\n      target_names=['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative']))\n\n# Comprehensive metrics\ntest_accuracy = np.mean(np.array(preds) == np.array(labels_all))\ntest_qwk = cohen_kappa_score(labels_all, preds, weights='quadratic')\ntest_f1 = f1_score(labels_all, preds, average='weighted')\ntest_precision = precision_score(labels_all, preds, average='weighted')\ntest_recall = recall_score(labels_all, preds, average='weighted')\n\nprint(f\"\\n{'='*60}\")\nprint(f\"Aggregated Metrics:\")\nprint(f\"{'='*60}\")\nprint(f\"Test Accuracy:              {test_accuracy:.4f}\")\nprint(f\"Test F1 Score (weighted):   {test_f1:.4f}\")\nprint(f\"Test Precision (weighted):  {test_precision:.4f}\")\nprint(f\"Test Recall (weighted):     {test_recall:.4f}\")\nprint(f\"Quadratic Weighted Kappa:   {test_qwk:.4f}\")\nprint(f\"{'='*60}\")","metadata":{"execution":{"iopub.status.busy":"2026-04-08T00:37:04.055750Z","iopub.execute_input":"2026-04-08T00:37:04.056180Z","iopub.status.idle":"2026-04-08T00:37:54.674620Z","shell.execute_reply.started":"2026-04-08T00:37:04.056152Z","shell.execute_reply":"2026-04-08T00:37:54.673816Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cm = confusion_matrix(labels_all, preds)\n\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.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.title(\"Confusion Matrix - EfficientNet-B5\")\nplt.tight_layout()\nplt.savefig(os.path.join(SAVE_DIR, 'confusion_matrix.png'), dpi=150, bbox_inches='tight')\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2026-04-08T00:37:54.676013Z","iopub.execute_input":"2026-04-08T00:37:54.676326Z","iopub.status.idle":"2026-04-08T00:37:55.136474Z","shell.execute_reply.started":"2026-04-08T00:37:54.676288Z","shell.execute_reply":"2026-04-08T00:37:55.135788Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(os.listdir(\"/kaggle/working/\"))","metadata":{"execution":{"iopub.status.busy":"2026-04-08T00:37:55.137482Z","iopub.execute_input":"2026-04-08T00:37:55.137749Z","iopub.status.idle":"2026-04-08T00:37:55.142097Z","shell.execute_reply.started":"2026-04-08T00:37:55.137723Z","shell.execute_reply":"2026-04-08T00:37:55.141512Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink\n\nFileLink('/kaggle/working/eff_best.pth')","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}