{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":4104},{"sourceType":"datasetVersion","sourceId":7866129},{"sourceType":"datasetVersion","sourceId":7869237}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":9035.275407,"end_time":"2026-08-29T18:26:07.036657","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-08-29T15:55:31.76125","version":"2.5.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Importation des librairies","metadata":{"papermill":{"duration":0.006341,"end_time":"2026-08-29T15:55:34.38074","exception":false,"start_time":"2026-08-29T15:55:34.374399","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\n\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.metrics import cohen_kappa_score, confusion_matrix\n\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import EfficientNetB0\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:15:18.538076Z","iopub.execute_input":"2026-08-30T14:15:18.538682Z","iopub.status.idle":"2026-08-30T14:15:33.730349Z","shell.execute_reply.started":"2026-08-30T14:15:18.538651Z","shell.execute_reply":"2026-08-30T14:15:33.729594Z"},"papermill":{"duration":12.7795,"end_time":"2026-08-29T15:55:47.166153","exception":false,"start_time":"2026-08-29T15:55:34.386653","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test des paramètres","metadata":{"papermill":{"duration":0.005911,"end_time":"2026-08-29T15:55:47.178505","exception":false,"start_time":"2026-08-29T15:55:47.172594","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"Plusieurs résolutions ont été testées avec la taille de IMG_SIZE. Une résolution de 512×512 a donné de meilleurs résultats sur notre validation que les résolutions testées précédemment, au prix d'un temps de calcul plus important. Ce paramètre a amélioré le score global de + de 0.80\n\nBATCH_SIZE permet de prendre en compte la mémoire qui sera plus importante avec une résolution plus grande.","metadata":{"papermill":{"duration":0.006858,"end_time":"2026-08-29T15:55:47.191526","exception":false,"start_time":"2026-08-29T15:55:47.184668","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# Chemins\nCSV_DIR = \"/kaggle/input/competitions/diabetic-retinopathy-detection\"\nTRAIN_DIR = \"/kaggle/input/datasets/josephrynkiewicz/diabetic-retinopathy-train-unzipped/train\"\nTEST_DIR = \"/kaggle/input/datasets/josephrynkiewicz/diabetic-retinopathy-test-unzipped/test\"\n\n# Paramètres\nIMG_SIZE = 512\nBATCH_SIZE = 8\nNUM_CLASSES = 5\n\nEPOCHS_HEAD = 2\nEPOCHS_FINE = 4\n\nLR_HEAD = 0.001\nLR_FINE = 0.0001\n\nAUTOTUNE = tf.data.AUTOTUNE","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:15:33.731670Z","iopub.execute_input":"2026-08-30T14:15:33.732523Z","iopub.status.idle":"2026-08-30T14:15:33.738249Z","shell.execute_reply.started":"2026-08-30T14:15:33.732495Z","shell.execute_reply":"2026-08-30T14:15:33.737278Z"},"papermill":{"duration":0.015057,"end_time":"2026-08-29T15:55:47.212514","exception":false,"start_time":"2026-08-29T15:55:47.197457","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Données\n\nOn reconstruit le chemin à partir du nom en reliant les images, leur nom et leur note.","metadata":{"papermill":{"duration":0.005896,"end_time":"2026-08-29T15:55:47.224812","exception":false,"start_time":"2026-08-29T15:55:47.218916","status":"completed"},"tags":[]}},{"cell_type":"code","source":"train_df = pd.read_csv(f\"{CSV_DIR}/trainLabels.csv.zip\")\ntrain_df[\"path\"] = (TRAIN_DIR + \"/\" + train_df[\"image\"] + \".jpeg\")\ntrain_df = train_df[train_df[\"path\"].apply(os.path.exists)].reset_index(drop=True)\n\nprint(f\"Nombre d'images : {len(train_df)}\")\ndisplay(train_df.head())","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:15:37.431496Z","iopub.execute_input":"2026-08-30T14:15:37.431985Z","iopub.status.idle":"2026-08-30T14:17:45.419195Z","shell.execute_reply.started":"2026-08-30T14:15:37.431958Z","shell.execute_reply":"2026-08-30T14:17:45.418316Z"},"papermill":{"duration":230.576758,"end_time":"2026-08-29T15:59:37.807638","exception":false,"start_time":"2026-08-29T15:55:47.23088","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Déséquilibre des classes\n\nPresque 75% des images ne présentent pas de maladie (note de 0). Dans ce cas, on utilise la métrique QWK qui permet de pénaliser l'écart entre les classes éloignées, sinon l'accuracy serait biaisée. Il est plus important d'être proche que de prédire une classe fortement éloignée, l'ordre est ordinal et non nominal. D'autant que l'accuracy avec cette encodage est biaisée donc, elle est juste présente à titre indicatrice.","metadata":{"papermill":{"duration":0.00603,"end_time":"2026-08-29T15:59:37.820355","exception":false,"start_time":"2026-08-29T15:59:37.814325","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#visualisation\nclass_counts = train_df[\"level\"].value_counts().sort_index()\n\nplt.figure(figsize=(6,4))\nplt.bar(class_counts.index, class_counts.values)\nplt.xticks(range(NUM_CLASSES))\nplt.xlabel(\"Rétinopathie\")\nplt.ylabel(\"Nombre d'images\")\nplt.title(\"Répartition des classes\")\nplt.show()\nprint(class_counts / len(train_df))","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:17:45.420513Z","iopub.execute_input":"2026-08-30T14:17:45.421646Z","iopub.status.idle":"2026-08-30T14:17:45.594294Z","shell.execute_reply.started":"2026-08-30T14:17:45.421621Z","shell.execute_reply":"2026-08-30T14:17:45.593552Z"},"papermill":{"duration":0.198358,"end_time":"2026-08-29T15:59:38.025092","exception":false,"start_time":"2026-08-29T15:59:37.826734","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Split train / validation\n\nDivision split avec une taille de test de 0.15, GroupShuffleSplit permet de retrouver un patient uniquement dans le train ou la validation mais pas les 2 sinon cela engendre de l'overfitting. Si l'oeil d'un patient est présent dans le train et l'autre en validation, cela peut biaiser le score. Un train test sur les images a été réalisé mais cela engendrait une hausse de la validation.","metadata":{"papermill":{"duration":0.007701,"end_time":"2026-08-29T15:59:38.039723","exception":false,"start_time":"2026-08-29T15:59:38.032022","status":"completed"},"tags":[]}},{"cell_type":"code","source":"from sklearn.model_selection import GroupShuffleSplit\n\n# Identifiant du patient\ndf = train_df.copy()\ndf[\"patient\"] = df[\"image\"].str.split(\"_\").str[0]\n\n# Séparation par patient\ngss = GroupShuffleSplit(n_splits=1, test_size=0.15, random_state=42)\nfor train_idx, val_idx in gss.split(df, groups=df[\"patient\"]):\n    train_df = df.iloc[train_idx].copy()\n    val_df = df.iloc[val_idx].copy()\n\n#suppression colonne temporaire\ntrain_df = train_df.drop(columns=[\"patient\"]).reset_index(drop=True)\nval_df = val_df.drop(columns=[\"patient\"]).reset_index(drop=True)\n\n#nombre de patients en commun\ntrain_patients = set(train_df[\"image\"].str.split(\"_\").str[0])\nval_patients = set(val_df[\"image\"].str.split(\"_\").str[0])\nprint(\"Patients communs :\", len(train_patients & val_patients))","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:17:55.214075Z","iopub.execute_input":"2026-08-30T14:17:55.214819Z","iopub.status.idle":"2026-08-30T14:17:55.317981Z","shell.execute_reply.started":"2026-08-30T14:17:55.214791Z","shell.execute_reply":"2026-08-30T14:17:55.317167Z"},"papermill":{"duration":0.137808,"end_time":"2026-08-29T15:59:38.186711","exception":false,"start_time":"2026-08-29T15:59:38.048903","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Exemples d'image\n\nNous pouvons observer ici des traits communs aux photos : les bords noirs notamment. Nous pouvons également voir que l'orientation des photos pourrait influencer les résultats, le modèle reconnaît plus facilement des images similaires.","metadata":{"papermill":{"duration":0.006413,"end_time":"2026-08-29T15:59:38.200424","exception":false,"start_time":"2026-08-29T15:59:38.194011","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#Affichage d'images\n\nfig, axes = plt.subplots(2,3, figsize=(10,7))\nfor ax, (_, row) in zip(axes.flat, train_df.sample(6, random_state=42).iterrows()):\n\n    image = cv2.imread(row[\"path\"])\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n\n    ax.imshow(image)\n    ax.set_title(f\"Classe {row['level']}\")\n    ax.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:17:56.726275Z","iopub.execute_input":"2026-08-30T14:17:56.726617Z","iopub.status.idle":"2026-08-30T14:18:00.902230Z","shell.execute_reply.started":"2026-08-30T14:17:56.726591Z","shell.execute_reply":"2026-08-30T14:18:00.901352Z"},"papermill":{"duration":7.737355,"end_time":"2026-08-29T15:59:45.944157","exception":false,"start_time":"2026-08-29T15:59:38.206802","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Suppression des bords\n\nIdée reprise notamment du vainqueur de la compétition, prétraiter les images en supprimant les bords noirs notamment pour la résolution et le temps de traitement","metadata":{"papermill":{"duration":0.011924,"end_time":"2026-08-29T15:59:45.968478","exception":false,"start_time":"2026-08-29T15:59:45.956554","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#Suppression des bords noirs\ndef crop_dark_border(image, threshold=12):\n    gray = tf.image.rgb_to_grayscale(image)[..., 0]\n    mask = gray > threshold\n    rows = tf.reduce_any(mask, axis=1)\n    cols = tf.reduce_any(mask, axis=0)\n\n    def crop():\n        y = tf.where(rows)[:, 0]\n        x = tf.where(cols)[:, 0]\n        return image[\n            tf.reduce_min(y):tf.reduce_max(y)+1,\n            tf.reduce_min(x):tf.reduce_max(x)+1,:]\n\n    return tf.cond(tf.reduce_any(mask), crop, lambda: image)","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:00.903961Z","iopub.execute_input":"2026-08-30T14:18:00.904268Z","iopub.status.idle":"2026-08-30T14:18:00.909640Z","shell.execute_reply.started":"2026-08-30T14:18:00.904235Z","shell.execute_reply":"2026-08-30T14:18:00.908820Z"},"papermill":{"duration":0.020951,"end_time":"2026-08-29T15:59:46.001751","exception":false,"start_time":"2026-08-29T15:59:45.9808","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Lecture des images\n\nLa fonction decode_jpeg de tensor flow permet de décoder l'image. Le paramètre ratio permet une résolution réduite. Le temps d'entraînement du modèle après découverte de ce modèle a bcp diminuée.\n","metadata":{"papermill":{"duration":0.011862,"end_time":"2026-08-29T15:59:46.025788","exception":false,"start_time":"2026-08-29T15:59:46.013926","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#lecture d'une image \ndef load_image(path):\n    image = tf.io.read_file(path)\n    image = tf.io.decode_jpeg(image, channels=3, ratio=2)\n    image = tf.cast(image, tf.float32)\n    image = crop_dark_border(image) #suppression des bords\n    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE)) #reisze de l'image\n\n    return image","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:04.287532Z","iopub.execute_input":"2026-08-30T14:18:04.287795Z","iopub.status.idle":"2026-08-30T14:18:04.293549Z","shell.execute_reply.started":"2026-08-30T14:18:04.287773Z","shell.execute_reply":"2026-08-30T14:18:04.292762Z"},"papermill":{"duration":0.020125,"end_time":"2026-08-29T15:59:46.058144","exception":false,"start_time":"2026-08-29T15:59:46.038019","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Encodage ordinal\n\nUn encodage utilisé dans une compétition similaire (APTOS 2019) notamment du notebook de xhlulu \"APTOS 2019: DenseNet Keras Starter\" consiste à encoder les notes de façon ordinale (pour prendre en compte l'écart entre le 0 et le 4). Le codage se déroule donc de façon à classer pour la classe 0 [1,0,0,0,0], [1,1,0,0,0] pour la classe 1,..., [1,1,1,1,1] pour la classe 4.\n\nCette approche force à entraîner le modèle avec une autre fonction loss car on est plus dans le cas initial de classification ordinale.\n\nIdée aussi de MamatShamshiev","metadata":{"papermill":{"duration":0.012,"end_time":"2026-08-29T15:59:46.082798","exception":false,"start_time":"2026-08-29T15:59:46.070798","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#encodage ordinal\ndef ordinal_encoding(label):\n    label = tf.cast(label, tf.int32)\n    return tf.cast(tf.range(NUM_CLASSES, dtype=tf.int32) <= label,tf.float32)","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:05.282910Z","iopub.execute_input":"2026-08-30T14:18:05.283579Z","iopub.status.idle":"2026-08-30T14:18:05.288511Z","shell.execute_reply.started":"2026-08-30T14:18:05.283546Z","shell.execute_reply":"2026-08-30T14:18:05.287593Z"},"papermill":{"duration":0.019654,"end_time":"2026-08-29T15:59:46.114664","exception":false,"start_time":"2026-08-29T15:59:46.09501","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data augmentation\n\nLes images subissent des rotations pour apprendre au modèle tous les cas de photo, indépendamment du sens. L'oeil n'a pas d'orientation, ce qui peut permettre d'expliquer les rotations complètes.\n","metadata":{"papermill":{"duration":0.012039,"end_time":"2026-08-29T15:59:46.138897","exception":false,"start_time":"2026-08-29T15:59:46.126858","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#data augmentation\ndef augment(image):\n    image = tf.image.random_flip_left_right(image)\n    image = tf.image.random_flip_up_down(image)\n    k = tf.random.uniform(\n        shape=[],\n        minval=0,\n        maxval=4,\n        dtype=tf.int32)\n    image = tf.image.rot90(image, k)\n\n    return image","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:05.912785Z","iopub.execute_input":"2026-08-30T14:18:05.913212Z","iopub.status.idle":"2026-08-30T14:18:05.918004Z","shell.execute_reply.started":"2026-08-30T14:18:05.913184Z","shell.execute_reply":"2026-08-30T14:18:05.917076Z"},"papermill":{"duration":0.020893,"end_time":"2026-08-29T15:59:46.172099","exception":false,"start_time":"2026-08-29T15:59:46.151206","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Pipeline \n\nOn construit le dataset final en appliquant les transformations vues précédemment (data augmentation, crop les images et mélange du train uniquement (pour ne pas modifier les prédictions). Cependant pour la validation, on souhaite éviter de rajouter de la data augmentation pour tester. \n\nL'ordre doit aussi rester le même que dans val_df pour la prédiction sinon cela ne correspond plus aux vraies notes.\n\nsource utilisée aussi :\ncifartransferlearning.ipynb ","metadata":{"papermill":{"duration":0.012623,"end_time":"2026-08-29T15:59:46.196908","exception":false,"start_time":"2026-08-29T15:59:46.184285","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#construction des datasets\ndef create_dataset(dataframe, training=False):\n    paths = dataframe[\"path\"].values\n    labels = dataframe[\"level\"].values\n    dataset = tf.data.Dataset.from_tensor_slices((paths, labels))\n\n    if training:\n        dataset = dataset.shuffle(len(dataframe)) #mélanger pr le train\n\n    dataset = dataset.map(\n        lambda path, label: (load_image(path), label), #lire les images\n        num_parallel_calls=AUTOTUNE\n    )\n\n    if training:\n        dataset = dataset.map(\n            lambda image, label: (augment(image), label), #augmenter les données\n            num_parallel_calls=AUTOTUNE\n        )\n\n    dataset = dataset.map(\n        lambda image, label: (\n            image,\n            ordinal_encoding(label) #encodage ordinal\n        ),\n        num_parallel_calls=AUTOTUNE\n    )\n\n    dataset = dataset.batch(BATCH_SIZE) #batch\n    dataset = dataset.prefetch(AUTOTUNE) \n\n    return dataset","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:09.683361Z","iopub.execute_input":"2026-08-30T14:18:09.683908Z","iopub.status.idle":"2026-08-30T14:18:09.691377Z","shell.execute_reply.started":"2026-08-30T14:18:09.683880Z","shell.execute_reply":"2026-08-30T14:18:09.690733Z"},"papermill":{"duration":0.022553,"end_time":"2026-08-29T15:59:46.231765","exception":false,"start_time":"2026-08-29T15:59:46.209212","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = create_dataset(train_df, training=True)\nval_ds = create_dataset(val_df)","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:11.879209Z","iopub.execute_input":"2026-08-30T14:18:11.879978Z","iopub.status.idle":"2026-08-30T14:18:13.363091Z","shell.execute_reply.started":"2026-08-30T14:18:11.879946Z","shell.execute_reply":"2026-08-30T14:18:13.362509Z"},"papermill":{"duration":1.110787,"end_time":"2026-08-29T15:59:47.355502","exception":false,"start_time":"2026-08-29T15:59:46.244715","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"images, labels = next(iter(train_ds))\nprint(images.shape)\nprint(labels.shape)\n\nplt.figure(figsize=(5,5))\nplt.imshow(tf.cast(images[0], tf.uint8))\nplt.title(f\"Encodage : {labels[0].numpy()}\")\nplt.axis(\"off\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2026-08-30T14:18:13.364600Z","iopub.execute_input":"2026-08-30T14:18:13.365387Z","iopub.status.idle":"2026-08-30T14:18:14.398024Z","shell.execute_reply.started":"2026-08-30T14:18:13.365336Z","shell.execute_reply":"2026-08-30T14:18:14.397423Z"},"papermill":{"duration":1.155294,"end_time":"2026-08-29T15:59:48.52468","exception":false,"start_time":"2026-08-29T15:59:47.369386","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Modèle\n\n## Transfer learning avec EfficientNetB3\n\nLe modèle EfficientNet est déjà entraîné sur ImageNet, on utilise la tête que l'on a entraîné pour essayer de détecter les patterns. Les couches convolutives du modèle. (Plusieurs solutions de la compétition APTOS en 2019) mentionnent l'utisation de ce modèle. En raison de l'encodage, on utilise donc le paramètre sigmoid et non softmax\n\n\nUtiliser un Modèle B3 permet de gagner relativement du temps qu'un modèle plus important comme B5 sur le train sans avoir de hausse significative globale en utilisant les modèles juste au dessus. Sachant que plusieurs essais ont été menés avec un modèle B0.","metadata":{"papermill":{"duration":0.014526,"end_time":"2026-08-29T15:59:48.554218","exception":false,"start_time":"2026-08-29T15:59:48.539692","status":"completed"},"tags":[]}},{"cell_type":"code","source":"from tensorflow.keras.applications.efficientnet import preprocess_input\nfrom tensorflow.keras.applications import EfficientNetB3\n\ndef build_model():\n    base_model = EfficientNetB3(          \n        weights=\"imagenet\",\n        include_top=False,\n        input_shape=(IMG_SIZE, IMG_SIZE, 3))\n    inputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\n    x = preprocess_input(inputs)\n    x = base_model(x, training=False)\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.BatchNormalization()(x)\n    x = layers.Dropout(0.3)(x)\n    outputs = layers.Dense(NUM_CLASSES, activation=\"sigmoid\")(x)\n    return models.Model(inputs, outputs), base_model","metadata":{"execution":{"iopub.execute_input":"2026-08-29T15:59:48.592231Z","iopub.status.busy":"2026-08-29T15:59:48.591532Z","iopub.status.idle":"2026-08-29T15:59:48.601811Z","shell.execute_reply":"2026-08-29T15:59:48.600606Z"},"papermill":{"duration":0.035792,"end_time":"2026-08-29T15:59:48.604645","exception":false,"start_time":"2026-08-29T15:59:48.568853","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model, base_model = build_model()\nmodel.summary()","metadata":{"execution":{"iopub.execute_input":"2026-08-29T15:59:48.647475Z","iopub.status.busy":"2026-08-29T15:59:48.646825Z","iopub.status.idle":"2026-08-29T15:59:51.264887Z","shell.execute_reply":"2026-08-29T15:59:51.26389Z"},"papermill":{"duration":2.640284,"end_time":"2026-08-29T15:59:51.266659","exception":false,"start_time":"2026-08-29T15:59:48.626375","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#compilation\nmodel.compile(optimizer=tf.keras.optimizers.Adam(LR_HEAD),loss=\"binary_crossentropy\",metrics=[\"accuracy\"])","metadata":{"execution":{"iopub.execute_input":"2026-08-29T15:59:51.313028Z","iopub.status.busy":"2026-08-29T15:59:51.312186Z","iopub.status.idle":"2026-08-29T15:59:51.326345Z","shell.execute_reply":"2026-08-29T15:59:51.325117Z"},"papermill":{"duration":0.035493,"end_time":"2026-08-29T15:59:51.328434","exception":false,"start_time":"2026-08-29T15:59:51.292941","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Callbacks\n\nEarlyStopping arrête si la val_loss ne s'améliore plus pendant 3 epochs et \nremet les meilleurs poids. ReduceLROnPlateau divise le learning rate par 2 \nquand ça stagne","metadata":{"papermill":{"duration":0.019177,"end_time":"2026-08-29T15:59:51.365918","exception":false,"start_time":"2026-08-29T15:59:51.346741","status":"completed"},"tags":[]}},{"cell_type":"code","source":"from tensorflow.keras.callbacks import ModelCheckpoint\n\n#callbacks\ncheckpoint = ModelCheckpoint(\"best_model.keras\",monitor=\"val_loss\",save_best_only=True,verbose=1)\nearly_stop = EarlyStopping(monitor=\"val_loss\",patience=3,restore_best_weights=True,verbose=1)\nreduce_lr = ReduceLROnPlateau(monitor=\"val_loss\",factor=0.5,patience=2,min_lr=0.000001,verbose=1)","metadata":{"execution":{"iopub.execute_input":"2026-08-29T15:59:51.402776Z","iopub.status.busy":"2026-08-29T15:59:51.402118Z","iopub.status.idle":"2026-08-29T15:59:51.407724Z","shell.execute_reply":"2026-08-29T15:59:51.406713Z"},"papermill":{"duration":0.025758,"end_time":"2026-08-29T15:59:51.409411","exception":false,"start_time":"2026-08-29T15:59:51.383653","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train\n\nOn entraîne uniquement la tête sur le modèle Efficient Net à l'aide de poids aléatoires. On utilise ensuite un learning rate plus petit pour s'adapter vraiment aux données. Cette méthode était utilisée parmi certaines bonnes solutions également en gelant toutes les couches sauf la tête","metadata":{"papermill":{"duration":0.068494,"end_time":"2026-08-29T15:59:51.496513","exception":false,"start_time":"2026-08-29T15:59:51.428019","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#train avec transfert learning\nbase_model.trainable = False\nhistory_head = model.fit(train_ds,validation_data=val_ds,epochs=EPOCHS_HEAD,\n                         callbacks=[checkpoint],verbose=1)","metadata":{"execution":{"iopub.execute_input":"2026-08-29T15:59:51.532189Z","iopub.status.busy":"2026-08-29T15:59:51.531823Z","iopub.status.idle":"2026-08-29T16:42:14.095831Z","shell.execute_reply":"2026-08-29T16:42:14.093931Z"},"papermill":{"duration":2542.586868,"end_time":"2026-08-29T16:42:14.100486","exception":false,"start_time":"2026-08-29T15:59:51.513618","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#fine tuning\nbase_model.trainable = True\nmodel.compile(optimizer=tf.keras.optimizers.Adam(LR_FINE),loss=\"binary_crossentropy\",metrics=[\"accuracy\"])\n\nhistory_fine = model.fit(train_ds,validation_data=val_ds,epochs=EPOCHS_FINE,\n                         callbacks=[checkpoint, early_stop, reduce_lr],verbose=1)","metadata":{"execution":{"iopub.execute_input":"2026-08-29T16:42:14.85467Z","iopub.status.busy":"2026-08-29T16:42:14.853505Z","iopub.status.idle":"2026-08-29T18:12:08.805066Z","shell.execute_reply":"2026-08-29T18:12:08.802126Z"},"papermill":{"duration":5395.154709,"end_time":"2026-08-29T18:12:09.63395","exception":false,"start_time":"2026-08-29T16:42:14.479241","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Evaluation\n\nOn sort ensuite des probabilités et on regarde combien dépassent le seuil. En fonction du nombre de dépassements, on définit la classe.","metadata":{"papermill":{"duration":1.11769,"end_time":"2026-08-29T18:12:11.885397","exception":false,"start_time":"2026-08-29T18:12:10.767707","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#évalution\npredictions = model.predict(val_ds, verbose=0)\ny_pred = np.clip((predictions > 0.5).sum(axis=1) - 1,0,NUM_CLASSES - 1)\ny_true = val_df[\"level\"].values\n\nqwk = cohen_kappa_score(y_true,y_pred,weights=\"quadratic\")\n\nprint(f\"QWK : {qwk}\")","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:12:14.04953Z","iopub.status.busy":"2026-08-29T18:12:14.048933Z","iopub.status.idle":"2026-08-29T18:15:10.782745Z","shell.execute_reply":"2026-08-29T18:15:10.781928Z"},"papermill":{"duration":178.970286,"end_time":"2026-08-29T18:15:11.902435","exception":false,"start_time":"2026-08-29T18:12:12.932149","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#matrice de confusion\ncm = confusion_matrix(y_true, y_pred)\n\nplt.figure(figsize=(6,6))\nplt.imshow(cm, cmap=\"Blues\")\nplt.colorbar()\nplt.xlabel(\"Classe prédite\")\nplt.ylabel(\"Classe réelle\")\nplt.title(\"Matrice de confusion\")\nplt.xticks(range(NUM_CLASSES))\nplt.yticks(range(NUM_CLASSES))\nfor i in range(NUM_CLASSES):\n    for j in range(NUM_CLASSES):\n        plt.text(j,i,\n            cm[i, j],\n            ha=\"center\",\n            va=\"center\")\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:14.070177Z","iopub.status.busy":"2026-08-29T18:15:14.069251Z","iopub.status.idle":"2026-08-29T18:15:14.382605Z","shell.execute_reply":"2026-08-29T18:15:14.381625Z"},"papermill":{"duration":1.440124,"end_time":"2026-08-29T18:15:14.384523","exception":false,"start_time":"2026-08-29T18:15:12.944399","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#fct de prédiction\ndef predict_labels(probabilities, thresholds):\n    thresholds = np.asarray(thresholds)\n    predictions = (probabilities > thresholds).sum(axis=1) - 1\n    return np.clip(predictions, 0, NUM_CLASSES - 1)","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:16.539075Z","iopub.status.busy":"2026-08-29T18:15:16.53827Z","iopub.status.idle":"2026-08-29T18:15:16.543333Z","shell.execute_reply":"2026-08-29T18:15:16.54245Z"},"papermill":{"duration":1.107802,"end_time":"2026-08-29T18:15:16.545182","exception":false,"start_time":"2026-08-29T18:15:15.43738","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## TTA \n\n(https://www.sciencedirect.com/science/article/pii/S1877050923000947) + utilisé dans des solutions gagnantes (ex : https://diyago.github.io/2019/10/04/kaggle-blindness.html) permet de faire la moyenne des probas sur les différentes transformations","metadata":{"papermill":{"duration":1.165057,"end_time":"2026-08-29T18:15:18.824797","exception":false,"start_time":"2026-08-29T18:15:17.65974","status":"completed"},"tags":[]}},{"cell_type":"code","source":"# TTA : on prédit chaque image en 4 versions (original + flips) puis on fait la moyenne\ndef predict_tta(model, paths):\n    preds = []\n    for fh in [False, True]:\n        for fv in [False, True]:\n            def load_tta(p):\n                img = load_image(p)\n                if fh: img = tf.image.flip_left_right(img)\n                if fv: img = tf.image.flip_up_down(img)\n                return img\n            ds = (tf.data.Dataset.from_tensor_slices(paths)\n                  .map(load_tta, num_parallel_calls=AUTOTUNE)\n                  .batch(BATCH_SIZE).prefetch(AUTOTUNE))\n            preds.append(model.predict(ds, verbose=1))\n    return np.mean(preds, axis=0)","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:20.987986Z","iopub.status.busy":"2026-08-29T18:15:20.987618Z","iopub.status.idle":"2026-08-29T18:15:20.993672Z","shell.execute_reply":"2026-08-29T18:15:20.99274Z"},"papermill":{"duration":1.127806,"end_time":"2026-08-29T18:15:20.995182","exception":false,"start_time":"2026-08-29T18:15:19.867376","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Optimisation des seuils\n\nLe seuil utilise est arbitraire. Modifier ces seuils afin de détecter la maladie permet d'ajuster la sensibilité des différentes classes. Par exemple, baisser les seuils sur les classes rares plus sensibles. Cela permet de corriger le déséquilibre.\n\nEn termes de score cela a permis de passer sur le QWK de validation de 0.66 à  plus de 0.70 avant de choisir les meilleurs images sur les premiers tests.\n\nLa 3ème place de cette compétition utilisait une méthodologie similaire + le code de xhlulu. Cependant, les seuils choisis peuvent biaiser totalement les performances du modèle.","metadata":{"papermill":{"duration":1.047156,"end_time":"2026-08-29T18:15:23.150395","exception":false,"start_time":"2026-08-29T18:15:22.103239","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#optimisation des seuils\nfrom scipy.optimize import minimize\n\ndef optimize_thresholds(probabilities, y_true):\n    def objective(thresholds):\n        y_pred = predict_labels(probabilities, thresholds)\n        score = cohen_kappa_score(y_true,y_pred,weights=\"quadratic\")\n        return -score\n    initial = [0.5] * NUM_CLASSES\n    bounds = [(0.2, 0.8)] * NUM_CLASSES\n    result = minimize(objective,initial,bounds=bounds,method=\"Powell\")\n    return result.x\n\nbest_thresholds = optimize_thresholds(predictions,y_true)\nprint(\"Seuils optimaux :\")\nprint(np.round(best_thresholds, 3))","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:25.407916Z","iopub.status.busy":"2026-08-29T18:15:25.407544Z","iopub.status.idle":"2026-08-29T18:15:25.788806Z","shell.execute_reply":"2026-08-29T18:15:25.787853Z"},"papermill":{"duration":1.514532,"end_time":"2026-08-29T18:15:25.790665","exception":false,"start_time":"2026-08-29T18:15:24.276133","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#predictions optimisées\nbest_pred = predict_labels(predictions,best_thresholds)\nbest_qwk = cohen_kappa_score(y_true,best_pred,weights=\"quadratic\")\n\nprint(f\"QWK opti: {best_qwk:.4f}\")","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:27.960233Z","iopub.status.busy":"2026-08-29T18:15:27.959869Z","iopub.status.idle":"2026-08-29T18:15:27.967653Z","shell.execute_reply":"2026-08-29T18:15:27.96644Z"},"papermill":{"duration":1.052187,"end_time":"2026-08-29T18:15:27.969998","exception":false,"start_time":"2026-08-29T18:15:26.917811","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# on vérifie sur la validation AVANT de l'appliquer au test\nval_tta = predict_tta(model, val_df[\"path\"].values)\n\nqwk_sans = cohen_kappa_score(y_true, predict_labels(predictions, best_thresholds), weights=\"quadratic\")\nqwk_avec = cohen_kappa_score(y_true, predict_labels(val_tta, best_thresholds), weights=\"quadratic\")\nprint(f\"QWK sans TTA {qwk_sans:.4f}\")\nprint(f\"QWK avec TTA {qwk_avec:.4f}\")\n\n# on recalcule les seuils sur les probas TTA\nthresholds_tta = optimize_thresholds(val_tta, y_true)\nprint(f\"Seuils TTA :\", np.round(thresholds_tta, 3))\nprint(f\"QWK TTA et seuils: {cohen_kappa_score(y_true, predict_labels(val_tta, thresholds_tta), weights='quadratic'):.4f}\")","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:15:30.30423Z","iopub.status.busy":"2026-08-29T18:15:30.303415Z","iopub.status.idle":"2026-08-29T18:25:46.158847Z","shell.execute_reply":"2026-08-29T18:25:46.157731Z"},"papermill":{"duration":616.9849,"end_time":"2026-08-29T18:25:46.16071","exception":false,"start_time":"2026-08-29T18:15:29.17581","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Blending des deux yeux\n\nUn problème est que les yeux du patient gauches et droits qui sont nommés patient_oeil sont corrélés. L'information d'un oeil peut être utilisé pour l'autre. Cette idée de regrouper l'information avec du blending est réalisée dans différents projets : (https://github.com/sveitser/kaggle_diabetic), un blending est réalisé : une moyenne pondérée entre l'oeil et la moyenne du patient. Le seuil alpha permet de prendre plus ou moins eb compte la moyenne.\n","metadata":{"papermill":{"duration":1.286288,"end_time":"2026-08-29T18:25:48.695195","exception":false,"start_time":"2026-08-29T18:25:47.408907","status":"completed"},"tags":[]}},{"cell_type":"code","source":"#essai blending en regroupant les probas des yeux gauches et droites\n\ndef patient_mean(df, proba):\n    d = df.reset_index(drop=True).copy()\n    d[\"patient\"] = d[\"image\"].str.split(\"_\").str[0]\n    cols = [f\"p{k}\" for k in range(NUM_CLASSES)]\n    for k in range(NUM_CLASSES):\n        d[f\"p{k}\"] = proba[:, k]\n    return d[cols].values, d.groupby(\"patient\")[cols].transform(\"mean\").values\n\np_self, p_moy = patient_mean(val_df, val_tta)\n\nALPHA, thresholds_blend, best_q = 1.0, thresholds_tta, -1\nfor a in [1.0, 0.9, 0.85, 0.8, 0.7, 0.6, 0.5]:\n    pf = a * p_self + (1 - a) * p_moy\n    th = optimize_thresholds(pf, y_true)\n    q = cohen_kappa_score(y_true, predict_labels(pf, th), weights=\"quadratic\")\n    print(f\"alpha={a} : QWK = {q:.4f}\")\n    if q > best_q:\n        best_q, ALPHA, thresholds_blend = q, a, th\n\nprint(f\"alpha final {ALPHA} (QWK {best_q:.4f})\")\nprint(f\"Seuils\", np.round(thresholds_blend, 3))","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:25:51.103798Z","iopub.status.busy":"2026-08-29T18:25:51.103433Z","iopub.status.idle":"2026-08-29T18:25:53.498443Z","shell.execute_reply":"2026-08-29T18:25:53.497383Z"},"papermill":{"duration":3.563921,"end_time":"2026-08-29T18:25:53.500374","exception":false,"start_time":"2026-08-29T18:25:49.936453","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Sauvegarde\n\nOn sauvegarde les poids du modèle, les seuils optimisés et le coefficient alpha \ndu blending.","metadata":{"papermill":{"duration":1.233364,"end_time":"2026-08-29T18:25:55.966871","exception":false,"start_time":"2026-08-29T18:25:54.733507","status":"completed"},"tags":[]}},{"cell_type":"code","source":"model.save_weights(\"model.weights.h5\")\nmodel.save(\n    \"final_model.keras\"\n)\nnp.save(\"thresholds.npy\", np.array(thresholds_blend))\nnp.save(\"alpha.npy\", np.array([ALPHA]))\nprint(\"ALPHA\", ALPHA, \"seuils\", np.round(thresholds_blend, 3))","metadata":{"execution":{"iopub.execute_input":"2026-08-29T18:25:58.453028Z","iopub.status.busy":"2026-08-29T18:25:58.452245Z","iopub.status.idle":"2026-08-29T18:25:59.448717Z","shell.execute_reply":"2026-08-29T18:25:59.447781Z"},"papermill":{"duration":2.253441,"end_time":"2026-08-29T18:25:59.450687","exception":false,"start_time":"2026-08-29T18:25:57.197246","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Limites \n\n-Manque de validation croisée en raison du temps de traitement\n-Pas de regroupement de modèles\n-La classe 0 et 1 sont assez confondues et la classe 2 est sous prédite.","metadata":{"papermill":{"duration":1.239169,"end_time":"2026-08-29T18:26:01.860477","exception":false,"start_time":"2026-08-29T18:26:00.621308","status":"completed"},"tags":[]}}]}