{"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":[{"sourceId":124046,"databundleVersionId":14573278,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Imports du projet","metadata":{}},{"cell_type":"code","source":"# IMPORTS pour les blocs suivants\n\nfrom tensorflow.keras import layers, models, optimizers\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nimport tensorflow as tf\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport cv2\nimport os","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T08:09:43.458122Z","iopub.execute_input":"2026-01-21T08:09:43.458344Z","iopub.status.idle":"2026-01-21T08:09:58.817651Z","shell.execute_reply.started":"2026-01-21T08:09:43.458322Z","shell.execute_reply":"2026-01-21T08:09:58.816960Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Chargement des données d'entraînement et de test\n\nAfin de développer un réseau de neuronnes convolutif à partir d'un jeu de données, on importe le tout en se référant aux fichiers .csv associés et en attribuant un label aux classes numérotés de 0 à 4 pour créer un affichage plus compréhensible à la lecture.\n\nEnsuite on sélectionne 5 inférences d'images appartenant aux 5 classes du jeu d'entrainement pour les afficher afin de vérifier la bonne importation des données avant définition du modèle.","metadata":{}},{"cell_type":"code","source":"# Chargement des données\n\nbase_path = '/kaggle/input/2025-competition-apprentissage-i-3-d/'\ntrain_dir = f'{base_path}train/'\ntest_dir = f'{base_path}test/'\n\n# Importation des fichiers csv\ntrain_df = pd.read_csv(f'{base_path}train.csv')\ntrain_df['ID'] = train_df['ID'].astype(str) + \".png\"\ntrain_df['Category'] = train_df['Category'].astype(str)\n\ntest_df = pd.read_csv(f'{base_path}sampleSubmission.csv')\ntest_df['ID'] = test_df['ID'].astype(str) + \".png\"\n\n# Identification des classes\nCLASS_NAMES = {\n    '0': 'Autre', \n    '1': 'Mitochondrie', \n    '2': 'Réticulum E.', \n    '3': 'Memb. Nucléaire', \n    '4': 'Memb. Cellulaire'\n}\n\n# Affichage d'une sélection d'images à traiter (5 aléatoires par classe)\nprint(\"Visualisation des classes\")\nfig, axes = plt.subplots(5, 5, figsize=(15, 12))\nfor class_id in range(5):\n    samples = train_df[train_df['Category'] == str(class_id)].sample(5, random_state=42)\n    for idx, (_, row) in enumerate(samples.iterrows()):\n        img_path = os.path.join(train_dir, row['ID'])\n        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n        \n        ax = axes[class_id, idx]\n        if img is not None:\n            ax.imshow(img, cmap='gray')\n        ax.axis('off')\n        if idx == 0:\n            ax.set_title(CLASS_NAMES[str(class_id)], x=-0.5, y=0.3, fontsize=12, fontweight='bold')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T08:09:58.819816Z","iopub.execute_input":"2026-01-21T08:09:58.820263Z","iopub.status.idle":"2026-01-21T08:10:00.683250Z","shell.execute_reply.started":"2026-01-21T08:09:58.820237Z","shell.execute_reply":"2026-01-21T08:10:00.682304Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Traitement et augmentation des jeux de données\n\nAfin de permette un meilleur traitement des données lors de l'apprentissage du modèle que nous allons définir dans la prochaine section, nous devons modifier les valeurs afin de permettre une meilleure convergence de l'accuracy.\n\n**1. Normalisation**\n\n   Dans un premier temps on réduit les valeurs de chaque pixels en flottants dans l'intervalle \\[0, 1\\] :\n\n```python\nrescale=1./255,\n```\n\n**2. Augmentation des données**\n\n   Comme le jeu de données d'entrainement initial n'est pas assez explicite pour que notre modèle s'entraine de manière efficace, nous allons \"étendre\" cet ensemble en appliquant de simple transormations aux images préexistantes pour en créer de nouvelles :\n\n   ```python\n   rotation_range=20,    # Rotation à 20°\n   horizontal_flip=True, # Réflection horizontale\n   vertical_flip=True,   # Réflection verticale\n   fill_mode='nearest',  # Comble les pixels vides créés\n                         # lors de la rotation\n   ```\n\n**3. Utilisation du jeu de données**\n\n   Après avoir importé les données à étudier dans le bloc précédent, nous allons diviser les données d'entrainement en 2 sous-ensembles : un ensemble d'entrainement (80%) et un ensemble de validation (20% restant) d'où les deux ensembles __train_generator__ et __val_generator__.\n\n   Dans les champs de ces nouveaux sous-ensembles, on définit que les images cibles sont de proportions 257  x 257 (format original), que l'entrainement se fera par lot de 32 images pour permettre une meilleure performance de l'entrainement et que le mode de classification visait à catégoriser chaque échantillon à une classe parmi plusieurs :\n\n   ```python\n    target_size = (257, 257),\n    batch_size = 32,\n    class_mode = 'categorical',\n```","metadata":{}},{"cell_type":"code","source":"# Préparation des générateurs de données\n\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255,\n    validation_split=0.2,\n    rotation_range=20,\n    horizontal_flip=True,\n    vertical_flip=True,\n    fill_mode='nearest'\n)\n\ntest_datagen = ImageDataGenerator(rescale=1./255)\n\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe = train_df,\n    directory = train_dir,\n    x_col = \"ID\",\n    y_col = \"Category\",\n    target_size = (257, 257),\n    batch_size = 32,\n    class_mode = \"categorical\",\n    subset = \"training\"\n)\n\nval_generator = train_datagen.flow_from_dataframe(\n    dataframe=train_df,\n    directory=train_dir,\n    x_col=\"ID\",\n    y_col=\"Category\",\n    target_size=(257, 257),\n    batch_size=32,\n    class_mode=\"categorical\",\n    subset=\"validation\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T08:10:00.684673Z","iopub.execute_input":"2026-01-21T08:10:00.684980Z","iopub.status.idle":"2026-01-21T08:10:49.826294Z","shell.execute_reply.started":"2026-01-21T08:10:00.684946Z","shell.execute_reply":"2026-01-21T08:10:49.825417Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Définition du modèle et entrainement\n\n### Structure du réseau\n\nAfin de permettre une apprentissage précis et une convergence de l'accuracy, le modèle comporte 15 couches au total suivant un schéma répétitif afin de s'entrainer sur des exemples de plus en plus précis après chaque traitement : \n\n**1. Couche de convolution**\n\n   On commence le traitement par une couche de convolution avec un padding définit à __same__ qui signifie que la taille de l'image de sortie est la même que la taille de l'image d'entrée. \n   \n   Avec un masque de taille 3 x 3 les pixels sur les bords de l'image seront comblés avec du padding.\n\n**2. Couche de max pooling**\n\n   La couche de max pooling permet de récupérer la valeur maximale sur la taille du masque appliquée et ainsi de réduire la taille de l'image pour les couches suivantes ainsi que son nombre de paramètres. \n\n   Ici avec un masque de taille 2 x 2 la taille de l'image de sortie sera divisée par 2 par rapport à l'image d'entrée, réduisant ainsi son nombre de paramètres appris.\n\n**3. Couches denses** \n\n   Après avoir suffisament réduit la taille de l'image et effectué un nombre de convolutions croissant au fur et à mesure du modèle, nous applatissons ces images 3D en un vecteur de taille 256 pour la première couche et de taille 5 pour la dernière couche en appliquant différentes fonctions d'activation.\n\n   Dans cette partie nous avons ajouté une couche de dropout permettant de mettre à la valeur 0, 50% des poids de la couche dense précédente pour que le modèle ne dépende pas de neurones spécifiques.\n\n   Enfin la dernière couche à 5 neuronnes consister à permettre une attribution plus claire des images en sortie à l'une des 5 classes étudiée.\n\n## Optimisation et entrainement\n\nIci le modèle est entrainé avec l'optimiseur **Adam**, efficace grâce à l'adaptation des learning rate. La fonction de perte **Categorical crossentropy** est la plus adéquate dans le cas d'une classification multi-classes, d'où ce choix.\n\nEn ce qui concerne l'optimisation du réseau, on fait le choix de rajouter 2 options aux callbacks (les mécanismes de régulations) : \n\n- EarlyStopping : Le modèle s'entrainant sur 100 époques et monopolisant le CPU ainsi que le GPU de Kaggle pendant un temps certains, l'entrainement s'arrête de lui-même si la val_accuracy ne progresse plus pendant 15 époques. A l'arrêt anticipé, les poids les plus optimaux sont restaurés pour les calculs finaux.\n\n- ReduceLROnPlateau : Comme dans la condition précédente, si le modèle stagne et que la val_accuracy de varie plus, le learning rate est divisé par 2 (factor = 0.5), cela permet au modèle de s'entrainer de manière plus précise dans les époques à suivre.","metadata":{}},{"cell_type":"code","source":"# Définition du modèle\n\nmodel = models.Sequential([\n    layers.Input(shape=(257, 257, 3)),\n    \n    layers.Conv2D(32, (3, 3), activation='relu', padding='same'), \n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n\n    layers.Conv2D(32, (3, 3), activation='relu', padding='same'), \n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n    \n    layers.Conv2D(64, (3, 3), activation='relu', padding='same'),\n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n    \n    layers.Conv2D(128, (3, 3), activation='relu', padding='same'),\n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n\n    layers.Conv2D(256, (3, 3), activation='relu', padding='same'),\n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n\n    layers.Conv2D(256, (3, 3), activation='relu', padding='same'),\n    layers.BatchNormalization(),\n    layers.MaxPooling2D((2, 2)),\n    \n    layers.GlobalAveragePooling2D(),\n    layers.Dense(256, activation='relu'),\n    layers.BatchNormalization(),\n    layers.Dropout(0.5),\n    layers.Dense(5, activation='softmax')\n])\n\nmodel.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])\n\n# Redéfinition des callbacks \n\ncallbacks = [\n    EarlyStopping(monitor='val_accuracy', patience=15, restore_best_weights=True, verbose=2),\n    ReduceLROnPlateau(monitor='val_accuracy', factor=0.5, patience=7, min_lr=1e-6, verbose=2)\n]\n\n# Entrainement du modèle\n\nhistory = model.fit(\n    train_generator,\n    epochs=100,\n    callbacks=callbacks,\n    validation_data=val_generator,\n    verbose=2\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T08:10:49.827487Z","iopub.execute_input":"2026-01-21T08:10:49.827862Z","iopub.status.idle":"2026-01-21T08:19:47.399113Z","shell.execute_reply.started":"2026-01-21T08:10:49.827826Z","shell.execute_reply":"2026-01-21T08:19:47.398510Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Analyse des courbes et génération des prédictions\n\n### Génération des courbes d'accuracy et loss\n\nAprès entrainement du modèle, on recueille les informations d'accuracy, val_accuracy, loss et val_loss pour les représenter sur un graphe permettant de distinguer la convergence du modèle.\n\nAvec le programme suivant on constate une stagnation de la val_accuracy et de la val_loss aux alentours de la 40ème époque, tout en continuation de faire augmenter l'accuracy et baisser la loss. A la fin de l'entrainement les poids optimaux ont été restaurés permettant ainsi un score final de **0.925**.\n\n### Sauvegarde des résultats de prédictions\n\nEnfin après l'entrainement du modèle, on prédit la classe des données de tests après les avoirs importées dans un générateur afin de préciser la taille des paquets de traitement (ici 1) dans leur ordre d'apparition dans le fichier (shuffle=false).\n\nUne fois les données prédites, le nom de chaque donnée est à nouveau modifié pour supprimer l'extension de l'image et écrire un fichier de type .csv proche des fichiers données en entrée.","metadata":{}},{"cell_type":"code","source":"# Affichage des courbes d'entrainement (accuracy, val_accuracy, loss et val_loss)\n\nplt.figure(figsize=(12,5))\nplt.subplot(1,2,1)\nplt.plot(history.history['accuracy'], label='Train Acc', color='#2ecc71', lw=2)\nplt.plot(history.history['val_accuracy'], label='Val Acc', color='#e74c3c', lw=2)\nplt.legend(); plt.title(\"Évolution de la Précision (Accuracy)\"); plt.grid(True, alpha=0.3)\n\nplt.subplot(1,2,2)\nplt.plot(history.history['loss'], label='Train Loss', color='#2ecc71', lw=2)\nplt.plot(history.history['val_loss'], label='Val Loss', color='#e74c3c', lw=2)\nplt.legend(); plt.title(\"Évolution de la Perte (Loss)\"); plt.grid(True, alpha=0.3)\nplt.show()\n\n# Prédictions et génération du fichier .csv en sortie\n\ntest_generator = test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    directory=test_dir,\n    x_col=\"ID\",\n    y_col=None,\n    target_size=(257, 257),\n    batch_size=1,\n    class_mode=None,\n    shuffle=False\n)\n\npredictions = model.predict(test_generator)\npredicted_classes = predictions.argmax(axis=1)\n\nsubmission = pd.DataFrame({\n    \"ID\": test_df[\"ID\"].str.replace(\".png\", \"\", regex=False),\n    \"Category\": predicted_classes\n})\n\nprint(\"Création du fichier de sortie \\\"resultat.csv\\\"\")\nsubmission.to_csv(\"resultat.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T08:19:47.399999Z","iopub.execute_input":"2026-01-21T08:19:47.400292Z","iopub.status.idle":"2026-01-21T08:21:06.601417Z","shell.execute_reply.started":"2026-01-21T08:19:47.400267Z","shell.execute_reply":"2026-01-21T08:21:06.600603Z"}},"outputs":[],"execution_count":null}]}