{"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":"5050b5e9","cell_type":"markdown","source":"# EfficientNetB0 — Diabetic Retinopathy (Blindness) Detection\n\nBinary classification: **No DR (0)** vs **Has DR (1-4)**, trained on the\nAPTOS 2019 Blindness Detection dataset using **EfficientNetB0** transfer learning.\n\n**Target accuracy: 90%+**\n\n### Split strategy\n- **Train: 70%** | **Validation: 15%** | **Test: 15%** of the full dataset (stratified)\n- For the final evaluation report specifically, a **stratified sample of exactly 300**\n  images is drawn from the test set, so the classification report's support column\n  totals exactly 300 as requested. (The full 15% test set is still used to validate\n  the model is not overfit — 300 is just the report sample.)\n\n### Before running\n1. Make sure you've added the dataset via **+ Add Input → Competitions tab → aptos2019-blindness-detection** (and accepted the competition rules on its page first).\n2. Turn on GPU: **Session options → Accelerator → GPU T4 x2**.\n3. Run all cells top to bottom.\n","metadata":{}},{"id":"c198d860","cell_type":"code","source":"import os\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 EfficientNetB0\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\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 = 224\nBATCH_SIZE = 32\nEPOCHS_HEAD = 6        # phase 1: frozen base, train head only\nEPOCHS_FINE_TUNE = 25  # phase 2: fine-tuning (raise this further if accuracy < 90%)\nREPORT_SAMPLE_SIZE = 300\n\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T19:50:05.135942Z","iopub.execute_input":"2026-06-21T19:50:05.136385Z","iopub.status.idle":"2026-06-21T19:50:09.454640Z","shell.execute_reply.started":"2026-06-21T19:50:05.136361Z","shell.execute_reply":"2026-06-21T19:50:09.453922Z"}},"outputs":[],"execution_count":null},{"id":"901c24a6","cell_type":"markdown","source":"## Step 1 — Load data and labels","metadata":{}},{"id":"7606d648","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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T19:50:09.455518Z","iopub.execute_input":"2026-06-21T19:50:09.456190Z","iopub.status.idle":"2026-06-21T19:50:09.481919Z","shell.execute_reply.started":"2026-06-21T19:50:09.456162Z","shell.execute_reply":"2026-06-21T19:50:09.481307Z"}},"outputs":[],"execution_count":null},{"id":"4f78959e","cell_type":"markdown","source":"## Step 2 — Split: Train (70%) / Validation (15%) / Test (15%)","metadata":{}},{"id":"bf49b90c","cell_type":"code","source":"# First split off train (70%) vs everything else (30%)\ntrain_df, temp_df = train_test_split(\n    df, test_size=0.30, stratify=df[\"has_dr\"], random_state=SEED\n)\n\n# Split the remaining 30% evenly into validation (15%) and test (15%)\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-21T19:50:09.482730Z","iopub.execute_input":"2026-06-21T19:50:09.482987Z","iopub.status.idle":"2026-06-21T19:50:09.501036Z","shell.execute_reply.started":"2026-06-21T19:50:09.482955Z","shell.execute_reply":"2026-06-21T19:50:09.500293Z"}},"outputs":[],"execution_count":null},{"id":"e272f638","cell_type":"markdown","source":"## Step 3 — Data generators (light augmentation)\n\nOnly mild augmentation is used here — small rotations, slight zoom, and\nhorizontal flips. This is deliberately gentle (vs heavier augmentation) since\nretina images are mostly centered and consistent, and heavy distortion can hurt\nthis particular dataset more than it helps.","metadata":{}},{"id":"4a5c28a9","cell_type":"code","source":"# NOTE: No rescale=1./255 here. EfficientNetB0 has a built-in Rescaling +\n# Normalization layer and expects raw pixel values in [0, 255]. Manually\n# rescaling here would divide pixel values by 255 twice, destroying the signal.\ntrain_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()  # for 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":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T19:50:09.502004Z","iopub.execute_input":"2026-06-21T19:50:09.502323Z","iopub.status.idle":"2026-06-21T19:50:11.667537Z","shell.execute_reply.started":"2026-06-21T19:50:09.502285Z","shell.execute_reply":"2026-06-21T19:50:11.666965Z"}},"outputs":[],"execution_count":null},{"id":"6a1c82dd","cell_type":"markdown","source":"## Step 4 — Build EfficientNetB0 model (transfer learning)","metadata":{}},{"id":"642aa203","cell_type":"code","source":"base_model = EfficientNetB0(\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=\"EfficientNetB0_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-21T19:50:11.668438Z","iopub.execute_input":"2026-06-21T19:50:11.668714Z","iopub.status.idle":"2026-06-21T19:50:14.541650Z","shell.execute_reply.started":"2026-06-21T19:50:11.668676Z","shell.execute_reply":"2026-06-21T19:50:14.541081Z"}},"outputs":[],"execution_count":null},{"id":"513c29ae","cell_type":"markdown","source":"## Step 5 — Train: Phase 1 (frozen base) then Phase 2 (fine-tuning)\n\nTraining prints **loss and accuracy for both train and validation after every epoch**\n(this is the `verbose=1` default Keras output — no extra code needed, it's built in).","metadata":{}},{"id":"ec32b5fa","cell_type":"code","source":"callbacks = [\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(\"/kaggle/working/best_model.keras\", monitor=\"val_accuracy\", save_best_only=True),\n]\n\nprint(\"=\" * 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-21T19:50:14.542607Z","iopub.execute_input":"2026-06-21T19:50:14.542824Z","iopub.status.idle":"2026-06-21T20:22:27.795167Z","shell.execute_reply.started":"2026-06-21T19:50:14.542803Z","shell.execute_reply":"2026-06-21T20:22:27.794476Z"}},"outputs":[],"execution_count":null},{"id":"f87d043d","cell_type":"code","source":"print(\"=\" * 70)\nprint(\"PHASE 2: Fine-tuning (unfreezing top layers of EfficientNetB0)\")\nprint(\"=\" * 70)\n\nbase_model.trainable = True\n# Keep the earliest ~80% of layers frozen, fine-tune only the later layers\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-21T20:22:27.796181Z","iopub.execute_input":"2026-06-21T20:22:27.796556Z","iopub.status.idle":"2026-06-21T20:49:24.196008Z","shell.execute_reply.started":"2026-06-21T20:22:27.796520Z","shell.execute_reply":"2026-06-21T20:49:24.195272Z"}},"outputs":[],"execution_count":null},{"id":"5775bffe","cell_type":"markdown","source":"## Step 6 — Training curves (loss & accuracy, train vs validation)","metadata":{}},{"id":"091502fc","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(\"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(\"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(\"/kaggle/working/training_curves.png\", dpi=150)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T20:49:24.197151Z","iopub.execute_input":"2026-06-21T20:49:24.197468Z","iopub.status.idle":"2026-06-21T20:49:24.869970Z","shell.execute_reply.started":"2026-06-21T20:49:24.197445Z","shell.execute_reply":"2026-06-21T20:49:24.868952Z"}},"outputs":[],"execution_count":null},{"id":"228b80ac","cell_type":"markdown","source":"## Step 7 — Evaluation report (test set sample, support = 300)\n\nWe evaluate on the full 15% test set first to confirm real-world performance,\nthen draw a **stratified 300-image sample** from it specifically for the\nclassification report / confusion matrix, so the report's support total is 300.","metadata":{}},{"id":"85ef5e14","cell_type":"code","source":"# Full test set accuracy (sanity check on all ~15% of data)\ntest_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-21T20:49:24.872191Z","iopub.execute_input":"2026-06-21T20:49:24.872401Z","iopub.status.idle":"2026-06-21T20:50:30.783428Z","shell.execute_reply.started":"2026-06-21T20:49:24.872382Z","shell.execute_reply":"2026-06-21T20:50:30.782410Z"}},"outputs":[],"execution_count":null},{"id":"c60b9b34","cell_type":"code","source":"# Stratified 300-image sample from the test set, for the formal report\nreport_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-21T20:50:30.784452Z","iopub.execute_input":"2026-06-21T20:50:30.784662Z","iopub.status.idle":"2026-06-21T20:51:21.852855Z","shell.execute_reply.started":"2026-06-21T20:50:30.784642Z","shell.execute_reply":"2026-06-21T20:51:21.852114Z"}},"outputs":[],"execution_count":null},{"id":"9e4b8563","cell_type":"markdown","source":"## Step 8 — Confusion matrix","metadata":{}},{"id":"d5d23230","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(\"Confusion Matrix (test sample, n=300)\")\nplt.savefig(\"/kaggle/working/confusion_matrix.png\", dpi=150)\nplt.show()\n\nprint(cm)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T20:51:21.854042Z","iopub.execute_input":"2026-06-21T20:51:21.854460Z","iopub.status.idle":"2026-06-21T20:51:22.087599Z","shell.execute_reply.started":"2026-06-21T20:51:21.854380Z","shell.execute_reply":"2026-06-21T20:51:22.086856Z"}},"outputs":[],"execution_count":null},{"id":"7165f45f","cell_type":"markdown","source":"## Step 9 — Save the final model\n\nSaved into `/kaggle/working/output/` — download this from the notebook's\n**Output** panel (right sidebar) once the run finishes.","metadata":{}},{"id":"43ce9225","cell_type":"code","source":"os.makedirs(\"/kaggle/working/output\", exist_ok=True)\n\nmodel.save(\"/kaggle/working/output/efficientnetb0_blindness_model.keras\")\n\nimport json\nfinal_results = {\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(\"/kaggle/working/output/results_summary.json\", \"w\") as f:\n    json.dump(final_results, f, indent=2)\n\nprint(\"Saved model to /kaggle/working/output/efficientnetb0_blindness_model.keras\")\nprint(\"Saved results summary to /kaggle/working/output/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 try IMG_SIZE = 300.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-21T20:51:22.088496Z","iopub.execute_input":"2026-06-21T20:51:22.088816Z","iopub.status.idle":"2026-06-21T20:51:22.848805Z","shell.execute_reply.started":"2026-06-21T20:51:22.088762Z","shell.execute_reply":"2026-06-21T20:51:22.847926Z"}},"outputs":[],"execution_count":null}]}