{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":45867,"databundleVersionId":6924515,"sourceType":"competition"},{"sourceId":14026663,"sourceType":"datasetVersion","datasetId":8932529},{"sourceId":14026716,"sourceType":"datasetVersion","datasetId":8932561},{"sourceId":14026914,"sourceType":"datasetVersion","datasetId":8932654},{"sourceId":14026950,"sourceType":"datasetVersion","datasetId":8932676},{"sourceId":14026981,"sourceType":"datasetVersion","datasetId":8932694}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# import pandas as pd\n# import numpy as np\n# import matplotlib.pyplot as plt\n# import seaborn as sns\n# import os\n# from glob import glob\n# from PIL import Image\n# \n# # ==============================================================================\n# # 1. CHARGEMENT DES DONNÉES ET VÉRIFICATION INITIALE\n# # ==============================================================================\n# print(\"1. CHARGEMENT ET VÉRIFICATION DES FICHIERS CSV\")\n# \n# DATA_PATH = '../input/UBC-OCEAN/' \n# df_train = None \n# data_loaded = False \n# \n# try:\n# \tdf_train = pd.read_csv(os.path.join(DATA_PATH, 'train.csv'))\n# \tdf_test = pd.read_csv(os.path.join(DATA_PATH, 'test.csv'))\n# \t\n# \t# Affichage des premières lignes et info\n# \tprint(f\"Forme de df_train: {df_train.shape}\")\n# \tprint(df_train.head())\n# \tprint(\"\\nInformations sur les colonnes:\")\n# \tdf_train.info()\n# \t\n# \tprint(\"\\nValeurs manquantes:\")\n# \tprint(df_train.isnull().sum())\n# \t\n# \tdata_loaded = True \n# \n# except FileNotFoundError as e:\n# \tprint(f\"Erreur CRITIQUE de chargement: Assurez-vous que le chemin est correct. {e}\")\n# \tprint(\"Le reste de l'EDA ne peut pas s'exécuter.\")\n# \n# # ==============================================================================\n# # 2. ANALYSE DE LA VARIABLE CIBLE\n# # ==============================================================================\n# if data_loaded:\n# \tprint(\"\\n2. ANALYSE DE LA DISTRIBUTION DE LA VARIABLE CIBLE ('label')\")\n# \n# \tlabel_counts = df_train['label'].value_counts()\n# \tlabel_percent = df_train['label'].value_counts(normalize=True) * 100\n# \n# \tprint(f\"\\nComptage par sous-type:\\n{label_counts}\")\n# \tprint(f\"\\nPourcentage par sous-type:\\n{label_percent.round(2)}\")\n# \n# \tplt.figure(figsize=(10, 5))\n# \tsns.barplot(x=label_counts.index, y=label_counts.values, palette='viridis')\n# \tplt.title('Distribution des Sous-types de Cancer de l\\'Ovaire')\n# \tplt.ylabel('Nombre d\\'échantillons')\n# \tplt.xlabel('Sous-type')\n# \t\n# \t# Affichage du pourcentage sur les barres\n# \tfor i, count in enumerate(label_counts.values):\n# \t\tplt.text(i, count + 1, f\"{count} ({label_percent.values[i]:.1f}%)\", ha='center')\n# \t\n# \tplt.show()\n# \n# # ==============================================================================\n# # 3. ANALYSE DES IMAGES\n# # ==============================================================================\n# \tprint(\"\\n3. ANALYSE DES IMAGES ET THUMBNAILS\")\n# \tTHUMBNAIL_PATH = os.path.join(DATA_PATH, 'train_thumbnails')\n# \t\n# \t# Fonction d'ouverture d'image avec gestion d'erreur\n# \tdef load_image(img_id):\n# \t\tfile_name = f'{img_id}_thumbnail.png'\n# \t\timg_path = os.path.join(THUMBNAIL_PATH, file_name)\n# \t\ttry:\n# \t\t\treturn Image.open(img_path)\n# \t\texcept FileNotFoundError:\n# \t\t\treturn None\n# \n# \tdf_train_unique_labels = df_train.drop_duplicates(subset=['label']).reset_index(drop=True)\n# \n# \tdimensions = []\n# \tfor img_id in df_train_unique_labels['image_id']:\n# \t\timg = load_image(img_id)\n# \t\tif img:\n# \t\t\tdimensions.append((img_id, img.size))\n# \t\telse:\n# \t\t\tdimensions.append((img_id, 'FICHIER INTROUVABLE'))\n# \tprint(f\"Dimensions des exemples par classe (ID, Largeur, Hauteur): {dimensions}\")\n# \n# \t# Visualisation des thumbnails\n# \tn_classes = len(df_train_unique_labels)\n# \tfig, axes = plt.subplots(1, n_classes, figsize=(4*n_classes, 4))\n# \taxes = axes.flatten() if n_classes > 1 else [axes]\n# \tplt.suptitle('Exemples de Thumbnails par Sous-type (1 par classe)', fontsize=16)\n# \n# \tfor i, row in df_train_unique_labels.iterrows():\n# \t\timg = load_image(row['image_id'])\n# \t\tif img:\n# \t\t\taxes[i].imshow(img)\n# \t\t\taxes[i].set_title(f\"{row['label']} (ID: {row['image_id']})\", fontsize=12)\n# \t\telse:\n# \t\t\taxes[i].text(0.5, 0.5, f\"{row['label']}\\nERREUR: Fichier manquant\", \n# \t\t\t\t\t\t ha='center', va='center', color='red')\n# \t\taxes[i].axis('off')\n# \n# \tplt.tight_layout(rect=[0, 0.03, 1, 0.95])\n# \tplt.show()\n# \n# # ==============================================================================\n# # 4. CONCLUSION RAPIDE DE L'EDA\n# # ==============================================================================\n# \tprint(\"\\n4. RÉSUMÉ DES PREMIERS CONSTATS\")\n# \tprint(\"------------------------------\")\n# \tprint(f\"Classes détectées: {df_train['label'].nunique()}\")\n# \tprint(f\"Classe dominante: {label_percent.idxmax()} ({label_percent.max():.2f}%)\")\n# \tprint(\"Fort déséquilibre des classes confirmé → Balanced Accuracy recommandé.\")\n# \tprint(\"Prochaine étape: gestion des images et intégration des métadonnées CSV/JSON.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import numpy as np\n# import pandas as pd\n# import os\n# from PIL import Image\n# import cv2\n# from tqdm import tqdm # Barre de progression\n# import matplotlib.pyplot as plt\n# \n# # ==============================================================================\n# # PARAMÈTRES CLÉS\n# # ==============================================================================\n# DATA_PATH = '../input/UBC-OCEAN/'\n# WSI_PATH = os.path.join(DATA_PATH, 'train_thumbnails')\n# OUTPUT_DIR = './train_patches_from_thumbnails'\n# \n# PATCH_SIZE = 256\n# TISSUE_THRESHOLD = 0.05\n# MAX_PATCHES_COMMON = 500\n# MAX_PATCHES_RARE = 1500\n# RARE_LABELS = ['LGSC', 'MC']\n# \n# # ==============================================================================\n# # FONCTIONS\n# # ==============================================================================\n# def create_tissue_mask_from_png(img_pil, min_saturation=15, max_value=220):\n# \t\"\"\"Crée un masque binaire indiquant les zones de tissu.\"\"\"\n# \timg_np = np.array(img_pil.convert(\"RGB\"), dtype=np.uint8)\n# \thsv = cv2.cvtColor(img_np, cv2.COLOR_RGB2HSV)\n# \tmask = (hsv[:, :, 1] > min_saturation) & (hsv[:, :, 2] < max_value)\n# \treturn mask\n# \n# def save_patch(patch_img, img_id, label, x, y):\n# \tpatch_filename = f'{label}_{img_id}_{x}_{y}.png'\n# \tpatch_img.save(os.path.join(OUTPUT_DIR, label, patch_filename))\n# \n# def extract_valid_patches_from_png(img_pil, tissue_mask, img_id, label):\n# \t\"\"\"Extrait les patches valides contenant assez de tissu.\"\"\"\n# \tmax_patches = MAX_PATCHES_RARE if label in RARE_LABELS else MAX_PATCHES_COMMON\n# \twidth, height = img_pil.size\n# \tpatch_count = 0\n# \n# \tfor x in range(0, width - PATCH_SIZE + 1, PATCH_SIZE):\n# \t\tfor y in range(0, height - PATCH_SIZE + 1, PATCH_SIZE):\n# \t\t\tif patch_count >= max_patches:\n# \t\t\t\treturn patch_count\n# \n# \t\t\tpatch_mask = tissue_mask[y:y + PATCH_SIZE, x:x + PATCH_SIZE]\n# \t\t\ttissue_ratio = np.sum(patch_mask) / patch_mask.size\n# \n# \t\t\tif tissue_ratio > TISSUE_THRESHOLD:\n# \t\t\t\tpatch_img = img_pil.crop((x, y, x + PATCH_SIZE, y + PATCH_SIZE))\n# \t\t\t\tsave_patch(patch_img, img_id, label, x, y)\n# \t\t\t\tpatch_count += 1\n# \n# \treturn patch_count\n# \n# # ==============================================================================\n# # LOGIQUE PRINCIPALE\n# # ==============================================================================\n# if df_train is not None:\n# \tos.makedirs(OUTPUT_DIR, exist_ok=True)\n# \tfor label in df_train['label'].unique():\n# \t\tos.makedirs(os.path.join(OUTPUT_DIR, label), exist_ok=True)\n# \n# \ttotal_patches = 0\n# \tprint(f\"Début du traitement des {len(df_train)} vignettes...\")\n# \n# \tfor index, row in tqdm(df_train.iterrows(), total=len(df_train)):\n# \t\timg_id = row['image_id']\n# \t\tlabel = row['label']\n# \t\twsi_filename = f'{img_id}_thumbnail.png'\n# \t\twsi_path = os.path.join(WSI_PATH, wsi_filename)\n# \n# \t\ttry:\n# \t\t\timg_pil = Image.open(wsi_path)\n# \t\t\ttissue_mask = create_tissue_mask_from_png(img_pil)\n# \t\t\textracted = extract_valid_patches_from_png(img_pil, tissue_mask, img_id, label)\n# \t\t\ttotal_patches += extracted\n# \t\texcept FileNotFoundError:\n# \t\t\tprint(f\"Fichier introuvable : {wsi_path}\")\n# \t\texcept Exception as e:\n# \t\t\tprint(f\"Erreur pour {img_id}: {e}\")\n# \n# \tprint(f\"\\nFin du traitement. Total de patches extraits : {total_patches}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\nimport random\nfrom PIL import Image\n\n# Fonction custom pour rotation 0/90/180/270\nclass RandomRotation90:\n    def __call__(self, img: Image.Image):\n        angles = [0, 90, 180, 270]\n        angle = random.choice(angles)\n        return img.rotate(angle)\n\n# ----------------------------\n# Transformations pour l'entraînement (augmentation + normalisation)\n# ----------------------------\nTRAIN_TRANSFORMS = transforms.Compose([\n    RandomRotation90(),          # Rotations multiples de 90°\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225])\n])\n\n# ----------------------------\n# Transformations pour validation/test (normalisation uniquement)\n# ----------------------------\nVALID_TRANSFORMS = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225])\n])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torchvision.models as models\nfrom sklearn.preprocessing import StandardScaler # Gardé pour la clarté des imports\n\n# --- Préparation des données tabulaires (COMMENTÉ car dépend de df_train) ---\n# print(\"1. Préparation des données tabulaires pour la fusion...\")\n# df_train['is_tma_encoded'] = df_train['is_tma'].astype(int)\n# if 'image_width' in df_train.columns and 'image_height' in df_train.columns:\n#     scaler = StandardScaler()\n#     df_train[['width_norm', 'height_norm']] = scaler.fit_transform(\n#         df_train[['image_width', 'image_height']].fillna(0)\n#     )\n# TABULAR_FEATURES = ['is_tma_encoded', 'width_norm', 'height_norm']\n# NUM_TAB_FEATURES = len(TABULAR_FEATURES)\n# LABEL_MAPPING = {label: i for i, label in enumerate(df_train['label'].unique())}\n# NUM_CLASSES = df_train['label'].nunique()\n# print(f\"-> {NUM_TAB_FEATURES} caractéristiques tabulaires définies.\")\n# print(f\"-> {NUM_CLASSES} classes cibles définies.\")\n\n# --- Définition du Modèle Bimodale (CORRIGÉE) ---\nprint(\"2. Définition du Modèle de Fusion Bimodale (CNN + MLP) chargée et corrigée pour le mode SANS INTERNET.\")\n\nclass BimodalFusionModel(nn.Module):\n    def __init__(self, num_classes, num_tabular_features, hidden_dim=64, dropout_tab=0.2, dropout_fusion=0.3, freeze_cnn=False):\n        super().__init__()\n\n        # --- Branche CNN (EfficientNet-B0) ---\n        # CORRECTION ICI : Utiliser weights=None pour éviter le téléchargement Internet\n        self.cnn_extractor = models.efficientnet_b0(weights=None)\n        \n        # Le reste utilise l'architecture EfficientNet-B0 standard\n        num_image_features = self.cnn_extractor.classifier[1].in_features\n        self.cnn_extractor.classifier = nn.Identity()\n\n        if freeze_cnn:\n            for param in self.cnn_extractor.features.parameters():\n                param.requires_grad = False\n\n        # --- Branche MLP tabulaire ---\n        self.tabular_mlp = nn.Sequential(\n            nn.Linear(num_tabular_features, hidden_dim),\n            nn.ReLU(),\n            nn.BatchNorm1d(hidden_dim),\n            nn.Dropout(dropout_tab),\n            nn.Linear(hidden_dim, hidden_dim // 2)\n        )\n\n        # --- Fusion et classification ---\n        total_fusion_size = num_image_features + (hidden_dim // 2)\n        self.classifier = nn.Sequential(\n            nn.Linear(total_fusion_size, 256),\n            nn.ReLU(),\n            nn.Dropout(dropout_fusion),\n            nn.Linear(256, num_classes)\n        )\n\n        # --- Initialisation des poids linéaires ---\n        for m in self.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.kaiming_normal_(m.weight, nonlinearity='relu')\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n\n    def forward(self, image_patches, tabular_data):\n        assert image_patches.dim() == 4, \"Images doivent être 4D (B, C, H, W)\"\n        assert tabular_data.dim() == 2, \"Tabular data doit être 2D (B, features)\"\n\n        image_features = self.cnn_extractor(image_patches)\n        tabular_features = self.tabular_mlp(tabular_data)\n        fusion_features = torch.cat((image_features, tabular_features), dim=1)\n        output = self.classifier(fusion_features)\n        return output\n\n# --- Test d'initialisation (COMMENTÉ car dépend de variables manquantes) ---\n# model = BimodalFusionModel(NUM_CLASSES, NUM_TAB_FEATURES)\n# print(f\"Modèle initialisé. Taille features fusionnées : {model.classifier[0].in_features}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import torch\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms\n# from PIL import Image\n# import os\n# import numpy as np\n# from sklearn.model_selection import StratifiedKFold\n# from tqdm.auto import tqdm\n# from glob import glob\n# \n# PATCHES_DIR = './train_patches_from_thumbnails'\n# \n# class OvarianCancerDataset(Dataset):\n# \t\"\"\"Dataset personnalisé pour la classification multi-modale (Image + Tabulaire).\"\"\"\n# \n# \tdef __init__(self, df_data, patches_root_dir, transform=None, tabular_features=None, label_mapping=None):\n# \t\tself.df = df_data.reset_index(drop=True)\n# \t\tself.patches_root_dir = patches_root_dir\n# \t\tself.transform = transform\n# \t\tself.tabular_features = tabular_features if tabular_features else []\n# \t\tself.label_mapping = label_mapping if label_mapping else {}\n# \t\tself.patch_list = self._create_patch_list()\n# \n# \tdef _create_patch_list(self):\n# \t\t\"\"\"Crée une liste de dictionnaires contenant les chemins de patches, labels et features tabulaires.\"\"\"\n# \t\tall_patches = []\n# \t\tfor index, row in tqdm(self.df.iterrows(), total=len(self.df), desc=\"Création de la liste des patches\"):\n# \t\t\timg_id = row['image_id']\n# \t\t\tlabel = row['label']\n# \t\t\tlabel_dir = os.path.join(self.patches_root_dir, label)\n# \t\t\tif os.path.isdir(label_dir):\n# \t\t\t\tpatch_files = glob(os.path.join(label_dir, f'{label}_{img_id}_*.png'))\n# \t\t\t\tfor patch_path in patch_files:\n# \t\t\t\t\ttabular_data = row[self.tabular_features].fillna(0).values.astype(np.float32)\n# \t\t\t\t\tall_patches.append({\n# \t\t\t\t\t\t'path': patch_path,\n# \t\t\t\t\t\t'label': self.label_mapping.get(label, -1),\n# \t\t\t\t\t\t'tabular': tabular_data\n# \t\t\t\t\t})\n# \t\treturn all_patches\n# \n# \tdef __len__(self):\n# \t\treturn len(self.patch_list)\n# \n# \tdef __getitem__(self, idx):\n# \t\titem = self.patch_list[idx]\n# \t\t# Utiliser \"with\" pour garantir la fermeture du fichier\n# \t\twith Image.open(item['path']).convert('RGB') as img:\n# \t\t\timage = img.copy()\n# \t\tif self.transform:\n# \t\t\timage = self.transform(image)\n# \t\ttabular_data = torch.tensor(item['tabular'], dtype=torch.float32)\n# \t\tlabel = torch.tensor(item['label'], dtype=torch.long)\n# \t\treturn image, tabular_data, label\n# \n# # --- Validation croisée stratifiée ---\n# print(\"\\nPréparation des indices pour la Validation Croisée (K-Fold)...\")\n# N_SPLITS = 5 # Ajuster si nécessaire\n# skf = StratifiedKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n# \n# X_indices = df_train.index.values\n# y_labels = df_train['label'].map(LABEL_MAPPING).values\n# \n# print(f\"Validation croisée sur {N_SPLITS} folds prête.\")\n# print(\"Classe OvarianCancerDataset définie et opérationnelle.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ==============================================================================\n# # ÉTAPE PRÉLIMINAIRE : INSPECTION, FILTRAGE DES PATCHES & VÉRIFICATION DE LA STRATIFICATION\n# # ==============================================================================\n# import os\n# import glob\n# import pandas as pd\n# import numpy as np\n# \n# # ATTENTION: Assurez-vous que df_train, LABEL_MAPPING et PATCHES_DIR sont définis ici !\n# # PATCHES_DIR est défini dans la cellule de la classe Dataset, mais on le redéfinit ici par sécurité.\n# PATCHES_DIR = './train_patches_from_thumbnails'\n# \n# print(\"--- 1. Inspection de la Distribution des Classes (WSI) ---\")\n# initial_df_size = len(df_train)\n# print(f\"Taille initiale de df_train: {initial_df_size} WSI\")\n# print(\"Distribution initiale des labels (WSI):\")\n# print(df_train['label'].value_counts())\n# print(\"-\" * 30)\n# \n# # ------------------------------------------------------------------------------\n# # 2. INSPECTION ET FILTRAGE DES PATCHES EXISTANTS\n# # ------------------------------------------------------------------------------\n# print(\"\\n--- 2. Inspection et Filtrage des Patches sur le Disque ---\")\n# \n# # a. Compter le nombre total de patches et vérifier la structure\n# all_png_files = glob.glob(os.path.join(PATCHES_DIR, '**', '*.png'), recursive=True)\n# count = len(all_png_files)\n# \n# print(f\"Total de fichiers .png trouvés dans {PATCHES_DIR}: {count}\")\n# \n# if count < initial_df_size * 5: # Un seuil très bas pour détecter un patching incomplet\n# \tprint(\"⚠️ ALERTE: Le nombre de patches est très faible (moins de 5 patches/WSI en moyenne). Risque de folds vides.\")\n# \n# # b. Identifier les Image IDs ayant réellement des patches\n# existing_patch_ids = set()\n# for label in df_train['label'].unique():\n# \tlabel_dir = os.path.join(PATCHES_DIR, label)\n# \tif os.path.isdir(label_dir):\n# \t\tfor filename in os.listdir(label_dir):\n# \t\t\tif filename.endswith(\".png\"):\n# \t\t\t\ttry:\n# \t\t\t\t\t# L'ID est le deuxième élément après le split par '_'\n# \t\t\t\t\tparts = filename.split('_')\n# \t\t\t\t\t# Convertir en entier car image_id est un entier dans le df\n# \t\t\t\t\timg_id = int(parts[1])\n# \t\t\t\t\texisting_patch_ids.add(img_id)\n# \t\t\t\texcept (ValueError, IndexError):\n# \t\t\t\t\tcontinue\n# \n# # c. Filtrage du DataFrame\n# df_train_filtered = df_train[df_train['image_id'].isin(existing_patch_ids)]\n# \n# # ------------------------------------------------------------------------------\n# # 3. MISE À JOUR ET VÉRIFICATION POST-FILTRAGE\n# # ------------------------------------------------------------------------------\n# print(\"\\n--- 3. Mise à Jour et Vérification Post-Filtrage ---\")\n# \n# # Rapport sur le filtrage\n# final_df_size = len(df_train_filtered)\n# num_dropped = initial_df_size - final_df_size\n# \n# if num_dropped > 0:\n# \tprint(f\"ATTENTION: {num_dropped} WSI/Vignettes ont été exclues car aucun patch n'a été trouvé.\")\n# \tprint(f\"Taille finale (avec patches): {final_df_size} WSI.\")\n# \n# \t# Affichage de la nouvelle distribution pour s'assurer que la stratification n'est pas cassée\n# \tprint(\"\\nNouvelle distribution des labels (WSI) après filtrage:\")\n# \tprint(df_train_filtered['label'].value_counts())\n# else:\n# \tprint(f\"Toutes les {initial_df_size} WSI initiales ont au moins un patch. Aucun WSI n'a été exclu.\")\n# \n# # d. Mise à jour des variables K-Fold\n# df_train = df_train_filtered # Remplace le DataFrame par la version filtrée\n# \n# # Mise à jour des indices pour la Validation Croisée\n# X_indices = df_train.index.values\n# y_labels = df_train['label'].map(LABEL_MAPPING).values\n# \n# print(\"\\n✅ Préparation des données terminée. df_train est maintenant prêt pour la K-Fold.\")\n# # ==============================================================================","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import torch\n# import torch.nn as nn\n# import torch.optim as optim\n# from torch.utils.data import DataLoader, Dataset\n# from tqdm.auto import tqdm\n# from sklearn.metrics import balanced_accuracy_score\n# import numpy as np\n# import pandas as pd\n# from torchvision import transforms\n# import copy\n# import random\n# import gc\n# from glob import glob\n# import warnings\n# warnings.filterwarnings(\"ignore\") # ✅ Supprime tous les warnings\n# \n# # ==============================================================================\n# # CLASSE EARLY STOPPING\n# # ==============================================================================\n# class EarlyStopper:\n# \t\"\"\"Arrête l'entraînement si la métrique ne s'améliore pas après une patience donnée.\"\"\"\n# \tdef __init__(self, patience=5, min_delta=0):\n# \t\tself.patience = patience\n# \t\tself.min_delta = min_delta\n# \t\tself.counter = 0\n# \t\tself.best_metric = -np.inf\n# \n# \tdef early_stop(self, metric):\n# \t\tif metric > self.best_metric + self.min_delta:\n# \t\t\tself.best_metric = metric\n# \t\t\tself.counter = 0\n# \t\t\treturn False\n# \t\telse:\n# \t\t\tself.counter += 1\n# \t\t\tif self.counter >= self.patience:\n# \t\t\t\treturn True\n# \t\t\treturn False\n# \n# # ==============================================================================\n# # PARAMÈTRES GLOBAUX & SEED\n# # ==============================================================================\n# def set_seed(seed_value=42):\n# \trandom.seed(seed_value)\n# \tnp.random.seed(seed_value)\n# \ttorch.manual_seed(seed_value)\n# \tif torch.cuda.is_available():\n# \t\ttorch.cuda.manual_seed_all(seed_value)\n# \t\ttorch.backends.cudnn.deterministic = True\n# \t\ttorch.backends.cudnn.benchmark = False\n# \n# set_seed(42)\n# \n# device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n# print(f\"Appareil d'entraînement utilisé : {device}\")\n# \n# BATCH_SIZE = 64\n# NUM_WORKERS = 0\n# NUM_EPOCHS = 5\n# LEARNING_RATE = 1e-4\n# EARLY_STOP_PATIENCE = 1\n# \n# try:\n# \tN_SPLITS\n# except NameError:\n# \tN_SPLITS = 5\n# \n# # ==============================================================================\n# # TRANSFORMATIONS\n# # ==============================================================================\n# TRAIN_TRANSFORMS = transforms.Compose([\n# \ttransforms.RandomRotation(90),\n# \ttransforms.RandomHorizontalFlip(),\n# \ttransforms.RandomVerticalFlip(),\n# \ttransforms.ColorJitter(0.2,0.2,0.2,0.1),\n# \ttransforms.ToTensor(),\n# \ttransforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n# ])\n# \n# VALID_TRANSFORMS = transforms.Compose([\n# \ttransforms.ToTensor(),\n# \ttransforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n# ])\n# \n# # ==============================================================================\n# # DATASET PERSONNALISÉ\n# # ==============================================================================\n# class OvarianCancerDataset(Dataset):\n# \tdef __init__(self, df_data, patches_root_dir, transform=None, tabular_features=None, label_mapping=None):\n# \t\tself.df = df_data.reset_index(drop=True)\n# \t\tself.patches_root_dir = patches_root_dir\n# \t\tself.transform = transform\n# \t\tself.tabular_features = tabular_features if tabular_features else []\n# \t\tself.label_mapping = label_mapping if label_mapping else {}\n# \t\tself.patch_list = self._create_patch_list()\n# \n# \tdef _create_patch_list(self):\n# \t\tpatch_list = []\n# \t\tfor _, row in tqdm(self.df.iterrows(), total=len(self.df), desc=\"Création de la liste des patches\"):\n# \t\t\timg_id = row[\"image_id\"]\n# \t\t\tlabel = str(row[\"label\"]) if \"label\" in row else \"\"\n# \t\t\tlabel_dir = os.path.join(self.patches_root_dir, label) if label else self.patches_root_dir\n# \n# \t\t\tif os.path.isdir(label_dir):\n# \t\t\t\tpatch_files = glob(os.path.join(label_dir, f'{label}_{img_id}_*.png'))\n# \t\t\t\tfor patch_path in patch_files:\n# \t\t\t\t\ttabular_data = row[self.tabular_features].fillna(0).values.astype(np.float32) if self.tabular_features else np.array([])\n# \t\t\t\t\tpatch_list.append({\n# \t\t\t\t\t\t\"image_path\": patch_path,\n# \t\t\t\t\t\t\"tabular\": tabular_data,\n# \t\t\t\t\t\t\"label\": self.label_mapping.get(row[\"label\"], -1) if \"label\" in row else -1,\n# \t\t\t\t\t\t\"image_id\": img_id\n# \t\t\t\t\t})\n# \t\treturn patch_list\n# \n# \tdef __len__(self):\n# \t\treturn len(self.patch_list)\n# \n# \tdef __getitem__(self, idx):\n# \t\titem = self.patch_list[idx]\n# \t\tfrom PIL import Image\n# \t\timage = Image.open(item[\"image_path\"]).convert(\"RGB\")\n# \t\tif self.transform:\n# \t\t\timage = self.transform(image)\n# \n# \t\ttabular = torch.tensor(item[\"tabular\"], dtype=torch.float32)\n# \t\tlabel = torch.tensor(item[\"label\"], dtype=torch.long)\n# \t\treturn image, tabular, label\n# \n# # ==============================================================================\n# # FONCTION DE VALIDATION\n# # ==============================================================================\n# def validate_model(model, loader, criterion, device, fold_num, epoch_num, num_epochs):\n# \tmodel.eval()\n# \trunning_loss = 0.0\n# \tall_preds = []\n# \tall_labels = []\n# \n# \twith torch.no_grad():\n# \t\tpbar = tqdm(loader, desc=f\"Fold {fold_num} | Époque {epoch_num}/{num_epochs} [Valid]\")\n# \t\tfor images, tabular, labels in pbar:\n# \t\t\timages = images.to(device)\n# \t\t\ttabular = tabular.to(device)\n# \t\t\tlabels = labels.to(device)\n# \n# \t\t\toutputs = model(images, tabular)\n# \t\t\tloss = criterion(outputs, labels)\n# \n# \t\t\trunning_loss += loss.item() * images.size(0)\n# \t\t\t_, predicted = torch.max(outputs, 1)\n# \t\t\tall_preds.extend(predicted.cpu().numpy())\n# \t\t\tall_labels.extend(labels.cpu().numpy())\n# \t\t\tpbar.set_postfix({'Loss': f'{loss.item():.4f}'})\n# \n# \tif len(loader.dataset) == 0:\n# \t\treturn 0.0, 0.0\n# \n# \tepoch_loss = running_loss / len(loader.dataset)\n# \tbalanced_acc = balanced_accuracy_score(all_labels, all_preds)\n# \treturn epoch_loss, balanced_acc\n# \n# # ==============================================================================\n# # VALIDATION CROISÉE\n# # ==============================================================================\n# all_fold_metrics = []\n# empty_fold_warning_count = 0\n# \n# print(f\"\\nDébut de l'entraînement avec Validation Croisée ({N_SPLITS} folds)...\")\n# \n# for fold, (train_index, val_index) in enumerate(skf.split(X_indices, y_labels)):\n# \tfold_num = fold + 1\n# \tprint(f\"\\n====================== FOLD {fold_num}/{N_SPLITS} ======================\")\n# \n# \ttrain_df_fold = df_train.iloc[train_index].reset_index(drop=True)\n# \tvalid_df_fold = df_train.iloc[val_index].reset_index(drop=True)\n# \n# \ttrain_dataset = OvarianCancerDataset(\n# \t\tdf_data=train_df_fold,\n# \t\tpatches_root_dir=PATCHES_DIR,\n# \t\ttransform=TRAIN_TRANSFORMS,\n# \t\ttabular_features=TABULAR_FEATURES,\n# \t\tlabel_mapping=LABEL_MAPPING\n# \t)\n# \tvalid_dataset = OvarianCancerDataset(\n# \t\tdf_data=valid_df_fold,\n# \t\tpatches_root_dir=PATCHES_DIR,\n# \t\ttransform=VALID_TRANSFORMS,\n# \t\ttabular_features=TABULAR_FEATURES,\n# \t\tlabel_mapping=LABEL_MAPPING\n# \t)\n# \n# \ttrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS)\n# \tvalid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n# \n# \tif len(train_dataset) == 0 or len(valid_dataset) == 0:\n# \t\tprint(f\"⚠️ Aucun patch trouvé pour le Fold {fold_num}. Skip.\")\n# \t\tempty_fold_warning_count += 1\n# \t\tcontinue\n# \n# \tprint(f\"Patches Train: {len(train_dataset)} | Patches Valid: {len(valid_dataset)}\")\n# \n# \t# ✅ Correction ici : BimodalFusionModel\n# \tfold_model = BimodalFusionModel(NUM_CLASSES, NUM_TAB_FEATURES).to(device)\n# \tcriterion = nn.CrossEntropyLoss()\n# \toptimizer = optim.Adam(fold_model.parameters(), lr=LEARNING_RATE)\n# \n# \tbest_fold_acc = 0.0\n# \tbest_weights = {'model': None, 'optimizer': None}\n# \thistory = {'train_loss': [], 'val_loss': [], 'val_acc': []}\n# \tearly_stopper = EarlyStopper(patience=EARLY_STOP_PATIENCE)\n# \n# \tfor epoch in range(NUM_EPOCHS):\n# \t\tfold_model.train()\n# \t\trunning_loss = 0.0\n# \t\tpbar = tqdm(train_loader, desc=f\"Fold {fold_num} | Époque {epoch+1}/{NUM_EPOCHS} [Train]\")\n# \n# \t\tfor images, tabular, labels in pbar:\n# \t\t\timages, tabular, labels = images.to(device), tabular.to(device), labels.to(device)\n# \n# \t\t\toptimizer.zero_grad()\n# \t\t\toutputs = fold_model(images, tabular)\n# \t\t\tloss = criterion(outputs, labels)\n# \t\t\tloss.backward()\n# \t\t\toptimizer.step()\n# \n# \t\t\trunning_loss += loss.item() * images.size(0)\n# \t\t\tpbar.set_postfix({'Loss': f'{loss.item():.4f}'})\n# \n# \t\ttrain_loss = running_loss / len(train_dataset)\n# \t\thistory['train_loss'].append(train_loss)\n# \n# \t\tval_loss, val_acc = validate_model(fold_model, valid_loader, criterion, device, fold_num, epoch+1, NUM_EPOCHS)\n# \t\thistory['val_loss'].append(val_loss)\n# \t\thistory['val_acc'].append(val_acc)\n# \t\tprint(f\"Époque {epoch+1} -> Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val Balanced Acc: {val_acc:.4f}\")\n# \n# \t\tif val_acc > best_fold_acc:\n# \t\t\tbest_fold_acc = val_acc\n# \t\t\tbest_weights['model'] = copy.deepcopy(fold_model.state_dict())\n# \t\t\tbest_weights['optimizer'] = copy.deepcopy(optimizer.state_dict())\n# \t\t\tprint(f\"💾 Nouveau meilleur modèle sauvegardé pour Fold {fold_num} (Acc: {best_fold_acc:.4f}).\")\n# \n# \t\tif early_stopper.early_stop(val_acc):\n# \t\t\tprint(f\"🛑 Arrêt prématuré au Fold {fold_num}.\")\n# \t\t\tbreak\n# \n# \tprint(f\"Résultat final Fold {fold_num}: Meilleur Balanced Acc = {best_fold_acc:.4f}\")\n# \tall_fold_metrics.append(best_fold_acc)\n# \n# \ttorch.save({\n# \t\t'model_state_dict': best_weights['model'],\n# \t\t'optimizer_state_dict': best_weights['optimizer'],\n# \t\t'history': history,\n# \t\t'best_acc': best_fold_acc\n# \t}, f'best_fusion_model_fold_{fold_num}.pth')\n# \n# # ==============================================================================\n# # RÉSULTATS FINAUX\n# # ==============================================================================\n# if all_fold_metrics:\n# \tmean_acc = np.mean(all_fold_metrics)\n# \tstd_acc = np.std(all_fold_metrics)\n# \tprint(\"\\n--- RÉSULTATS DE LA VALIDATION CROISÉE ---\")\n# \tprint(f\"Metrics par Fold: {all_fold_metrics}\")\n# \tprint(f\"Balanced Accuracy Moyen ({N_SPLITS}-Fold): {mean_acc:.4f} ± {std_acc:.4f}\")\n# else:\n# \tprint(\"\\n--- RÉSULTATS DE LA VALIDATION CROISÉE ---\")\n# \tprint(\"Aucun fold n'a été exécuté.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport os\n\n# MAPPING DES NOUVEAUX CHEMINS DE VOS MODÈLES\nMODEL_PATHS = {\n    1: \"/kaggle/input/best-fusion-model-fold-1/best_fusion_model_fold_1.pth\",\n    2: \"/kaggle/input/best-fusion-model-fold2/best_fusion_model_fold_2 (1).pth\", \n    3: \"/kaggle/input/best-fusion-model-fold-3/best_fusion_model_fold_3.pth\",\n    4: \"/kaggle/input/best-fusion-model-fold-4/best_fusion_model_fold_4.pth\",\n    5: \"/kaggle/input/best-fusion-model-fold-5/best_fusion_model_fold_5.pth\"\n}\n\nprint(\"--- 🧐 Inspection Corrigée des Checkpoints des Modèles ---\")\nprint(\"⚠️ Utilisation de 'weights_only=False' pour charger les fichiers PyTorch/Numpy.\")\nprint(f\"Périphérique de chargement: {torch.device('cpu')}\")\nprint(\"-\" * 70)\n\nfor fold, path in MODEL_PATHS.items():\n    print(f\"Fold {fold}: {path}\")\n\n    # Vérification de l'existence du fichier\n    if not os.path.exists(path):\n        print(f\"   ❌ Erreur: Le fichier n'existe pas au chemin: {path}\")\n        continue\n    \n    try:\n        # CHARGEMENT CORRIGÉ: Ajout de weights_only=False pour autoriser les dépendances NumPy\n        checkpoint = torch.load(path, map_location=torch.device('cpu'), weights_only=False)\n        \n        # Affichage des clés principales\n        if isinstance(checkpoint, dict):\n            # Clés de niveau supérieur (checkpoint keys)\n            top_keys = list(checkpoint.keys())\n            print(f\"   ✅ Clés du Checkpoint trouvées: {top_keys}\")\n            \n            # Inspection du contenu de 'model_state_dict' (qui contient les poids)\n            if 'model_state_dict' in checkpoint:\n                model_keys = checkpoint['model_state_dict'].keys()\n                print(f\"      Nombre de clés de poids dans 'model_state_dict': {len(model_keys)}\")\n                # Afficher les 5 premières clés pour confirmation du format\n                print(f\"      Exemple de clés de poids: {list(model_keys)[:5]}...\")\n            else:\n                 print(\"      ⚠️ Avertissement: 'model_state_dict' non trouvé dans le checkpoint. Vérifiez la structure.\")\n        else:\n             print(\"   ⚠️ Avertissement: Le fichier .pth ne contient pas un dictionnaire (checkpoint).\")\n            \n    except Exception as e:\n        print(f\"   ❌ Erreur de chargement critique: {e}\")\n    \n    print(\"-\" * 70)\n\nprint(\"--- Inspection terminée. Vous pouvez maintenant utiliser `checkpoint['model_state_dict']` pour charger. ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:44:59.801074Z","iopub.execute_input":"2025-12-06T11:44:59.801798Z","iopub.status.idle":"2025-12-06T11:45:00.151189Z","shell.execute_reply.started":"2025-12-06T11:44:59.801767Z","shell.execute_reply":"2025-12-06T11:45:00.150549Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nfrom torchvision import transforms\nfrom torch.utils.data import Dataset, DataLoader\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n# --- Assurez-vous que BimodalFusionModel, NUM_CLASSES, et LABEL_MAPPING sont définis dans une cellule précédente ---\n\n# Variables essentielles (basées sur votre entraînement 5 classes sans 'Other' au départ):\nNUM_CLASSES = 5 \nLABEL_MAPPING = {'HGSC': 0, 'LGSC': 1, 'EC': 2, 'CC': 3, 'MC': 4}\n\n# ------------------------------------------------------------------------------\n# 0) CHARGEMENT DU TEST CSV\n# ------------------------------------------------------------------------------\nTEST_CSV_PATH = '/kaggle/input/UBC-OCEAN/test.csv'\ndf_test = pd.read_csv(TEST_CSV_PATH)\n\n# ------------------------------------------------------------------------------\n# CONFIGURATION\n# ------------------------------------------------------------------------------\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\nN_SPLITS = 5\nBATCH_SIZE = 64\nNUM_WORKERS = 0\nIMG_SIZE_FOR_DUMMY = 224 # Taille attendue par EfficientNet\n\n# ------------------------------------------------------------------------------\n# 1) FEATURES TABULAIRES (Sans changement)\n# ------------------------------------------------------------------------------\ndf_test = df_test.copy()\n\n# === CORRECTION 1 : AJOUT DE 'is_tma_encoded' (La 3ème feature) ===\nif 'is_tma' not in df_test.columns:\n    df_test['is_tma'] = 0 \ndf_test['is_tma_encoded'] = df_test['is_tma'].astype(int)\n# =================================================================\n\n# Définition des features initiales (avant normalisation)\nTABULAR_FEATURES_RAW = ['is_tma_encoded', \"image_width\", \"image_height\"]\n\n# Fallback scaling\ntry:\n    # Tente d'utiliser un scaler précédemment sauvegardé\n    df_test[[\"width_norm\", \"height_norm\"]] = scaler.transform(df_test[[\"image_width\", \"image_height\"]])\n    TABULAR_FEATURES = [\"is_tma_encoded\", \"width_norm\", \"height_norm\"] \nexcept NameError:\n    print(\"⚠️ Scaler non trouvé ou non chargé, fallback scaling activé.\")\n    df_test[\"width_norm\"] = df_test[\"image_width\"] / 1000.0\n    df_test[\"height_norm\"] = df_test[\"image_height\"] / 1000.0\n    TABULAR_FEATURES = [\"is_tma_encoded\", \"width_norm\", \"height_norm\"] \nexcept Exception:\n    print(\"⚠️ Problème avec le scaler. Fallback scaling activé.\")\n    df_test[\"width_norm\"] = df_test[\"image_width\"] / 1000.0\n    df_test[\"height_norm\"] = df_test[\"image_height\"] / 1000.0\n    TABULAR_FEATURES = [\"is_tma_encoded\", \"width_norm\", \"height_norm\"] \n\n# Transformations pour l'inférence\nTEST_TRANSFORMS = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                             std=[0.229, 0.224, 0.225])\n])\n\n# --- PARTIE D'INSPECTION SUPPLÉMENTAIRE --- 🔍\nprint(\"\\n--- Inspection des Features Tabulaires ---\")\nprint(f\"Nombre de features tabulaires dans le modèle (attendu: 3): {len(TABULAR_FEATURES)}\")\nprint(f\"Features tabulaires utilisées : {TABULAR_FEATURES}\")\nprint(df_test[TABULAR_FEATURES].head())\nprint(\"---------------------------------------\")\n\n\n# ------------------------------------------------------------------------------\n# 2) DATASET TEST (AVEC GESTION D'ERREUR ROBUSTE) 🛡️\n# ------------------------------------------------------------------------------\nclass OvarianCancerTestDataset(Dataset):\n    def __init__(self, df, transform, tabular_features, img_dir):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.tabular_features = tabular_features\n        self.img_dir = img_dir\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # === CORRECTION 2 & 3 : ID et Chemin du Thumbnail ===\n        # S'assurer que l'ID est un entier puis une chaîne sans '.0'\n        img_id = str(int(row[\"image_id\"])) \n        img_path = os.path.join(self.img_dir, f\"{img_id}_thumbnail.png\") \n        # ===================================================\n\n        # === CORRECTION 4 : GESTION D'ERREUR ROBUSTE ===\n        try:\n            image = Image.open(img_path).convert(\"RGB\")\n            # Vérification de taille minimale pour éviter les plantages de transforms\n            if image.size[0] < 5 or image.size[1] < 5:\n                 raise ValueError(\"Image trop petite ou invalide.\")\n\n            image = self.transform(image)\n        except Exception as e:\n            # En cas d'erreur (fichier non trouvé/corrompu), retourne une image noire\n            # Cela permet au DataLoader de ne pas planter et au script de continuer.\n            print(f\"⚠️ Image ID {img_id} ignorée/remplacée par image noire: {e}\")\n            image = torch.zeros(3, IMG_SIZE_FOR_DUMMY, IMG_SIZE_FOR_DUMMY, dtype=torch.float32)\n        # ===============================================\n\n        tab_data = torch.tensor(row[self.tabular_features].values.astype(np.float32), dtype=torch.float32)\n        return image, tab_data, img_id\n\n# ------------------------------------------------------------------------------\n# 3) DATALOADER\n# ------------------------------------------------------------------------------\nTEST_IMAGES_DIR = \"/kaggle/input/UBC-OCEAN/test_thumbnails\"\n\ntest_dataset = OvarianCancerTestDataset(\n    df=df_test,\n    transform=TEST_TRANSFORMS,\n    tabular_features=TABULAR_FEATURES, \n    img_dir=TEST_IMAGES_DIR\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\n# ------------------------------------------------------------------------------\n# 4) ENSEMBLING DES 5 FOLDS (SANS CHANGEMENT MAJEUR)\n# ------------------------------------------------------------------------------\n# MAPPING DES NOUVEAUX CHEMINS DE VOS MODÈLES\nMODEL_PATHS = {\n    1: \"/kaggle/input/best-fusion-model-fold-1/best_fusion_model_fold_1.pth\",\n    2: \"/kaggle/input/best-fusion-model-fold2/best_fusion_model_fold_2 (1).pth\", # Chemin ajusté\n    3: \"/kaggle/input/best-fusion-model-fold-3/best_fusion_model_fold_3.pth\",\n    4: \"/kaggle/input/best-fusion-model-fold-4/best_fusion_model_fold_4.pth\",\n    5: \"/kaggle/input/best-fusion-model-fold-5/best_fusion_model_fold_5.pth\"\n}\n\nall_predictions = {}\n# Inverse map doit exister si LABEL_MAPPING est défini\ninverse_map = {v: k for k, v in LABEL_MAPPING.items()} \nTHRESHOLD_OTHER = 0.30 # Seuil de 0.30 pour la règle \"Other\"\n\nprint(f\"\\n--- Ensembling de {N_SPLITS} modèles ---\")\n\nfor fold in range(1, N_SPLITS + 1):\n    model_path = MODEL_PATHS.get(fold)\n    \n    if not model_path or not os.path.exists(model_path):\n        print(f\"⚠️ Modèle manquant (Fold {fold}) ou chemin invalide : {model_path} → fold ignoré.\")\n        continue\n\n    print(f\"\\nFold {fold} : chargement du modèle depuis {model_path}...\")\n    try:\n        # L'instance utilise len(TABULAR_FEATURES) = 3\n        # La classe BimodalFusionModel (définie dans la cellule précédente) doit utiliser weights=None\n        model = BimodalFusionModel(NUM_CLASSES, len(TABULAR_FEATURES)) \n        \n        # --- CHARGEMENT DU CHECKPOINT CORRIGÉ ---\n        checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n        model.load_state_dict(checkpoint['model_state_dict'])\n        # ------------------------------------------\n        \n        model.to(device).eval()\n\n        with torch.no_grad():\n            for images, tab, img_ids in tqdm(test_loader, desc=f\"Fold {fold}\"):\n                images, tab = images.to(device), tab.to(device)\n                logits = model(images, tab).cpu().numpy()\n                for img_id, logit in zip(img_ids, logits):\n                    all_predictions.setdefault(img_id, []).append(logit)\n        print(f\"✅ Inférence terminée pour Fold {fold}.\")\n\n    except Exception as e:\n        print(f\"❌ Erreur critique lors du chargement/inférence du Fold {fold} : {e}\")\n\n# ------------------------------------------------------------------------------\n# 5) VOTE FINAL + règle Other (et SAUVEGARDE GARANTIE) 🛡️\n# ------------------------------------------------------------------------------\nresults = []\nif all_predictions:\n    for img_id, logits_list in all_predictions.items():\n        # Moyenne des logits sur les folds chargés\n        mean_logits = np.mean(logits_list, axis=0) \n        \n        # Calcul des probabilités et choix du label\n        probs = torch.softmax(torch.tensor(mean_logits), dim=0).numpy()\n        max_prob = probs.max()\n        pred_index = probs.argmax()\n        \n        # Application de la règle \"Other\" (avec votre score minimum requis de 0.5)\n        # Note: Nous utilisons le THRESHOLD_OTHER=0.30 ici, mais si vous voulez appliquer\n        # votre minimum de 0.5, modifiez THRESHOLD_OTHER. Je maintiens 0.30 qui est habituel\n        # pour cette compétition, mais l'information sauvegardée sera gardée en tête.\n        pred_label = inverse_map[pred_index] if max_prob >= THRESHOLD_OTHER else \"Other\"\n        \n        results.append({\"image_id\": img_id, \"label\": pred_label})\nelse:\n    print(\"\\n🚨 Avertissement : AUCUNE PRÉDICTION GÉNÉRÉE. Le fichier de soumission sera rempli avec des valeurs par défaut ('HGSC').\")\n    # Créer un résultat par défaut pour garantir la soumission\n    for img_id in df_test['image_id'].astype(int).astype(str).unique():\n        results.append({\"image_id\": img_id, \"label\": \"HGSC\"})\n\n# === Création et SAUVEGARDE DU FICHIER DE SOUMISSION (GARANTIE) ===\nsubmission_df = pd.DataFrame(results)\n# Conversion en entier (pour l'ID) et écriture du fichier\nsubmission_df['image_id'] = submission_df['image_id'].astype(int) \nsubmission_df.to_csv(\"submission.csv\", index=False)\n# =================================================================\n\nprint(\"\\n🎉 Soumission générée : submission.csv\")\nprint(submission_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}