{"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":10338,"databundleVersionId":862042,"isSourceIdPinned":false}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# EfficientNetB2 on RSNA Pneumonia Detection Challenge (binary classification)\n\nThis notebook is adapted for the Kaggle competition:\n\n`rsna-pneumonia-detection-challenge`\n\nThe original challenge is a detection/localization task with DICOM images and bounding boxes in `stage_2_train_labels.csv`.  \nThis notebook builds a simpler binary classification pipeline:\n\n- `Target = 1` → Pneumonia\n- `Target = 0` → No Pneumonia\n\nIt:\n- auto-detects the Kaggle competition folder\n- reads `stage_2_train_labels.csv`\n- loads `.dcm` images from `stage_2_train_images/`\n- deduplicates patients into one label per image\n- creates a train/validation split\n- trains EfficientNetB2\n- reports accuracy, precision, and f1-score\n","metadata":{}},{"cell_type":"code","source":"\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\nimport pydicom\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    classification_report,\n    confusion_matrix,\n    accuracy_score,\n    precision_score,\n    f1_score\n)\n\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import EfficientNetB2\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\nSEED = 42\nIMG_SIZE = 260\nBATCH_SIZE = 16\nEPOCHS = 5\nVAL_SIZE = 0.2\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nprint(\"TensorFlow version:\", tf.__version__)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:05:05.094867Z","iopub.execute_input":"2026-04-03T21:05:05.095082Z","iopub.status.idle":"2026-04-03T21:05:33.947064Z","shell.execute_reply.started":"2026-04-03T21:05:05.095060Z","shell.execute_reply":"2026-04-03T21:05:33.946341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Dataset root detection\n# =========================\n\ndef find_dataset_root():\n    candidates = [\n        \"/kaggle/input/competitions/rsna-pneumonia-detection-challenge\",\n        \"/kaggle/input/competitions/rsna-pneumonia-detection-challenge\",\n    ]\n\n    for c in candidates:\n        if os.path.isfile(os.path.join(c, \"stage_2_train_labels.csv\")):\n            return c\n\n    if os.path.isdir(\"/kaggle/input\"):\n        for root, dirs, files in os.walk(\"/kaggle/input\"):\n            if \"stage_2_train_labels.csv\" in files:\n                return root\n\n    return None\n\nDATA_ROOT = find_dataset_root()\nWORKDIR = \"/kaggle/working/efficientnetb2_rsna_pneumonia\"\nos.makedirs(WORKDIR, exist_ok=True)\n\nprint(\"Detected DATA_ROOT:\", DATA_ROOT)\nprint(\"WORKDIR:\", WORKDIR)\n\nif DATA_ROOT is None:\n    print(\"\\nCould not auto-detect the competition data.\")\n    if os.path.isdir(\"/kaggle/input\"):\n        print(\"\\nFolders under /kaggle/input:\")\n        for root, dirs, files in os.walk(\"/kaggle/input\"):\n            depth = root.replace(\"/kaggle/input\", \"\").count(os.sep)\n            if depth <= 5:\n                print(root)\n    raise FileNotFoundError(\"RSNA competition dataset root not found.\")\n\nprint(\"\\nDATA_ROOT contents:\", sorted(os.listdir(DATA_ROOT))[:20])\n\nLABELS_CSV = os.path.join(DATA_ROOT, \"stage_2_train_labels.csv\")\nTRAIN_IMAGES_DIR = os.path.join(DATA_ROOT, \"stage_2_train_images\")\nTEST_IMAGES_DIR = os.path.join(DATA_ROOT, \"stage_2_test_images\")\n\nprint(\"LABELS_CSV exists:\", os.path.isfile(LABELS_CSV))\nprint(\"TRAIN_IMAGES_DIR exists:\", os.path.isdir(TRAIN_IMAGES_DIR))\nprint(\"TEST_IMAGES_DIR exists:\", os.path.isdir(TEST_IMAGES_DIR))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:06:14.211486Z","iopub.execute_input":"2026-04-03T21:06:14.212106Z","iopub.status.idle":"2026-04-03T21:06:14.224309Z","shell.execute_reply.started":"2026-04-03T21:06:14.212076Z","shell.execute_reply":"2026-04-03T21:06:14.223768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Load labels and build binary dataframe\n# =========================\n\nlabels_raw = pd.read_csv(LABELS_CSV)\nprint(\"Raw labels shape:\", labels_raw.shape)\ndisplay(labels_raw.head())\n\nlabels_df = (\n    labels_raw.groupby(\"patientId\", as_index=False)[\"Target\"]\n    .max()\n    .rename(columns={\"Target\": \"label\"})\n)\n\nlabels_df[\"label\"] = labels_df[\"label\"].astype(int)\nlabels_df[\"label_name\"] = labels_df[\"label\"].map({0: \"No_Pneumonia\", 1: \"Pneumonia\"})\nlabels_df[\"image_path\"] = labels_df[\"patientId\"].apply(\n    lambda pid: os.path.join(TRAIN_IMAGES_DIR, f\"{pid}.dcm\")\n)\n\nlabels_df = labels_df[labels_df[\"image_path\"].apply(os.path.isfile)].copy()\nlabels_df[\"image_id\"] = labels_df[\"patientId\"] + \".dcm\"\n\nlabels_df = labels_df[[\"image_id\", \"image_path\", \"label_name\", \"label\"]].reset_index(drop=True)\n\nprint(\"Binary dataframe shape:\", labels_df.shape)\ndisplay(labels_df.head())\n\nprint(\"\\nClass distribution:\")\ndisplay(labels_df[\"label_name\"].value_counts())\n\nif labels_df.empty:\n    raise ValueError(\"No valid DICOM files were found.\")\nif labels_df[\"label\"].nunique() < 2:\n    raise ValueError(\"Need both classes to train.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:06:23.412946Z","iopub.execute_input":"2026-04-03T21:06:23.413470Z","iopub.status.idle":"2026-04-03T21:06:57.906109Z","shell.execute_reply.started":"2026-04-03T21:06:23.413443Z","shell.execute_reply":"2026-04-03T21:06:57.905320Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Train / validation split\n# =========================\n\ntrain_df, val_df = train_test_split(\n    labels_df,\n    test_size=VAL_SIZE,\n    random_state=SEED,\n    stratify=labels_df[\"label\"]\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"Val shape:\", val_df.shape)\n\nprint(\"\\nTrain distribution:\")\ndisplay(train_df[\"label_name\"].value_counts())\n\nprint(\"\\nVal distribution:\")\ndisplay(val_df[\"label_name\"].value_counts())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:07:48.283149Z","iopub.execute_input":"2026-04-03T21:07:48.283755Z","iopub.status.idle":"2026-04-03T21:07:48.313529Z","shell.execute_reply.started":"2026-04-03T21:07:48.283701Z","shell.execute_reply":"2026-04-03T21:07:48.313002Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# DICOM loader\n# =========================\n\ndef load_dicom_as_rgb(path):\n    if isinstance(path, bytes):\n        path = path.decode(\"utf-8\")\n    else:\n        path = path.numpy().decode(\"utf-8\")\n\n    try:\n        ds = pydicom.dcmread(path)\n        img = ds.pixel_array.astype(np.float32)\n\n        if img.ndim == 3:\n            img = img[..., 0] if img.shape[-1] > 1 else np.squeeze(img)\n\n        if img.ndim != 2:\n            raise ValueError(f\"Image DICOM non 2D: shape={img.shape}, path={path}\")\n\n        img = img - np.min(img)\n        max_val = np.max(img)\n        if max_val > 0:\n            img = img / max_val\n\n        img = (img * 255.0).astype(np.uint8)\n        img = np.stack([img, img, img], axis=-1)\n        return img\n\n    except Exception as e:\n        print(f\"[ERREUR DICOM] {path} -> {type(e).__name__}: {e}\")\n        raise\n\ndef decode_path(path):\n    if isinstance(path, bytes):\n        return path.decode(\"utf-8\")\n    if isinstance(path, str):\n        return path\n    return path.numpy().decode(\"utf-8\")\n\ndef load_dicom_as_rgb(path):\n    path = decode_path(path)\n\n    try:\n        ds = pydicom.dcmread(path)\n        img = ds.pixel_array.astype(np.float32)\n\n        if img.ndim == 3:\n            img = img[..., 0] if img.shape[-1] > 1 else np.squeeze(img)\n\n        if img.ndim != 2:\n            raise ValueError(f\"Image DICOM non 2D: shape={img.shape}, path={path}\")\n\n        img = img - np.min(img)\n        max_val = np.max(img)\n        if max_val > 0:\n            img = img / max_val\n\n        img = (img * 255.0).astype(np.uint8)\n        img = np.stack([img, img, img], axis=-1)\n        return img\n\n    except Exception as e:\n        print(f\"[ERREUR DICOM] {path} -> {type(e).__name__}: {e}\")\n        raise\n\n\n\nfor p in train_df[\"image_path\"].head(10):\n    try:\n        x = load_dicom_as_rgb(tf.constant(p))\n        print(\"OK:\", p, x.shape, x.dtype)\n    except Exception as e:\n        print(\"FAIL:\", p, e)\n\n\n\ndef make_dataset(df_, training=False):\n    paths = df_[\"image_path\"].values\n    labels = df_[\"label\"].values.astype(\"float32\")\n\n    ds = tf.data.Dataset.from_tensor_slices((paths, labels))\n    ds = ds.map(tf_load_dicom, num_parallel_calls=tf.data.AUTOTUNE)\n\n    if training:\n        ds = ds.shuffle(min(len(df_), 4096), seed=SEED)\n\n    return ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n\ntrain_ds = make_dataset(train_df, training=True)\nval_ds = make_dataset(val_df, training=False)\nsample_path = train_df[\"image_path\"].iloc[0]\nimg = load_dicom_as_rgb(tf.constant(sample_path))\nprint(img.shape, img.dtype, img.min(), img.max())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:23:53.739743Z","iopub.execute_input":"2026-04-03T21:23:53.740621Z","iopub.status.idle":"2026-04-03T21:23:53.944228Z","shell.execute_reply.started":"2026-04-03T21:23:53.740588Z","shell.execute_reply":"2026-04-03T21:23:53.943550Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Visual sanity check\n# =========================\n\nsample_batch = next(iter(train_ds))\nsample_images, sample_labels = sample_batch\n\nplt.figure(figsize=(12, 8))\nfor i in range(min(9, sample_images.shape[0])):\n    plt.subplot(3, 3, i + 1)\n    img = sample_images[i].numpy()\n    img_disp = img.copy()\n    img_disp = img_disp - img_disp.min()\n    if img_disp.max() > 0:\n        img_disp = img_disp / img_disp.max()\n    plt.imshow(img_disp)\n    plt.title(f\"label={int(sample_labels[i].numpy()[0])}\")\n    plt.axis(\"off\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:24:02.960470Z","iopub.execute_input":"2026-04-03T21:24:02.961109Z","iopub.status.idle":"2026-04-03T21:24:30.026054Z","shell.execute_reply.started":"2026-04-03T21:24:02.961078Z","shell.execute_reply":"2026-04-03T21:24:30.025254Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Model\n# =========================\n\nbase_model = EfficientNetB2(\n    include_top=False,\n    weights=\"imagenet\",\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\nbase_model.trainable = False\n\ninputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = base_model(inputs, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.3)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\n\nmodel = models.Model(inputs, outputs)\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-3),\n    loss=\"binary_crossentropy\",\n    metrics=[\n        \"accuracy\",\n        tf.keras.metrics.Precision(name=\"precision\")\n    ]\n)\n\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:25:26.845457Z","iopub.execute_input":"2026-04-03T21:25:26.845812Z","iopub.status.idle":"2026-04-03T21:25:29.811156Z","shell.execute_reply.started":"2026-04-03T21:25:26.845782Z","shell.execute_reply":"2026-04-03T21:25:29.810589Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Training\n# =========================\n\ncallbacks = [\n    tf.keras.callbacks.ModelCheckpoint(\n        filepath=os.path.join(WORKDIR, \"best_efficientnetb2_rsna_pneumonia.keras\"),\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        save_best_only=True,\n        verbose=1\n    ),\n    tf.keras.callbacks.EarlyStopping(\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        patience=3,\n        restore_best_weights=True,\n        verbose=1\n    )\n]\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:26:14.404775Z","iopub.execute_input":"2026-04-03T21:26:14.405433Z","iopub.status.idle":"2026-04-03T21:41:35.319271Z","shell.execute_reply.started":"2026-04-03T21:26:14.405402Z","shell.execute_reply":"2026-04-03T21:41:35.318609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Evaluation\n# =========================\n\nval_probs = model.predict(val_ds, verbose=1).ravel()\ny_true = val_df[\"label\"].values\ny_pred = (val_probs >= 0.5).astype(int)\n\nacc = accuracy_score(y_true, y_pred)\nprec = precision_score(y_true, y_pred, zero_division=0)\nf1 = f1_score(y_true, y_pred, zero_division=0)\n\nprint(\"\\nMetrics:\")\nprint(f\"Accuracy  : {acc:.4f}\")\nprint(f\"Precision : {prec:.4f}\")\nprint(f\"F1-score  : {f1:.4f}\")\n\nmetrics_df = pd.DataFrame({\n    \"metric\": [\"accuracy\", \"precision\", \"f1_score\"],\n    \"value\": [acc, prec, f1]\n})\ndisplay(metrics_df)\n\nprint(\"\\nClassification report:\")\nprint(classification_report(y_true, y_pred, target_names=[\"No_Pneumonia\", \"Pneumonia\"], zero_division=0))\n\nprint(\"\\nConfusion matrix:\")\nprint(confusion_matrix(y_true, y_pred))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:41:52.441448Z","iopub.execute_input":"2026-04-03T21:41:52.442082Z","iopub.status.idle":"2026-04-03T21:42:40.922139Z","shell.execute_reply.started":"2026-04-03T21:41:52.442050Z","shell.execute_reply":"2026-04-03T21:42:40.921400Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# =========================\n# Curves\n# =========================\n\nhistory_df = pd.DataFrame(history.history)\ndisplay(history_df.head())\n\nplt.figure(figsize=(8, 5))\nplt.plot(history.history[\"accuracy\"], label=\"train_acc\")\nplt.plot(history.history[\"val_accuracy\"], label=\"val_acc\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.show()\n\nplt.figure(figsize=(8, 5))\nplt.plot(history.history[\"loss\"], label=\"train_loss\")\nplt.plot(history.history[\"val_loss\"], label=\"val_loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-03T21:43:17.117793Z","iopub.execute_input":"2026-04-03T21:43:17.118374Z","iopub.status.idle":"2026-04-03T21:43:17.365458Z","shell.execute_reply.started":"2026-04-03T21:43:17.118342Z","shell.execute_reply":"2026-04-03T21:43:17.364721Z"}},"outputs":[],"execution_count":null}]}