{"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":"import torch\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(\"Device:\", device)\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:15:45.694046Z","iopub.execute_input":"2026-09-07T13:15:45.694316Z","iopub.status.idle":"2026-09-07T13:15:50.853592Z","shell.execute_reply.started":"2026-09-07T13:15:45.694295Z","shell.execute_reply":"2026-09-07T13:15:50.852779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    classification_report,\n    confusion_matrix,\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:15:50.854471Z","iopub.execute_input":"2026-09-07T13:15:50.854771Z","iopub.status.idle":"2026-09-07T13:15:55.891212Z","shell.execute_reply.started":"2026-09-07T13:15:50.854749Z","shell.execute_reply":"2026-09-07T13:15:55.890570Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:16:56.276348Z","iopub.execute_input":"2026-09-07T13:16:56.277044Z","iopub.status.idle":"2026-09-07T13:16:56.282553Z","shell.execute_reply.started":"2026-09-07T13:16:56.277015Z","shell.execute_reply":"2026-09-07T13:16:56.281906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n\nCSV_PATH = os.path.join(DATA_DIR, \"train.csv\")\nIMAGE_DIR = os.path.join(DATA_DIR, \"train_images\")\n\nprint(\"CSV:\", CSV_PATH)\nprint(\"Images:\", IMAGE_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:02.431091Z","iopub.execute_input":"2026-09-07T13:17:02.431747Z","iopub.status.idle":"2026-09-07T13:17:02.436625Z","shell.execute_reply.started":"2026-09-07T13:17:02.431717Z","shell.execute_reply":"2026-09-07T13:17:02.435682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(CSV_PATH)\n\nprint(\"Dataset shape:\", df.shape)\nprint(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:11.940349Z","iopub.execute_input":"2026-09-07T13:17:11.941111Z","iopub.status.idle":"2026-09-07T13:17:11.975252Z","shell.execute_reply.started":"2026-09-07T13:17:11.941065Z","shell.execute_reply":"2026-09-07T13:17:11.974240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Class distribution:\")\nprint(df[\"diagnosis\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:19.915750Z","iopub.execute_input":"2026-09-07T13:17:19.916317Z","iopub.status.idle":"2026-09-07T13:17:19.934339Z","shell.execute_reply.started":"2026-09-07T13:17:19.916289Z","shell.execute_reply":"2026-09-07T13:17:19.933678Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\nprint(class_names)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:41.453365Z","iopub.execute_input":"2026-09-07T13:17:41.453650Z","iopub.status.idle":"2026-09-07T13:17:41.458276Z","shell.execute_reply.started":"2026-09-07T13:17:41.453628Z","shell.execute_reply":"2026-09-07T13:17:41.457351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df[\"image_path\"] = df[\"id_code\"].apply(\n    lambda x: os.path.join(IMAGE_DIR, x + \".png\")\n)\n\nprint(df[[\"id_code\", \"diagnosis\", \"image_path\"]].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:50.139662Z","iopub.execute_input":"2026-09-07T13:17:50.140379Z","iopub.status.idle":"2026-09-07T13:17:50.152200Z","shell.execute_reply.started":"2026-09-07T13:17:50.140348Z","shell.execute_reply":"2026-09-07T13:17:50.151023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_images = df[\n    ~df[\"image_path\"].apply(os.path.exists)\n]\n\nprint(\"Missing images:\", len(missing_images))\n\nif len(missing_images) > 0:\n    print(missing_images.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:17:59.737247Z","iopub.execute_input":"2026-09-07T13:17:59.738057Z","iopub.status.idle":"2026-09-07T13:18:04.125216Z","shell.execute_reply.started":"2026-09-07T13:17:59.738025Z","shell.execute_reply":"2026-09-07T13:18:04.124418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df, temp_df = train_test_split(\n    df,\n    test_size=0.30,\n    stratify=df[\"diagnosis\"],\n    random_state=SEED\n)\n\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.50,\n    stratify=temp_df[\"diagnosis\"],\n    random_state=SEED\n)\n\nprint(\"Train:\", len(train_df))\nprint(\"Validation:\", len(val_df))\nprint(\"Test:\", len(test_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:18:13.080668Z","iopub.execute_input":"2026-09-07T13:18:13.080984Z","iopub.status.idle":"2026-09-07T13:18:13.095971Z","shell.execute_reply.started":"2026-09-07T13:18:13.080961Z","shell.execute_reply":"2026-09-07T13:18:13.095051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"TRAIN:\")\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-09-07T13:18:22.247405Z","iopub.execute_input":"2026-09-07T13:18:22.248217Z","iopub.status.idle":"2026-09-07T13:18:22.256370Z","shell.execute_reply.started":"2026-09-07T13:18:22.248186Z","shell.execute_reply":"2026-09-07T13:18:22.255440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n\n    transforms.RandomHorizontalFlip(p=0.5),\n\n    transforms.RandomRotation(10),\n\n    transforms.ColorJitter(\n        brightness=0.10,\n        contrast=0.10,\n        saturation=0.05\n    ),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:18:32.643614Z","iopub.execute_input":"2026-09-07T13:18:32.644061Z","iopub.status.idle":"2026-09-07T13:18:32.649510Z","shell.execute_reply.started":"2026-09-07T13:18:32.644032Z","shell.execute_reply":"2026-09-07T13:18:32.648700Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_test_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n\n    transforms.ToTensor(),\n\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:18:40.360174Z","iopub.execute_input":"2026-09-07T13:18:40.360686Z","iopub.status.idle":"2026-09-07T13:18:40.365048Z","shell.execute_reply.started":"2026-09-07T13:18:40.360658Z","shell.execute_reply":"2026-09-07T13:18:40.364327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RetinopathyDataset(Dataset):\n\n    def __init__(self, dataframe, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\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 = Image.open(\n            row[\"image_path\"]\n        ).convert(\"RGB\")\n\n        label = int(row[\"diagnosis\"])\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:18:49.404091Z","iopub.execute_input":"2026-09-07T13:18:49.404375Z","iopub.status.idle":"2026-09-07T13:18:49.410374Z","shell.execute_reply.started":"2026-09-07T13:18:49.404354Z","shell.execute_reply":"2026-09-07T13:18:49.409565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = RetinopathyDataset(\n    train_df,\n    transform=train_transform\n)\n\nval_dataset = RetinopathyDataset(\n    val_df,\n    transform=val_test_transform\n)\n\ntest_dataset = RetinopathyDataset(\n    test_df,\n    transform=val_test_transform\n)\n\nprint(\"Train dataset:\", len(train_dataset))\nprint(\"Validation dataset:\", len(val_dataset))\nprint(\"Test dataset:\", len(test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:18:57.609015Z","iopub.execute_input":"2026-09-07T13:18:57.609901Z","iopub.status.idle":"2026-09-07T13:18:57.619316Z","shell.execute_reply.started":"2026-09-07T13:18:57.609827Z","shell.execute_reply":"2026-09-07T13:18:57.618280Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 32\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(\"DataLoaders ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:19:16.083192Z","iopub.execute_input":"2026-09-07T13:19:16.084208Z","iopub.status.idle":"2026-09-07T13:19:16.090769Z","shell.execute_reply.started":"2026-09-07T13:19:16.084161Z","shell.execute_reply":"2026-09-07T13:19:16.089762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"weights = models.ResNet50_Weights.DEFAULT\n\nmodel = models.resnet50(\n    weights=weights\n)\n\nnum_features = model.fc.in_features\n\nmodel.fc = nn.Sequential(\n    nn.Dropout(0.4),\n    nn.Linear(num_features, 5)\n)\n\nmodel = model.to(device)\n\nprint(model.fc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:19:24.261363Z","iopub.execute_input":"2026-09-07T13:19:24.261656Z","iopub.status.idle":"2026-09-07T13:19:25.625021Z","shell.execute_reply.started":"2026-09-07T13:19:24.261615Z","shell.execute_reply":"2026-09-07T13:19:25.624151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_counts_train = (\n    train_df[\"diagnosis\"]\n    .value_counts()\n    .sort_index()\n)\n\nclass_weights = (\n    len(train_df) /\n    (\n        len(class_counts_train)\n        * class_counts_train.values\n    )\n)\n\nclass_weights = torch.tensor(\n    class_weights,\n    dtype=torch.float32\n).to(device)\n\nprint(\"Class counts:\")\nprint(class_counts_train)\n\nprint(\"\\nClass weights:\")\nprint(class_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:19:39.749032Z","iopub.execute_input":"2026-09-07T13:19:39.749948Z","iopub.status.idle":"2026-09-07T13:19:40.068709Z","shell.execute_reply.started":"2026-09-07T13:19:39.749904Z","shell.execute_reply":"2026-09-07T13:19:40.068030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss(\n    weight=class_weights\n)\n\nprint(\"Weighted CrossEntropyLoss ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:19:49.223774Z","iopub.execute_input":"2026-09-07T13:19:49.224177Z","iopub.status.idle":"2026-09-07T13:19:49.229121Z","shell.execute_reply.started":"2026-09-07T13:19:49.224152Z","shell.execute_reply":"2026-09-07T13:19:49.228093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"optimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-4,\n    weight_decay=1e-4\n)\n\nprint(\"Optimizer ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:20:03.350202Z","iopub.execute_input":"2026-09-07T13:20:03.350910Z","iopub.status.idle":"2026-09-07T13:20:03.356010Z","shell.execute_reply.started":"2026-09-07T13:20:03.350876Z","shell.execute_reply":"2026-09-07T13:20:03.355064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode=\"min\",\n    factor=0.5,\n    patience=2\n)\n\nprint(\"Scheduler ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:20:15.132275Z","iopub.execute_input":"2026-09-07T13:20:15.133018Z","iopub.status.idle":"2026-09-07T13:20:15.137494Z","shell.execute_reply.started":"2026-09-07T13:20:15.132986Z","shell.execute_reply":"2026-09-07T13:20:15.136687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(\n    model,\n    loader,\n    criterion,\n    optimizer,\n    device\n):\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(\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        predictions = outputs.argmax(\n            dim=1\n        )\n\n        correct += (\n            predictions == labels\n        ).sum().item()\n\n        total += labels.size(0)\n\n    epoch_loss = running_loss / total\n    epoch_accuracy = correct / total\n\n    return epoch_loss, epoch_accuracy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:20:27.333391Z","iopub.execute_input":"2026-09-07T13:20:27.333678Z","iopub.status.idle":"2026-09-07T13:20:27.341566Z","shell.execute_reply.started":"2026-09-07T13:20:27.333656Z","shell.execute_reply":"2026-09-07T13:20:27.340575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(\n    model,\n    loader,\n    criterion,\n    device\n):\n\n    model.eval()\n\n    running_loss = 0.0\n    total = 0\n\n    all_labels = []\n    all_predictions = []\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            predictions = outputs.argmax(\n                dim=1\n            )\n\n            running_loss += (\n                loss.item() * images.size(0)\n            )\n\n            total += labels.size(0)\n\n            all_labels.extend(\n                labels.cpu().numpy()\n            )\n\n            all_predictions.extend(\n                predictions.cpu().numpy()\n            )\n\n    val_loss = running_loss / total\n\n    val_accuracy = accuracy_score(\n        all_labels,\n        all_predictions\n    )\n\n    all_labels_np = np.array(all_labels)\n    all_predictions_np = np.array(\n        all_predictions\n    )\n\n    # DR vs No DR\n    actual_dr = all_labels_np != 0\n    predicted_dr = all_predictions_np != 0\n\n    tp = np.sum(\n        actual_dr & predicted_dr\n    )\n\n    fn = np.sum(\n        actual_dr & ~predicted_dr\n    )\n\n    tn = np.sum(\n        ~actual_dr & ~predicted_dr\n    )\n\n    fp = np.sum(\n        ~actual_dr & predicted_dr\n    )\n\n    dr_sensitivity = (\n        tp / (tp + fn)\n        if (tp + fn) > 0\n        else 0\n    )\n\n    no_dr_specificity = (\n        tn / (tn + fp)\n        if (tn + fp) > 0\n        else 0\n    )\n\n    class_recalls = recall_score(\n        all_labels,\n        all_predictions,\n        labels=[0, 1, 2, 3, 4],\n        average=None,\n        zero_division=0\n    )\n\n    macro_f1 = f1_score(\n        all_labels,\n        all_predictions,\n        average=\"macro\",\n        zero_division=0\n    )\n\n    return (\n        val_loss,\n        val_accuracy,\n        dr_sensitivity,\n        no_dr_specificity,\n        class_recalls,\n        macro_f1,\n        all_labels_np,\n        all_predictions_np\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:20:41.680052Z","iopub.execute_input":"2026-09-07T13:20:41.680326Z","iopub.status.idle":"2026-09-07T13:20:41.689535Z","shell.execute_reply.started":"2026-09-07T13:20:41.680304Z","shell.execute_reply":"2026-09-07T13:20:41.688900Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_EPOCHS = 15\n\nMODEL_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_dr_best.pth\"\n)\n\nHISTORY_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_training_history.json\"\n)\n\nBEST_METRICS_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_best_metrics.json\"\n)\n\nBEST_RESULTS_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_best_validation_results.csv\"\n)\n\nbest_dr_sensitivity = -1\nbest_macro_f1 = -1\nbest_val_loss = float(\"inf\")\n\nhistory = []","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:20:55.782656Z","iopub.execute_input":"2026-09-07T13:20:55.782946Z","iopub.status.idle":"2026-09-07T13:20:55.787836Z","shell.execute_reply.started":"2026-09-07T13:20:55.782922Z","shell.execute_reply":"2026-09-07T13:20:55.786916Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for epoch in range(NUM_EPOCHS):\n\n    print(\"=\" * 60)\n    print(f\"Epoch {epoch + 1}/{NUM_EPOCHS}\")\n    print(\"=\" * 60)\n\n    train_loss, train_accuracy = train_one_epoch(\n        model,\n        train_loader,\n        criterion,\n        optimizer,\n        device\n    )\n\n    (\n        val_loss,\n        val_accuracy,\n        dr_sensitivity,\n        no_dr_specificity,\n        class_recalls,\n        macro_f1,\n        val_labels,\n        val_predictions\n    ) = validate(\n        model,\n        val_loader,\n        criterion,\n        device\n    )\n\n    scheduler.step(val_loss)\n\n    print(f\"Train Loss:           {train_loss:.4f}\")\n    print(f\"Train Accuracy:       {train_accuracy:.4f}\")\n    print(f\"Validation Loss:      {val_loss:.4f}\")\n    print(f\"Validation Accuracy:  {val_accuracy:.4f}\")\n    print(f\"DR Sensitivity:       {dr_sensitivity:.4f}\")\n    print(f\"No-DR Specificity:    {no_dr_specificity:.4f}\")\n    print(f\"Macro F1:             {macro_f1:.4f}\")\n\n    print(\"\\nClass Recall:\")\n\n    for i, recall in enumerate(class_recalls):\n        print(\n            f\"  {class_names[i]:<18} \"\n            f\"{recall:.4f}\"\n        )\n\n    epoch_record = {\n        \"epoch\": epoch + 1,\n        \"train_loss\": float(train_loss),\n        \"train_accuracy\": float(train_accuracy),\n        \"val_loss\": float(val_loss),\n        \"val_accuracy\": float(val_accuracy),\n        \"dr_sensitivity\": float(dr_sensitivity),\n        \"no_dr_specificity\": float(no_dr_specificity),\n        \"macro_f1\": float(macro_f1),\n        \"class_recalls\": [\n            float(x) for x in class_recalls\n        ]\n    }\n\n    history.append(epoch_record)\n\n    # Selection:\n    # 1. Highest DR sensitivity\n    # 2. Highest Macro F1 if tied\n    # 3. Lowest validation loss if still tied\n\n    is_better = False\n\n    if dr_sensitivity > best_dr_sensitivity:\n        is_better = True\n\n    elif (\n        dr_sensitivity == best_dr_sensitivity\n        and macro_f1 > best_macro_f1\n    ):\n        is_better = True\n\n    elif (\n        dr_sensitivity == best_dr_sensitivity\n        and macro_f1 == best_macro_f1\n        and val_loss < best_val_loss\n    ):\n        is_better = True\n\n    if is_better:\n\n        best_dr_sensitivity = dr_sensitivity\n        best_macro_f1 = macro_f1\n        best_val_loss = val_loss\n\n        checkpoint = {\n            \"epoch\": epoch + 1,\n            \"model_state_dict\": model.state_dict(),\n            \"optimizer_state_dict\": optimizer.state_dict(),\n            \"best_dr_sensitivity\": float(\n                best_dr_sensitivity\n            ),\n            \"best_macro_f1\": float(\n                best_macro_f1\n            ),\n            \"best_val_loss\": float(\n                best_val_loss\n            ),\n            \"class_recalls\": [\n                float(x)\n                for x in class_recalls\n            ]\n        }\n\n        torch.save(\n            checkpoint,\n            MODEL_PATH\n        )\n\n        best_metrics = {\n            \"best_epoch\": epoch + 1,\n            \"best_dr_sensitivity\": float(\n                best_dr_sensitivity\n            ),\n            \"best_macro_f1\": float(\n                best_macro_f1\n            ),\n            \"best_validation_loss\": float(\n                best_val_loss\n            ),\n            \"best_validation_accuracy\": float(\n                val_accuracy\n            ),\n            \"best_no_dr_specificity\": float(\n                no_dr_specificity\n            ),\n            \"best_class_recalls\": [\n                float(x)\n                for x in class_recalls\n            ]\n        }\n\n        with open(\n            BEST_METRICS_PATH,\n            \"w\"\n        ) as f:\n            json.dump(\n                best_metrics,\n                f,\n                indent=4\n            )\n\n        pd.DataFrame({\n            \"actual\": val_labels,\n            \"predicted\": val_predictions\n        }).to_csv(\n            BEST_RESULTS_PATH,\n            index=False\n        )\n\n        print(\"\\n✓ NEW BEST MODEL SAVED\")\n        print(\n            f\"  Epoch: {epoch + 1}\"\n        )\n        print(\n            f\"  DR Sensitivity: \"\n            f\"{dr_sensitivity:.4f}\"\n        )\n        print(\n            f\"  Macro F1: \"\n            f\"{macro_f1:.4f}\"\n        )\n        print(\n            f\"  Validation Loss: \"\n            f\"{val_loss:.4f}\"\n        )\n\n    print()\n\nwith open(\n    HISTORY_PATH,\n    \"w\"\n) as f:\n\n    json.dump(\n        history,\n        f,\n        indent=4\n    )\n\nprint(\"=\" * 60)\nprint(\"TRAINING COMPLETE\")\nprint(\"=\" * 60)\n\nprint(f\"\\nBest Epoch: {history[np.argmax([\n    x['dr_sensitivity'] for x in history\n])]['epoch']}\")\n\nprint(\n    f\"Best DR Sensitivity: \"\n    f\"{best_dr_sensitivity:.4f}\"\n)\n\nprint(\n    f\"Best Macro F1: \"\n    f\"{best_macro_f1:.4f}\"\n)\n\nprint(\n    f\"Best Validation Loss: \"\n    f\"{best_val_loss:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T13:21:12.192457Z","iopub.execute_input":"2026-09-07T13:21:12.193105Z","iopub.status.idle":"2026-09-07T14:14:01.700600Z","shell.execute_reply.started":"2026-09-07T13:21:12.193076Z","shell.execute_reply":"2026-09-07T14:14:01.699758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"checkpoint = torch.load(\n    MODEL_PATH,\n    map_location=device\n)\n\nmodel.load_state_dict(\n    checkpoint[\"model_state_dict\"]\n)\n\nmodel = model.to(device)\n\nmodel.eval()\n\nprint(\"Best model loaded successfully.\")\n\nprint(\n    \"Best epoch:\",\n    checkpoint[\"epoch\"]\n)\n\nprint(\n    \"Best DR sensitivity:\",\n    checkpoint[\"best_dr_sensitivity\"]\n)\n\nprint(\n    \"Best Macro F1:\",\n    checkpoint[\"best_macro_f1\"]\n)\n\nprint(\n    \"Best validation loss:\",\n    checkpoint[\"best_val_loss\"]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:14:56.878038Z","iopub.execute_input":"2026-09-07T14:14:56.878865Z","iopub.status.idle":"2026-09-07T14:14:57.240411Z","shell.execute_reply.started":"2026-09-07T14:14:56.878810Z","shell.execute_reply":"2026-09-07T14:14:57.239730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_labels = []\ntest_predictions = []\n\nmodel.eval()\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        predictions = outputs.argmax(\n            dim=1\n        )\n\n        test_labels.extend(\n            labels.numpy()\n        )\n\n        test_predictions.extend(\n            predictions.cpu().numpy()\n        )\n\nprint(\n    \"Test predictions:\",\n    len(test_predictions)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:15:10.260560Z","iopub.execute_input":"2026-09-07T14:15:10.261263Z","iopub.status.idle":"2026-09-07T14:15:53.241212Z","shell.execute_reply.started":"2026-09-07T14:15:10.261232Z","shell.execute_reply":"2026-09-07T14:15:53.240381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_labels = np.array(test_labels)\ntest_predictions = np.array(test_predictions)\n\ntest_accuracy = accuracy_score(\n    test_labels,\n    test_predictions\n)\n\ntest_precision = precision_score(\n    test_labels,\n    test_predictions,\n    average=\"macro\",\n    zero_division=0\n)\n\ntest_recall = recall_score(\n    test_labels,\n    test_predictions,\n    average=\"macro\",\n    zero_division=0\n)\n\ntest_macro_f1 = f1_score(\n    test_labels,\n    test_predictions,\n    average=\"macro\",\n    zero_division=0\n)\n\nactual_dr = test_labels != 0\npredicted_dr = test_predictions != 0\n\ntp = np.sum(\n    actual_dr & predicted_dr\n)\n\nfn = np.sum(\n    actual_dr & ~predicted_dr\n)\n\ntn = np.sum(\n    ~actual_dr & ~predicted_dr\n)\n\nfp = np.sum(\n    ~actual_dr & predicted_dr\n)\n\ntest_dr_sensitivity = (\n    tp / (tp + fn)\n    if (tp + fn) > 0\n    else 0\n)\n\ntest_no_dr_specificity = (\n    tn / (tn + fp)\n    if (tn + fp) > 0\n    else 0\n)\n\nprint(\"=\" * 60)\nprint(\"FINAL RESNET50 TEST RESULTS\")\nprint(\"=\" * 60)\n\nprint(\n    f\"\\nAccuracy:             \"\n    f\"{test_accuracy:.4f}\"\n)\n\nprint(\n    f\"Macro Precision:      \"\n    f\"{test_precision:.4f}\"\n)\n\nprint(\n    f\"Macro Recall:         \"\n    f\"{test_recall:.4f}\"\n)\n\nprint(\n    f\"Macro F1:             \"\n    f\"{test_macro_f1:.4f}\"\n)\n\nprint(\n    f\"\\nDR Sensitivity:       \"\n    f\"{test_dr_sensitivity:.4f}\"\n)\n\nprint(\n    f\"No-DR Specificity:    \"\n    f\"{test_no_dr_specificity:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:16:06.975983Z","iopub.execute_input":"2026-09-07T14:16:06.976265Z","iopub.status.idle":"2026-09-07T14:16:06.995196Z","shell.execute_reply.started":"2026-09-07T14:16:06.976238Z","shell.execute_reply":"2026-09-07T14:16:06.994273Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\n    classification_report(\n        test_labels,\n        test_predictions,\n        target_names=class_names,\n        zero_division=0\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:16:33.105162Z","iopub.execute_input":"2026-09-07T14:16:33.106156Z","iopub.status.idle":"2026-09-07T14:16:33.122203Z","shell.execute_reply.started":"2026-09-07T14:16:33.106123Z","shell.execute_reply":"2026-09-07T14:16:33.121261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cm = confusion_matrix(\n    test_labels,\n    test_predictions,\n    labels=[0, 1, 2, 3, 4]\n)\n\nprint(\"Confusion Matrix:\")\nprint(cm)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:23:45.146378Z","iopub.execute_input":"2026-09-07T14:23:45.147068Z","iopub.status.idle":"2026-09-07T14:23:45.153883Z","shell.execute_reply.started":"2026-09-07T14:23:45.147033Z","shell.execute_reply":"2026-09-07T14:23:45.153196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TEST_METRICS_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_test_metrics.json\"\n)\n\nTEST_PREDICTIONS_PATH = (\n    \"/kaggle/working/\"\n    \"resnet50_test_predictions.csv\"\n)\n\ntest_metrics = {\n    \"accuracy\": float(test_accuracy),\n    \"macro_precision\": float(test_precision),\n    \"macro_recall\": float(test_recall),\n    \"macro_f1\": float(test_macro_f1),\n    \"dr_sensitivity\": float(\n        test_dr_sensitivity\n    ),\n    \"no_dr_specificity\": float(\n        test_no_dr_specificity\n    ),\n    \"classification_report\": classification_report(\n        test_labels,\n        test_predictions,\n        target_names=class_names,\n        output_dict=True,\n        zero_division=0\n    ),\n    \"confusion_matrix\": cm.tolist()\n}\n\nwith open(\n    TEST_METRICS_PATH,\n    \"w\"\n) as f:\n\n    json.dump(\n        test_metrics,\n        f,\n        indent=4\n    )\n\npd.DataFrame({\n    \"actual\": test_labels,\n    \"predicted\": test_predictions\n}).to_csv(\n    TEST_PREDICTIONS_PATH,\n    index=False\n)\n\nprint(\n    \"Saved:\",\n    TEST_METRICS_PATH\n)\n\nprint(\n    \"Saved:\",\n    TEST_PREDICTIONS_PATH\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:23:48.405842Z","iopub.execute_input":"2026-09-07T14:23:48.406501Z","iopub.status.idle":"2026-09-07T14:23:48.423689Z","shell.execute_reply.started":"2026-09-07T14:23:48.406452Z","shell.execute_reply":"2026-09-07T14:23:48.422927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=\" * 60)\nprint(\"FINAL FILE CHECK\")\nprint(\"=\" * 60)\n\nfiles_to_check = [\n    MODEL_PATH,\n    HISTORY_PATH,\n    BEST_METRICS_PATH,\n    BEST_RESULTS_PATH,\n    TEST_METRICS_PATH,\n    TEST_PREDICTIONS_PATH\n]\n\nfor file_path in files_to_check:\n\n    exists = os.path.exists(file_path)\n\n    if exists:\n        size_mb = (\n            os.path.getsize(file_path)\n            / (1024 * 1024)\n        )\n\n        print(\n            f\"\\n{file_path}\"\n        )\n\n        print(\n            f\"Exists: {exists}\"\n        )\n\n        print(\n            f\"Size: {size_mb:.2f} MB\"\n        )\n\n    else:\n\n        print(\n            f\"\\nMISSING: {file_path}\"\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:23:53.488663Z","iopub.execute_input":"2026-09-07T14:23:53.489374Z","iopub.status.idle":"2026-09-07T14:23:53.495732Z","shell.execute_reply.started":"2026-09-07T14:23:53.489343Z","shell.execute_reply":"2026-09-07T14:23:53.494991Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nfor f in [\n    \"/kaggle/working/resnet50_dr_best.pth\",\n    \"/kaggle/working/resnet50_training_history.json\",\n    \"/kaggle/working/resnet50_best_metrics.json\",\n    \"/kaggle/working/resnet50_test_metrics.json\",\n]:\n    print(f, \"→\", os.path.exists(f))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:22:51.933103Z","iopub.execute_input":"2026-09-07T14:22:51.933715Z","iopub.status.idle":"2026-09-07T14:22:51.939099Z","shell.execute_reply.started":"2026-09-07T14:22:51.933685Z","shell.execute_reply":"2026-09-07T14:22:51.938274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink\n\nFileLink('/kaggle/working/resnet50_dr_best.pth')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-07T14:23:27.763189Z","iopub.execute_input":"2026-09-07T14:23:27.763473Z","iopub.status.idle":"2026-09-07T14:23:27.771024Z","shell.execute_reply.started":"2026-09-07T14:23:27.763443Z","shell.execute_reply":"2026-09-07T14:23:27.769944Z"}},"outputs":[],"execution_count":null}]}