{"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":"#1\n\nimport os\n\nprint(\"Everything available under /kaggle/input/:\\n\")\nfor folder in os.listdir('/kaggle/input'):\n    full_path = os.path.join('/kaggle/input', folder)\n    print(f\"📁 {folder}\")\n    try:\n        contents = os.listdir(full_path)\n        for item in contents[:10]:\n            print(f\"    - {item}\")\n        if len(contents) > 10:\n            print(f\"    ... and {len(contents) - 10} more\")\n    except Exception as e:\n        print(f\"    (couldn't list contents: {e})\")\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T10:58:13.09704Z","iopub.execute_input":"2026-06-19T10:58:13.097341Z","iopub.status.idle":"2026-06-19T10:58:13.107262Z","shell.execute_reply.started":"2026-06-19T10:58:13.097302Z","shell.execute_reply":"2026-06-19T10:58:13.106425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Inside datasets/jaycejamesmayo:\")\nfor f in os.listdir('/kaggle/input/datasets/jaycejamesmayo'):\n    print(\" -\", f)\n\nprint(\"\\nInside datasets/andrewmvd:\")\nfor f in os.listdir('/kaggle/input/datasets/andrewmvd'):\n    print(\" -\", f)\n\nprint(\"\\nInside competitions/diabetic-retinopathy-detection:\")\nfor f in os.listdir('/kaggle/input/competitions/diabetic-retinopathy-detection'):\n    print(\" -\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:20:36.495886Z","iopub.execute_input":"2026-06-19T05:20:36.496803Z","iopub.status.idle":"2026-06-19T05:20:36.506147Z","shell.execute_reply.started":"2026-06-19T05:20:36.496768Z","shell.execute_reply":"2026-06-19T05:20:36.505376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"EyePACS train zip part sizes (MB):\")\neyepacs_path = '/kaggle/input/competitions/diabetic-retinopathy-detection'\nfor f in sorted(os.listdir(eyepacs_path)):\n    if 'train' in f:\n        size_mb = os.path.getsize(os.path.join(eyepacs_path, f)) / (1024*1024)\n        print(f\"  {f}: {size_mb:.1f} MB\")\n\nprint(\"\\nInside ODIR folder:\")\nodir_path = '/kaggle/input/datasets/andrewmvd/ocular-disease-recognition-odir5k'\nfor f in os.listdir(odir_path):\n    print(\" -\", f)\n\nprint(\"\\nInside RFMiD folder:\")\nrfmid_path = '/kaggle/input/datasets/andrewmvd/retinal-disease-classification'\nfor f in os.listdir(rfmid_path):\n    print(\" -\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:25:18.926323Z","iopub.execute_input":"2026-06-19T05:25:18.927076Z","iopub.status.idle":"2026-06-19T05:25:18.972136Z","shell.execute_reply.started":"2026-06-19T05:25:18.927044Z","shell.execute_reply":"2026-06-19T05:25:18.971385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#4\nimport os\nimport zipfile\n\nclass MultiPartFile:\n    \"\"\"Lets us read across split zip parts as if they were one file, without merging them on disk.\"\"\"\n    def __init__(self, parts):\n        self.parts = parts\n        self.part_sizes = [os.path.getsize(p) for p in parts]\n        self.cumulative = [0]\n        for s in self.part_sizes:\n            self.cumulative.append(self.cumulative[-1] + s)\n        self.total_size = self.cumulative[-1]\n        self.pos = 0\n        self._fh_index = None\n        self._fh = None\n\n    def _locate(self, pos):\n        for i in range(len(self.parts)):\n            if self.cumulative[i] <= pos < self.cumulative[i+1]:\n                return i, pos - self.cumulative[i]\n        return len(self.parts) - 1, self.part_sizes[-1]\n\n    def seekable(self):\n        return True\n\n    def seek(self, offset, whence=0):\n        if whence == 0:\n            self.pos = offset\n        elif whence == 1:\n            self.pos += offset\n        elif whence == 2:\n            self.pos = self.total_size + offset\n        return self.pos\n\n    def tell(self):\n        return self.pos\n\n    def read(self, size=-1):\n        if size == -1 or size is None:\n            size = self.total_size - self.pos\n        result = bytearray()\n        remaining = size\n        pos = self.pos\n        while remaining > 0 and pos < self.total_size:\n            idx, offset_in_part = self._locate(pos)\n            if self._fh_index != idx:\n                if self._fh:\n                    self._fh.close()\n                self._fh = open(self.parts[idx], 'rb')\n                self._fh_index = idx\n            self._fh.seek(offset_in_part)\n            chunk = self._fh.read(min(remaining, self.part_sizes[idx] - offset_in_part))\n            if not chunk:\n                break\n            result.extend(chunk)\n            pos += len(chunk)\n            remaining -= len(chunk)\n        self.pos = pos\n        return bytes(result)\n\neyepacs_path = '/kaggle/input/competitions/diabetic-retinopathy-detection'\npart_names = [f'train.zip.00{i}' for i in range(1, 6)]\nparts = [os.path.join(eyepacs_path, p) for p in part_names]\n\nmpf = MultiPartFile(parts)\nzf = zipfile.ZipFile(mpf)\nnames = zf.namelist()\nprint(f\"Total entries found in train.zip: {len(names)}\")\nprint(\"First 5 entries:\", names[:5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T10:59:27.900997Z","iopub.execute_input":"2026-06-19T10:59:27.901863Z","iopub.status.idle":"2026-06-19T10:59:28.102558Z","shell.execute_reply.started":"2026-06-19T10:59:27.901831Z","shell.execute_reply":"2026-06-19T10:59:28.101816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\nimport pandas as pd\n\n# trainLabels.csv.zip is small, so we can just open it normally\nlabels_zip_path = '/kaggle/input/competitions/diabetic-retinopathy-detection/trainLabels.csv.zip'\nwith zipfile.ZipFile(labels_zip_path) as z:\n    csv_name = z.namelist()[0]\n    with z.open(csv_name) as f:\n        eyepacs_labels = pd.read_csv(f)\n\nprint(eyepacs_labels.head())\nprint(f\"\\nTotal labeled images: {len(eyepacs_labels)}\")\nprint(f\"\\nDR grade distribution:\")\nprint(eyepacs_labels['level'].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:36:59.91158Z","iopub.execute_input":"2026-06-19T05:36:59.912325Z","iopub.status.idle":"2026-06-19T05:36:59.947032Z","shell.execute_reply.started":"2026-06-19T05:36:59.912296Z","shell.execute_reply":"2026-06-19T05:36:59.94606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check actual file sizes for a few sample entries\nsample_entries = [n for n in names if n.endswith('.jpeg')][:5]\nfor entry in sample_entries:\n    info = zf.getinfo(entry)\n    print(f\"{entry}: {info.file_size / (1024*1024):.2f} MB (compressed: {info.compress_size / (1024*1024):.2f} MB)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:39:18.526062Z","iopub.execute_input":"2026-06-19T05:39:18.526914Z","iopub.status.idle":"2026-06-19T05:39:18.537821Z","shell.execute_reply.started":"2026-06-19T05:39:18.526882Z","shell.execute_reply":"2026-06-19T05:39:18.537094Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\n\n# Stratified sample preserving the dataset's natural class proportions\nTARGET_TOTAL = 6000\nrandom.seed(42)\n\nsampled_rows = []\nfor level, group in eyepacs_labels.groupby('level'):\n    proportion = len(group) / len(eyepacs_labels)\n    n_sample = max(1, round(TARGET_TOTAL * proportion))\n    sampled = group.sample(n=min(n_sample, len(group)), random_state=42)\n    sampled_rows.append(sampled)\n\neyepacs_subset = pd.concat(sampled_rows).reset_index(drop=True)\nprint(\"Subset class distribution:\")\nprint(eyepacs_subset['level'].value_counts().sort_index())\nprint(f\"\\nTotal images to extract: {len(eyepacs_subset)}\")\n\n# Now extract just these specific images from the multi-part zip\noutput_dir = '/kaggle/working/eyepacs_subset'\nos.makedirs(output_dir, exist_ok=True)\n\nextracted = 0\nerrors = 0\nfor i, row in eyepacs_subset.iterrows():\n    img_name = row['image']\n    # EyePACS images can be .jpeg with left/right suffix already in the name\n    entry_path = f\"train/{img_name}.jpeg\"\n    try:\n        data = zf.read(entry_path)\n        with open(os.path.join(output_dir, f\"{img_name}.jpeg\"), 'wb') as out:\n            out.write(data)\n        extracted += 1\n        if extracted % 500 == 0:\n            print(f\"Extracted {extracted}/{len(eyepacs_subset)}...\")\n    except Exception as e:\n        errors += 1\n\nprint(f\"\\nDone! Extracted: {extracted}, Errors: {errors}\")\n\n# Save the label mapping for this subset, with binary DR label added\neyepacs_subset['DR_binary'] = (eyepacs_subset['level'] > 0).astype(int)\neyepacs_subset.to_csv('/kaggle/working/eyepacs_subset_labels.csv', index=False)\nprint(\"\\nLabel file saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:42:26.207832Z","iopub.execute_input":"2026-06-19T05:42:26.208704Z","iopub.status.idle":"2026-06-19T05:44:54.274266Z","shell.execute_reply.started":"2026-06-19T05:42:26.20867Z","shell.execute_reply":"2026-06-19T05:44:54.2735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rfmid_path = '/kaggle/input/datasets/andrewmvd/retinal-disease-classification'\n\nfor split_folder in ['Training_Set', 'Evaluation_Set', 'Test_Set']:\n    full = os.path.join(rfmid_path, split_folder)\n    print(f\"\\n{split_folder}:\")\n    contents = os.listdir(full)\n    for c in contents:\n        print(\" -\", c)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:46:27.486114Z","iopub.execute_input":"2026-06-19T05:46:27.486408Z","iopub.status.idle":"2026-06-19T05:46:27.493765Z","shell.execute_reply.started":"2026-06-19T05:46:27.486384Z","shell.execute_reply":"2026-06-19T05:46:27.492956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rfmid_path = '/kaggle/input/datasets/andrewmvd/retinal-disease-classification'\n\nfor split_folder in ['Training_Set', 'Evaluation_Set', 'Test_Set']:\n    nested = os.path.join(rfmid_path, split_folder, split_folder)\n    print(f\"\\n{split_folder}/{split_folder}:\")\n    contents = os.listdir(nested)\n    for c in contents:\n        print(\" -\", c)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:47:33.788562Z","iopub.execute_input":"2026-06-19T05:47:33.788837Z","iopub.status.idle":"2026-06-19T05:47:33.826734Z","shell.execute_reply.started":"2026-06-19T05:47:33.788816Z","shell.execute_reply":"2026-06-19T05:47:33.825998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nrfmid_path = '/kaggle/input/datasets/andrewmvd/retinal-disease-classification'\n\nrfmid_train = pd.read_csv(os.path.join(rfmid_path, 'Training_Set/Training_Set/RFMiD_Training_Labels.csv'))\nrfmid_val   = pd.read_csv(os.path.join(rfmid_path, 'Evaluation_Set/Evaluation_Set/RFMiD_Validation_Labels.csv'))\nrfmid_test  = pd.read_csv(os.path.join(rfmid_path, 'Test_Set/Test_Set/RFMiD_Testing_Labels.csv'))\n\nprint(\"Columns:\", list(rfmid_train.columns))\nprint(f\"\\nTrain: {len(rfmid_train)} | Val: {len(rfmid_val)} | Test: {len(rfmid_test)}\")\n\ntrain_imgs_path = os.path.join(rfmid_path, 'Training_Set/Training_Set/Training')\nprint(\"\\nFirst 5 files in Training image folder:\")\nfor f in os.listdir(train_imgs_path)[:5]:\n    print(\" -\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:49:40.030225Z","iopub.execute_input":"2026-06-19T05:49:40.030874Z","iopub.status.idle":"2026-06-19T05:49:40.133687Z","shell.execute_reply.started":"2026-06-19T05:49:40.030839Z","shell.execute_reply":"2026-06-19T05:49:40.132862Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"odir_path = '/kaggle/input/datasets/andrewmvd/ocular-disease-recognition-odir5k'\n\nimport pandas as pd\nodir_df = pd.read_csv(os.path.join(odir_path, 'full_df.csv'))\nprint(\"Columns:\", list(odir_df.columns))\nprint(f\"\\nTotal rows: {len(odir_df)}\")\nprint(odir_df.head())\n\nprint(\"\\nFirst 5 files in preprocessed_images:\")\nimg_folder = os.path.join(odir_path, 'preprocessed_images')\nfor f in os.listdir(img_folder)[:5]:\n    print(\" -\", f)\nprint(f\"\\nTotal files in preprocessed_images: {len(os.listdir(img_folder))}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:51:57.749643Z","iopub.execute_input":"2026-06-19T05:51:57.750479Z","iopub.status.idle":"2026-06-19T05:51:57.913227Z","shell.execute_reply.started":"2026-06-19T05:51:57.750448Z","shell.execute_reply":"2026-06-19T05:51:57.912342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Confirm your uploaded files are where we expect\ncustom_path = '/kaggle/input/datasets/jaycejamesmayo/retinal-project-files'\nprint(\"Your uploaded files:\")\nfor f in os.listdir(custom_path):\n    print(\" -\", f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:55:23.515228Z","iopub.execute_input":"2026-06-19T05:55:23.515852Z","iopub.status.idle":"2026-06-19T05:55:23.525768Z","shell.execute_reply.started":"2026-06-19T05:55:23.515821Z","shell.execute_reply":"2026-06-19T05:55:23.525048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#2\nimport torch\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Device: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T10:58:38.007964Z","iopub.execute_input":"2026-06-19T10:58:38.008547Z","iopub.status.idle":"2026-06-19T10:58:42.954654Z","shell.execute_reply.started":"2026-06-19T10:58:38.008517Z","shell.execute_reply":"2026-06-19T10:58:42.953698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#3\nimport timm\n\nmodel_disease = timm.create_model('efficientnet_b3', pretrained=False, num_classes=4)\ncheckpoint_path = '/kaggle/input/datasets/jaycejamesmayo/retinal-project-files/Disease_EfficientNetB3_v2.pth'\nmodel_disease.load_state_dict(torch.load(checkpoint_path, map_location=device))\nmodel_disease = model_disease.to(device)\n\ntotal_params = sum(p.numel() for p in model_disease.parameters())\nprint(\"Loaded your trained epoch-3 checkpoint!\")\nprint(f\"Parameters: {total_params:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T10:59:04.405475Z","iopub.execute_input":"2026-06-19T10:59:04.406215Z","iopub.status.idle":"2026-06-19T10:59:04.973665Z","shell.execute_reply.started":"2026-06-19T10:59:04.406183Z","shell.execute_reply":"2026-06-19T10:59:04.972623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport pandas as pd\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\n\n# ============ PATHS ============\nrfmid_path = '/kaggle/input/datasets/andrewmvd/retinal-disease-classification'\nodir_path = '/kaggle/input/datasets/andrewmvd/ocular-disease-recognition-odir5k'\n\nrfmid_train = pd.read_csv(os.path.join(rfmid_path, 'Training_Set/Training_Set/RFMiD_Training_Labels.csv'))\nrfmid_val   = pd.read_csv(os.path.join(rfmid_path, 'Evaluation_Set/Evaluation_Set/RFMiD_Validation_Labels.csv'))\nrfmid_test  = pd.read_csv(os.path.join(rfmid_path, 'Test_Set/Test_Set/RFMiD_Testing_Labels.csv'))\n\nrfmid_train_imgs = os.path.join(rfmid_path, 'Training_Set/Training_Set/Training')\nrfmid_val_imgs   = os.path.join(rfmid_path, 'Evaluation_Set/Evaluation_Set/Validation')\nrfmid_test_imgs  = os.path.join(rfmid_path, 'Test_Set/Test_Set/Test')\n\nodir_df = pd.read_csv(os.path.join(odir_path, 'full_df.csv'))\nodir_imgs = os.path.join(odir_path, 'preprocessed_images')\n\neyepacs_subset = pd.read_csv('/kaggle/working/eyepacs_subset_labels.csv')\neyepacs_imgs = '/kaggle/working/eyepacs_subset'\n\n# ============ SPLIT ODIR (same stratified method as before, same seed) ============\nodir_df['strat_key'] = (\n    odir_df['D'].astype(int).astype(str) +\n    odir_df['G'].astype(int).astype(str) +\n    odir_df['A'].astype(int).astype(str) +\n    odir_df['H'].astype(int).astype(str)\n)\nodir_train_df, odir_temp_df = train_test_split(odir_df, test_size=0.30, stratify=odir_df['strat_key'], random_state=42)\nodir_val_df, odir_test_df = train_test_split(odir_temp_df, test_size=0.50, stratify=odir_temp_df['strat_key'], random_state=42)\n\n# ============ SPLIT EyePACS subset (70/15/15, stratified by DR_binary, per proposal section 4.2.2) ============\neyepacs_train_df, eyepacs_temp_df = train_test_split(eyepacs_subset, test_size=0.30, stratify=eyepacs_subset['DR_binary'], random_state=42)\neyepacs_val_df, eyepacs_test_df = train_test_split(eyepacs_temp_df, test_size=0.50, stratify=eyepacs_temp_df['DR_binary'], random_state=42)\n\nprint(f\"EyePACS subset split — train: {len(eyepacs_train_df)} | val: {len(eyepacs_val_df)} | test: {len(eyepacs_test_df)}\")\n\n# ============ COMBINED DATASET CLASS ============\nclass CombinedRetinalDatasetV2(Dataset):\n    def __init__(self, rfmid_df, rfmid_img_folder,\n                 odir_df, odir_img_folder,\n                 eyepacs_df, eyepacs_img_folder,\n                 transform=None):\n        self.transform = transform\n        self.samples = []\n\n        # RFMiD: DR, ODC(Glaucoma), ARMD, 0.0 (no real HTN in RFMiD)\n        for _, row in rfmid_df.iterrows():\n            img_path = os.path.join(rfmid_img_folder, f\"{int(row['ID'])}.png\")\n            if os.path.exists(img_path):\n                label = [float(row['DR']), float(row['ODC']), float(row['ARMD']), 0.0]\n                self.samples.append((img_path, label))\n\n        # ODIR: D, G, A, H\n        for _, row in odir_df.iterrows():\n            img_path = os.path.join(odir_img_folder, str(row['filename']))\n            if os.path.exists(img_path):\n                label = [float(row['D']), float(row['G']), float(row['A']), float(row['H'])]\n                self.samples.append((img_path, label))\n\n        # EyePACS: only contributes DR signal — Glaucoma/AMD/HTN unknown, left at 0.0\n        # (these images have no ground truth for the other 3 diseases)\n        for _, row in eyepacs_df.iterrows():\n            img_path = os.path.join(eyepacs_img_folder, f\"{row['image']}.jpeg\")\n            if os.path.exists(img_path):\n                label = [float(row['DR_binary']), 0.0, 0.0, 0.0]\n                self.samples.append((img_path, label))\n\n        print(f\"Total samples loaded: {len(self.samples)}\")\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        img_path, label = self.samples[idx]\n        try:\n            img = Image.open(img_path).convert('RGB')\n        except Exception:\n            img = Image.new('RGB', (300, 300))\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(label, dtype=torch.float32)\n\ntransform_train = transforms.Compose([\n    transforms.Resize((300, 300)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\ntransform_val = transforms.Compose([\n    transforms.Resize((300, 300)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\ntrain_dataset = CombinedRetinalDatasetV2(rfmid_train, rfmid_train_imgs, odir_train_df, odir_imgs, eyepacs_train_df, eyepacs_imgs, transform_train)\nval_dataset   = CombinedRetinalDatasetV2(rfmid_val, rfmid_val_imgs, odir_val_df, odir_imgs, eyepacs_val_df, eyepacs_imgs, transform_val)\ntest_dataset  = CombinedRetinalDatasetV2(rfmid_test, rfmid_test_imgs, odir_test_df, odir_imgs, eyepacs_test_df, eyepacs_imgs, transform_val)\n\ntrain_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=2)\nval_loader   = DataLoader(val_dataset, batch_size=16, shuffle=False, num_workers=2)\ntest_loader  = DataLoader(test_dataset, batch_size=16, shuffle=False, num_workers=2)\n\nprint(f\"\\nTrain batches: {len(train_loader)} | Val batches: {len(val_loader)} | Test batches: {len(test_loader)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T05:59:20.737258Z","iopub.execute_input":"2026-06-19T05:59:20.73809Z","iopub.status.idle":"2026-06-19T05:59:50.777811Z","shell.execute_reply.started":"2026-06-19T05:59:20.738054Z","shell.execute_reply":"2026-06-19T05:59:50.776837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch.nn as nn\n\n# ============ RECOMPUTE CLASS WEIGHTS for the new combined dataset ============\nall_labels = np.array([s[1] for s in train_dataset.samples])\ndisease_names = ['Diabetic Retinopathy', 'Glaucoma', 'AMD', 'Hypertensive Retinopathy']\n\nclass_weights = []\nprint(\"Class weight (higher = rarer disease):\")\nfor i, name in enumerate(disease_names):\n    pos_count = all_labels[:, i].sum()\n    total = len(all_labels)\n    weight = total / (2 * pos_count) if pos_count > 0 else 1.0\n    class_weights.append(weight)\n    print(f\"  {name}: {weight:.2f} (positive cases: {int(pos_count)})\")\n\nclass_weights_tensor = torch.tensor(class_weights, dtype=torch.float32).to(device)\n\n# ============ FOCAL LOSS (same gamma=2.0 as before) ============\nclass FocalLoss(nn.Module):\n    def __init__(self, gamma=2.0, weight=None):\n        super().__init__()\n        self.gamma = gamma\n        self.weight = weight\n\n    def forward(self, inputs, targets):\n        bce = nn.functional.binary_cross_entropy_with_logits(inputs, targets, reduction='none')\n        probs = torch.sigmoid(inputs)\n        pt = torch.where(targets == 1, probs, 1 - probs)\n        focal_term = (1 - pt) ** self.gamma\n        loss = focal_term * bce\n        if self.weight is not None:\n            loss = loss * self.weight\n        return loss.mean()\n\ncriterion = FocalLoss(gamma=2.0, weight=class_weights_tensor)\nprint(\"\\nFocal loss ready with updated class weights\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T06:01:18.239906Z","iopub.execute_input":"2026-06-19T06:01:18.240639Z","iopub.status.idle":"2026-06-19T06:01:18.255898Z","shell.execute_reply.started":"2026-06-19T06:01:18.240606Z","shell.execute_reply":"2026-06-19T06:01:18.25491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\n\noptimizer = optim.Adam(model_disease.parameters(), lr=0.0001)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\nnum_epochs = 15\nbest_val_loss = float('inf')\nsave_path = '/kaggle/working/Disease_EfficientNetB3_v3.pth'\n\nprint(\"Continuing training from epoch-3 checkpoint, with EyePACS added...\")\nprint(f\"Epochs: {num_epochs}\")\nprint(f\"Train batches: {len(train_loader)}\")\nprint(\"-\" * 50)\n\nfor epoch in range(num_epochs):\n    model_disease.train()\n    train_loss = 0.0\n    for i, (images, labels) in enumerate(train_loader):\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model_disease(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item()\n        if i % 100 == 0:\n            print(f\"Epoch {epoch+1}/{num_epochs} | Batch {i}/{len(train_loader)} | Loss: {loss.item():.4f}\")\n\n    model_disease.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model_disease(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item()\n\n    avg_train = train_loss / len(train_loader)\n    avg_val = val_loss / len(val_loader)\n    scheduler.step(avg_val)\n\n    print(f\"\\nEpoch {epoch+1} complete!\")\n    print(f\"Train Loss: {avg_train:.4f} | Val Loss: {avg_val:.4f}\")\n\n    if avg_val < best_val_loss:\n        best_val_loss = avg_val\n        torch.save(model_disease.state_dict(), save_path)\n        print(f\"Best model saved! Val Loss: {avg_val:.4f}\")\n    print(\"-\" * 50)\n\nprint(\"Training complete!\")\nprint(f\"Best model saved to: {save_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T06:03:19.321029Z","iopub.execute_input":"2026-06-19T06:03:19.32189Z","iopub.status.idle":"2026-06-19T07:06:00.096779Z","shell.execute_reply.started":"2026-06-19T06:03:19.321857Z","shell.execute_reply":"2026-06-19T07:06:00.094954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Reload the actual best checkpoint from disk (epoch 1, val loss 0.3313)\nmodel_disease.load_state_dict(torch.load('/kaggle/working/Disease_EfficientNetB3_v3.pth', map_location=device))\nmodel_disease.eval()\nprint(\"Reloaded the saved best checkpoint (epoch 1, val loss 0.3313)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:07:22.349396Z","iopub.execute_input":"2026-06-19T07:07:22.349864Z","iopub.status.idle":"2026-06-19T07:07:22.531768Z","shell.execute_reply.started":"2026-06-19T07:07:22.349828Z","shell.execute_reply":"2026-06-19T07:07:22.530971Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import roc_auc_score, confusion_matrix, matthews_corrcoef, f1_score\n\ndisease_names = ['Diabetic Retinopathy', 'Glaucoma', 'AMD', 'Hypertensive Retinopathy']\n\n# ============ Get probabilities on VAL set ============\nval_probs_list, val_labels_list = [], []\nprint(\"Running on validation set...\")\nwith torch.no_grad():\n    for i, (images, labels) in enumerate(val_loader):\n        images = images.to(device)\n        outputs = model_disease(images)\n        probs = torch.sigmoid(outputs).cpu().numpy()\n        val_probs_list.append(probs)\n        val_labels_list.append(labels.numpy())\n        if i % 30 == 0:\n            print(f\"Val batch {i}/{len(val_loader)} done...\")\n\nval_probs = np.concatenate(val_probs_list, axis=0)\nval_labels = np.concatenate(val_labels_list, axis=0)\nprint(\"Validation pass complete.\\n\")\n\n# ============ Calibrate thresholds (sensitivity floor 0.85) ============\nmin_sensitivity = 0.85\ncalibrated_thresholds_v3 = {}\n\nprint(f\"{'Disease':<28}{'Best Threshold':>16}{'Val Sens':>12}{'Val Spec':>12}{'Note':>20}\")\nprint(\"-\" * 90)\n\nfor i, name in enumerate(disease_names):\n    y_true = val_labels[:, i]\n    y_prob = val_probs[:, i]\n    best_spec, best_thresh, best_sens, found_valid = -1, 0.5, 0, False\n\n    for thresh in np.arange(0.05, 0.96, 0.01):\n        y_pred = (y_prob >= thresh).astype(int)\n        tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n        sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n        spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n        if sens >= min_sensitivity and spec > best_spec:\n            best_spec, best_thresh, best_sens, found_valid = spec, thresh, sens, True\n\n    note = \"meets 85% sens\" if found_valid else \"CANNOT reach 85% sens\"\n    if not found_valid:\n        best_j = -1\n        for thresh in np.arange(0.05, 0.96, 0.01):\n            y_pred = (y_prob >= thresh).astype(int)\n            tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n            sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n            if sens > best_j:\n                best_j, best_thresh, best_sens = sens, thresh, sens\n                best_spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n\n    calibrated_thresholds_v3[name] = best_thresh\n    print(f\"{name:<28}{best_thresh:>16.2f}{best_sens:>12.3f}{best_spec:>12.3f}{note:>20}\")\n\n# ============ Evaluate on TEST set ============\ntest_probs_list, test_labels_list = [], []\nprint(\"\\nRunning on test set...\")\nwith torch.no_grad():\n    for i, (images, labels) in enumerate(test_loader):\n        images = images.to(device)\n        outputs = model_disease(images)\n        probs = torch.sigmoid(outputs).cpu().numpy()\n        test_probs_list.append(probs)\n        test_labels_list.append(labels.numpy())\n        if i % 30 == 0:\n            print(f\"Test batch {i}/{len(test_loader)} done...\")\n\ntest_probs = np.concatenate(test_probs_list, axis=0)\ntest_labels = np.concatenate(test_labels_list, axis=0)\nprint(\"Test pass complete.\\n\")\n\nprint(f\"{'Disease':<28}{'Sens':>8}{'Spec':>8}{'PPV':>8}{'NPV':>8}{'AUC':>8}{'F1':>8}{'MCC':>8}\")\nprint(\"-\" * 80)\nfor i, name in enumerate(disease_names):\n    y_true = test_labels[:, i]\n    y_prob = test_probs[:, i]\n    thresh = calibrated_thresholds_v3[name]\n    y_pred = (y_prob >= thresh).astype(int)\n    tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n    sens = tp / (tp + fn) if (tp + fn) > 0 else float('nan')\n    spec = tn / (tn + fp) if (tn + fp) > 0 else float('nan')\n    ppv = tp / (tp + fp) if (tp + fp) > 0 else float('nan')\n    npv = tn / (tn + fn) if (tn + fn) > 0 else float('nan')\n    auc = roc_auc_score(y_true, y_prob) if len(np.unique(y_true)) > 1 else float('nan')\n    f1 = f1_score(y_true, y_pred, zero_division=0)\n    mcc = matthews_corrcoef(y_true, y_pred)\n    print(f\"{name:<28}{sens:>8.3f}{spec:>8.3f}{ppv:>8.3f}{npv:>8.3f}{auc:>8.3f}{f1:>8.3f}{mcc:>8.3f}  (thresh={thresh:.2f})\")\n    print(f\"   (positive cases in test set: {int(y_true.sum())} / {len(y_true)})\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:07:41.036316Z","iopub.execute_input":"2026-06-19T07:07:41.037013Z","iopub.status.idle":"2026-06-19T07:11:35.394267Z","shell.execute_reply.started":"2026-06-19T07:07:41.036981Z","shell.execute_reply":"2026-06-19T07:11:35.393492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import roc_auc_score, confusion_matrix\n\n# Test set composition, in the exact order they were added: RFMiD (640) + ODIR (959) + EyePACS (900) = 2499\nrfmid_test_count = 640\nodir_test_count = 959\neyepacs_test_count = 900\n\nsources = {\n    'RFMiD': (0, rfmid_test_count),\n    'ODIR': (rfmid_test_count, rfmid_test_count + odir_test_count),\n    'EyePACS': (rfmid_test_count + odir_test_count, rfmid_test_count + odir_test_count + eyepacs_test_count)\n}\n\ndisease_names = ['Diabetic Retinopathy', 'Glaucoma', 'AMD', 'Hypertensive Retinopathy']\n\nfor i, name in enumerate(disease_names):\n    print(f\"\\n=== {name} (threshold={calibrated_thresholds_v3[name]:.2f}) ===\")\n    for source_name, (start, end) in sources.items():\n        y_true = test_labels[start:end, i]\n        y_prob = test_probs[start:end, i]\n        if y_true.sum() == 0 or y_true.sum() == len(y_true):\n            print(f\"  {source_name}: skipped (no positive/negative cases to compute on)\")\n            continue\n        y_pred = (y_prob >= calibrated_thresholds_v3[name]).astype(int)\n        tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n        sens = tp / (tp + fn) if (tp + fn) > 0 else float('nan')\n        spec = tn / (tn + fp) if (tn + fp) > 0 else float('nan')\n        auc = roc_auc_score(y_true, y_prob) if len(np.unique(y_true)) > 1 else float('nan')\n        print(f\"  {source_name:<10} n={end-start:<5} sens={sens:.3f}  spec={spec:.3f}  auc={auc:.3f}  (positives: {int(y_true.sum())})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:14:25.171932Z","iopub.execute_input":"2026-06-19T07:14:25.172726Z","iopub.status.idle":"2026-06-19T07:14:25.205603Z","shell.execute_reply.started":"2026-06-19T07:14:25.17269Z","shell.execute_reply":"2026-06-19T07:14:25.204744Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import roc_auc_score, confusion_matrix, f1_score, matthews_corrcoef\n\n# Same source ranges as before (val set has identical composition to test set)\nrfmid_count = 640\nodir_count = 959\neyepacs_count = 900\n\nsources = {\n    'RFMiD': (0, rfmid_count),\n    'ODIR': (rfmid_count, rfmid_count + odir_count),\n    'EyePACS': (rfmid_count + odir_count, rfmid_count + odir_count + eyepacs_count)\n}\n\ndisease_names = ['Diabetic Retinopathy', 'Glaucoma', 'AMD', 'Hypertensive Retinopathy']\nmin_sensitivity = 0.85\n\nper_source_thresholds = {}\n\nprint(\"Per-source calibrated thresholds (sensitivity floor 0.85):\\n\")\nfor i, name in enumerate(disease_names):\n    per_source_thresholds[name] = {}\n    print(f\"--- {name} ---\")\n    for source_name, (start, end) in sources.items():\n        y_true = val_labels[start:end, i]\n        y_prob = val_probs[start:end, i]\n\n        if y_true.sum() == 0 or y_true.sum() == len(y_true):\n            print(f\"  {source_name}: no valid positive/negative mix to calibrate — skipped\")\n            continue\n\n        best_spec, best_thresh, best_sens, found_valid = -1, 0.5, 0, False\n        for thresh in np.arange(0.05, 0.96, 0.01):\n            y_pred = (y_prob >= thresh).astype(int)\n            tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n            sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n            spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n            if sens >= min_sensitivity and spec > best_spec:\n                best_spec, best_thresh, best_sens, found_valid = spec, thresh, sens, True\n\n        if not found_valid:\n            best_j = -1\n            for thresh in np.arange(0.05, 0.96, 0.01):\n                y_pred = (y_prob >= thresh).astype(int)\n                tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n                sens = tp / (tp + fn) if (tp + fn) > 0 else 0\n                if sens > best_j:\n                    best_j, best_thresh, best_sens = sens, thresh, sens\n                    best_spec = tn / (tn + fp) if (tn + fp) > 0 else 0\n\n        per_source_thresholds[name][source_name] = best_thresh\n        note = \"OK\" if found_valid else \"fallback (couldn't hit 85%)\"\n        print(f\"  {source_name:<10} thresh={best_thresh:.2f}  sens={best_sens:.3f}  spec={best_spec:.3f}  [{note}]\")\n    print()\n\n# ============ Re-evaluate TEST set using per-source thresholds ============\nprint(\"\\n\" + \"=\"*80)\nprint(\"TEST RESULTS using per-source calibrated thresholds\")\nprint(\"=\"*80)\n\nfor i, name in enumerate(disease_names):\n    print(f\"\\n=== {name} ===\")\n    for source_name, (start, end) in sources.items():\n        y_true = test_labels[start:end, i]\n        y_prob = test_probs[start:end, i]\n\n        if y_true.sum() == 0 or y_true.sum() == len(y_true):\n            continue\n        if source_name not in per_source_thresholds[name]:\n            print(f\"  {source_name}: no calibrated threshold available — skipped\")\n            continue\n\n        thresh = per_source_thresholds[name][source_name]\n        y_pred = (y_prob >= thresh).astype(int)\n        tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()\n        sens = tp / (tp + fn) if (tp + fn) > 0 else float('nan')\n        spec = tn / (tn + fp) if (tn + fp) > 0 else float('nan')\n        auc = roc_auc_score(y_true, y_prob) if len(np.unique(y_true)) > 1 else float('nan')\n        f1 = f1_score(y_true, y_pred, zero_division=0)\n        print(f\"  {source_name:<10} thresh={thresh:.2f}  sens={sens:.3f}  spec={spec:.3f}  auc={auc:.3f}  f1={f1:.3f}  (n={end-start}, positives={int(y_true.sum())})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:19:20.956725Z","iopub.execute_input":"2026-06-19T07:19:20.957582Z","iopub.status.idle":"2026-06-19T07:19:21.547019Z","shell.execute_reply.started":"2026-06-19T07:19:20.957542Z","shell.execute_reply":"2026-06-19T07:19:21.546238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\nfrom torchvision import models\n\n# ============ Load IQA model architecture (MobileNetV2, 2-class) ============\nmodel_iqa = models.mobilenet_v2(weights=None)\nmodel_iqa.classifier[1] = nn.Linear(model_iqa.last_channel, 2)\n\niqa_checkpoint_path = '/kaggle/input/datasets/jaycejamesmayo/retinal-project-files/IQA_MobileNetV2.pth'\nmodel_iqa.load_state_dict(torch.load(iqa_checkpoint_path, map_location=device))\nmodel_iqa = model_iqa.to(device)\nmodel_iqa.eval()\n\ntotal_params = sum(p.numel() for p in model_iqa.parameters())\nprint(f\"IQA model loaded! Parameters: {total_params:,}\")\n\n# ============ Check EyeQ label files against what's actually in train.zip ============\neyeq_train = pd.read_csv('/kaggle/input/datasets/jaycejamesmayo/retinal-project-files/Label_EyeQ_train.csv')\neyeq_test = pd.read_csv('/kaggle/input/datasets/jaycejamesmayo/retinal-project-files/Label_EyeQ_test.csv')\n\nzip_image_names = set(n.replace('train/', '') for n in names if n.endswith('.jpeg'))\n\neyeq_train_matches = eyeq_train['image'].apply(lambda x: x in zip_image_names).sum()\neyeq_test_matches = eyeq_test['image'].apply(lambda x: x in zip_image_names).sum()\n\nprint(f\"\\nEyeQ train labels: {len(eyeq_train)} total, {eyeq_train_matches} found in our accessible EyePACS train.zip\")\nprint(f\"EyeQ test labels: {len(eyeq_test)} total, {eyeq_test_matches} found in our accessible EyePACS train.zip\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:29:20.341527Z","iopub.execute_input":"2026-06-19T07:29:20.341797Z","iopub.status.idle":"2026-06-19T07:29:20.550118Z","shell.execute_reply.started":"2026-06-19T07:29:20.341775Z","shell.execute_reply":"2026-06-19T07:29:20.549102Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"eyepacs_subset_filenames = set(eyepacs_subset['image'])\neyeq_train_filenames = set(eyeq_train['image'].str.replace('.jpeg', '', regex=False))\n\noverlap = eyepacs_subset_filenames & eyeq_train_filenames\nprint(f\"Our EyePACS DR subset: {len(eyepacs_subset_filenames)} images\")\nprint(f\"EyeQ train-labeled images: {len(eyeq_train_filenames)} images\")\nprint(f\"Overlap (images in our subset that have a real EyeQ quality label): {len(overlap)}\")\nprint(f\"Coverage: {len(overlap) / len(eyepacs_subset_filenames) * 100:.1f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T07:31:32.587582Z","iopub.execute_input":"2026-06-19T07:31:32.588514Z","iopub.status.idle":"2026-06-19T07:31:32.607618Z","shell.execute_reply.started":"2026-06-19T07:31:32.588479Z","shell.execute_reply":"2026-06-19T07:31:32.606606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import io\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom sklearn.metrics import confusion_matrix, cohen_kappa_score, roc_auc_score\n\n# ============ Map EyeQ quality grades to binary Accept/Retake ============\n# quality 0 = Good, 1 = Usable -> Accept (0)\n# quality 2 = Reject -> Retake (1)\neyeq_train['binary_label'] = (eyeq_train['quality'] == 2).astype(int)\nprint(\"EyeQ train binary distribution:\")\nprint(eyeq_train['binary_label'].value_counts())\nprint(f\"  (0 = Accept/Good+Usable, 1 = Retake/Reject)\\n\")\n\n# ============ Dataset that reads directly from the zip, no extraction ============\nclass EyeQZipDataset(Dataset):\n    def __init__(self, df, zipfile_obj, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.zf = zipfile_obj\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        entry_path = f\"train/{row['image']}\"\n        try:\n            data = self.zf.read(entry_path)\n            img = Image.open(io.BytesIO(data)).convert('RGB')\n        except Exception:\n            img = Image.new('RGB', (224, 224))\n        if self.transform:\n            img = self.transform(img)\n        return img, row['binary_label']\n\niqa_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\neyeq_dataset = EyeQZipDataset(eyeq_train, zf, transform=iqa_transform)\neyeq_loader = DataLoader(eyeq_dataset, batch_size=32, shuffle=False, num_workers=0)\n\nprint(f\"Total batches to evaluate: {len(eyeq_loader)}\")\n\n# ============ Run IQA model over all 12,543 labeled images ============\nall_probs_class1 = []\nall_true = []\n\nmodel_iqa.eval()\nwith torch.no_grad():\n    for i, (images, labels) in enumerate(eyeq_loader):\n        images = images.to(device)\n        outputs = model_iqa(images)\n        probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()  # probability of class index 1\n        all_probs_class1.append(probs)\n        all_true.append(labels.numpy())\n        if i % 50 == 0:\n            print(f\"Batch {i}/{len(eyeq_loader)} done...\")\n\nall_probs_class1 = np.concatenate(all_probs_class1)\nall_true = np.concatenate(all_true)\nprint(\"Evaluation complete.\\n\")\n\n# ============ Sanity check: confirm class index 1 actually means \"Retake\" ============\nauc_check = roc_auc_score(all_true, all_probs_class1)\nprint(f\"AUC treating class index 1 as 'Retake': {auc_check:.3f}\")\n\nif auc_check < 0.5:\n    print(\"AUC below 0.5 — class indices appear flipped. Auto-correcting: using (1 - prob) instead.\")\n    final_probs = 1 - all_probs_class1\nelse:\n    print(\"AUC above 0.5 — class index 1 correctly corresponds to 'Retake'. No correction needed.\")\n    final_probs = all_probs_class1\n\n# ============ Compute rejection sensitivity + kappa at default threshold 0.5 ============\nthreshold = 0.5\npreds = (final_probs >= threshold).astype(int)\n\ntn, fp, fn, tp = confusion_matrix(all_true, preds, labels=[0, 1]).ravel()\nrejection_sensitivity = tp / (tp + fn) if (tp + fn) > 0 else float('nan')\nspecificity = tn / (tn + fp) if (tn + fp) > 0 else float('nan')\nkappa = cohen_kappa_score(all_true, preds)\nfinal_auc = roc_auc_score(all_true, final_probs)\n\nprint(f\"\\n=== Phase 3 — IQA Gate Evaluation (n={len(all_true)}) ===\")\nprint(f\"Rejection sensitivity (target ≥90%): {rejection_sensitivity:.3f}\")\nprint(f\"Specificity: {specificity:.3f}\")\nprint(f\"Cohen's kappa (target ≥0.75): {kappa:.3f}\")\nprint(f\"AUC: {final_auc:.3f}\")\nprint(f\"\\nConfusion matrix:\")\nprint(f\"  True Accept, Pred Accept: {tn}  |  True Accept, Pred Retake: {fp}\")\nprint(f\"  True Retake, Pred Accept: {fn}  |  True Retake, Pred Retake: {tp}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T08:00:05.983856Z","iopub.execute_input":"2026-06-19T08:00:05.984139Z","iopub.status.idle":"2026-06-19T08:21:31.016638Z","shell.execute_reply.started":"2026-06-19T08:00:05.984105Z","shell.execute_reply":"2026-06-19T08:21:31.015557Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Raw probability statistics (before thresholding):\")\nprint(f\"  Mean: {final_probs.mean():.4f}\")\nprint(f\"  Std:  {final_probs.std():.4f}\")\nprint(f\"  Min:  {final_probs.min():.4f}\")\nprint(f\"  Max:  {final_probs.max():.4f}\")\n\n# Show the raw model output logits (not probabilities) for a small batch,\n# to see if the model is producing varied, confident-looking outputs or flat/dead ones\nsample_images, sample_labels = next(iter(eyeq_loader))\nwith torch.no_grad():\n    sample_outputs = model_iqa(sample_images.to(device))\nprint(\"\\nRaw logits for first 10 images:\")\nprint(sample_outputs[:10].cpu().numpy())\nprint(\"\\nTrue labels for those same 10 images:\")\nprint(sample_labels[:10].numpy())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T08:25:32.875975Z","iopub.execute_input":"2026-06-19T08:25:32.876527Z","iopub.status.idle":"2026-06-19T08:25:35.565504Z","shell.execute_reply.started":"2026-06-19T08:25:32.876494Z","shell.execute_reply":"2026-06-19T08:25:35.564359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nfig, axes = plt.subplots(1, 4, figsize=(16, 4))\nfor idx in range(4):\n    img_tensor = sample_images[idx]\n    img_display = img_tensor.permute(1, 2, 0).numpy()\n    img_display = (img_display * np.array([0.229, 0.224, 0.225])) + np.array([0.485, 0.456, 0.406])\n    img_display = np.clip(img_display, 0, 1)\n    axes[idx].imshow(img_display)\n    axes[idx].set_title(f\"True label: {sample_labels[idx].item()}\")\n    axes[idx].axis('off')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T08:30:10.725061Z","iopub.execute_input":"2026-06-19T08:30:10.725699Z","iopub.status.idle":"2026-06-19T08:30:11.305509Z","shell.execute_reply.started":"2026-06-19T08:30:10.725669Z","shell.execute_reply":"2026-06-19T08:30:11.304457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import train_test_split\nimport numpy as np\n\n# ============ STRATIFIED 70/15/15 SPLIT ============\niqa_train_df, iqa_temp_df = train_test_split(\n    eyeq_train, test_size=0.30, stratify=eyeq_train['binary_label'], random_state=42\n)\niqa_val_df, iqa_test_df = train_test_split(\n    iqa_temp_df, test_size=0.50, stratify=iqa_temp_df['binary_label'], random_state=42\n)\n\nprint(f\"IQA train: {len(iqa_train_df)} | val: {len(iqa_val_df)} | test: {len(iqa_test_df)}\")\nprint(f\"\\nTrain distribution:\\n{iqa_train_df['binary_label'].value_counts()}\")\n\n# ============ DATALOADERS (reusing the zip-reading dataset class from before) ============\niqa_train_dataset = EyeQZipDataset(iqa_train_df, zf, transform=iqa_transform)\niqa_val_dataset = EyeQZipDataset(iqa_val_df, zf, transform=iqa_transform)\niqa_test_dataset = EyeQZipDataset(iqa_test_df, zf, transform=iqa_transform)\n\niqa_train_loader = DataLoader(iqa_train_dataset, batch_size=32, shuffle=True, num_workers=0)\niqa_val_loader = DataLoader(iqa_val_dataset, batch_size=32, shuffle=False, num_workers=0)\niqa_test_loader = DataLoader(iqa_test_dataset, batch_size=32, shuffle=False, num_workers=0)\n\nprint(f\"\\nTrain batches: {len(iqa_train_loader)} | Val batches: {len(iqa_val_loader)} | Test batches: {len(iqa_test_loader)}\")\n\n# ============ FRESH MODEL (don't reuse the broken checkpoint's weights) ============\nfrom torchvision import models\nmodel_iqa = models.mobilenet_v2(weights='IMAGENET1K_V1')\nmodel_iqa.classifier[1] = nn.Linear(model_iqa.last_channel, 2)\nmodel_iqa = model_iqa.to(device)\n\n# ============ CLASS-WEIGHTED LOSS — the actual fix for the collapse ============\nclass_counts = iqa_train_df['binary_label'].value_counts().sort_index()\ntotal = class_counts.sum()\nweights = torch.tensor([total / (2 * class_counts[0]), total / (2 * class_counts[1])], dtype=torch.float32).to(device)\nprint(f\"\\nClass weights (Accept, Retake): {weights.cpu().numpy()}\")\n\ncriterion = nn.CrossEntropyLoss(weight=weights)\noptimizer = optim.Adam(model_iqa.parameters(), lr=0.0001)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T08:45:25.726093Z","iopub.execute_input":"2026-06-19T08:45:25.726949Z","iopub.status.idle":"2026-06-19T08:45:26.273084Z","shell.execute_reply.started":"2026-06-19T08:45:25.726915Z","shell.execute_reply":"2026-06-19T08:45:26.272225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#5\nimport pandas as pd\n\neyeq_train = pd.read_csv('/kaggle/input/datasets/jaycejamesmayo/retinal-project-files/Label_EyeQ_train.csv')\neyeq_train['binary_label'] = (eyeq_train['quality'] == 2).astype(int)\nprint(\"EyeQ train binary distribution:\")\nprint(eyeq_train['binary_label'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:01:13.555283Z","iopub.execute_input":"2026-06-19T11:01:13.555927Z","iopub.status.idle":"2026-06-19T11:01:13.576539Z","shell.execute_reply.started":"2026-06-19T11:01:13.5559Z","shell.execute_reply":"2026-06-19T11:01:13.575824Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#6\nimport io\nfrom PIL import Image\nfrom torch.utils.data import Dataset\nfrom torchvision import transforms\n\nclass EyeQZipDataset(Dataset):\n    def __init__(self, df, zipfile_obj, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.zf = zipfile_obj\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        entry_path = f\"train/{row['image']}\"\n        try:\n            data = self.zf.read(entry_path)\n            img = Image.open(io.BytesIO(data)).convert('RGB')\n        except Exception:\n            img = Image.new('RGB', (224, 224))\n        if self.transform:\n            img = self.transform(img)\n        return img, row['binary_label']\n\niqa_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\nprint(\"Dataset class and transform ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:01:41.365374Z","iopub.execute_input":"2026-06-19T11:01:41.36602Z","iopub.status.idle":"2026-06-19T11:01:41.37352Z","shell.execute_reply.started":"2026-06-19T11:01:41.365991Z","shell.execute_reply":"2026-06-19T11:01:41.372656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#7\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import DataLoader\n\niqa_train_df, iqa_temp_df = train_test_split(\n    eyeq_train, test_size=0.30, stratify=eyeq_train['binary_label'], random_state=42\n)\niqa_val_df, iqa_test_df = train_test_split(\n    iqa_temp_df, test_size=0.50, stratify=iqa_temp_df['binary_label'], random_state=42\n)\n\niqa_train_dataset = EyeQZipDataset(iqa_train_df, zf, transform=iqa_transform)\niqa_val_dataset = EyeQZipDataset(iqa_val_df, zf, transform=iqa_transform)\niqa_test_dataset = EyeQZipDataset(iqa_test_df, zf, transform=iqa_transform)\n\niqa_train_loader = DataLoader(iqa_train_dataset, batch_size=32, shuffle=True, num_workers=0)\niqa_val_loader = DataLoader(iqa_val_dataset, batch_size=32, shuffle=False, num_workers=0)\niqa_test_loader = DataLoader(iqa_test_dataset, batch_size=32, shuffle=False, num_workers=0)\n\nprint(f\"IQA train: {len(iqa_train_df)} | val: {len(iqa_val_df)} | test: {len(iqa_test_df)}\")\nprint(f\"Train batches: {len(iqa_train_loader)} | Val batches: {len(iqa_val_loader)} | Test batches: {len(iqa_test_loader)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:01:46.244251Z","iopub.execute_input":"2026-06-19T11:01:46.245017Z","iopub.status.idle":"2026-06-19T11:01:46.266865Z","shell.execute_reply.started":"2026-06-19T11:01:46.244985Z","shell.execute_reply":"2026-06-19T11:01:46.265946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#8\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\n\nmodel_iqa = models.mobilenet_v2(weights='IMAGENET1K_V1')\nmodel_iqa.classifier[1] = nn.Linear(model_iqa.last_channel, 2)\nmodel_iqa = model_iqa.to(device)\n\nclass_counts = iqa_train_df['binary_label'].value_counts().sort_index()\ntotal = class_counts.sum()\nweights = torch.tensor([total / (2 * class_counts[0]), total / (2 * class_counts[1])], dtype=torch.float32).to(device)\nprint(f\"Class weights (Accept, Retake): {weights.cpu().numpy()}\")\n\ncriterion = nn.CrossEntropyLoss(weight=weights)\noptimizer = optim.Adam(model_iqa.parameters(), lr=0.0001)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:01:51.866361Z","iopub.execute_input":"2026-06-19T11:01:51.867199Z","iopub.status.idle":"2026-06-19T11:01:52.256188Z","shell.execute_reply.started":"2026-06-19T11:01:51.867163Z","shell.execute_reply":"2026-06-19T11:01:52.255365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:34:01.511322Z","iopub.execute_input":"2026-06-19T11:34:01.512127Z","iopub.status.idle":"2026-06-19T11:34:01.515549Z","shell.execute_reply.started":"2026-06-19T11:34:01.512098Z","shell.execute_reply":"2026-06-19T11:34:01.514922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#9\nmax_epochs = 20\npatience = 4\nepochs_without_improvement = 0\nbest_val_acc = 0.0\nbest_val_loss = float('inf')\nsave_path = '/kaggle/working/IQA_MobileNetV2_v2.pth'\n\nprint(\"Starting corrected IQA Gate training (class-weighted)...\")\nprint(f\"Max epochs: {max_epochs} | Early stopping patience: {patience}\")\nprint(\"-\" * 50)\n\nfor epoch in range(max_epochs):\n    model_iqa.train()\n    running_loss = 0.0\n    correct, total = 0, 0\n\n    for i, (images, labels) in enumerate(iqa_train_loader):\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model_iqa(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n        _, predicted = outputs.max(1)\n        total += labels.size(0)\n        correct += predicted.eq(labels).sum().item()\n\n        if i % 50 == 0:\n            print(f\"Epoch {epoch+1} | Batch {i}/{len(iqa_train_loader)} | Loss: {loss.item():.4f}\")\n\n    train_acc = 100. * correct / total\n    avg_train_loss = running_loss / len(iqa_train_loader)\n\n    # Validation\n    model_iqa.eval()\n    val_loss = 0.0\n    val_correct, val_total = 0, 0\n    all_val_preds, all_val_true = [], []\n    with torch.no_grad():\n        for images, labels in iqa_val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model_iqa(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item()\n            _, predicted = outputs.max(1)\n            val_total += labels.size(0)\n            val_correct += predicted.eq(labels).sum().item()\n            all_val_preds.extend(predicted.cpu().numpy())\n            all_val_true.extend(labels.cpu().numpy())\n\n    val_acc = 100. * val_correct / val_total\n    avg_val_loss = val_loss / len(iqa_val_loader)\n\n    # Check rejection sensitivity specifically — the metric that actually matters\n    all_val_preds = np.array(all_val_preds)\n    all_val_true = np.array(all_val_true)\n    reject_mask = all_val_true == 1\n    rejection_sens = (all_val_preds[reject_mask] == 1).mean() if reject_mask.sum() > 0 else 0\n\n    print(f\"\\nEpoch {epoch+1} complete!\")\n    print(f\"Train Loss: {avg_train_loss:.4f} | Train Acc: {train_acc:.2f}%\")\n    print(f\"Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.2f}% | Val Rejection Sensitivity: {rejection_sens:.3f}\")\n\n    if avg_val_loss < best_val_loss:\n        best_val_loss = avg_val_loss\n        epochs_without_improvement = 0\n        torch.save(model_iqa.state_dict(), save_path)\n        print(f\"Best model saved! Val Loss: {avg_val_loss:.4f}\")\n    else:\n        epochs_without_improvement += 1\n        print(f\"No improvement for {epochs_without_improvement} epoch(s)\")\n\n    if epochs_without_improvement >= patience:\n        print(f\"\\nEarly stopping triggered after epoch {epoch+1}.\")\n        break\n    print(\"-\" * 50)\n\nprint(\"\\nTraining complete!\")\nprint(f\"Best model saved to: {save_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T11:34:08.103996Z","iopub.execute_input":"2026-06-19T11:34:08.104293Z","iopub.status.idle":"2026-06-19T13:24:24.147327Z","shell.execute_reply.started":"2026-06-19T11:34:08.104269Z","shell.execute_reply":"2026-06-19T13:24:24.146588Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\neyepacs_subset_path = '/kaggle/working/eyepacs_subset'\nif os.path.exists(eyepacs_subset_path):\n    count = len(os.listdir(eyepacs_subset_path))\n    print(f\"Folder exists with {count} files\")\nelse:\n    print(\"Folder does NOT exist — it was wiped when the session reset\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T13:26:39.506931Z","iopub.execute_input":"2026-06-19T13:26:39.507703Z","iopub.status.idle":"2026-06-19T13:26:39.512689Z","shell.execute_reply.started":"2026-06-19T13:26:39.507673Z","shell.execute_reply":"2026-06-19T13:26:39.511819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom sklearn.metrics import confusion_matrix, cohen_kappa_score, roc_auc_score\n\n# Reload the actual best checkpoint from disk, just to be certain we're evaluating\n# the real saved version and not whatever's left in memory after early stopping\nmodel_iqa.load_state_dict(torch.load('/kaggle/working/IQA_MobileNetV2_v2.pth', map_location=device))\nmodel_iqa.eval()\nprint(\"Reloaded best checkpoint (epoch 1, val loss 0.1116)\")\n\nall_probs_class1 = []\nall_true = []\n\nprint(\"\\nRunning on held-out test set...\")\nwith torch.no_grad():\n    for i, (images, labels) in enumerate(iqa_test_loader):\n        images = images.to(device)\n        outputs = model_iqa(images)\n        probs = torch.softmax(outputs, dim=1)[:, 1].cpu().numpy()\n        all_probs_class1.append(probs)\n        all_true.append(labels.numpy())\n        if i % 15 == 0:\n            print(f\"Batch {i}/{len(iqa_test_loader)} done...\")\n\nall_probs_class1 = np.concatenate(all_probs_class1)\nall_true = np.concatenate(all_true)\nprint(\"Test pass complete.\\n\")\n\nthreshold = 0.5\npreds = (all_probs_class1 >= threshold).astype(int)\n\ntn, fp, fn, tp = confusion_matrix(all_true, preds, labels=[0, 1]).ravel()\nrejection_sensitivity = tp / (tp + fn) if (tp + fn) > 0 else float('nan')\nspecificity = tn / (tn + fp) if (tn + fp) > 0 else float('nan')\nkappa = cohen_kappa_score(all_true, preds)\nauc = roc_auc_score(all_true, all_probs_class1)\n\nprint(f\"=== Phase 3 — IQA Gate FINAL Test Evaluation (n={len(all_true)}) ===\")\nprint(f\"Rejection sensitivity (target ≥90%): {rejection_sensitivity:.3f}\")\nprint(f\"Specificity: {specificity:.3f}\")\nprint(f\"Cohen's kappa (target ≥0.75): {kappa:.3f}\")\nprint(f\"AUC: {auc:.3f}\")\nprint(f\"\\nConfusion matrix:\")\nprint(f\"  True Accept, Pred Accept: {tn}  |  True Accept, Pred Retake: {fp}\")\nprint(f\"  True Retake, Pred Accept: {fn}  |  True Retake, Pred Retake: {tp}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T13:29:18.145164Z","iopub.execute_input":"2026-06-19T13:29:18.145866Z","iopub.status.idle":"2026-06-19T13:33:47.092493Z","shell.execute_reply.started":"2026-06-19T13:29:18.145836Z","shell.execute_reply":"2026-06-19T13:33:47.091715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport pandas as pd\nimport zipfile\n\n# Reload the EyePACS DR labels (trainLabels.csv.zip)\nlabels_zip_path = '/kaggle/input/competitions/diabetic-retinopathy-detection/trainLabels.csv.zip'\nwith zipfile.ZipFile(labels_zip_path) as z:\n    csv_name = z.namelist()[0]\n    with z.open(csv_name) as f:\n        eyepacs_labels = pd.read_csv(f)\n\nTARGET_TOTAL = 6000\nrandom.seed(42)\n\nsampled_rows = []\nfor level, group in eyepacs_labels.groupby('level'):\n    proportion = len(group) / len(eyepacs_labels)\n    n_sample = max(1, round(TARGET_TOTAL * proportion))\n    sampled = group.sample(n=min(n_sample, len(group)), random_state=42)\n    sampled_rows.append(sampled)\n\neyepacs_subset = pd.concat(sampled_rows).reset_index(drop=True)\nprint(\"Subset class distribution:\")\nprint(eyepacs_subset['level'].value_counts().sort_index())\nprint(f\"\\nTotal images to extract: {len(eyepacs_subset)}\")\n\noutput_dir = '/kaggle/working/eyepacs_subset'\nos.makedirs(output_dir, exist_ok=True)\n\nextracted, errors = 0, 0\nfor i, row in eyepacs_subset.iterrows():\n    img_name = row['image']\n    entry_path = f\"train/{img_name}.jpeg\"\n    try:\n        data = zf.read(entry_path)\n        with open(os.path.join(output_dir, f\"{img_name}.jpeg\"), 'wb') as out:\n            out.write(data)\n        extracted += 1\n        if extracted % 500 == 0:\n            print(f\"Extracted {extracted}/{len(eyepacs_subset)}...\")\n    except Exception:\n        errors += 1\n\nprint(f\"\\nDone! Extracted: {extracted}, Errors: {errors}\")\n\neyepacs_subset['DR_binary'] = (eyepacs_subset['level'] > 0).astype(int)\neyepacs_subset.to_csv('/kaggle/working/eyepacs_subset_labels.csv', index=False)\nprint(\"Label file saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-19T13:39:51.164036Z","iopub.execute_input":"2026-06-19T13:39:51.16432Z","iopub.status.idle":"2026-06-19T13:41:47.096162Z","shell.execute_reply.started":"2026-06-19T13:39:51.164299Z","shell.execute_reply":"2026-06-19T13:41:47.095219Z"}},"outputs":[],"execution_count":null}]}