{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":14774,"databundleVersionId":875431},{"sourceType":"datasetVersion","sourceId":988278,"datasetId":541202,"databundleVersionId":1016790}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n        break\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:12:34.986916Z","iopub.execute_input":"2026-03-24T10:12:34.987464Z","iopub.status.idle":"2026-03-24T10:13:41.154772Z","shell.execute_reply.started":"2026-03-24T10:12:34.987433Z","shell.execute_reply":"2026-03-24T10:13:41.153709Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 1 — Check GPU & install missing library","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nprint(\"GPU available:\", tf.config.list_physical_devices('GPU'))\nprint(\"TF version:\", tf.__version__)\n\n!pip install -q seaborn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:41.156643Z","iopub.execute_input":"2026-03-24T10:13:41.156897Z","iopub.status.idle":"2026-03-24T10:13:44.766416Z","shell.execute_reply.started":"2026-03-24T10:13:41.156873Z","shell.execute_reply":"2026-03-24T10:13:44.765595Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 2 — Imports & config","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import DenseNet121\nimport tensorflow.keras.applications.densenet as dn_pre\n\nIMG_SIZE   = 224\nBATCH_SIZE = 32\n\nimport cv2, numpy as np, pandas as pd, os, matplotlib.pyplot as plt, seaborn as sns\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers, callbacks\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nNUM_CLASSES = 5\nSAVE_DIR    = \"/kaggle/working\"\nCLASS_NAMES = {0:\"No DR\", 1:\"Mild\", 2:\"Moderate\", 3:\"Severe\", 4:\"Proliferative\"}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:44.767993Z","iopub.execute_input":"2026-03-24T10:13:44.768350Z","iopub.status.idle":"2026-03-24T10:13:44.775327Z","shell.execute_reply.started":"2026-03-24T10:13:44.768318Z","shell.execute_reply":"2026-03-24T10:13:44.774414Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 3 — Load & verify dataset\n","metadata":{}},{"cell_type":"code","source":"TRAIN_CSV = \"/kaggle/input/competitions/aptos2019-blindness-detection/train.csv\"\nTRAIN_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection/train_images/\"\n\nEYEPACS_BASE = \"/kaggle/input/datasets/sovitrath/diabetic-retinopathy-2015-data-colored-resized/colored_images/colored_images/\"\n\naptos_df = pd.read_csv(TRAIN_CSV)\naptos_df[\"image_path\"]      = aptos_df[\"id_code\"].apply(lambda x: os.path.join(TRAIN_DIR, x + \".png\"))\naptos_df[\"diagnosis\"]       = aptos_df[\"diagnosis\"].astype(str)\naptos_df[\"ben_graham_done\"] = False\n\nmissing_aptos = aptos_df[\"image_path\"].apply(lambda p: not os.path.exists(p)).sum()\nprint(f\"APTOS samples: {len(aptos_df)}, missing: {missing_aptos}\")\nprint(aptos_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(f\"\\nEyePACS subfolders: {os.listdir(EYEPACS_BASE)}\")\n\nCLASS_NAME_MAP = {\n    \"No_DR\": \"0\", \"Mild\": \"1\", \"Moderate\": \"2\",\n    \"Severe\": \"3\", \"Proliferate_DR\": \"4\"\n}\n\nrows = []\nfor folder_name, label in CLASS_NAME_MAP.items():\n    folder_path = os.path.join(EYEPACS_BASE, folder_name)\n    if not os.path.isdir(folder_path):\n        print(f\"WARNING: folder not found → {folder_path}\")\n        continue\n    for fname in os.listdir(folder_path):\n        if fname.lower().endswith((\".png\", \".jpeg\", \".jpg\")):\n            rows.append({\n                \"image_path\": os.path.join(folder_path, fname),\n                \"diagnosis\": label,\n                \"ben_graham_done\": True\n            })\n\neyepacs_df = pd.DataFrame(rows)\nprint(f\"EyePACS samples: {len(eyepacs_df)}\")\nprint(eyepacs_df[\"diagnosis\"].value_counts().sort_index())\nprint(f\"\\nSummary — APTOS: {len(aptos_df)}, EyePACS: {len(eyepacs_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:44.776369Z","iopub.execute_input":"2026-03-24T10:13:44.776709Z","iopub.status.idle":"2026-03-24T10:13:44.958573Z","shell.execute_reply.started":"2026-03-24T10:13:44.776672Z","shell.execute_reply":"2026-03-24T10:13:44.957817Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 4 — Train/val split\n","metadata":{}},{"cell_type":"code","source":"# EyePACS — two-way split (train/val only, used for pre-training stages)\neyepacs_train_df, eyepacs_val_df = train_test_split(\n    eyepacs_df, test_size=0.15,\n    stratify=eyepacs_df[\"diagnosis\"], random_state=42\n)\neyepacs_train_df = eyepacs_train_df.reset_index(drop=True)\neyepacs_val_df   = eyepacs_val_df.reset_index(drop=True)\n\nclasses = sorted(eyepacs_train_df[\"diagnosis\"].unique())\nweights = compute_class_weight(\"balanced\", classes=np.array(classes), y=eyepacs_train_df[\"diagnosis\"])\neyepacs_class_weights = {int(k): v for k, v in zip(classes, weights)}\nprint(\"EyePACS class weights:\", eyepacs_class_weights)\nprint(f\"EyePACS Train: {len(eyepacs_train_df)} | Val: {len(eyepacs_val_df)}\")\n\n# APTOS — three-way split (train/val/test)\n# test set is held out completely — never seen during training or callbacks\naptos_train_df, aptos_test_df = train_test_split(\n    aptos_df, test_size=0.20,\n    stratify=aptos_df[\"diagnosis\"], random_state=42\n)\naptos_train_df, aptos_val_df = train_test_split(\n    aptos_train_df, test_size=0.125,   # 0.125 × 0.80 = 10% of total\n    stratify=aptos_train_df[\"diagnosis\"], random_state=42\n)\naptos_train_df = aptos_train_df.reset_index(drop=True)\naptos_val_df   = aptos_val_df.reset_index(drop=True)\naptos_test_df  = aptos_test_df.reset_index(drop=True)\n\nclasses_a = sorted(aptos_train_df[\"diagnosis\"].unique())\nweights_a  = compute_class_weight(\"balanced\", classes=np.array(classes_a), y=aptos_train_df[\"diagnosis\"])\naptos_class_weights = {int(k): v for k, v in zip(classes_a, weights_a)}\nprint(\"APTOS class weights:\", aptos_class_weights)\nprint(f\"APTOS Train: {len(aptos_train_df)} | Val: {len(aptos_val_df)} | Test: {len(aptos_test_df)}\")\n\n\n# The expected output will look roughly like:\n# ```\n# EyePACS — Train: 29857 | Val: 5269\n# APTOS   — Train: 2563  | Val:  366  | Test: 733","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:44.961144Z","iopub.execute_input":"2026-03-24T10:13:44.961693Z","iopub.status.idle":"2026-03-24T10:13:45.033005Z","shell.execute_reply.started":"2026-03-24T10:13:44.961665Z","shell.execute_reply":"2026-03-24T10:13:45.032325Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 5 — Sanity check (see one preprocessed image)","metadata":{}},{"cell_type":"code","source":"def crop_image_from_gray(img, tol=7):\n    if img.ndim == 2:\n        mask = img > tol\n        return img[np.ix_(mask.any(1), mask.any(0))]\n    gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n    mask = gray > tol\n    if img[:,:,0][np.ix_(mask.any(1), mask.any(0))].shape[0] == 0:\n        return img\n    return np.stack([img[:,:,c][np.ix_(mask.any(1), mask.any(0))] for c in range(3)], axis=-1)\n\ndef preprocess_image(path, sigmaX=10, apply_ben_graham=True):\n    img = cv2.imread(path)\n    if img is None:\n        return np.zeros((IMG_SIZE, IMG_SIZE, 3), dtype=np.uint8)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = crop_image_from_gray(img)\n    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n    if apply_ben_graham:\n        img = cv2.addWeighted(img, 4, cv2.GaussianBlur(img, (0,0), sigmaX), -4, 128)\n    return np.clip(img, 0, 255).astype(np.uint8)\n\naptos_path    = aptos_train_df[\"image_path\"].iloc[0]\naptos_label   = aptos_train_df[\"diagnosis\"].iloc[0]\neyepacs_path  = eyepacs_train_df[\"image_path\"].iloc[0]\neyepacs_label = eyepacs_train_df[\"diagnosis\"].iloc[0]\n\nfig, axes = plt.subplots(2, 2, figsize=(12, 10))\naxes[0][0].imshow(cv2.resize(cv2.cvtColor(cv2.imread(aptos_path), cv2.COLOR_BGR2RGB), (IMG_SIZE,IMG_SIZE)))\naxes[0][0].set_title(f\"APTOS original (Class {aptos_label})\")\naxes[0][1].imshow(preprocess_image(aptos_path, apply_ben_graham=True))\naxes[0][1].set_title(\"APTOS after Ben Graham\")\naxes[1][0].imshow(cv2.resize(cv2.cvtColor(cv2.imread(eyepacs_path), cv2.COLOR_BGR2RGB), (IMG_SIZE,IMG_SIZE)))\naxes[1][0].set_title(f\"EyePACS original (Class {eyepacs_label})\")\naxes[1][1].imshow(preprocess_image(eyepacs_path, apply_ben_graham=False))\naxes[1][1].set_title(\"EyePACS — no preprocessing (already done)\")\nfor row in axes:\n    for ax in row: ax.axis(\"off\")\nplt.tight_layout(); plt.show()\nprint(\"Preprocessing looks good ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:45.034070Z","iopub.execute_input":"2026-03-24T10:13:45.034402Z","iopub.status.idle":"2026-03-24T10:13:46.432285Z","shell.execute_reply.started":"2026-03-24T10:13:45.034375Z","shell.execute_reply":"2026-03-24T10:13:46.431373Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 6 — Build data generators","metadata":{}},{"cell_type":"code","source":"def build_generators(train_df, val_df, preprocess_fn):\n    train_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_fn,\n        rotation_range=360,\n        horizontal_flip=True, vertical_flip=True,\n        zoom_range=0.1,\n        brightness_range=[0.8, 1.2],\n        fill_mode=\"constant\", cval=0\n    )\n    val_datagen = ImageDataGenerator(preprocessing_function=preprocess_fn)\n\n    train_gen = train_datagen.flow_from_dataframe(\n        dataframe=train_df, x_col=\"image_path\", y_col=\"diagnosis\",\n        target_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE,\n        class_mode=\"categorical\", shuffle=True, seed=42\n    )\n    val_gen = val_datagen.flow_from_dataframe(\n        dataframe=val_df, x_col=\"image_path\", y_col=\"diagnosis\",\n        target_size=(IMG_SIZE, IMG_SIZE), batch_size=BATCH_SIZE,\n        class_mode=\"categorical\", shuffle=False\n    )\n    print(f\"Class indices: {train_gen.class_indices}\")\n    return train_gen, val_gen\n\neyepacs_train_gen, eyepacs_val_gen = build_generators(\n    eyepacs_train_df, eyepacs_val_df,\n    preprocess_fn=dn_pre.preprocess_input\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:13:46.433716Z","iopub.execute_input":"2026-03-24T10:13:46.434447Z","iopub.status.idle":"2026-03-24T10:14:44.428676Z","shell.execute_reply.started":"2026-03-24T10:13:46.434406Z","shell.execute_reply":"2026-03-24T10:14:44.427935Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 7 — Build DenseNet121 model","metadata":{}},{"cell_type":"code","source":"def build_model():\n    inp  = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n    base = DenseNet121(include_top=False, weights=\"imagenet\", input_tensor=inp)\n    base.trainable = False\n\n    x   = layers.GlobalAveragePooling2D()(base.output)\n    x   = layers.BatchNormalization()(x)\n    x   = layers.Dropout(0.4)(x)\n    x   = layers.Dense(256, activation=\"relu\")(x)\n    x   = layers.Dropout(0.3)(x)\n    out = layers.Dense(NUM_CLASSES, activation=\"softmax\")(x)\n\n    model = models.Model(inputs=inp, outputs=out)\n    return model, base\n\nmodel, base_model = build_model()\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:14:44.429716Z","iopub.execute_input":"2026-03-24T10:14:44.430270Z","iopub.status.idle":"2026-03-24T10:14:50.286109Z","shell.execute_reply.started":"2026-03-24T10:14:44.430210Z","shell.execute_reply":"2026-03-24T10:14:50.285476Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 8 — Diagnostic (NEW — run before any training)","metadata":{}},{"cell_type":"code","source":"print(f\"Total layers in base model : {len(base_model.layers)}\")\nprint(f\"Total layers in full model : {len(model.layers)}\")\n\nrec_base = len(base_model.layers) // 4\nrec_full = len(model.layers) // 4\n\nprint(f\"\\nOld setting (hardcoded last 30):\")\nprint(f\"  % of base model trained : {30 / len(base_model.layers) * 100:.1f}%\")\n\nprint(f\"\\nNew setting (last 25% of layers):\")\nprint(f\"  Base model — unfreeze last : {rec_base} layers\")\nprint(f\"  Full model — unfreeze last : {rec_full} layers\")\nprint(f\"  % of base model trained   : {rec_base / len(base_model.layers) * 100:.1f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:14:50.287273Z","iopub.execute_input":"2026-03-24T10:14:50.287966Z","iopub.status.idle":"2026-03-24T10:14:50.294354Z","shell.execute_reply.started":"2026-03-24T10:14:50.287939Z","shell.execute_reply":"2026-03-24T10:14:50.293536Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 9 — Phase 1: Train head only (frozen backbone)","metadata":{}},{"cell_type":"code","source":"cb_phase1 = [\n    callbacks.ModelCheckpoint(\"/kaggle/working/model_eyepacs_phase1_best.keras\",\n                               monitor=\"val_accuracy\", save_best_only=True, verbose=1),\n    callbacks.EarlyStopping(monitor=\"val_loss\", patience=5,\n                            restore_best_weights=True, verbose=1),\n    callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5,\n                                patience=3, min_lr=1e-7, verbose=1),\n    callbacks.CSVLogger(\"/kaggle/working/phase1_log.csv\")\n]\nmodel.compile(optimizer=optimizers.Adam(1e-3),\n              loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\n\nprint(\"Stage 1 — Phase 1: Training head (backbone frozen)...\")\nhistory1 = model.fit(\n    eyepacs_train_gen, validation_data=eyepacs_val_gen,\n    epochs=10, class_weight=eyepacs_class_weights,\n    callbacks=cb_phase1, verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T10:14:50.295303Z","iopub.execute_input":"2026-03-24T10:14:50.295544Z","iopub.status.idle":"2026-03-24T11:47:12.192743Z","shell.execute_reply.started":"2026-03-24T10:14:50.295522Z","shell.execute_reply":"2026-03-24T11:47:12.192035Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 10 — Phase 2: Fine-tune top layers","metadata":{}},{"cell_type":"code","source":"base_model.trainable = True\n\n# Proportion-based unfreeze — scales correctly for any architecture\nunfreeze_count = len(base_model.layers) // 4\nfor layer in base_model.layers[:-unfreeze_count]:\n    layer.trainable = False\n\ntrainable_count = sum(1 for l in base_model.layers if l.trainable)\nprint(f\"Base model total layers  : {len(base_model.layers)}\")\nprint(f\"Unfreezing last          : {unfreeze_count} layers\")\nprint(f\"Trainable layers         : {trainable_count}\")\nprint(f\"% of base model training : {trainable_count / len(base_model.layers) * 100:.1f}%\")\n\ncb_phase2 = [\n    callbacks.ModelCheckpoint(\"/kaggle/working/model_eyepacs_phase2_best.keras\",\n                               monitor=\"val_accuracy\", save_best_only=True, verbose=1),\n    callbacks.EarlyStopping(monitor=\"val_loss\", patience=5,\n                            restore_best_weights=True, verbose=1),\n    callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5,\n                                patience=3, min_lr=1e-7, verbose=1),\n    callbacks.CSVLogger(\"/kaggle/working/phase2_log.csv\")\n]\nmodel.compile(optimizer=optimizers.Adam(1e-5),\n              loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\n\nprint(\"\\nStage 1 — Phase 2: Fine-tuning top layers on EyePACS...\")\nhistory2 = model.fit(\n    eyepacs_train_gen, validation_data=eyepacs_val_gen,\n    epochs=20, class_weight=eyepacs_class_weights,\n    callbacks=cb_phase2, verbose=1\n)\nmodel.save(\"/kaggle/working/model_eyepacs_pretrained.keras\")\nprint(\"EyePACS pre-trained model saved ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T11:47:12.194040Z","iopub.execute_input":"2026-03-24T11:47:12.194370Z","iopub.status.idle":"2026-03-24T14:45:15.527532Z","shell.execute_reply.started":"2026-03-24T11:47:12.194345Z","shell.execute_reply":"2026-03-24T14:45:15.526594Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# NEW Cell 11 — load + finetune on APTOS","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.models import load_model\n\nmodel = load_model(\"/kaggle/working/model_eyepacs_pretrained.keras\")\n\naptos_train_gen, aptos_val_gen = build_generators(\n    aptos_train_df, aptos_val_df,\n    preprocess_fn=dn_pre.preprocess_input\n)\n\n# Proportion-based unfreeze on full model\nunfreeze_count = len(model.layers) // 4\nfor layer in model.layers[:-unfreeze_count]:\n    layer.trainable = False\n\ntrainable_count = sum(1 for l in model.layers if l.trainable)\nprint(f\"Full model total layers  : {len(model.layers)}\")\nprint(f\"Unfreezing last          : {unfreeze_count} layers\")\nprint(f\"Trainable layers         : {trainable_count}\")\nprint(f\"% of full model training : {trainable_count / len(model.layers) * 100:.1f}%\")\n\ncb_aptos = [\n    callbacks.ModelCheckpoint(\"/kaggle/working/model_aptos_best.keras\",\n                               monitor=\"val_accuracy\", save_best_only=True, verbose=1),\n    callbacks.EarlyStopping(monitor=\"val_loss\", patience=5,\n                            restore_best_weights=True, verbose=1),\n    callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5,\n                                patience=3, min_lr=1e-7, verbose=1),\n    callbacks.CSVLogger(\"/kaggle/working/aptos_log.csv\")\n]\nmodel.compile(optimizer=optimizers.Adam(1e-5),\n              loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\n\nprint(\"\\nStage 2: Fine-tuning on APTOS...\")\nhistory_aptos = model.fit(\n    aptos_train_gen, validation_data=aptos_val_gen,\n    epochs=15, class_weight=aptos_class_weights,\n    callbacks=cb_aptos, verbose=1\n)\nmodel.save(\"/kaggle/working/model_final_aptos.keras\")\nprint(\"Final model saved ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T14:45:15.528943Z","iopub.execute_input":"2026-03-24T14:45:15.529618Z","iopub.status.idle":"2026-03-24T16:15:35.606464Z","shell.execute_reply.started":"2026-03-24T14:45:15.529592Z","shell.execute_reply":"2026-03-24T16:15:35.605651Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 11 — Plot training curves","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.models import load_model\n\nmodel = load_model(\"/kaggle/working/model_eyepacs_pretrained.keras\")\n\naptos_train_gen, aptos_val_gen = build_generators(\n    aptos_train_df, aptos_val_df,\n    preprocess_fn=dn_pre.preprocess_input\n)\n\n# Proportion-based unfreeze on full model\nunfreeze_count = len(model.layers) // 4\nfor layer in model.layers[:-unfreeze_count]:\n    layer.trainable = False\n\ntrainable_count = sum(1 for l in model.layers if l.trainable)\nprint(f\"Full model total layers  : {len(model.layers)}\")\nprint(f\"Unfreezing last          : {unfreeze_count} layers\")\nprint(f\"Trainable layers         : {trainable_count}\")\nprint(f\"% of full model training : {trainable_count / len(model.layers) * 100:.1f}%\")\n\ncb_aptos = [\n    callbacks.ModelCheckpoint(\"/kaggle/working/model_aptos_best.keras\",\n                               monitor=\"val_accuracy\", save_best_only=True, verbose=1),\n    callbacks.EarlyStopping(monitor=\"val_loss\", patience=5,\n                            restore_best_weights=True, verbose=1),\n    callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5,\n                                patience=3, min_lr=1e-7, verbose=1),\n    callbacks.CSVLogger(\"/kaggle/working/aptos_log.csv\")\n]\nmodel.compile(optimizer=optimizers.Adam(1e-5),\n              loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\n\nprint(\"\\nStage 2: Fine-tuning on APTOS...\")\nhistory_aptos = model.fit(\n    aptos_train_gen, validation_data=aptos_val_gen,\n    epochs=15, class_weight=aptos_class_weights,\n    callbacks=cb_aptos, verbose=1\n)\nmodel.save(\"/kaggle/working/model_final_aptos.keras\")\nprint(\"Final model saved ✓\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-24T16:15:35.607866Z","iopub.execute_input":"2026-03-24T16:15:35.608296Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 12 — Evaluate (accuracy, F1, confusion matrix)","metadata":{}},{"cell_type":"code","source":"acc      = history1.history[\"accuracy\"] + history2.history[\"accuracy\"] + history_aptos.history[\"accuracy\"]\nval_acc  = history1.history[\"val_accuracy\"] + history2.history[\"val_accuracy\"] + history_aptos.history[\"val_accuracy\"]\nloss     = history1.history[\"loss\"] + history2.history[\"loss\"] + history_aptos.history[\"loss\"]\nval_loss = history1.history[\"val_loss\"] + history2.history[\"val_loss\"] + history_aptos.history[\"val_loss\"]\nb1 = len(history1.history[\"accuracy\"])\nb2 = b1 + len(history2.history[\"accuracy\"])\nepochs_range = range(1, len(acc) + 1)\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\nfor ax, m, vm, title in [(ax1, acc, val_acc, \"Accuracy\"), (ax2, loss, val_loss, \"Loss\")]:\n    ax.plot(epochs_range, m,  \"b-\", label=f\"Train {title}\")\n    ax.plot(epochs_range, vm, \"r-\", label=f\"Val {title}\")\n    ax.axvline(x=b1, color=\"gray\",   linestyle=\"--\", label=\"EyePACS fine-tune start\")\n    ax.axvline(x=b2, color=\"purple\", linestyle=\"--\", label=\"APTOS fine-tune start\")\n    ax.set_xlabel(\"Epoch\"); ax.legend(fontsize=9)\n    ax.set_title(f\"{title}\")\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/training_curves.png\", dpi=150); plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 13 — Grad-CAM visualization","metadata":{}},{"cell_type":"code","source":"aptos_val_gen.reset()\ny_prob = model.predict(aptos_val_gen, verbose=1)\ny_pred = np.argmax(y_prob, axis=1)\ny_true = aptos_val_gen.classes\n\nprint(f\"Accuracy : {accuracy_score(y_true, y_pred):.4f}\")\nprint(f\"F1 Score : {f1_score(y_true, y_pred, average='weighted'):.4f}\")\nprint(classification_report(y_true, y_pred, target_names=list(CLASS_NAMES.values())))\n\ncm = confusion_matrix(y_true, y_pred)\nplt.figure(figsize=(8, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n            xticklabels=CLASS_NAMES.values(), yticklabels=CLASS_NAMES.values())\nplt.title(\"Confusion Matrix — DenseNet121\")\nplt.xlabel(\"Predicted\"); plt.ylabel(\"Actual\")\nplt.tight_layout()\nplt.savefig(\"/kaggle/working/confusion_matrix.png\", dpi=150); plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"def get_gradcam(model, img_batch, class_idx):\n    last_conv = next(l.name for l in reversed(model.layers) if len(l.output.shape) == 4)\n    grad_model = tf.keras.models.Model(\n        inputs=model.inputs,\n        outputs=[model.get_layer(last_conv).output, model.output]\n    )\n    with tf.GradientTape() as tape:\n        conv_out, preds = grad_model(img_batch)\n        score = preds[:, class_idx]\n    grads   = tape.gradient(score, conv_out)\n    pooled  = tf.reduce_mean(grads, axis=(0,1,2))\n    heatmap = tf.squeeze(conv_out[0] @ pooled[..., tf.newaxis])\n    heatmap = tf.maximum(heatmap, 0) / (tf.reduce_max(heatmap) + 1e-8)\n    return heatmap.numpy()\n\ndef show_gradcam(image_path, preprocess_fn, true_label=None):\n    img_bgr = cv2.imread(image_path)\n    img_rgb = cv2.resize(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB), (IMG_SIZE, IMG_SIZE))\n    inp     = preprocess_fn(img_rgb.astype(\"float32\"))\n    batch   = np.expand_dims(inp, 0)\n    preds   = model.predict(batch, verbose=0)[0]\n    cls     = int(np.argmax(preds))\n    conf    = preds[cls]\n    heatmap    = get_gradcam(model, batch, cls)\n    hm_resized = cv2.resize(heatmap, (IMG_SIZE, IMG_SIZE))\n    hm_colored = cv2.cvtColor(cv2.applyColorMap(np.uint8(255*hm_resized), cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB)\n    overlay    = cv2.addWeighted(img_rgb, 0.6, hm_colored, 0.4, 0)\n    fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n    fig.suptitle(f\"Predicted: {CLASS_NAMES[cls]} ({conf:.1%})\"\n                 + (f\" | True: {CLASS_NAMES[int(true_label)]}\" if true_label is not None else \"\"))\n    axes[0].imshow(img_rgb);             axes[0].set_title(\"Original\")\n    axes[1].imshow(heatmap, cmap=\"jet\"); axes[1].set_title(\"Grad-CAM\")\n    axes[2].imshow(overlay);             axes[2].set_title(\"Overlay\")\n    for ax in axes: ax.axis(\"off\")\n    plt.tight_layout(); plt.show()\n\nfor i in range(5):\n    row = aptos_val_df.iloc[i]\n    show_gradcam(row[\"image_path\"], dn_pre.preprocess_input,\n                 true_label=row[\"diagnosis\"])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cell 14 — Download your model","metadata":{}},{"cell_type":"code","source":"for f in os.listdir(\"/kaggle/working\"):\n    if f.endswith((\".keras\", \".csv\", \".png\")):\n        size = os.path.getsize(f\"/kaggle/working/{f}\") / 1e6\n        print(f\"{f:<50} {size:.1f} MB\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}