{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"gpu","dataSources":[{"sourceId":124046,"databundleVersionId":14573278,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":8396.254863,"end_time":"2026-01-15T02:27:21.947230","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-15T00:07:25.692367","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Projet de Classification d'Images FIB/SEM\n\n## Contexte\n\nL'imagerie de microscopie électronique à balayage à faisceau d'ions focalisé (FIB/SEM) permet d'obtenir des images 3D à très haute résolution pour visualiser des objets cellulaires. Ce projet vise à classifier des imagettes 2D de taille 247x247 obtenu à partir de ces images 3D selon 5 classes basées sur le pixel central :\n\n Classe à classifier:\n - 0 : Aucune classe annotée\n - 1 : Mitochondrie\n - 2 : Réticulum endoplasmique\n - 3 : Membrane nucléaire\n - 4 : Membrane cellulaire\n","metadata":{"papermill":{"duration":0.003641,"end_time":"2026-01-15T00:07:29.620488","exception":false,"start_time":"2026-01-15T00:07:29.616847","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# IMPORTS ET CONFIGURATION\n\ncompetition_name = \"2025-competition-apprentissage-i-3-d\"\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nimport os\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\n\nfrom PIL import Image\n# TensorFlow / Keras\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Dense, Activation, Conv2D, MaxPooling2D, Flatten, Dropout\nfrom tensorflow.keras.utils import to_categorical, image_dataset_from_directory, load_img, img_to_array\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.losses import CategoricalCrossentropy\nfrom tensorflow.keras.regularizers import l2\n\nnp.random.seed(10)\n\n# Config\nTRAIN_PATH =\"/kaggle/input/2025-competition-apprentissage-i-3-d/train\"\nTEST_PATH = \"/kaggle/input/2025-competition-apprentissage-i-3-d/test\"\nTRAIN_CSV = \"/kaggle/input/2025-competition-apprentissage-i-3-d/train.csv\"\n\nCLASS = {0: \"Aucune classe annotée\", 1: \"Mitochondrie\", 2: \"Reticulum endoplasmique\", 3: \"Membrane nucléaire\", 4: \"Membrane cellulaire\"}","metadata":{"execution":{"iopub.status.busy":"2026-01-20T02:12:22.853584Z","iopub.execute_input":"2026-01-20T02:12:22.854395Z","iopub.status.idle":"2026-01-20T02:13:11.664977Z","shell.execute_reply.started":"2026-01-20T02:12:22.854366Z","shell.execute_reply":"2026-01-20T02:13:11.664370Z"},"papermill":{"duration":43.662054,"end_time":"2026-01-15T00:08:13.287818","exception":false,"start_time":"2026-01-15T00:07:29.625764","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Chargement et préparation des données\n\n- chargement des données CSV à l'aide de `read_csv`\n- chargement des données des images dans des tableaux numpy en itérants sur les lignes du fichiers csv pour récupérer les données des images et en les convertissant en tableau pour `X` et en récupérant la valeur contenant dans la colonne `Category` pour `y`.\n- Séparation des données en un jeu d’entraînement (80 %) et un jeu de validation (20 %), en utilisant la stratification (stratify) afin de conserver les mêmes proportions de classes dans les deux jeux.\n- Conversion en catégories à l’aide de `to_categorical` qui réalise un one-hot encoding sur les labels","metadata":{}},{"cell_type":"code","source":"# Chargement des données CSV\n\ntrain = pd.read_csv(TRAIN_CSV)\nprint(f\"Nombre d'images: {len(train)}\")\n\n# Chargement des données des images d'entrainement et de validation\n\nX =[]\ny = []\n    \nfor idx, row in train.iterrows():\n    # On utilise load_img pour charger l image en grayscale\n    # os.path.join permet de construire le path complet de l'image en joignant le lien du \n    # dossier et le nom de l'image\n    # l'image sera transformé au format Py5\n    img = load_img(os.path.join(TRAIN_PATH, f\"{row['ID']}.png\"),color_mode='grayscale')\n    # Conversion en tableau\n    img_array = img_to_array(img, dtype='float32')\n    # Ajout du tableau précédent à la liste X\n    X.append(img_array)\n    # Ajout de la catégorie à la liste y\n    y.append(row['Category'])\n    \nprint(\"Images chargées\")\n\n#transformation en tableau numpy\nX = np.array(X)\ny = np.array(y)\n\nprint(f\"Shape: {X.shape}\")\n\n#Separation des données X et y 80/20 avec 80 pour l'entrainement et 20 pour la validation\n# stratify=y permet de conserver le même pourcentage de chaque classe dans les données d'entrainement et de validation\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)\n\n# Conversion en categories en utilisant le one-hot encoding\ny_train_cat = to_categorical(y_train, len(CLASS))\ny_val_cat = to_categorical(y_val, len(CLASS))\n\nprint(f\"X_train: {X_train.shape}, X_val: {X_val.shape}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-20T02:19:48.816804Z","iopub.execute_input":"2026-01-20T02:19:48.817743Z","iopub.status.idle":"2026-01-20T02:20:44.793635Z","shell.execute_reply.started":"2026-01-20T02:19:48.817713Z","shell.execute_reply":"2026-01-20T02:20:44.792710Z"},"papermill":{"duration":90.679527,"end_time":"2026-01-15T00:09:43.969773","exception":false,"start_time":"2026-01-15T00:08:13.290246","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Définition du modèle\n\nle modèle est décomposé de la façon suivante:\n- normalisation des pixels\n- génération de nouvelles données d'entrainement en réalisant des rotations et des inversions horizontales et verticales avec une probabilité aléatoire pour augmenter le jeu de donnéee et éviter le surentrainement\n- une 1ère couche de convulution avec 16 filtres avec une régularisation des données L2 (réduction de la valeur des poids les plus forts), normalisation des données des batch et une activation relu. ces opérations sont réalisés une seconde fois avant de réalisé un maxpooling et ainsi condensé les informations sur une zone d'image plus petite\n- une 2ème couche de convolution cette fois-ci avec 32 filtres qui suit le même principe que la couche précédente\n- une 3ème couche de convulution avec 64 filtres x2 qui sera applatie dans un vecteur à une dimension\n- Une couche dense de 256 neurones avec régularition des poids, normalisation des batchs et activation relu\n- couche finale de 5 neurones (pour les 5 classes) avec une activation softmax utiliser pour la classification","metadata":{}},{"cell_type":"code","source":"# model\n\n# Definition du model de convolution\n\ndef model_convolution(input_shape, nb_class):\n\n    model = Sequential([\n        layers.Input(shape=input_shape),\n        \n        # preprocessing\n        \n        # normalisation des pixels\n        layers.Rescaling(1./255),\n        #permet de générer des images modifiées pour augmenter la taille des données d'entrainement\n        # en réalisant des inversions horizontales et verticales ainsi que des rotations de 90°\n        # de façon aléatoire\n        layers.RandomFlip(\"horizontal_and_vertical\"),\n        layers.RandomRotation(0.25),\n        \n        # Couche 1: Convolution : 257x257 -> 129x129\n\n        # Convolution avec regularization L2\n        # L2 regularization aide à réduire le surapprentissage en ajoutant \n        # une pénalité aux poids du modèle les plus grands\n        layers.Conv2D(16, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        #layer de normalisation par lot pour stabiliser et accélérer l'apprentissage\n        layers.BatchNormalization(),\n        # activation relu standard pour les réseaux de neurones convolutifs\n        layers.Activation('relu'),\n        # deuxième passe de convolution\n        layers.Conv2D(16, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        # réduction de la taille de l'image par 2\n        layers.MaxPooling2D(pool_size=(2, 2), padding='same'),\n        \n        # Couche 2: Convolution : 129x129 -> 65x65\n        \n        # même principes que la couche précédente sauf qu'on augmente le nombre de filtres\n        # et donc de caractéristiques apprises\n        layers.Conv2D(32, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        layers.Conv2D(32, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        layers.MaxPooling2D(pool_size=(2, 2), padding='same'),\n        \n        # Couche 3: Convolution : 65x65 -> 33x33\n        layers.Conv2D(64, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        layers.Conv2D(64, (3, 3), padding='same', kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        layers.MaxPooling2D(pool_size=(2, 2), padding='same'),\n        layers.Dropout(0.4),\n        \n        # Couche 4: Dense : 256 neurones\n        \n        # Aplatir les caractéristiques extraites en un vecteur 1une dimension\n        layers.Flatten(),\n\n        # transformation en couche dense avec 256 neurones\n        layers.Dense(256, kernel_regularizer=l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Activation('relu'),\n        layers.Dropout(0.5),\n\n        # Couche 5: Dense : 5 neurones\n        layers.Dense(nb_class, activation='softmax')\n    ])\n    return model\n\n# Créer le modèle\nmodel = model_convolution((257, 257, 1), len(CLASS))\n\n# Afficher le résumé du modèle\nmodel.summary()\n\n# Compilation du modèle\nmodel.compile(\n    # Utilisation de la entropie croisée catégorielle pour la classification multi-classes\n    loss=CategoricalCrossentropy(),\n    # Utilisation de Adam comme en tp\n    optimizer = Adam(learning_rate=0.001), \n    metrics=['accuracy']\n)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2026-01-20T00:48:48.700354Z","iopub.execute_input":"2026-01-20T00:48:48.700877Z","iopub.status.idle":"2026-01-20T00:48:48.711237Z","shell.execute_reply.started":"2026-01-20T00:48:48.700850Z","shell.execute_reply":"2026-01-20T00:48:48.710372Z"},"papermill":{"duration":2.498402,"end_time":"2026-01-15T00:09:46.471179","exception":false,"start_time":"2026-01-15T00:09:43.972777","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Entrainement du modèle\n\nAvant l'entrainement du modèle, on définit les tailles des lots (`32`) ainsi que le nombre d'époque (`100`) sur lequel notre modèle sera entrainé.\nPour commencer avec une validation élevé (supéraieur à 55% à la 1ère époque) on utilise `compute_class_weight` qui permet de déterminer les poids des classes en fonction de leur fréquence dans les données d'entrainement.\nPour permettre d'arriver un niveau plateau, on ajoute 2 fonctions de callback qui permettent:\n- pour `EarlyStopping` d'arrêter le modèle si il n'y a pas, dans notre cas, d'amélioration de la loss après 10 époques\n- pour `ReduceLROnPlateau` de réduite le learning rate si il n'y a pas d'amélioration de la loss de validation après 4 époques\n\nverbose 2 est utilisé pour faciliter la compréhension des logs.\n","metadata":{}},{"cell_type":"code","source":"# paramètres d'entrainement\nbatch_size = 32\nepoch = 100\n\nprint(f\"Configuration de l'entrainement:\")\nprint(f\"Batch size: {batch_size}\")\nprint(f\"epoch max: {epoch}\")\nprint(f\"Taille X_train: {len(X_train)}\")\nprint(f\"Taille X_val: {len(X_val)}\")\n\n# Calcul des poids de classe\n# compute_class_weight permet de calculer des poids pour chaque classe\n# en fonction de leur fréquence dans les données d'entrainement\n# class_weight='balanced' ajuste les poids de sorte que chaque classe ait la même influence égale\nclass_weights = compute_class_weight(\n    class_weight='balanced',\n    classes=np.unique(y_train),\n    y=y_train\n)\n\n# formatage des poids des classes dans un dictionnaire pour le format de la fonction fit\ny_weight = dict(enumerate(class_weights))\nprint(\"Poids des classes:\", y_weight)\n\n# optimiser l entrainement\ncallbacks = [\n    # Arrêter l'entrainement si la validation loss ne s'améliore pas pendant 10 epochs\n    EarlyStopping(\n        monitor='val_loss',\n        patience=10,\n        restore_best_weights=True,\n        verbose=2\n    ),\n    # Réduire le taux d'apprentissage si la validation loss ne s'améliore pas pendant 4 epochs\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=4,\n        min_lr=1e-7,\n        verbose=2\n    )\n]\n\n\nprint(\"Debut de l entrainement\")\n\n# entrainement du modèle\nhistory = model.fit(\n    X_train,\n    y_train_cat,\n    batch_size=batch_size,\n    epochs=epoch,\n    validation_data=(X_val, y_val_cat),\n    callbacks=callbacks,\n    class_weight=y_weight,\n    validation_batch_size=batch_size,\n    verbose=2\n)\n\nprint(\"Entrainement termine\")","metadata":{"execution":{"iopub.execute_input":"2026-01-15T00:09:46.479967Z","iopub.status.busy":"2026-01-15T00:09:46.479682Z","iopub.status.idle":"2026-01-15T02:26:21.817405Z","shell.execute_reply":"2026-01-15T02:26:21.816479Z"},"papermill":{"duration":8195.343504,"end_time":"2026-01-15T02:26:21.818770","exception":false,"start_time":"2026-01-15T00:09:46.475266","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Courbes d'apprentissage de la loss et de l'accuracy\n\nCourbes d'apprentissage de la loss et de l'accuracy pour comparer les valeurs d'entrainement et de validation et ainsi vérifier la présence ou non de surentrainement (si les deux courbes se sépare à partir d'une certaine époque alors il y a surentrainement).\nDans notre cas on peut remarquer que les valeurs d'accuracy et de loss sont proches l'un de l'autre et donc il n'y a pas de surapprentissage.","metadata":{}},{"cell_type":"code","source":"# COURBES D'APPRENTISSAGE\n\nplt.figure(figsize=(12.4, 4.8))\n\n# Affichage des courbes d'apprentissage de la loss d'entrainement et de validation\nplt.subplot(1, 2, 1)\nplt.plot(history.history['loss'], label='entrainement')\nplt.plot(history.history['val_loss'], label='validation')\nplt.title('Loss entrainement vs Loss validation')\nplt.ylabel('Loss')\nplt.xlabel('époque')\nplt.legend()\n\n# Affichage des courbes d'apprentissage de l'accuracy d'entrainement et de validation\nplt.subplot(1, 2, 2)\nplt.plot(history.history['accuracy'], label='entrainement')\nplt.plot(history.history['val_accuracy'], label='validation')\nplt.title('Accuracy entrainement vs Accuracy validation')\nplt.ylabel('Accuracy')\nplt.xlabel('époque')\nplt.legend()\n\nplt.show()\n\n# Evalution du modèle\nevaluation = model.evaluate(X_val, y_val_cat)\nprint(f\"evaluation : {evaluation}\")","metadata":{"execution":{"iopub.status.busy":"2026-01-20T00:41:28.969163Z","iopub.execute_input":"2026-01-20T00:41:28.969393Z","iopub.status.idle":"2026-01-20T00:41:28.979255Z","shell.execute_reply.started":"2026-01-20T00:41:28.969371Z","shell.execute_reply":"2026-01-20T00:41:28.978193Z"},"papermill":{"duration":6.049362,"end_time":"2026-01-15T02:26:27.880047","exception":false,"start_time":"2026-01-15T02:26:21.830685","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prédiction du modèle\n\nPour réaliser la prédiction du modèle, il faut dans un premier temps chargés les images de test.\nPour ce faire, on parcours tous les éléments du dossier à l'aide de la fonction `os.listdir`. On charge les images en joignant le path pour arriver au dossier avec les noms des images présentes dans le fichier. ces images sont ensuite transformer en tableau ajouter à la liste x_test puis seront transformer en tableau numpy. On extrait aussi les ID des images qui nous servirons à créé le CSV résultat.","metadata":{}},{"cell_type":"code","source":"# PRÉDICTION\n\n# Chargement des images de test\n\ntest_ids = []\nX_test = []\n# parcours du dossier de test\nfor path in os.listdir(TEST_PATH):\n      # chargement de l'image en grayscale\n      img = load_img(os.path.join(TEST_PATH, path), color_mode='grayscale')\n      # conversion en tableau et ajout à la liste X_test\n      X_test.append(img_to_array(img, dtype='float32'))\n      # extraction de l'ID de l'image et ajout à la liste test_ids\n      test_ids.append(int(path.replace('.png', '')))\n\n# conversion en tableau numpy\nX_test = np.array(X_test)\n\nprint(f\"X_test: {X_test.shape}\")\n\n# Prédiction du modèle sur les données de test\ny_pred = model.predict(X_test)\n# Conversion des probabilités en classes\ntest_pred = np.argmax(y_pred, axis=1)\nprint(f\"Distribution: {np.bincount(test_pred)}\")","metadata":{"execution":{"iopub.execute_input":"2026-01-15T02:26:27.913175Z","iopub.status.busy":"2026-01-15T02:26:27.912902Z","iopub.status.idle":"2026-01-15T02:27:18.693861Z","shell.execute_reply":"2026-01-15T02:27:18.692963Z"},"papermill":{"duration":50.798944,"end_time":"2026-01-15T02:27:18.695364","exception":false,"start_time":"2026-01-15T02:26:27.896420","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Création du fichier de rendu\n\nles données prédictes sont enregistrés dan un tableau avant d'être enregistrer dans `/kaggle/working/resultat.csv`.","metadata":{}},{"cell_type":"code","source":"# SOUMISSION\n\n# Création d'un tableau de données\nresultat = pd.DataFrame({'ID': test_ids, 'Category': test_pred})\n# Enregistrement du tableau de données au format CSV\nresultat.to_csv(\"/kaggle/working/resultat.csv\", index=False)\nprint(\"resultat enregistre\")","metadata":{"execution":{"iopub.execute_input":"2026-01-15T02:27:18.733342Z","iopub.status.busy":"2026-01-15T02:27:18.733089Z","iopub.status.idle":"2026-01-15T02:27:18.748455Z","shell.execute_reply":"2026-01-15T02:27:18.747658Z"},"papermill":{"duration":0.03548,"end_time":"2026-01-15T02:27:18.749582","exception":false,"start_time":"2026-01-15T02:27:18.714102","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}