{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-11T16:58:00.506363Z","iopub.execute_input":"2026-07-11T16:58:00.507227Z","iopub.status.idle":"2026-07-11T16:58:14.265121Z","shell.execute_reply.started":"2026-07-11T16:58:00.507197Z","shell.execute_reply":"2026-07-11T16:58:14.264492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nbase = '/kaggle/input/notebooks/sreecharithakadiri/fyp-review1'\nprint(\"Contents of the input folder:\")\nfor item in os.listdir(base):\n    print(f\"  - {item}\")\n    sub_path = os.path.join(base, item)\n    if os.path.isdir(sub_path):\n        for sub_item in os.listdir(sub_path):\n            print(f\"      - {sub_item}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T16:58:59.991620Z","iopub.execute_input":"2026-07-11T16:58:59.993171Z","iopub.status.idle":"2026-07-11T16:59:00.006822Z","shell.execute_reply.started":"2026-07-11T16:58:59.993140Z","shell.execute_reply":"2026-07-11T16:59:00.005588Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# CELL 15: PYTORCH DATASET & DATALOADERS (CORRECT PATH)\n# ==========================================\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport numpy as np\nimport pandas as pd\nimport os\nimport cv2\nimport albumentations as A\nfrom tqdm import tqdm\n\nprint(\"🔍 Loading data from the APTOS_Preprocessed folder...\")\n\n# --- 1. CORRECTED HARDCODED PATH ---\n# Your files are inside the APTOS_Preprocessed subfolder\ndata_base = '/kaggle/input/notebooks/sreecharithakadiri/fyp-review1/APTOS_Preprocessed'\n\n# Verify the path exists\nif not os.path.exists(data_base):\n    raise FileNotFoundError(f\"❌ Path not found: {data_base}. Please check the folder name.\")\n\nprint(f\"✅ Data path found: {data_base}\")\n\n# --- 2. DEFINE PATHS ---\n# Data paths (Read-Only from the input mount)\nTRAIN_DIR = os.path.join(data_base, 'train/')\nVAL_DIR = os.path.join(data_base, 'val/')\nTEST_DIR = os.path.join(data_base, 'test/')\n\n# Output paths (Writeable - for saving models and new results)\nOUTPUT_BASE = '/kaggle/working/'\nos.makedirs(OUTPUT_BASE, exist_ok=True)\n\n# Check if the train folder has files\ntrain_files = os.listdir(TRAIN_DIR)\nprint(f\"✅ Found {len(train_files)} files in train/ directory.\")\n\n# --- 3. LOAD OR COMPUTE MEAN/STD ---\nmean_path = os.path.join(data_base, 'retina_mean.npy')\nstd_path = os.path.join(data_base, 'retina_std.npy')\n\nif os.path.exists(mean_path) and os.path.exists(std_path):\n    dataset_mean = np.load(mean_path)\n    dataset_std = np.load(std_path)\n    print(f\"✅ Loaded Mean (R,G,B): {dataset_mean}\")\n    print(f\"✅ Loaded Std  (R,G,B): {dataset_std}\")\nelse:\n    print(\"⚠️ Mean/Std files not found in input. Computing from saved .npy files...\")\n    train_df = pd.read_csv(os.path.join(data_base, 'train_split.csv'))\n    all_pixels = []\n    for idx, row in tqdm(train_df.iterrows(), total=len(train_df)):\n        npy_path = os.path.join(TRAIN_DIR, row['id_code'] + '.npy')\n        img = np.load(npy_path)\n        all_pixels.append(img.reshape(-1, 3))\n    all_pixels = np.vstack(all_pixels)\n    dataset_mean = np.mean(all_pixels, axis=0)\n    dataset_std = np.std(all_pixels, axis=0)\n    # Save them to the working directory for future runs\n    np.save(os.path.join(OUTPUT_BASE, 'retina_mean.npy'), dataset_mean)\n    np.save(os.path.join(OUTPUT_BASE, 'retina_std.npy'), dataset_std)\n    print(f\"✅ Computed Mean: {dataset_mean}, Std: {dataset_std}\")\n\n# Convert to torch tensors\nmean_tensor = torch.tensor(dataset_mean, dtype=torch.float32).view(1, 3, 1, 1)\nstd_tensor = torch.tensor(dataset_std, dtype=torch.float32).view(1, 3, 1, 1)\n\n# --- 4. PYTORCH DATASET CLASS ---\n# --- 4. PYTORCH DATASET CLASS (FIXED for Albumentations >= 1.3.0) ---\nclass APTOSDataset(Dataset):\n    def __init__(self, csv_path, data_dir, phase='train'):\n        self.df = pd.read_csv(csv_path)\n        self.data_dir = data_dir\n        self.phase = phase\n        self.mean = mean_tensor\n        self.std = std_tensor\n        \n        # LIGHT AUGMENTATIONS (Only for training)\n        if phase == 'train':\n            self.transform = A.Compose([\n                A.HorizontalFlip(p=0.5),\n                A.Rotate(limit=15, border_mode=cv2.BORDER_CONSTANT, fill_value=0, p=0.8),  # <-- FIXED HERE\n                A.RandomBrightnessContrast(brightness_limit=0.05, contrast_limit=0.05, p=0.2),\n            ])\n        else:\n            self.transform = None\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row['id_code']\n        label = int(row['diagnosis'])\n        \n        # Load preprocessed .npy\n        npy_path = os.path.join(self.data_dir, img_id + '.npy')\n        img = np.load(npy_path)  # Shape: (384, 384, 3)\n        \n        # Apply augmentations (if training)\n        if self.transform is not None:\n            augmented = self.transform(image=img)\n            img = augmented['image']\n        \n        # Convert to tensor: (H, W, C) -> (C, H, W)\n        img = torch.from_numpy(img).permute(2, 0, 1).float()\n        \n        # Final normalization using your retina stats\n        img = (img - self.mean.squeeze(0)) / self.std.squeeze(0)\n        \n        return img, label\n       \n        \n# --- 5. CREATE DATALOADERS ---\nBATCH_SIZE = 16  # Reduce to 12 or 8 if you get CUDA out of memory\n\n# Load the split CSV files from the input path\ntrain_dataset = APTOSDataset(\n    csv_path=os.path.join(data_base, 'train_split.csv'),\n    data_dir=TRAIN_DIR,\n    phase='train'\n)\nval_dataset = APTOSDataset(\n    csv_path=os.path.join(data_base, 'val_split.csv'),\n    data_dir=VAL_DIR,\n    phase='val'\n)\ntest_dataset = APTOSDataset(\n    csv_path=os.path.join(data_base, 'test_split.csv'),\n    data_dir=TEST_DIR,\n    phase='test'\n)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\ntest_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f\"\\n✅ DataLoaders Created Successfully:\")\nprint(f\"   Train batches: {len(train_loader)}\")\nprint(f\"   Val batches:   {len(val_loader)}\")\nprint(f\"   Test batches:  {len(test_loader)}\")\n\n# --- Sanity check: Load one batch ---\nfor images, labels in train_loader:\n    print(f\"\\n✅ Sanity Check Passed:\")\n    print(f\"   Images shape: {images.shape}\")  # Expected: (16, 3, 384, 384)\n    print(f\"   Labels: {labels[:5]}\")\n    print(f\"   Image min: {images.min():.3f}, max: {images.max():.3f}\")  # Around -2 to +2\n    break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T16:59:03.304836Z","iopub.execute_input":"2026-07-11T16:59:03.305209Z","iopub.status.idle":"2026-07-11T16:59:06.033922Z","shell.execute_reply.started":"2026-07-11T16:59:03.305183Z","shell.execute_reply":"2026-07-11T16:59:06.033096Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# VERIFICATION: AUGMENTATION IN ACTION\n# ==========================================\nimport matplotlib.pyplot as plt\nimport torch\nimport numpy as np\nimport os\n\n# Get one batch from the train loader\nimages, labels = next(iter(train_loader))\n\n# Denormalize for display (using your saved mean/std)\nmean = dataset_mean\nstd = dataset_std\n\n# Create a 4x2 grid: Original (Normalized) vs Augmented (Normalized)\nfig, axes = plt.subplots(4, 2, figsize=(10, 16))\n\nfor i in range(4):\n    # Get the i-th image from the batch\n    img_tensor = images[i]  # Shape: (3, 384, 384), normalized\n    \n    # Denormalize for display\n    img_display = img_tensor.cpu().numpy().transpose(1, 2, 0)  # (H, W, C)\n    img_display = img_display * std + mean  # Undo normalization\n    img_display = np.clip(img_display, 0, 1)  # Clip to [0,1] for display\n    \n    # Left column: Original (without augmentation)\n    axes[i, 0].imshow(img_display)\n    axes[i, 0].set_title(f\"Sample {i+1}: Original (No Aug)\")\n    axes[i, 0].axis('off')\n    \n    # Right column: We need to load the SAME image but force augmentation.\n    # Since the DataLoader applies augmentation on the fly, we simulate it by\n    # re-loading the same image ID from the dataset with transform=True.\n    # Get the image ID from the dataset\n    idx = train_dataset.df.iloc[i]['id_code']\n    \n    # Reload the raw .npy file\n    npy_path = os.path.join(TRAIN_DIR, idx + '.npy')\n    raw_img = np.load(npy_path)  # (384, 384, 3), range [0,1]\n    \n    # Apply augmentation manually\n    augmented = train_dataset.transform(image=raw_img)\n    aug_img = augmented['image']\n    \n    # Convert to tensor and normalize\n    aug_img_tensor = torch.from_numpy(aug_img).permute(2, 0, 1).float()\n    aug_img_tensor = (aug_img_tensor - mean_tensor.squeeze(0)) / std_tensor.squeeze(0)\n    \n    # Denormalize for display\n    aug_display = aug_img_tensor.cpu().numpy().transpose(1, 2, 0)\n    aug_display = aug_display * std + mean\n    aug_display = np.clip(aug_display, 0, 1)\n    \n    # Right column: Augmented\n    axes[i, 1].imshow(aug_display)\n    axes[i, 1].set_title(f\"Sample {i+1}: Augmented\")\n    axes[i, 1].axis('off')\n\nplt.suptitle('Augmentation Verification: Original (Left) vs Augmented (Right)', fontsize=14)\nplt.tight_layout()\nplt.show()\n\nprint(\"✅ If you see rotations, flips, or brightness changes in the right column, augmentation is working!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T16:59:11.158587Z","iopub.execute_input":"2026-07-11T16:59:11.159240Z","iopub.status.idle":"2026-07-11T16:59:13.379764Z","shell.execute_reply.started":"2026-07-11T16:59:11.159197Z","shell.execute_reply":"2026-07-11T16:59:13.378911Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# CELL 16: CLASS WEIGHTS & FOCAL LOSS\n# ==========================================\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport os\nfrom sklearn.utils.class_weight import compute_class_weight\n\n# --- 1. PATH TO YOUR DATA (Same as Cell 15) ---\ndata_base = '/kaggle/input/notebooks/sreecharithakadiri/fyp-review1/APTOS_Preprocessed'\n\n# --- 2. LOAD TRAINING LABELS TO COMPUTE CLASS WEIGHTS ---\ntrain_df = pd.read_csv(os.path.join(data_base, 'train_split.csv'))\ntrain_labels = train_df['diagnosis'].values\n\n# --- 3. COMPUTE INVERSE FREQUENCY WEIGHTS ---\n# 'balanced' calculates weights inversely proportional to class frequencies\nclass_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=np.array([0, 1, 2, 3, 4]),\n    y=train_labels\n)\nclass_weights = torch.tensor(class_weights, dtype=torch.float32)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"📊 CLASS WEIGHTS (Inverse Frequency)\")\nprint(\"=\"*50)\nfor i, w in enumerate(class_weights):\n    print(f\"   Class {i} (Count: {np.sum(train_labels == i)}): {w:.4f}\")\nprint(\"=\"*50)\nprint(\"💡 Higher weight = More penalty for misclassifying this class.\")\nprint(\"   Class 3 (Severe) and Class 4 (Proliferative) get the highest weights.\")\nprint(\"=\"*50)\n\n# --- 4. FOCAL LOSS DEFINITION ---\nclass FocalLoss(nn.Module):\n    \"\"\"\n    Focal Loss with Class Weights for extreme imbalance.\n    gamma=2.0 is the standard value from the original paper.\n    \n    Formula: FL = -alpha * (1 - pt)^gamma * log(pt)\n    - pt: probability of the true class\n    - alpha: class weight for the true class\n    - gamma: focusing parameter (higher = more focus on hard examples)\n    \"\"\"\n    def __init__(self, alpha=None, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha  # Class weights (tensor of shape [num_classes])\n        self.gamma = gamma  # Focusing parameter\n\n    def forward(self, inputs, targets):\n        # Standard cross-entropy loss per sample (reduction='none' gives per-sample loss)\n        ce_loss = F.cross_entropy(inputs, targets, reduction='none')\n        \n        # Probability of the true class (pt)\n        pt = torch.exp(-ce_loss)  # Since ce_loss = -log(pt)\n        \n        # Apply class weights if provided\n        if self.alpha is not None:\n            # Pick the weight corresponding to each target class\n            alpha_t = self.alpha[targets]\n            # Focal Loss: alpha * (1 - pt)^gamma * ce_loss\n            focal_loss = alpha_t * (1 - pt) ** self.gamma * ce_loss\n        else:\n            # Without class weights: (1 - pt)^gamma * ce_loss\n            focal_loss = (1 - pt) ** self.gamma * ce_loss\n        \n        # Return mean loss over the batch\n        return focal_loss.mean()\n\n# --- 5. INSTANTIATE THE LOSS FUNCTION ---\n# Move the class weights to GPU later in Cell 17\ncriterion = FocalLoss(alpha=class_weights, gamma=2.0)\n\nprint(\"\\n✅ Focal Loss initialized successfully!\")\nprint(f\"   gamma = 2.0 (standard value)\")\nprint(f\"   alpha = {class_weights.numpy()} (class weights)\")\nprint(\"\\n⏳ Proceed to Cell 17 to start training ResNet-18.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T16:59:19.961519Z","iopub.execute_input":"2026-07-11T16:59:19.962743Z","iopub.status.idle":"2026-07-11T16:59:19.995240Z","shell.execute_reply.started":"2026-07-11T16:59:19.962693Z","shell.execute_reply":"2026-07-11T16:59:19.994296Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# CELL 17: BASELINE CNN – DENSENET-121\n# ==========================================\n# Run this cell after Cell 15 (DataLoaders) and Cell 16 (Focal Loss + Class Weights)\n# All necessary variables (train_loader, val_loader, test_loader, criterion) are already defined.\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torchvision.models as models\nimport numpy as np\nimport pandas as pd\nimport os\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm\nfrom sklearn.metrics import (\n    cohen_kappa_score, \n    accuracy_score, \n    confusion_matrix, \n    classification_report,\n    roc_auc_score,\n    roc_curve\n)\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ------------------------------\n# 1. DEVICE & PATHS\n# ------------------------------\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"🚀 Using device: {device}\")\nif device.type == 'cuda':\n    print(f\"   GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"   Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB\")\n\ndata_base = '/kaggle/input/notebooks/sreecharithakadiri/fyp-review1/APTOS_Preprocessed'\nOUTPUT_BASE = '/kaggle/working/'\nos.makedirs(OUTPUT_BASE, exist_ok=True)\n\n# ------------------------------\n# 2. MODEL – DENSENET-121 (Baseline)\n# ------------------------------\nmodel = models.densenet121(weights=models.DenseNet121_Weights.IMAGENET1K_V1)\n# Replace the classifier head for 5 classes\nmodel.classifier = nn.Linear(model.classifier.in_features, 5)\nmodel = model.to(device)\n\nprint(f\"\\n✅ Baseline Model: DenseNet-121 (pretrained on ImageNet)\")\nprint(f\"   Total parameters: {sum(p.numel() for p in model.parameters()) / 1e6:.2f}M\")\n\n# ------------------------------\n# 3. LOSS, OPTIMIZER, SCHEDULER\n# ------------------------------\n# class_weights and criterion (FocalLoss) are defined in Cell 16\ncriterion = FocalLoss(alpha=class_weights.to(device), gamma=2.0)\n\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='min', factor=0.5, patience=2\n)\n\nprint(f\"\\n✅ Training Configuration:\")\nprint(f\"   Loss: Focal Loss (gamma=2.0, alpha=balanced)\")\nprint(f\"   Optimizer: AdamW (lr=1e-4, weight_decay=1e-4)\")\nprint(f\"   Scheduler: ReduceLROnPlateau (factor=0.5, patience=2)\")\n\n# ------------------------------\n# 4. TRAINING LOOP (15 Epochs)\n# ------------------------------\nEPOCHS = 15\nbest_val_kappa = 0.0\nbest_epoch = 0\n\nhistory = {\n    'train_loss': [],\n    'val_loss': [],\n    'val_acc': [],\n    'val_kappa': [],\n    'lr': []\n}\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🏋️  STARTING BASELINE TRAINING (DenseNet-121)\")\nprint(\"=\"*60)\n\nfor epoch in range(1, EPOCHS + 1):\n    # --- Training Phase ---\n    model.train()\n    train_loss = 0.0\n    train_correct = 0\n    train_total = 0\n    progress = tqdm(train_loader, desc=f\"Epoch {epoch}/{EPOCHS} [Train]\")\n    for images, labels in progress:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item() * images.size(0)\n        _, preds = torch.max(outputs, 1)\n        train_correct += (preds == labels).sum().item()\n        train_total += labels.size(0)\n        progress.set_postfix({\n            'loss': f\"{loss.item():.4f}\",\n            'acc': f\"{(train_correct/train_total)*100:.1f}%\"\n        })\n    avg_train_loss = train_loss / len(train_dataset)\n    train_acc = train_correct / train_total\n\n    # --- Validation Phase ---\n    model.eval()\n    val_loss = 0.0\n    all_preds, all_labels = [], []\n    with torch.no_grad():\n        for images, labels in tqdm(val_loader, desc=f\"Epoch {epoch}/{EPOCHS} [Val]\", leave=False):\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * images.size(0)\n            _, preds = torch.max(outputs, 1)\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n    avg_val_loss = val_loss / len(val_dataset)\n    val_acc = accuracy_score(all_labels, all_preds)\n    val_kappa = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\n\n    # Store history\n    history['train_loss'].append(avg_train_loss)\n    history['val_loss'].append(avg_val_loss)\n    history['val_acc'].append(val_acc)\n    history['val_kappa'].append(val_kappa)\n    history['lr'].append(optimizer.param_groups[0]['lr'])\n\n    # Print summary\n    print(f\"\\n📊 Epoch {epoch}/{EPOCHS} Summary:\")\n    print(f\"   Train Loss: {avg_train_loss:.4f} | Train Acc: {train_acc*100:.2f}%\")\n    print(f\"   Val Loss:   {avg_val_loss:.4f} | Val Acc:   {val_acc*100:.2f}% | QWK: {val_kappa:.4f}\")\n    print(f\"   LR: {optimizer.param_groups[0]['lr']:.2e}\")\n\n    # Step scheduler\n    scheduler.step(avg_val_loss)\n\n    # Save best model based on QWK\n    if val_kappa > best_val_kappa:\n        best_val_kappa = val_kappa\n        best_epoch = epoch\n        torch.save(model.state_dict(), os.path.join(OUTPUT_BASE, 'densenet121_baseline_best.pth'))\n        print(f\"   ✅ New best model saved! (QWK = {val_kappa:.4f})\")\n    print(\"-\"*60)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🏁 BASELINE TRAINING COMPLETE!\")\nprint(f\"🏆 Best Validation QWK: {best_val_kappa:.4f} (Epoch {best_epoch})\")\nprint(f\"📁 Model saved to: {OUTPUT_BASE}densenet121_baseline_best.pth\")\nprint(\"=\"*60)\n\n# ------------------------------\n# 5. LOAD BEST MODEL FOR EVALUATION\n# ------------------------------\nmodel.load_state_dict(torch.load(os.path.join(OUTPUT_BASE, 'densenet121_baseline_best.pth')))\nmodel.eval()\n\n# Re‑evaluate on validation set (to get all predictions and probabilities)\nall_preds = []\nall_probs = []   # for AUC-ROC\nall_labels = []\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device)\n        outputs = model(images)\n        probs = torch.softmax(outputs, dim=1)  # probabilities for each class\n        _, preds = torch.max(outputs, 1)\n        all_preds.extend(preds.cpu().numpy())\n        all_probs.extend(probs.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\nall_preds = np.array(all_preds)\nall_probs = np.array(all_probs)\nall_labels = np.array(all_labels)\n\n# ------------------------------\n# 6. METRICS COMPUTATION\n# ------------------------------\n# Accuracy\nfinal_acc = accuracy_score(all_labels, all_preds)\n# QWK\nfinal_kappa = cohen_kappa_score(all_labels, all_preds, weights='quadratic')\n# Confusion Matrix\ncm = confusion_matrix(all_labels, all_preds)\n# Classification Report\nclass_names = ['No DR', 'Mild', 'Moderate', 'Severe', 'Proliferative']\nreport = classification_report(all_labels, all_preds, target_names=class_names, digits=4)\n\n# AUC-ROC (one-vs-rest) – compute macro and per-class\ntry:\n    # For multi-class, use 'ovr' (one-vs-rest) strategy\n    auc_roc = roc_auc_score(all_labels, all_probs, multi_class='ovr', average='macro')\n    per_class_auc = roc_auc_score(all_labels, all_probs, multi_class='ovr', average=None)\nexcept ValueError as e:\n    print(f\"⚠️ AUC-ROC could not be computed: {e}\")\n    auc_roc = None\n    per_class_auc = None\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"📊 FINAL VALIDATION METRICS\")\nprint(\"=\"*60)\nprint(f\"Accuracy  : {final_acc:.4f}\")\nprint(f\"QWK       : {final_kappa:.4f}\")\nif auc_roc is not None:\n    print(f\"AUC-ROC (macro) : {auc_roc:.4f}\")\nprint(\"\\nClassification Report:\\n\", report)\nprint(\"Confusion Matrix:\\n\", cm)\nprint(\"=\"*60)\n\n# ------------------------------\n# 7. VISUALISATIONS\n# ------------------------------\n# 7.1 Loss Curves\nfig, axes = plt.subplots(2, 2, figsize=(14, 12))\nfig.suptitle('Baseline DenseNet-121 – Performance Metrics', fontsize=16)\n\n# Loss\naxes[0, 0].plot(history['train_loss'], label='Train Loss', marker='o', linewidth=2)\naxes[0, 0].plot(history['val_loss'], label='Val Loss', marker='s', linewidth=2)\naxes[0, 0].axvline(x=best_epoch-1, color='red', linestyle='--', label=f'Best Epoch ({best_epoch})')\naxes[0, 0].set_xlabel('Epoch')\naxes[0, 0].set_ylabel('Loss')\naxes[0, 0].set_title('Training & Validation Loss')\naxes[0, 0].legend()\naxes[0, 0].grid(True, alpha=0.3)\n\n# QWK (primary)\naxes[0, 1].plot(history['val_kappa'], label='Val QWK', marker='o', color='green', linewidth=2)\naxes[0, 1].axvline(x=best_epoch-1, color='red', linestyle='--', label=f'Best Epoch ({best_epoch})')\naxes[0, 1].axhline(y=0.75, color='gray', linestyle=':', label='Good Threshold (0.75)')\naxes[0, 1].set_xlabel('Epoch')\naxes[0, 1].set_ylabel('Quadratic Weighted Kappa')\naxes[0, 1].set_title('Validation QWK')\naxes[0, 1].legend()\naxes[0, 1].grid(True, alpha=0.3)\n\n# Accuracy & Learning Rate\nax2 = axes[1, 0]\nax2.plot(history['val_acc'], label='Val Accuracy', marker='^', color='blue', linewidth=2)\nax2.set_xlabel('Epoch')\nax2.set_ylabel('Accuracy')\nax2.set_title('Validation Accuracy')\nax2.legend(loc='upper left')\nax2.grid(True, alpha=0.3)\nax2_2 = ax2.twinx()\nax2_2.plot(history['lr'], label='Learning Rate', marker='x', color='orange', linestyle='--', linewidth=2)\nax2_2.set_ylabel('Learning Rate')\nax2_2.legend(loc='upper right')\nax2_2.set_yscale('log')\n\n# Confusion Matrix\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_names, yticklabels=class_names, ax=axes[1, 1])\naxes[1, 1].set_xlabel('Predicted')\naxes[1, 1].set_ylabel('True')\naxes[1, 1].set_title(f'Confusion Matrix\\nQWK: {final_kappa:.4f}')\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_BASE, 'densenet121_baseline_metrics.png'), dpi=150, bbox_inches='tight')\nplt.show()\nprint(f\"📊 Metrics plot saved to: {OUTPUT_BASE}densenet121_baseline_metrics.png\")\n\n# 7.2 ROC Curves (if AUC computed)\nif auc_roc is not None and per_class_auc is not None:\n    plt.figure(figsize=(8, 6))\n    # Compute ROC curves for each class\n    for i in range(5):\n        fpr, tpr, _ = roc_curve(all_labels == i, all_probs[:, i])\n        plt.plot(fpr, tpr, label=f'{class_names[i]} (AUC = {per_class_auc[i]:.3f})')\n    plt.plot([0, 1], [0, 1], 'k--', label='Random')\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('False Positive Rate')\n    plt.ylabel('True Positive Rate')\n    plt.title(f'ROC Curves (One-vs-Rest)\\nMacro AUC = {auc_roc:.4f}')\n    plt.legend(loc='lower right')\n    plt.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.savefig(os.path.join(OUTPUT_BASE, 'densenet121_roc_curves.png'), dpi=150, bbox_inches='tight')\n    plt.show()\n    print(f\"📊 ROC curves saved to: {OUTPUT_BASE}densenet121_roc_curves.png\")\n\n# 7.3 Per-class performance bar plot (optional)\nper_class_metrics = classification_report(all_labels, all_preds, target_names=class_names, output_dict=True)\ndf_report = pd.DataFrame(per_class_metrics).transpose()\ndf_report = df_report.iloc[:5, :3]  # keep precision, recall, f1-score for the 5 classes\ndf_report.plot(kind='bar', figsize=(10, 6), colormap='viridis')\nplt.title('Per-Class Precision, Recall, F1-Score')\nplt.ylabel('Score')\nplt.ylim(0, 1)\nplt.xticks(rotation=45)\nplt.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_BASE, 'densenet121_per_class_scores.png'), dpi=150, bbox_inches='tight')\nplt.show()\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"✅ ALL METRICS COMPUTED AND SAVED.\")\nprint(\"   Baseline DenseNet-121 is ready.\")\nprint(\"   Next Step: Train EfficientNet-B3 and ResNet-50 for the ensemble.\")\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T17:02:01.770756Z","iopub.execute_input":"2026-07-11T17:02:01.771214Z","iopub.status.idle":"2026-07-11T17:21:33.229160Z","shell.execute_reply.started":"2026-07-11T17:02:01.771181Z","shell.execute_reply":"2026-07-11T17:21:33.228299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}