{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"5a8e5c3b","cell_type":"markdown","source":"# EfficientNetV2-S — Diabetic Retinopathy (Blindness) Detection\n\nBinary classification: **No DR (0)** vs **Has DR (1-4)**, using **EfficientNetV2-S**\ntransfer learning. Same task/dataset/split logic as the B0/B4/B6 notebooks.\n\n**Target accuracy: 90%+**\n\n### Notes specific to EfficientNetV2-S\n- **IMG_SIZE = 384** — EfficientNetV2-S's official recommended input resolution\n- **BATCH_SIZE = 16** — V2-S has ~24M parameters (between B4's 19M and B6's 41M), so this batch size is a reasonable starting point for a T4 GPU\n- **No manual rescale** — confirmed via Keras's own documentation: EfficientNetV2 models also have a built-in Rescaling layer and expect raw pixel values in **[0, 255]**, exactly like B0/B4/B6. Manually dividing by 255 here would cause the same \"stuck at ~50% accuracy\" bug from before.\n- **The save fix is included** — a checkpoint is written after every single epoch (not just on improvement), plus a live JSON progress log, so a Kaggle disconnect mid-training won't cost you the whole run.\n\n### Before running\n1. Dataset already added via Competitions tab — no change needed.\n2. GPU: **Session options → Accelerator → GPU T4 x2**.\n3. **Use \"Save Version\" → \"Save & Run All (Commit)\"**, not interactive Run.\n","metadata":{}},{"id":"ab4faa56","cell_type":"code","source":"import os\nimport json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow.keras.applications import EfficientNetV2S\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint, Callback\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, roc_auc_score\n\nprint(\"TensorFlow version:\", tf.__version__)\nprint(\"GPUs available:\", tf.config.list_physical_devices('GPU'))\n\nSEED = 42\nIMG_SIZE = 384          # EfficientNetV2-S's recommended input resolution\nBATCH_SIZE = 16\nEPOCHS_HEAD = 6         # phase 1: frozen base, train head only\nEPOCHS_FINE_TUNE = 25   # phase 2: fine-tuning\nREPORT_SAMPLE_SIZE = 300\nMODEL_NAME = \"EfficientNetV2S\"\n\nOUTPUT_DIR = \"/kaggle/working/output\"\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"fbc3d759","cell_type":"markdown","source":"## Step 1 — Load data and labels","metadata":{}},{"id":"1f45fc78","cell_type":"code","source":"DATA_DIR = \"/kaggle/input/competitions/aptos2019-blindness-detection\"\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train_images\")\n\ndf = pd.read_csv(TRAIN_CSV)\ndf[\"filepath\"] = df[\"id_code\"].apply(lambda x: os.path.join(TRAIN_IMG_DIR, f\"{x}.png\"))\n\n# Binary target: 0 = No DR, 1 = Has DR (severity 1-4 combined)\ndf[\"has_dr\"] = (df[\"diagnosis\"] > 0).astype(int).astype(str)\n\nprint(\"Total images:\", len(df))\nprint(df[\"has_dr\"].value_counts())\ndf.head()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"532bbf75","cell_type":"markdown","source":"## Step 2 — Split: Train (70%) / Validation (15%) / Test (15%)","metadata":{}},{"id":"c272ca0a","cell_type":"code","source":"train_df, temp_df = train_test_split(\n    df, test_size=0.30, stratify=df[\"has_dr\"], random_state=SEED\n)\nval_df, test_df = train_test_split(\n    temp_df, test_size=0.50, stratify=temp_df[\"has_dr\"], random_state=SEED\n)\n\nprint(f\"Train: {len(train_df)} ({len(train_df)/len(df)*100:.1f}%)\")\nprint(f\"Val:   {len(val_df)} ({len(val_df)/len(df)*100:.1f}%)\")\nprint(f\"Test:  {len(test_df)} ({len(test_df)/len(df)*100:.1f}%)\")\n\nprint(\"\\nTrain class balance:\\n\", train_df[\"has_dr\"].value_counts())\nprint(\"\\nVal class balance:\\n\", val_df[\"has_dr\"].value_counts())\nprint(\"\\nTest class balance:\\n\", test_df[\"has_dr\"].value_counts())\n","metadata":{},"outputs":[],"execution_count":null},{"id":"5c75ba02","cell_type":"markdown","source":"## Step 3 — Data generators (light augmentation, no manual rescale)\n\n**No `rescale=1./255`** — EfficientNetV2-S expects raw [0,255] pixel values\nand normalizes them internally via its own built-in Rescaling layer.","metadata":{}},{"id":"89b4e27f","cell_type":"code","source":"train_aug = ImageDataGenerator(\n    rotation_range=10,\n    zoom_range=0.08,\n    horizontal_flip=True,\n    width_shift_range=0.05,\n    height_shift_range=0.05,\n)\nplain_aug = ImageDataGenerator()  # val/test: no augmentation, no manual rescale\n\ntrain_gen = train_aug.flow_from_dataframe(\n    train_df, x_col=\"filepath\", y_col=\"has_dr\",\n    target_size=(IMG_SIZE, IMG_SIZE), class_mode=\"binary\",\n    batch_size=BATCH_SIZE, seed=SEED,\n)\nval_gen = plain_aug.flow_from_dataframe(\n    val_df, x_col=\"filepath\", y_col=\"has_dr\",\n    target_size=(IMG_SIZE, IMG_SIZE), class_mode=\"binary\",\n    batch_size=BATCH_SIZE, shuffle=False,\n)\ntest_gen = plain_aug.flow_from_dataframe(\n    test_df, x_col=\"filepath\", y_col=\"has_dr\",\n    target_size=(IMG_SIZE, IMG_SIZE), class_mode=\"binary\",\n    batch_size=BATCH_SIZE, shuffle=False,\n)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"6a5e3b08","cell_type":"markdown","source":"## Step 4 — Build EfficientNetV2-S model (transfer learning)\n\n**If this cell throws an OOM / ResourceExhausted error:** drop `BATCH_SIZE`\nto 8 in the config cell above and re-run from the top.","metadata":{}},{"id":"b66c7cc4","cell_type":"code","source":"base_model = EfficientNetV2S(\n    weights=\"imagenet\", include_top=False, input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\nbase_model.trainable = False  # freeze for phase 1\n\nx = GlobalAveragePooling2D()(base_model.output)\nx = Dense(128, activation=\"relu\")(x)\nx = Dropout(0.4)(x)\noutput = Dense(1, activation=\"sigmoid\")(x)\n\nmodel = Model(inputs=base_model.input, outputs=output, name=\"EfficientNetV2S_BlindnessDetector\")\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"],\n)\nmodel.summary()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"45fc36f9","cell_type":"markdown","source":"## Step 5 — The save fix: crash-proof checkpoint + progress logger\n\nSame protection as the B4/B6 notebooks: a checkpoint after every epoch (not\njust on improvement), plus a running JSON log of metrics on disk.","metadata":{}},{"id":"9738840e","cell_type":"code","source":"class CrashProofCheckpoint(Callback):\n    def __init__(self, output_dir, model_name):\n        super().__init__()\n        self.output_dir = output_dir\n        self.model_name = model_name\n        self.progress_path = os.path.join(output_dir, \"training_progress.json\")\n        self.history_log = []\n        if os.path.exists(self.progress_path):\n            with open(self.progress_path) as f:\n                self.history_log = json.load(f)\n\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        latest_path = os.path.join(self.output_dir, f\"{self.model_name}_latest_checkpoint.keras\")\n        self.model.save(latest_path)\n\n        entry = {\"epoch\": epoch + 1, **{k: float(v) for k, v in logs.items()}}\n        self.history_log.append(entry)\n        with open(self.progress_path, \"w\") as f:\n            json.dump(self.history_log, f, indent=2)\n\n        print(f\"[CrashProofCheckpoint] Saved latest checkpoint + logged epoch {epoch + 1}\")\n\n\ncrash_proof_cb = CrashProofCheckpoint(OUTPUT_DIR, MODEL_NAME)\n\ncallbacks = [\n    EarlyStopping(monitor=\"val_accuracy\", patience=5, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=3, min_lr=1e-7),\n    ModelCheckpoint(\n        os.path.join(OUTPUT_DIR, f\"{MODEL_NAME}_best_checkpoint.keras\"),\n        monitor=\"val_accuracy\", save_best_only=True,\n    ),\n    crash_proof_cb,\n]\n","metadata":{},"outputs":[],"execution_count":null},{"id":"6d2765b2","cell_type":"markdown","source":"## Step 6 — Train: Phase 1 (frozen base) then Phase 2 (fine-tuning)\n\nUse **Save Version → Save & Run All (Commit)** from here on, so the run\nsurvives even if your connection drops.","metadata":{}},{"id":"ad367a8f","cell_type":"code","source":"print(\"=\" * 70)\nprint(\"PHASE 1: Training head (base frozen)\")\nprint(\"=\" * 70)\nhistory1 = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS_HEAD,\n    callbacks=callbacks,\n    verbose=1,\n)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"f027d5f9","cell_type":"code","source":"print(\"=\" * 70)\nprint(\"PHASE 2: Fine-tuning (unfreezing top layers of EfficientNetV2-S)\")\nprint(\"=\" * 70)\n\nbase_model.trainable = True\nfine_tune_at = int(len(base_model.layers) * 0.8)\nfor layer in base_model.layers[:fine_tune_at]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"],\n)\n\nhistory2 = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS_FINE_TUNE,\n    callbacks=callbacks,\n    verbose=1,\n)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"d8b7de06","cell_type":"markdown","source":"## Step 7 — Training curves (loss & accuracy, train vs validation)","metadata":{}},{"id":"405d52ff","cell_type":"code","source":"def combine_history(h1, h2, key):\n    return h1.history[key] + h2.history[key]\n\nacc = combine_history(history1, history2, \"accuracy\")\nval_acc = combine_history(history1, history2, \"val_accuracy\")\nloss = combine_history(history1, history2, \"loss\")\nval_loss = combine_history(history1, history2, \"val_loss\")\nepochs_range = range(1, len(acc) + 1)\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\naxes[0].plot(epochs_range, acc, label=\"Train Accuracy\", marker=\"o\")\naxes[0].plot(epochs_range, val_acc, label=\"Validation Accuracy\", marker=\"o\")\naxes[0].axvline(x=EPOCHS_HEAD, color=\"gray\", linestyle=\"--\", label=\"Fine-tuning starts\")\naxes[0].set_title(f\"{MODEL_NAME} Accuracy: Train vs Validation\")\naxes[0].set_xlabel(\"Epoch\")\naxes[0].set_ylabel(\"Accuracy\")\naxes[0].legend()\naxes[0].grid(alpha=0.3)\n\naxes[1].plot(epochs_range, loss, label=\"Train Loss\", marker=\"o\")\naxes[1].plot(epochs_range, val_loss, label=\"Validation Loss\", marker=\"o\")\naxes[1].axvline(x=EPOCHS_HEAD, color=\"gray\", linestyle=\"--\", label=\"Fine-tuning starts\")\naxes[1].set_title(f\"{MODEL_NAME} Loss: Train vs Validation\")\naxes[1].set_xlabel(\"Epoch\")\naxes[1].set_ylabel(\"Loss\")\naxes[1].legend()\naxes[1].grid(alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(os.path.join(OUTPUT_DIR, f\"{MODEL_NAME}_training_curves.png\"), dpi=150)\nplt.show()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"d58e7572","cell_type":"markdown","source":"## Step 8 — Evaluation report (test set sample, support = 300)","metadata":{}},{"id":"9f2dfc22","cell_type":"code","source":"test_loss, test_acc = model.evaluate(test_gen, verbose=0)\nprint(f\"Full test set ({len(test_df)} images) -> Loss: {test_loss:.4f}  Accuracy: {test_acc:.4f}\")\n","metadata":{},"outputs":[],"execution_count":null},{"id":"e10055d9","cell_type":"code","source":"report_df, _ = train_test_split(\n    test_df, train_size=REPORT_SAMPLE_SIZE, stratify=test_df[\"has_dr\"], random_state=SEED\n)\nprint(\"Report sample size:\", len(report_df))\nprint(report_df[\"has_dr\"].value_counts())\n\nreport_gen = plain_aug.flow_from_dataframe(\n    report_df, x_col=\"filepath\", y_col=\"has_dr\",\n    target_size=(IMG_SIZE, IMG_SIZE), class_mode=\"binary\",\n    batch_size=BATCH_SIZE, shuffle=False,\n)\n\ny_true = report_df[\"has_dr\"].astype(int).values\ny_probs = model.predict(report_gen, verbose=0).ravel()\ny_pred = (y_probs > 0.5).astype(int)\n\noverall_acc = accuracy_score(y_true, y_pred)\noverall_auc = roc_auc_score(y_true, y_probs)\nprint(f\"\\nReport-sample Accuracy: {overall_acc:.4f}\")\nprint(f\"Report-sample AUC: {overall_auc:.4f}\")\n\nprint(\"\\nClassification Report (support should total 300):\\n\")\nprint(classification_report(y_true, y_pred, target_names=[\"No DR\", \"Has DR\"]))\n","metadata":{},"outputs":[],"execution_count":null},{"id":"ece29059","cell_type":"markdown","source":"## Step 9 — Confusion matrix","metadata":{}},{"id":"8b595e10","cell_type":"code","source":"cm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(6, 5))\nsns.heatmap(\n    cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n    xticklabels=[\"No DR\", \"Has DR\"], yticklabels=[\"No DR\", \"Has DR\"],\n)\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.title(f\"{MODEL_NAME} Confusion Matrix (test sample, n=300)\")\nplt.savefig(os.path.join(OUTPUT_DIR, f\"{MODEL_NAME}_confusion_matrix.png\"), dpi=150)\nplt.show()\n\nprint(cm)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"5868e6e5","cell_type":"markdown","source":"## Step 10 — Save the final model","metadata":{}},{"id":"f5ee0bc4","cell_type":"code","source":"final_model_path = os.path.join(OUTPUT_DIR, f\"{MODEL_NAME}_blindness_model.keras\")\nmodel.save(final_model_path)\n\nfinal_results = {\n    \"model_name\": MODEL_NAME,\n    \"img_size\": IMG_SIZE,\n    \"full_test_accuracy\": round(float(test_acc), 4),\n    \"full_test_loss\": round(float(test_loss), 4),\n    \"report_sample_accuracy\": round(float(overall_acc), 4),\n    \"report_sample_auc\": round(float(overall_auc), 4),\n    \"report_sample_size\": len(report_df),\n    \"confusion_matrix\": cm.tolist(),\n}\nwith open(os.path.join(OUTPUT_DIR, f\"{MODEL_NAME}_results_summary.json\"), \"w\") as f:\n    json.dump(final_results, f, indent=2)\n\nprint(f\"Saved final model to {final_model_path}\")\nprint(f\"Saved results summary to {OUTPUT_DIR}/{MODEL_NAME}_results_summary.json\")\nprint()\nprint(\"FINAL ACCURACY (full test set):\", round(float(test_acc) * 100, 2), \"%\")\nif test_acc >= 0.90:\n    print(\"Target of 90%+ accuracy reached.\")\nelse:\n    print(\"Below 90% target. To improve: raise EPOCHS_FINE_TUNE, unfreeze more layers\")\n    print(\"(lower the 0.8 fraction in fine_tune_at), or check class balance.\")\n","metadata":{},"outputs":[],"execution_count":null}]}