{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":29653,"databundleVersionId":2420395,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🧠 Synthèse globale du projet — Prédiction du statut MGMT par CNN 3D\n**Réalisation : Jaouad El Morabit — Master BIAM, FSDM (USMBA, Fès)**\n\n---\n\n## 🎯 Objectif\nPrédire le statut de **méthylation du promoteur MGMT** (0 = non méthylé, 1 = méthylé) à partir d'IRM cérébrales (modalité **FLAIR**), à l'aide d'un réseau de neurones convolutif **3D** (*3D CNN*). Objectif secondaire : améliorer l'accuracy **et** équilibrer la matrice de confusion, de façon **honnête et rigoureuse**.\n\n> Contexte important : prédire MGMT à partir de l'IRM seule est une tâche **réputée très difficile** — dans la littérature, la plupart des modèles restent proches du hasard (AUC 0,50–0,65). Les résultats doivent donc être jugés dans ce cadre.\n\n---\n\n## 🛠️ Démarche\nUn pipeline complet (chargement DICOM → volumes 3D → CNN 3D → évaluation), puis **5 optimisations testées une par une**, chacune mesurée avant d'être conservée ou abandonnée :\n1. Pondération des classes (*class weight*)\n2. Seuil de décision optimisé (*threshold tuning*)\n3. Meilleures coupes + normalisation par volume (*z-score*)\n4. Augmentation de données (*data augmentation*)\n5. Validation croisée 5-fold (*cross-validation*)\n\nPuis deux **leviers** pour tenter de dépasser la cible : fusion **multi-modalités** et **ensembling**. Enfin, un **correctif méthodologique** (le test n'est évalué qu'une seule fois, après que le seuil a été fixé sur la validation).\n\n---\n\n## ✅ Ce qui a réussi\n- **Une méthodologie honnête et rigoureuse** : seuil réglé sur la validation (jamais sur le test), évaluation du test en une seule fois, et surtout **validation croisée** pour obtenir un chiffre fiable (et non le résultat chanceux d'un seul entraînement).\n- **L'ensembling** (moyenne de 5 modèles) : c'est le levier qui a le mieux marché — il **augmente l'AUC et réduit la variance**, permettant de **dépasser la cible de référence**.\n- **La documentation d'un résultat négatif** (multi-modalités, voir ci-dessous) : c'est une vraie démarche scientifique.\n\n## ❌ Ce qui n'a pas marché (et pourquoi)\n- **Pondération des classes** → effet **neutre**. Raison : les classes sont déjà quasi équilibrées (56 vs 61), il n'y avait donc rien à corriger.\n- **Multi-modalités** (empiler FLAIR + T1w + T1wCE + T2w) → **dégrade** les performances (AUC en validation croisée 0,56 vs 0,61). Raison probable : les 4 modalités ne sont **pas recalées** (*non alignées* pixel à pixel), ce qui ajoute du bruit plutôt que du signal ; de plus, davantage de canaux = plus de sur-apprentissage sur un jeu de données réduit. → **Abandonné, retour à FLAIR seul.**\n- **Seuil et augmentation** → effets **modestes** : utiles pour la rigueur, mais incapables de faire fortement monter un modèle limité par la difficulté de la tâche.\n\n**Cause de fond des limites** : (1) **peu de données** (~585 patients, alors qu'un CNN 3D en réclame des milliers), (2) un **prétraitement grossier** (coupes centrales approximatives, sans localisation précise de la tumeur), (3) le **signal intrinsèquement faible** de la tâche.\n\n---\n\n## 📊 Résultat final\n| Métrique | Valeur | Cible du prof (AUC 0,635) |\n|---|---|---|\n| **AUC — ensemble (test)** | **≈ 0,64** | ✅ atteinte / dépassée |\n| AUC — validation croisée (niveau honnête) | **0,61 ± 0,04** | référence fiable |\n| Balanced accuracy (ensemble) | ≈ 0,60 | — |\n\n> **Formulation retenue :** « En validation croisée 5-fold, le modèle atteint un AUC de **0,61 ± 0,04**. En combinant 5 modèles par *ensembling*, l'AUC sur le test atteint **≈ 0,64**, au niveau/au-dessus de la cible de référence (0,635). »\n>\n> Nuance honnête : le résultat d'ensemble est mesuré sur **un seul** découpage de test ; l'ensembling réduit la variance du modèle, pas celle du découpage. La validation croisée reste la mesure la plus fiable du niveau réel.\n\n---\n\n## 🔭 Perspectives (pistes non explorées)\nPar ordre de promesse pour vraiment améliorer l'AUC :\n1. **Focus sur la tumeur (segmentation)** — n'analyser que la zone tumorale (masques BraTS) au lieu de toute l'image → concentre le signal. *Levier n°1 réaliste.*\n2. **Transfer learning** — partir d'un réseau pré-entraîné (ImageNet / MedicalNet) au lieu d'entraîner de zéro → très efficace quand les données sont rares.\n3. **Radiomics + machine learning classique** — transformer l'image en descripteurs chiffrés (texture, forme) + modèle simple → souvent aussi bon, et **interprétable**. Approche alternative idéale pour comparer.\n4. **Recalage des modalités** avant fusion — pour que le multi-modalités devienne enfin bénéfique.","metadata":{}},{"cell_type":"markdown","source":"## 1. 📦 Importation des librairies\n*Chargement des outils : manipulation de fichiers, calcul numérique, lecture d'images DICOM, TensorFlow.*","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib\nimport pydicom as dicom\nimport cv2\nimport ast\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:37.723649Z","iopub.execute_input":"2026-07-11T09:51:37.723873Z","iopub.status.idle":"2026-07-11T09:51:43.463288Z","shell.execute_reply.started":"2026-07-11T09:51:37.723849Z","shell.execute_reply":"2026-07-11T09:51:43.462554Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. 📊 Chargement & exploration des données\n*Lecture des étiquettes (`train_labels.csv`) et répartition des classes MGMT (0 = non méthylé, 1 = méthylé).*","metadata":{}},{"cell_type":"code","source":"path = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification/'\nos.listdir(path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.465114Z","iopub.execute_input":"2026-07-11T09:51:43.465547Z","iopub.status.idle":"2026-07-11T09:51:43.472777Z","shell.execute_reply.started":"2026-07-11T09:51:43.465523Z","shell.execute_reply":"2026-07-11T09:51:43.472066Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"path = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification/'\nos.listdir(path)\ntrain_data = pd.read_csv(path+'train_labels.csv')\nsamp_subm = pd.read_csv(path+'sample_submission.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.473648Z","iopub.execute_input":"2026-07-11T09:51:43.474166Z","iopub.status.idle":"2026-07-11T09:51:43.509850Z","shell.execute_reply.started":"2026-07-11T09:51:43.474144Z","shell.execute_reply":"2026-07-11T09:51:43.509105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('Samples train:', len(train_data))\nprint('Samples test:', len(samp_subm))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.510698Z","iopub.execute_input":"2026-07-11T09:51:43.510930Z","iopub.status.idle":"2026-07-11T09:51:43.515904Z","shell.execute_reply.started":"2026-07-11T09:51:43.510909Z","shell.execute_reply":"2026-07-11T09:51:43.514951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.518032Z","iopub.execute_input":"2026-07-11T09:51:43.518339Z","iopub.status.idle":"2026-07-11T09:51:43.544530Z","shell.execute_reply.started":"2026-07-11T09:51:43.518322Z","shell.execute_reply":"2026-07-11T09:51:43.543816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data[\"MGMT_value\"].value_counts().head(2).plot(kind = 'pie', autopct='%1.1f%%', figsize=(8, 8)).legend()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.545272Z","iopub.execute_input":"2026-07-11T09:51:43.545541Z","iopub.status.idle":"2026-07-11T09:51:43.944463Z","shell.execute_reply.started":"2026-07-11T09:51:43.545518Z","shell.execute_reply":"2026-07-11T09:51:43.943643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data[\"MGMT_value\"].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.945358Z","iopub.execute_input":"2026-07-11T09:51:43.945656Z","iopub.status.idle":"2026-07-11T09:51:43.952758Z","shell.execute_reply.started":"2026-07-11T09:51:43.945633Z","shell.execute_reply":"2026-07-11T09:51:43.952020Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"samp_subm.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.953474Z","iopub.execute_input":"2026-07-11T09:51:43.953758Z","iopub.status.idle":"2026-07-11T09:51:43.971044Z","shell.execute_reply.started":"2026-07-11T09:51:43.953735Z","shell.execute_reply":"2026-07-11T09:51:43.970216Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. 🖼️ Visualisation des images IRM (DICOM)\n*Aperçu des 4 modalités d'IRM disponibles : FLAIR, T1w, T1wCE, T2w.*","metadata":{}},{"cell_type":"code","source":"folder = str(train_data.loc[0, 'BraTS21ID']).zfill(5)\nfolder","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.972011Z","iopub.execute_input":"2026-07-11T09:51:43.972299Z","iopub.status.idle":"2026-07-11T09:51:43.984208Z","shell.execute_reply.started":"2026-07-11T09:51:43.972275Z","shell.execute_reply":"2026-07-11T09:51:43.983319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"os.listdir(path+'train/'+folder)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:43.984968Z","iopub.execute_input":"2026-07-11T09:51:43.986067Z","iopub.status.idle":"2026-07-11T09:51:44.004653Z","shell.execute_reply.started":"2026-07-11T09:51:43.986050Z","shell.execute_reply":"2026-07-11T09:51:44.003821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('Number of FLAIR images:', len(os.listdir(path+'train/'+folder+'/'+'FLAIR')))\nprint('Number of T1w images:', len(os.listdir(path+'train/'+folder+'/'+'T1w')))\nprint('Number of T1wCE images:', len(os.listdir(path+'train/'+folder+'/'+'T1wCE')))\nprint('Number of T2w images:', len(os.listdir(path+'train/'+folder+'/'+'T2w')))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:44.006372Z","iopub.execute_input":"2026-07-11T09:51:44.006862Z","iopub.status.idle":"2026-07-11T09:51:44.064179Z","shell.execute_reply.started":"2026-07-11T09:51:44.006846Z","shell.execute_reply":"2026-07-11T09:51:44.063413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"path_file = ''.join([path, 'train/', folder, '/', 'FLAIR/'])\nimage = os.listdir(path_file)[0]\ndata_file = dicom.dcmread(path_file+image)\nimg = data_file.pixel_array\nprint('Image shape:', img.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:44.065037Z","iopub.execute_input":"2026-07-11T09:51:44.065420Z","iopub.status.idle":"2026-07-11T09:51:44.081672Z","shell.execute_reply.started":"2026-07-11T09:51:44.065396Z","shell.execute_reply":"2026-07-11T09:51:44.080829Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Flair Image\ndef plot_examples(row = 0, cat = 'FLAIR'): \n    folder = str(train_data.loc[row, 'BraTS21ID']).zfill(5)\n    path_file = ''.join([path, 'train/', folder, '/', cat, '/'])\n    images = os.listdir(path_file)\n    \n    fig, axs = plt.subplots(1, 5, figsize=(30, 30))\n    fig.subplots_adjust(hspace = .2, wspace=.2)\n    axs = axs.ravel()\n    \n    for num in range(5):\n        data_file = dicom.dcmread(path_file+images[num])\n        img = data_file.pixel_array\n        axs[num].imshow(img, cmap='gray')\n        axs[num].set_title(cat+' '+images[num])\n        axs[num].set_xticklabels([])\n        axs[num].set_yticklabels([])\n        \nrow = 0\nplot_examples(row = row, cat = 'FLAIR')\nplt.show()","metadata":{"trusted":true,"scrolled":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:44.082449Z","iopub.execute_input":"2026-07-11T09:51:44.082756Z","iopub.status.idle":"2026-07-11T09:51:44.939941Z","shell.execute_reply.started":"2026-07-11T09:51:44.082736Z","shell.execute_reply":"2026-07-11T09:51:44.939059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#T1w Images\nplot_examples(row = row, cat = 'T1w')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:44.943789Z","iopub.execute_input":"2026-07-11T09:51:44.944070Z","iopub.status.idle":"2026-07-11T09:51:45.658657Z","shell.execute_reply.started":"2026-07-11T09:51:44.944053Z","shell.execute_reply":"2026-07-11T09:51:45.657791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#T1wCE Images\nplot_examples(row = row, cat = 'T1wCE')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:45.659501Z","iopub.execute_input":"2026-07-11T09:51:45.659826Z","iopub.status.idle":"2026-07-11T09:51:46.360038Z","shell.execute_reply.started":"2026-07-11T09:51:45.659808Z","shell.execute_reply":"2026-07-11T09:51:46.359131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#T2w Images\nplot_examples(row = row, cat = 'T2w')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:46.360910Z","iopub.execute_input":"2026-07-11T09:51:46.361209Z","iopub.status.idle":"2026-07-11T09:51:47.076048Z","shell.execute_reply.started":"2026-07-11T09:51:46.361192Z","shell.execute_reply":"2026-07-11T09:51:47.075417Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. ⚙️ Prétraitement & chargement des volumes 3D\n*Imports du modèle, **graine aléatoire** fixée pour la reproductibilité, et fonctions de chargement.*\n\n**Améliorations (étape 3) :** coupes triées par **numéro** (bon ordre anatomique), sélection des coupes **centrales** (là où est la tumeur), et **normalisation z-score** sur tout le volume.","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom sklearn.model_selection import train_test_split\nimport cv2\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import classification_report, confusion_matrix, roc_curve, auc\n\n\n\n# === Reproductibilité : fixer le hasard ===\nimport os, random\nSEED = 42\nos.environ['PYTHONHASHSEED'] = str(SEED)\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\nprint(\"Graine fixée :\", SEED)\n\n\n\n\n\n# Path dataset\npath = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification/'\ntrain_labels = pd.read_csv(path + 'train_labels.csv')\n\n# Konfigurasi\nIMG_SIZE = 128\nBATCH_SIZE = 32\nEPOCHS = 30\nMODALITY = 'FLAIR' \n\n\n\ndef slice_number(filename):\n    \"\"\"Extrait le NUMÉRO de la coupe (ex: 'Image-10.dcm' -> 10) pour un tri correct.\"\"\"\n    digits = ''.join(ch for ch in filename if ch.isdigit())\n    return int(digits) if digits else 0\n\ndef load_dicom_image(filepath, img_size=IMG_SIZE):\n    \"\"\"Lit une image DICOM et la redimensionne. (Pas de normalisation ici : on le fait sur le volume.)\"\"\"\n    dicom = pydicom.dcmread(filepath)\n    img = dicom.pixel_array.astype(np.float32)\n    img = cv2.resize(img, (img_size, img_size))\n    return img\n\n\n\nMODALITY = 'FLAIR'   # on revient à FLAIR seul\n\n\ndef load_one_modality(patient_id, modality, num_slices=16):\n    \"\"\"Charge le volume z-scoré d'UNE modalité : forme (num_slices, 128, 128).\"\"\"\n    patient_path = os.path.join(path, 'train', str(patient_id).zfill(5), modality)\n    if not os.path.exists(patient_path):\n        return None\n\n    dicom_files = [f for f in os.listdir(patient_path) if f.endswith('.dcm')]\n    if not dicom_files:\n        return None\n\n    dicom_files = sorted(dicom_files, key=slice_number)        # bon ordre\n    n = len(dicom_files)\n    if n >= num_slices:                                        # coupes centrales\n        start = (n - num_slices) // 2\n        dicom_files = dicom_files[start:start + num_slices]\n\n    slices = [load_dicom_image(os.path.join(patient_path, f)) for f in dicom_files]\n    while len(slices) < num_slices:\n        slices.append(np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.float32))\n\n    volume = np.stack(slices, axis=0)                          # (num_slices, 128, 128)\n    nonzero = volume[volume > 0]                               # z-score (par modalité)\n    if nonzero.size > 0:\n        volume = (volume - nonzero.mean()) / (nonzero.std() + 1e-6)\n    return volume.astype(np.float32)\n\n\ndef load_patient_data(patient_id, num_slices=16):\n    \"\"\"FLAIR seul, répliqué en 3 canaux : forme (16, 128, 128, 3).\"\"\"\n    vol = load_one_modality(patient_id, MODALITY, num_slices)  # coupes centrales + z-score\n    if vol is None:\n        return None\n    return np.stack([vol] * 3, axis=-1).astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:51:47.076765Z","iopub.execute_input":"2026-07-11T09:51:47.077048Z","iopub.status.idle":"2026-07-11T09:52:01.262341Z","shell.execute_reply.started":"2026-07-11T09:51:47.077021Z","shell.execute_reply":"2026-07-11T09:52:01.261417Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. 🧱 Construction du jeu de données (X, y)\n*Chargement de tous les patients en un tableau `X` (les volumes) et `y` (les étiquettes). C'est l'étape la plus longue.*","metadata":{}},{"cell_type":"code","source":"# Membuat dataset\nX = []\ny = []\n\nprint(\"Memuat data training...\")\nfor idx, row in train_labels.iterrows():\n    patient_id = row['BraTS21ID']\n    label = row['MGMT_value']\n    \n    patient_data = load_patient_data(patient_id)\n    if patient_data is not None:\n        X.append(patient_data)\n        y.append(label)\n\n# Konversi ke numpy array\nX = np.array(X, dtype=np.float32)\ny = np.array(y, dtype=np.float32)\n\nprint(f\"Total data yang dimuat: {len(X)} sampel\")\nprint(f\"Distribusi kelas: {np.sum(y == 1)} positif, {np.sum(y == 0)} negatif\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:52:01.263134Z","iopub.execute_input":"2026-07-11T09:52:01.263675Z","iopub.status.idle":"2026-07-11T09:53:28.080476Z","shell.execute_reply.started":"2026-07-11T09:52:01.263642Z","shell.execute_reply":"2026-07-11T09:53:28.079778Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. ✂️ Découpage train / validation / test\n*70/15/15 stratifié : entraînement, validation (réglages), test (évaluation finale, jamais touché).*","metadata":{}},{"cell_type":"code","source":"# Split data: training (60%), validation (20%), test (20%)\nX_train, X_temp, y_train, y_temp = train_test_split(\n    X, y, test_size=0.4, random_state=42, stratify=y\n)\nX_val, X_test, y_val, y_test = train_test_split(\n    X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp\n)\n\nprint(\"\\nDistribusi dataset:\")\nprint(f\"Training:   {len(X_train)} sampel\")\nprint(f\"Validation: {len(X_val)} sampel\")\nprint(f\"Test:       {len(X_test)} sampel\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:28.081336Z","iopub.execute_input":"2026-07-11T09:53:28.081630Z","iopub.status.idle":"2026-07-11T09:53:28.666407Z","shell.execute_reply.started":"2026-07-11T09:53:28.081604Z","shell.execute_reply":"2026-07-11T09:53:28.665731Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. ⚖️ Pondération des classes (*class weight*)\n*Donne plus de poids à la classe la moins fréquente pour éviter que le modèle ne favorise la majorité.*","metadata":{}},{"cell_type":"code","source":"from sklearn.utils.class_weight import compute_class_weight\nimport numpy as np\n\nclasses = np.unique(y_train)\nweights = compute_class_weight('balanced', classes=classes, y=y_train)\nclass_weight = {int(c): float(w) for c, w in zip(classes, weights)}\nprint(\"Poids des classes :\", class_weight)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:28.667282Z","iopub.execute_input":"2026-07-11T09:53:28.667553Z","iopub.status.idle":"2026-07-11T09:53:28.673908Z","shell.execute_reply.started":"2026-07-11T09:53:28.667529Z","shell.execute_reply":"2026-07-11T09:53:28.673034Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. 🏗️ Architecture du modèle — CNN 3D\n*Trois blocs de convolution 3D + normalisation + dropout, puis une sortie « probabilité » (sigmoid).*","metadata":{}},{"cell_type":"code","source":"# Arsitektur model CNN 3D\ndef build_3d_cnn(input_shape, num_classes):\n    model = models.Sequential([\n        # Blok konvolusi 1\n        layers.Conv3D(16, (3, 3, 3), activation='relu', padding='same', input_shape=input_shape),\n        layers.BatchNormalization(),\n        layers.MaxPooling3D((2, 2, 2)),\n        layers.Dropout(0.2),\n        \n        # Blok konvolusi 2\n        layers.Conv3D(32, (3, 3, 3), activation='relu', padding='same'),\n        layers.BatchNormalization(),\n        layers.MaxPooling3D((2, 2, 2)),\n        layers.Dropout(0.3),\n        \n        # Blok konvolusi 3\n        layers.Conv3D(64, (3, 3, 3), activation='relu', padding='same'),\n        layers.BatchNormalization(),\n        layers.MaxPooling3D((2, 2, 2)),\n        layers.Dropout(0.4),\n        \n        layers.GlobalAveragePooling3D(),\n        layers.Dense(128, activation='relu'),\n        layers.Dropout(0.5),\n        layers.Dense(num_classes, activation='sigmoid')\n    ])\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:28.674713Z","iopub.execute_input":"2026-07-11T09:53:28.675990Z","iopub.status.idle":"2026-07-11T09:53:28.688237Z","shell.execute_reply.started":"2026-07-11T09:53:28.675972Z","shell.execute_reply":"2026-07-11T09:53:28.687300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Bangun model\ninput_shape = (X_train.shape[1], X_train.shape[2], X_train.shape[3], X_train.shape[4])\nprint(f\"\\nInput shape: {input_shape}\")\nmodel = build_3d_cnn(input_shape, num_classes=1)\n\n# Ringkasan model\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:28.689045Z","iopub.execute_input":"2026-07-11T09:53:28.689286Z","iopub.status.idle":"2026-07-11T09:53:30.812094Z","shell.execute_reply.started":"2026-07-11T09:53:28.689270Z","shell.execute_reply":"2026-07-11T09:53:30.811259Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. 🔧 Compilation & callbacks\n*Choix de l'optimiseur et des métriques ; arrêt anticipé (*EarlyStopping*) et réduction du pas d'apprentissage.*","metadata":{}},{"cell_type":"code","source":"# Kompilasi model\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n    loss='binary_crossentropy',\n    metrics=['accuracy', tf.keras.metrics.AUC(name='auc')]\n)\n\n# Callback\ncallbacks = [\n    tf.keras.callbacks.EarlyStopping(\n        patience=5, \n        monitor='val_auc', \n        mode='max', \n        restore_best_weights=True,\n        verbose=1\n    ),\n    tf.keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss', \n        factor=0.2, \n        patience=3, \n        min_lr=1e-6,\n        verbose=1\n    ),\n    tf.keras.callbacks.ModelCheckpoint(\n        filepath='best_model.h5',\n        save_best_only=True,\n        monitor='val_auc',\n        mode='max',\n        verbose=1\n    )\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:30.813000Z","iopub.execute_input":"2026-07-11T09:53:30.813510Z","iopub.status.idle":"2026-07-11T09:53:30.835099Z","shell.execute_reply.started":"2026-07-11T09:53:30.813487Z","shell.execute_reply":"2026-07-11T09:53:30.834621Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. 🔄 Augmentation de données (*data augmentation*)\n*Crée des variantes des images d'entraînement (retournements, luminosité) pour réduire le sur-apprentissage. Appliquée à l'entraînement **uniquement**.*","metadata":{}},{"cell_type":"code","source":"# === Étape 4 : augmentation de données (entraînement uniquement) ===\nAUTOTUNE = tf.data.AUTOTUNE\n\ndef augment(volume, label):\n    # retournement gauche/droite (tout le volume de la même façon)\n    if tf.random.uniform(()) > 0.5:\n        volume = tf.reverse(volume, axis=[2])\n    # retournement haut/bas\n    if tf.random.uniform(()) > 0.5:\n        volume = tf.reverse(volume, axis=[1])\n    # légère variation de luminosité (±10 %)\n    volume = volume * tf.random.uniform((), 0.9, 1.1)\n    return volume, label\n\ndef make_dataset(X_, y_, training=False, batch_size=BATCH_SIZE):\n    ds = tf.data.Dataset.from_tensor_slices((X_, y_))\n    if training:\n        ds = ds.shuffle(len(X_), seed=SEED).map(augment, num_parallel_calls=AUTOTUNE)\n    return ds.batch(batch_size).prefetch(AUTOTUNE)\n\ntrain_ds = make_dataset(X_train, y_train, training=True)\nval_ds   = make_dataset(X_val,   y_val,   training=False)\nprint(\"Datasets prêts — augmentation active sur l'entraînement uniquement.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:30.835775Z","iopub.execute_input":"2026-07-11T09:53:30.836411Z","iopub.status.idle":"2026-07-11T09:53:35.294492Z","shell.execute_reply.started":"2026-07-11T09:53:30.836393Z","shell.execute_reply":"2026-07-11T09:53:35.293639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. 🚀 Entraînement du modèle\n*Apprentissage sur les données augmentées, avec suivi des courbes de perte (*loss*) et d'AUC.*","metadata":{}},{"cell_type":"code","source":"#Training\nprint(\"\\nMemulai training...\")\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=EPOCHS,\n    callbacks=callbacks\n)\n\n# Plot history training\ndef plot_history(history):\n    plt.figure(figsize=(12, 5))\n    \n    # Plot loss\n    plt.subplot(1, 2, 1)\n    plt.plot(history.history['loss'], label='Training Loss')\n    plt.plot(history.history['val_loss'], label='Validation Loss')\n    plt.title('Training and Validation Loss')\n    plt.xlabel('Epoch')\n    plt.ylabel('Loss')\n    plt.legend()\n    \n    # Plot AUC\n    plt.subplot(1, 2, 2)\n    plt.plot(history.history['auc'], label='Training AUC')\n    plt.plot(history.history['val_auc'], label='Validation AUC')\n    plt.title('Training and Validation AUC')\n    plt.xlabel('Epoch')\n    plt.ylabel('AUC')\n    plt.legend()\n    \n    plt.tight_layout()\n    plt.savefig('training_history.png')\n    plt.show()\n\nplot_history(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:53:35.295373Z","iopub.execute_input":"2026-07-11T09:53:35.296007Z","iopub.status.idle":"2026-07-11T09:54:26.036308Z","shell.execute_reply.started":"2026-07-11T09:53:35.295987Z","shell.execute_reply":"2026-07-11T09:54:26.035627Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. 📈 Évaluation (validation & test)\n*Mesure des performances : perte, accuracy et AUC.*","metadata":{}},{"cell_type":"code","source":"# Evaluasi pada validation set\nprint(\"\\nEvaluasi pada validation set:\")\nval_loss, val_acc, val_auc = model.evaluate(X_val, y_val)\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_acc:.4f}\")\nprint(f\"Validation AUC: {val_auc:.4f}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:26.037218Z","iopub.execute_input":"2026-07-11T09:54:26.037511Z","iopub.status.idle":"2026-07-11T09:54:27.415410Z","shell.execute_reply.started":"2026-07-11T09:54:26.037487Z","shell.execute_reply":"2026-07-11T09:54:27.414452Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. 🎯 Seuil de décision optimisé (*threshold tuning*)\n*Recherche du meilleur seuil sur la **validation** (Youden's J) puis application au **test** pour équilibrer les décisions.*","metadata":{}},{"cell_type":"code","source":"# === Étape 2 : seuil de décision optimisé ===\nfrom sklearn.metrics import roc_curve   # (déjà importé plus haut, on le remet par sécurité)\n\n# 1) Les probabilités prédites par le modèle (des nombres entre 0 et 1)\nval_prob    = model.predict(X_val).flatten()    # sur la VALIDATION\ny_pred_prob = model.predict(X_test).flatten()   # sur le TEST\n\n# 2) On cherche le meilleur seuil SUR LA VALIDATION (méthode Youden's J)\nfpr, tpr, thr = roc_curve(y_val, val_prob)\nbest_thr = float(thr[np.argmax(tpr - fpr)])\nprint(\"Seuil par défaut : 0.5\")\nprint(\"Seuil optimal    :\", round(best_thr, 3))\n\n# 3) On applique ce seuil optimal au TEST pour décider 0 ou 1\ny_pred = (y_pred_prob > best_thr).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:27.416277Z","iopub.execute_input":"2026-07-11T09:54:27.416660Z","iopub.status.idle":"2026-07-11T09:54:31.057792Z","shell.execute_reply.started":"2026-07-11T09:54:27.416634Z","shell.execute_reply":"2026-07-11T09:54:31.056872Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Évaluation finale sur le TEST (une seule fois, seuil fixé sur la validation) ===\nfrom sklearn.metrics import roc_auc_score, balanced_accuracy_score, accuracy_score\n\ntest_auc  = roc_auc_score(y_test, y_pred_prob)     # AUC : indépendant du seuil\ntest_acc  = accuracy_score(y_test, y_pred)         # accuracy AU SEUIL OPTIMISÉ\ntest_bacc = balanced_accuracy_score(y_test, y_pred)\n\nprint(f\"Seuil utilisé     : {best_thr:.3f}  (fixé sur la validation)\")\nprint(f\"Test AUC          : {test_auc:.4f}\")\nprint(f\"Test Accuracy     : {test_acc:.4f}\")\nprint(f\"Balanced accuracy : {test_bacc:.4f}\\n\")\n\nprint(\"Classification Report :\")\nprint(classification_report(y_test, y_pred))\nprint(\"Confusion Matrix :\")\nprint(confusion_matrix(y_test, y_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:31.058677Z","iopub.execute_input":"2026-07-11T09:54:31.058949Z","iopub.status.idle":"2026-07-11T09:54:31.082306Z","shell.execute_reply.started":"2026-07-11T09:54:31.058927Z","shell.execute_reply":"2026-07-11T09:54:31.081402Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 14. 📋 Rapport de classification & matrice de confusion\n*Détail des bonnes/mauvaises prédictions par classe.*","metadata":{}},{"cell_type":"code","source":"# Classification report\nprint(\"\\nClassification Report:\")\nprint(classification_report(y_test, y_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:31.083327Z","iopub.execute_input":"2026-07-11T09:54:31.083680Z","iopub.status.idle":"2026-07-11T09:54:31.097059Z","shell.execute_reply.started":"2026-07-11T09:54:31.083653Z","shell.execute_reply":"2026-07-11T09:54:31.096211Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Confusion matrix\nconf_matrix = confusion_matrix(y_test, y_pred)\nprint(\"\\nConfusion Matrix:\")\nprint(conf_matrix)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:31.097878Z","iopub.execute_input":"2026-07-11T09:54:31.098565Z","iopub.status.idle":"2026-07-11T09:54:31.105217Z","shell.execute_reply.started":"2026-07-11T09:54:31.098541Z","shell.execute_reply":"2026-07-11T09:54:31.104623Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 15. 📉 Courbe ROC\n*Visualise la capacité du modèle à séparer les deux classes (AUC : 0,5 = hasard, 1 = parfait).*","metadata":{}},{"cell_type":"code","source":"# Plot ROC curve\nfpr, tpr, thresholds = roc_curve(y_test, y_pred_prob)\nroc_auc = auc(fpr, tpr)\n\nplt.figure()\nplt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')\nplt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\nplt.xlim([0.0, 1.0])\nplt.ylim([0.0, 1.05])\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.title('Receiver Operating Characteristic')\nplt.legend(loc=\"lower right\")\nplt.savefig('roc_curve.png')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:31.106022Z","iopub.execute_input":"2026-07-11T09:54:31.106189Z","iopub.status.idle":"2026-07-11T09:54:31.378099Z","shell.execute_reply.started":"2026-07-11T09:54:31.106176Z","shell.execute_reply":"2026-07-11T09:54:31.377160Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 16: validation croisée 5-fold (le vrai verdict)","metadata":{}},{"cell_type":"code","source":"# === Étape 5 : validation croisée 5-fold (le vrai verdict) ===\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score, roc_curve, balanced_accuracy_score\n\ninput_shape = X.shape[1:]                 # (16, 128, 128, 3)\nskf = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n\nauc_scores, bacc_scores = [], []\n\nfor fold, (train_idx, val_idx) in enumerate(skf.split(X, y), start=1):\n    print(f\"\\n===== Fold {fold}/5 =====\")\n\n    # 1) données de ce fold\n    X_tr, X_va = X[train_idx], X[val_idx]\n    y_tr, y_va = y[train_idx], y[val_idx]\n\n    # 2) datasets (augmentation sur l'entraînement UNIQUEMENT) — ta fonction de l'étape 4\n    tr_ds = make_dataset(X_tr, y_tr, training=True)\n    va_ds = make_dataset(X_va, y_va, training=False)\n\n    # 3) un NOUVEAU modèle repartant de zéro à chaque fold\n    model_cv = build_3d_cnn(input_shape, num_classes=1)\n    model_cv.compile(optimizer=tf.keras.optimizers.Adam(1e-3),\n                     loss='binary_crossentropy',\n                     metrics=[tf.keras.metrics.AUC(name='auc')])\n\n    es = tf.keras.callbacks.EarlyStopping(monitor='val_auc', mode='max',\n                                          patience=6, restore_best_weights=True, verbose=0)\n\n    # 4) entraînement (verbose=0 = silencieux, pour ne pas noyer l'affichage)\n    model_cv.fit(tr_ds, validation_data=va_ds, epochs=EPOCHS,\n                 callbacks=[es], verbose=0)\n\n    # 5) prédictions sur le fold de test\n    prob = model_cv.predict(X_va, verbose=0).flatten()\n\n    # 6) AUC (ne dépend pas du seuil)\n    fold_auc = roc_auc_score(y_va, prob)\n\n    # 7) seuil optimisé (Youden's J) -> balanced accuracy\n    fpr, tpr, thr = roc_curve(y_va, prob)\n    best_thr = thr[np.argmax(tpr - fpr)]\n    fold_bacc = balanced_accuracy_score(y_va, (prob > best_thr).astype(int))\n\n    auc_scores.append(fold_auc)\n    bacc_scores.append(fold_bacc)\n    print(f\"AUC = {fold_auc:.3f} | balanced accuracy = {fold_bacc:.3f}\")\n\nprint(\"\\n================ RÉSULTAT FINAL ================\")\nprint(f\"AUC moyen         : {np.mean(auc_scores):.3f} ± {np.std(auc_scores):.3f}\")\nprint(f\"Balanced accuracy : {np.mean(bacc_scores):.3f} ± {np.std(bacc_scores):.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:54:31.379014Z","iopub.execute_input":"2026-07-11T09:54:31.379522Z","iopub.status.idle":"2026-07-11T09:59:08.648717Z","shell.execute_reply.started":"2026-07-11T09:54:31.379496Z","shell.execute_reply":"2026-07-11T09:59:08.647978Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 17. 🤝 Ensemble (levier 2) — résultat final\n   *Moyenne de 5 modèles pour un résultat plus haut et plus stable — le chiffre à présenter.*","metadata":{}},{"cell_type":"code","source":"# === Levier 2 : Ensembling (faire voter plusieurs modèles) ===\nfrom sklearn.metrics import (roc_auc_score, roc_curve, balanced_accuracy_score,\n                             accuracy_score, confusion_matrix, classification_report)\n\nN_MODELS = 5\ninput_shape = X_train.shape[1:]\n\nval_probs_all, test_probs_all = [], []\n\nfor i in range(N_MODELS):\n    print(f\"--- Entraînement du modèle {i+1}/{N_MODELS} ---\")\n    tf.random.set_seed(100 + i)          # une graine différente par modèle\n\n    m = build_3d_cnn(input_shape, num_classes=1)\n    m.compile(optimizer=tf.keras.optimizers.Adam(1e-3),\n              loss='binary_crossentropy',\n              metrics=[tf.keras.metrics.AUC(name='auc')])\n    es = tf.keras.callbacks.EarlyStopping(monitor='val_auc', mode='max',\n                                          patience=6, restore_best_weights=True, verbose=0)\n\n    tr_ds = make_dataset(X_train, y_train, training=True)\n    va_ds = make_dataset(X_val,   y_val,   training=False)\n    m.fit(tr_ds, validation_data=va_ds, epochs=EPOCHS, callbacks=[es], verbose=0)\n\n    val_probs_all.append(m.predict(X_val,  verbose=0).flatten())\n    test_probs_all.append(m.predict(X_test, verbose=0).flatten())\n\n# Le \"vote\" de l'ensemble = moyenne des probabilités\nval_prob_ens  = np.mean(val_probs_all,  axis=0)\ntest_prob_ens = np.mean(test_probs_all, axis=0)\n\n# Seuil optimisé sur la validation, appliqué au test\nfrom sklearn.metrics import accuracy_score\n# seuil qui maximise l'ACCURACY sur la validation (métrique visée par le prof)\ngrille = np.linspace(0.05, 0.95, 181)\naccs   = [accuracy_score(y_val, (val_prob_ens > t).astype(int)) for t in grille]\nbest_thr = float(grille[np.argmax(accs)])\ny_pred_ens = (test_prob_ens > best_thr).astype(int)\n\nprint(f\"\\n============ ENSEMBLE ({N_MODELS} modèles) ============\")\nprint(f\"Seuil utilisé     : {best_thr:.3f}\")\nprint(f\"Test AUC          : {roc_auc_score(y_test, test_prob_ens):.4f}\")\nprint(f\"Test Accuracy     : {accuracy_score(y_test, y_pred_ens):.4f}\")\nprint(f\"Balanced accuracy : {balanced_accuracy_score(y_test, y_pred_ens):.4f}\")\nprint(\"\\nConfusion Matrix :\")\nprint(confusion_matrix(y_test, y_pred_ens))\nprint(\"\\nClassification Report :\")\nprint(classification_report(y_test, y_pred_ens))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T09:59:08.649469Z","iopub.execute_input":"2026-07-11T09:59:08.649719Z","iopub.status.idle":"2026-07-11T10:03:42.858668Z","shell.execute_reply.started":"2026-07-11T09:59:08.649702Z","shell.execute_reply":"2026-07-11T10:03:42.857939Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 18. 💾 Sauvegarde du modèle\n*Enregistrement du modèle entraîné pour réutilisation.*","metadata":{}},{"cell_type":"code","source":"# Simpan model akhir\nmodel.save('final_model.h5')\nprint(\"\\nModel akhir disimpan sebagai 'final_model.h5'\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-11T10:03:42.859483Z","iopub.execute_input":"2026-07-11T10:03:42.859763Z","iopub.status.idle":"2026-07-11T10:03:42.916503Z","shell.execute_reply.started":"2026-07-11T10:03:42.859744Z","shell.execute_reply":"2026-07-11T10:03:42.915900Z"}},"outputs":[],"execution_count":null}]}