{"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":14774,"databundleVersionId":875431,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"collapsed":true,"jupyter":{"outputs_hidden":true},"execution":{"iopub.status.busy":"2026-01-04T03:59:00.098708Z","iopub.execute_input":"2026-01-04T03:59:00.099273Z","iopub.status.idle":"2026-01-04T03:59:06.511940Z","shell.execute_reply.started":"2026-01-04T03:59:00.099244Z","shell.execute_reply":"2026-01-04T03:59:06.511292Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2 **Dataset and Task**","metadata":{}},{"cell_type":"code","source":"# Reproducibility: set random seeds\nimport random\nimport numpy as np\nimport tensorflow as tf\n\nSEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.513196Z","iopub.execute_input":"2026-01-04T03:59:06.513437Z","iopub.status.idle":"2026-01-04T03:59:06.573726Z","shell.execute_reply.started":"2026-01-04T03:59:06.513416Z","shell.execute_reply":"2026-01-04T03:59:06.572183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# Charger train.csv( lire, afficher, analyser, nettoyer les données )\ndf = pd.read_csv(\"/kaggle/input/aptos2019-blindness-detection/train.csv\")\ndf.head()\ndf[\"id_code\"] = df[\"id_code\"] + \".png\"\ndf.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.576222Z","iopub.execute_input":"2026-01-04T03:59:06.577303Z","iopub.status.idle":"2026-01-04T03:59:06.599466Z","shell.execute_reply.started":"2026-01-04T03:59:06.576821Z","shell.execute_reply":"2026-01-04T03:59:06.598827Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df['binary_diag'] = df['diagnosis'].apply(lambda x: 0 if x == 0 else 1) ## gemini","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.600809Z","iopub.execute_input":"2026-01-04T03:59:06.601093Z","iopub.status.idle":"2026-01-04T03:59:06.606710Z","shell.execute_reply.started":"2026-01-04T03:59:06.601049Z","shell.execute_reply":"2026-01-04T03:59:06.605933Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Analysis of the distribution\ndf[\"diagnosis\"].value_counts().sort_index()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.607729Z","iopub.execute_input":"2026-01-04T03:59:06.608038Z","iopub.status.idle":"2026-01-04T03:59:06.620762Z","shell.execute_reply.started":"2026-01-04T03:59:06.608007Z","shell.execute_reply":"2026-01-04T03:59:06.620131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualization\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(6,4))\ndf[\"diagnosis\"].value_counts().sort_index().plot(kind=\"bar\", color=\"teal\")\nplt.title(\"Distribution of DR Severity Classes\")\nplt.xlabel(\"DR Class (0 = No DR, 4 = Proliferative)\")\nplt.ylabel(\"Number of Images\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.621569Z","iopub.execute_input":"2026-01-04T03:59:06.621958Z","iopub.status.idle":"2026-01-04T03:59:06.747096Z","shell.execute_reply.started":"2026-01-04T03:59:06.621936Z","shell.execute_reply":"2026-01-04T03:59:06.746288Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Examples Images from each DR\n\nimport cv2\nimport matplotlib.pyplot as plt\n\nimage_path = \"/kaggle/input/aptos2019-blindness-detection/train_images/\"\n\ndef show_image(filename):\n    img = cv2.imread(image_path + filename)\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    plt.imshow(img)\n    plt.axis(\"off\")\n\n# Afficher un exemple pour chaque classe 0 → 4\nfor diag in range(5):\n    example = df[df[\"diagnosis\"] == diag].iloc[0][\"id_code\"]\n    print(f\"Example for class {diag}: {example}\")\n    show_image(example)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:06.748146Z","iopub.execute_input":"2026-01-04T03:59:06.748531Z","iopub.status.idle":"2026-01-04T03:59:08.969803Z","shell.execute_reply.started":"2026-01-04T03:59:06.748502Z","shell.execute_reply":"2026-01-04T03:59:08.969114Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Import the function used to split the dataset into train/val/test\nfrom sklearn.model_selection import train_test_split\n\n# Split the dataset into:\n# 70% training data\n# 30% temporary data (which we will later split into validation + test)\n# Stratification ensures each subset keeps the same class distribution\ntrain_df, temp_df = train_test_split(\n    df,\n    test_size=0.30,        # 30% of the dataset goes to temp_df\n    random_state=42,       # for reproducibility\n    stratify=df[\"diagnosis\"]   # preserve class distribution\n)\n\ntrain_df.head()\n\n# Split the temporary set into:\n# 1/3 validation (10% of total)\n# 2/3 test (20% of total)\nval_df, test_df = train_test_split(\n    temp_df,\n    test_size=2/3,                     # 2/3 of 30% = 20% of total\n    random_state=42,\n    stratify=temp_df[\"diagnosis\"]      # preserve class distribution\n)\n\nval_df.head()\n\n# Print the number of samples in each split\nprint(\"Train:\", len(train_df))\nprint(\"Validation:\", len(val_df))\nprint(\"Test:\", len(test_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:08.971395Z","iopub.execute_input":"2026-01-04T03:59:08.971685Z","iopub.status.idle":"2026-01-04T03:59:08.984402Z","shell.execute_reply.started":"2026-01-04T03:59:08.971663Z","shell.execute_reply":"2026-01-04T03:59:08.983702Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check class balance in each subset\nprint(\"Train class distribution:\")\nprint(train_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nValidation class distribution:\")\nprint(val_df[\"diagnosis\"].value_counts().sort_index())\n\nprint(\"\\nTest class distribution:\")\nprint(test_df[\"diagnosis\"].value_counts().sort_index())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:08.985324Z","iopub.execute_input":"2026-01-04T03:59:08.985557Z","iopub.status.idle":"2026-01-04T03:59:08.993846Z","shell.execute_reply.started":"2026-01-04T03:59:08.985536Z","shell.execute_reply":"2026-01-04T03:59:08.993313Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Comment savoir que je préserve class balance, je ne comprends pas.","metadata":{}},{"cell_type":"markdown","source":"# ***3 Methods***","metadata":{}},{"cell_type":"markdown","source":"### Binary classification","metadata":{}},{"cell_type":"code","source":"# Create binary label column: 0 = No DR, 1 = DR\ntrain_df[\"binary_label\"] = train_df[\"diagnosis\"].apply(lambda x: 0 if x == 0 else 1)\nval_df[\"binary_label\"] = val_df[\"diagnosis\"].apply(lambda x: 0 if x == 0 else 1)\ntest_df[\"binary_label\"] = test_df[\"diagnosis\"].apply(lambda x: 0 if x == 0 else 1)\n\n# Convert labels to string as required by Keras generators\ntrain_df[\"binary_label\"] = train_df[\"binary_label\"].astype(str)\nval_df[\"binary_label\"] = val_df[\"binary_label\"].astype(str)\ntest_df[\"binary_label\"] = test_df[\"binary_label\"].astype(str)\n\n# Là on vient de créér le label binaire (DR vs No DR) et convertir en string pour keras\n# On va maintenant créer les générateurs qui permettent de charger les images les redimensionner les normaliser diviser en batch ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:08.996492Z","iopub.execute_input":"2026-01-04T03:59:08.996721Z","iopub.status.idle":"2026-01-04T03:59:09.009748Z","shell.execute_reply.started":"2026-01-04T03:59:08.996701Z","shell.execute_reply":"2026-01-04T03:59:09.009215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\n# 1. Pour l'entraînement (Augmentation + Preprocess)\nIMG_SIZE = 224\ntrain_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    rotation_range=20,\n    zoom_range=0.15,\n    horizontal_flip=True,\n    vertical_flip=True,\n    brightness_range=[0.7, 1.3]\n)\n\n# 2. Pour Validation et Test (UNIQUEMENT Preprocess, pas de rescale !)\nval_test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)\n\n# --- GÉNÉRATEURS ---\n\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe=train_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"binary_label\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"binary\",\n    shuffle=True\n)\n\nval_generator = val_test_datagen.flow_from_dataframe(\n    dataframe=val_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"binary_label\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"binary\",\n    shuffle=False\n)\n\ntest_generator = val_test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"binary_label\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"binary\",\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:09.010747Z","iopub.execute_input":"2026-01-04T03:59:09.011263Z","iopub.status.idle":"2026-01-04T03:59:09.088049Z","shell.execute_reply.started":"2026-01-04T03:59:09.011234Z","shell.execute_reply":"2026-01-04T03:59:09.087502Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Construction of EfficientNetB3 : C'est un réseau déjà entraîné sur ImageNet\nfrom tensorflow.keras.applications import EfficientNetB3\nfrom tensorflow.keras.layers import Dense, GlobalAveragePooling2D\nfrom tensorflow.keras.models import Model\nimport tensorflow as tf\n\n# 1. Charger EfficientNetB3\nbase_model = EfficientNetB3(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(224, 224, 3)\n)\n\n# 2. Geler TOUTES les couches d'un coup (Remplace avantageusement ton 'for')\nbase_model.trainable = False \n\n# 3. Connecter les couches\nx = base_model.output\nx = GlobalAveragePooling2D()(x)\noutputs = Dense(1, activation=\"sigmoid\")(x)\n\n# 4. Créer le modèle\nmodel = Model(inputs=base_model.input, outputs=outputs)\n\n# 5. Compiler\nmodel.compile(\n    optimizer=\"adam\",\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:09.088858Z","iopub.execute_input":"2026-01-04T03:59:09.089126Z","iopub.status.idle":"2026-01-04T03:59:11.532941Z","shell.execute_reply.started":"2026-01-04T03:59:09.089096Z","shell.execute_reply":"2026-01-04T03:59:11.532378Z"},"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Nombre de couches entraînables : {len(model.trainable_weights)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:11.533995Z","iopub.execute_input":"2026-01-04T03:59:11.534343Z","iopub.status.idle":"2026-01-04T03:59:11.539166Z","shell.execute_reply.started":"2026-01-04T03:59:11.534298Z","shell.execute_reply":"2026-01-04T03:59:11.538512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n\n# Callbacks to prevent overfitting and stabilize training\ncallbacks = [\n    EarlyStopping(\n        monitor='val_loss',\n        patience=3,\n        restore_best_weights=True\n    ),\n    \n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.3,\n        patience=2,\n        min_lr=1e-7\n    )\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:11.539993Z","iopub.execute_input":"2026-01-04T03:59:11.540310Z","iopub.status.idle":"2026-01-04T03:59:11.550637Z","shell.execute_reply.started":"2026-01-04T03:59:11.540278Z","shell.execute_reply":"2026-01-04T03:59:11.549889Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training of the model\nhistory = model.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=10,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T03:59:11.551585Z","iopub.execute_input":"2026-01-04T03:59:11.551869Z","iopub.status.idle":"2026-01-04T04:49:10.302111Z","shell.execute_reply.started":"2026-01-04T03:59:11.551839Z","shell.execute_reply":"2026-01-04T04:49:10.301257Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Explication de tout ce qui a été fait depuis le début / \n1. On a récupéré les données APTOS dans Kaggle (le dataset contient 3662 images du fond de l'oeil, un fichier train.csv )\n2. on a analysé la distribution des classes et découvert que le dataset est deséquilibré (***pourquoi), fait un graphique, montré un exemple d'image pour chaque classe\n3. On a découpé le dataset en train val et test(stratify = garder le même équilibre que le dataset original)\n4. On a transformé les labels pour la classification binaire (0 = No DR, 1,2,3,4 = DR), puis convertir en string pour Keras\n5. On a préparé les DataGenerators\n6. On a construit EfficientNetB3, a l'a préparé en un modèle binaire et efficace\n7. Avec history, on a commencé à entrainer le modèle","metadata":{}},{"cell_type":"markdown","source":"Plus haut, on a fait ceci freeze convolutional layers and train only the new head; maintenant on va faire ceci\nunfreeze and fine-tune some deeper layers, cela permettra d'augmenter l'accuracy. Si on fine-tune trop tôt le modèle diverge","metadata":{}},{"cell_type":"markdown","source":"On va maintenant ajouter les callbacks pour éviter le overfitting, la dégradation du modèle, un apprentissage trop long et inutile, un mauvaise choix de learning rate. Les deux plus importants et suffisants pour le projet  : EarlyStopping qui empêche de continuer à entraîner un modèle qui commence à overfit et ReduceLROnPlateau permet au modèle d'apprendre plus finement empêche les oscillations dans le loss.","metadata":{}},{"cell_type":"code","source":"# Unfreeze the last layers of EfficientNetB3 for fine-tuning\nfor layer in base_model.layers[-20:]:\n    layer.trainable = True\n    \nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-5), # LR = 1e-5 car autrement on détruit les poids préentrainés\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\n# Retrain (fine-tuning)\nhistory_finetune = model.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=10,\n    callbacks=callbacks\n) \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T04:49:10.303231Z","iopub.execute_input":"2026-01-04T04:49:10.303509Z","iopub.status.idle":"2026-01-04T05:34:28.702341Z","shell.execute_reply.started":"2026-01-04T04:49:10.303476Z","shell.execute_reply":"2026-01-04T05:34:28.701637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Evaluate the model on the test set (binary classification)\ntest_loss, test_acc = model.evaluate(test_generator)\nprint(\"Test Accuracy:\", test_acc)\nprint(\"Test Loss:\", test_loss)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:34:28.703347Z","iopub.execute_input":"2026-01-04T05:34:28.703600Z","iopub.status.idle":"2026-01-04T05:35:44.038045Z","shell.execute_reply.started":"2026-01-04T05:34:28.703572Z","shell.execute_reply":"2026-01-04T05:35:44.037310Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"On va à présent visualiser les courbes d'apprentissage. Elles montrent si le modèle aprrend vraiment, overfits, si le fine-tuning a amélioré l'apprentissage","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Accuracy curves\nplt.figure(figsize=(8,4))\nplt.plot(history.history[\"accuracy\"], label=\"Train Accuracy (head)\")\nplt.plot(history.history[\"val_accuracy\"], label=\"Val Accuracy (head)\")\nplt.plot(history_finetune.history[\"accuracy\"], label=\"Train Accuracy (finetune)\")\nplt.plot(history_finetune.history[\"val_accuracy\"], label=\"Val Accuracy (finetune)\")\nplt.title(\"Accuracy Curves\")\nplt.legend()\nplt.show()\n\n# Loss curves\nplt.figure(figsize=(8,4))\nplt.plot(history.history[\"loss\"], label=\"Train Loss (head)\")\nplt.plot(history.history[\"val_loss\"], label=\"Val Loss (head)\")\nplt.plot(history_finetune.history[\"loss\"], label=\"Train Loss (finetune)\")\nplt.plot(history_finetune.history[\"val_loss\"], label=\"Val Loss (finetune)\")\nplt.title(\"Loss Curves\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:35:44.039284Z","iopub.execute_input":"2026-01-04T05:35:44.039642Z","iopub.status.idle":"2026-01-04T05:35:44.311816Z","shell.execute_reply.started":"2026-01-04T05:35:44.039620Z","shell.execute_reply":"2026-01-04T05:35:44.311283Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"On a deux phasess dans l'entrainement : Head-only training(on entraine seulement la derniere couche du modèle, le reseau EfficientNet reste gelé, il n'apprend pas encore) and fine-tuning(on degele les couches profondes du reseau EfficientNet elles commencent à apprendre des patterns dans les images). Un modèle apprend normalement si :\n\ntrain loss ↓\n\ntrain accuracy ↑\n\nval accuracy ↑ puis ↓ légèrement\n\nval loss ↓ puis ↑ légèrement.\nAcuracy curves : - Head-only : train augmente un peu cest normal la tete apprend, val_accuracy head monte fortement puis redescend cest normal aussi(=le modele generalise bien au debut puis se met à overfit le train ) le modele nest pas encore connecté aux vraies features visuelles. Train accruracy for fine tuning augment régulierement jusqu'à 70%, Val_Accuracy monte vers 75% puis redescend vers 60%. Le modèle apprend réellement mais à un moment il commence à overfit legerement cest pour ca quon utilise early stopping. Train loss head baisse légèrement cest normal, validation loss head instable normal car la tête seule n'a pas assez d'information; train loss fine tune baisse regulierement signe clair que le modele apprend des patterns réels dans les images et val-loss fine tune baisse au début remonte nesuite donc overfitting. Le modele apprend reellement il ameliore dabord la validation puis overfits, early stopping etait une bonne decision","metadata":{}},{"cell_type":"markdown","source":"# ***4. Evaluation Metrics***","metadata":{}},{"cell_type":"code","source":"# --- Step 1: Make predictions on the test set ---\ny_prob = model.predict(test_generator)            # probabilities from model\ny_pred = (y_prob > 0.5).astype(int).ravel()       # convert prob → class 0/1\n\n# True labels\ny_true = test_generator.classes\n\n\n# --- Step 2: Compute the metrics ---\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\n\nacc = accuracy_score(y_true, y_pred)\nprec = precision_score(y_true, y_pred)\nrec = recall_score(y_true, y_pred)\nf1 = f1_score(y_true, y_pred)\n\nprint(\"Accuracy :\", acc)\nprint(\"Precision:\", prec)\nprint(\"Recall   :\", rec)\nprint(\"F1-score :\", f1)\n\n\n# --- Step 3: Confusion matrix ---\nfrom sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Confusion Matrix - Binary DR Detection\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:35:44.312605Z","iopub.execute_input":"2026-01-04T05:35:44.312819Z","iopub.status.idle":"2026-01-04T05:37:10.351857Z","shell.execute_reply.started":"2026-01-04T05:35:44.312788Z","shell.execute_reply":"2026-01-04T05:37:10.351298Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Recall = 95% = le modèle ne rate presque aucun malade, c'est le comportement idéal pour un modèle de screening médical.\nPrecision = 63% = le modèle fait des faux DR (normal)\nAccuracy = 63% correct pour un modele binaire sur APTOS\nF1 = 75% Score équilibré entre Recall et Précision.\nConfusion matrix est un tableau qui compare la vérité avec la predictiondu modèle. Elle dit exactement combien de fois le modèle a raison et combien de fois il se trompe. [predicted, true]: [0,1] = 17 le modèle a predit que NO DR, mais en réalité c'est DR this is the case le plus grave in médécine. Be careful : je n'ai pas les meme resultats à chaque à cause de shuffle = true qui peremet deviter le overfitiing et des differentes augmentations","metadata":{}},{"cell_type":"markdown","source":"A ce niveau, on a déjà fait :\n1. classification binaire \"DR vs No Dr\"\n2. Preprocessing split 70/10/20\n3. Modele EfficientNetB3 + fine-tuning partiel\n4. Métriques + Confusion matrix\n5. Maintenant, il faut faire la classification 5classes(0-4)","metadata":{}},{"cell_type":"markdown","source":"### 5 class classification","metadata":{}},{"cell_type":"code","source":"# Preparation des labels\n# Convert labels to integers\ntrain_df[\"diag_int\"] = train_df[\"diagnosis\"].astype(int)\nval_df[\"diag_int\"]  = val_df[\"diagnosis\"].astype(int)\ntest_df[\"diag_int\"] = test_df[\"diagnosis\"].astype(int)\n\nprint(train_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:37:10.352677Z","iopub.execute_input":"2026-01-04T05:37:10.352962Z","iopub.status.idle":"2026-01-04T05:37:10.360161Z","shell.execute_reply.started":"2026-01-04T05:37:10.352916Z","shell.execute_reply":"2026-01-04T05:37:10.359446Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Data generators\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\nIMG_SIZE = 224\nimage_path = \"/kaggle/input/aptos2019-blindness-detection/train_images/\"\n\n# Train generator with augmentation\ntrain_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    rotation_range=20,\n    zoom_range=0.15,\n    horizontal_flip=True,\n    vertical_flip=True,\n    brightness_range=[0.7, 1.3]\n)\n\n# Validation & Test generators — no augmentation\nval_test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)\n\ntrain_generator = train_datagen.flow_from_dataframe(\n    train_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"diag_int\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"raw\",\n    shuffle=True\n)\n\nval_generator = val_test_datagen.flow_from_dataframe(\n    val_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"diag_int\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"raw\",\n    shuffle=False\n)\n\ntest_generator = val_test_datagen.flow_from_dataframe(\n    test_df,\n    directory=image_path,\n    x_col=\"id_code\",\n    y_col=\"diag_int\",\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=16,\n    class_mode=\"raw\",\n    shuffle=False\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:37:10.361053Z","iopub.execute_input":"2026-01-04T05:37:10.361316Z","iopub.status.idle":"2026-01-04T05:37:11.901879Z","shell.execute_reply.started":"2026-01-04T05:37:10.361297Z","shell.execute_reply":"2026-01-04T05:37:11.901130Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calcul des class weight\nfrom sklearn.utils.class_weight import compute_class_weight\nimport numpy as np\n\nclasses = np.array([0,1,2,3,4])\nweights = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=classes,\n    y=train_df[\"diag_int\"]\n)\n\nclass_weights = dict(zip(classes, weights))\nprint(\"Class weights:\", class_weights)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:37:11.902811Z","iopub.execute_input":"2026-01-04T05:37:11.903118Z","iopub.status.idle":"2026-01-04T05:37:11.909893Z","shell.execute_reply.started":"2026-01-04T05:37:11.903076Z","shell.execute_reply":"2026-01-04T05:37:11.909241Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Construction du modèle EfficientNetB3\nimport tensorflow as tf\nfrom tensorflow.keras.applications import EfficientNetB3\nfrom tensorflow.keras.layers import Dense, GlobalAveragePooling2D\nfrom tensorflow.keras.models import Model\n\n# Load EfficientNetB3 (backbone only)\nbase_model = EfficientNetB3(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\n\n# Freeze all layers\nbase_model.trainable = False\n\n# Build classification head\ninputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = base_model(inputs, training=False)\nx = GlobalAveragePooling2D()(x)\noutputs = Dense(5, activation=\"softmax\")(x)\n\nmodel_5c = Model(inputs, outputs)\n\n# Compile\nmodel_5c.compile(\n    optimizer=\"adam\",\n    loss=\"sparse_categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nmodel_5c.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:37:11.910775Z","iopub.execute_input":"2026-01-04T05:37:11.911101Z","iopub.status.idle":"2026-01-04T05:37:13.440316Z","shell.execute_reply.started":"2026-01-04T05:37:11.911046Z","shell.execute_reply":"2026-01-04T05:37:13.439770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Phase 1 : Training du head\nlr_scheduler = tf.keras.callbacks.ReduceLROnPlateau(\n    monitor=\"val_loss\",\n    factor=0.5,\n    patience=2,\n    min_lr=1e-7,\n    verbose=1\n)\n\nhistory_head = model_5c.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=7,\n    class_weight=class_weights,\n    callbacks=[lr_scheduler]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T05:37:13.441154Z","iopub.execute_input":"2026-01-04T05:37:13.441353Z","iopub.status.idle":"2026-01-04T06:12:34.799948Z","shell.execute_reply.started":"2026-01-04T05:37:13.441333Z","shell.execute_reply":"2026-01-04T06:12:34.799158Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Phase 2 : Fine-tuning\n# Unfreeze last 20 layers\nfor layer in base_model.layers[-20:]:\n    layer.trainable = True\n\n# Recompile with very small LR\nmodel_5c.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-5),\n    loss=\"sparse_categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nhistory_finetune = model_5c.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=7,\n    class_weight=class_weights,\n    callbacks=[lr_scheduler]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T06:12:34.801091Z","iopub.execute_input":"2026-01-04T06:12:34.801502Z","iopub.status.idle":"2026-01-04T06:48:02.661132Z","shell.execute_reply.started":"2026-01-04T06:12:34.801466Z","shell.execute_reply":"2026-01-04T06:48:02.660641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Evaluation : predictions\ny_prob = model_5c.predict(test_generator)\ny_pred = y_prob.argmax(axis=1)\ny_true = test_df[\"diag_int\"].values\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T06:48:02.662732Z","iopub.execute_input":"2026-01-04T06:48:02.663013Z","iopub.status.idle":"2026-01-04T06:49:29.155483Z","shell.execute_reply.started":"2026-01-04T06:48:02.662992Z","shell.execute_reply":"2026-01-04T06:49:29.154738Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Classification Report\nfrom sklearn.metrics import classification_report\nprint(classification_report(y_true, y_pred))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T06:49:29.156642Z","iopub.execute_input":"2026-01-04T06:49:29.156979Z","iopub.status.idle":"2026-01-04T06:49:29.169498Z","shell.execute_reply.started":"2026-01-04T06:49:29.156945Z","shell.execute_reply":"2026-01-04T06:49:29.168804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Confusion matrix\nfrom sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(7,6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"True\")\nplt.title(\"Confusion Matrix - 5-Class DR Classification\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T06:49:29.173600Z","iopub.execute_input":"2026-01-04T06:49:29.173826Z","iopub.status.idle":"2026-01-04T06:49:29.359357Z","shell.execute_reply.started":"2026-01-04T06:49:29.173797Z","shell.execute_reply":"2026-01-04T06:49:29.358603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# QWK\nfrom sklearn.metrics import cohen_kappa_score\n\nqwk = cohen_kappa_score(y_true, y_pred, weights=\"quadratic\")\nprint(\"QWK:\", qwk)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T06:49:29.360220Z","iopub.execute_input":"2026-01-04T06:49:29.360504Z","iopub.status.idle":"2026-01-04T06:49:29.367345Z","shell.execute_reply.started":"2026-01-04T06:49:29.360471Z","shell.execute_reply":"2026-01-04T06:49:29.366701Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df.dtypes\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T07:51:09.182090Z","iopub.execute_input":"2026-01-04T07:51:09.182530Z","iopub.status.idle":"2026-01-04T07:51:09.190411Z","shell.execute_reply.started":"2026-01-04T07:51:09.182498Z","shell.execute_reply":"2026-01-04T07:51:09.189755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for layer in model_5c.get_layer(\"efficientnetb3\").layers[-20:]:\n    print(layer.name)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T07:51:13.256026Z","iopub.execute_input":"2026-01-04T07:51:13.256358Z","iopub.status.idle":"2026-01-04T07:51:13.260873Z","shell.execute_reply.started":"2026-01-04T07:51:13.256334Z","shell.execute_reply":"2026-01-04T07:51:13.260006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T07:51:17.984581Z","iopub.execute_input":"2026-01-04T07:51:17.985115Z","iopub.status.idle":"2026-01-04T07:51:17.992963Z","shell.execute_reply.started":"2026-01-04T07:51:17.985088Z","shell.execute_reply":"2026-01-04T07:51:17.992346Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for layer in model_5c.get_layer(\"efficientnetb3\").layers[::-1]:\n    if isinstance(layer, tf.keras.layers.Conv2D):\n        print(layer.name)\n        break\n# verification de la derniere couche","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T08:34:10.457965Z","iopub.execute_input":"2026-01-04T08:34:10.458644Z","iopub.status.idle":"2026-01-04T08:34:10.463277Z","shell.execute_reply.started":"2026-01-04T08:34:10.458613Z","shell.execute_reply":"2026-01-04T08:34:10.462637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\n\ndef occlusion_sensitivity(model, img_array, patch_size=20, stride=10):\n    h, w, _ = img_array.shape\n    heatmap = np.zeros((h, w))\n\n    img_preprocessed = preprocess_input(img_array.copy())\n    base_pred = model.predict(np.expand_dims(img_preprocessed, axis=0))[0]\n    predicted_class = np.argmax(base_pred)\n\n    for y in range(0, h - patch_size, stride):\n        for x in range(0, w - patch_size, stride):\n\n            occluded = img_array.copy()\n            occluded[y:y+patch_size, x:x+patch_size, :] = 0  # Masque noir\n\n            occluded_pre = preprocess_input(occluded.copy())\n            pred = model.predict(np.expand_dims(occluded_pre, axis=0))[0]\n\n            heatmap[y:y+patch_size, x:x+patch_size] = base_pred[predicted_class] - pred[predicted_class]\n\n    heatmap = np.maximum(heatmap, 0)\n    heatmap /= np.max(heatmap)\n    return heatmap\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T08:50:16.454622Z","iopub.execute_input":"2026-01-04T08:50:16.454938Z","iopub.status.idle":"2026-01-04T08:50:16.461383Z","shell.execute_reply.started":"2026-01-04T08:50:16.454911Z","shell.execute_reply":"2026-01-04T08:50:16.460777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_occlusion(img_path, model, patch_size=20, stride=10):\n    img = tf.keras.preprocessing.image.load_img(img_path, target_size=(224,224))\n    img_arr = tf.keras.preprocessing.image.img_to_array(img)\n\n    heatmap = occlusion_sensitivity(model, img_arr, patch_size, stride)\n\n    plt.figure(figsize=(6,6))\n    plt.imshow(img)\n    plt.imshow(heatmap, cmap='jet', alpha=0.5)\n    plt.title(\"Occlusion Sensitivity Map\")\n    plt.axis(\"off\")\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T08:50:19.474034Z","iopub.execute_input":"2026-01-04T08:50:19.474316Z","iopub.status.idle":"2026-01-04T08:50:19.479565Z","shell.execute_reply.started":"2026-01-04T08:50:19.474295Z","shell.execute_reply":"2026-01-04T08:50:19.478749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image_folder = \"/kaggle/input/aptos2019-blindness-detection/train_images/\"\n\nfor c in [\"0\",\"1\",\"2\",\"3\",\"4\"]:\n    ex = test_df[test_df[\"diagnosis\"].astype(str) == c]\n\n    if not ex.empty:\n        img_id = ex.iloc[0][\"id_code\"].replace(\".png\",\"\")\n        img_path = image_folder + img_id + \".png\"\n\n        print(f\"\\n=== Occlusion Map – Classe {c} ===\")\n        display_occlusion(img_path, model_5c)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-04T08:50:24.003020Z","iopub.execute_input":"2026-01-04T08:50:24.003754Z","execution_failed":"2026-01-04T09:38:57.718Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"La derniere partie du projet concerne l'interpretabilité. On veut savoir ou le modele regarde dans l'image pour prendre sa decision : c'est ça qu'on appelle grad-cam. Cest une lumiere rouge qui montre les zones importantes. on doit choisir une image du dataset ","metadata":{}}]}