{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"1c2b0aea","cell_type":"markdown","source":"# RSNA Pneumonia Classification with EfficientNetB2\n\nCe notebook remplace l'approche **YOLO (détection)** par une approche **classification binaire** avec **EfficientNetB2** pour prédire `pneumonia` vs `normal`.\n\n> Remarque : EfficientNetB2 est un modèle de **classification d'images**, donc il ne prédit pas les boîtes englobantes. Si vous voulez garder la détection d'objets, il faut rester sur YOLO ou un autre détecteur.","metadata":{}},{"id":"05aa3f27","cell_type":"code","source":"!pip install -q pydicom","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:21:04.794887Z","iopub.execute_input":"2026-03-26T19:21:04.795527Z","iopub.status.idle":"2026-03-26T19:21:08.040368Z","shell.execute_reply.started":"2026-03-26T19:21:04.795496Z","shell.execute_reply":"2026-03-26T19:21:08.039324Z"}},"outputs":[],"execution_count":null},{"id":"655d3054","cell_type":"code","source":"\nimport os\nimport gc\nimport cv2\nimport math\nimport random\nimport warnings\nimport pydicom\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.applications import EfficientNetB2\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\nwarnings.filterwarnings('ignore')\nprint('TensorFlow version:', tf.__version__)\nSEED = 42\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\nrandom.seed(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:21:12.613520Z","iopub.execute_input":"2026-03-26T19:21:12.614297Z","iopub.status.idle":"2026-03-26T19:21:22.550146Z","shell.execute_reply.started":"2026-03-26T19:21:12.614250Z","shell.execute_reply":"2026-03-26T19:21:22.549430Z"}},"outputs":[],"execution_count":null},{"id":"3941af78","cell_type":"code","source":"\nIMG_SIZE = 224  # taille native recommandée pour EfficientNetB2\nBATCH_SIZE = 8\nEPOCHS = 10\nAUTOTUNE = tf.data.AUTOTUNE\n\nRSNA_IMAGES = '/kaggle/input/competitions/rsna-pneumonia-detection-challenge/stage_2_train_images'\nLABELS_CSV = '/kaggle/input/competitions/rsna-pneumonia-detection-challenge/stage_2_train_labels.csv'\nWORKDIR = '/kaggle/working/efficientnetb2_rsna'\nJPEG_DIR = os.path.join(WORKDIR, 'jpg_images')\nos.makedirs(JPEG_DIR, exist_ok=True)\nprint('Workdir ready:', WORKDIR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:21:41.717347Z","iopub.execute_input":"2026-03-26T19:21:41.718174Z","iopub.status.idle":"2026-03-26T19:21:41.723143Z","shell.execute_reply.started":"2026-03-26T19:21:41.718144Z","shell.execute_reply":"2026-03-26T19:21:41.722377Z"}},"outputs":[],"execution_count":null},{"id":"bb88c943","cell_type":"markdown","source":"## 1) Chargement et préparation des labels (niveau patient)","metadata":{}},{"id":"afa2d1c8","cell_type":"code","source":"\ndf = pd.read_csv(LABELS_CSV)\ndf.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:21:58.582061Z","iopub.execute_input":"2026-03-26T19:21:58.582647Z","iopub.status.idle":"2026-03-26T19:21:58.623787Z","shell.execute_reply.started":"2026-03-26T19:21:58.582617Z","shell.execute_reply":"2026-03-26T19:21:58.623029Z"}},"outputs":[],"execution_count":null},{"id":"f2b041e8","cell_type":"code","source":"\n# Pour la classification, on veut une seule ligne par patient_id\n# Target = 1 si le patient a au moins une annotation de pneumonie, sinon 0\npatient_df = (\n    df.groupby('patientId', as_index=False)['Target']\n      .max()\n      .rename(columns={'Target': 'label'})\n)\n\npatient_df['label_name'] = patient_df['label'].map({0: 'normal', 1: 'pneumonia'})\npatient_df['dcm_path'] = patient_df['patientId'].apply(lambda x: os.path.join(RSNA_IMAGES, f'{x}.dcm'))\npatient_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:22:07.954967Z","iopub.execute_input":"2026-03-26T19:22:07.955324Z","iopub.status.idle":"2026-03-26T19:22:08.014843Z","shell.execute_reply.started":"2026-03-26T19:22:07.955296Z","shell.execute_reply":"2026-03-26T19:22:08.014116Z"}},"outputs":[],"execution_count":null},{"id":"34c7648c","cell_type":"code","source":"\nprint('Nombre de patients :', len(patient_df))\nprint(patient_df['label_name'].value_counts())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:22:15.339926Z","iopub.execute_input":"2026-03-26T19:22:15.340754Z","iopub.status.idle":"2026-03-26T19:22:15.348316Z","shell.execute_reply.started":"2026-03-26T19:22:15.340722Z","shell.execute_reply":"2026-03-26T19:22:15.347219Z"}},"outputs":[],"execution_count":null},{"id":"fcfe5a4d","cell_type":"code","source":"\npatient_df['label_name'].value_counts().plot(kind='bar', title='Distribution des classes')\nplt.xlabel('Classe')\nplt.ylabel('Count')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:22:21.063923Z","iopub.execute_input":"2026-03-26T19:22:21.064741Z","iopub.status.idle":"2026-03-26T19:22:21.207933Z","shell.execute_reply.started":"2026-03-26T19:22:21.064709Z","shell.execute_reply":"2026-03-26T19:22:21.207138Z"}},"outputs":[],"execution_count":null},{"id":"2245bbe5","cell_type":"markdown","source":"## 2) Conversion DICOM -> JPG","metadata":{}},{"id":"f70db4f3","cell_type":"code","source":"\ndef dicom_to_jpg(dcm_path, out_path):\n    dcm = pydicom.dcmread(dcm_path)\n    img = dcm.pixel_array.astype(np.float32)\n    img -= img.min()\n    if img.max() > 0:\n        img /= img.max()\n    img = (img * 255).astype(np.uint8)\n    Image.fromarray(img).save(out_path, quality=95)\n\nmissing = []\nfor patient_id, dcm_path in tqdm(patient_df[['patientId', 'dcm_path']].itertuples(index=False), total=len(patient_df)):\n    jpg_path = os.path.join(JPEG_DIR, f'{patient_id}.jpg')\n    if not os.path.exists(dcm_path):\n        missing.append(dcm_path)\n        continue\n    if not os.path.exists(jpg_path):\n        dicom_to_jpg(dcm_path, jpg_path)\n\npatient_df['image_path'] = patient_df['patientId'].apply(lambda x: os.path.join(JPEG_DIR, f'{x}.jpg'))\nprint('Images manquantes:', len(missing))\nprint(patient_df[['image_path', 'label_name']].head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:22:28.596986Z","iopub.execute_input":"2026-03-26T19:22:28.597717Z","iopub.status.idle":"2026-03-26T19:23:09.663510Z","shell.execute_reply.started":"2026-03-26T19:22:28.597688Z","shell.execute_reply":"2026-03-26T19:23:09.662645Z"}},"outputs":[],"execution_count":null},{"id":"7c345c85","cell_type":"markdown","source":"## 3) Split train / validation / test","metadata":{}},{"id":"6bddbf33","cell_type":"code","source":"\ntrain_df, temp_df = train_test_split(\n    patient_df,\n    test_size=0.2,\n    random_state=SEED,\n    stratify=patient_df['label']\n)\n\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=0.5,\n    random_state=SEED,\n    stratify=temp_df['label']\n)\n\nfor name, subset in [('train', train_df), ('val', val_df), ('test', test_df)]:\n    print(name, subset.shape)\n    print(subset['label_name'].value_counts(normalize=True))\n    print('-' * 40)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:23:19.315846Z","iopub.execute_input":"2026-03-26T19:23:19.316636Z","iopub.status.idle":"2026-03-26T19:23:19.348975Z","shell.execute_reply.started":"2026-03-26T19:23:19.316602Z","shell.execute_reply":"2026-03-26T19:23:19.348086Z"}},"outputs":[],"execution_count":null},{"id":"03fe898b","cell_type":"markdown","source":"## 4) Visualisation rapide","metadata":{}},{"id":"b6f2cbe2","cell_type":"code","source":"\ndef show_samples(dataframe, n=6):\n    sample_df = dataframe.sample(n=min(n, len(dataframe)), random_state=SEED).reset_index(drop=True)\n    plt.figure(figsize=(14, 8))\n    for i in range(len(sample_df)):\n        img = Image.open(sample_df.loc[i, 'image_path']).convert('L')\n        plt.subplot(2, math.ceil(len(sample_df)/2), i+1)\n        plt.imshow(img, cmap='gray')\n        plt.title(sample_df.loc[i, 'label_name'])\n        plt.axis('off')\n    plt.tight_layout()\n    plt.show()\n\nshow_samples(train_df, n=6)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:23:33.045407Z","iopub.execute_input":"2026-03-26T19:23:33.046024Z","iopub.status.idle":"2026-03-26T19:23:34.588721Z","shell.execute_reply.started":"2026-03-26T19:23:33.045994Z","shell.execute_reply":"2026-03-26T19:23:34.587891Z"}},"outputs":[],"execution_count":null},{"id":"2db5759f","cell_type":"markdown","source":"## 5) Pipelines TensorFlow","metadata":{}},{"id":"2ceaec53","cell_type":"code","source":"\ndef decode_image(path, label):\n    img = tf.io.read_file(path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, (IMG_SIZE, IMG_SIZE))\n    img = preprocess_input(tf.cast(img, tf.float32))\n    return img, tf.cast(label, tf.float32)\n\ndef make_dataset(dataframe, training=False):\n    ds = tf.data.Dataset.from_tensor_slices(\n        (dataframe['image_path'].values, dataframe['label'].values)\n    )\n    ds = ds.map(decode_image, num_parallel_calls=AUTOTUNE)\n    \n    if training:\n        ds = ds.shuffle(buffer_size=1000, seed=SEED)\n\n    return ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)\ntrain_ds = make_dataset(train_df, training=True)\nval_ds = make_dataset(val_df)\ntest_ds = make_dataset(test_df)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:23:44.045893Z","iopub.execute_input":"2026-03-26T19:23:44.046711Z","iopub.status.idle":"2026-03-26T19:23:46.037838Z","shell.execute_reply.started":"2026-03-26T19:23:44.046680Z","shell.execute_reply":"2026-03-26T19:23:46.037005Z"}},"outputs":[],"execution_count":null},{"id":"f30d563b","cell_type":"markdown","source":"## 6) Modèle EfficientNetB2","metadata":{}},{"id":"02ee098b","cell_type":"code","source":"\ndata_augmentation = keras.Sequential([\n    layers.RandomFlip('horizontal'),\n    layers.RandomRotation(0.05),\n    layers.RandomZoom(0.10),\n], name='data_augmentation')\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 = keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = data_augmentation(inputs)\nx = base_model(x, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.3)(x)\noutputs = layers.Dense(1, activation='sigmoid')(x)\nmodel = keras.Model(inputs, outputs)\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=1e-3),\n    loss='binary_crossentropy',\n    metrics=[\n        'accuracy',\n        keras.metrics.AUC(name='auc'),\n        keras.metrics.Precision(name='precision'),\n        keras.metrics.Recall(name='recall')\n    ]\n)\n\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:24:01.603415Z","iopub.execute_input":"2026-03-26T19:24:01.603753Z","iopub.status.idle":"2026-03-26T19:24:04.381590Z","shell.execute_reply.started":"2026-03-26T19:24:01.603723Z","shell.execute_reply":"2026-03-26T19:24:04.381002Z"}},"outputs":[],"execution_count":null},{"id":"98620829","cell_type":"code","source":"\nclass_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=np.array([0, 1]),\n    y=train_df['label'].values\n)\nclass_weights = {0: class_weights[0], 1: class_weights[1]}\nclass_weights\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:24:26.895297Z","iopub.execute_input":"2026-03-26T19:24:26.895608Z","iopub.status.idle":"2026-03-26T19:24:26.904970Z","shell.execute_reply.started":"2026-03-26T19:24:26.895583Z","shell.execute_reply":"2026-03-26T19:24:26.904261Z"}},"outputs":[],"execution_count":null},{"id":"cba76c16","cell_type":"code","source":"\ncallbacks = [\n    keras.callbacks.ModelCheckpoint(\n        filepath=os.path.join(WORKDIR, 'best_efficientnetb2.keras'),\n        monitor='val_auc',\n        mode='max',\n        save_best_only=True\n    ),\n    keras.callbacks.EarlyStopping(\n        monitor='val_auc',\n        mode='max',\n        patience=3,\n        restore_best_weights=True\n    ),\n    keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.2,\n        patience=2,\n        verbose=1\n    )\n]\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    class_weight=class_weights,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:24:34.353721Z","iopub.execute_input":"2026-03-26T19:24:34.354055Z","iopub.status.idle":"2026-03-26T19:45:20.124451Z","shell.execute_reply.started":"2026-03-26T19:24:34.354029Z","shell.execute_reply":"2026-03-26T19:45:20.123666Z"}},"outputs":[],"execution_count":null},{"id":"8e621074","cell_type":"markdown","source":"## 7) Fine-tuning","metadata":{}},{"id":"47d538aa","cell_type":"code","source":"\nbase_model.trainable = True\nfine_tune_at = int(len(base_model.layers) * 0.7)\nfor layer in base_model.layers[:fine_tune_at]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=1e-5),\n    loss='binary_crossentropy',\n    metrics=[\n        'accuracy',\n        keras.metrics.AUC(name='auc'),\n        keras.metrics.Precision(name='precision'),\n        keras.metrics.Recall(name='recall')\n    ]\n)\n\nhistory_finetune = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=5,\n    class_weight=class_weights,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T19:45:57.214997Z","iopub.execute_input":"2026-03-26T19:45:57.215429Z","iopub.status.idle":"2026-03-26T20:01:51.587870Z","shell.execute_reply.started":"2026-03-26T19:45:57.215398Z","shell.execute_reply":"2026-03-26T20:01:51.587205Z"}},"outputs":[],"execution_count":null},{"id":"cf861be4","cell_type":"markdown","source":"## 8) Évaluation","metadata":{}},{"id":"66b147cb","cell_type":"code","source":"\ntest_metrics = model.evaluate(test_ds, return_dict=True)\ntest_metrics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T20:02:01.155699Z","iopub.execute_input":"2026-03-26T20:02:01.156022Z","iopub.status.idle":"2026-03-26T20:02:13.190215Z","shell.execute_reply.started":"2026-03-26T20:02:01.155995Z","shell.execute_reply":"2026-03-26T20:02:13.189625Z"}},"outputs":[],"execution_count":null},{"id":"6e7efd39","cell_type":"code","source":"\ndef plot_history(histories, metric='loss'):\n    plt.figure(figsize=(8, 5))\n    for i, hist in enumerate(histories, start=1):\n        if metric in hist.history:\n            plt.plot(hist.history[metric], label=f'train_{metric}_{i}')\n        val_metric = f'val_{metric}'\n        if val_metric in hist.history:\n            plt.plot(hist.history[val_metric], label=f'val_{metric}_{i}')\n    plt.title(metric)\n    plt.legend()\n    plt.show()\n\nplot_history([history, history_finetune], metric='loss')\nplot_history([history, history_finetune], metric='auc')\nplot_history([history, history_finetune], metric='accuracy')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T20:02:23.017494Z","iopub.execute_input":"2026-03-26T20:02:23.018091Z","iopub.status.idle":"2026-03-26T20:02:23.422114Z","shell.execute_reply.started":"2026-03-26T20:02:23.018062Z","shell.execute_reply":"2026-03-26T20:02:23.421459Z"}},"outputs":[],"execution_count":null},{"id":"ab2852fe","cell_type":"markdown","source":"## 9) Sauvegarde du modèle","metadata":{}},{"id":"0b333e8f","cell_type":"code","source":"\nfinal_model_path = os.path.join(WORKDIR, 'efficientnetb2_rsna_pneumonia.keras')\nmodel.save(final_model_path)\nprint('Modèle sauvegardé dans :', final_model_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T20:02:40.611546Z","iopub.execute_input":"2026-03-26T20:02:40.611899Z","iopub.status.idle":"2026-03-26T20:02:41.718466Z","shell.execute_reply.started":"2026-03-26T20:02:40.611870Z","shell.execute_reply":"2026-03-26T20:02:41.717710Z"}},"outputs":[],"execution_count":null},{"id":"c4713342","cell_type":"markdown","source":"## 10) Prédiction sur quelques images","metadata":{}},{"id":"3fb14048","cell_type":"code","source":"def predict_sample(dataframe, n=6):\n    sample_df = dataframe.sample(n=min(n, len(dataframe)), random_state=SEED).reset_index(drop=True)\n    plt.figure(figsize=(14, 8))\n\n    for i in range(len(sample_df)):\n        img = Image.open(sample_df.loc[i, 'image_path']).convert('RGB').resize((IMG_SIZE, IMG_SIZE))\n        arr = np.array(img, dtype=np.float32)\n        arr = preprocess_input(arr)\n\n        pred = model.predict(np.expand_dims(arr, axis=0), verbose=0)[0][0]\n        label = 'pneumonia' if pred >= 0.5 else 'normal'\n\n        plt.subplot(2, math.ceil(len(sample_df)/2), i+1)\n        plt.imshow(img)\n        plt.title(f'True: {sample_df.loc[i, \"label_name\"]}\\nPred: {label} ({pred:.2f})')\n        plt.axis('off')\n\n    plt.tight_layout()\n    plt.show()\n\npredict_sample(test_df, n=6)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T20:07:58.401188Z","iopub.execute_input":"2026-03-26T20:07:58.401562Z","iopub.status.idle":"2026-03-26T20:08:03.046709Z","shell.execute_reply.started":"2026-03-26T20:07:58.401533Z","shell.execute_reply":"2026-03-26T20:08:03.045852Z"}},"outputs":[],"execution_count":null}]}