{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"12e287b1","cell_type":"markdown","source":"# EfficientNetB6 — Diabetic Retinopathy (Blindness) Detection\n\nBinary classification: **No DR (0)** vs **Has DR (1-4)**, using **EfficientNetB6**\ntransfer learning. Same task/dataset/split logic as the B0 and B4 notebooks,\nupgraded to B6's recommended input size (528x528).\n\n**Target accuracy: 90%+**\n\n### What's different from B0/B4\n- **IMG_SIZE = 528** (B6's official recommended resolution)\n- **BATCH_SIZE = 8** (dropped further) — B6 has ~43M parameters and 528px images use\n  significantly more GPU memory than B4's 380px. If you still hit an\n  \"Out of Memory\" / \"ResourceExhausted\" error on Kaggle's T4, drop `BATCH_SIZE` to\n  4, or reduce `IMG_SIZE` to 456 (B5's resolution) as a fallback — both are noted\n  again right above the relevant cell below.\n- Everything else (no manual rescale, 70/15/15 split, light augmentation,\n  crash-proof checkpoint, 300-sample report) is identical to the B4 notebook.\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.\n4. Expect this to be the slowest run yet — budget extra time/GPU-hours.\n","metadata":{}},{"id":"6f54bccc","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 EfficientNetB6\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 = 528          # EfficientNetB6's recommended input resolution\nBATCH_SIZE = 8          # dropped further -- B6 + 528px images are memory-heavy\nEPOCHS_HEAD = 6         # phase 1: frozen base, train head only\nEPOCHS_FINE_TUNE = 25   # phase 2: fine-tuning\nREPORT_SAMPLE_SIZE = 300\nMODEL_NAME = \"EfficientNetB6\"\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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:13.846123Z","iopub.execute_input":"2026-06-22T11:39:13.846478Z","iopub.status.idle":"2026-06-22T11:39:32.717919Z","shell.execute_reply.started":"2026-06-22T11:39:13.846447Z","shell.execute_reply":"2026-06-22T11:39:32.717201Z"}},"outputs":[],"execution_count":null},{"id":"a64dad86","cell_type":"markdown","source":"## Step 1 — Load data and labels","metadata":{}},{"id":"6474a532","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\"))\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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:32.719467Z","iopub.execute_input":"2026-06-22T11:39:32.719970Z","iopub.status.idle":"2026-06-22T11:39:32.771673Z","shell.execute_reply.started":"2026-06-22T11:39:32.719945Z","shell.execute_reply":"2026-06-22T11:39:32.770935Z"}},"outputs":[],"execution_count":null},{"id":"39d0e3d8","cell_type":"markdown","source":"## Step 2 — Split: Train (70%) / Validation (15%) / Test (15%)","metadata":{}},{"id":"eba4a0d0","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:32.772536Z","iopub.execute_input":"2026-06-22T11:39:32.772783Z","iopub.status.idle":"2026-06-22T11:39:32.821931Z","shell.execute_reply.started":"2026-06-22T11:39:32.772761Z","shell.execute_reply":"2026-06-22T11:39:32.821356Z"}},"outputs":[],"execution_count":null},{"id":"1f4a2a62","cell_type":"markdown","source":"## Step 3 — Data generators (light augmentation, no manual rescale)\n\nSame fix as before: **no `rescale=1./255`** — EfficientNetB6 also expects raw\n[0,255] pixel values and normalizes internally.","metadata":{}},{"id":"ca33c3e7","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()\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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:32.822763Z","iopub.execute_input":"2026-06-22T11:39:32.823038Z","iopub.status.idle":"2026-06-22T11:39:39.425057Z","shell.execute_reply.started":"2026-06-22T11:39:32.823006Z","shell.execute_reply":"2026-06-22T11:39:39.424264Z"}},"outputs":[],"execution_count":null},{"id":"2f50dc51","cell_type":"markdown","source":"## Step 4 — Build EfficientNetB6 model (transfer learning)\n\n**If this cell throws an OOM / ResourceExhausted error:** go back to the config\ncell above and either drop `BATCH_SIZE` to `4`, or drop `IMG_SIZE` to `456`\n(EfficientNetB5's resolution) and re-run from the top.","metadata":{}},{"id":"220ba64d","cell_type":"code","source":"base_model = EfficientNetB6(\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=\"EfficientNetB6_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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:39.426916Z","iopub.execute_input":"2026-06-22T11:39:39.427172Z","iopub.status.idle":"2026-06-22T11:39:47.236619Z","shell.execute_reply.started":"2026-06-22T11:39:39.427150Z","shell.execute_reply":"2026-06-22T11:39:47.235767Z"}},"outputs":[],"execution_count":null},{"id":"9a1247ff","cell_type":"markdown","source":"## Step 5 — The save fix: crash-proof checkpoint + progress logger\n\nSame protection as the B4 notebook: a checkpoint is written after **every**\nepoch (not just on improvement), plus a running JSON log of metrics, so a\ndisconnect mid-training doesn't cost you the whole run.","metadata":{}},{"id":"20dda5c1","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:47.237592Z","iopub.execute_input":"2026-06-22T11:39:47.238288Z","iopub.status.idle":"2026-06-22T11:39:47.245820Z","shell.execute_reply.started":"2026-06-22T11:39:47.238256Z","shell.execute_reply":"2026-06-22T11:39:47.245102Z"}},"outputs":[],"execution_count":null},{"id":"9630c838","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.","metadata":{}},{"id":"b7320a87","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T11:39:47.246904Z","iopub.execute_input":"2026-06-22T11:39:47.247304Z","iopub.status.idle":"2026-06-22T12:27:04.307063Z","shell.execute_reply.started":"2026-06-22T11:39:47.247280Z","shell.execute_reply":"2026-06-22T12:27:04.306457Z"}},"outputs":[],"execution_count":null},{"id":"206172c4","cell_type":"code","source":"print(\"=\" * 70)\nprint(\"PHASE 2: Fine-tuning (unfreezing top layers of EfficientNetB6)\")\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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T12:27:04.308044Z","iopub.execute_input":"2026-06-22T12:27:04.308399Z","iopub.status.idle":"2026-06-22T14:32:33.739334Z","shell.execute_reply.started":"2026-06-22T12:27:04.308364Z","shell.execute_reply":"2026-06-22T14:32:33.738640Z"}},"outputs":[],"execution_count":null},{"id":"8a28aa75","cell_type":"markdown","source":"## Step 7 — Training curves (loss & accuracy, train vs validation)","metadata":{}},{"id":"e738abad","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T14:32:33.740374Z","iopub.execute_input":"2026-06-22T14:32:33.740743Z","iopub.status.idle":"2026-06-22T14:32:34.429496Z","shell.execute_reply.started":"2026-06-22T14:32:33.740711Z","shell.execute_reply":"2026-06-22T14:32:34.428592Z"}},"outputs":[],"execution_count":null},{"id":"baab7943","cell_type":"markdown","source":"## Step 8 — Evaluation report (test set sample, support = 300)","metadata":{}},{"id":"dceca534","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T14:32:34.430475Z","iopub.execute_input":"2026-06-22T14:32:34.430779Z","iopub.status.idle":"2026-06-22T14:34:05.739956Z","shell.execute_reply.started":"2026-06-22T14:32:34.430756Z","shell.execute_reply":"2026-06-22T14:34:05.739290Z"}},"outputs":[],"execution_count":null},{"id":"c3a715ec","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T14:34:05.740827Z","iopub.execute_input":"2026-06-22T14:34:05.741166Z","iopub.status.idle":"2026-06-22T14:35:20.888615Z","shell.execute_reply.started":"2026-06-22T14:34:05.741121Z","shell.execute_reply":"2026-06-22T14:35:20.887847Z"}},"outputs":[],"execution_count":null},{"id":"3456f56a","cell_type":"markdown","source":"## Step 9 — Confusion matrix","metadata":{}},{"id":"905748eb","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T14:35:20.889544Z","iopub.execute_input":"2026-06-22T14:35:20.889871Z","iopub.status.idle":"2026-06-22T14:35:21.123073Z","shell.execute_reply.started":"2026-06-22T14:35:20.889833Z","shell.execute_reply":"2026-06-22T14:35:21.122340Z"}},"outputs":[],"execution_count":null},{"id":"f84c7b9f","cell_type":"markdown","source":"## Step 10 — Save the final model","metadata":{}},{"id":"4ee70377","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-22T14:35:21.124707Z","iopub.execute_input":"2026-06-22T14:35:21.125556Z","iopub.status.idle":"2026-06-22T14:35:23.688412Z","shell.execute_reply.started":"2026-06-22T14:35:21.125521Z","shell.execute_reply":"2026-06-22T14:35:23.687747Z"}},"outputs":[],"execution_count":null},{"id":"8f73e4c4-63bd-40cf-b041-a2793c208952","cell_type":"code","source":"from IPython.display import FileLink\n\n# Using the exact path printed in your output\nmodel_path = '/kaggle/working/output/EfficientNetB6_blindness_model.keras'\n\n# This will generate a clickable link in the cell output\ndisplay(FileLink(model_path))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}