{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.13.15","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":"markdown","source":"\n# SmartRetina: An Explainable Diabetic Retinopathy Grading System\n\n## 1. Research Objective\n\nThis project investigates automated diabetic retinopathy (DR) severity grading from retinal fundus images using deep learning and explainable AI.\n\n### Initial datasets\n1. APTOS 2019 Blindness Detection\n2. EyePACS Diabetic Retinopathy Detection\n\n### Research workflow\n1. Environment setup and reproducibility\n2. Dataset access, structure and integrity audit\n3. Exploratory data analysis (EDA) for each dataset\n4. Label harmonization and leakage-aware data splitting\n5. Image preprocessing and supervised baseline\n6. Self-supervised learning experiments\n7. Fine-tuning and model selection\n8. Held-out evaluation\n9. Explainability analysis\n10. Limitations and conclusions\n\n**Methodological note:** Experiments, metrics and conclusions will only be reported after they have actually been run and verified.\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"# ============================================================\n# 2. ENVIRONMENT SETUP\n# ============================================================\n\nimport os\nimport sys\nimport random\nimport platform\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torchvision\nfrom IPython.display import display\n\nSEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Python:\", sys.version.split()[0])\nprint(\"Platform:\", platform.platform())\nprint(\"PyTorch:\", torch.__version__)\nprint(\"Torchvision:\", torchvision.__version__)\nprint(\"NumPy:\", np.__version__)\nprint(\"Pandas:\", pd.__version__)\nprint(\"Device:\", DEVICE)\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))\nelse:\n    print(\"GPU unavailable — check Notebook Settings if GPU is needed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:25.133037Z","iopub.execute_input":"2026-10-19T12:07:25.133784Z","iopub.status.idle":"2026-10-19T12:07:38.060644Z","shell.execute_reply.started":"2026-10-19T12:07:25.133756Z","shell.execute_reply":"2026-10-19T12:07:38.059822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\nimport os\n\nINPUT_DIR = Path(\"/kaggle/input\")\n\nfor item in sorted(INPUT_DIR.iterdir()):\n    if item.is_dir():\n        print(f\"\\nDataset folder: {item.name}\")\n\n        for child in sorted(item.iterdir())[:15]:\n            if child.is_dir():\n                print(f\"  [DIR]  {child.name}\")\n            else:\n                print(f\"  [FILE] {child.name}\")\n                ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:38.062121Z","iopub.execute_input":"2026-10-19T12:07:38.062575Z","iopub.status.idle":"2026-10-19T12:07:38.068872Z","shell.execute_reply.started":"2026-10-19T12:07:38.062552Z","shell.execute_reply":"2026-10-19T12:07:38.068216Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\n\nfor dataset_name in [\n    \"aptos2019-blindness-detection\",\n    \"diabetic-retinopathy-detection\"\n]:\n    dataset_path = INPUT_DIR / dataset_name\n\n    print(\"\\n\" + \"=\" * 65)\n    print(\"DATASET:\", dataset_name)\n\n    for item in sorted(dataset_path.iterdir()):\n        if item.is_dir():\n            print(f\"[DIR]  {item.name}\")\n        else:\n            print(f\"[FILE] {item.name}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:38.069898Z","iopub.execute_input":"2026-10-19T12:07:38.07017Z","iopub.status.idle":"2026-10-19T12:07:38.14049Z","shell.execute_reply.started":"2026-10-19T12:07:38.070149Z","shell.execute_reply":"2026-10-19T12:07:38.13971Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nfor dataset_name in [\n    \"aptos2019-blindness-detection\",\n    \"diabetic-retinopathy-detection\"\n]:\n    dataset_path = INPUT_DIR / dataset_name\n\n    print(\"\\n\" + \"=\" * 65)\n    print(\"DATASET:\", dataset_name)\n\n    for csv_path in sorted(dataset_path.rglob(\"*.csv\")):\n        print(\"\\nCSV:\", csv_path.name)\n\n        df = pd.read_csv(csv_path, nrows=5)\n        print(\"Columns:\", df.columns.tolist())\n        display(df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:38.142379Z","iopub.execute_input":"2026-10-19T12:07:38.142705Z","iopub.status.idle":"2026-10-19T12:07:46.386841Z","shell.execute_reply.started":"2026-10-19T12:07:38.142682Z","shell.execute_reply":"2026-10-19T12:07:46.385829Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 6. Exploratory Data Analysis (EDA)\n\n### 6.1 APTOS 2019: Label Distribution and Data Integrity\n\nWe inspect the labelled training metadata, quantify class imbalance, and verify that each training record corresponds to an available image.\n\nThe competition test set is kept separate because ground-truth labels are not provided in its test metadata.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport zipfile\n\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\nAPTOS_DIR = INPUT_DIR / \"aptos2019-blindness-detection\"\nEYEPACS_DIR = INPUT_DIR / \"diabetic-retinopathy-detection\"\n\n# Load complete APTOS training metadata\naptos_train = pd.read_csv(APTOS_DIR / \"train.csv\")\n\nprint(\"APTOS training rows:\", len(aptos_train))\nprint(\"Columns:\", aptos_train.columns.tolist())\nprint(\"\\nMissing values:\")\ndisplay(aptos_train.isna().sum().to_frame(\"missing_count\"))\n\nprint(\"Duplicate IDs:\", aptos_train[\"id_code\"].duplicated().sum())\nprint(\"Duplicate rows:\", aptos_train.duplicated().sum())\n\n# Grade distribution\ngrade_counts = (\n    aptos_train[\"diagnosis\"]\n    .value_counts()\n    .reindex(range(5), fill_value=0)\n)\n\ngrade_summary = pd.DataFrame({\n    \"grade\": grade_counts.index,\n    \"image_count\": grade_counts.values,\n    \"percentage\": (grade_counts.values / len(aptos_train) * 100).round(2)\n})\n\ndisplay(grade_summary)\n\nplt.figure(figsize=(8, 4))\nplt.bar(grade_summary[\"grade\"], grade_summary[\"image_count\"])\nplt.xlabel(\"DR Grade\")\nplt.ylabel(\"Number of images\")\nplt.title(\"APTOS 2019: Training Class Distribution\")\nplt.xticks(range(5))\nplt.show()\n\n# Verify image files exist\nimage_dir = APTOS_DIR / \"train_images\"\navailable_ids = {\n    p.stem for p in image_dir.iterdir()\n    if p.is_file() and p.suffix.lower() in {\".png\", \".jpg\", \".jpeg\"}\n}\n\naptos_ids = set(aptos_train[\"id_code\"].dropna().astype(str))\nmissing_ids = sorted(aptos_ids - available_ids)\n\nprint(\"Unique training IDs:\", len(aptos_ids))\nprint(\"Image files found:\", len(available_ids))\nprint(\"Training IDs with missing images:\", len(missing_ids))\nprint(\"Examples of missing IDs:\", missing_ids[:10])\n\n# Inspect EyePACS labels archive without extracting images\nlabels_zip = EYEPACS_DIR / \"trainLabels.csv.zip\"\n\nif labels_zip.exists():\n    with zipfile.ZipFile(labels_zip) as zf:\n        print(\"\\nEyePACS label archive contents:\", zf.namelist())\n        csv_names = [\n            name for name in zf.namelist()\n            if name.lower().endswith(\".csv\")\n        ]\n\n        if csv_names:\n            with zf.open(csv_names[0]) as f:\n                eyepacs_train = pd.read_csv(f)\n\n            print(\"EyePACS label rows:\", len(eyepacs_train))\n            print(\"EyePACS columns:\", eyepacs_train.columns.tolist())\n            display(eyepacs_train.head())\n            print(\"\\nEyePACS missing values:\")\n            display(eyepacs_train.isna().sum().to_frame(\"missing_count\"))\n        else:\n            print(\"No CSV found inside the label archive.\")\nelse:\n    print(\"\\nEyePACS labels archive not found at:\", labels_zip)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:46.388496Z","iopub.execute_input":"2026-10-19T12:07:46.388729Z","iopub.status.idle":"2026-10-19T12:07:46.789611Z","shell.execute_reply.started":"2026-10-19T12:07:46.388709Z","shell.execute_reply":"2026-10-19T12:07:46.788988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 7. Visual Exploratory Data Analysis\n\n### 7.1 Representative APTOS Retinal Images\n\nWe visualize representative images from each of the five diabetic retinopathy grades. This helps us inspect image quality, field-of-view consistency, illumination, and visible retinal characteristics before selecting preprocessing techniques.\n\nThe examples are exploratory only; visual inspection is not a substitute for clinical grading.\n","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport random\nimport pandas as pd\n\n# Reinitialize required variables\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\nAPTOS_DIR = INPUT_DIR / \"aptos2019-blindness-detection\"\nAPTOS_IMAGE_DIR = APTOS_DIR / \"train_images\"\n\naptos_train = pd.read_csv(APTOS_DIR / \"train.csv\")\n\nSEED = 42\nrandom.seed(SEED)\n\n# Map image IDs to paths\nimage_paths = {\n    p.stem: p\n    for p in APTOS_IMAGE_DIR.iterdir()\n    if p.is_file() and p.suffix.lower() in {\".png\", \".jpg\", \".jpeg\"}\n}\n\nprint(\"Training records:\", len(aptos_train))\nprint(\"Accessible image files:\", len(image_paths))\n\n# Display two images per grade\nfig, axes = plt.subplots(5, 2, figsize=(10, 20))\n\nfor grade in range(5):\n    ids = aptos_train.loc[\n        aptos_train[\"diagnosis\"] == grade, \"id_code\"\n    ].astype(str).tolist()\n\n    selected_ids = random.sample(ids, min(2, len(ids)))\n\n    for col in range(2):\n        ax = axes[grade, col]\n        ax.axis(\"off\")\n\n        if col < len(selected_ids):\n            image_id = selected_ids[col]\n            image_path = image_paths.get(image_id)\n\n            if image_path:\n                with Image.open(image_path) as img:\n                    ax.imshow(img.convert(\"RGB\"))\n                ax.set_title(f\"Grade {grade} | {image_id}\")\n            else:\n                ax.set_title(f\"Image missing: {image_id}\")\n\nplt.suptitle(\n    \"APTOS 2019 — Representative Images by DR Grade\",\n    y=1.01\n)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:46.791009Z","iopub.execute_input":"2026-10-19T12:07:46.791404Z","iopub.status.idle":"2026-10-19T12:07:52.336324Z","shell.execute_reply.started":"2026-10-19T12:07:46.791379Z","shell.execute_reply":"2026-10-19T12:07:52.335387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom collections import Counter\n\ndimension_counts = Counter()\nmode_counts = Counter()\nfailed_images = []\n\nfor image_id, image_path in image_paths.items():\n    try:\n        with Image.open(image_path) as img:\n            dimension_counts[img.size] += 1\n            mode_counts[img.mode] += 1\n    except Exception as exc:\n        failed_images.append((image_id, str(exc)))\n\nprint(\"Most common image dimensions:\")\ndisplay(pd.DataFrame(\n    dimension_counts.most_common(10),\n    columns=[\"dimensions\", \"count\"]\n))\n\nprint(\"\\nImage color modes:\")\ndisplay(pd.DataFrame(\n    mode_counts.most_common(),\n    columns=[\"mode\", \"count\"]\n))\n\nprint(\"\\nImages that failed to open:\", len(failed_images))\nprint(\"Examples:\", failed_images[:5])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:07:52.337802Z","iopub.execute_input":"2026-10-19T12:07:52.338367Z","iopub.status.idle":"2026-10-19T12:08:27.079986Z","shell.execute_reply.started":"2026-10-19T12:07:52.338325Z","shell.execute_reply":"2026-10-19T12:08:27.079251Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 8. Dataset Audit: Image Characteristics and Cross-Dataset Labels\n\nThis section examines APTOS image characteristics by grade and compares the labelled training distributions of APTOS and EyePACS. EyePACS image availability will be assessed separately because its images are supplied as split archive files.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nfrom PIL import Image\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# Reuse or recreate the required variables\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\nAPTOS_DIR = INPUT_DIR / \"aptos2019-blindness-detection\"\nAPTOS_IMAGE_DIR = APTOS_DIR / \"train_images\"\n\naptos_train = pd.read_csv(APTOS_DIR / \"train.csv\")\n\nrecords = []\n\nfor row in aptos_train.itertuples(index=False):\n    image_id = str(row.id_code)\n    matches = [\n        p for p in APTOS_IMAGE_DIR.glob(image_id + \".*\")\n        if p.suffix.lower() in {\".jpg\", \".jpeg\", \".png\"}\n    ]\n\n    if not matches:\n        continue\n\n    try:\n        with Image.open(matches[0]) as img:\n            width, height = img.size\n            records.append({\n                \"id_code\": image_id,\n                \"grade\": int(row.diagnosis),\n                \"width\": width,\n                \"height\": height,\n                \"aspect_ratio\": width / height,\n                \"pixels\": width * height,\n            })\n    except Exception:\n        pass\n\nimage_meta = pd.DataFrame(records)\n\nprint(\"Images successfully inspected:\", len(image_meta))\ndisplay(\n    image_meta.groupby(\"grade\")[\n        [\"width\", \"height\", \"aspect_ratio\", \"pixels\"]\n    ].median().round(2)\n)\n\nplt.figure(figsize=(8, 4))\nplt.boxplot(\n    [\n        image_meta.loc[image_meta[\"grade\"] == g, \"aspect_ratio\"].values\n        for g in range(5)\n    ],\n    tick_labels=[str(g) for g in range(5)]\n)\nplt.xlabel(\"DR grade\")\nplt.ylabel(\"Image width / height\")\nplt.title(\"APTOS: Image aspect ratio by grade\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:27.08105Z","iopub.execute_input":"2026-10-19T12:08:27.081375Z","iopub.status.idle":"2026-10-19T12:08:44.164606Z","shell.execute_reply.started":"2026-10-19T12:08:27.081354Z","shell.execute_reply":"2026-10-19T12:08:44.1639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom pathlib import Path\nimport pandas as pd\n\nEYEPACS_DIR = INPUT_DIR / \"diabetic-retinopathy-detection\"\n\narchive_records = []\n\nfor p in sorted(EYEPACS_DIR.iterdir()):\n    if p.is_file():\n        archive_records.append({\n            \"filename\": p.name,\n            \"size_GB\": round(p.stat().st_size / (1024**3), 3),\n            \"size_MB\": round(p.stat().st_size / (1024**2), 1),\n        })\n\narchive_df = pd.DataFrame(archive_records)\ndisplay(archive_df)\n\nprint(\"EyePACS labelled records:\", len(eyepacs_train))\nprint(\"EyePACS duplicate image IDs:\", eyepacs_train[\"image\"].duplicated().sum())\nprint(\"EyePACS grades:\", sorted(eyepacs_train[\"level\"].dropna().unique()))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:44.165573Z","iopub.execute_input":"2026-10-19T12:08:44.166263Z","iopub.status.idle":"2026-10-19T12:08:44.195845Z","shell.execute_reply.started":"2026-10-19T12:08:44.16624Z","shell.execute_reply":"2026-10-19T12:08:44.195308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Correct APTOS vs EyePACS comparison\n\naptos_counts = (\n    aptos_train[\"diagnosis\"]\n    .value_counts()\n    .reindex(range(5), fill_value=0)\n)\n\neyepacs_counts = (\n    eyepacs_train[\"level\"]\n    .value_counts()\n    .reindex(range(5), fill_value=0)\n)\n\ncomparison = pd.DataFrame({\n    \"grade\": list(range(5)),\n    \"APTOS_count\": [int(aptos_counts.loc[g]) for g in range(5)],\n    \"APTOS_percent\": [\n        round(aptos_counts.loc[g] / len(aptos_train) * 100, 2)\n        for g in range(5)\n    ],\n    \"EyePACS_count\": [int(eyepacs_counts.loc[g]) for g in range(5)],\n    \"EyePACS_percent\": [\n        round(eyepacs_counts.loc[g] / len(eyepacs_train) * 100, 2)\n        for g in range(5)\n    ],\n})\n\ndisplay(comparison)\n\ncomparison.set_index(\"grade\")[\n    [\"APTOS_percent\", \"EyePACS_percent\"]\n].plot(kind=\"bar\", figsize=(9, 5))\n\nplt.xlabel(\"DR grade\")\nplt.ylabel(\"Percentage of labelled training records\")\nplt.title(\"APTOS vs EyePACS: Grade Distribution\")\nplt.xticks(rotation=0)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:44.19851Z","iopub.execute_input":"2026-10-19T12:08:44.19882Z","iopub.status.idle":"2026-10-19T12:08:44.470012Z","shell.execute_reply.started":"2026-10-19T12:08:44.198798Z","shell.execute_reply":"2026-10-19T12:08:44.468974Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 9. EyePACS Archive Inspection\n\nThe EyePACS training images are provided as multiple archive parts. Before extracting or loading images, we inspect the archive format and available utilities to determine a safe way to access the images without unnecessarily duplicating the dataset.\n","metadata":{}},{"cell_type":"code","source":"import shutil\nimport subprocess\nfrom pathlib import Path\n\nEYEPACS_DIR = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\nprint(\"Available archive utilities:\")\nfor utility in [\"7z\", \"7zz\", \"unzip\", \"zipinfo\"]:\n    print(f\"{utility}: {shutil.which(utility)}\")\n\nprint(\"\\nTraining archive files:\")\nfor p in sorted(EYEPACS_DIR.glob(\"train.zip.*\")):\n    print(f\"{p.name}: {p.stat().st_size / (1024**3):.3f} GiB\")\n\nprint(\"\\nLabel CSV:\")\nprint(\"Rows:\", len(eyepacs_train))\nprint(\"Unique labelled IDs:\", eyepacs_train[\"image\"].nunique())\nprint(\"Grade values:\", sorted(eyepacs_train[\"level\"].unique()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:44.471262Z","iopub.execute_input":"2026-10-19T12:08:44.471606Z","iopub.status.idle":"2026-10-19T12:08:44.491677Z","shell.execute_reply.started":"2026-10-19T12:08:44.471582Z","shell.execute_reply":"2026-10-19T12:08:44.490345Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 10. EyePACS Archive Structure Inspection\n\nThe EyePACS training images are distributed across five split archive files. We inspect the first archive to identify its format and image naming convention before deciding how to access the images efficiently.\n","metadata":{}},{"cell_type":"code","source":"\nimport subprocess\nfrom pathlib import Path\n\nEYEPACS_DIR = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\nfirst_part = EYEPACS_DIR / \"train.zip.001\"\n\nresult = subprocess.run(\n    [\"7z\", \"l\", str(first_part)],\n    capture_output=True,\n    text=True,\n    timeout=120\n)\n\nprint(\"Return code:\", result.returncode)\nprint(result.stdout[-6000:])\n\nif result.stderr:\n    print(\"\\nDiagnostics:\")\n    print(result.stderr[-2000:])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:44.493Z","iopub.execute_input":"2026-10-19T12:08:44.493701Z","iopub.status.idle":"2026-10-19T12:08:45.225735Z","shell.execute_reply.started":"2026-10-19T12:08:44.493674Z","shell.execute_reply":"2026-10-19T12:08:45.225054Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 11. EyePACS Image Access Test\n\nWe test whether an individual retinal image can be read directly from the split archive volumes. This avoids extracting the entire dataset and helps determine a practical data-loading strategy.\n","metadata":{}},{"cell_type":"code","source":"\nimport subprocess\nfrom pathlib import Path\nfrom PIL import Image\nfrom io import BytesIO\nimport matplotlib.pyplot as plt\n\nEYEPACS_DIR = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\nfirst_part = EYEPACS_DIR / \"train.zip.001\"\nmember = \"train/10_left.jpeg\"\n\nresult = subprocess.run(\n    [\"7z\", \"x\", \"-so\", str(first_part), member],\n    stdout=subprocess.PIPE,\n    stderr=subprocess.PIPE,\n    timeout=120\n)\n\nprint(\"Return code:\", result.returncode)\nprint(\"Bytes returned:\", len(result.stdout))\nprint(\"Diagnostics:\", result.stderr.decode(errors=\"replace\")[-1000:])\n\nif result.returncode == 0 and result.stdout:\n    image = Image.open(BytesIO(result.stdout)).convert(\"RGB\")\n    print(\"Image size:\", image.size)\n    plt.figure(figsize=(6, 6))\n    plt.imshow(image)\n    plt.title(member)\n    plt.axis(\"off\")\n    plt.show()\nelse:\n    print(\n        \"Direct extraction from the first volume did not succeed. \"\n        \"We will use another archive-access strategy.\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:45.226737Z","iopub.execute_input":"2026-10-19T12:08:45.227087Z","iopub.status.idle":"2026-10-19T12:08:46.42679Z","shell.execute_reply.started":"2026-10-19T12:08:45.227063Z","shell.execute_reply":"2026-10-19T12:08:46.425984Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 12. EyePACS Visual Exploratory Data Analysis\n\nWe inspect representative images from each of the five DR grades. Images are read directly from the split archive into memory, avoiding full dataset extraction.\n\nThe displayed samples are for exploratory inspection and do not independently establish the correctness of their labels.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nfrom PIL import Image\nfrom io import BytesIO\nimport subprocess\nimport random\nimport matplotlib.pyplot as plt\n\nEYEPACS_DIR = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\nEYEPACS_ARCHIVE = EYEPACS_DIR / \"train.zip.001\"\n\n# Reuse the labels loaded earlier\neyepacs_train = eyepacs_train.copy()\nrandom.seed(42)\n\nfig, axes = plt.subplots(5, 2, figsize=(10, 20))\n\nfor grade in range(5):\n    grade_rows = eyepacs_train[\n        eyepacs_train[\"level\"] == grade\n    ]\n\n    samples = grade_rows.sample(\n        n=min(2, len(grade_rows)),\n        random_state=42\n    )\n\n    for col, (_, row) in enumerate(samples.iterrows()):\n        ax = axes[grade, col]\n        ax.axis(\"off\")\n\n        image_name = f\"train/{row['image']}.jpeg\"\n\n        result = subprocess.run(\n            [\n                \"7z\", \"x\", \"-so\",\n                str(EYEPACS_ARCHIVE),\n                image_name\n            ],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            timeout=120\n        )\n\n        if result.returncode == 0 and result.stdout:\n            try:\n                with Image.open(BytesIO(result.stdout)) as img:\n                    ax.imshow(img.convert(\"RGB\"))\n                    ax.set_title(\n                        f\"Grade {grade} | {row['image']}\"\n                    )\n            except Exception as exc:\n                ax.set_title(f\"Could not decode image: {exc}\")\n        else:\n            ax.set_title(\n                f\"Could not read {image_name}\\n\"\n                + result.stderr.decode(errors=\"replace\")[-150:]\n            )\n\nplt.suptitle(\n    \"EyePACS — Representative Images by DR Grade\",\n    y=1.01\n)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:46.427956Z","iopub.execute_input":"2026-10-19T12:08:46.428191Z","iopub.status.idle":"2026-10-19T12:08:55.01527Z","shell.execute_reply.started":"2026-10-19T12:08:46.42817Z","shell.execute_reply":"2026-10-19T12:08:55.014245Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 13. Dataset Manifest and Label Harmonization\n\nWe create a unified metadata table for APTOS and EyePACS, preserving each dataset's source and original label. The manifest will be used for integrity checks, reproducible splitting and subsequent experiments.\n\nThe datasets remain identifiable by source; labels are not assumed to make the datasets interchangeable.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nimport pandas as pd\n\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\nAPTOS_DIR = INPUT_DIR / \"aptos2019-blindness-detection\"\nEYEPACS_DIR = INPUT_DIR / \"diabetic-retinopathy-detection\"\n\n# APTOS manifest\naptos_df = pd.read_csv(APTOS_DIR / \"train.csv\")\n\naptos_manifest = pd.DataFrame({\n    \"image_id\": aptos_df[\"id_code\"].astype(str),\n    \"label\": aptos_df[\"diagnosis\"].astype(int),\n    \"source\": \"APTOS\",\n    \"relative_path\": (\n        \"train_images/\" + aptos_df[\"id_code\"].astype(str)\n    ),\n    \"patient_id\": pd.NA,  # No patient identifier established here\n})\n\n# EyePACS manifest\n# Reuse eyepacs_train already loaded from trainLabels.csv.zip\neyepacs_manifest = pd.DataFrame({\n    \"image_id\": eyepacs_train[\"image\"].astype(str),\n    \"label\": eyepacs_train[\"level\"].astype(int),\n    \"source\": \"EyePACS\",\n    \"relative_path\": (\n        \"train/\" + eyepacs_train[\"image\"].astype(str) + \".jpeg\"\n    ),\n    \"patient_id\": pd.NA,  # Patient linkage needs separate validation\n})\n\nmanifest = pd.concat(\n    [aptos_manifest, eyepacs_manifest],\n    ignore_index=True\n)\n\nprint(\"Manifest rows:\", len(manifest))\nprint(\"\\nRows by source:\")\ndisplay(manifest.groupby(\"source\").size().rename(\"rows\").to_frame())\n\nprint(\"\\nLabel distribution by source:\")\ndisplay(pd.crosstab(manifest[\"source\"], manifest[\"label\"]))\n\nprint(\"\\nMissing manifest values:\")\ndisplay(manifest.isna().sum().rename(\"missing_count\").to_frame())\n\nprint(\"\\nDuplicate source/image pairs:\",\n      manifest.duplicated([\"source\", \"image_id\"]).sum())\n\ndisplay(manifest.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:55.016939Z","iopub.execute_input":"2026-10-19T12:08:55.017494Z","iopub.status.idle":"2026-10-19T12:08:55.109899Z","shell.execute_reply.started":"2026-10-19T12:08:55.01745Z","shell.execute_reply":"2026-10-19T12:08:55.109294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport subprocess\nfrom pathlib import Path\n\nEYEPACS_DIR = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\narchive = EYEPACS_DIR / \"train.zip.001\"\n\n# Check a small set of labelled image IDs without extracting them\nsample_ids = eyepacs_train[\"image\"].sample(\n    n=10, random_state=42\n).astype(str).tolist()\n\nfor image_id in sample_ids:\n    member = f\"train/{image_id}.jpeg\"\n\n    result = subprocess.run(\n        [\"7z\", \"x\", \"-so\", str(archive), member],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        timeout=120\n    )\n\n    print(\n        image_id,\n        \"OK\" if result.returncode == 0 and result.stdout else \"NOT FOUND IN THIS PART\",\n        f\"({len(result.stdout):,} bytes)\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:55.111001Z","iopub.execute_input":"2026-10-19T12:08:55.111387Z","iopub.status.idle":"2026-10-19T12:08:55.986529Z","shell.execute_reply.started":"2026-10-19T12:08:55.111362Z","shell.execute_reply":"2026-10-19T12:08:55.985813Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 14. Leakage-Aware Train/Validation Split\n\nAPTOS and EyePACS are split separately to preserve dataset provenance.\n\n- APTOS: stratified image-level split because patient identifiers are not provided in the inspected metadata.\n- EyePACS: grouped split using the filename prefix as a grouping key, keeping left/right images with the same prefix together.\n- The competition test sets are not used for validation because their ground-truth labels are unavailable.\n\nThe validation sets are reserved for model selection; final performance claims will require a clearly defined held-out evaluation protocol.\n","metadata":{}},{"cell_type":"code","source":"\nfrom sklearn.model_selection import train_test_split, StratifiedGroupKFold\nimport pandas as pd\nimport numpy as np\n\nSEED = 42\n\n# Ensure manifest exists from the previous step\nassert {\"image_id\", \"label\", \"source\"}.issubset(manifest.columns)\n\naptos_manifest = manifest[manifest[\"source\"] == \"APTOS\"].copy()\neyepacs_manifest = manifest[manifest[\"source\"] == \"EyePACS\"].copy()\n\n# APTOS: stratified image-level split\naptos_train, aptos_val = train_test_split(\n    aptos_manifest,\n    test_size=0.20,\n    random_state=SEED,\n    stratify=aptos_manifest[\"label\"]\n)\n\naptos_train = aptos_train.copy()\naptos_val = aptos_val.copy()\naptos_train[\"split\"] = \"train\"\naptos_val[\"split\"] = \"validation\"\n\n# EyePACS: group both eyes by the identifier before \"_\"\neyepacs_manifest[\"patient_group\"] = (\n    eyepacs_manifest[\"image_id\"].str.split(\"_\").str[0]\n)\n\nsgkf = StratifiedGroupKFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=SEED\n)\n\neye_train_idx, eye_val_idx = next(\n    sgkf.split(\n        eyepacs_manifest,\n        y=eyepacs_manifest[\"label\"],\n        groups=eyepacs_manifest[\"patient_group\"]\n    )\n)\n\neyepacs_train_split = eyepacs_manifest.iloc[eye_train_idx].copy()\neyepacs_val_split = eyepacs_manifest.iloc[eye_val_idx].copy()\n\neyepacs_train_split[\"split\"] = \"train\"\neyepacs_val_split[\"split\"] = \"validation\"\n\n# Combine split manifests while preserving source and original IDs\ntrain_manifest = pd.concat(\n    [aptos_train, eyepacs_train_split],\n    ignore_index=True\n)\n\nval_manifest = pd.concat(\n    [aptos_val, eyepacs_val_split],\n    ignore_index=True\n)\n\nprint(\"Training rows:\", len(train_manifest))\nprint(\"Validation rows:\", len(val_manifest))\n\nprint(\"\\nCounts by source and split:\")\ndisplay(pd.crosstab(train_manifest[\"source\"], train_manifest[\"split\"]))\ndisplay(pd.crosstab(val_manifest[\"source\"], val_manifest[\"split\"]))\n\nprint(\"\\nValidation label distribution by source:\")\ndisplay(pd.crosstab(val_manifest[\"source\"], val_manifest[\"label\"]))\n\n# Verify that EyePACS groups do not cross the split boundary\ntrain_groups = set(eyepacs_train_split[\"patient_group\"])\nval_groups = set(eyepacs_val_split[\"patient_group\"])\n\nprint(\"\\nEyePACS groups shared across splits:\", len(train_groups & val_groups))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:08:55.987557Z","iopub.execute_input":"2026-10-19T12:08:55.987861Z","iopub.status.idle":"2026-10-19T12:09:03.066404Z","shell.execute_reply.started":"2026-10-19T12:08:55.987839Z","shell.execute_reply":"2026-10-19T12:09:03.065675Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 15. Reproducibility: Save Split Manifests\n\nWe save the train/validation assignments and preserve dataset provenance, labels and image identifiers. This allows subsequent experiments to reuse the same split rather than generating new assignments each time.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\n\nOUTPUT_DIR = Path(\"/kaggle/working/smartretina\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\ntrain_manifest.to_csv(\n    OUTPUT_DIR / \"train_manifest.csv\",\n    index=False\n)\n\nval_manifest.to_csv(\n    OUTPUT_DIR / \"validation_manifest.csv\",\n    index=False\n)\n\nprint(\"Saved files:\")\nprint(OUTPUT_DIR / \"train_manifest.csv\")\nprint(OUTPUT_DIR / \"validation_manifest.csv\")\n\nprint(\"\\nTrain shape:\", train_manifest.shape)\nprint(\"Validation shape:\", val_manifest.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:03.067473Z","iopub.execute_input":"2026-10-19T12:09:03.068197Z","iopub.status.idle":"2026-10-19T12:09:03.202478Z","shell.execute_reply.started":"2026-10-19T12:09:03.068171Z","shell.execute_reply":"2026-10-19T12:09:03.200328Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 16. Image Loading and Preprocessing\n\nWe implement a shared image-loading interface for APTOS and EyePACS. Images are decoded as RGB, resized to a common input size, and converted to tensors.\n\nFor the initial baseline, we use deterministic resizing and normalization. Augmentation will be introduced only for training data, while validation preprocessing remains deterministic.\n","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nfrom io import BytesIO\nfrom PIL import Image\nimport subprocess\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nINPUT_DIR = Path(\"/kaggle/input/competitions\")\nAPTOS_DIR = INPUT_DIR / \"aptos2019-blindness-detection\"\nEYEPACS_DIR = INPUT_DIR / \"diabetic-retinopathy-detection\"\n\nIMAGE_SIZE = 224\nBATCH_SIZE = 16\nNUM_WORKERS = 2\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:03.204485Z","iopub.execute_input":"2026-10-19T12:09:03.204821Z","iopub.status.idle":"2026-10-19T12:09:03.213477Z","shell.execute_reply.started":"2026-10-19T12:09:03.204777Z","shell.execute_reply":"2026-10-19T12:09:03.212853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:03.215048Z","iopub.execute_input":"2026-10-19T12:09:03.215381Z","iopub.status.idle":"2026-10-19T12:09:03.235044Z","shell.execute_reply.started":"2026-10-19T12:09:03.21535Z","shell.execute_reply":"2026-10-19T12:09:03.234302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RetinopathyDataset(Dataset):\n    def __init__(self, dataframe, transform=None):\n        self.df = dataframe.reset_index(drop=True).copy()\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_aptos(self, row):\n        image_id = str(row[\"image_id\"])\n        image_dir = APTOS_DIR / \"train_images\"\n\n        for suffix in [\".png\", \".jpg\", \".jpeg\"]:\n            path = image_dir / f\"{image_id}{suffix}\"\n            if path.exists():\n                with Image.open(path) as img:\n                    return img.convert(\"RGB\")\n\n        raise FileNotFoundError(f\"APTOS image not found: {image_id}\")\n\n    def _load_eyepacs(self, row):\n        # EyePACS images are stored in split volumes.\n        # Try the first volume for this initial loader test.\n        image_id = str(row[\"image_id\"])\n        archive = EYEPACS_DIR / \"train.zip.001\"\n        member = f\"train/{image_id}.jpeg\"\n\n        result = subprocess.run(\n            [\"7z\", \"x\", \"-so\", str(archive), member],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            timeout=120\n        )\n\n        if result.returncode != 0 or not result.stdout:\n            raise FileNotFoundError(\n                f\"EyePACS image not found in first archive part: {member}\"\n            )\n\n        with Image.open(BytesIO(result.stdout)) as img:\n            return img.convert(\"RGB\")\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        if row[\"source\"] == \"APTOS\":\n            image = self._load_aptos(row)\n        elif row[\"source\"] == \"EyePACS\":\n            image = self._load_eyepacs(row)\n        else:\n            raise ValueError(f\"Unknown dataset source: {row['source']}\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        return {\n            \"image\": image,\n            \"label\": int(row[\"label\"]),\n            \"source\": row[\"source\"],\n            \"image_id\": row[\"image_id\"],\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:03.237077Z","iopub.execute_input":"2026-10-19T12:09:03.237561Z","iopub.status.idle":"2026-10-19T12:09:03.259512Z","shell.execute_reply.started":"2026-10-19T12:09:03.237484Z","shell.execute_reply":"2026-10-19T12:09:03.257998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"aptos_train_only = train_manifest[\n    train_manifest[\"source\"] == \"APTOS\"\n].sample(n=8, random_state=42)\n\naptos_dataset = RetinopathyDataset(\n    aptos_train_only,\n    transform=val_transform\n)\n\naptos_loader = DataLoader(\n    aptos_dataset,\n    batch_size=4,\n    shuffle=False,\n    num_workers=0\n)\n\nbatch = next(iter(aptos_loader))\n\nprint(\"Image batch shape:\", batch[\"image\"].shape)\nprint(\"Labels:\", batch[\"label\"])\nprint(\"Sources:\", batch[\"source\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:03.260963Z","iopub.execute_input":"2026-10-19T12:09:03.261607Z","iopub.status.idle":"2026-10-19T12:09:04.41512Z","shell.execute_reply.started":"2026-10-19T12:09:03.261568Z","shell.execute_reply":"2026-10-19T12:09:04.414195Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 16.2 EyePACS Loader Validation\n\nWe test a small batch of EyePACS images using the archive-backed loader. This verifies image decoding, tensor shape and label preservation before any training begins.\n","metadata":{}},{"cell_type":"code","source":"\n# Select a small reproducible EyePACS sample\neyepacs_test_rows = train_manifest[\n    train_manifest[\"source\"] == \"EyePACS\"\n].sample(n=4, random_state=42)\n\neyepacs_dataset_test = RetinopathyDataset(\n    eyepacs_test_rows,\n    transform=val_transform\n)\n\neyepacs_loader_test = DataLoader(\n    eyepacs_dataset_test,\n    batch_size=4,\n    shuffle=False,\n    num_workers=0\n)\n\nbatch = next(iter(eyepacs_loader_test))\n\nprint(\"Image batch shape:\", batch[\"image\"].shape)\nprint(\"Labels:\", batch[\"label\"])\nprint(\"Sources:\", batch[\"source\"])\nprint(\"Image IDs:\", batch[\"image_id\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:04.416328Z","iopub.execute_input":"2026-10-19T12:09:04.416679Z","iopub.status.idle":"2026-10-19T12:09:05.273355Z","shell.execute_reply.started":"2026-10-19T12:09:04.416655Z","shell.execute_reply":"2026-10-19T12:09:05.272461Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 17. Image Preprocessing Strategy\n\nThe initial supervised baseline uses RGB images resized to 224 × 224 pixels and normalized using ImageNet channel statistics, matching the expected input convention of an ImageNet-pretrained backbone.\n\nThe initial pipeline deliberately avoids aggressive enhancement. Retinal lesions may be subtle, so transformations must be evaluated rather than assumed to improve performance.\n\n- Training: resizing, tensor conversion, normalization; augmentation will be introduced in a controlled experiment.\n- Validation: deterministic resizing, tensor conversion and normalization only.\n- Labels: retain the original five grades (0–4).\n- Image loading: use source-specific loaders and avoid extracting the full EyePACS archive.\n- Limitations: direct square resizing can distort image geometry and will be compared with an aspect-preserving alternative.\n","metadata":{}},{"cell_type":"code","source":"\nfrom torchvision import transforms\n\nIMAGE_SIZE = 224\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(degrees=10),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])\n\nprint(\"Training and validation transforms defined.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:05.274604Z","iopub.execute_input":"2026-10-19T12:09:05.275017Z","iopub.status.idle":"2026-10-19T12:09:05.281367Z","shell.execute_reply.started":"2026-10-19T12:09:05.274992Z","shell.execute_reply":"2026-10-19T12:09:05.280781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport matplotlib.pyplot as plt\nimport torch\n\n# Test the APTOS pipeline first\naptos_sample = train_manifest[\n    train_manifest[\"source\"] == \"APTOS\"\n].sample(n=8, random_state=42)\n\naptos_transform_test = RetinopathyDataset(\n    aptos_sample,\n    transform=train_transform\n)\n\nimages = [aptos_transform_test[i] for i in range(min(8, len(aptos_transform_test)))]\n\nmean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)\nstd = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)\n\nfig, axes = plt.subplots(2, 4, figsize=(14, 7))\n\nfor ax, item in zip(axes.flat, images):\n    image = item[\"image\"] * std + mean\n    image = image.clamp(0, 1).permute(1, 2, 0).numpy()\n\n    ax.imshow(image)\n    ax.set_title(\n        f\"Grade {item['label']}\",\n        fontsize=10\n    )\n    ax.axis(\"off\")\n\nplt.suptitle(\"APTOS: Training Transform Preview\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:05.282262Z","iopub.execute_input":"2026-10-19T12:09:05.282483Z","iopub.status.idle":"2026-10-19T12:09:07.701664Z","shell.execute_reply.started":"2026-10-19T12:09:05.282463Z","shell.execute_reply":"2026-10-19T12:09:07.700726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport shutil\nfrom pathlib import Path\n\nworking_disk = shutil.disk_usage(\"/kaggle/working\")\ninput_disk = shutil.disk_usage(\"/kaggle/input\")\n\nprint(\"Kaggle working directory\")\nprint(f\"Total:     {working_disk.total / (1024**3):.2f} GiB\")\nprint(f\"Free:      {working_disk.free / (1024**3):.2f} GiB\")\n\nprint(\"\\nKaggle input mount\")\nprint(f\"Total:     {input_disk.total / (1024**3):.2f} GiB\")\nprint(f\"Free:      {input_disk.free / (1024**3):.2f} GiB\")\n\narchive_dir = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\nfor archive in sorted(archive_dir.glob(\"train.zip.*\")):\n    print(f\"{archive.name}: {archive.stat().st_size / (1024**3):.2f} GiB\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:07.702831Z","iopub.execute_input":"2026-10-19T12:09:07.703154Z","iopub.status.idle":"2026-10-19T12:09:07.711015Z","shell.execute_reply.started":"2026-10-19T12:09:07.703132Z","shell.execute_reply":"2026-10-19T12:09:07.710326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport subprocess\nimport time\nfrom pathlib import Path\nfrom PIL import Image\nfrom io import BytesIO\n\narchive_dir = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\n\narchive_parts = sorted(archive_dir.glob(\"train.zip.*\"))\n\nprint(\"Archive volumes:\")\nfor part in archive_parts:\n    print(part.name)\n\n# Use actual image IDs from the EyePACS labels.\n# These are test candidates, not assumptions about which volume contains them.\ncandidate_ids = [\"10_left\", \"9933_left\", \"19088_right\"]\n\nfor image_id in candidate_ids:\n    member = f\"train/{image_id}.jpeg\"\n    start = time.perf_counter()\n\n    result = subprocess.run(\n        [\"7z\", \"x\", \"-so\", str(archive_parts[0]), member],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE\n    )\n\n    elapsed = time.perf_counter() - start\n\n    if result.returncode == 0 and result.stdout:\n        try:\n            image = Image.open(BytesIO(result.stdout)).convert(\"RGB\")\n            print(\n                f\"{image_id}: SUCCESS | \"\n                f\"size={image.size} | \"\n                f\"time={elapsed:.2f}s | \"\n                f\"bytes={len(result.stdout):,}\"\n            )\n        except Exception as exc:\n            print(f\"{image_id}: image decode failed: {exc}\")\n    else:\n        print(\n            f\"{image_id}: NOT FOUND through first volume \"\n            f\"(return code {result.returncode})\"\n        )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:07.712062Z","iopub.execute_input":"2026-10-19T12:09:07.712321Z","iopub.status.idle":"2026-10-19T12:09:08.153411Z","shell.execute_reply.started":"2026-10-19T12:09:07.7123Z","shell.execute_reply":"2026-10-19T12:09:08.152341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom pathlib import Path\nfrom io import BytesIO\nfrom collections import OrderedDict\nimport subprocess\nimport time\n\nimport pandas as pd\nfrom PIL import Image\nfrom torch.utils.data import Dataset\nimport torch\n\nEYE_ROOT = Path(\n    \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n)\nAPTOS_ROOT = Path(\n    \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n)\n\nARCHIVE_PARTS = sorted(EYE_ROOT.glob(\"train.zip.*\"))\n\nassert len(ARCHIVE_PARTS) == 5, (\n    f\"Expected 5 EyePACS archive volumes, found {len(ARCHIVE_PARTS)}\"\n)\n\nclass EyePACSArchiveReader:\n    \"\"\"Read individual EyePACS images without extracting the full archive.\"\"\"\n\n    def __init__(self, cache_size=32):\n        self.cache_size = cache_size\n        self.cache = OrderedDict()\n        self.archive = str(ARCHIVE_PARTS[0])\n\n    def read(self, image_id):\n        if image_id in self.cache:\n            self.cache.move_to_end(image_id)\n            return self.cache[image_id].copy()\n\n        member = f\"train/{image_id}.jpeg\"\n\n        result = subprocess.run(\n            [\"7z\", \"x\", \"-so\", self.archive, member],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            timeout=30\n        )\n\n        if result.returncode != 0 or not result.stdout:\n            raise RuntimeError(\n                f\"Could not read {member} from the archive. \"\n                f\"Check archive membership and volume handling.\"\n            )\n\n        image = Image.open(BytesIO(result.stdout)).convert(\"RGB\")\n        image.load()\n\n        self.cache[image_id] = image.copy()\n\n        if len(self.cache) > self.cache_size:\n            self.cache.popitem(last=False)\n\n        return image\n\neye_reader = EyePACSArchiveReader(cache_size=32)\n\n# Benchmark a few labelled images\neye_labels = pd.read_csv(\n    EYE_ROOT / \"trainLabels.csv.zip\"\n)\n\nsample_ids = (\n    eye_labels.groupby(\"level\", group_keys=False)\n    .sample(n=2, random_state=42)[\"image\"]\n    .tolist()\n)\n\nfor image_id in sample_ids:\n    start = time.perf_counter()\n    image = eye_reader.read(image_id)\n    elapsed = time.perf_counter() - start\n\n    print(\n        f\"{image_id}: size={image.size}, \"\n        f\"time={elapsed:.3f}s\"\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:08.158775Z","iopub.execute_input":"2026-10-19T12:09:08.159156Z","iopub.status.idle":"2026-10-19T12:09:09.784671Z","shell.execute_reply.started":"2026-10-19T12:09:08.159131Z","shell.execute_reply":"2026-10-19T12:09:09.783994Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 18. Unified PyTorch Dataset\n\nThis section implements a common dataset interface for APTOS and EyePACS.\n\nBoth datasets use the same five-grade label convention (0–4), image transformations and output structure. Source identifiers are retained so that performance can later be evaluated separately for each dataset.\n\nImages are loaded on demand. EyePACS images are read from the archive without extracting the entire dataset.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nimport pandas as pd\n\nclass RetinopathyDataset(Dataset):\n    def __init__(self, manifest, transform=None, eye_reader=None):\n        self.manifest = manifest.reset_index(drop=True).copy()\n        self.transform = transform\n        self.eye_reader = eye_reader\n\n    def __len__(self):\n        return len(self.manifest)\n\n    def _load_aptos(self, image_id):\n        image_dir = APTOS_ROOT / \"train_images\"\n\n        for ext in [\".png\", \".jpg\", \".jpeg\"]:\n            path = image_dir / f\"{image_id}{ext}\"\n            if path.exists():\n                with Image.open(path) as img:\n                    return img.convert(\"RGB\")\n\n        raise FileNotFoundError(\n            f\"APTOS image not found: {image_id}\"\n        )\n\n    def __getitem__(self, index):\n        row = self.manifest.iloc[index]\n\n        image_id = str(row[\"image_id\"])\n        source = row[\"source\"]\n        label = int(row[\"label\"])\n\n        if source == \"APTOS\":\n            image = self._load_aptos(image_id)\n\n        elif source == \"EyePACS\":\n            if self.eye_reader is None:\n                raise ValueError(\n                    \"Pass eye_reader=eye_reader for EyePACS images.\"\n                )\n            image = self.eye_reader.read(image_id)\n\n        else:\n            raise ValueError(f\"Unknown source: {source}\")\n\n        if self.transform is not None:\n            image = self.transform(image)\n\n        return {\n            \"image\": image,\n            \"label\": label,\n            \"image_id\": image_id,\n            \"source\": source,\n        }\n\n\n# Small mixed-source test manifest\nmixed_sample = pd.concat([\n    train_manifest[train_manifest[\"source\"] == \"APTOS\"]\n        .sample(n=4, random_state=42),\n    train_manifest[train_manifest[\"source\"] == \"EyePACS\"]\n        .sample(n=4, random_state=42),\n], ignore_index=True)\n\nmixed_dataset = RetinopathyDataset(\n    mixed_sample,\n    transform=val_transform,\n    eye_reader=eye_reader,\n)\n\nmixed_loader = DataLoader(\n    mixed_dataset,\n    batch_size=4,\n    shuffle=False,\n    num_workers=0,\n)\n\nfor batch in mixed_loader:\n    print(\"Image batch shape:\", batch[\"image\"].shape)\n    print(\"Labels:\", batch[\"label\"].tolist())\n    print(\"Image IDs:\", batch[\"image_id\"])\n    print(\"Sources:\", batch[\"source\"])\n    print(\"-\" * 60)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:09.785761Z","iopub.execute_input":"2026-10-19T12:09:09.78647Z","iopub.status.idle":"2026-10-19T12:09:11.499205Z","shell.execute_reply.started":"2026-10-19T12:09:09.786434Z","shell.execute_reply":"2026-10-19T12:09:11.498186Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 19. Training and Validation DataLoaders\n\nThe training and validation manifests define the split used for the initial supervised baseline.\n\nTraining batches are shuffled to reduce ordering effects. Validation batches are not shuffled, making evaluation reproducible. The validation set uses deterministic transformations without random augmentation.\n\nThis experiment uses a batch size of 16 as an initial setting; it may be reduced if GPU memory is insufficient.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nimport pandas as pd\nimport torch\nfrom torch.utils.data import DataLoader\n\n# Restore manifests if needed\nmanifest_dir = Path(\"/kaggle/working/smartretina\")\n\nif \"train_manifest\" not in globals():\n    train_manifest = pd.read_csv(\n        manifest_dir / \"train_manifest.csv\"\n    )\n\nif \"validation_manifest\" not in globals():\n    validation_manifest = pd.read_csv(\n        manifest_dir / \"validation_manifest.csv\"\n    )\n\n# Confirm required objects exist before creating loaders\nrequired = [\n    \"RetinopathyDataset\",\n    \"eye_reader\",\n    \"train_transform\",\n    \"val_transform\",\n]\n\nmissing = [name for name in required if name not in globals()]\n\nif missing:\n    raise RuntimeError(\n        f\"These objects are missing: {missing}. \"\n        \"Rerun their definition cells first.\"\n    )\n\nBATCH_SIZE = 16\nNUM_WORKERS = 0\n\ntrain_dataset = RetinopathyDataset(\n    train_manifest,\n    transform=train_transform,\n    eye_reader=eye_reader,\n)\n\nval_dataset = RetinopathyDataset(\n    validation_manifest,\n    transform=val_transform,\n    eye_reader=eye_reader,\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=NUM_WORKERS,\n    pin_memory=torch.cuda.is_available(),\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=torch.cuda.is_available(),\n)\n\nprint(\"DataLoaders created successfully.\")\nprint(\"Training batches:\", len(train_loader))\nprint(\"Validation batches:\", len(val_loader))\nprint(\"GPU available:\", torch.cuda.is_available())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:11.500262Z","iopub.execute_input":"2026-10-19T12:09:11.500516Z","iopub.status.idle":"2026-10-19T12:09:11.533527Z","shell.execute_reply.started":"2026-10-19T12:09:11.500494Z","shell.execute_reply":"2026-10-19T12:09:11.532624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport time\n\nstart = time.perf_counter()\nbatch = next(iter(train_loader))\nelapsed = time.perf_counter() - start\n\nprint(f\"First batch loading time: {elapsed:.2f} seconds\")\nprint(\"Batch shape:\", batch[\"image\"].shape)\nprint(\"Labels:\", batch[\"label\"].tolist())\nprint(\"Sources:\", batch[\"source\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:11.534622Z","iopub.execute_input":"2026-10-19T12:09:11.534973Z","iopub.status.idle":"2026-10-19T12:09:15.901122Z","shell.execute_reply.started":"2026-10-19T12:09:11.53494Z","shell.execute_reply":"2026-10-19T12:09:15.900345Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n## 21. Training Configuration\n\nThe baseline model is optimized using cross-entropy loss and the Adam optimizer.\n\nThe initial experiment uses a learning rate of 0.0001 and a small number of epochs to verify that training, validation, and metric computation work correctly before committing to longer experiments.\n\nClass weighting is not applied in this first run. Its effect will be evaluated separately because the dataset has an imbalanced grade distribution.\n","metadata":{}},{"cell_type":"markdown","source":"\n## 20. Lightweight Supervised Baseline: MobileNetV3-Small\n\nMobileNetV3-Small is selected as a computationally efficient baseline for five-class diabetic retinopathy grading. ImageNet-pretrained weights are used for initialization, and the final classification layer is replaced to predict grades 0–4.\n\nThis experiment establishes a lightweight supervised reference before self-supervised representation learning. Model performance, inference speed and per-grade classification metrics will be recorded.\n","metadata":{}},{"cell_type":"code","source":"\nimport torch\nimport torch.nn as nn\nfrom torchvision.models import (\n    mobilenet_v3_small,\n    MobileNet_V3_Small_Weights,\n)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# Load ImageNet-pretrained MobileNetV3-Small\nweights = MobileNet_V3_Small_Weights.DEFAULT\nmodel = mobilenet_v3_small(weights=weights)\n\n# Replace the final layer with five DR grade outputs\nin_features = model.classifier[3].in_features\nmodel.classifier[3] = nn.Linear(in_features, 5)\n\nmodel = model.to(device)\n\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(\n    p.numel() for p in model.parameters() if p.requires_grad\n)\n\nprint(\"Model: MobileNetV3-Small\")\nprint(\"Device:\", device)\nprint(\"Output classes:\", model.classifier[3].out_features)\nprint(f\"Total parameters: {total_params:,}\")\nprint(f\"Trainable parameters: {trainable_params:,}\")\nprint(\"Model initialized successfully.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:15.902158Z","iopub.execute_input":"2026-10-19T12:09:15.902403Z","iopub.status.idle":"2026-10-19T12:09:16.256295Z","shell.execute_reply.started":"2026-10-19T12:09:15.902381Z","shell.execute_reply":"2026-10-19T12:09:16.255325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# APTOS-only pilot: use the existing train/validation split\naptos_train = train_manifest[\n    train_manifest[\"source\"] == \"APTOS\"\n].reset_index(drop=True)\n\naptos_val = validation_manifest[\n    validation_manifest[\"source\"] == \"APTOS\"\n].reset_index(drop=True)\n\nprint(\"APTOS training images:\", len(aptos_train))\nprint(\"APTOS validation images:\", len(aptos_val))\nprint(\"\\nTraining grade distribution:\")\nprint(aptos_train[\"label\"].value_counts().sort_index())\nprint(\"\\nValidation grade distribution:\")\nprint(aptos_val[\"label\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:16.258051Z","iopub.execute_input":"2026-10-19T12:09:16.258503Z","iopub.status.idle":"2026-10-19T12:09:16.273577Z","shell.execute_reply.started":"2026-10-19T12:09:16.258481Z","shell.execute_reply":"2026-10-19T12:09:16.272795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import inspect\n\nprint(\"Dataset constructor:\")\nprint(inspect.signature(RetinopathyDataset))\n\nprint(\"\\nTraining transform defined:\", \"train_transform\" in globals())\nprint(\"Validation transform defined:\", \"val_transform\" in globals())\nprint(\"Training manifest available:\", \"train_manifest\" in globals())\nprint(\"Validation manifest available:\", \"validation_manifest\" in globals())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:16.274668Z","iopub.execute_input":"2026-10-19T12:09:16.275038Z","iopub.status.idle":"2026-10-19T12:09:16.282543Z","shell.execute_reply.started":"2026-10-19T12:09:16.275013Z","shell.execute_reply":"2026-10-19T12:09:16.281961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import DataLoader\n\n# APTOS-only datasets: no EyePACS archive reader needed\ntrain_dataset_pilot = RetinopathyDataset(\n    manifest=aptos_train,\n    transform=train_transform,\n    eye_reader=None\n)\n\nval_dataset_pilot = RetinopathyDataset(\n    manifest=aptos_val,\n    transform=val_transform,\n    eye_reader=None\n)\n\ntrain_loader_pilot = DataLoader(\n    train_dataset_pilot,\n    batch_size=32,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nval_loader_pilot = DataLoader(\n    val_dataset_pilot,\n    batch_size=32,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(\"Training batches:\", len(train_loader_pilot))\nprint(\"Validation batches:\", len(val_loader_pilot))\nprint(\"Data loaders created.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:16.283597Z","iopub.execute_input":"2026-10-19T12:09:16.283876Z","iopub.status.idle":"2026-10-19T12:09:16.298235Z","shell.execute_reply.started":"2026-10-19T12:09:16.283825Z","shell.execute_reply":"2026-10-19T12:09:16.297386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"batch = next(iter(train_loader_pilot))\n\nprint(\"Image batch shape:\", batch[\"image\"].shape)\nprint(\"Label batch shape:\", batch[\"label\"].shape)\nprint(\"Labels in batch:\", batch[\"label\"].tolist()[:10])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:16.299402Z","iopub.execute_input":"2026-10-19T12:09:16.299898Z","iopub.status.idle":"2026-10-19T12:09:25.189782Z","shell.execute_reply.started":"2026-10-19T12:09:16.299875Z","shell.execute_reply":"2026-10-19T12:09:25.188962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nimport torch\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = model.to(device)\n\ncriterion = torch.nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=1e-4,\n    weight_decay=1e-4\n)\n\ndef run_epoch(model, loader, training):\n    model.train() if training else model.eval()\n\n    total_loss = 0.0\n    all_labels = []\n    all_preds = []\n\n    start = time.time()\n\n    for batch in loader:\n        images = batch[\"image\"].to(device, non_blocking=True)\n        labels = batch[\"label\"].to(device, dtype=torch.long, non_blocking=True)\n\n        if training:\n            optimizer.zero_grad(set_to_none=True)\n\n        with torch.set_grad_enabled(training):\n            logits = model(images)\n            loss = criterion(logits, labels)\n\n            if training:\n                loss.backward()\n                optimizer.step()\n\n        total_loss += loss.item() * labels.size(0)\n        preds = logits.argmax(dim=1)\n\n        all_labels.extend(labels.cpu().tolist())\n        all_preds.extend(preds.cpu().tolist())\n\n    return {\n        \"loss\": total_loss / len(loader.dataset),\n        \"accuracy\": accuracy_score(all_labels, all_preds),\n        \"macro_f1\": f1_score(\n            all_labels, all_preds, average=\"macro\", zero_division=0\n        ),\n        \"qwk\": cohen_kappa_score(\n            all_labels, all_preds, weights=\"quadratic\"\n        ),\n        \"seconds\": time.time() - start\n    }\n\ntrain_metrics = run_epoch(model, train_loader_pilot, training=True)\nval_metrics = run_epoch(model, val_loader_pilot, training=False)\n\nprint(\"TRAIN:\", train_metrics)\nprint(\"VALIDATION:\", val_metrics)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:09:25.191177Z","iopub.execute_input":"2026-10-19T12:09:25.192094Z","iopub.status.idle":"2026-10-19T12:13:32.182396Z","shell.execute_reply.started":"2026-10-19T12:09:25.192066Z","shell.execute_reply":"2026-10-19T12:13:32.181154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nmodel.eval()\n\nall_true = []\nall_pred = []\n\nwith torch.no_grad():\n    for batch in val_loader_pilot:\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"].to(device, dtype=torch.long)\n\n        logits = model(images)\n        predictions = logits.argmax(dim=1)\n\n        all_true.extend(labels.cpu().numpy())\n        all_pred.extend(predictions.cpu().numpy())\n\ngrade_names = [\n    \"No DR (0)\",\n    \"Mild (1)\",\n    \"Moderate (2)\",\n    \"Severe (3)\",\n    \"Proliferative DR (4)\"\n]\n\nprint(\"CLASSIFICATION REPORT\\n\")\nprint(classification_report(\n    all_true,\n    all_pred,\n    labels=[0, 1, 2, 3, 4],\n    target_names=grade_names,\n    digits=4,\n    zero_division=0\n))\n\ncm = confusion_matrix(all_true, all_pred, labels=[0, 1, 2, 3, 4])\n\nprint(\"CONFUSION MATRIX\")\nprint(pd.DataFrame(\n    cm,\n    index=grade_names,\n    columns=grade_names\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:13:32.184277Z","iopub.execute_input":"2026-10-19T12:13:32.18466Z","iopub.status.idle":"2026-10-19T12:14:20.893753Z","shell.execute_reply.started":"2026-10-19T12:13:32.184626Z","shell.execute_reply":"2026-10-19T12:14:20.892611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"baseline_results = {\n    \"model\": \"MobileNetV3-Small\",\n    \"dataset\": \"APTOS\",\n    \"epochs\": 1,\n    \"validation_accuracy\": 0.6835,\n    \"validation_macro_f1\": 0.3096,\n    \"validation_qwk\": 0.5909\n}\n\nprint(\"Baseline recorded:\")\nfor key, value in baseline_results.items():\n    print(f\"{key}: {value}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:14:20.895479Z","iopub.execute_input":"2026-10-19T12:14:20.895947Z","iopub.status.idle":"2026-10-19T12:14:20.902883Z","shell.execute_reply.started":"2026-10-19T12:14:20.895874Z","shell.execute_reply":"2026-10-19T12:14:20.901998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nfrom sklearn.utils.class_weight import compute_class_weight\n\nclasses = np.array([0, 1, 2, 3, 4])\ntrain_labels = aptos_train[\"label\"].to_numpy()\n\nweights = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=classes,\n    y=train_labels\n)\n\nclass_weights = torch.tensor(\n    weights, dtype=torch.float32, device=device\n)\n\nprint(\"Class weights:\")\nfor grade, weight in enumerate(weights):\n    print(f\"Grade {grade}: {weight:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:14:20.904067Z","iopub.execute_input":"2026-10-19T12:14:20.904419Z","iopub.status.idle":"2026-10-19T12:14:20.922881Z","shell.execute_reply.started":"2026-10-19T12:14:20.904383Z","shell.execute_reply":"2026-10-19T12:14:20.921981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights\nimport torch.nn as nn\n\nmodel_weighted = mobilenet_v3_small(\n    weights=MobileNet_V3_Small_Weights.DEFAULT\n)\n\nmodel_weighted.classifier[3] = nn.Linear(\n    model_weighted.classifier[3].in_features, 5\n)\n\nmodel_weighted = model_weighted.to(device)\n\ncriterion_weighted = nn.CrossEntropyLoss(\n    weight=class_weights\n)\n\noptimizer_weighted = torch.optim.AdamW(\n    model_weighted.parameters(),\n    lr=1e-4,\n    weight_decay=1e-4\n)\n\nprint(\"Fresh MobileNetV3-Small initialized.\")\nprint(\"Weighted cross-entropy enabled.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:14:20.923992Z","iopub.execute_input":"2026-10-19T12:14:20.924393Z","iopub.status.idle":"2026-10-19T12:14:21.037252Z","shell.execute_reply.started":"2026-10-19T12:14:20.92437Z","shell.execute_reply":"2026-10-19T12:14:21.036373Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\ndef run_weighted_epoch(model, loader, training):\n    model.train() if training else model.eval()\n\n    total_loss = 0.0\n    all_labels, all_preds = [], []\n    start = time.time()\n\n    for batch in loader:\n        images = batch[\"image\"].to(device, non_blocking=True)\n        labels = batch[\"label\"].to(\n            device, dtype=torch.long, non_blocking=True\n        )\n\n        if training:\n            optimizer_weighted.zero_grad(set_to_none=True)\n\n        with torch.set_grad_enabled(training):\n            logits = model(images)\n            loss = criterion_weighted(logits, labels)\n\n            if training:\n                loss.backward()\n                optimizer_weighted.step()\n\n        total_loss += loss.item() * labels.size(0)\n        predictions = logits.argmax(dim=1)\n\n        all_labels.extend(labels.cpu().tolist())\n        all_preds.extend(predictions.cpu().tolist())\n\n    return {\n        \"loss\": total_loss / len(loader.dataset),\n        \"accuracy\": accuracy_score(all_labels, all_preds),\n        \"macro_f1\": f1_score(\n            all_labels, all_preds, average=\"macro\", zero_division=0\n        ),\n        \"qwk\": cohen_kappa_score(\n            all_labels, all_preds, weights=\"quadratic\"\n        ),\n        \"seconds\": time.time() - start,\n        \"y_true\": all_labels,\n        \"y_pred\": all_preds\n    }\n\nweighted_train_metrics = run_weighted_epoch(\n    model_weighted, train_loader_pilot, training=True\n)\n\nweighted_val_metrics = run_weighted_epoch(\n    model_weighted, val_loader_pilot, training=False\n)\n\nfor split, metrics in [\n    (\"Weighted training\", weighted_train_metrics),\n    (\"Weighted validation\", weighted_val_metrics)\n]:\n    print(f\"\\n{split}\")\n    for metric in [\"loss\", \"accuracy\", \"macro_f1\", \"qwk\", \"seconds\"]:\n        print(f\"{metric}: {metrics[metric]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:14:21.038385Z","iopub.execute_input":"2026-10-19T12:14:21.038795Z","iopub.status.idle":"2026-10-19T12:18:23.826854Z","shell.execute_reply.started":"2026-10-19T12:14:21.03877Z","shell.execute_reply":"2026-10-19T12:18:23.826016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\n\nSAVE_DIR = \"/kaggle/working/smartretina\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n# Standard baseline\ntorch.save({\n    \"model_state_dict\": model.state_dict(),\n    \"model_name\": \"MobileNetV3-Small\",\n    \"loss_type\": \"CrossEntropy\",\n    \"validation_metrics\": val_metrics,\n    \"epoch\": 1\n}, f\"{SAVE_DIR}/baseline_standard.pth\")\n\n# Weighted baseline\ntorch.save({\n    \"model_state_dict\": model_weighted.state_dict(),\n    \"model_name\": \"MobileNetV3-Small\",\n    \"loss_type\": \"WeightedCrossEntropy\",\n    \"class_weights\": class_weights.cpu(),\n    \"validation_metrics\": {\n        k: v for k, v in weighted_val_metrics.items()\n        if k not in (\"y_true\", \"y_pred\")\n    },\n    \"epoch\": 1\n}, f\"{SAVE_DIR}/baseline_weighted.pth\")\n\nprint(\"Both baseline checkpoints saved!\")\nprint(os.listdir(SAVE_DIR))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:23.828681Z","iopub.execute_input":"2026-10-19T12:18:23.829015Z","iopub.status.idle":"2026-10-19T12:18:23.907816Z","shell.execute_reply.started":"2026-10-19T12:18:23.828986Z","shell.execute_reply":"2026-10-19T12:18:23.907076Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report, confusion_matrix\nimport pandas as pd\n\ngrade_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\ny_true = weighted_val_metrics[\"y_true\"]\ny_pred = weighted_val_metrics[\"y_pred\"]\n\nprint(\"WEIGHTED MODEL CLASSIFICATION REPORT\\n\")\n\nprint(classification_report(\n    y_true,\n    y_pred,\n    labels=[0, 1, 2, 3, 4],\n    target_names=grade_names,\n    digits=4,\n    zero_division=0\n))\n\nprint(\"\\nCONFUSION MATRIX\")\n\ncm = confusion_matrix(\n    y_true,\n    y_pred,\n    labels=[0, 1, 2, 3, 4]\n)\n\ndisplay(pd.DataFrame(\n    cm,\n    index=grade_names,\n    columns=grade_names\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:23.908806Z","iopub.execute_input":"2026-10-19T12:18:23.909272Z","iopub.status.idle":"2026-10-19T12:18:23.93938Z","shell.execute_reply.started":"2026-10-19T12:18:23.909247Z","shell.execute_reply":"2026-10-19T12:18:23.938305Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pandas as pd\nfrom PIL import Image\nfrom pathlib import Path\n\n# Select a few APTOS training images only\nsample_df = aptos_train.sample(\n    n=min(8, len(aptos_train)),\n    random_state=42\n).reset_index(drop=True)\n\ndef get_image_path(row):\n    # Inspect the manifest to identify its actual image-path column\n    return row\n\nprint(\"Manifest columns:\", aptos_train.columns.tolist())\ndisplay(sample_df[[\"source\", \"label\"]].head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:23.94058Z","iopub.execute_input":"2026-10-19T12:18:23.941421Z","iopub.status.idle":"2026-10-19T12:18:24.52496Z","shell.execute_reply.started":"2026-10-19T12:18:23.941394Z","shell.execute_reply":"2026-10-19T12:18:24.524246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nfrom pathlib import Path\n\nimage_id = str(sample_df.iloc[0][\"image_id\"])\nrelative_path = str(sample_df.iloc[0][\"relative_path\"])\n\nprint(\"Image ID:\", image_id)\nprint(\"Relative path:\", relative_path)\n\n# Search likely APTOS dataset locations\nroots = [\n    Path(\"/kaggle/input/competitions/aptos2019-blindness-detection\"),\n    Path(\"/kaggle/input\")\n]\n\nmatches = []\n\nfor root in roots:\n    if root.exists():\n        for pattern in [\n            f\"{image_id}.*\",\n            f\"{image_id}\",\n            f\"**/{image_id}.*\",\n            f\"**/{image_id}\"\n        ]:\n            matches.extend(root.glob(pattern))\n\nmatches = list(dict.fromkeys(p for p in matches if p.is_file()))\n\nprint(\"\\nMatching files:\")\nfor path in matches[:10]:\n    print(path)\n\nif not matches:\n    print(\"No matching file found in the searched roots.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:24.526116Z","iopub.execute_input":"2026-10-19T12:18:24.526505Z","iopub.status.idle":"2026-10-19T12:18:47.891037Z","shell.execute_reply.started":"2026-10-19T12:18:24.52648Z","shell.execute_reply":"2026-10-19T12:18:47.890263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport cv2\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nAPTOS_ROOT = Path(\n    \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n)\n\ndef load_aptos_image(row):\n    image_id = str(row[\"image_id\"])\n\n    # APTOS images use PNG files\n    path = APTOS_ROOT / \"train_images\" / f\"{image_id}.png\"\n\n    if not path.is_file():\n        raise FileNotFoundError(f\"Image not found: {path}\")\n\n    image = cv2.imread(str(path))\n    if image is None:\n        raise ValueError(f\"Could not decode image: {path}\")\n\n    return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n\ndef apply_clahe_rgb(image):\n    lab = cv2.cvtColor(image, cv2.COLOR_RGB2LAB)\n    l, a, b = cv2.split(lab)\n\n    clahe = cv2.createCLAHE(\n        clipLimit=2.0,\n        tileGridSize=(8, 8)\n    )\n\n    enhanced_l = clahe.apply(l)\n    enhanced = cv2.merge([enhanced_l, a, b])\n\n    return cv2.cvtColor(enhanced, cv2.COLOR_LAB2RGB)\n\n\nsample_df = aptos_train.sample(\n    n=min(8, len(aptos_train)),\n    random_state=42\n).reset_index(drop=True)\n\nfig, axes = plt.subplots(8, 2, figsize=(10, 28))\n\nfor i, row in sample_df.iterrows():\n    original = load_aptos_image(row)\n    enhanced = apply_clahe_rgb(original)\n\n    axes[i, 0].imshow(original)\n    axes[i, 0].set_title(f\"Original | Grade {row['label']}\")\n    axes[i, 0].axis(\"off\")\n\n    axes[i, 1].imshow(enhanced)\n    axes[i, 1].set_title(\"CLAHE enhanced\")\n    axes[i, 1].axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:47.892125Z","iopub.execute_input":"2026-10-19T12:18:47.892993Z","iopub.status.idle":"2026-10-19T12:18:57.680604Z","shell.execute_reply.started":"2026-10-19T12:18:47.89296Z","shell.execute_reply":"2026-10-19T12:18:57.677Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nrow = sample_df.iloc[0]\n\noriginal = load_aptos_image(row)\nenhanced = apply_clahe_rgb(original)\n\nprint(\"Image ID:\", row[\"image_id\"])\nprint(\"DR grade:\", row[\"label\"])\nprint(\"Original shape:\", original.shape)\nprint(\"CLAHE shape:\", enhanced.shape)\nprint(\"Original dtype:\", original.dtype)\nprint(\"CLAHE dtype:\", enhanced.dtype)\nprint(\"CLAHE pixel range:\", enhanced.min(), \"to\", enhanced.max())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:57.682018Z","iopub.execute_input":"2026-10-19T12:18:57.682576Z","iopub.status.idle":"2026-10-19T12:18:58.173637Z","shell.execute_reply.started":"2026-10-19T12:18:57.682531Z","shell.execute_reply":"2026-10-19T12:18:58.172861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport cv2\nimport numpy as np\nfrom PIL import Image\nfrom torch.utils.data import Dataset\n\nclass CLAHERetinopathyDataset(Dataset):\n    def __init__(self, manifest, transform=None, eye_reader=None):\n        self.base_dataset = RetinopathyDataset(\n            manifest=manifest,\n            transform=None,\n            eye_reader=eye_reader\n        )\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.base_dataset)\n\n    def __getitem__(self, idx):\n        item = self.base_dataset[idx]\n\n        # Convert tensor/PIL/array safely to an RGB uint8 image\n        image = item[\"image\"]\n\n        if isinstance(image, Image.Image):\n            image = np.array(image.convert(\"RGB\"))\n        elif isinstance(image, torch.Tensor):\n            image = image.detach().cpu().permute(1, 2, 0).numpy()\n            if image.max() <= 1:\n                image = (image * 255).clip(0, 255).astype(np.uint8)\n        else:\n            image = np.asarray(image)\n\n        if image.dtype != np.uint8:\n            image = np.clip(image, 0, 255).astype(np.uint8)\n\n        # Apply CLAHE to luminance\n        lab = cv2.cvtColor(image, cv2.COLOR_RGB2LAB)\n        l, a, b = cv2.split(lab)\n\n        clahe = cv2.createCLAHE(\n            clipLimit=2.0,\n            tileGridSize=(8, 8)\n        )\n\n        enhanced_l = clahe.apply(l)\n        enhanced = cv2.merge((enhanced_l, a, b))\n        enhanced = cv2.cvtColor(enhanced, cv2.COLOR_LAB2RGB)\n\n        item[\"image\"] = (\n            self.transform(Image.fromarray(enhanced))\n            if self.transform\n            else Image.fromarray(enhanced)\n        )\n\n        return item\n\nprint(\"CLAHE dataset class defined.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:58.174684Z","iopub.execute_input":"2026-10-19T12:18:58.175062Z","iopub.status.idle":"2026-10-19T12:18:58.186694Z","shell.execute_reply.started":"2026-10-19T12:18:58.175036Z","shell.execute_reply":"2026-10-19T12:18:58.186045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclahe_test_dataset = CLAHERetinopathyDataset(\n    manifest=aptos_train.head(8).reset_index(drop=True),\n    transform=train_transform,\n    eye_reader=None\n)\n\ntest_item = clahe_test_dataset[0]\n\nprint(\"Image tensor shape:\", test_item[\"image\"].shape)\nprint(\"Label:\", test_item[\"label\"])\nprint(\"Image dtype:\", test_item[\"image\"].dtype)\nprint(\"CLAHE preprocessing test passed.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:58.187801Z","iopub.execute_input":"2026-10-19T12:18:58.188666Z","iopub.status.idle":"2026-10-19T12:18:58.288094Z","shell.execute_reply.started":"2026-10-19T12:18:58.188636Z","shell.execute_reply":"2026-10-19T12:18:58.287263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import DataLoader\nimport pandas as pd\n\n# Small experimental subset; use training data only for fitting\n# Stratify so all five grades are represented.\npreprocess_train, _ = train_test_split(\n    aptos_train,\n    train_size=min(500, len(aptos_train)),\n    random_state=42,\n    stratify=aptos_train[\"label\"]\n)\n\n# Use a small, stratified sample from the existing validation split\npreprocess_val, _ = train_test_split(\n    aptos_val,\n    train_size=min(150, len(aptos_val)),\n    random_state=123,\n    stratify=aptos_val[\"label\"]\n)\n\npreprocess_train = preprocess_train.reset_index(drop=True)\npreprocess_val = preprocess_val.reset_index(drop=True)\n\nprint(\"Preprocessing training subset:\", len(preprocess_train))\nprint(\"Preprocessing validation subset:\", len(preprocess_val))\nprint(\"\\nTraining grades:\")\nprint(preprocess_train[\"label\"].value_counts().sort_index())\nprint(\"\\nValidation grades:\")\nprint(preprocess_val[\"label\"].value_counts().sort_index())\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:58.289075Z","iopub.execute_input":"2026-10-19T12:18:58.289357Z","iopub.status.idle":"2026-10-19T12:18:58.309254Z","shell.execute_reply.started":"2026-10-19T12:18:58.289335Z","shell.execute_reply":"2026-10-19T12:18:58.308009Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom torch.utils.data import DataLoader\n\n# Original images: same resize, augmentation and normalization as baseline\noriginal_train_dataset = RetinopathyDataset(\n    manifest=preprocess_train,\n    transform=train_transform,\n    eye_reader=None\n)\n\noriginal_val_dataset = RetinopathyDataset(\n    manifest=preprocess_val,\n    transform=val_transform,\n    eye_reader=None\n)\n\n# CLAHE images: same transforms after contrast enhancement\nclahe_train_dataset = CLAHERetinopathyDataset(\n    manifest=preprocess_train,\n    transform=train_transform,\n    eye_reader=None\n)\n\nclahe_val_dataset = CLAHERetinopathyDataset(\n    manifest=preprocess_val,\n    transform=val_transform,\n    eye_reader=None\n)\n\n# Keep batch size modest to limit memory usage\noriginal_train_loader = DataLoader(\n    original_train_dataset,\n    batch_size=32,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\noriginal_val_loader = DataLoader(\n    original_val_dataset,\n    batch_size=32,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nclahe_train_loader = DataLoader(\n    clahe_train_dataset,\n    batch_size=32,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nclahe_val_loader = DataLoader(\n    clahe_val_dataset,\n    batch_size=32,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\nprint(\"Original train batches:\", len(original_train_loader))\nprint(\"Original validation batches:\", len(original_val_loader))\nprint(\"CLAHE train batches:\", len(clahe_train_loader))\nprint(\"CLAHE validation batches:\", len(clahe_val_loader))\nprint(\"All loaders created.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:58.310637Z","iopub.execute_input":"2026-10-19T12:18:58.311091Z","iopub.status.idle":"2026-10-19T12:18:58.320519Z","shell.execute_reply.started":"2026-10-19T12:18:58.311067Z","shell.execute_reply":"2026-10-19T12:18:58.319597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\noriginal_batch = next(iter(original_train_loader))\nclahe_batch = next(iter(clahe_train_loader))\n\nprint(\"Original batch:\", original_batch[\"image\"].shape)\nprint(\"CLAHE batch:\", clahe_batch[\"image\"].shape)\n\nprint(\"Original labels:\", original_batch[\"label\"][:8].tolist())\nprint(\"CLAHE labels:\", clahe_batch[\"label\"][:8].tolist())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:18:58.321609Z","iopub.execute_input":"2026-10-19T12:18:58.322065Z","iopub.status.idle":"2026-10-19T12:19:22.998109Z","shell.execute_reply.started":"2026-10-19T12:18:58.322031Z","shell.execute_reply":"2026-10-19T12:19:22.996879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport copy\nimport torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights\n\n# Create one common initialization, then copy it for both experiments\ntorch.manual_seed(42)\n\nbase_model = mobilenet_v3_small(\n    weights=MobileNet_V3_Small_Weights.DEFAULT\n)\nbase_model.classifier[3] = nn.Linear(\n    base_model.classifier[3].in_features, 5\n)\n\ninitial_state = copy.deepcopy(base_model.state_dict())\n\nmodel_original = mobilenet_v3_small(weights=None)\nmodel_original.classifier[3] = nn.Linear(\n    model_original.classifier[3].in_features, 5\n)\nmodel_original.load_state_dict(initial_state)\nmodel_original = model_original.to(device)\n\nmodel_clahe = mobilenet_v3_small(weights=None)\nmodel_clahe.classifier[3] = nn.Linear(\n    model_clahe.classifier[3].in_features, 5\n)\nmodel_clahe.load_state_dict(initial_state)\nmodel_clahe = model_clahe.to(device)\n\nprint(\"Original and CLAHE models initialized identically.\")\nprint(\"Device:\", device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:19:23.000327Z","iopub.execute_input":"2026-10-19T12:19:23.001501Z","iopub.status.idle":"2026-10-19T12:19:23.240545Z","shell.execute_reply.started":"2026-10-19T12:19:23.001468Z","shell.execute_reply":"2026-10-19T12:19:23.239185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport time\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\ndef run_preprocessing_experiment(model, train_loader, val_loader):\n    criterion = nn.CrossEntropyLoss()\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=1e-4,\n        weight_decay=1e-4\n    )\n\n    model.train()\n    start = time.time()\n\n    for batch in train_loader:\n        images = batch[\"image\"].to(device, non_blocking=True)\n        labels = batch[\"label\"].to(\n            device, dtype=torch.long, non_blocking=True\n        )\n\n        optimizer.zero_grad(set_to_none=True)\n        logits = model(images)\n        loss = criterion(logits, labels)\n        loss.backward()\n        optimizer.step()\n\n    train_seconds = time.time() - start\n\n    model.eval()\n    y_true, y_pred = [], []\n    val_loss_total = 0.0\n    start = time.time()\n\n    with torch.no_grad():\n        for batch in val_loader:\n            images = batch[\"image\"].to(device, non_blocking=True)\n            labels = batch[\"label\"].to(\n                device, dtype=torch.long, non_blocking=True\n            )\n\n            logits = model(images)\n            loss = criterion(logits, labels)\n            val_loss_total += loss.item() * labels.size(0)\n\n            y_true.extend(labels.cpu().tolist())\n            y_pred.extend(logits.argmax(dim=1).cpu().tolist())\n\n    val_seconds = time.time() - start\n\n    return {\n        \"accuracy\": accuracy_score(y_true, y_pred),\n        \"macro_f1\": f1_score(\n            y_true, y_pred, average=\"macro\", zero_division=0\n        ),\n        \"qwk\": cohen_kappa_score(\n            y_true, y_pred, weights=\"quadratic\"\n        ),\n        \"val_loss\": val_loss_total / len(val_loader.dataset),\n        \"train_seconds\": train_seconds,\n        \"val_seconds\": val_seconds\n    }\n\noriginal_results = run_preprocessing_experiment(\n    model_original,\n    original_train_loader,\n    original_val_loader\n)\n\nprint(\"Original image results:\")\nprint(original_results)\n\nclahe_results = run_preprocessing_experiment(\n    model_clahe,\n    clahe_train_loader,\n    clahe_val_loader\n)\n\nprint(\"\\nCLAHE image results:\")\nprint(clahe_results)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:19:23.241554Z","iopub.execute_input":"2026-10-19T12:19:23.24248Z","iopub.status.idle":"2026-10-19T12:21:39.997064Z","shell.execute_reply.started":"2026-10-19T12:19:23.242456Z","shell.execute_reply":"2026-10-19T12:21:39.995988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We should use only training images for SSL pretraining, without passing their grade labels into the learning objective. Validation images remain held out.","metadata":{}},{"cell_type":"code","source":"\nprint(\"Full APTOS training dataset:\", len(aptos_train))\nprint(\"Full APTOS validation dataset:\", len(aptos_val))\n\nprint(\"\\nAvailable loaders:\")\nfor name in [\n    \"train_loader_pilot\",\n    \"val_loader_pilot\",\n    \"original_train_loader\",\n    \"original_val_loader\",\n    \"clahe_train_loader\",\n    \"clahe_val_loader\"\n]:\n    obj = globals().get(name)\n    if obj is not None:\n        print(f\"{name}: {len(obj.dataset)} images\")\n    else:\n        print(f\"{name}: not available\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:21:39.998849Z","iopub.execute_input":"2026-10-19T12:21:39.999335Z","iopub.status.idle":"2026-10-19T12:21:40.006819Z","shell.execute_reply.started":"2026-10-19T12:21:39.999307Z","shell.execute_reply":"2026-10-19T12:21:40.005987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport time\nimport torch\nimport torch.nn as nn\nimport pandas as pd\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\n# Start from the existing standard baseline model\nmodel = model.to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=5e-5,              # Lower LR for fine-tuning\n    weight_decay=1e-4\n)\n\nNUM_EPOCHS = 3\nbest_qwk = -1.0\nhistory = []\n\nsave_dir = \"/kaggle/working/smartretina\"\nos.makedirs(save_dir, exist_ok=True)\n\ndef evaluate_model(model, loader):\n    model.eval()\n    y_true, y_pred = [], []\n    total_loss = 0.0\n\n    with torch.no_grad():\n        for batch in loader:\n            images = batch[\"image\"].to(device, non_blocking=True)\n            labels = batch[\"label\"].to(\n                device, dtype=torch.long, non_blocking=True\n            )\n\n            logits = model(images)\n            loss = criterion(logits, labels)\n\n            total_loss += loss.item() * labels.size(0)\n            y_true.extend(labels.cpu().tolist())\n            y_pred.extend(logits.argmax(dim=1).cpu().tolist())\n\n    return {\n        \"val_loss\": total_loss / len(loader.dataset),\n        \"accuracy\": accuracy_score(y_true, y_pred),\n        \"macro_f1\": f1_score(\n            y_true, y_pred, average=\"macro\", zero_division=0\n        ),\n        \"qwk\": cohen_kappa_score(\n            y_true, y_pred, weights=\"quadratic\"\n        )\n    }\n\nfor epoch in range(1, NUM_EPOCHS + 1):\n    model.train()\n    running_loss = 0.0\n    start = time.time()\n\n    for batch_idx, batch in enumerate(train_loader_pilot, start=1):\n        images = batch[\"image\"].to(device, non_blocking=True)\n        labels = batch[\"label\"].to(\n            device, dtype=torch.long, non_blocking=True\n        )\n\n        optimizer.zero_grad(set_to_none=True)\n        logits = model(images)\n        loss = criterion(logits, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * labels.size(0)\n\n        if batch_idx % 25 == 0:\n            print(\n                f\"Epoch {epoch}/{NUM_EPOCHS} | \"\n                f\"Batch {batch_idx}/{len(train_loader_pilot)}\"\n            )\n\n    train_loss = running_loss / len(train_loader_pilot.dataset)\n    metrics = evaluate_model(model, val_loader_pilot)\n\n    row = {\n        \"epoch\": epoch,\n        \"train_loss\": train_loss,\n        **metrics,\n        \"seconds\": time.time() - start\n    }\n    history.append(row)\n\n    print(\"\\nEpoch results:\", row)\n\n    if metrics[\"qwk\"] > best_qwk:\n        best_qwk = metrics[\"qwk\"]\n        torch.save({\n            \"epoch\": epoch,\n            \"model_state_dict\": model.state_dict(),\n            \"optimizer_state_dict\": optimizer.state_dict(),\n            \"metrics\": metrics\n        }, f\"{save_dir}/mobilenetv3_full_aptos_best.pth\")\n        print(\"Saved new best checkpoint.\")\n\nresults_df = pd.DataFrame(history)\ndisplay(results_df)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:21:40.008697Z","iopub.execute_input":"2026-10-19T12:21:40.009427Z","iopub.status.idle":"2026-10-19T12:33:55.135802Z","shell.execute_reply.started":"2026-10-19T12:21:40.009403Z","shell.execute_reply":"2026-10-19T12:33:55.134841Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nbatch = next(iter(val_loader_pilot))\n\nprint(\"Batch type:\", type(batch))\n\nif isinstance(batch, dict):\n    print(\"Batch keys:\", batch.keys())\n    for key, value in batch.items():\n        print(\n            f\"Key: {key!r} | Type: {type(value)} | \"\n            f\"Shape: {getattr(value, 'shape', 'N/A')}\"\n        )\nelse:\n    print(\"Batch length:\", len(batch))\n    for i, value in enumerate(batch):\n        print(\n            f\"Index: {i} | Type: {type(value)} | \"\n            f\"Shape: {getattr(value, 'shape', 'N/A')}\"\n        )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:33:55.137584Z","iopub.execute_input":"2026-10-19T12:33:55.138003Z","iopub.status.idle":"2026-10-19T12:34:03.482297Z","shell.execute_reply.started":"2026-10-19T12:33:55.137966Z","shell.execute_reply":"2026-10-19T12:34:03.481254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport torch\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.metrics import classification_report, confusion_matrix\n\ncheckpoint_path = \"/kaggle/working/smartretina/mobilenetv3_full_aptos_best.pth\"\n\ncheckpoint = torch.load(\n    checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\nmodel.load_state_dict(checkpoint[\"model_state_dict\"])\nmodel = model.to(device)\nmodel.eval()\n\ny_true = []\ny_pred = []\n\nwith torch.no_grad():\n    for batch in val_loader_pilot:\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"]\n\n        outputs = model(images)\n        predictions = outputs.argmax(dim=1)\n\n        y_true.extend(labels.cpu().numpy())\n        y_pred.extend(predictions.cpu().numpy())\n\nclass_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\nprint(\"Best checkpoint epoch:\", checkpoint[\"epoch\"])\nprint(\"\\nClassification Report:\\n\")\n\nprint(classification_report(\n    y_true,\n    y_pred,\n    labels=[0, 1, 2, 3, 4],\n    target_names=class_names,\n    digits=4,\n    zero_division=0\n))\n\ncm = confusion_matrix(y_true, y_pred, labels=[0, 1, 2, 3, 4])\n\nplt.figure(figsize=(9, 7))\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    xticklabels=class_names,\n    yticklabels=class_names\n)\nplt.xlabel(\"Predicted grade\")\nplt.ylabel(\"Actual grade\")\nplt.title(\"MobileNetV3-Small — APTOS Validation Confusion Matrix\")\nplt.xticks(rotation=35, ha=\"right\")\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:03.484077Z","iopub.execute_input":"2026-10-19T12:34:03.485054Z","iopub.status.idle":"2026-10-19T12:34:52.721597Z","shell.execute_reply.started":"2026-10-19T12:34:03.485028Z","shell.execute_reply":"2026-10-19T12:34:52.720828Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport json\nimport os\n\nsave_dir = \"/kaggle/working/smartretina\"\nos.makedirs(save_dir, exist_ok=True)\n\nbaseline_results = {\n    \"model\": \"MobileNetV3-Small\",\n    \"dataset\": \"APTOS 2019\",\n    \"best_epoch\": 3,\n    \"validation_accuracy\": 0.7844474761,\n    \"validation_macro_f1\": 0.5458697880,\n    \"validation_qwk\": 0.8267928095,\n    \"validation_loss\": 0.5725563947,\n    \"per_class_f1\": {\n        \"No DR\": 0.9529,\n        \"Mild\": 0.4545,\n        \"Moderate\": 0.7637,\n        \"Severe\": 0.2400,\n        \"Proliferative DR\": 0.3182\n    }\n}\n\nresults_path = os.path.join(save_dir, \"supervised_baseline_results.json\")\n\nwith open(results_path, \"w\") as f:\n    json.dump(baseline_results, f, indent=4)\n\nprint(\"Baseline results saved to:\", results_path)\nprint(json.dumps(baseline_results, indent=2))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:52.723209Z","iopub.execute_input":"2026-10-19T12:34:52.723617Z","iopub.status.idle":"2026-10-19T12:34:52.73167Z","shell.execute_reply.started":"2026-10-19T12:34:52.723592Z","shell.execute_reply":"2026-10-19T12:34:52.730762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport pandas as pd\n\nAPTOS_ROOT = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nEYEPACS_ROOT = \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n\nprint(\"APTOS root exists:\", os.path.exists(APTOS_ROOT))\nprint(\"EyePACS root exists:\", os.path.exists(EYEPACS_ROOT))\n\nprint(\"\\nAPTOS files:\")\nif os.path.exists(APTOS_ROOT):\n    print(os.listdir(APTOS_ROOT))\n\nprint(\"\\nEyePACS files:\")\nif os.path.exists(EYEPACS_ROOT):\n    print(os.listdir(EYEPACS_ROOT))\n\nprint(\"\\nExisting manifests:\")\nfor name in [\"train_manifest\", \"validation_manifest\", \"aptos_train\", \"aptos_val\"]:\n    obj = globals().get(name)\n    if isinstance(obj, pd.DataFrame):\n        print(f\"\\n{name}: {obj.shape}\")\n        print(\"Columns:\", obj.columns.tolist())\n        if \"source\" in obj.columns:\n            print(\"Sources:\", obj[\"source\"].value_counts().to_dict())\n        if \"label\" in obj.columns:\n            print(\"Grade counts:\", obj[\"label\"].value_counts().sort_index().to_dict())\n    else:\n        print(f\"{name}: not found\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:52.732817Z","iopub.execute_input":"2026-10-19T12:34:52.733631Z","iopub.status.idle":"2026-10-19T12:34:52.775481Z","shell.execute_reply.started":"2026-10-19T12:34:52.733599Z","shell.execute_reply":"2026-10-19T12:34:52.774366Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport zipfile\nimport pandas as pd\n\nEYEPACS_ROOT = \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\n\n# Inspect the existing EyePACS reader, if already defined.\nprint(\"EyePACS-related variables:\")\nfor name in globals():\n    if \"eye\" in name.lower() and (\n        \"reader\" in name.lower() or \"zip\" in name.lower()\n    ):\n        obj = globals()[name]\n        print(name, \"->\", type(obj))\n\nprint(\"\\nEyePACS archive files:\")\narchive_files = sorted(\n    f for f in os.listdir(EYEPACS_ROOT)\n    if f.startswith(\"train.zip.\")\n)\n\nfor filename in archive_files:\n    path = os.path.join(EYEPACS_ROOT, filename)\n    print(f\"{filename}: {os.path.getsize(path) / (1024**3):.2f} GiB\")\n\nprint(\"\\nEyePACS manifest examples:\")\ndisplay(\n    train_manifest.loc[\n        train_manifest[\"source\"] == \"EyePACS\",\n        [\"image_id\", \"label\", \"relative_path\", \"split\"]\n    ].head(5)\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:52.776677Z","iopub.execute_input":"2026-10-19T12:34:52.777058Z","iopub.status.idle":"2026-10-19T12:34:52.843164Z","shell.execute_reply.started":"2026-10-19T12:34:52.777018Z","shell.execute_reply":"2026-10-19T12:34:52.842464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport pandas as pd\n\nAPTOS_ROOT = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nAPTOS_IMAGE_DIR = os.path.join(APTOS_ROOT, \"train_images\")\n\n# Use only images from the existing APTOS training split.\nssl_manifest = aptos_train.copy().reset_index(drop=True)\n\n# Confirm each manifest entry maps to an existing PNG file.\nssl_manifest[\"image_path\"] = ssl_manifest[\"image_id\"].apply(\n    lambda image_id: os.path.join(\n        APTOS_IMAGE_DIR,\n        f\"{image_id}.png\"\n    )\n)\n\nssl_manifest[\"exists\"] = ssl_manifest[\"image_path\"].apply(\n    os.path.isfile\n)\n\nprint(\"APTOS images prepared for SSL:\", len(ssl_manifest))\nprint(\"Existing image files:\", int(ssl_manifest[\"exists\"].sum()))\nprint(\"Missing image files:\", int((~ssl_manifest[\"exists\"]).sum()))\n\nprint(\"\\nImage examples:\")\ndisplay(\n    ssl_manifest[\n        [\"image_id\", \"image_path\", \"exists\"]\n    ].head()\n)\n\nassert ssl_manifest[\"exists\"].all(), (\n    \"Some APTOS image paths are missing. Check the paths before continuing.\"\n)\n\nprint(\"\\nReady for SSL data preparation!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:52.844439Z","iopub.execute_input":"2026-10-19T12:34:52.844725Z","iopub.status.idle":"2026-10-19T12:34:58.230164Z","shell.execute_reply.started":"2026-10-19T12:34:52.8447Z","shell.execute_reply":"2026-10-19T12:34:58.229314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport random\nimport numpy as np\nimport torch\nfrom PIL import Image, ImageEnhance, ImageFilter\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.transforms import InterpolationMode\n\nclass RetinalSSLTransform:\n    \"\"\"Create two independently augmented views of one retinal image.\"\"\"\n\n    def __init__(self, image_size=224):\n        self.transform = transforms.Compose([\n            transforms.Resize(\n                (image_size, image_size),\n                interpolation=InterpolationMode.BILINEAR\n            ),\n            transforms.RandomHorizontalFlip(p=0.5),\n            transforms.RandomRotation(\n                degrees=10,\n                interpolation=InterpolationMode.BILINEAR\n            ),\n            transforms.ColorJitter(\n                brightness=0.15,\n                contrast=0.15,\n                saturation=0.05,\n                hue=0.01\n            ),\n            transforms.ToTensor(),\n            transforms.Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225]\n            )\n        ])\n\n    def __call__(self, image):\n        view1 = self.transform(image)\n        view2 = self.transform(image)\n        return view1, view2\n\n\nclass APTOSSSLDataset(Dataset):\n    def __init__(self, manifest, transform):\n        self.manifest = manifest.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.manifest)\n\n    def __getitem__(self, idx):\n        row = self.manifest.iloc[idx]\n\n        with Image.open(row[\"image_path\"]) as img:\n            image = img.convert(\"RGB\")\n\n        view1, view2 = self.transform(image)\n\n        # No grade label is returned to the SSL training loop.\n        return view1, view2\n\n\nssl_transform = RetinalSSLTransform(image_size=224)\n\nssl_dataset = APTOSSSLDataset(\n    ssl_manifest,\n    transform=ssl_transform\n)\n\nssl_loader = DataLoader(\n    ssl_dataset,\n    batch_size=32,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=torch.cuda.is_available(),\n    drop_last=True\n)\n\n# Inspect one batch before training.\nview1, view2 = next(iter(ssl_loader))\n\nprint(\"SSL images:\", len(ssl_dataset))\nprint(\"Batches per epoch:\", len(ssl_loader))\nprint(\"View 1 shape:\", view1.shape)\nprint(\"View 2 shape:\", view2.shape)\nprint(\"Same view shape:\", view1.shape == view2.shape)\nprint(\"Device available:\", torch.cuda.get_device_name(0)\n      if torch.cuda.is_available() else \"CPU\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:34:58.231465Z","iopub.execute_input":"2026-10-19T12:34:58.23184Z","iopub.status.idle":"2026-10-19T12:35:08.046362Z","shell.execute_reply.started":"2026-10-19T12:34:58.231815Z","shell.execute_reply":"2026-10-19T12:35:08.045026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights\n\n# Encoder: MobileNetV3-Small\nencoder = mobilenet_v3_small(\n    weights=MobileNet_V3_Small_Weights.DEFAULT\n)\n\nfeature_dim = encoder.classifier[0].in_features\n\n# Remove the original ImageNet classification head.\nencoder.classifier = nn.Identity()\n\n\nclass SimCLR(nn.Module):\n    def __init__(self, encoder, feature_dim, projection_dim=128):\n        super().__init__()\n        self.encoder = encoder\n\n        self.projector = nn.Sequential(\n            nn.Linear(feature_dim, 256),\n            nn.ReLU(inplace=True),\n            nn.Linear(256, projection_dim)\n        )\n\n    def forward(self, x):\n        features = self.encoder(x)\n        projections = self.projector(features)\n        projections = F.normalize(projections, dim=1)\n        return features, projections\n\n\ndef nt_xent_loss(z1, z2, temperature=0.2):\n    \"\"\"\n    Normalized temperature-scaled cross-entropy loss.\n    Positive pairs are the two views of the same image.\n    \"\"\"\n    batch_size = z1.size(0)\n\n    z = torch.cat([z1, z2], dim=0)\n    similarity = torch.matmul(z, z.T) / temperature\n\n    # Exclude each sample's similarity with itself.\n    self_mask = torch.eye(\n        2 * batch_size,\n        device=z.device,\n        dtype=torch.bool\n    )\n    similarity = similarity.masked_fill(self_mask, -1e9)\n\n    # Pair each view with the other view of the same image.\n    targets = torch.cat([\n        torch.arange(batch_size, 2 * batch_size, device=z.device),\n        torch.arange(0, batch_size, device=z.device)\n    ])\n\n    return F.cross_entropy(similarity, targets)\n\n\nssl_model = SimCLR(\n    encoder=encoder,\n    feature_dim=feature_dim,\n    projection_dim=128\n).to(device)\n\n# Test a forward pass before training.\nsample_view = view1[:4].to(device)\n\nwith torch.no_grad():\n    features, projections = ssl_model(sample_view)\n\ntest_loss = nt_xent_loss(projections, projections)\n\nprint(\"Encoder feature dimension:\", feature_dim)\nprint(\"Features shape:\", features.shape)\nprint(\"Projections shape:\", projections.shape)\nprint(\"Test loss:\", round(test_loss.item(), 4))\nprint(\"Model device:\", next(ssl_model.parameters()).device)\nprint(\n    \"Trainable parameters:\",\n    f\"{sum(p.numel() for p in ssl_model.parameters() if p.requires_grad):,}\"\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:35:08.048097Z","iopub.execute_input":"2026-10-19T12:35:08.048491Z","iopub.status.idle":"2026-10-19T12:35:08.422162Z","shell.execute_reply.started":"2026-10-19T12:35:08.048465Z","shell.execute_reply":"2026-10-19T12:35:08.421378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport time\nimport torch\nimport torch.optim as optim\n\nsave_dir = \"/kaggle/working/smartretina\"\nos.makedirs(save_dir, exist_ok=True)\n\n# Keep training stable for the first SSL experiment.\nssl_model = ssl_model.to(device)\nssl_optimizer = optim.AdamW(\n    ssl_model.parameters(),\n    lr=3e-4,\n    weight_decay=1e-4\n)\n\nSSL_EPOCHS = 5\nssl_history = []\nbest_ssl_loss = float(\"inf\")\n\nfor epoch in range(1, SSL_EPOCHS + 1):\n    ssl_model.train()\n    running_loss = 0.0\n    total_samples = 0\n    start_time = time.time()\n\n    for batch_idx, (x1, x2) in enumerate(ssl_loader, start=1):\n        x1 = x1.to(device, non_blocking=True)\n        x2 = x2.to(device, non_blocking=True)\n\n        ssl_optimizer.zero_grad(set_to_none=True)\n\n        _, z1 = ssl_model(x1)\n        _, z2 = ssl_model(x2)\n\n        loss = nt_xent_loss(z1, z2, temperature=0.2)\n        loss.backward()\n        ssl_optimizer.step()\n\n        batch_size = x1.size(0)\n        running_loss += loss.item() * batch_size\n        total_samples += batch_size\n\n        if batch_idx % 20 == 0 or batch_idx == len(ssl_loader):\n            print(\n                f\"Epoch {epoch}/{SSL_EPOCHS} | \"\n                f\"Batch {batch_idx}/{len(ssl_loader)} | \"\n                f\"Loss: {loss.item():.4f}\"\n            )\n\n    epoch_loss = running_loss / total_samples\n    elapsed = time.time() - start_time\n\n    row = {\n        \"epoch\": epoch,\n        \"ssl_train_loss\": epoch_loss,\n        \"seconds\": elapsed\n    }\n    ssl_history.append(row)\n\n    print(\n        f\"\\nEpoch {epoch} complete | \"\n        f\"Mean SSL loss: {epoch_loss:.4f} | \"\n        f\"Time: {elapsed:.1f}s\"\n    )\n\n    if epoch_loss < best_ssl_loss:\n        best_ssl_loss = epoch_loss\n\n        torch.save(\n            {\n                \"epoch\": epoch,\n                \"encoder_state_dict\": ssl_model.encoder.state_dict(),\n                \"projector_state_dict\": ssl_model.projector.state_dict(),\n                \"optimizer_state_dict\": ssl_optimizer.state_dict(),\n                \"ssl_train_loss\": epoch_loss\n            },\n            os.path.join(save_dir, \"simclr_aptos_best.pth\")\n        )\n\n        print(\"Saved best SSL checkpoint.\")\n\n    print(\"-\" * 60)\n\nprint(\"\\nSSL training summary:\")\ndisplay(__import__(\"pandas\").DataFrame(ssl_history))\nprint(\"Best training loss:\", best_ssl_loss)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:35:08.423157Z","iopub.execute_input":"2026-10-19T12:35:08.423568Z","iopub.status.idle":"2026-10-19T12:54:50.976952Z","shell.execute_reply.started":"2026-10-19T12:35:08.423544Z","shell.execute_reply":"2026-10-19T12:54:50.975966Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small\n\nsave_dir = \"/kaggle/working/smartretina\"\nssl_checkpoint_path = os.path.join(\n    save_dir, \"simclr_aptos_best.pth\"\n)\n\n# Load the best SSL checkpoint.\nssl_checkpoint = torch.load(\n    ssl_checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\n# Rebuild MobileNetV3-Small encoder.\nssl_encoder = mobilenet_v3_small(weights=None)\nssl_encoder.classifier = nn.Identity()\nssl_encoder.load_state_dict(\n    ssl_checkpoint[\"encoder_state_dict\"]\n)\n\n# Add a five-grade classification head.\nssl_classifier = mobilenet_v3_small(weights=None)\nssl_classifier.classifier[3] = nn.Linear(\n    ssl_classifier.classifier[3].in_features, 5\n)\n\n# Transfer the SSL-trained feature extractor.\nssl_classifier.features.load_state_dict(\n    ssl_encoder.features.state_dict()\n)\n\nssl_classifier = ssl_classifier.to(device)\n\nprint(\"Loaded SSL checkpoint epoch:\", ssl_checkpoint[\"epoch\"])\nprint(\"SSL training loss:\", round(ssl_checkpoint[\"ssl_train_loss\"], 4))\nprint(\"Classifier output classes:\", ssl_classifier.classifier[3].out_features)\nprint(\"Model device:\", next(ssl_classifier.parameters()).device)\nprint(\n    \"Trainable parameters:\",\n    f\"{sum(p.numel() for p in ssl_classifier.parameters() if p.requires_grad):,}\"\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:54:50.978564Z","iopub.execute_input":"2026-10-19T12:54:50.978978Z","iopub.status.idle":"2026-10-19T12:54:51.530853Z","shell.execute_reply.started":"2026-10-19T12:54:50.978948Z","shell.execute_reply":"2026-10-19T12:54:51.530063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom pathlib import Path\n\nAPTOS_ROOT = Path(\n    \"/kaggle/input/competitions/aptos2019-blindness-detection\"\n)\n\nprint(\"APTOS_ROOT type:\", type(APTOS_ROOT))\nprint(\"Image directory exists:\", (APTOS_ROOT / \"train_images\").exists())\n\n# Test the existing dataset again.\nsample = train_loader_pilot.dataset[0]\n\nprint(\"\\nSample loaded successfully!\")\nprint(\"Sample type:\", type(sample))\n\nif isinstance(sample, dict):\n    print(\"Keys:\", sample.keys())\n    print(\"Image shape:\", sample[\"image\"].shape)\n    print(\"Label:\", sample[\"label\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:54:51.532028Z","iopub.execute_input":"2026-10-19T12:54:51.532343Z","iopub.status.idle":"2026-10-19T12:54:51.582679Z","shell.execute_reply.started":"2026-10-19T12:54:51.532321Z","shell.execute_reply":"2026-10-19T12:54:51.582059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Quick pre-training check\nbatch = next(iter(train_loader_pilot))\n\nprint(\"Images:\", batch[\"image\"].shape)\nprint(\"Labels:\", batch[\"label\"].shape)\nprint(\"Label values:\", batch[\"label\"].unique().tolist())\n\nval_batch = next(iter(val_loader_pilot))\nprint(\"Validation images:\", val_batch[\"image\"].shape)\n\nprint(\"\\nReady to fine-tune the SSL classifier.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:54:51.583718Z","iopub.execute_input":"2026-10-19T12:54:51.584051Z","iopub.status.idle":"2026-10-19T12:55:08.911064Z","shell.execute_reply.started":"2026-10-19T12:54:51.58403Z","shell.execute_reply":"2026-10-19T12:55:08.910036Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nimport os\nimport time\nimport torch\nimport torch.nn as nn\nimport pandas as pd\nfrom torch.optim import AdamW\n\nsave_dir = \"/kaggle/working/smartretina\"\nos.makedirs(save_dir, exist_ok=True)\n\n# Loss function\ncriterion_ssl = nn.CrossEntropyLoss()\n\n# Lower learning rate for pretrained features,\n# higher learning rate for the new classification head.\noptimizer_ssl = AdamW([\n    {\n        \"params\": ssl_classifier.features.parameters(),\n        \"lr\": 1e-5\n    },\n    {\n        \"params\": ssl_classifier.classifier.parameters(),\n        \"lr\": 1e-4\n    }\n], weight_decay=1e-4)\n\nSSL_FINETUNE_EPOCHS = 3\nssl_finetune_history = []\nbest_ssl_qwk = -1.0\n\nfor epoch in range(1, SSL_FINETUNE_EPOCHS + 1):\n    ssl_classifier.train()\n    running_loss = 0.0\n    total_samples = 0\n    start_time = time.time()\n\n    for batch_idx, batch in enumerate(train_loader_pilot, start=1):\n        images = batch[\"image\"].to(device, non_blocking=True)\n        labels = batch[\"label\"].to(device, non_blocking=True).long()\n\n        optimizer_ssl.zero_grad(set_to_none=True)\n\n        logits = ssl_classifier(images)\n        loss = criterion_ssl(logits, labels)\n\n        loss.backward()\n        optimizer_ssl.step()\n\n        batch_size = images.size(0)\n        running_loss += loss.item() * batch_size\n        total_samples += batch_size\n\n        if batch_idx % 20 == 0 or batch_idx == len(train_loader_pilot):\n            print(\n                f\"Epoch {epoch}/{SSL_FINETUNE_EPOCHS} | \"\n                f\"Batch {batch_idx}/{len(train_loader_pilot)} | \"\n                f\"Loss: {loss.item():.4f}\"\n            )\n\n    train_loss = running_loss / total_samples\n\n    # Evaluate on the same APTOS validation split.\n    val_metrics = evaluate_model(\n        ssl_classifier,\n        val_loader_pilot\n    )\n\n    row = {\n        \"epoch\": epoch,\n        \"train_loss\": train_loss,\n        **val_metrics,\n        \"seconds\": time.time() - start_time\n    }\n    ssl_finetune_history.append(row)\n\n    print(\n        f\"\\nEpoch {epoch} | \"\n        f\"Train loss: {train_loss:.4f} | \"\n        f\"Val accuracy: {val_metrics['accuracy']:.4f} | \"\n        f\"Macro-F1: {val_metrics['macro_f1']:.4f} | \"\n        f\"QWK: {val_metrics['qwk']:.4f}\"\n    )\n\n    if val_metrics[\"qwk\"] > best_ssl_qwk:\n        best_ssl_qwk = val_metrics[\"qwk\"]\n\n        torch.save({\n            \"epoch\": epoch,\n            \"model_state_dict\": ssl_classifier.state_dict(),\n            \"optimizer_state_dict\": optimizer_ssl.state_dict(),\n            \"metrics\": val_metrics\n        }, os.path.join(\n            save_dir,\n            \"simclr_aptos_finetuned_best.pth\"\n        ))\n\n        print(\"Saved best fine-tuned SSL classifier.\")\n\n    print(\"-\" * 60)\n\nprint(\"\\nFine-tuning summary:\")\ndisplay(pd.DataFrame(ssl_finetune_history))\nprint(\"Best SSL validation QWK:\", best_ssl_qwk)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T12:55:08.91267Z","iopub.execute_input":"2026-10-19T12:55:08.913006Z","iopub.status.idle":"2026-10-19T13:06:56.042845Z","shell.execute_reply.started":"2026-10-19T12:55:08.91298Z","shell.execute_reply":"2026-10-19T13:06:56.041862Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.metrics import classification_report, confusion_matrix\n\n# Load the best fine-tuned SSL checkpoint\ncheckpoint_path = (\n    \"/kaggle/working/smartretina/\"\n    \"simclr_aptos_finetuned_best.pth\"\n)\n\ncheckpoint = torch.load(\n    checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\nssl_classifier.load_state_dict(checkpoint[\"model_state_dict\"])\nssl_classifier = ssl_classifier.to(device)\nssl_classifier.eval()\n\ny_true_ssl = []\ny_pred_ssl = []\n\nwith torch.no_grad():\n    for batch in val_loader_pilot:\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"]\n\n        logits = ssl_classifier(images)\n        predictions = logits.argmax(dim=1).cpu().numpy()\n\n        y_true_ssl.extend(labels.cpu().numpy())\n        y_pred_ssl.extend(predictions)\n\ngrade_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\nprint(\"SSL Classification Report:\\n\")\nprint(\n    classification_report(\n        y_true_ssl,\n        y_pred_ssl,\n        labels=[0, 1, 2, 3, 4],\n        target_names=grade_names,\n        digits=4,\n        zero_division=0\n    )\n)\n\ncm = confusion_matrix(\n    y_true_ssl,\n    y_pred_ssl,\n    labels=[0, 1, 2, 3, 4]\n)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(\n    cm,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    xticklabels=grade_names,\n    yticklabels=grade_names\n)\nplt.xlabel(\"Predicted Grade\")\nplt.ylabel(\"Actual Grade\")\nplt.title(\"SimCLR Fine-tuned Model — Confusion Matrix\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:07:59.78133Z","iopub.execute_input":"2026-10-19T13:07:59.781786Z","iopub.status.idle":"2026-10-19T13:08:48.830816Z","shell.execute_reply.started":"2026-10-19T13:07:59.781754Z","shell.execute_reply":"2026-10-19T13:08:48.82979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Training manifest columns:\")\nprint(aptos_train.columns.tolist())\n\nprint(\"\\nTraining manifest sample:\")\ndisplay(aptos_train.head(3))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:10:39.066271Z","iopub.execute_input":"2026-10-19T13:10:39.067012Z","iopub.status.idle":"2026-10-19T13:10:39.079897Z","shell.execute_reply.started":"2026-10-19T13:10:39.066973Z","shell.execute_reply":"2026-10-19T13:10:39.079144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nfrom sklearn.utils.class_weight import compute_class_weight\n\n# Get labels from the APTOS training split only\ntrain_labels = aptos_train[\"label\"].astype(int).to_numpy()\n\nclasses = np.array([0, 1, 2, 3, 4])\n\n# Calculate balanced class weights\nweights = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=classes,\n    y=train_labels\n)\n\n# Use square-root weights for a more moderate correction\nweights = np.sqrt(weights)\n\n# Normalize so the average weight is 1\nweights = weights / weights.mean()\n\nclass_weights = torch.tensor(\n    weights,\n    dtype=torch.float32,\n    device=device\n)\n\nprint(\"Class distribution:\")\nfor grade, count in zip(*np.unique(train_labels, return_counts=True)):\n    print(f\"Grade {grade}: {count} images\")\n\nprint(\"\\nClass weights:\")\nfor grade, weight in enumerate(weights):\n    print(f\"Grade {grade}: {weight:.4f}\")\n\nprint(\"\\nWeight tensor:\", class_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:11:27.044741Z","iopub.execute_input":"2026-10-19T13:11:27.04538Z","iopub.status.idle":"2026-10-19T13:11:27.255641Z","shell.execute_reply.started":"2026-10-19T13:11:27.045352Z","shell.execute_reply":"2026-10-19T13:11:27.254985Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torch.optim import AdamW\nfrom torchvision.models import mobilenet_v3_small\n\n# Load the original SimCLR pretrained checkpoint\nssl_checkpoint_path = (\n    \"/kaggle/working/smartretina/simclr_aptos_best.pth\"\n)\n\nssl_checkpoint = torch.load(\n    ssl_checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\n# Reconstruct the pretrained encoder\nweighted_encoder = mobilenet_v3_small(weights=None)\nweighted_encoder.classifier = nn.Identity()\nweighted_encoder.load_state_dict(\n    ssl_checkpoint[\"encoder_state_dict\"]\n)\n\n# Create a separate five-class classifier\nweighted_classifier = mobilenet_v3_small(weights=None)\nweighted_classifier.classifier[3] = nn.Linear(\n    weighted_classifier.classifier[3].in_features,\n    5\n)\n\n# Transfer the SimCLR-learned feature extractor\nweighted_classifier.features.load_state_dict(\n    weighted_encoder.features.state_dict()\n)\n\nweighted_classifier = weighted_classifier.to(device)\n\n# Weighted cross-entropy loss\ncriterion_weighted = nn.CrossEntropyLoss(\n    weight=class_weights\n)\n\n# Lower learning rate for the pretrained feature extractor\noptimizer_weighted = AdamW(\n    [\n        {\n            \"params\": weighted_classifier.features.parameters(),\n            \"lr\": 1e-5\n        },\n        {\n            \"params\": weighted_classifier.classifier.parameters(),\n            \"lr\": 1e-4\n        }\n    ],\n    weight_decay=1e-4\n)\n\nprint(\"Class-weighted SimCLR model prepared.\")\nprint(\"Trainable parameters:\",\n      sum(p.numel() for p in weighted_classifier.parameters()\n          if p.requires_grad))\nprint(\"Loss function:\", criterion_weighted)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:13:15.494693Z","iopub.execute_input":"2026-10-19T13:13:15.495146Z","iopub.status.idle":"2026-10-19T13:13:15.721501Z","shell.execute_reply.started":"2026-10-19T13:13:15.49512Z","shell.execute_reply":"2026-10-19T13:13:15.720613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nimport copy\nimport pandas as pd\nfrom pathlib import Path\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\nSAVE_DIR = Path(\"/kaggle/working/smartretina\")\nSAVE_DIR.mkdir(parents=True, exist_ok=True)\n\nweighted_history = []\nbest_weighted_qwk = -float(\"inf\")\nweighted_checkpoint_path = SAVE_DIR / \"simclr_classweighted_best.pth\"\n\nfor epoch in range(1, 4):\n    start_time = time.time()\n\n    weighted_classifier.train()\n    running_loss = 0.0\n    total_samples = 0\n\n    for batch_idx, batch in enumerate(train_loader_pilot, start=1):\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"].to(device).long()\n\n        optimizer_weighted.zero_grad(set_to_none=True)\n\n        logits = weighted_classifier(images)\n        loss = criterion_weighted(logits, labels)\n\n        loss.backward()\n        optimizer_weighted.step()\n\n        running_loss += loss.item() * labels.size(0)\n        total_samples += labels.size(0)\n\n        if batch_idx % 20 == 0 or batch_idx == len(train_loader_pilot):\n            print(\n                f\"Epoch {epoch}/3 | \"\n                f\"Batch {batch_idx}/{len(train_loader_pilot)} | \"\n                f\"Loss: {loss.item():.4f}\"\n            )\n\n    train_loss = running_loss / total_samples\n\n    # Evaluate without updating model weights\n    weighted_classifier.eval()\n    y_true, y_pred = [], []\n    val_loss_sum = 0.0\n    val_samples = 0\n\n    with torch.no_grad():\n        for batch in val_loader_pilot:\n            images = batch[\"image\"].to(device)\n            labels = batch[\"label\"].to(device).long()\n\n            logits = weighted_classifier(images)\n            loss = criterion_weighted(logits, labels)\n\n            val_loss_sum += loss.item() * labels.size(0)\n            val_samples += labels.size(0)\n\n            y_true.extend(labels.cpu().numpy())\n            y_pred.extend(logits.argmax(dim=1).cpu().numpy())\n\n    val_loss = val_loss_sum / val_samples\n    accuracy = accuracy_score(y_true, y_pred)\n    macro_f1 = f1_score(\n        y_true, y_pred, labels=[0, 1, 2, 3, 4],\n        average=\"macro\", zero_division=0\n    )\n    qwk = cohen_kappa_score(y_true, y_pred, weights=\"quadratic\")\n    elapsed = time.time() - start_time\n\n    weighted_history.append({\n        \"epoch\": epoch,\n        \"train_loss\": train_loss,\n        \"val_loss\": val_loss,\n        \"accuracy\": accuracy,\n        \"macro_f1\": macro_f1,\n        \"qwk\": qwk,\n        \"seconds\": elapsed\n    })\n\n    print(\n        f\"\\nEpoch {epoch} | \"\n        f\"Train loss: {train_loss:.4f} | \"\n        f\"Val loss: {val_loss:.4f} | \"\n        f\"Accuracy: {accuracy:.4f} | \"\n        f\"Macro-F1: {macro_f1:.4f} | \"\n        f\"QWK: {qwk:.4f}\\n\"\n    )\n\n    # Save the best model based on validation QWK\n    if qwk > best_weighted_qwk:\n        best_weighted_qwk = qwk\n\n        torch.save({\n            \"epoch\": epoch,\n            \"model_state_dict\": weighted_classifier.state_dict(),\n            \"optimizer_state_dict\": optimizer_weighted.state_dict(),\n            \"metrics\": weighted_history[-1],\n            \"class_weights\": class_weights.detach().cpu()\n        }, weighted_checkpoint_path)\n\n        print(f\"Saved best checkpoint: {weighted_checkpoint_path}\")\n\nprint(\"\\nClass-weighted fine-tuning summary:\")\ndisplay(pd.DataFrame(weighted_history))\n\nprint(f\"Best validation QWK: {best_weighted_qwk:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:13:41.368634Z","iopub.execute_input":"2026-10-19T13:13:41.369247Z","iopub.status.idle":"2026-10-19T13:25:41.433037Z","shell.execute_reply.started":"2026-10-19T13:13:41.369221Z","shell.execute_reply":"2026-10-19T13:25:41.431996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.metrics import classification_report, confusion_matrix\n\ncheckpoint_path = (\n    \"/kaggle/working/smartretina/\"\n    \"simclr_classweighted_best.pth\"\n)\n\ncheckpoint = torch.load(\n    checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\nweighted_classifier.load_state_dict(\n    checkpoint[\"model_state_dict\"]\n)\nweighted_classifier.eval()\n\ny_true_weighted = []\ny_pred_weighted = []\n\nwith torch.no_grad():\n    for batch in val_loader_pilot:\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"].to(device).long()\n\n        logits = weighted_classifier(images)\n        predictions = logits.argmax(dim=1)\n\n        y_true_weighted.extend(labels.cpu().numpy())\n        y_pred_weighted.extend(predictions.cpu().numpy())\n\ngrade_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\nprint(\"Class-weighted SimCLR Classification Report:\\n\")\nprint(classification_report(\n    y_true_weighted,\n    y_pred_weighted,\n    labels=[0, 1, 2, 3, 4],\n    target_names=grade_names,\n    digits=4,\n    zero_division=0\n))\n\ncm_weighted = confusion_matrix(\n    y_true_weighted,\n    y_pred_weighted,\n    labels=[0, 1, 2, 3, 4]\n)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(\n    cm_weighted,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    xticklabels=grade_names,\n    yticklabels=grade_names\n)\nplt.xlabel(\"Predicted Grade\")\nplt.ylabel(\"Actual Grade\")\nplt.title(\"Class-Weighted SimCLR — Confusion Matrix\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:26:31.351088Z","iopub.execute_input":"2026-10-19T13:26:31.351859Z","iopub.status.idle":"2026-10-19T13:27:19.643445Z","shell.execute_reply.started":"2026-10-19T13:26:31.351827Z","shell.execute_reply":"2026-10-19T13:27:19.642556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import copy\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nfrom torchvision.models import (\n    mobilenet_v3_small,\n    MobileNet_V3_Small_Weights\n)\n\n# Reproducibility\ntorch.manual_seed(42)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(42)\n\n# Create the online encoder using ImageNet-pretrained weights\nonline_encoder = mobilenet_v3_small(\n    weights=MobileNet_V3_Small_Weights.DEFAULT\n)\nonline_encoder.classifier = nn.Identity()\n\n# MobileNetV3-Small feature dimension\nfeature_dim = 576\nprojection_dim = 128\nhidden_dim = 256\n\n# Projector: encoder features -> representation\nonline_projector = nn.Sequential(\n    nn.Linear(feature_dim, hidden_dim, bias=False),\n    nn.BatchNorm1d(hidden_dim),\n    nn.ReLU(inplace=True),\n    nn.Linear(hidden_dim, projection_dim)\n)\n\n# Predictor: online representation -> prediction\nonline_predictor = nn.Sequential(\n    nn.Linear(projection_dim, hidden_dim, bias=False),\n    nn.BatchNorm1d(hidden_dim),\n    nn.ReLU(inplace=True),\n    nn.Linear(hidden_dim, projection_dim)\n)\n\n# Target network starts as an exact copy of the online network\ntarget_encoder = copy.deepcopy(online_encoder)\ntarget_projector = copy.deepcopy(online_projector)\n\n# Target network is updated by EMA, not gradient descent\nfor module in [target_encoder, target_projector]:\n    module.requires_grad_(False)\n    module.eval()\n\n# Move all components to the selected device\nonline_encoder = online_encoder.to(device)\nonline_projector = online_projector.to(device)\nonline_predictor = online_predictor.to(device)\ntarget_encoder = target_encoder.to(device)\ntarget_projector = target_projector.to(device)\n\n# Optimizer updates only the online network\noptimizer_byol = torch.optim.AdamW(\n    list(online_encoder.parameters())\n    + list(online_projector.parameters())\n    + list(online_predictor.parameters()),\n    lr=3e-4,\n    weight_decay=1e-4\n)\n\nprint(\"BYOL model prepared successfully.\")\nprint(\"Feature dimension:\", feature_dim)\nprint(\"Projection dimension:\", projection_dim)\nprint(\"Target network gradients disabled:\",\n      not any(p.requires_grad for p in target_encoder.parameters()))\nprint(\"Training device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:29:44.373642Z","iopub.execute_input":"2026-10-19T13:29:44.374274Z","iopub.status.idle":"2026-10-19T13:29:44.509685Z","shell.execute_reply.started":"2026-10-19T13:29:44.374246Z","shell.execute_reply":"2026-10-19T13:29:44.508987Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\n\n# BYOL target-network EMA momentum\nBYOL_MOMENTUM = 0.996\n\n\ndef byol_loss_fn(prediction, target):\n    \"\"\"\n    Negative cosine similarity loss.\n    The target representation is detached from gradients.\n    \"\"\"\n    prediction = F.normalize(prediction, dim=1)\n    target = F.normalize(target.detach(), dim=1)\n\n    return 2 - 2 * (prediction * target).sum(dim=1).mean()\n\n\n@torch.no_grad()\ndef update_target_network(\n    online_encoder,\n    online_projector,\n    target_encoder,\n    target_projector,\n    momentum=BYOL_MOMENTUM\n):\n    \"\"\"\n    Exponential moving average update for BYOL target networks.\n    \"\"\"\n    for online_param, target_param in zip(\n        online_encoder.parameters(),\n        target_encoder.parameters()\n    ):\n        target_param.data.mul_(momentum).add_(\n            online_param.data,\n            alpha=1 - momentum\n        )\n\n    for online_param, target_param in zip(\n        online_projector.parameters(),\n        target_projector.parameters()\n    ):\n        target_param.data.mul_(momentum).add_(\n            online_param.data,\n            alpha=1 - momentum\n        )\n\n\ndef byol_forward(view1, view2):\n    \"\"\"\n    Compute symmetric BYOL loss for two augmented image views.\n    \"\"\"\n    # Online predictions\n    online_z1 = online_projector(online_encoder(view1))\n    online_z2 = online_projector(online_encoder(view2))\n\n    pred1 = online_predictor(online_z1)\n    pred2 = online_predictor(online_z2)\n\n    # Target representations; no gradient flows through them\n    with torch.no_grad():\n        target_z1 = target_projector(target_encoder(view1))\n        target_z2 = target_projector(target_encoder(view2))\n\n    loss1 = byol_loss_fn(pred1, target_z2)\n    loss2 = byol_loss_fn(pred2, target_z1)\n\n    return (loss1 + loss2) / 2\n\n\nprint(\"BYOL loss and EMA update functions defined.\")\nprint(\"EMA momentum:\", BYOL_MOMENTUM)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:31:00.720301Z","iopub.execute_input":"2026-10-19T13:31:00.721294Z","iopub.status.idle":"2026-10-19T13:31:00.730405Z","shell.execute_reply.started":"2026-10-19T13:31:00.721265Z","shell.execute_reply":"2026-10-19T13:31:00.729526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Inspect one batch from the existing SSL DataLoader\nbatch = next(iter(ssl_loader))\n\nprint(\"Batch type:\", type(batch))\n\nif isinstance(batch, (list, tuple)):\n    print(\"Number of views:\", len(batch))\n\n    for i, view in enumerate(batch):\n        print(\n            f\"View {i + 1}: \"\n            f\"shape={view.shape}, \"\n            f\"dtype={view.dtype}\"\n        )\nelse:\n    print(\"Batch structure:\", batch.keys() if isinstance(batch, dict) else \"Unknown\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:32:37.213133Z","iopub.execute_input":"2026-10-19T13:32:37.213593Z","iopub.status.idle":"2026-10-19T13:32:46.596499Z","shell.execute_reply.started":"2026-10-19T13:32:37.213568Z","shell.execute_reply":"2026-10-19T13:32:46.595418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nfrom pathlib import Path\n\nBYOL_SAVE_DIR = Path(\"/kaggle/working/smartretina\")\nBYOL_SAVE_DIR.mkdir(parents=True, exist_ok=True)\n\nbyol_history = []\nbest_byol_loss = float(\"inf\")\n\nbyol_checkpoint_path = BYOL_SAVE_DIR / \"byol_aptos_best.pth\"\nbyol_latest_path = BYOL_SAVE_DIR / \"byol_aptos_latest.pth\"\n\nfor epoch in range(1, 6):\n    start_time = time.time()\n\n    online_encoder.train()\n    online_projector.train()\n    online_predictor.train()\n\n    target_encoder.eval()\n    target_projector.eval()\n\n    running_loss = 0.0\n    num_batches = 0\n\n    for batch_idx, (view1, view2) in enumerate(ssl_loader, start=1):\n        view1 = view1.to(device, non_blocking=True)\n        view2 = view2.to(device, non_blocking=True)\n\n        optimizer_byol.zero_grad(set_to_none=True)\n\n        loss = byol_forward(view1, view2)\n        loss.backward()\n        optimizer_byol.step()\n\n        # Update target networks after the online update\n        update_target_network(\n            online_encoder,\n            online_projector,\n            target_encoder,\n            target_projector,\n            momentum=BYOL_MOMENTUM\n        )\n\n        running_loss += loss.item()\n        num_batches += 1\n\n        if batch_idx % 20 == 0 or batch_idx == len(ssl_loader):\n            print(\n                f\"Epoch {epoch}/5 | \"\n                f\"Batch {batch_idx}/{len(ssl_loader)} | \"\n                f\"Loss: {loss.item():.4f}\"\n            )\n\n    epoch_loss = running_loss / num_batches\n    elapsed = time.time() - start_time\n\n    byol_history.append({\n        \"epoch\": epoch,\n        \"ssl_loss\": epoch_loss,\n        \"seconds\": elapsed\n    })\n\n    checkpoint_data = {\n        \"epoch\": epoch,\n        \"online_encoder_state_dict\": online_encoder.state_dict(),\n        \"online_projector_state_dict\": online_projector.state_dict(),\n        \"online_predictor_state_dict\": online_predictor.state_dict(),\n        \"target_encoder_state_dict\": target_encoder.state_dict(),\n        \"target_projector_state_dict\": target_projector.state_dict(),\n        \"optimizer_state_dict\": optimizer_byol.state_dict(),\n        \"ssl_loss\": epoch_loss\n    }\n\n    # Always preserve the most recent completed epoch\n    torch.save(checkpoint_data, byol_latest_path)\n\n    # Preserve the lowest observed training SSL loss\n    if epoch_loss < best_byol_loss:\n        best_byol_loss = epoch_loss\n        torch.save(checkpoint_data, byol_checkpoint_path)\n        print(f\"Saved best BYOL checkpoint: {byol_checkpoint_path}\")\n\n    print(\n        f\"\\nEpoch {epoch} | \"\n        f\"SSL loss: {epoch_loss:.4f} | \"\n        f\"Time: {elapsed:.1f}s\\n\"\n    )\n\nprint(\"BYOL pretraining summary:\")\ndisplay(pd.DataFrame(byol_history))\n\nprint(\"Best training SSL loss:\", round(best_byol_loss, 4))\nprint(\"Best checkpoint:\", byol_checkpoint_path)\nprint(\"Latest checkpoint:\", byol_latest_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:33:45.453107Z","iopub.execute_input":"2026-10-19T13:33:45.453583Z","iopub.status.idle":"2026-10-19T13:53:42.963594Z","shell.execute_reply.started":"2026-10-19T13:33:45.453554Z","shell.execute_reply":"2026-10-19T13:53:42.962732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small\n\nBYOL_CHECKPOINT_PATH = (\n    \"/kaggle/working/smartretina/byol_aptos_best.pth\"\n)\n\nbyol_checkpoint = torch.load(\n    BYOL_CHECKPOINT_PATH,\n    map_location=device,\n    weights_only=False\n)\n\n# Reconstruct the BYOL online encoder\nbyol_encoder = mobilenet_v3_small(weights=None)\nbyol_encoder.classifier = nn.Identity()\n\nbyol_encoder.load_state_dict(\n    byol_checkpoint[\"online_encoder_state_dict\"]\n)\n\n# Create a five-class downstream classifier\nbyol_classifier = mobilenet_v3_small(weights=None)\nbyol_classifier.classifier[3] = nn.Linear(\n    byol_classifier.classifier[3].in_features,\n    5\n)\n\n# Transfer the BYOL-learned feature extractor\nbyol_classifier.features.load_state_dict(\n    byol_encoder.features.state_dict()\n)\n\nbyol_classifier = byol_classifier.to(device)\n\n# Fine-tuning optimizer\noptimizer_byol_ft = torch.optim.AdamW(\n    [\n        {\n            \"params\": byol_classifier.features.parameters(),\n            \"lr\": 1e-5\n        },\n        {\n            \"params\": byol_classifier.classifier.parameters(),\n            \"lr\": 1e-4\n        }\n    ],\n    weight_decay=1e-4\n)\n\ncriterion_byol_ft = nn.CrossEntropyLoss()\n\nprint(\"BYOL classifier prepared successfully.\")\nprint(\"Loaded BYOL pretraining epoch:\", byol_checkpoint[\"epoch\"])\nprint(\"Trainable parameters:\", sum(\n    p.numel() for p in byol_classifier.parameters()\n    if p.requires_grad\n))\nprint(\"Number of output classes:\", byol_classifier.classifier[3].out_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:54:23.367897Z","iopub.execute_input":"2026-10-19T13:54:23.368758Z","iopub.status.idle":"2026-10-19T13:54:23.638136Z","shell.execute_reply.started":"2026-10-19T13:54:23.368728Z","shell.execute_reply":"2026-10-19T13:54:23.637301Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nimport pandas as pd\nfrom pathlib import Path\nfrom sklearn.metrics import accuracy_score, f1_score, cohen_kappa_score\n\nBYOL_SAVE_DIR = Path(\"/kaggle/working/smartretina\")\nBYOL_SAVE_DIR.mkdir(parents=True, exist_ok=True)\n\nbyol_ft_history = []\nbest_byol_qwk = -float(\"inf\")\n\nbyol_ft_best_path = BYOL_SAVE_DIR / \"byol_aptos_finetuned_best.pth\"\nbyol_ft_latest_path = BYOL_SAVE_DIR / \"byol_aptos_finetuned_latest.pth\"\n\nfor epoch in range(1, 6):\n    start_time = time.time()\n\n    byol_classifier.train()\n    running_loss = 0.0\n    total_samples = 0\n\n    for batch_idx, batch in enumerate(train_loader_pilot, start=1):\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"].to(device).long()\n\n        optimizer_byol_ft.zero_grad(set_to_none=True)\n\n        logits = byol_classifier(images)\n        loss = criterion_byol_ft(logits, labels)\n\n        loss.backward()\n        optimizer_byol_ft.step()\n\n        running_loss += loss.item() * labels.size(0)\n        total_samples += labels.size(0)\n\n        if batch_idx % 20 == 0 or batch_idx == len(train_loader_pilot):\n            print(\n                f\"Epoch {epoch}/5 | \"\n                f\"Batch {batch_idx}/{len(train_loader_pilot)} | \"\n                f\"Loss: {loss.item():.4f}\"\n            )\n\n    train_loss = running_loss / total_samples\n\n    # Validation\n    byol_classifier.eval()\n    y_true, y_pred = [], []\n    val_loss_sum = 0.0\n    val_samples = 0\n\n    with torch.no_grad():\n        for batch in val_loader_pilot:\n            images = batch[\"image\"].to(device)\n            labels = batch[\"label\"].to(device).long()\n\n            logits = byol_classifier(images)\n            loss = criterion_byol_ft(logits, labels)\n\n            val_loss_sum += loss.item() * labels.size(0)\n            val_samples += labels.size(0)\n\n            y_true.extend(labels.cpu().numpy())\n            y_pred.extend(logits.argmax(dim=1).cpu().numpy())\n\n    val_loss = val_loss_sum / val_samples\n    accuracy = accuracy_score(y_true, y_pred)\n    macro_f1 = f1_score(\n        y_true, y_pred,\n        labels=[0, 1, 2, 3, 4],\n        average=\"macro\",\n        zero_division=0\n    )\n    qwk = cohen_kappa_score(\n        y_true, y_pred, weights=\"quadratic\"\n    )\n\n    elapsed = time.time() - start_time\n\n    metrics = {\n        \"epoch\": epoch,\n        \"train_loss\": train_loss,\n        \"val_loss\": val_loss,\n        \"accuracy\": accuracy,\n        \"macro_f1\": macro_f1,\n        \"qwk\": qwk,\n        \"seconds\": elapsed\n    }\n    byol_ft_history.append(metrics)\n\n    print(\n        f\"\\nEpoch {epoch} | \"\n        f\"Train loss: {train_loss:.4f} | \"\n        f\"Val loss: {val_loss:.4f} | \"\n        f\"Accuracy: {accuracy:.4f} | \"\n        f\"Macro-F1: {macro_f1:.4f} | \"\n        f\"QWK: {qwk:.4f}\\n\"\n    )\n\n    checkpoint_data = {\n        \"epoch\": epoch,\n        \"model_state_dict\": byol_classifier.state_dict(),\n        \"optimizer_state_dict\": optimizer_byol_ft.state_dict(),\n        \"metrics\": metrics\n    }\n\n    # Preserve latest completed epoch\n    torch.save(checkpoint_data, byol_ft_latest_path)\n\n    # Save best checkpoint by validation QWK\n    if qwk > best_byol_qwk:\n        best_byol_qwk = qwk\n        torch.save(checkpoint_data, byol_ft_best_path)\n        print(f\"Saved best BYOL classifier: {byol_ft_best_path}\")\n\nprint(\"BYOL fine-tuning summary:\")\ndisplay(pd.DataFrame(byol_ft_history))\n\nprint(f\"Best validation QWK: {best_byol_qwk:.4f}\")\nprint(f\"Best checkpoint: {byol_ft_best_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T13:54:55.553299Z","iopub.execute_input":"2026-10-19T13:54:55.553737Z","iopub.status.idle":"2026-10-19T14:15:10.579355Z","shell.execute_reply.started":"2026-10-19T13:54:55.553711Z","shell.execute_reply":"2026-10-19T14:15:10.578406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.metrics import classification_report, confusion_matrix\n\ncheckpoint_path = (\n    \"/kaggle/working/smartretina/\"\n    \"byol_aptos_finetuned_best.pth\"\n)\n\ncheckpoint = torch.load(\n    checkpoint_path,\n    map_location=device,\n    weights_only=False\n)\n\nbyol_classifier.load_state_dict(checkpoint[\"model_state_dict\"])\nbyol_classifier.eval()\n\ny_true_byol = []\ny_pred_byol = []\n\nwith torch.no_grad():\n    for batch in val_loader_pilot:\n        images = batch[\"image\"].to(device)\n        labels = batch[\"label\"].to(device).long()\n\n        logits = byol_classifier(images)\n        predictions = logits.argmax(dim=1)\n\n        y_true_byol.extend(labels.cpu().numpy())\n        y_pred_byol.extend(predictions.cpu().numpy())\n\ngrade_names = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\nprint(\"BYOL Classification Report:\\n\")\nprint(classification_report(\n    y_true_byol,\n    y_pred_byol,\n    labels=[0, 1, 2, 3, 4],\n    target_names=grade_names,\n    digits=4,\n    zero_division=0\n))\n\ncm_byol = confusion_matrix(\n    y_true_byol,\n    y_pred_byol,\n    labels=[0, 1, 2, 3, 4]\n)\n\nplt.figure(figsize=(8, 6))\nsns.heatmap(\n    cm_byol,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"Blues\",\n    xticklabels=grade_names,\n    yticklabels=grade_names\n)\nplt.xlabel(\"Predicted Grade\")\nplt.ylabel(\"Actual Grade\")\nplt.title(\"BYOL Fine-tuned Model — Confusion Matrix\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:15:52.66531Z","iopub.execute_input":"2026-10-19T14:15:52.665972Z","iopub.status.idle":"2026-10-19T14:16:43.153343Z","shell.execute_reply.started":"2026-10-19T14:15:52.665943Z","shell.execute_reply":"2026-10-19T14:16:43.152358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nSAVE_DIR = Path(\"/kaggle/working/smartretina\")\n\nfor filename in [\n    \"mobilenetv3_full_aptos_best.pth\",\n    \"simclr_classweighted_best.pth\",\n    \"byol_aptos_finetuned_best.pth\",\n]:\n    path = SAVE_DIR / filename\n    print(\n        f\"{filename}: \"\n        f\"{'FOUND' if path.exists() else 'MISSING'}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:20:03.085214Z","iopub.execute_input":"2026-10-19T14:20:03.08555Z","iopub.status.idle":"2026-10-19T14:20:03.091842Z","shell.execute_reply.started":"2026-10-19T14:20:03.085527Z","shell.execute_reply":"2026-10-19T14:20:03.090986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small\n\nBASELINE_CHECKPOINT_PATH = (\n    \"/kaggle/working/smartretina/\"\n    \"mobilenetv3_full_aptos_best.pth\"\n)\n\nbaseline_checkpoint = torch.load(\n    BASELINE_CHECKPOINT_PATH,\n    map_location=device,\n    weights_only=False\n)\n\nbaseline_model = mobilenet_v3_small(weights=None)\nbaseline_model.classifier[3] = nn.Linear(\n    baseline_model.classifier[3].in_features,\n    5\n)\n\n# Support checkpoints saved either as a state dict or a training dictionary\nif \"model_state_dict\" in baseline_checkpoint:\n    baseline_state = baseline_checkpoint[\"model_state_dict\"]\nelse:\n    baseline_state = baseline_checkpoint\n\nbaseline_model.load_state_dict(baseline_state)\nbaseline_model = baseline_model.to(device)\nbaseline_model.eval()\n\nprint(\"Supervised baseline loaded successfully.\")\nprint(\"Device:\", device)\nprint(\"Output classes:\", baseline_model.classifier[3].out_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:20:53.207651Z","iopub.execute_input":"2026-10-19T14:20:53.207998Z","iopub.status.idle":"2026-10-19T14:20:53.410587Z","shell.execute_reply.started":"2026-10-19T14:20:53.20797Z","shell.execute_reply":"2026-10-19T14:20:53.409898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn.functional as F\n\nclass GradCAM:\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.activations = None\n        self.gradients = None\n\n        self.forward_handle = target_layer.register_forward_hook(\n            self._save_activations\n        )\n\n    def _save_activations(self, module, inputs, output):\n        self.activations = output\n        output.register_hook(self._save_gradients)\n\n    def _save_gradients(self, gradients):\n        self.gradients = gradients\n\n    def generate(self, input_tensor, target_class=None):\n        self.model.eval()\n        self.model.zero_grad(set_to_none=True)\n        self.activations = None\n        self.gradients = None\n\n        input_tensor = input_tensor.to(device)\n\n        logits = self.model(input_tensor)\n        predicted_class = int(logits.argmax(dim=1).item())\n\n        if target_class is None:\n            target_class = predicted_class\n\n        score = logits[:, target_class].sum()\n        score.backward()\n\n        if self.activations is None or self.gradients is None:\n            raise RuntimeError(\"Grad-CAM could not capture activations/gradients.\")\n\n        weights = self.gradients.mean(dim=(2, 3), keepdim=True)\n\n        cam = (weights * self.activations).sum(dim=1, keepdim=True)\n        cam = F.relu(cam)\n\n        cam = F.interpolate(\n            cam,\n            size=input_tensor.shape[-2:],\n            mode=\"bilinear\",\n            align_corners=False\n        )\n\n        cam = cam[0, 0].detach().cpu().numpy()\n        cam_min, cam_max = cam.min(), cam.max()\n\n        if cam_max > cam_min:\n            cam = (cam - cam_min) / (cam_max - cam_min)\n        else:\n            cam = np.zeros_like(cam)\n\n        return cam, predicted_class\n\n    def close(self):\n        self.forward_handle.remove()\n\n\n# MobileNetV3-Small's final convolutional feature block\ngradcam = GradCAM(\n    model=baseline_model,\n    target_layer=baseline_model.features[-1]\n)\n\nprint(\"Grad-CAM initialized successfully.\")\nprint(\"Target layer:\", baseline_model.features[-1])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:22:53.205666Z","iopub.execute_input":"2026-10-19T14:22:53.206069Z","iopub.status.idle":"2026-10-19T14:22:53.223583Z","shell.execute_reply.started":"2026-10-19T14:22:53.206043Z","shell.execute_reply":"2026-10-19T14:22:53.222769Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ImageNet normalization values used to prepare the images\nIMAGENET_MEAN = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)\nIMAGENET_STD = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)\n\nCLASS_NAMES = [\n    \"No DR\",\n    \"Mild\",\n    \"Moderate\",\n    \"Severe\",\n    \"Proliferative DR\"\n]\n\n# Get one validation sample\nsample_idx = 0\nsample = val_loader_pilot.dataset[sample_idx]\n\nimage_tensor = sample[\"image\"].unsqueeze(0).to(device)\ntrue_label = int(sample[\"label\"])\nimage_id = sample.get(\"image_id\", f\"Validation sample {sample_idx}\")\n\n# Generate Grad-CAM for the model's predicted class\ncam, predicted_class = gradcam.generate(image_tensor)\n\n# Convert normalized image back to displayable RGB\nimage = sample[\"image\"].cpu() * IMAGENET_STD + IMAGENET_MEAN\nimage = image.clamp(0, 1).permute(1, 2, 0).numpy()\n\n# Plot original image, heatmap, and overlay\nplt.figure(figsize=(15, 5))\n\nplt.subplot(1, 3, 1)\nplt.imshow(image)\nplt.title(f\"Original Retina\\nTrue: {CLASS_NAMES[true_label]}\")\nplt.axis(\"off\")\n\nplt.subplot(1, 3, 2)\nplt.imshow(cam, cmap=\"jet\", vmin=0, vmax=1)\nplt.title(\"Grad-CAM Heatmap\")\nplt.axis(\"off\")\n\nplt.subplot(1, 3, 3)\nplt.imshow(image)\nplt.imshow(cam, cmap=\"jet\", alpha=0.4, vmin=0, vmax=1)\nplt.title(f\"Overlay\\nPredicted: {CLASS_NAMES[predicted_class]}\")\nplt.axis(\"off\")\n\nplt.suptitle(f\"MobileNetV3-Small Grad-CAM | Image: {image_id}\")\nplt.tight_layout()\nplt.show()\n\nprint(\"Image ID:\", image_id)\nprint(\"True grade:\", CLASS_NAMES[true_label])\nprint(\"Predicted grade:\", CLASS_NAMES[predicted_class])\nprint(\"Prediction:\", \"Correct\" if true_label == predicted_class else \"Incorrect\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:23:21.432294Z","iopub.execute_input":"2026-10-19T14:23:21.432722Z","iopub.status.idle":"2026-10-19T14:23:22.192472Z","shell.execute_reply.started":"2026-10-19T14:23:21.432696Z","shell.execute_reply":"2026-10-19T14:23:22.191717Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dr_sample_idx = None\ngradcam.forward_handle.remove()\n\ntry:\n    for idx in range(len(val_loader_pilot.dataset)):\n        sample = val_loader_pilot.dataset[idx]\n        true_label = int(sample[\"label\"])\n\n        if true_label == 0:\n            continue\n\n        image_batch = sample[\"image\"].unsqueeze(0).to(device)\n\n        with torch.no_grad():\n            logits = baseline_model(image_batch)\n            pred_label = int(logits.argmax(dim=1).item())\n\n        if pred_label == true_label:\n            dr_sample_idx = idx\n            break\n\nfinally:\n    # Restore the hook so Grad-CAM continues to work\n    gradcam.forward_handle = baseline_model.features[-1].register_forward_hook(\n        gradcam._save_activations\n    )\n\nif dr_sample_idx is None:\n    print(\"No correctly classified DR-positive example found in this scan.\")\nelse:\n    selected_sample = val_loader_pilot.dataset[dr_sample_idx]\n\n    print(\"Found correctly classified DR-positive sample.\")\n    print(\"Validation index:\", dr_sample_idx)\n    print(\"Image ID:\", selected_sample.get(\"image_id\"))\n    print(\"True grade:\", CLASS_NAMES[int(selected_sample[\"label\"])])\n    print(\"Predicted grade:\", CLASS_NAMES[pred_label])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:25:12.472953Z","iopub.execute_input":"2026-10-19T14:25:12.473338Z","iopub.status.idle":"2026-10-19T14:25:13.593932Z","shell.execute_reply.started":"2026-10-19T14:25:12.473313Z","shell.execute_reply":"2026-10-19T14:25:13.593133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Retrieve the selected validation sample\nsample = val_loader_pilot.dataset[dr_sample_idx]\n\nimage_tensor = sample[\"image\"].unsqueeze(0).to(device)\ntrue_label = int(sample[\"label\"])\nimage_id = sample.get(\"image_id\", f\"Validation sample {dr_sample_idx}\")\n\n# Generate Grad-CAM\ncam, predicted_class = gradcam.generate(image_tensor)\n\n# Undo ImageNet normalization for visualization\nimage = sample[\"image\"].cpu() * IMAGENET_STD + IMAGENET_MEAN\nimage = image.clamp(0, 1).permute(1, 2, 0).numpy()\n\n# Display original, heatmap and overlay\nfig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\naxes[0].imshow(image)\naxes[0].set_title(f\"Original Retina\\nTrue: {CLASS_NAMES[true_label]}\")\naxes[0].axis(\"off\")\n\naxes[1].imshow(cam, cmap=\"jet\", vmin=0, vmax=1)\naxes[1].set_title(\"Grad-CAM Heatmap\")\naxes[1].axis(\"off\")\n\naxes[2].imshow(image)\naxes[2].imshow(cam, cmap=\"jet\", alpha=0.4, vmin=0, vmax=1)\naxes[2].set_title(f\"Overlay\\nPredicted: {CLASS_NAMES[predicted_class]}\")\naxes[2].axis(\"off\")\n\nplt.suptitle(f\"MobileNetV3-Small Grad-CAM | Image: {image_id}\")\nplt.tight_layout()\nplt.show()\n\nprint(\"Image ID:\", image_id)\nprint(\"True grade:\", CLASS_NAMES[true_label])\nprint(\"Predicted grade:\", CLASS_NAMES[predicted_class])\nprint(\"Prediction:\", \"Correct\" if true_label == predicted_class else \"Incorrect\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:25:35.996486Z","iopub.execute_input":"2026-10-19T14:25:35.997446Z","iopub.status.idle":"2026-10-19T14:25:36.703753Z","shell.execute_reply.started":"2026-10-19T14:25:35.997402Z","shell.execute_reply":"2026-10-19T14:25:36.702768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small\n\nWEIGHTED_SIMCLR_PATH = (\n    \"/kaggle/working/smartretina/simclr_classweighted_best.pth\"\n)\n\nweighted_checkpoint = torch.load(\n    WEIGHTED_SIMCLR_PATH,\n    map_location=device,\n    weights_only=False\n)\n\nweighted_simclr_model = mobilenet_v3_small(weights=None)\nweighted_simclr_model.classifier[3] = nn.Linear(\n    weighted_simclr_model.classifier[3].in_features,\n    5\n)\n\nif \"model_state_dict\" in weighted_checkpoint:\n    weighted_state = weighted_checkpoint[\"model_state_dict\"]\nelif \"state_dict\" in weighted_checkpoint:\n    weighted_state = weighted_checkpoint[\"state_dict\"]\nelse:\n    weighted_state = weighted_checkpoint\n\nweighted_simclr_model.load_state_dict(weighted_state)\nweighted_simclr_model = weighted_simclr_model.to(device)\nweighted_simclr_model.eval()\n\nprint(\"Class-weighted SimCLR model loaded successfully.\")\nprint(\"Device:\", device)\nprint(\"Output classes:\", weighted_simclr_model.classifier[3].out_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:26:09.799989Z","iopub.execute_input":"2026-10-19T14:26:09.800759Z","iopub.status.idle":"2026-10-19T14:26:09.979383Z","shell.execute_reply.started":"2026-10-19T14:26:09.80073Z","shell.execute_reply":"2026-10-19T14:26:09.978678Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nfrom sklearn.metrics import (\n    accuracy_score,\n    f1_score,\n    cohen_kappa_score,\n    classification_report\n)\n\ndef evaluate_model(model, loader, model_name):\n    model.eval()\n    all_labels = []\n    all_predictions = []\n\n    # Disable Grad-CAM's activation hook during evaluation\n    removed_gradcam_hook = False\n\n    if model is baseline_model and gradcam.forward_handle is not None:\n        gradcam.forward_handle.remove()\n        removed_gradcam_hook = True\n\n    try:\n        with torch.inference_mode():\n            for batch in loader:\n                images = batch[\"image\"].to(device)\n                labels = batch[\"label\"].to(device)\n\n                logits = model(images)\n                predictions = logits.argmax(dim=1)\n\n                all_labels.extend(labels.cpu().numpy().tolist())\n                all_predictions.extend(predictions.cpu().numpy().tolist())\n\n    finally:\n        # Restore Grad-CAM for future visualizations\n        if removed_gradcam_hook:\n            gradcam.forward_handle = baseline_model.features[-1].register_forward_hook(\n                gradcam._save_activations\n            )\n\n    y_true = np.asarray(all_labels)\n    y_pred = np.asarray(all_predictions)\n\n    results = {\n        \"Model\": model_name,\n        \"Accuracy\": accuracy_score(y_true, y_pred),\n        \"Macro-F1\": f1_score(\n            y_true, y_pred, average=\"macro\", zero_division=0\n        ),\n        \"QWK\": cohen_kappa_score(\n            y_true, y_pred, weights=\"quadratic\"\n        ),\n        \"y_true\": y_true,\n        \"y_pred\": y_pred\n    }\n\n    print(f\"\\n{'=' * 60}\")\n    print(model_name)\n    print(\"=\" * 60)\n    print(f\"Accuracy : {results['Accuracy']:.4f}\")\n    print(f\"Macro-F1 : {results['Macro-F1']:.4f}\")\n    print(f\"QWK      : {results['QWK']:.4f}\")\n\n    print(\"\\nPer-grade classification report:\")\n    print(classification_report(\n        y_true,\n        y_pred,\n        labels=[0, 1, 2, 3, 4],\n        target_names=CLASS_NAMES,\n        digits=4,\n        zero_division=0\n    ))\n\n    return results\n\n\nbaseline_results = evaluate_model(\n    baseline_model,\n    val_loader_pilot,\n    \"ImageNet-Supervised MobileNetV3-Small\"\n)\n\nweighted_results = evaluate_model(\n    weighted_simclr_model,\n    val_loader_pilot,\n    \"Class-Weighted SimCLR + MobileNetV3-Small\"\n)\n\nprint(\"\\nBoth models evaluated on the same validation set.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:27:07.301101Z","iopub.execute_input":"2026-10-19T14:27:07.301542Z","iopub.status.idle":"2026-10-19T14:28:42.97808Z","shell.execute_reply.started":"2026-10-19T14:27:07.301516Z","shell.execute_reply":"2026-10-19T14:28:42.977011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n\nfig, axes = plt.subplots(1, 2, figsize=(15, 6))\n\nmodels_to_compare = [\n    (baseline_results, \"Supervised MobileNetV3-Small\"),\n    (weighted_results, \"Class-Weighted SimCLR\")\n]\n\nfor ax, (results, title) in zip(axes, models_to_compare):\n    cm = confusion_matrix(\n        results[\"y_true\"],\n        results[\"y_pred\"],\n        labels=[0, 1, 2, 3, 4]\n    )\n\n    disp = ConfusionMatrixDisplay(\n        confusion_matrix=cm,\n        display_labels=CLASS_NAMES\n    )\n    disp.plot(ax=ax, cmap=\"Blues\", values_format=\"d\", colorbar=False)\n    ax.set_title(title)\n    ax.tick_params(axis=\"x\", labelrotation=35)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:29:38.98Z","iopub.execute_input":"2026-10-19T14:29:38.98079Z","iopub.status.idle":"2026-10-19T14:29:39.28882Z","shell.execute_reply.started":"2026-10-19T14:29:38.98076Z","shell.execute_reply":"2026-10-19T14:29:39.287895Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import mobilenet_v3_small\n\nSIMCLR_CHECKPOINT_PATH = (\n    \"/kaggle/working/smartretina/simclr_aptos_finetuned_best.pth\"\n)\n\nsimclr_checkpoint = torch.load(\n    SIMCLR_CHECKPOINT_PATH,\n    map_location=device,\n    weights_only=False\n)\n\nsimclr_model = mobilenet_v3_small(weights=None)\nsimclr_model.classifier[3] = nn.Linear(\n    simclr_model.classifier[3].in_features,\n    5\n)\n\nif \"model_state_dict\" in simclr_checkpoint:\n    simclr_state = simclr_checkpoint[\"model_state_dict\"]\nelif \"state_dict\" in simclr_checkpoint:\n    simclr_state = simclr_checkpoint[\"state_dict\"]\nelse:\n    simclr_state = simclr_checkpoint\n\nsimclr_model.load_state_dict(simclr_state)\nsimclr_model = simclr_model.to(device)\nsimclr_model.eval()\n\nprint(\"Original SimCLR model loaded successfully.\")\nprint(\"Checkpoint:\", SIMCLR_CHECKPOINT_PATH)\n\nsimclr_results = evaluate_model(\n    simclr_model,\n    val_loader_pilot,\n    \"Original SimCLR + MobileNetV3-Small\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:30:19.065355Z","iopub.execute_input":"2026-10-19T14:30:19.065794Z","iopub.status.idle":"2026-10-19T14:31:08.053898Z","shell.execute_reply.started":"2026-10-19T14:30:19.065768Z","shell.execute_reply":"2026-10-19T14:31:08.052876Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BYOL_CHECKPOINT_PATH = (\n    \"/kaggle/working/smartretina/byol_aptos_finetuned_best.pth\"\n)\n\nbyol_checkpoint = torch.load(\n    BYOL_CHECKPOINT_PATH,\n    map_location=device,\n    weights_only=False\n)\n\nbyol_model = mobilenet_v3_small(weights=None)\nbyol_model.classifier[3] = nn.Linear(\n    byol_model.classifier[3].in_features,\n    5\n)\n\nif \"model_state_dict\" in byol_checkpoint:\n    byol_state = byol_checkpoint[\"model_state_dict\"]\nelif \"state_dict\" in byol_checkpoint:\n    byol_state = byol_checkpoint[\"state_dict\"]\nelse:\n    byol_state = byol_checkpoint\n\nbyol_model.load_state_dict(byol_state)\nbyol_model = byol_model.to(device)\nbyol_model.eval()\n\nprint(\"BYOL model loaded successfully.\")\n\nbyol_results = evaluate_model(\n    byol_model,\n    val_loader_pilot,\n    \"BYOL + MobileNetV3-Small\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:31:48.598272Z","iopub.execute_input":"2026-10-19T14:31:48.598721Z","iopub.status.idle":"2026-10-19T14:32:36.987307Z","shell.execute_reply.started":"2026-10-19T14:31:48.598696Z","shell.execute_reply":"2026-10-19T14:32:36.986371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.metrics import recall_score, f1_score, accuracy_score, cohen_kappa_score\n\nmodel_results = [\n    baseline_results,\n    simclr_results,\n    weighted_results,\n    byol_results\n]\n\nsummary_rows = []\nclass_rows = []\n\nfor result in model_results:\n    y_true = result[\"y_true\"]\n    y_pred = result[\"y_pred\"]\n\n    summary_rows.append({\n        \"Model\": result[\"Model\"],\n        \"Accuracy\": accuracy_score(y_true, y_pred),\n        \"Macro_F1\": f1_score(\n            y_true, y_pred, average=\"macro\", zero_division=0\n        ),\n        \"QWK\": cohen_kappa_score(\n            y_true, y_pred, weights=\"quadratic\"\n        )\n    })\n\n    for class_idx, class_name in enumerate(CLASS_NAMES):\n        class_mask = (y_true == class_idx)\n        class_recall = (\n            (y_pred[class_mask] == class_idx).mean()\n            if class_mask.any() else 0.0\n        )\n\n        class_rows.append({\n            \"Model\": result[\"Model\"],\n            \"Grade\": class_name,\n            \"Recall\": class_recall,\n            \"Support\": int(class_mask.sum())\n        })\n\nsummary_df = pd.DataFrame(summary_rows)\nclass_recall_df = pd.DataFrame(class_rows)\n\noutput_dir = \"/kaggle/working/smartretina\"\nimport os\nos.makedirs(output_dir, exist_ok=True)\n\nsummary_df.to_csv(\n    f\"{output_dir}/model_comparison_validation.csv\", index=False\n)\nclass_recall_df.to_csv(\n    f\"{output_dir}/classwise_recall_validation.csv\", index=False\n)\n\nprint(\"OVERALL VALIDATION COMPARISON\")\ndisplay(summary_df.round(4))\n\nprint(\"\\nCLASS-WISE RECALL\")\ndisplay(\n    class_recall_df.pivot(\n        index=\"Grade\", columns=\"Model\", values=\"Recall\"\n    ).round(4)\n)\n\nprint(\"\\nSaved comparison files to:\", output_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:33:17.891429Z","iopub.execute_input":"2026-10-19T14:33:17.892185Z","iopub.status.idle":"2026-10-19T14:33:17.940658Z","shell.execute_reply.started":"2026-10-19T14:33:17.892155Z","shell.execute_reply":"2026-10-19T14:33:17.939961Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nfrom sklearn.metrics import (\n    accuracy_score,\n    f1_score,\n    cohen_kappa_score,\n    classification_report,\n    confusion_matrix,\n    ConfusionMatrixDisplay\n)\n\n# --------------------------------------------------\n# 1. Output directory\n# --------------------------------------------------\nREPORT_DIR = \"/kaggle/working/smartretina/report_figures\"\nos.makedirs(REPORT_DIR, exist_ok=True)\n\nCLASS_NAMES = [\n    \"No DR\", \"Mild\", \"Moderate\", \"Severe\", \"Proliferative DR\"\n]\n\nmodel_results = [\n    baseline_results,\n    simclr_results,\n    weighted_results,\n    byol_results\n]\n\n# --------------------------------------------------\n# 2. Overall comparison table\n# --------------------------------------------------\nsummary_rows = []\nclass_metric_rows = []\nconfusion_matrices = {}\n\nfor result in model_results:\n    y_true = np.asarray(result[\"y_true\"])\n    y_pred = np.asarray(result[\"y_pred\"])\n    model_name = result[\"Model\"]\n\n    summary_rows.append({\n        \"Model\": model_name,\n        \"Accuracy\": accuracy_score(y_true, y_pred),\n        \"Macro_F1\": f1_score(\n            y_true, y_pred, average=\"macro\", zero_division=0\n        ),\n        \"Weighted_F1\": f1_score(\n            y_true, y_pred, average=\"weighted\", zero_division=0\n        ),\n        \"QWK\": cohen_kappa_score(\n            y_true, y_pred, weights=\"quadratic\"\n        )\n    })\n\n    report = classification_report(\n        y_true,\n        y_pred,\n        labels=[0, 1, 2, 3, 4],\n        target_names=CLASS_NAMES,\n        output_dict=True,\n        zero_division=0\n    )\n\n    for grade in CLASS_NAMES:\n        class_metric_rows.append({\n            \"Model\": model_name,\n            \"Grade\": grade,\n            \"Precision\": report[grade][\"precision\"],\n            \"Recall\": report[grade][\"recall\"],\n            \"F1_Score\": report[grade][\"f1-score\"],\n            \"Support\": int(report[grade][\"support\"])\n        })\n\n    confusion_matrices[model_name] = confusion_matrix(\n        y_true, y_pred, labels=[0, 1, 2, 3, 4]\n    )\n\nsummary_df = pd.DataFrame(summary_rows)\nclass_metrics_df = pd.DataFrame(class_metric_rows)\n\nsummary_df.to_csv(\n    f\"{REPORT_DIR}/overall_model_comparison.csv\", index=False\n)\nclass_metrics_df.to_csv(\n    f\"{REPORT_DIR}/classwise_metrics_all_models.csv\", index=False\n)\n\nprint(\"OVERALL COMPARISON\")\ndisplay(summary_df.round(4))\n\nprint(\"CLASS-WISE METRICS\")\ndisplay(class_metrics_df.round(4))\n\n# --------------------------------------------------\n# 3. Figure: overall performance comparison\n# --------------------------------------------------\nmetric_columns = [\"Accuracy\", \"Macro_F1\", \"QWK\"]\nshort_names = [\n    \"Supervised\", \"Original SimCLR\",\n    \"Weighted SimCLR\", \"BYOL\"\n]\n\nfig, axes = plt.subplots(1, 3, figsize=(16, 5))\n\nfor ax, metric in zip(axes, metric_columns):\n    bars = ax.bar(short_names, summary_df[metric])\n    ax.set_title(metric.replace(\"_\", \"-\"))\n    ax.set_ylim(0, 1)\n    ax.tick_params(axis=\"x\", rotation=35)\n    ax.bar_label(bars, fmt=\"%.3f\", padding=3)\n    ax.grid(axis=\"y\", alpha=0.25)\n\nfig.suptitle(\"SmartRetina: Model Performance on APTOS Validation Set\")\nfig.tight_layout()\nfig.savefig(\n    f\"{REPORT_DIR}/figure_overall_comparison.png\",\n    dpi=300, bbox_inches=\"tight\"\n)\nplt.show()\n\n# --------------------------------------------------\n# 4. Figure: class-wise recall comparison\n# --------------------------------------------------\nrecall_pivot = class_metrics_df.pivot(\n    index=\"Grade\", columns=\"Model\", values=\"Recall\"\n)\n\nax = recall_pivot.plot(\n    kind=\"bar\", figsize=(13, 6), ylim=(0, 1), width=0.8\n)\nax.set_title(\"Class-wise Recall Across Four Models\")\nax.set_ylabel(\"Recall\")\nax.set_xlabel(\"DR Grade\")\nax.tick_params(axis=\"x\", rotation=20)\nax.grid(axis=\"y\", alpha=0.25)\nax.legend(title=\"Model\", bbox_to_anchor=(1.02, 1), loc=\"upper left\")\nplt.tight_layout()\nplt.savefig(\n    f\"{REPORT_DIR}/figure_classwise_recall.png\",\n    dpi=300, bbox_inches=\"tight\"\n)\nplt.show()\n\n# --------------------------------------------------\n# 5. Figure: confusion matrices for all four models\n# --------------------------------------------------\nfig, axes = plt.subplots(2, 2, figsize=(16, 13))\n\nfor ax, result in zip(axes.flat, model_results):\n    name = result[\"Model\"]\n    cm = confusion_matrices[name]\n\n    display_obj = ConfusionMatrixDisplay(\n        confusion_matrix=cm,\n        display_labels=CLASS_NAMES\n    )\n    display_obj.plot(\n        ax=ax, cmap=\"Blues\", values_format=\"d\", colorbar=False\n    )\n    ax.set_title(name, fontsize=10)\n    ax.tick_params(axis=\"x\", labelrotation=40)\n\nfig.suptitle(\"Confusion Matrices on APTOS Validation Set\", fontsize=15)\nfig.tight_layout()\nfig.savefig(\n    f\"{REPORT_DIR}/figure_all_confusion_matrices.png\",\n    dpi=300, bbox_inches=\"tight\"\n)\nplt.show()\n\n# Also save individual confusion matrices as CSV files\nfor result in model_results:\n    name = result[\"Model\"]\n    safe_name = (\n        name.lower()\n        .replace(\" \", \"_\")\n        .replace(\"+\", \"plus\")\n        .replace(\"-\", \"_\")\n    )\n    pd.DataFrame(\n        confusion_matrices[name],\n        index=CLASS_NAMES,\n        columns=CLASS_NAMES\n    ).to_csv(f\"{REPORT_DIR}/confusion_matrix_{safe_name}.csv\")\n\n# --------------------------------------------------\n# 6. Optional: dataset class distribution\n# --------------------------------------------------\nif \"aptos_train\" in globals() and \"label\" in aptos_train.columns:\n    counts = (\n        aptos_train[\"label\"]\n        .value_counts()\n        .reindex(range(5), fill_value=0)\n    )\n\n    distribution_df = pd.DataFrame({\n        \"Grade\": CLASS_NAMES,\n        \"Count\": counts.values\n    })\n    distribution_df.to_csv(\n        f\"{REPORT_DIR}/dataset_class_distribution.csv\", index=False\n    )\n\n    fig, ax = plt.subplots(figsize=(9, 5))\n    bars = ax.bar(distribution_df[\"Grade\"], distribution_df[\"Count\"])\n    ax.bar_label(bars, padding=3)\n    ax.set_title","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:38:50.145036Z","iopub.execute_input":"2026-10-19T14:38:50.145735Z","iopub.status.idle":"2026-10-19T14:38:54.551488Z","shell.execute_reply.started":"2026-10-19T14:38:50.145706Z","shell.execute_reply":"2026-10-19T14:38:54.550781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport matplotlib.pyplot as plt\n\nIMAGENET_MEAN = torch.tensor(\n    [0.485, 0.456, 0.406]\n).view(3, 1, 1)\n\nIMAGENET_STD = torch.tensor(\n    [0.229, 0.224, 0.225]\n).view(3, 1, 1)\n\n# Reuse the existing Grad-CAM object and baseline model.\n# These are validation indices used in our earlier analysis.\ngradcam_examples = [\n    (0, \"no_dr_example\"),\n    (6, \"moderate_dr_example\")\n]\n\nfor sample_idx, file_tag in gradcam_examples:\n    sample = val_loader_pilot.dataset[sample_idx]\n    image_tensor = sample[\"image\"].unsqueeze(0).to(device)\n    true_label = int(sample[\"label\"])\n\n    # Grad-CAM requires gradients, so do not wrap this in no_grad().\n    cam, predicted_class = gradcam.generate(image_tensor)\n\n    image = sample[\"image\"].cpu() * IMAGENET_STD + IMAGENET_MEAN\n    image = image.clamp(0, 1).permute(1, 2, 0).numpy()\n\n    fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\n    axes[0].imshow(image)\n    axes[0].set_title(f\"Original Retina\\nTrue: {CLASS_NAMES[true_label]}\")\n    axes[0].axis(\"off\")\n\n    axes[1].imshow(cam, cmap=\"jet\", vmin=0, vmax=1)\n    axes[1].set_title(\"Grad-CAM Heatmap\")\n    axes[1].axis(\"off\")\n\n    axes[2].imshow(image)\n    axes[2].imshow(cam, cmap=\"jet\", alpha=0.4, vmin=0, vmax=1)\n    axes[2].set_title(f\"Predicted: {CLASS_NAMES[predicted_class]}\")\n    axes[2].axis(\"off\")\n\n    image_id = sample.get(\"image_id\", str(sample_idx))\n    fig.suptitle(f\"Grad-CAM | Image ID: {image_id}\")\n    fig.tight_layout()\n\n    output_path = f\"{REPORT_DIR}/figure_gradcam_{file_tag}.png\"\n    fig.savefig(output_path, dpi=300, bbox_inches=\"tight\")\n    plt.show()\n    plt.close(fig)\n\n    print(\n        f\"{file_tag}: true={CLASS_NAMES[true_label]}, \"\n        f\"predicted={CLASS_NAMES[predicted_class]}, saved={output_path}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:39:12.49688Z","iopub.execute_input":"2026-10-19T14:39:12.4977Z","iopub.status.idle":"2026-10-19T14:39:15.415953Z","shell.execute_reply.started":"2026-10-19T14:39:12.49767Z","shell.execute_reply":"2026-10-19T14:39:15.41505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nzip_base = \"/kaggle/working/smartretina_report_assets\"\n\nzip_path = shutil.make_archive(\n    zip_base,\n    \"zip\",\n    root_dir=REPORT_DIR\n)\n\nprint(\"Report assets ZIP created:\", zip_path)\nprint(\"ZIP size (MB):\", round(os.path.getsize(zip_path) / (1024**2), 2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-19T14:39:30.950163Z","iopub.execute_input":"2026-10-19T14:39:30.950786Z","iopub.status.idle":"2026-10-19T14:39:31.02106Z","shell.execute_reply.started":"2026-10-19T14:39:30.950759Z","shell.execute_reply":"2026-10-19T14:39:31.020204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}