{"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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\n\nfrom PIL import Image\n\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom torchvision import transforms\nfrom torchvision.models import (\n    efficientnet_v2_s,\n    EfficientNet_V2_S_Weights\n)\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    classification_report,\n    confusion_matrix\n)\n\nprint(\"Libraries loaded successfully!\")\nprint(\"PyTorch version:\", torch.__version__)\n\ndevice = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Using device:\", device)\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:35:12.634551Z","iopub.execute_input":"2026-08-26T13:35:12.634826Z","iopub.status.idle":"2026-08-26T13:35:21.316439Z","shell.execute_reply.started":"2026-08-26T13:35:12.634797Z","shell.execute_reply":"2026-08-26T13:35:21.315687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# APTOS dataset location\nbase_path = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\n# Paths\ntrain_csv = os.path.join(base_path, \"train.csv\")\ntrain_image_folder = os.path.join(base_path, \"train_images\")\n\n# Load CSV\ndf = pd.read_csv(train_csv)\n\nprint(\"Dataset loaded!\")\nprint(\"Number of images:\", len(df))\n\nprint(\"\\nFirst 5 rows:\")\nprint(df.head())\n\nprint(\"\\nClass distribution:\")\nprint(df[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:35:49.588663Z","iopub.execute_input":"2026-08-26T13:35:49.589368Z","iopub.status.idle":"2026-08-26T13:35:49.601165Z","shell.execute_reply.started":"2026-08-26T13:35:49.589340Z","shell.execute_reply":"2026-08-26T13:35:49.600540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# First split: 70% training, 30% temporary\ntrain_df, temp_df = train_test_split(\n    df,\n    test_size=0.30,\n    stratify=df[\"diagnosis\"],\n    random_state=42\n)\n\n# Second split: split the 30% temporary set equally\n# → 15% validation\n# → 15% test\n\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.50,\n    stratify=temp_df[\"diagnosis\"],\n    random_state=42\n)\n\n# Reset indexes\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\ntest_df = test_df.reset_index(drop=True)\n\nprint(\"Training images:\", len(train_df))\nprint(\"Validation images:\", len(val_df))\nprint(\"Test images:\", len(test_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:36:12.624743Z","iopub.execute_input":"2026-08-26T13:36:12.625508Z","iopub.status.idle":"2026-08-26T13:36:12.639070Z","shell.execute_reply.started":"2026-08-26T13:36:12.625481Z","shell.execute_reply":"2026-08-26T13:36:12.638491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Move one training image to validation\none_row = train_df.iloc[[0]]\n\ntrain_df = train_df.iloc[1:].reset_index(drop=True)\nval_df = pd.concat(\n    [val_df, one_row],\n    ignore_index=True\n)\n\nprint(\"Training images:\", len(train_df))\nprint(\"Validation images:\", len(val_df))\nprint(\"Test images:\", len(test_df))\n\nprint(\"\\nTotal:\", len(train_df) + len(val_df) + len(test_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:36:38.335535Z","iopub.execute_input":"2026-08-26T13:36:38.335793Z","iopub.status.idle":"2026-08-26T13:36:38.342555Z","shell.execute_reply.started":"2026-08-26T13:36:38.335771Z","shell.execute_reply":"2026-08-26T13:36:38.341763Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Training:\")\nprint(train_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nValidation:\")\nprint(val_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nTest:\")\nprint(test_df[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:36:56.014995Z","iopub.execute_input":"2026-08-26T13:36:56.015660Z","iopub.status.idle":"2026-08-26T13:36:56.023395Z","shell.execute_reply.started":"2026-08-26T13:36:56.015630Z","shell.execute_reply":"2026-08-26T13:36:56.022542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 512\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(10),\n    transforms.ToTensor(),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n])\n\nprint(\"Image transformations created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:37:22.590724Z","iopub.execute_input":"2026-08-26T13:37:22.591321Z","iopub.status.idle":"2026-08-26T13:37:22.596561Z","shell.execute_reply.started":"2026-08-26T13:37:22.591282Z","shell.execute_reply":"2026-08-26T13:37:22.595730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class APTOSDataset(Dataset):\n\n    def __init__(self, dataframe, image_folder, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.image_folder = image_folder\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, index):\n\n        row = self.dataframe.iloc[index]\n\n        image_name = row[\"id_code\"] + \".png\"\n\n        image_path = os.path.join(\n            self.image_folder,\n            image_name\n        )\n\n        image = Image.open(image_path).convert(\"RGB\")\n\n        label = int(row[\"diagnosis\"])\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, torch.tensor(label, dtype=torch.long)\n\n\nprint(\"APTOS Dataset class created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:37:48.512270Z","iopub.execute_input":"2026-08-26T13:37:48.512890Z","iopub.status.idle":"2026-08-26T13:37:48.519092Z","shell.execute_reply.started":"2026-08-26T13:37:48.512860Z","shell.execute_reply":"2026-08-26T13:37:48.518420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = APTOSDataset(\n    train_df,\n    train_image_folder,\n    train_transform\n)\n\nval_dataset = APTOSDataset(\n    val_df,\n    train_image_folder,\n    val_transform\n)\n\ntest_dataset = APTOSDataset(\n    test_df,\n    train_image_folder,\n    val_transform\n)\n\nprint(\"Training dataset:\", len(train_dataset))\nprint(\"Validation dataset:\", len(val_dataset))\nprint(\"Test dataset:\", len(test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:38:22.491074Z","iopub.execute_input":"2026-08-26T13:38:22.491533Z","iopub.status.idle":"2026-08-26T13:38:22.497574Z","shell.execute_reply.started":"2026-08-26T13:38:22.491505Z","shell.execute_reply":"2026-08-26T13:38:22.496743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image, label = train_dataset[0]\n\nprint(\"Image shape:\", image.shape)\nprint(\"Label:\", label)\nprint(\"Pixel minimum:\", image.min().item())\nprint(\"Pixel maximum:\", image.max().item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:38:33.051204Z","iopub.execute_input":"2026-08-26T13:38:33.051561Z","iopub.status.idle":"2026-08-26T13:38:33.210524Z","shell.execute_reply.started":"2026-08-26T13:38:33.051535Z","shell.execute_reply":"2026-08-26T13:38:33.209731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    train_dataset,\n    batch_size=16,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(\"Training batches:\", len(train_loader))\nprint(\"Validation batches:\", len(val_loader))\nprint(\"Test batches:\", len(test_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:38:54.431153Z","iopub.execute_input":"2026-08-26T13:38:54.431778Z","iopub.status.idle":"2026-08-26T13:38:54.437372Z","shell.execute_reply.started":"2026-08-26T13:38:54.431749Z","shell.execute_reply":"2026-08-26T13:38:54.436703Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images, labels = next(iter(train_loader))\n\nprint(\"Images shape:\", images.shape)\nprint(\"Labels shape:\", labels.shape)\nprint(\"Labels:\", labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:39:24.290979Z","iopub.execute_input":"2026-08-26T13:39:24.291733Z","iopub.status.idle":"2026-08-26T13:39:31.511643Z","shell.execute_reply.started":"2026-08-26T13:39:24.291705Z","shell.execute_reply":"2026-08-26T13:39:31.510711Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"weights = EfficientNet_V2_S_Weights.DEFAULT\n\nmodel = efficientnet_v2_s(\n    weights=weights\n)\n\n# Replace ImageNet's 1000-class classifier\n# with our 5 DR classes\nmodel.classifier[1] = nn.Linear(\n    model.classifier[1].in_features,\n    5\n)\n\n# Move model to Tesla T4\nmodel = model.to(device)\n\nprint(\"EfficientNetV2-S loaded!\")\nprint(model.classifier)\nprint(\"Device:\", next(model.parameters()).device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:39:56.731384Z","iopub.execute_input":"2026-08-26T13:39:56.732048Z","iopub.status.idle":"2026-08-26T13:39:58.122906Z","shell.execute_reply.started":"2026-08-26T13:39:56.732003Z","shell.execute_reply":"2026-08-26T13:39:58.122297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_counts = (\n    train_df[\"diagnosis\"]\n    .value_counts()\n    .sort_index()\n    .values\n)\n\nclass_weights_np = len(train_df) / (\n    5 * class_counts\n)\n\nprint(\"Class counts:\")\nprint(class_counts)\n\nprint(\"\\nClass weights:\")\nprint(class_weights_np)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:40:31.853609Z","iopub.execute_input":"2026-08-26T13:40:31.854433Z","iopub.status.idle":"2026-08-26T13:40:31.861799Z","shell.execute_reply.started":"2026-08-26T13:40:31.854401Z","shell.execute_reply":"2026-08-26T13:40:31.860914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_weights = torch.tensor(\n    class_weights_np,\n    dtype=torch.float32\n).to(device)\n\ncriterion = nn.CrossEntropyLoss(\n    weight=class_weights\n)\n\nprint(\"Class weights:\", class_weights)\nprint(\"Device:\", class_weights.device)\nprint(\"Weighted loss created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:40:54.265111Z","iopub.execute_input":"2026-08-26T13:40:54.265959Z","iopub.status.idle":"2026-08-26T13:40:54.467834Z","shell.execute_reply.started":"2026-08-26T13:40:54.265922Z","shell.execute_reply":"2026-08-26T13:40:54.467120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Freeze all EfficientNet feature layers\nfor param in model.features.parameters():\n    param.requires_grad = False\n\n# Unfreeze the deeper feature blocks\nfor block in [5, 6, 7]:\n    for param in model.features[block].parameters():\n        param.requires_grad = True\n\n# Keep the classifier trainable\nfor param in model.classifier.parameters():\n    param.requires_grad = True\n\n# Check everything\nfor i, layer in enumerate(model.features):\n\n    trainable = any(\n        param.requires_grad\n        for param in layer.parameters()\n    )\n\n    print(\n        f\"Block {i}:\",\n        \"TRAINABLE 🔥\" if trainable\n        else \"FROZEN ❄️\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:41:13.049791Z","iopub.execute_input":"2026-08-26T13:41:13.050306Z","iopub.status.idle":"2026-08-26T13:41:13.059195Z","shell.execute_reply.started":"2026-08-26T13:41:13.050278Z","shell.execute_reply":"2026-08-26T13:41:13.058459Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"optimizer = torch.optim.AdamW(\n    [\n        {\n            \"params\": model.features[5:].parameters(),\n            \"lr\": 1e-5\n        },\n        {\n            \"params\": model.classifier.parameters(),\n            \"lr\": 1e-4\n        }\n    ],\n    weight_decay=1e-4\n)\n\nprint(\"Fine-tuning optimizer ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:41:42.319235Z","iopub.execute_input":"2026-08-26T13:41:42.319991Z","iopub.status.idle":"2026-08-26T13:41:42.326191Z","shell.execute_reply.started":"2026-08-26T13:41:42.319963Z","shell.execute_reply":"2026-08-26T13:41:42.325315Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    optimizer,\n    T_max=10\n)\n\nprint(\"Fine-tuning scheduler ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:42:04.209174Z","iopub.execute_input":"2026-08-26T13:42:04.209647Z","iopub.status.idle":"2026-08-26T13:42:04.214163Z","shell.execute_reply.started":"2026-08-26T13:42:04.209620Z","shell.execute_reply":"2026-08-26T13:42:04.213564Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(\n    model,\n    loader,\n    criterion,\n    optimizer,\n    device\n):\n    model.train()\n\n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    for images, labels in loader:\n\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        optimizer.zero_grad()\n\n        outputs = model(images)\n\n        loss = criterion(outputs, labels)\n\n        loss.backward()\n\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n\n        predictions = torch.argmax(outputs, dim=1)\n\n        correct += (predictions == labels).sum().item()\n        total += labels.size(0)\n\n    epoch_loss = running_loss / total\n    epoch_accuracy = correct / total\n\n    return epoch_loss, epoch_accuracy\n\n\ndef validate_one_epoch(\n    model,\n    loader,\n    criterion,\n    device\n):\n    model.eval()\n\n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    with torch.no_grad():\n\n        for images, labels in loader:\n\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            outputs = model(images)\n\n            loss = criterion(outputs, labels)\n\n            running_loss += loss.item() * images.size(0)\n\n            predictions = torch.argmax(outputs, dim=1)\n\n            correct += (predictions == labels).sum().item()\n            total += labels.size(0)\n\n    epoch_loss = running_loss / total\n    epoch_accuracy = correct / total\n\n    return epoch_loss, epoch_accuracy\n\n\nprint(\"Training functions ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:43:08.258579Z","iopub.execute_input":"2026-08-26T13:43:08.258853Z","iopub.status.idle":"2026-08-26T13:43:08.267038Z","shell.execute_reply.started":"2026-08-26T13:43:08.258831Z","shell.execute_reply":"2026-08-26T13:43:08.266290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_epochs = 10\n\nbest_val_loss = float(\"inf\")\npatience = 3\npatience_counter = 0\n\nbest_epoch = 0\n\nfor epoch in range(num_epochs):\n\n    # TRAIN\n    train_loss, train_acc = train_one_epoch(\n        model,\n        train_loader,\n        criterion,\n        optimizer,\n        device\n    )\n\n    # VALIDATION\n    val_loss, val_acc = validate_one_epoch(\n        model,\n        val_loader,\n        criterion,\n        device\n    )\n\n    # Update learning rate\n    scheduler.step()\n\n    # SAVE BEST MODEL\n    if val_loss < best_val_loss:\n\n        best_val_loss = val_loss\n        patience_counter = 0\n        best_epoch = epoch + 1\n\n        torch.save(\n            model.state_dict(),\n            \"/kaggle/working/best_finetuned_aptos.pth\"\n        )\n\n        print(\"  ⭐ Best model saved!\")\n\n    else:\n        patience_counter += 1\n\n    print(\n        f\"Epoch {epoch + 1}/{num_epochs} | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Train Acc: {train_acc:.4f} | \"\n        f\"Val Loss: {val_loss:.4f} | \"\n        f\"Val Acc: {val_acc:.4f}\"\n    )\n\n    # EARLY STOPPING\n    if patience_counter >= patience:\n\n        print(\"Early stopping triggered.\")\n        break\n\nprint(\"\\nBest epoch:\", best_epoch)\nprint(\"Best validation loss:\", best_val_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T13:44:03.699046Z","iopub.execute_input":"2026-08-26T13:44:03.699742Z","iopub.status.idle":"2026-08-26T14:21:31.869494Z","shell.execute_reply.started":"2026-08-26T13:44:03.699711Z","shell.execute_reply":"2026-08-26T14:21:31.868632Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nmodel_path = \"/kaggle/working/best_finetuned_aptos.pth\"\n\nprint(\"Model file exists:\", os.path.exists(model_path))\n\nif os.path.exists(model_path):\n    size_mb = os.path.getsize(model_path) / (1024 * 1024)\n    print(f\"Model size: {size_mb:.2f} MB\")\n    print(\"Model saved safely! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:22:01.336495Z","iopub.execute_input":"2026-08-26T14:22:01.336770Z","iopub.status.idle":"2026-08-26T14:22:01.342562Z","shell.execute_reply.started":"2026-08-26T14:22:01.336746Z","shell.execute_reply":"2026-08-26T14:22:01.341783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink\n\nFileLink(\n    \"/kaggle/working/best_finetuned_aptos.pth\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:22:45.991473Z","iopub.execute_input":"2026-08-26T14:22:45.992368Z","iopub.status.idle":"2026-08-26T14:22:45.998782Z","shell.execute_reply.started":"2026-08-26T14:22:45.992340Z","shell.execute_reply":"2026-08-26T14:22:45.997910Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_path = \"/kaggle/working/best_finetuned_aptos.pth\"\n\nmodel.load_state_dict(\n    torch.load(\n        model_path,\n        map_location=device\n    )\n)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Best model loaded successfully! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:26:22.116568Z","iopub.execute_input":"2026-08-26T14:26:22.116985Z","iopub.status.idle":"2026-08-26T14:26:22.388504Z","shell.execute_reply.started":"2026-08-26T14:26:22.116957Z","shell.execute_reply":"2026-08-26T14:26:22.387672Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\ntest_predictions = []\ntest_labels = []\ntest_probabilities = []\n\nwith torch.no_grad():\n\n    for images, labels in test_loader:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        outputs = model(images)\n\n        probabilities = torch.softmax(\n            outputs,\n            dim=1\n        )\n\n        predictions = torch.argmax(\n            probabilities,\n            dim=1\n        )\n\n        test_predictions.extend(\n            predictions.cpu().numpy()\n        )\n\n        test_labels.extend(\n            labels.numpy()\n        )\n\n        test_probabilities.extend(\n            probabilities.cpu().numpy()\n        )\n\ntest_predictions = np.array(test_predictions)\ntest_labels = np.array(test_labels)\ntest_probabilities = np.array(test_probabilities)\n\nprint(\"Number of predictions:\", len(test_predictions))\nprint(\"Number of actual labels:\", len(test_labels))\nprint(\"Probability shape:\", test_probabilities.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:27:01.491822Z","iopub.execute_input":"2026-08-26T14:27:01.492376Z","iopub.status.idle":"2026-08-26T14:27:44.868768Z","shell.execute_reply.started":"2026-08-26T14:27:01.492347Z","shell.execute_reply":"2026-08-26T14:27:44.867963Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(classification_report(\n    test_labels,\n    test_predictions,\n    target_names=[\n        \"No DR\",\n        \"Mild\",\n        \"Moderate\",\n        \"Severe\",\n        \"Proliferative\"\n    ],\n    digits=4\n))\n\nprint(\"5-Class Test Confusion Matrix:\")\nprint(\n    confusion_matrix(\n        test_labels,\n        test_predictions\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:28:19.927136Z","iopub.execute_input":"2026-08-26T14:28:19.927459Z","iopub.status.idle":"2026-08-26T14:28:19.947862Z","shell.execute_reply.started":"2026-08-26T14:28:19.927430Z","shell.execute_reply":"2026-08-26T14:28:19.947294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Actual referable DR\nactual_referable = (\n    test_labels >= 2\n).astype(int)\n\n# Predicted referable DR\npredicted_referable = (\n    test_predictions >= 2\n).astype(int)\n\n# Confusion matrix\nreferable_cm = confusion_matrix(\n    actual_referable,\n    predicted_referable\n)\n\nprint(\"Referable DR Confusion Matrix:\")\nprint(referable_cm)\n\nTN, FP, FN, TP = referable_cm.ravel()\n\nsensitivity = TP / (TP + FN)\nspecificity = TN / (TN + FP)\n\nprint()\nprint(f\"Sensitivity: {sensitivity:.4f}\")\nprint(f\"Specificity: {specificity:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:28:45.300969Z","iopub.execute_input":"2026-08-26T14:28:45.301809Z","iopub.status.idle":"2026-08-26T14:28:45.309649Z","shell.execute_reply.started":"2026-08-26T14:28:45.301776Z","shell.execute_reply":"2026-08-26T14:28:45.308967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install grad-cam -q","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:29:30.091765Z","iopub.execute_input":"2026-08-26T14:29:30.092585Z","iopub.status.idle":"2026-08-26T14:29:41.405846Z","shell.execute_reply.started":"2026-08-26T14:29:30.092551Z","shell.execute_reply":"2026-08-26T14:29:41.405094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\nprint(\"Grad-CAM imported successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:30:04.232537Z","iopub.execute_input":"2026-08-26T14:30:04.233285Z","iopub.status.idle":"2026-08-26T14:30:04.582387Z","shell.execute_reply.started":"2026-08-26T14:30:04.233202Z","shell.execute_reply":"2026-08-26T14:30:04.581689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_layer = model.features[-1]\n\nprint(\"Target layer selected!\")\nprint(target_layer)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:30:58.272076Z","iopub.execute_input":"2026-08-26T14:30:58.272878Z","iopub.status.idle":"2026-08-26T14:30:58.278017Z","shell.execute_reply.started":"2026-08-26T14:30:58.272846Z","shell.execute_reply":"2026-08-26T14:30:58.277312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Take the first test image\nimage_tensor, true_label = test_dataset[0]\n\nprint(\"Image shape:\", image_tensor.shape)\nprint(\"True label:\", true_label.item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:31:20.271550Z","iopub.execute_input":"2026-08-26T14:31:20.271957Z","iopub.status.idle":"2026-08-26T14:31:20.430605Z","shell.execute_reply.started":"2026-08-26T14:31:20.271928Z","shell.execute_reply":"2026-08-26T14:31:20.429730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Put image into a batch\nimport matplotlib.pyplot as plt\ninput_tensor = image_tensor.unsqueeze(0).to(device)\n\n# Get model prediction\nmodel.eval()\n\nwith torch.no_grad():\n    output = model(input_tensor)\n    predicted_label = torch.argmax(\n        output,\n        dim=1\n    ).item()\n\nprint(\"True label:\", true_label.item())\nprint(\"Predicted label:\", predicted_label)\n\n# Explain the predicted class\ntargets = [\n    ClassifierOutputTarget(predicted_label)\n]\n\n# Create Grad-CAM\ncam = GradCAM(\n    model=model,\n    target_layers=[target_layer]\n)\n\n# Generate Grad-CAM\ngrayscale_cam = cam(\n    input_tensor=input_tensor,\n    targets=targets\n)[0]\n\n# Convert tensor to image\nrgb_image = (\n    image_tensor\n    .permute(1, 2, 0)\n    .cpu()\n    .numpy()\n)\n\nrgb_image = np.clip(rgb_image, 0, 1)\n\n# Create heatmap overlay\nvisualization = show_cam_on_image(\n    rgb_image,\n    grayscale_cam,\n    use_rgb=True\n)\n\n# Display\nplt.figure(figsize=(14, 6))\n\nplt.subplot(1, 2, 1)\nplt.imshow(rgb_image)\nplt.title(\n    f\"Original\\nTrue: {true_label.item()}\"\n)\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(visualization)\nplt.title(\n    f\"Grad-CAM\\nPredicted: {predicted_label}\"\n)\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:32:14.297541Z","iopub.execute_input":"2026-08-26T14:32:14.298146Z","iopub.status.idle":"2026-08-26T14:32:14.823267Z","shell.execute_reply.started":"2026-08-26T14:32:14.298117Z","shell.execute_reply":"2026-08-26T14:32:14.822321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Find incorrectly classified test images\n\nwrong_indices = np.where(\n    test_predictions != test_labels\n)[0]\n\nprint(\"Number of wrong predictions:\", len(wrong_indices))\n\n# Take the first wrong prediction\nwrong_index = wrong_indices[0]\n\nprint(\"Wrong image index:\", wrong_index)\nprint(\"True label:\", test_labels[wrong_index])\nprint(\"Predicted label:\", test_predictions[wrong_index])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:32:55.551875Z","iopub.execute_input":"2026-08-26T14:32:55.552356Z","iopub.status.idle":"2026-08-26T14:32:55.557848Z","shell.execute_reply.started":"2026-08-26T14:32:55.552327Z","shell.execute_reply":"2026-08-26T14:32:55.557119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get the wrongly classified image\nwrong_image, wrong_true_label = test_dataset[wrong_index]\n\n# Add batch dimension and move to GPU\nwrong_input = wrong_image.unsqueeze(0).to(device)\n\n# Model prediction\nmodel.eval()\n\nwith torch.no_grad():\n    wrong_output = model(wrong_input)\n\n    wrong_predicted_label = torch.argmax(\n        wrong_output,\n        dim=1\n    ).item()\n\nprint(\"True label:\", wrong_true_label.item())\nprint(\"Predicted label:\", wrong_predicted_label)\n\n# Tell Grad-CAM to explain the predicted class\nwrong_targets = [\n    ClassifierOutputTarget(wrong_predicted_label)\n]\n\n# Generate Grad-CAM\nwrong_cam = GradCAM(\n    model=model,\n    target_layers=[target_layer]\n)\n\nwrong_grayscale_cam = wrong_cam(\n    input_tensor=wrong_input,\n    targets=wrong_targets\n)[0]\n\n# Convert tensor to image\nwrong_rgb_image = (\n    wrong_image\n    .permute(1, 2, 0)\n    .cpu()\n    .numpy()\n)\n\nwrong_rgb_image = np.clip(\n    wrong_rgb_image,\n    0,\n    1\n)\n\n# Create heatmap\nwrong_visualization = show_cam_on_image(\n    wrong_rgb_image,\n    wrong_grayscale_cam,\n    use_rgb=True\n)\n\n# Display\nplt.figure(figsize=(14, 6))\n\nplt.subplot(1, 2, 1)\nplt.imshow(wrong_rgb_image)\nplt.title(\n    f\"Original\\nTrue: {wrong_true_label.item()}\"\n)\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(wrong_visualization)\nplt.title(\n    f\"Grad-CAM\\nPredicted: {wrong_predicted_label}\"\n)\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:33:21.912082Z","iopub.execute_input":"2026-08-26T14:33:21.912543Z","iopub.status.idle":"2026-08-26T14:33:22.662032Z","shell.execute_reply.started":"2026-08-26T14:33:21.912511Z","shell.execute_reply":"2026-08-26T14:33:22.661313Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Find useful examples\n\n# 1. Correctly classified No DR\ncorrect_no_dr = np.where(\n    (test_labels == 0) &\n    (test_predictions == 0)\n)[0][0]\n\n# 2. Correctly classified DR\ncorrect_dr = np.where(\n    (test_labels >= 2) &\n    (test_predictions >= 2)\n)[0][0]\n\n# 3. Wrong prediction\nwrong_index = np.where(\n    test_labels != test_predictions\n)[0][0]\n\nindices = [\n    correct_no_dr,\n    correct_dr,\n    wrong_index\n]\n\nnames = [\n    \"Correct No DR\",\n    \"Correct Referable DR\",\n    \"Wrong Prediction\"\n]\n\nprint(\"Selected images:\")\n\nfor name, idx in zip(names, indices):\n    print(\n        f\"{name}: Index {idx} | \"\n        f\"True={test_labels[idx]} | \"\n        f\"Predicted={test_predictions[idx]}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:34:51.936979Z","iopub.execute_input":"2026-08-26T14:34:51.937705Z","iopub.status.idle":"2026-08-26T14:34:51.944833Z","shell.execute_reply.started":"2026-08-26T14:34:51.937672Z","shell.execute_reply":"2026-08-26T14:34:51.943925Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(\n    3, 2,\n    figsize=(12, 15)\n)\n\nfor row, (idx, name) in enumerate(zip(indices, names)):\n\n    # Get image\n    image, true_label = test_dataset[idx]\n\n    # Add batch dimension\n    input_tensor = image.unsqueeze(0).to(device)\n\n    # Prediction\n    model.eval()\n\n    with torch.no_grad():\n        output = model(input_tensor)\n\n        predicted_label = torch.argmax(\n            output,\n            dim=1\n        ).item()\n\n    # Grad-CAM target\n    targets = [\n        ClassifierOutputTarget(predicted_label)\n    ]\n\n    # Create CAM\n    cam = GradCAM(\n        model=model,\n        target_layers=[target_layer]\n    )\n\n    grayscale_cam = cam(\n        input_tensor=input_tensor,\n        targets=targets\n    )[0]\n\n    # Convert image\n    rgb_image = (\n        image\n        .permute(1, 2, 0)\n        .cpu()\n        .numpy()\n    )\n\n    rgb_image = np.clip(\n        rgb_image,\n        0,\n        1\n    )\n\n    # Create overlay\n    visualization = show_cam_on_image(\n        rgb_image,\n        grayscale_cam,\n        use_rgb=True\n    )\n\n    # Original\n    axes[row, 0].imshow(rgb_image)\n\n    axes[row, 0].set_title(\n        f\"{name}\\n\"\n        f\"True: {true_label.item()} | \"\n        f\"Predicted: {predicted_label}\"\n    )\n\n    axes[row, 0].axis(\"off\")\n\n    # Grad-CAM\n    axes[row, 1].imshow(visualization)\n\n    axes[row, 1].set_title(\n        f\"Grad-CAM\\n\"\n        f\"Predicted: {predicted_label}\"\n    )\n\n    axes[row, 1].axis(\"off\")\n\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:35:28.057138Z","iopub.execute_input":"2026-08-26T14:35:28.057637Z","iopub.status.idle":"2026-08-26T14:35:29.708492Z","shell.execute_reply.started":"2026-08-26T14:35:28.057608Z","shell.execute_reply":"2026-08-26T14:35:29.707326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import copy\n\n# Make a separate copy of our trained 5-class model\nordinal_model = copy.deepcopy(model)\n\n# Replace 5-class classifier with 4 ordinal outputs\nordinal_model.classifier[1] = nn.Linear(\n    ordinal_model.classifier[1].in_features,\n    4\n)\n\n# Move to GPU\nordinal_model = ordinal_model.to(device)\n\nprint(\"Ordinal model created!\")\nprint(ordinal_model.classifier)\nprint(\"Device:\", next(ordinal_model.parameters()).device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:36:16.372464Z","iopub.execute_input":"2026-08-26T14:36:16.372906Z","iopub.status.idle":"2026-08-26T14:36:16.490280Z","shell.execute_reply.started":"2026-08-26T14:36:16.372877Z","shell.execute_reply":"2026-08-26T14:36:16.489487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def ordinal_encode(grade):\n    return torch.tensor(\n        [\n            1.0 if grade > 0 else 0.0,\n            1.0 if grade > 1 else 0.0,\n            1.0 if grade > 2 else 0.0,\n            1.0 if grade > 3 else 0.0\n        ],\n        dtype=torch.float32\n    )\n\n\n# Test all five grades\nfor grade in range(5):\n    print(\n        f\"Grade {grade} →\",\n        ordinal_encode(grade)\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:36:40.376645Z","iopub.execute_input":"2026-08-26T14:36:40.377411Z","iopub.status.idle":"2026-08-26T14:36:40.392941Z","shell.execute_reply.started":"2026-08-26T14:36:40.377382Z","shell.execute_reply":"2026-08-26T14:36:40.392006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class OrdinalAPTOSDataset(Dataset):\n\n    def __init__(self, dataframe, image_folder, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.image_folder = image_folder\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, index):\n\n        row = self.dataframe.iloc[index]\n\n        image_name = row[\"id_code\"] + \".png\"\n\n        image_path = os.path.join(\n            self.image_folder,\n            image_name\n        )\n\n        image = Image.open(\n            image_path\n        ).convert(\"RGB\")\n\n        grade = int(row[\"diagnosis\"])\n\n        # Convert grade 0–4 into 4 ordinal targets\n        ordinal_label = ordinal_encode(grade)\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, ordinal_label\n\n\nprint(\"Ordinal Dataset class created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:37:05.412595Z","iopub.execute_input":"2026-08-26T14:37:05.413559Z","iopub.status.idle":"2026-08-26T14:37:05.420943Z","shell.execute_reply.started":"2026-08-26T14:37:05.413521Z","shell.execute_reply":"2026-08-26T14:37:05.420257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_folder = train_image_folder\n\nprint(\"Image folder:\", image_folder)\nprint(\"Image folder exists:\", os.path.exists(image_folder))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:38:03.811982Z","iopub.execute_input":"2026-08-26T14:38:03.812720Z","iopub.status.idle":"2026-08-26T14:38:03.817948Z","shell.execute_reply.started":"2026-08-26T14:38:03.812688Z","shell.execute_reply":"2026-08-26T14:38:03.816987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_train_dataset = OrdinalAPTOSDataset(\n    train_df,\n    image_folder,\n    train_transform\n)\n\nordinal_val_dataset = OrdinalAPTOSDataset(\n    val_df,\n    image_folder,\n    val_transform\n)\n\nordinal_test_dataset = OrdinalAPTOSDataset(\n    test_df,\n    image_folder,\n    val_transform\n)\n\nprint(\"Ordinal training dataset:\", len(ordinal_train_dataset))\nprint(\"Ordinal validation dataset:\", len(ordinal_val_dataset))\nprint(\"Ordinal test dataset:\", len(ordinal_test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:38:24.837132Z","iopub.execute_input":"2026-08-26T14:38:24.837617Z","iopub.status.idle":"2026-08-26T14:38:24.843859Z","shell.execute_reply.started":"2026-08-26T14:38:24.837586Z","shell.execute_reply":"2026-08-26T14:38:24.843120Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image, ordinal_label = ordinal_train_dataset[0]\n\nprint(\"Image shape:\", image.shape)\nprint(\"Ordinal label:\", ordinal_label)\nprint(\"Label shape:\", ordinal_label.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:38:53.641366Z","iopub.execute_input":"2026-08-26T14:38:53.641642Z","iopub.status.idle":"2026-08-26T14:38:53.700850Z","shell.execute_reply.started":"2026-08-26T14:38:53.641619Z","shell.execute_reply":"2026-08-26T14:38:53.700015Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_train_loader = DataLoader(\n    ordinal_train_dataset,\n    batch_size=16,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nordinal_val_loader = DataLoader(\n    ordinal_val_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nordinal_test_loader = DataLoader(\n    ordinal_test_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(\"Ordinal training batches:\", len(ordinal_train_loader))\nprint(\"Ordinal validation batches:\", len(ordinal_val_loader))\nprint(\"Ordinal test batches:\", len(ordinal_test_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:39:16.372030Z","iopub.execute_input":"2026-08-26T14:39:16.372677Z","iopub.status.idle":"2026-08-26T14:39:16.378895Z","shell.execute_reply.started":"2026-08-26T14:39:16.372646Z","shell.execute_reply":"2026-08-26T14:39:16.377886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images, ordinal_labels = next(\n    iter(ordinal_train_loader)\n)\n\nprint(\"Images shape:\", images.shape)\nprint(\"Ordinal labels shape:\", ordinal_labels.shape)\nprint(\"First 5 ordinal labels:\")\nprint(ordinal_labels[:5])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:39:40.311337Z","iopub.execute_input":"2026-08-26T14:39:40.311781Z","iopub.status.idle":"2026-08-26T14:39:44.476841Z","shell.execute_reply.started":"2026-08-26T14:39:40.311752Z","shell.execute_reply":"2026-08-26T14:39:44.476054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_criterion = nn.BCEWithLogitsLoss()\n\nprint(\"Ordinal loss created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:40:06.602721Z","iopub.execute_input":"2026-08-26T14:40:06.603188Z","iopub.status.idle":"2026-08-26T14:40:06.608176Z","shell.execute_reply.started":"2026-08-26T14:40:06.603153Z","shell.execute_reply":"2026-08-26T14:40:06.607592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Freeze all feature layers first\nfor param in ordinal_model.features.parameters():\n    param.requires_grad = False\n\n# Train the deeper feature blocks\nfor block in [5, 6, 7]:\n    for param in ordinal_model.features[block].parameters():\n        param.requires_grad = True\n\n# Train the new ordinal classifier\nfor param in ordinal_model.classifier.parameters():\n    param.requires_grad = True\n\n# Check\nfor i, layer in enumerate(ordinal_model.features):\n\n    trainable = any(\n        param.requires_grad\n        for param in layer.parameters()\n    )\n\n    print(\n        f\"Block {i}:\",\n        \"TRAINABLE 🔥\" if trainable\n        else \"FROZEN ❄️\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:40:26.771883Z","iopub.execute_input":"2026-08-26T14:40:26.772863Z","iopub.status.idle":"2026-08-26T14:40:26.782835Z","shell.execute_reply.started":"2026-08-26T14:40:26.772823Z","shell.execute_reply":"2026-08-26T14:40:26.781987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_optimizer = torch.optim.AdamW(\n    [\n        {\n            \"params\": ordinal_model.features[5:].parameters(),\n            \"lr\": 1e-5\n        },\n        {\n            \"params\": ordinal_model.classifier.parameters(),\n            \"lr\": 1e-4\n        }\n    ],\n    weight_decay=1e-4\n)\n\nprint(\"Ordinal optimizer ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:40:57.931506Z","iopub.execute_input":"2026-08-26T14:40:57.932288Z","iopub.status.idle":"2026-08-26T14:40:57.938678Z","shell.execute_reply.started":"2026-08-26T14:40:57.932259Z","shell.execute_reply":"2026-08-26T14:40:57.937758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_ordinal_one_epoch(\n    model,\n    loader,\n    criterion,\n    optimizer,\n    device\n):\n    model.train()\n\n    running_loss = 0.0\n    total = 0\n\n    for images, labels in loader:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        labels = labels.to(\n            device,\n            non_blocking=True\n        )\n\n        optimizer.zero_grad()\n\n        outputs = model(images)\n\n        loss = criterion(\n            outputs,\n            labels\n        )\n\n        loss.backward()\n\n        optimizer.step()\n\n        running_loss += (\n            loss.item() * images.size(0)\n        )\n\n        total += images.size(0)\n\n    epoch_loss = running_loss / total\n\n    return epoch_loss\n\n\ndef validate_ordinal_one_epoch(\n    model,\n    loader,\n    criterion,\n    device\n):\n    model.eval()\n\n    running_loss = 0.0\n    total = 0\n\n    with torch.no_grad():\n\n        for images, labels in loader:\n\n            images = images.to(\n                device,\n                non_blocking=True\n            )\n\n            labels = labels.to(\n                device,\n                non_blocking=True\n            )\n\n            outputs = model(images)\n\n            loss = criterion(\n                outputs,\n                labels\n            )\n\n            running_loss += (\n                loss.item() * images.size(0)\n            )\n\n            total += images.size(0)\n\n    epoch_loss = running_loss / total\n\n    return epoch_loss\n\n\nprint(\"Ordinal training functions ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:41:32.011106Z","iopub.execute_input":"2026-08-26T14:41:32.011675Z","iopub.status.idle":"2026-08-26T14:41:32.020007Z","shell.execute_reply.started":"2026-08-26T14:41:32.011643Z","shell.execute_reply":"2026-08-26T14:41:32.019102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    ordinal_optimizer,\n    T_max=10\n)\n\nprint(\"Ordinal scheduler ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T14:41:59.111979Z","iopub.execute_input":"2026-08-26T14:41:59.112771Z","iopub.status.idle":"2026-08-26T14:41:59.117127Z","shell.execute_reply.started":"2026-08-26T14:41:59.112739Z","shell.execute_reply":"2026-08-26T14:41:59.116279Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision import models\n\ndevice = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\n# Create a fresh EfficientNetV2-S\nordinal_model = models.efficientnet_v2_s(\n    weights=None\n)\n\n# Load our saved 5-class model\ncheckpoint = torch.load(\n    \"/kaggle/working/best_finetuned_aptos.pth\",\n    map_location=\"cpu\"\n)\n\n# Remove the old 5-class classifier\ncheckpoint = {\n    k: v\n    for k, v in checkpoint.items()\n    if not k.startswith(\"classifier.\")\n}\n\n# Load the trained feature extractor\nordinal_model.load_state_dict(\n    checkpoint,\n    strict=False\n)\n\n# Replace 5-class classifier with 4 ordinal outputs\nordinal_model.classifier[1] = nn.Linear(\n    ordinal_model.classifier[1].in_features,\n    4\n)\n\nordinal_model = ordinal_model.to(device)\n\nprint(\"Ordinal model restored safely! ✅\")\nprint(ordinal_model.classifier)\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:06:48.406955Z","iopub.execute_input":"2026-08-26T15:06:48.407364Z","iopub.status.idle":"2026-08-26T15:06:49.049420Z","shell.execute_reply.started":"2026-08-26T15:06:48.407338Z","shell.execute_reply":"2026-08-26T15:06:49.048656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\n\n# APTOS paths\nbase_path = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\ntrain_csv = os.path.join(\n    base_path,\n    \"train.csv\"\n)\n\nimage_folder = os.path.join(\n    base_path,\n    \"train_images\"\n)\n\n# Load dataset\ndf = pd.read_csv(train_csv)\n\nprint(\"Total images:\", len(df))\n\n# First split: 2562 train + 1100 remaining\ntrain_df, remaining_df = train_test_split(\n    df,\n    test_size=1100,\n    stratify=df[\"diagnosis\"],\n    random_state=42\n)\n\n# Second split: 550 validation + 550 test\nval_df, test_df = train_test_split(\n    remaining_df,\n    test_size=550,\n    stratify=remaining_df[\"diagnosis\"],\n    random_state=42\n)\n\nprint(\"Training images:\", len(train_df))\nprint(\"Validation images:\", len(val_df))\nprint(\"Test images:\", len(test_df))\n\nprint(\"\\nTraining distribution:\")\nprint(train_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nValidation distribution:\")\nprint(val_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nTest distribution:\")\nprint(test_df[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:07:32.709792Z","iopub.execute_input":"2026-08-26T15:07:32.710440Z","iopub.status.idle":"2026-08-26T15:07:34.755562Z","shell.execute_reply.started":"2026-08-26T15:07:32.710404Z","shell.execute_reply":"2026-08-26T15:07:34.754646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class OrdinalAPTOSDataset(torch.utils.data.Dataset):\n\n    def __init__(\n        self,\n        dataframe,\n        image_folder,\n        transform=None\n    ):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.image_folder = image_folder\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, index):\n\n        row = self.dataframe.iloc[index]\n\n        image_name = row[\"id_code\"] + \".png\"\n\n        image_path = os.path.join(\n            self.image_folder,\n            image_name\n        )\n\n        image = Image.open(\n            image_path\n        ).convert(\"RGB\")\n\n        grade = int(row[\"diagnosis\"])\n\n        # Grade 0–4 → 4 ordinal targets\n        ordinal_label = torch.tensor(\n            [\n                1.0 if grade > 0 else 0.0,\n                1.0 if grade > 1 else 0.0,\n                1.0 if grade > 2 else 0.0,\n                1.0 if grade > 3 else 0.0\n            ],\n            dtype=torch.float32\n        )\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, ordinal_label\n\n\nprint(\"Ordinal Dataset class recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:10:39.489657Z","iopub.execute_input":"2026-08-26T15:10:39.490139Z","iopub.status.idle":"2026-08-26T15:10:39.498244Z","shell.execute_reply.started":"2026-08-26T15:10:39.490107Z","shell.execute_reply":"2026-08-26T15:10:39.497105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\n\nIMG_SIZE = 512\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(10),\n    transforms.ToTensor(),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n])\n\nprint(\"Image transformations recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:11:05.330155Z","iopub.execute_input":"2026-08-26T15:11:05.330573Z","iopub.status.idle":"2026-08-26T15:11:05.336548Z","shell.execute_reply.started":"2026-08-26T15:11:05.330545Z","shell.execute_reply":"2026-08-26T15:11:05.335692Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_train_dataset = OrdinalAPTOSDataset(\n    train_df,\n    image_folder,\n    train_transform\n)\n\nordinal_val_dataset = OrdinalAPTOSDataset(\n    val_df,\n    image_folder,\n    val_transform\n)\n\nordinal_test_dataset = OrdinalAPTOSDataset(\n    test_df,\n    image_folder,\n    val_transform\n)\n\nprint(\"Ordinal training dataset:\", len(ordinal_train_dataset))\nprint(\"Ordinal validation dataset:\", len(ordinal_val_dataset))\nprint(\"Ordinal test dataset:\", len(ordinal_test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:11:33.385340Z","iopub.execute_input":"2026-08-26T15:11:33.385835Z","iopub.status.idle":"2026-08-26T15:11:33.392460Z","shell.execute_reply.started":"2026-08-26T15:11:33.385802Z","shell.execute_reply":"2026-08-26T15:11:33.391567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\nprint(\"DataLoader imported! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:12:39.779096Z","iopub.execute_input":"2026-08-26T15:12:39.779673Z","iopub.status.idle":"2026-08-26T15:12:39.784045Z","shell.execute_reply.started":"2026-08-26T15:12:39.779643Z","shell.execute_reply":"2026-08-26T15:12:39.783017Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_train_loader = DataLoader(\n    ordinal_train_dataset,\n    batch_size=16,\n    shuffle=True,\n    num_workers=0,\n    pin_memory=True\n)\n\nordinal_val_loader = DataLoader(\n    ordinal_val_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=0,\n    pin_memory=True\n)\n\nordinal_test_loader = DataLoader(\n    ordinal_test_dataset,\n    batch_size=16,\n    shuffle=False,\n    num_workers=0,\n    pin_memory=True\n)\n\nprint(\"Ordinal training batches:\", len(ordinal_train_loader))\nprint(\"Ordinal validation batches:\", len(ordinal_val_loader))\nprint(\"Ordinal test batches:\", len(ordinal_test_loader))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:13:03.924634Z","iopub.execute_input":"2026-08-26T15:13:03.924928Z","iopub.status.idle":"2026-08-26T15:13:03.931602Z","shell.execute_reply.started":"2026-08-26T15:13:03.924905Z","shell.execute_reply":"2026-08-26T15:13:03.930732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nordinal_criterion = nn.BCEWithLogitsLoss()\n\nprint(\"Ordinal loss recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:13:41.058897Z","iopub.execute_input":"2026-08-26T15:13:41.059702Z","iopub.status.idle":"2026-08-26T15:13:41.064977Z","shell.execute_reply.started":"2026-08-26T15:13:41.059669Z","shell.execute_reply":"2026-08-26T15:13:41.064168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_optimizer = torch.optim.AdamW(\n    [\n        {\n            \"params\": ordinal_model.features[5:].parameters(),\n            \"lr\": 1e-5\n        },\n        {\n            \"params\": ordinal_model.classifier.parameters(),\n            \"lr\": 1e-4\n        }\n    ],\n    weight_decay=1e-4\n)\n\nprint(\"Ordinal optimizer recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:14:25.620352Z","iopub.execute_input":"2026-08-26T15:14:25.621369Z","iopub.status.idle":"2026-08-26T15:14:25.628653Z","shell.execute_reply.started":"2026-08-26T15:14:25.621324Z","shell.execute_reply":"2026-08-26T15:14:25.627864Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n    ordinal_optimizer,\n    T_max=10\n)\n\nprint(\"Ordinal scheduler recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:14:52.702134Z","iopub.execute_input":"2026-08-26T15:14:52.702454Z","iopub.status.idle":"2026-08-26T15:14:52.714856Z","shell.execute_reply.started":"2026-08-26T15:14:52.702432Z","shell.execute_reply":"2026-08-26T15:14:52.714248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_ordinal_one_epoch(\n    model,\n    loader,\n    criterion,\n    optimizer,\n    device\n):\n    model.train()\n\n    running_loss = 0.0\n    total = 0\n\n    for images, labels in loader:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        labels = labels.to(\n            device,\n            non_blocking=True\n        )\n\n        optimizer.zero_grad(set_to_none=True)\n\n        outputs = model(images)\n\n        loss = criterion(\n            outputs,\n            labels\n        )\n\n        loss.backward()\n\n        optimizer.step()\n\n        running_loss += (\n            loss.item() * images.size(0)\n        )\n\n        total += images.size(0)\n\n    return running_loss / total\n\n\ndef validate_ordinal_one_epoch(\n    model,\n    loader,\n    criterion,\n    device\n):\n    model.eval()\n\n    running_loss = 0.0\n    total = 0\n\n    with torch.no_grad():\n\n        for images, labels in loader:\n\n            images = images.to(\n                device,\n                non_blocking=True\n            )\n\n            labels = labels.to(\n                device,\n                non_blocking=True\n            )\n\n            outputs = model(images)\n\n            loss = criterion(\n                outputs,\n                labels\n            )\n\n            running_loss += (\n                loss.item() * images.size(0)\n            )\n\n            total += images.size(0)\n\n    return running_loss / total\n\n\nprint(\"Ordinal training functions recreated! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:15:27.200930Z","iopub.execute_input":"2026-08-26T15:15:27.201651Z","iopub.status.idle":"2026-08-26T15:15:27.209617Z","shell.execute_reply.started":"2026-08-26T15:15:27.201616Z","shell.execute_reply":"2026-08-26T15:15:27.208771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\nprint(\"GPU:\", torch.cuda.get_device_name(0))\n\nfree_memory, total_memory = torch.cuda.mem_get_info()\n\nprint(\n    f\"Free GPU memory: \"\n    f\"{free_memory / 1024**3:.2f} GB\"\n)\n\nprint(\n    f\"Total GPU memory: \"\n    f\"{total_memory / 1024**3:.2f} GB\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:16:24.851627Z","iopub.execute_input":"2026-08-26T15:16:24.852429Z","iopub.status.idle":"2026-08-26T15:16:24.858652Z","shell.execute_reply.started":"2026-08-26T15:16:24.852394Z","shell.execute_reply":"2026-08-26T15:16:24.857722Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\n\nprint(\"PIL Image imported! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:17:43.915003Z","iopub.execute_input":"2026-08-26T15:17:43.915885Z","iopub.status.idle":"2026-08-26T15:17:43.920281Z","shell.execute_reply.started":"2026-08-26T15:17:43.915852Z","shell.execute_reply":"2026-08-26T15:17:43.919453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_epochs = 10\n\nbest_ordinal_val_loss = float(\"inf\")\npatience = 3\npatience_counter = 0\nbest_ordinal_epoch = 0\n\nfor epoch in range(num_epochs):\n\n    train_loss = train_ordinal_one_epoch(\n        ordinal_model,\n        ordinal_train_loader,\n        ordinal_criterion,\n        ordinal_optimizer,\n        device\n    )\n\n    val_loss = validate_ordinal_one_epoch(\n        ordinal_model,\n        ordinal_val_loader,\n        ordinal_criterion,\n        device\n    )\n\n    ordinal_scheduler.step()\n\n    if val_loss < best_ordinal_val_loss:\n\n        best_ordinal_val_loss = val_loss\n        patience_counter = 0\n        best_ordinal_epoch = epoch + 1\n\n        torch.save(\n            ordinal_model.state_dict(),\n            \"/kaggle/working/best_ordinal_aptos.pth\"\n        )\n\n        print(\"  ⭐ Best ordinal model saved!\")\n\n    else:\n        patience_counter += 1\n\n    print(\n        f\"Epoch {epoch + 1}/{num_epochs} | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Val Loss: {val_loss:.4f}\"\n    )\n\n    if patience_counter >= patience:\n        print(\"Early stopping triggered.\")\n        break\n\nprint(\"\\nBest ordinal epoch:\", best_ordinal_epoch)\nprint(\"Best ordinal validation loss:\", best_ordinal_val_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T15:18:17.167320Z","iopub.execute_input":"2026-08-26T15:18:17.168149Z","iopub.status.idle":"2026-08-26T16:49:52.639689Z","shell.execute_reply.started":"2026-08-26T15:18:17.168115Z","shell.execute_reply":"2026-08-26T16:49:52.638796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ordinal_model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_ordinal_aptos.pth\",\n        map_location=device\n    )\n)\n\nordinal_model.eval()\n\nprint(\"Best ordinal model loaded! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T16:57:30.671647Z","iopub.execute_input":"2026-08-26T16:57:30.672370Z","iopub.status.idle":"2026-08-26T16:57:30.941664Z","shell.execute_reply.started":"2026-08-26T16:57:30.672336Z","shell.execute_reply":"2026-08-26T16:57:30.940662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import (\n    accuracy_score,\n    classification_report,\n    confusion_matrix\n)\n\nordinal_model.eval()\n\nordinal_predictions = []\nordinal_actuals = []\n\nwith torch.no_grad():\n\n    for images, labels in ordinal_test_loader:\n\n        images = images.to(\n            device,\n            non_blocking=True\n        )\n\n        outputs = ordinal_model(images)\n\n        # Convert logits → probabilities\n        probabilities = torch.sigmoid(outputs)\n\n        # Each output > 0.5 means that threshold is crossed\n        predicted_ordinal = (\n            probabilities >= 0.5\n        ).int()\n\n        # Number of crossed thresholds = predicted DR grade\n        predicted_grade = (\n            predicted_ordinal.sum(dim=1)\n        )\n\n        # Recover true grade\n        true_grade = labels.sum(dim=1)\n\n        ordinal_predictions.extend(\n            predicted_grade.cpu().numpy()\n        )\n\n        ordinal_actuals.extend(\n            true_grade.cpu().numpy()\n        )\n\nordinal_predictions = np.array(\n    ordinal_predictions\n)\n\nordinal_actuals = np.array(\n    ordinal_actuals\n)\n\nprint(\n    \"Number of predictions:\",\n    len(ordinal_predictions)\n)\n\nprint(\n    \"Number of actual labels:\",\n    len(ordinal_actuals)\n)\n\nprint(\n    \"Ordinal Test Accuracy:\",\n    accuracy_score(\n        ordinal_actuals,\n        ordinal_predictions\n    )\n)\n\nprint(\"\\nOrdinal Classification Report:\")\n\nprint(\n    classification_report(\n        ordinal_actuals,\n        ordinal_predictions,\n        target_names=[\n            \"No DR\",\n            \"Mild\",\n            \"Moderate\",\n            \"Severe\",\n            \"Proliferative\"\n        ],\n        zero_division=0\n    )\n)\n\nprint(\"\\nOrdinal Confusion Matrix:\")\n\nprint(\n    confusion_matrix(\n        ordinal_actuals,\n        ordinal_predictions\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T16:57:55.760046Z","iopub.execute_input":"2026-08-26T16:57:55.760767Z","iopub.status.idle":"2026-08-26T16:59:26.389822Z","shell.execute_reply.started":"2026-08-26T16:57:55.760732Z","shell.execute_reply":"2026-08-26T16:59:26.389030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\n\n# Convert 5-class predictions to binary Referable DR\nordinal_true_referable = (\n    ordinal_actuals >= 2\n).astype(int)\n\nordinal_pred_referable = (\n    ordinal_predictions >= 2\n).astype(int)\n\n# Confusion matrix\nordinal_ref_cm = confusion_matrix(\n    ordinal_true_referable,\n    ordinal_pred_referable\n)\n\nprint(\"Ordinal Referable DR Confusion Matrix:\")\nprint(ordinal_ref_cm)\n\n# Extract TN, FP, FN, TP\nTN, FP, FN, TP = ordinal_ref_cm.ravel()\n\n# Sensitivity\nordinal_sensitivity = TP / (TP + FN)\n\n# Specificity\nordinal_specificity = TN / (TN + FP)\n\nprint()\nprint(\n    f\"Ordinal Sensitivity: \"\n    f\"{ordinal_sensitivity:.4f}\"\n)\n\nprint(\n    f\"Ordinal Specificity: \"\n    f\"{ordinal_specificity:.4f}\"\n)\n\nprint()\nprint(\n    f\"Ordinal Sensitivity: \"\n    f\"{ordinal_sensitivity * 100:.2f}%\"\n)\n\nprint(\n    f\"Ordinal Specificity: \"\n    f\"{ordinal_specificity * 100:.2f}%\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:00:13.550662Z","iopub.execute_input":"2026-08-26T17:00:13.551599Z","iopub.status.idle":"2026-08-26T17:00:13.560608Z","shell.execute_reply.started":"2026-08-26T17:00:13.551565Z","shell.execute_reply":"2026-08-26T17:00:13.559635Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nordinal_model_path = \"/kaggle/working/best_ordinal_aptos.pth\"\n\nprint(\"File exists:\", os.path.exists(ordinal_model_path))\n\nif os.path.exists(ordinal_model_path):\n    size_mb = os.path.getsize(ordinal_model_path) / (1024 ** 2)\n    print(f\"Model size: {size_mb:.2f} MB\")\n    print(\"Ordinal model saved safely! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:01:01.110551Z","iopub.execute_input":"2026-08-26T17:01:01.110870Z","iopub.status.idle":"2026-08-26T17:01:01.116577Z","shell.execute_reply.started":"2026-08-26T17:01:01.110846Z","shell.execute_reply":"2026-08-26T17:01:01.115611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ncomparison = pd.DataFrame({\n    \"Metric\": [\n        \"Accuracy\",\n        \"Macro F1\",\n        \"Referable Sensitivity\",\n        \"Referable Specificity\"\n    ],\n    \"5-Class EfficientNetV2-S\": [\n        80.91,\n        68.54,\n        86.55,\n        95.72\n    ],\n    \"Ordinal EfficientNetV2-S\": [\n        81.27,\n        65.00,\n        95.07,\n        91.74\n    ]\n})\n\nprint(comparison.to_string(index=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:02:14.761909Z","iopub.execute_input":"2026-08-26T17:02:14.762602Z","iopub.status.idle":"2026-08-26T17:02:14.776065Z","shell.execute_reply.started":"2026-08-26T17:02:14.762567Z","shell.execute_reply":"2026-08-26T17:02:14.775132Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Original 5-class model\noriginal_cm = np.array([\n    [266, 5, 0, 0, 0],\n    [7, 35, 12, 0, 2],\n    [7, 18, 98, 17, 10],\n    [0, 1, 2, 21, 5],\n    [1, 3, 7, 8, 25]\n])\n\n# Ordinal model\nordinal_cm = np.array([\n    [261, 8, 2, 0, 0],\n    [7, 24, 24, 1, 0],\n    [1, 8, 125, 10, 6],\n    [0, 0, 12, 11, 6],\n    [0, 2, 10, 6, 26]\n])\n\nclasses = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative\"\n]\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\n\naxes[0].imshow(original_cm)\naxes[0].set_title(\"5-Class Model\")\naxes[0].set_xlabel(\"Predicted\")\naxes[0].set_ylabel(\"Actual\")\naxes[0].set_xticks(range(5), classes, rotation=45)\naxes[0].set_yticks(range(5), classes)\n\naxes[1].imshow(ordinal_cm)\naxes[1].set_title(\"Ordinal Model\")\naxes[1].set_xlabel(\"Predicted\")\naxes[1].set_ylabel(\"Actual\")\naxes[1].set_xticks(range(5), classes, rotation=45)\naxes[1].set_yticks(range(5), classes)\n\nfor ax, cm in zip(axes, [original_cm, ordinal_cm]):\n    for i in range(5):\n        for j in range(5):\n            ax.text(j, i, cm[i, j],\n                    ha=\"center\",\n                    va=\"center\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:03:09.935835Z","iopub.execute_input":"2026-08-26T17:03:09.936350Z","iopub.status.idle":"2026-08-26T17:03:10.211823Z","shell.execute_reply.started":"2026-08-26T17:03:09.936315Z","shell.execute_reply":"2026-08-26T17:03:10.211165Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nfiles = [\n    \"/kaggle/working/best_finetuned_aptos.pth\",\n    \"/kaggle/working/best_ordinal_aptos.pth\"\n]\n\nfor file in files:\n\n    exists = os.path.exists(file)\n\n    print(\"\\nFile:\", os.path.basename(file))\n    print(\"Exists:\", exists)\n\n    if exists:\n        size = os.path.getsize(file) / (1024 ** 2)\n        print(f\"Size: {size:.2f} MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:03:32.796867Z","iopub.execute_input":"2026-08-26T17:03:32.797756Z","iopub.status.idle":"2026-08-26T17:03:32.803363Z","shell.execute_reply.started":"2026-08-26T17:03:32.797721Z","shell.execute_reply":"2026-08-26T17:03:32.802508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nfinal_results = {\n    \"dataset\": \"APTOS 2019 Blindness Detection\",\n    \"total_images\": 3662,\n    \"train_images\": 2562,\n    \"validation_images\": 550,\n    \"test_images\": 550,\n\n    \"five_class_model\": {\n        \"accuracy\": 0.8091,\n        \"macro_f1\": 0.6854,\n        \"referable_sensitivity\": 0.8655,\n        \"referable_specificity\": 0.9572\n    },\n\n    \"ordinal_model\": {\n        \"accuracy\": 0.8127,\n        \"macro_f1\": 0.6500,\n        \"referable_sensitivity\": 0.9507,\n        \"referable_specificity\": 0.9174\n    }\n}\n\nwith open(\n    \"/kaggle/working/final_results.json\",\n    \"w\"\n) as f:\n    json.dump(final_results, f, indent=4)\n\nprint(\"Final results saved! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:04:37.076765Z","iopub.execute_input":"2026-08-26T17:04:37.077492Z","iopub.status.idle":"2026-08-26T17:04:37.085950Z","shell.execute_reply.started":"2026-08-26T17:04:37.077452Z","shell.execute_reply":"2026-08-26T17:04:37.085303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import efficientnet_v2_s\n\n# Device\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Recreate EfficientNetV2-S\nmodel = efficientnet_v2_s(weights=None)\n\n# Replace classifier with our 5-class DR classifier\nin_features = model.classifier[1].in_features\n\nmodel.classifier[1] = nn.Linear(\n    in_features,\n    5\n)\n\nmodel = model.to(device)\n\nprint(\"5-class model architecture recreated! ✅\")\nprint(model.classifier)\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:05:32.595875Z","iopub.execute_input":"2026-08-26T17:05:32.596373Z","iopub.status.idle":"2026-08-26T17:05:32.938933Z","shell.execute_reply.started":"2026-08-26T17:05:32.596339Z","shell.execute_reply":"2026-08-26T17:05:32.938139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(\n    torch.load(\n        \"/kaggle/working/best_finetuned_aptos.pth\",\n        map_location=device\n    )\n)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Final 5-class model loaded! ✅\")\nprint(\"Model ready for prediction!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:06:58.411402Z","iopub.execute_input":"2026-08-26T17:06:58.412334Z","iopub.status.idle":"2026-08-26T17:06:58.658565Z","shell.execute_reply.started":"2026-08-26T17:06:58.412302Z","shell.execute_reply":"2026-08-26T17:06:58.657613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom PIL import Image\n\nclass_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative\"\n]\n\ndef predict_dr(image_path):\n\n    # Open image\n    image = Image.open(image_path).convert(\"RGB\")\n\n    # Apply the same validation transformation used during testing\n    image_tensor = val_transform(image)\n\n    # Add batch dimension\n    image_tensor = image_tensor.unsqueeze(0).to(device)\n\n    # Prediction\n    with torch.no_grad():\n        output = model(image_tensor)\n        probabilities = torch.softmax(output, dim=1)\n\n    # Get predicted class\n    predicted_class = torch.argmax(probabilities, dim=1).item()\n\n    confidence = probabilities[0, predicted_class].item()\n\n    # Referable DR = grades 2, 3, 4\n    referable = predicted_class >= 2\n\n    print(\"Prediction Results\")\n    print(\"------------------\")\n    print(\"DR Grade:\", predicted_class)\n    print(\"Diagnosis:\", class_names[predicted_class])\n    print(f\"Confidence: {confidence * 100:.2f}%\")\n\n    if referable:\n        print(\"Referable DR: YES ⚠️\")\n    else:\n        print(\"Referable DR: NO ✅\")\n\n    return predicted_class, confidence","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:11:02.241750Z","iopub.execute_input":"2026-08-26T17:11:02.242705Z","iopub.status.idle":"2026-08-26T17:11:02.249046Z","shell.execute_reply.started":"2026-08-26T17:11:02.242670Z","shell.execute_reply":"2026-08-26T17:11:02.248080Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_image_path = os.path.join(\n    image_folder,\n    test_df.iloc[0][\"id_code\"] + \".png\"\n)\n\nprint(\"Testing image:\")\nprint(test_image_path)\n\npredict_dr(test_image_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:11:45.171663Z","iopub.execute_input":"2026-08-26T17:11:45.172783Z","iopub.status.idle":"2026-08-26T17:11:45.374931Z","shell.execute_reply.started":"2026-08-26T17:11:45.172734Z","shell.execute_reply":"2026-08-26T17:11:45.374297Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\n\nprint(\"Grad-CAM ready for final prediction! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:12:33.422407Z","iopub.execute_input":"2026-08-26T17:12:33.422926Z","iopub.status.idle":"2026-08-26T17:12:33.746751Z","shell.execute_reply.started":"2026-08-26T17:12:33.422894Z","shell.execute_reply":"2026-08-26T17:12:33.746043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.image import show_cam_on_image\n\n# Use the target layer we selected earlier\ntarget_layers = [model.features[-1]]\n\n# Load the same test image\nimage = Image.open(test_image_path).convert(\"RGB\")\n\n# Prepare image\nimage_np = np.array(image.resize((512, 512))) / 255.0\n\ninput_tensor = val_transform(image).unsqueeze(0).to(device)\n\n# Grad-CAM\ncam = GradCAM(\n    model=model,\n    target_layers=target_layers\n)\n\ntargets = [\n    ClassifierOutputTarget(\n        int(torch.argmax(model(input_tensor), dim=1).item())\n    )\n]\n\ngrayscale_cam = cam(\n    input_tensor=input_tensor,\n    targets=targets\n)[0]\n\n# Overlay Grad-CAM\nvisualization = show_cam_on_image(\n    image_np.astype(np.float32),\n    grayscale_cam,\n    use_rgb=True\n)\n\n# Display\nplt.figure(figsize=(12, 5))\n\nplt.subplot(1, 2, 1)\nplt.imshow(image_np)\nplt.title(\"Original Retinal Image\")\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(visualization)\nplt.title(\"Grad-CAM Explanation\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:13:37.222008Z","iopub.execute_input":"2026-08-26T17:13:37.223629Z","iopub.status.idle":"2026-08-26T17:13:37.856743Z","shell.execute_reply.started":"2026-08-26T17:13:37.223591Z","shell.execute_reply":"2026-08-26T17:13:37.855914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport torch\nfrom PIL import Image\n\n# -----------------------------------\n# 1. Load image\n# -----------------------------------\n\nimage = Image.open(test_image_path).convert(\"RGB\")\n\n# Original image for display\ndisplay_image = image.resize((512, 512))\ndisplay_np = np.array(display_image) / 255.0\n\n# -----------------------------------\n# 2. Model prediction\n# -----------------------------------\n\ninput_tensor = val_transform(image).unsqueeze(0).to(device)\n\nmodel.eval()\n\nwith torch.no_grad():\n    output = model(input_tensor)\n    probabilities = torch.softmax(output, dim=1)\n\npredicted_class = torch.argmax(\n    probabilities,\n    dim=1\n).item()\n\nconfidence = probabilities[0, predicted_class].item()\n\nclass_names = [\n    \"No DR\",\n    \"Mild DR\",\n    \"Moderate DR\",\n    \"Severe DR\",\n    \"Proliferative DR\"\n]\n\npredicted_name = class_names[predicted_class]\n\n# Referable DR = Moderate, Severe or Proliferative\nreferable = predicted_class >= 2\n\n# -----------------------------------\n# 3. Grad-CAM\n# -----------------------------------\n\ntarget_layers = [model.features[-1]]\n\ncam = GradCAM(\n    model=model,\n    target_layers=target_layers\n)\n\ntargets = [\n    ClassifierOutputTarget(predicted_class)\n]\n\ngrayscale_cam = cam(\n    input_tensor=input_tensor,\n    targets=targets\n)[0]\n\nvisualization = show_cam_on_image(\n    display_np.astype(np.float32),\n    grayscale_cam,\n    use_rgb=True\n)\n\n# -----------------------------------\n# 4. Display final result\n# -----------------------------------\n\nplt.figure(figsize=(14, 7))\n\nplt.subplot(1, 2, 1)\nplt.imshow(display_np)\nplt.title(\"Original Retinal Image\", fontsize=14)\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(visualization)\nplt.title(\"Grad-CAM Explanation\", fontsize=14)\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n\n# -----------------------------------\n# 5. Final screening result\n# -----------------------------------\n\nprint(\"=\" * 50)\nprint(\"       DIABETIC RETINOPATHY SCREENING\")\nprint(\"=\" * 50)\n\nprint(f\"Predicted Grade : {predicted_class}\")\nprint(f\"Diagnosis       : {predicted_name}\")\nprint(f\"Confidence      : {confidence * 100:.2f}%\")\n\nprint(\"-\" * 50)\n\nif referable:\n    print(\"Referable DR    : YES ⚠️\")\n    print(\"Recommendation  : Further ophthalmic evaluation\")\nelse:\n    print(\"Referable DR    : NO ✅\")\n    print(\"Recommendation  : Routine screening follow-up\")\n\nprint(\"=\" * 50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:14:39.899054Z","iopub.execute_input":"2026-08-26T17:14:39.899436Z","iopub.status.idle":"2026-08-26T17:14:40.676045Z","shell.execute_reply.started":"2026-08-26T17:14:39.899406Z","shell.execute_reply":"2026-08-26T17:14:40.675258Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Test DataFrame columns:\")\nprint(test_df.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:16:33.181281Z","iopub.execute_input":"2026-08-26T17:16:33.181995Z","iopub.status.idle":"2026-08-26T17:16:33.186097Z","shell.execute_reply.started":"2026-08-26T17:16:33.181963Z","shell.execute_reply":"2026-08-26T17:16:33.185143Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# STEP 71B — Test final model on 3 cases\n\nimport os\n\nselected_indices = [4, 0, 3]\n\nfor idx in selected_indices:\n\n    # Get image filename\n    image_name = test_df.iloc[idx][\"id_code\"] + \".png\"\n\n    # Build complete path\n    image_path = os.path.join(\n        image_folder,\n        image_name\n    )\n\n    # True label\n    true_label = int(\n        test_df.iloc[idx][\"diagnosis\"]\n    )\n\n    # Load image\n    image = Image.open(\n        image_path\n    ).convert(\"RGB\")\n\n    # Prepare image\n    input_tensor = val_transform(\n        image\n    ).unsqueeze(0).to(device)\n\n    # Prediction\n    model.eval()\n\n    with torch.no_grad():\n\n        output = model(\n            input_tensor\n        )\n\n        probabilities = torch.softmax(\n            output,\n            dim=1\n        )\n\n    predicted_class = torch.argmax(\n        probabilities,\n        dim=1\n    ).item()\n\n    confidence = probabilities[\n        0,\n        predicted_class\n    ].item()\n\n    # --------------------------------\n    # RESULT\n    # --------------------------------\n\n    print(\"=\" * 55)\n\n    print(f\"Image index     : {idx}\")\n    print(f\"Image name      : {image_name}\")\n    print(f\"True label      : {true_label}\")\n    print(f\"Predicted label : {predicted_class}\")\n    print(f\"Diagnosis       : {class_names[predicted_class]}\")\n    print(f\"Confidence      : {confidence * 100:.2f}%\")\n\n    if predicted_class >= 2:\n        print(\"Referable DR    : YES ⚠️\")\n    else:\n        print(\"Referable DR    : NO ✅\")\n\nprint(\"=\" * 55)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:17:18.756621Z","iopub.execute_input":"2026-08-26T17:17:18.757436Z","iopub.status.idle":"2026-08-26T17:17:19.285507Z","shell.execute_reply.started":"2026-08-26T17:17:18.757399Z","shell.execute_reply":"2026-08-26T17:17:19.284763Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# STEP 72 — Final DR Screening Function\n\ndef screen_retina(image_path):\n\n    # -----------------------------\n    # 1. Load image\n    # -----------------------------\n    image = Image.open(image_path).convert(\"RGB\")\n\n    # -----------------------------\n    # 2. Prepare image\n    # -----------------------------\n    input_tensor = val_transform(\n        image\n    ).unsqueeze(0).to(device)\n\n    # -----------------------------\n    # 3. Prediction\n    # -----------------------------\n    model.eval()\n\n    with torch.no_grad():\n\n        output = model(input_tensor)\n\n        probabilities = torch.softmax(\n            output,\n            dim=1\n        )\n\n    predicted_class = torch.argmax(\n        probabilities,\n        dim=1\n    ).item()\n\n    confidence = probabilities[\n        0,\n        predicted_class\n    ].item()\n\n    # -----------------------------\n    # 4. Diagnosis\n    # -----------------------------\n    class_names = [\n        \"No DR\",\n        \"Mild DR\",\n        \"Moderate DR\",\n        \"Severe DR\",\n        \"Proliferative DR\"\n    ]\n\n    diagnosis = class_names[predicted_class]\n\n    # -----------------------------\n    # 5. Referable DR\n    # -----------------------------\n    referable = predicted_class >= 2\n\n    # -----------------------------\n    # 6. Print result\n    # -----------------------------\n    print(\"=\" * 60)\n    print(\"        DIABETIC RETINOPATHY SCREENING\")\n    print(\"=\" * 60)\n\n    print(f\"Image           : {os.path.basename(image_path)}\")\n    print(f\"DR Grade        : {predicted_class}\")\n    print(f\"Diagnosis       : {diagnosis}\")\n    print(f\"Confidence      : {confidence * 100:.2f}%\")\n\n    print(\"-\" * 60)\n\n    if referable:\n        print(\"Referable DR    : YES ⚠️\")\n        print(\"Recommendation  : Further ophthalmic evaluation\")\n    else:\n        print(\"Referable DR    : NO ✅\")\n        print(\"Recommendation  : Routine screening follow-up\")\n\n    print(\"=\" * 60)\n\n    return predicted_class, confidence","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:18:54.936747Z","iopub.execute_input":"2026-08-26T17:18:54.937305Z","iopub.status.idle":"2026-08-26T17:18:54.944515Z","shell.execute_reply.started":"2026-08-26T17:18:54.937268Z","shell.execute_reply":"2026-08-26T17:18:54.943662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_image = \"/kaggle/input/competitions/aptos2019-blindness-detection/train_images/9b418ce42c13.png\"\n\nresult = screen_retina(test_image)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:20:02.221550Z","iopub.execute_input":"2026-08-26T17:20:02.222350Z","iopub.status.idle":"2026-08-26T17:20:02.320465Z","shell.execute_reply.started":"2026-08-26T17:20:02.222311Z","shell.execute_reply":"2026-08-26T17:20:02.319778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def screen_retina_with_explanation(image_path):\n\n    # -----------------------------\n    # 1. Load image\n    # -----------------------------\n    image = Image.open(image_path).convert(\"RGB\")\n\n    # -----------------------------\n    # 2. Prepare image\n    # -----------------------------\n    input_tensor = val_transform(\n        image\n    ).unsqueeze(0).to(device)\n\n    model.eval()\n\n    # -----------------------------\n    # 3. Prediction\n    # -----------------------------\n    with torch.no_grad():\n\n        output = model(input_tensor)\n\n        probabilities = torch.softmax(\n            output,\n            dim=1\n        )\n\n    predicted_class = torch.argmax(\n        probabilities,\n        dim=1\n    ).item()\n\n    confidence = probabilities[\n        0,\n        predicted_class\n    ].item()\n\n    class_names = [\n        \"No DR\",\n        \"Mild DR\",\n        \"Moderate DR\",\n        \"Severe DR\",\n        \"Proliferative DR\"\n    ]\n\n    diagnosis = class_names[predicted_class]\n\n    referable = predicted_class >= 2\n\n    # -----------------------------\n    # 4. Grad-CAM\n    # -----------------------------\n    target_layers = [model.features[-1]]\n\n    cam = GradCAM(\n        model=model,\n        target_layers=target_layers\n    )\n\n    targets = [\n        ClassifierOutputTarget(\n            predicted_class\n        )\n    ]\n\n    grayscale_cam = cam(\n        input_tensor=input_tensor,\n        targets=targets\n    )[0]\n\n    # -----------------------------\n    # 5. Prepare image for display\n    # -----------------------------\n    display_image = image.resize(\n        (512, 512)\n    )\n\n    display_np = (\n        np.array(display_image) / 255.0\n    )\n\n    visualization = show_cam_on_image(\n        display_np.astype(np.float32),\n        grayscale_cam,\n        use_rgb=True\n    )\n\n    # -----------------------------\n    # 6. Display\n    # -----------------------------\n    plt.figure(figsize=(14, 6))\n\n    plt.subplot(1, 2, 1)\n\n    plt.imshow(display_np)\n\n    plt.title(\n        \"Original Retinal Image\",\n        fontsize=14\n    )\n\n    plt.axis(\"off\")\n\n    plt.subplot(1, 2, 2)\n\n    plt.imshow(visualization)\n\n    plt.title(\n        \"Grad-CAM Explanation\",\n        fontsize=14\n    )\n\n    plt.axis(\"off\")\n\n    plt.tight_layout()\n\n    plt.show()\n\n    # -----------------------------\n    # 7. Final report\n    # -----------------------------\n    print(\"=\" * 60)\n    print(\"        DIABETIC RETINOPATHY SCREENING\")\n    print(\"=\" * 60)\n\n    print(\n        f\"Image           : \"\n        f\"{os.path.basename(image_path)}\"\n    )\n\n    print(\n        f\"DR Grade        : \"\n        f\"{predicted_class}\"\n    )\n\n    print(\n        f\"Diagnosis       : \"\n        f\"{diagnosis}\"\n    )\n\n    print(\n        f\"Confidence      : \"\n        f\"{confidence * 100:.2f}%\"\n    )\n\n    print(\"-\" * 60)\n\n    if referable:\n\n        print(\n            \"Referable DR    : YES ⚠️\"\n        )\n\n        print(\n            \"Recommendation  : \"\n            \"Further ophthalmic evaluation\"\n        )\n\n    else:\n\n        print(\n            \"Referable DR    : NO ✅\"\n        )\n\n        print(\n            \"Recommendation  : \"\n            \"Routine screening follow-up\"\n        )\n\n    print(\"=\" * 60)\n\n    return {\n        \"grade\": predicted_class,\n        \"diagnosis\": diagnosis,\n        \"confidence\": confidence,\n        \"referable\": referable\n    }\n\nprint(\"Final screening + Grad-CAM function ready! ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:21:10.246105Z","iopub.execute_input":"2026-08-26T17:21:10.246602Z","iopub.status.idle":"2026-08-26T17:21:10.257617Z","shell.execute_reply.started":"2026-08-26T17:21:10.246568Z","shell.execute_reply":"2026-08-26T17:21:10.256695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_image = \"/kaggle/input/competitions/aptos2019-blindness-detection/train_images/9b418ce42c13.png\"\n\nfinal_result = screen_retina_with_explanation(\n    test_image\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-26T17:21:26.051157Z","iopub.execute_input":"2026-08-26T17:21:26.051543Z","iopub.status.idle":"2026-08-26T17:21:26.680165Z","shell.execute_reply.started":"2026-08-26T17:21:26.051513Z","shell.execute_reply":"2026-08-26T17:21:26.679320Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🩺 AI-Based Diabetic Retinopathy Screening\n\n## Project Overview\n\nAn AI-based retinal image screening system for detecting and grading\nDiabetic Retinopathy using fundus photographs.\n\n### Model\n- EfficientNetV2-S backbone\n- 5-class DR severity classification\n- Ordinal learning for severity-aware prediction\n- Grad-CAM for visual explainability\n\n### DR Classes\n\n| Grade | Diagnosis |\n|------:|-----------|\n| 0 | No DR |\n| 1 | Mild DR |\n| 2 | Moderate DR |\n| 3 | Severe DR |\n| 4 | Proliferative DR |\n\n### Final Performance\n\n- 5-Class Accuracy: **81.27%**\n- Referable DR Sensitivity: **95.07%**\n- Referable DR Specificity: **91.74%**\n\n### Explainability\n\nGrad-CAM highlights the retinal regions that contributed to\nthe model's prediction, making the system more interpretable.\n\n### Final Output\n\nThe system provides:\n\n1. DR severity grade\n2. Predicted diagnosis\n3. Prediction confidence\n4. Referable DR decision\n5. Further-evaluation recommendation\n6. Grad-CAM visual explanation\n\n> **Note:** This system is intended as an AI-assisted screening\n> tool and not as a replacement for professional ophthalmic diagnosis.","metadata":{}},{"cell_type":"markdown","source":"             RETINAL FUNDUS IMAGE\n                      │\n                      ▼\n              IMAGE PREPROCESSING\n                      │\n                      ▼\n              EfficientNetV2-S\n                      │\n                      ▼\n              FEATURE EXTRACTION\n                      │\n             ┌────────┴────────┐\n             ▼                 ▼\n       5-Class Head       Ordinal Head\n             │                 │\n             ▼                 ▼\n       DR Grade 0–4      Severity-Aware\n                         Prediction\n             │\n             ▼\n       REFERABLE DR\n        YES / NO\n             │\n             ▼\n       Grad-CAM\n             │\n             ▼\n    VISUAL EXPLANATION","metadata":{}}]}