{"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":"train_df = pd.read_csv(\"/kaggle/input/competitions/aptos2019-blindness-detection/train.csv\")\n\nprint(train_df.head())\nprint(train_df.columns)\ntrain_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:00:44.915403Z","iopub.execute_input":"2026-09-16T18:00:44.915840Z","iopub.status.idle":"2026-09-16T18:00:44.931081Z","shell.execute_reply.started":"2026-09-16T18:00:44.915815Z","shell.execute_reply":"2026-09-16T18:00:44.930452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"diagnosis\"].value_counts()\n# max no img belong to 0 grade","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:00:44.932049Z","iopub.execute_input":"2026-09-16T18:00:44.932370Z","iopub.status.idle":"2026-09-16T18:00:44.944010Z","shell.execute_reply.started":"2026-09-16T18:00:44.932336Z","shell.execute_reply":"2026-09-16T18:00:44.943109Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"label\"] = (train_df[\"diagnosis\"] > 0).astype(np.int32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:00:44.945801Z","iopub.execute_input":"2026-09-16T18:00:44.946062Z","iopub.status.idle":"2026-09-16T18:00:44.960947Z","shell.execute_reply.started":"2026-09-16T18:00:44.946042Z","shell.execute_reply":"2026-09-16T18:00:44.960377Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from datasets import load_dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:00:44.961731Z","iopub.execute_input":"2026-09-16T18:00:44.962009Z","iopub.status.idle":"2026-09-16T18:00:44.976946Z","shell.execute_reply.started":"2026-09-16T18:00:44.961975Z","shell.execute_reply":"2026-09-16T18:00:44.976420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = load_dataset(\"imagefolder\",data_dir=str(\"/kaggle/input/competitions/aptos2019-blindness-detection/train_images\"))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:00:44.977708Z","iopub.execute_input":"2026-09-16T18:00:44.978410Z","iopub.status.idle":"2026-09-16T18:01:20.095200Z","shell.execute_reply.started":"2026-09-16T18:00:44.978387Z","shell.execute_reply":"2026-09-16T18:01:20.094633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:20.095978Z","iopub.execute_input":"2026-09-16T18:01:20.096395Z","iopub.status.idle":"2026-09-16T18:01:20.101431Z","shell.execute_reply.started":"2026-09-16T18:01:20.096366Z","shell.execute_reply":"2026-09-16T18:01:20.100636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(dataset[\"train\"].features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:20.102220Z","iopub.execute_input":"2026-09-16T18:01:20.102464Z","iopub.status.idle":"2026-09-16T18:01:20.117264Z","shell.execute_reply.started":"2026-09-16T18:01:20.102432Z","shell.execute_reply":"2026-09-16T18:01:20.116374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"all_grades = train_df[\"diagnosis\"]\nprint(all_grades.unique())\nall_grades = all_grades.to_numpy()\nall_grades","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:20.118261Z","iopub.execute_input":"2026-09-16T18:01:20.118532Z","iopub.status.idle":"2026-09-16T18:01:20.133674Z","shell.execute_reply.started":"2026-09-16T18:01:20.118503Z","shell.execute_reply":"2026-09-16T18:01:20.133036Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# mapping the labels of img in train.csv to their real img and saving the exact path\ntrain_df[\"image_path\"] = (\n    str(\"/kaggle/input/competitions/aptos2019-blindness-detection/\" + \"train_images\") \n    + \"/\" \n    + train_df[\"id_code\"].astype(str) \n    + \".png\"\n)\nprint(train_df[\"image_path\"].head(3))\nprint(f\"\\n {train_df.head(3)} \\n\")\ntrain_df.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:20.136201Z","iopub.execute_input":"2026-09-16T18:01:20.136724Z","iopub.status.idle":"2026-09-16T18:01:20.154017Z","shell.execute_reply.started":"2026-09-16T18:01:20.136701Z","shell.execute_reply":"2026-09-16T18:01:20.153423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# new dataframe for the mapping\nrecords_df = pd.DataFrame({\n    \"id_code\": train_df[\"id_code\"],\n    \"image_path\": train_df[\"image_path\"],\n    \"grade\": train_df[\"label\"], # label is new grade now , will later change to grades  0 to 4\n})\n\nrecords_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:20.154788Z","iopub.execute_input":"2026-09-16T18:01:20.155080Z","iopub.status.idle":"2026-09-16T18:01:20.182338Z","shell.execute_reply.started":"2026-09-16T18:01:20.155042Z","shell.execute_reply":"2026-09-16T18:01:20.181724Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# some imports form jai's notebook\nimport random\nimport torch\nfrom torch import nn\nfrom torchvision import models, transforms\nfrom PIL import Image\nfrom torch.utils.data import DataLoader, Dataset, WeightedRandomSampler\n\n\nSEED = 26038\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {DEVICE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:05:03.936183Z","iopub.execute_input":"2026-09-16T18:05:03.936948Z","iopub.status.idle":"2026-09-16T18:05:03.943414Z","shell.execute_reply.started":"2026-09-16T18:05:03.936917Z","shell.execute_reply":"2026-09-16T18:05:03.942543Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import (\n    accuracy_score,\n    confusion_matrix,\n    f1_score,\n    precision_score,\n    recall_score,\n    roc_auc_score,\n)\nfrom sklearn.model_selection import train_test_split","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:33.379983Z","iopub.execute_input":"2026-09-16T18:01:33.380698Z","iopub.status.idle":"2026-09-16T18:01:34.169441Z","shell.execute_reply.started":"2026-09-16T18:01:33.380668Z","shell.execute_reply":"2026-09-16T18:01:34.168408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# few more setups left\nARTIFACT_DIR = \"/kaggle/working/\" + \"artifacts\"\nIMAGE_SIZE = 512\nBATCH_SIZE = 16\nNUM_WORKERS = 0\nUSE_SMOKE_TEST = True\nSMOKE_TEST_SIZE = 800\nprint(ARTIFACT_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.170527Z","iopub.execute_input":"2026-09-16T18:01:34.171067Z","iopub.status.idle":"2026-09-16T18:01:34.175744Z","shell.execute_reply.started":"2026-09-16T18:01:34.171041Z","shell.execute_reply":"2026-09-16T18:01:34.174919Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.176637Z","iopub.execute_input":"2026-09-16T18:01:34.177238Z","iopub.status.idle":"2026-09-16T18:01:34.206508Z","shell.execute_reply.started":"2026-09-16T18:01:34.177206Z","shell.execute_reply":"2026-09-16T18:01:34.205845Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if USE_SMOKE_TEST:\n\n    records_df2, _ = train_test_split(\n        records_df,\n        train_size=min(SMOKE_TEST_SIZE, len(records_df)),\n        stratify=records_df[\"grade\"],\n        random_state=SEED\n    )\n\n    # records_df = records_df.reset_index(drop=True)\n\nelse:\n\n    records_df2 = train_df.copy()\n\nprint(train_df[\"diagnosis\"].value_counts())\n# print(records_df2[\"grade\"].value_counts())\nprint(records_df2[\"grade\"].value_counts())\nrecords_df2.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.207405Z","iopub.execute_input":"2026-09-16T18:01:34.207752Z","iopub.status.idle":"2026-09-16T18:01:34.237223Z","shell.execute_reply.started":"2026-09-16T18:01:34.207729Z","shell.execute_reply":"2026-09-16T18:01:34.236592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"records_df2.sample(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.237937Z","iopub.execute_input":"2026-09-16T18:01:34.238714Z","iopub.status.idle":"2026-09-16T18:01:34.245905Z","shell.execute_reply.started":"2026-09-16T18:01:34.238689Z","shell.execute_reply":"2026-09-16T18:01:34.245320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(8),\n    transforms.ColorJitter(\n        brightness=0.18,\n        contrast=0.18\n    ),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        [0.485, 0.456, 0.406],\n        [0.229, 0.224, 0.225]\n    ),\n])\n\ninference_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        [0.485, 0.456, 0.406],\n        [0.229, 0.224, 0.225]\n    ),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.246914Z","iopub.execute_input":"2026-09-16T18:01:34.247270Z","iopub.status.idle":"2026-09-16T18:01:34.262498Z","shell.execute_reply.started":"2026-09-16T18:01:34.247234Z","shell.execute_reply":"2026-09-16T18:01:34.261927Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Transfromer's info in both train n inference ","metadata":{}},{"cell_type":"code","source":"train_transform","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.263394Z","iopub.execute_input":"2026-09-16T18:01:34.263661Z","iopub.status.idle":"2026-09-16T18:01:34.281735Z","shell.execute_reply.started":"2026-09-16T18:01:34.263632Z","shell.execute_reply":"2026-09-16T18:01:34.281106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_transform","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.282646Z","iopub.execute_input":"2026-09-16T18:01:34.282932Z","iopub.status.idle":"2026-09-16T18:01:34.298107Z","shell.execute_reply.started":"2026-09-16T18:01:34.282900Z","shell.execute_reply":"2026-09-16T18:01:34.297321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset\nfrom PIL import Image\nimport torch\n\n# both the img n csv r disconnected , here this class and function connects them using the path\n# it apply transformation on image fetched and return data using tensor kuch krke\nclass APTOSDataset(Dataset):\n\n    def __init__(self, frame, transform=None):\n        self.frame = frame.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.frame)\n\n    def __getitem__(self, index):\n\n        row = self.frame.iloc[index]\n\n        image = Image.open(\n            row[\"image_path\"]\n        ).convert(\"RGB\")\n\n        label = int(row[\"grade\"])\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, torch.tensor(\n            label,\n            dtype=torch.float\n        )\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.299200Z","iopub.execute_input":"2026-09-16T18:01:34.299851Z","iopub.status.idle":"2026-09-16T18:01:34.313667Z","shell.execute_reply.started":"2026-09-16T18:01:34.299792Z","shell.execute_reply":"2026-09-16T18:01:34.312785Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 500 items info train test n val ie. 300 100 100 ratio\n\nfrom sklearn.model_selection import train_test_split\n\ntrain_df, temp_df = train_test_split(\n    records_df2,\n    test_size=0.20,\n    random_state=SEED,\n    stratify=records_df2[\"grade\"]\n)\n\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.50,\n    random_state=SEED,\n    stratify=temp_df[\"grade\"]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.314825Z","iopub.execute_input":"2026-09-16T18:01:34.315271Z","iopub.status.idle":"2026-09-16T18:01:34.337965Z","shell.execute_reply.started":"2026-09-16T18:01:34.315246Z","shell.execute_reply":"2026-09-16T18:01:34.337052Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# here naam karan kiya sabhi ka using class created above\ntrain_dataset = APTOSDataset(train_df,transform=train_transform)\n\nval_dataset = APTOSDataset(val_df,transform=inference_transform)\n\ntest_dataset = APTOSDataset(test_df,transform=inference_transform)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.338879Z","iopub.execute_input":"2026-09-16T18:01:34.339184Z","iopub.status.idle":"2026-09-16T18:01:34.344266Z","shell.execute_reply.started":"2026-09-16T18:01:34.339115Z","shell.execute_reply":"2026-09-16T18:01:34.343416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# checking if sabhi ko kush mila na hr grade se ki nhi [500 wala]\nprint(\"Full sample:\")\nprint(records_df2[\"grade\"].value_counts())\n\nprint(\"\\nTrain:\")\nprint(train_df[\"grade\"].value_counts())\n\nprint(\"\\nValidation:\")\nprint(val_df[\"grade\"].value_counts())\n\nprint(\"\\nTest:\")\nprint(test_df[\"grade\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.345222Z","iopub.execute_input":"2026-09-16T18:01:34.345573Z","iopub.status.idle":"2026-09-16T18:01:34.362787Z","shell.execute_reply.started":"2026-09-16T18:01:34.345537Z","shell.execute_reply":"2026-09-16T18:01:34.362206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4. Build DataLoaders\n\n# The APTOS dataset is a 5-class multiclass problem (grades 0-4).\n# The DR classes are imbalanced, so we use WeightedRandomSampler\n# to give minority classes a higher probability of being sampled.\n# This is adapted from the old EyePACS binary-classification code.\n\nclass_counts = (\n    train_df[\"grade\"]\n    .value_counts()\n    .sort_index()\n)\n\nclass_weights = 1.0 / class_counts\nsample_weights = train_df[\"grade\"].map(class_weights).to_numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.363785Z","iopub.execute_input":"2026-09-16T18:01:34.364121Z","iopub.status.idle":"2026-09-16T18:01:34.383251Z","shell.execute_reply.started":"2026-09-16T18:01:34.364097Z","shell.execute_reply":"2026-09-16T18:01:34.382668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# replacement=True means an image can be\n# selected more than once in an epoch.\n#\n# num_samples=len(sample_weights) means\n# one epoch still contains the same number\n# of samples as the original training set.\n\nsampler = WeightedRandomSampler(\n    weights=torch.as_tensor(\n        sample_weights,\n        dtype=torch.double\n    ),\n    num_samples=len(sample_weights),\n    replacement=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.384029Z","iopub.execute_input":"2026-09-16T18:01:34.384322Z","iopub.status.idle":"2026-09-16T18:01:34.417923Z","shell.execute_reply.started":"2026-09-16T18:01:34.384280Z","shell.execute_reply":"2026-09-16T18:01:34.417345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# DataLoaders\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    sampler=sampler,\n    num_workers=NUM_WORKERS\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS\n)\n\n\nprint(\n    f\"Batches: \"\n    f\"train={len(train_loader)}, \"\n    f\"val={len(val_loader)}, \"\n    f\"test={len(test_loader)}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:01:34.418910Z","iopub.execute_input":"2026-09-16T18:01:34.419221Z","iopub.status.idle":"2026-09-16T18:01:34.425957Z","shell.execute_reply.started":"2026-09-16T18:01:34.419188Z","shell.execute_reply":"2026-09-16T18:01:34.425325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 5. Build the Model\n\nUSE_PRETRAINED_WEIGHTS = True\n\nweights = models.ResNet18_Weights.DEFAULT if USE_PRETRAINED_WEIGHTS else None\nmodel = models.resnet18(weights=weights)\n\n# 5 output neurons → one for each DR grade: 0, 1, 2, 3, 4\nmodel.fc = nn.Linear(model.fc.in_features, 1)\nmodel = model.to(DEVICE)\n\n# Multiclass classification loss\nloss_function = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=2e-4,\n    weight_decay=1e-4\n)\n\nprint(\n    f\"Trainable parameters: \"\n    f\"{sum(p.numel() for p in model.parameters() if p.requires_grad):,}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:03:12.806294Z","iopub.execute_input":"2026-09-16T18:03:12.807031Z","iopub.status.idle":"2026-09-16T18:03:13.687814Z","shell.execute_reply.started":"2026-09-16T18:03:12.807004Z","shell.execute_reply":"2026-09-16T18:03:13.687123Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import copy\n\n# 6. Train the Model\n\ndef collect_predictions(model, loader):\n\n    model.eval()\n    probabilities, labels = [], []\n    with torch.inference_mode():\n\n        for images, targets in loader:\n            # Model output shape: [batch_size, 1]\n            logits = model(images.to(DEVICE)).squeeze(1)\n            # Convert logits to probability of DR\n            probs = torch.sigmoid(logits)\n            probabilities.extend(probs.cpu().numpy())\n            labels.extend(targets.numpy())\n\n    return (\n        np.asarray(labels, dtype=int),\n        np.asarray(probabilities, dtype=float)\n    )\n\n\ndef safe_auc(labels, probabilities):\n\n    # ROC-AUC requires both classes to be present\n    return (\n        float(roc_auc_score(labels, probabilities))\n        if len(np.unique(labels)) == 2\n        else float(\"nan\")\n    )\n\n\ndef train_model(model, train_loader, val_loader, epochs=5, patience=2):\n\n    best_auc = -np.inf\n    best_state = copy.deepcopy(model.state_dict())\n    wait = 0\n\n    history = []\n\n    for epoch in range(1, epochs + 1):\n\n        model.train()\n        running_loss = 0.0\n\n        for images, targets in train_loader:\n\n            # Clear previous gradients\n            optimizer.zero_grad(set_to_none=True)\n\n            # Forward pass\n            # Output shape: [batch_size, 1]\n            logits = model(images.to(DEVICE)).squeeze(1)\n\n            # BCEWithLogitsLoss expects:\n            # logits  -> [batch_size]\n            # targets -> [batch_size] with values 0 or 1\n            loss = loss_function(\n                logits,\n                targets.to(DEVICE)\n            )\n\n            # Backpropagation\n            loss.backward()\n\n            # Update model parameters\n            optimizer.step()\n\n            running_loss += loss.item() * len(images)\n\n        # Validation\n        val_labels, val_probabilities = collect_predictions(\n            model,\n            val_loader\n        )\n\n        val_auc = safe_auc(\n            val_labels,\n            val_probabilities\n        )\n\n        epoch_record = {\n            \"epoch\": epoch,\n            \"train_loss\": running_loss / len(train_loader.dataset),\n            \"val_auc\": val_auc\n        }\n\n        history.append(epoch_record)\n\n        print(epoch_record)\n\n        # Save best model based on validation AUC\n        if np.isfinite(val_auc) and val_auc > best_auc:\n\n            best_auc = val_auc\n            best_state = copy.deepcopy(model.state_dict())\n            wait = 0\n\n        else:\n\n            wait += 1\n\n            if wait >= patience:\n                break\n\n    # Restore the best model\n    model.load_state_dict(best_state)\n\n    return model, pd.DataFrame(history)\n\n\nmodel, history_df = train_model(\n    model,\n    train_loader,\n    val_loader\n)\n\nhistory_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:05:14.281056Z","iopub.execute_input":"2026-09-16T18:05:14.281740Z","iopub.status.idle":"2026-09-16T18:12:01.543287Z","shell.execute_reply.started":"2026-09-16T18:05:14.281710Z","shell.execute_reply":"2026-09-16T18:12:01.542418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nimport matplotlib.pyplot as plt\n# 7. Evaluate Model Performance\nval_labels, val_probabilities = collect_predictions(model, val_loader)\nthresholds = np.linspace(0.10, 0.90, 81)\nthreshold = max(thresholds, key=lambda value: f1_score(val_labels, val_probabilities >= value, zero_division=0))\n\ntest_labels, test_probabilities = collect_predictions(model, test_loader)\ntest_predictions = (test_probabilities >= threshold).astype(int)\ntn, fp, fn, tp = confusion_matrix(test_labels, test_predictions, labels=[0, 1]).ravel()\nmetrics = {\n    \"threshold\": float(threshold),\n    \"accuracy\": float(accuracy_score(test_labels, test_predictions)),\n    \"precision\": float(precision_score(test_labels, test_predictions, zero_division=0)),\n    \"recall_sensitivity\": float(recall_score(test_labels, test_predictions, zero_division=0)),\n    \"specificity\": float(tn / max(tn + fp, 1)),\n    \"f1\": float(f1_score(test_labels, test_predictions, zero_division=0)),\n    \"roc_auc\": safe_auc(test_labels, test_probabilities),\n    \"confusion_matrix\": [[int(tn), int(fp)], [int(fn), int(tp)]],\n}\nprint(json.dumps(metrics, indent=2))\n\nfig, axes = plt.subplots(1, 2, figsize=(10, 4))\naxes[0].plot(history_df[\"epoch\"], history_df[\"train_loss\"], marker=\"o\")\naxes[0].set(title=\"Training loss\", xlabel=\"Epoch\", ylabel=\"BCE loss\")\naxes[1].imshow([[tn, fp], [fn, tp]], cmap=\"Blues\")\naxes[1].set(title=\"Test confusion matrix\", xlabel=\"Predicted\", ylabel=\"Actual\")\nfor row in range(2):\n    for column in range(2):\n        axes[1].text(column, row, [[tn, fp], [fn, tp]][row][column], ha=\"center\", va=\"center\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:12:19.633075Z","iopub.execute_input":"2026-09-16T18:12:19.633824Z","iopub.status.idle":"2026-09-16T18:12:41.703286Z","shell.execute_reply.started":"2026-09-16T18:12:19.633790Z","shell.execute_reply":"2026-09-16T18:12:41.702411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 8. Make Predictions: image quality, robust evaluation, and abstention\n\nfrom PIL import Image\n\n\ndef image_quality(image):\n\n    image = np.asarray(\n        image.convert(\"RGB\")\n    ).astype(np.float32) / 255.0\n\n    gray = image.mean(axis=2)\n\n    laplacian_proxy = (\n        np.abs(np.diff(gray, axis=0)).mean()\n        + np.abs(np.diff(gray, axis=1)).mean()\n    )\n\n    brightness = float(gray.mean())\n    contrast = float(gray.std())\n    coverage = float((gray > 0.03).mean())\n\n    flags = []\n\n    if laplacian_proxy < 0.025:\n        flags.append(\"possible_blur\")\n\n    if brightness < 0.10 or brightness > 0.85:\n        flags.append(\"poor_brightness\")\n\n    if contrast < 0.07:\n        flags.append(\"low_contrast\")\n\n    if coverage < 0.35:\n        flags.append(\"limited_fundus_coverage\")\n\n    return {\n        \"brightness\": brightness,\n        \"contrast\": contrast,\n        \"coverage\": coverage,\n        \"quality_flags\": flags\n    }\n\n\ndef predict_image(image_path, model=model, review_probability_band=(0.35, 0.65)):\n\n    # Load image directly from APTOS image path\n    image = Image.open(image_path).convert(\"RGB\")\n\n    # Check image quality\n    quality = image_quality(image)\n\n    # Model prediction\n    with torch.inference_mode():\n\n        input_tensor = (\n            inference_transform(image)\n            .unsqueeze(0)\n            .to(DEVICE)\n        )\n\n        # Model output: [1, 1]\n        logit = model(input_tensor).squeeze(1)\n\n        # Convert logit to probability\n        probability = float(\n            torch.sigmoid(logit).item()\n        )\n\n    # Check whether prediction is uncertain\n    uncertain = (\n        review_probability_band[0]\n        <= probability\n        <= review_probability_band[1]\n    )\n\n    # Send to manual review if:\n    # 1. Image quality is poor, OR\n    # 2. Model probability is uncertain\n    needs_review = bool(\n        quality[\"quality_flags\"] or uncertain\n    )\n\n    return {\n        \"image_path\": str(image_path),\n        \"dr_probability\": probability,\n\n        \"screening_result\":\n            \"refer_for_review\"\n            if probability >= threshold\n            else \"screen_negative\",\n\n        \"confidence\":\n            max(probability, 1 - probability),\n\n        \"needs_manual_review\": needs_review,\n\n        **quality\n    }\n\n\n# Test on one test image\nsample_result = predict_image(\n    test_df.iloc[0][\"image_path\"]\n)\n\nprint(json.dumps(sample_result, indent=2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:12:58.403324Z","iopub.execute_input":"2026-09-16T18:12:58.404005Z","iopub.status.idle":"2026-09-16T18:12:58.642399Z","shell.execute_reply.started":"2026-09-16T18:12:58.403973Z","shell.execute_reply":"2026-09-16T18:12:58.641510Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Grad-CAM-style explanation for the final convolution block\n\ndef grad_cam(image_path, model=model):\n\n    activations, gradients = [], []\n\n    # Final convolution layer of ResNet18\n    target_layer = model.layer4[-1].conv2\n\n    # Capture activations\n    forward_handle = target_layer.register_forward_hook(\n        lambda _, __, output: activations.append(output)\n    )\n\n    # Capture gradients\n    backward_handle = target_layer.register_full_backward_hook(\n        lambda _, grad_input, grad_output: gradients.append(grad_output[0])\n    )\n\n    # Clear previous gradients\n    model.zero_grad(set_to_none=True)\n\n    # Load APTOS image directly from path\n    image = Image.open(image_path).convert(\"RGB\")\n\n    # Preprocess image\n    tensor = (\n        inference_transform(image)\n        .unsqueeze(0)\n        .to(DEVICE)\n    )\n\n    # Forward pass\n    logit = model(tensor).squeeze()\n\n    # Backward pass\n    logit.backward()\n\n    # Remove hooks\n    forward_handle.remove()\n    backward_handle.remove()\n\n    # Calculate Grad-CAM weights\n    weights = gradients[0].mean(\n        dim=(2, 3),\n        keepdim=True\n    )\n\n    # Create heatmap\n    heatmap = torch.relu(\n        (weights * activations[0]).sum(dim=1)\n    ).squeeze().detach().cpu().numpy()\n\n    # Normalize heatmap\n    heatmap = (\n        heatmap - heatmap.min()\n    ) / (\n        heatmap.max() - heatmap.min() + 1e-8\n    )\n\n    # Resize heatmap to original image size\n    heatmap_image = Image.fromarray(\n        np.uint8(255 * heatmap)\n    ).resize(image.size)\n\n    heatmap_array = (\n        np.asarray(heatmap_image).astype(np.float32) / 255.0\n    )\n\n    original = (\n        np.asarray(image).astype(np.float32) / 255.0\n    )\n\n    # Overlay heatmap on original image\n    overlay = np.clip(\n        0.65 * original\n        + 0.35 * plt.cm.jet(heatmap_array)[..., :3],\n        0,\n        1\n    )\n\n    # Convert logit to DR probability\n    probability = float(\n        torch.sigmoid(logit).item()\n    )\n\n    return image, overlay, probability\n\n\n# Select one test image\nexplain_path = test_df.iloc[0][\"image_path\"]\n\noriginal_image, explanation_overlay, explanation_probability = grad_cam(\n    explain_path\n)\n\n\n# Display results\nfig, axes = plt.subplots(\n    1,\n    2,\n    figsize=(9, 4)\n)\n\naxes[0].imshow(original_image)\naxes[0].set_title(\"Input fundus\")\n\naxes[1].imshow(explanation_overlay)\naxes[1].set_title(\n    f\"Grad-CAM | DR p={explanation_probability:.2f}\"\n)\n\nfor axis in axes:\n    axis.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:13:54.026675Z","iopub.execute_input":"2026-09-16T18:13:54.027091Z","iopub.status.idle":"2026-09-16T18:13:54.570380Z","shell.execute_reply.started":"2026-09-16T18:13:54.027063Z","shell.execute_reply":"2026-09-16T18:13:54.569619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\n# Save artifacts for the future dashboard service\n\nARTIFACT_DIR = Path(\"/kaggle/working/artifacts\")\nARTIFACT_DIR.mkdir(parents=True, exist_ok=True)\n\ncheckpoint_path = ARTIFACT_DIR / \"retino_resnet18.pt\"\nmetadata_path = ARTIFACT_DIR / \"model_metadata.json\"\n\n\n# Save model\ntorch.save(\n    {\n        \"model_state_dict\": model.state_dict(),\n        \"threshold\": threshold\n    },\n    checkpoint_path\n)\n\n\n# Save metadata\nmetadata = {\n    \"problem_statement_id\": \"26038\",\n    \"task\": \"referable diabetic retinopathy screening\",\n    \"model\": \"torchvision.resnet18\",\n    \"image_size\": IMAGE_SIZE,\n    \"threshold\": float(threshold),\n    \"metrics\": metrics,\n    \"device_used\": str(DEVICE),\n    \"quality_flags\": [\n        \"possible_blur\",\n        \"poor_brightness\",\n        \"low_contrast\",\n        \"limited_fundus_coverage\"\n    ],\n    \"review_policy\": (\n        \"abstain when quality flags exist or \"\n        \"probability is between 0.35 and 0.65\"\n    ),\n    \"clinical_note\": (\n        \"Research prototype only. Not a medical diagnosis \"\n        \"or substitute for clinician review.\"\n    ),\n}\n\n\nmetadata_path.write_text(\n    json.dumps(metadata, indent=2),\n    encoding=\"utf-8\"\n)\n\n\nprint(f\"Saved model: {checkpoint_path.resolve()}\")\nprint(f\"Saved metadata: {metadata_path.resolve()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-16T18:14:09.009355Z","iopub.execute_input":"2026-09-16T18:14:09.009792Z","iopub.status.idle":"2026-09-16T18:14:09.080844Z","shell.execute_reply.started":"2026-09-16T18:14:09.009765Z","shell.execute_reply":"2026-09-16T18:14:09.080213Z"}},"outputs":[],"execution_count":null}]}