{"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":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13682568,"sourceType":"datasetVersion","datasetId":8701170},{"sourceId":13691353,"sourceType":"datasetVersion","datasetId":8708131},{"sourceId":13699131,"sourceType":"datasetVersion","datasetId":8714068}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport os\nfrom sklearn.model_selection import KFold\nimport torch.optim as optim\nimport timm\nimport math\n\n# --- Configuration des chemins et constantes ---\nDATA_ROOT = '/kaggle/input/csiro-biomass'\nTRAIN_IMG_DIR = os.path.join(DATA_ROOT, 'train')\nTEST_IMG_DIR = os.path.join(DATA_ROOT, 'test')\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# --- STRATÉGIE OPTIMISÉE : 3 CIBLES INDÉPENDANTES ---\n# Noms des 3 cibles de biomasse qui doivent être prédites\nTARGET_NAMES = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g'] \n\n# Poids pour la Weighted MSE Loss (Réparti uniformément sur les 3 cibles d'entraînement)\n# (0.33 + 0.33 + 0.34 = 1.0)\nTARGET_WEIGHTS = torch.tensor([0.33, 0.33, 0.34]).float() \n\n# 🎯 PARAMÈTRES OPTIMISÉS pour l'approche Image-Only :\nIMAGE_SIZE = 300\nNUM_FOLDS = 5\nBATCH_SIZE = 32\nLEARNING_RATE = 2e-4\nNUM_EPOCHS = 25\n\nprint(f\"✅ Cellule 1 corrigée : Stratégie 3 cibles adoptée. TARGET_NAMES: {TARGET_NAMES}\")\nprint(f\"Configuration chargée. DEVICE: {DEVICE}, IMAGE_SIZE: {IMAGE_SIZE}, LR: {LEARNING_RATE}\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:52:12.028657Z","iopub.execute_input":"2025-11-13T08:52:12.029436Z","iopub.status.idle":"2025-11-13T08:52:12.036761Z","shell.execute_reply.started":"2025-11-13T08:52:12.029409Z","shell.execute_reply":"2025-11-13T08:52:12.035977Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 💡 Note Stratégique sur les Cibles (Target Optimization)\n\nMême si le challenge spécifie 5 cibles (`TARGET_NAMES`), il est crucial de noter les relations mathématiques suivantes, que nous allons exploiter :\n\n1.  Total dry biomass = Dry green vegetation + Dry dead material+ Dry clover biomass = GDM + Dry clover biomass\n2.  Green dry matter (GDM) = Dry green vegetation + Dry clover biomass\n\n**Conséquence pour le Stacking :**\n\n* **Entraînement des CNN (en cours) :** Nous continuons à entraîner les CNN sur les **5 cibles** (Apprentissage Multi-Tâche) pour garantir une extraction de *features* plus riche et plus stable.\n* **Stratégie Finale :** À l'étape du **LightGBM/Stacking**, nous n'entraînerons le modèle que sur les **3 cibles indépendantes** Dry green vegetation + Dry dead material+ Dry clover biomass, puis nous **calculerons** les deux autres pour garantir une cohérence mathématique parfaite dans la soumission finale.","metadata":{}},{"cell_type":"code","source":"# Cellule 2 (CORRIGÉE : Prépare les DataFrames et le Dataset)\n\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport os\nfrom sklearn.model_selection import KFold # Utile pour initialiser kf dans une cellule suivante\n\n# Note: Les variables DATA_ROOT, TRAIN_IMG_DIR, TEST_IMG_DIR, IMAGE_SIZE et TARGET_NAMES \n# sont supposées avoir été définies correctement en Cellule 1.\n\n# --- 0. Chargement initial des DataFrames (CORRECTION de la NameError) ---\ntry:\n    train_df_raw = pd.read_csv(os.path.join(DATA_ROOT, 'train.csv'))\n    test_df_raw = pd.read_csv(os.path.join(DATA_ROOT, 'test.csv'))\n    print(f\"✅ Fichiers train.csv et test.csv chargés. (Lignes Train: {len(train_df_raw)})\")\nexcept NameError as e:\n    print(f\"ERREUR: Une variable globale (comme DATA_ROOT) n'est pas définie. {e}\")\n    raise\n\n# 1. Fonction prepare_df (CORRIGÉE pour le chemin d'image)\ndef prepare_df(df_raw, img_dir):\n    \"\"\"Calcule l'ImageID, le chemin complet de l'image (sans redondance).\"\"\"\n    df_raw['ImageID'] = df_raw['sample_id'].apply(lambda x: x.split('__')[0])\n    \n    # 🛑 CORRECTION ICI : Extraction du nom du fichier uniquement\n    df_raw['image_filename'] = df_raw['image_path'].apply(lambda x: x.split('/')[-1])\n    \n    # Rejoint le dossier de base avec le nom du fichier\n    df_raw['full_image_path'] = df_raw['image_filename'].apply(lambda x: os.path.join(img_dir, x))\n    \n    return df_raw\n\ntrain_df_raw = prepare_df(train_df_raw, TRAIN_IMG_DIR)\ntest_df_raw = prepare_df(test_df_raw, TEST_IMG_DIR)\n\n# 2. Pivot et fusion\nMETADATA_COLS = ['ImageID', 'full_image_path']\ntrain_df_base = train_df_raw[METADATA_COLS].drop_duplicates(subset=['ImageID']).reset_index(drop=True)\ntrain_df_targets = train_df_raw.pivot(index='ImageID', columns='target_name', values='target').reset_index()\ntrain_df = train_df_base.merge(train_df_targets, on='ImageID', how='left')\ntrain_df = train_df[['ImageID', 'full_image_path'] + TARGET_NAMES]\n\ntest_df = test_df_raw[METADATA_COLS].drop_duplicates(subset=['ImageID']).reset_index(drop=True)\n\nY_train_raw = train_df[TARGET_NAMES].values\n\n# 3. Dataset\nclass BiomassDataset(Dataset):\n    def __init__(self, df, targets, transform=None):\n        self.df = df\n        self.paths = df['full_image_path'].values\n        self.targets = targets\n        self.transform = transform\n        if targets is not None:\n            # S'assurer que les colonnes sont des nombres flottants avant de créer le tensor\n            self.labels = torch.tensor(df[targets].values, dtype=torch.float32)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        # La correction ci-dessus garantit que self.paths[idx] est valide\n        image = Image.open(self.paths[idx]).convert(\"RGB\") \n        if self.transform:\n            image = self.transform(image)\n        if self.targets is not None:\n            return image, self.labels[idx]\n        else:\n            return image\n\n# 4. Transformations\nNORM_MEAN = [0.485, 0.456, 0.406]\nNORM_STD = [0.229, 0.224, 0.225]\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ToTensor(),\n    transforms.Normalize(NORM_MEAN, NORM_STD)\n])\n\nvalid_test_transform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(NORM_MEAN, NORM_STD)\n])\n\nprint(\"✅ Cellule 2 corrigée : DataFrames, Dataset et Transformations prêts.\")\n\n# --- INSPECTION CRITIQUE (AJOUT) ---\nprint(\"\\n--- 🧐 Inspection du train_df après pivotage (Cellule 2) ---\")\nprint(f\"Colonnes actuelles : {train_df.columns.tolist()}\")\nprint(f\"Les colonnes cibles ({TARGET_NAMES}) sont-elles présentes ? {'Dry_Green_g' in train_df.columns and 'Dry_Dead_g' in train_df.columns}\")\nprint(\"Aperçu des 5 premières lignes (ImageID, Cibles) :\")\nprint(train_df[['ImageID'] + TARGET_NAMES].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:52:16.199764Z","iopub.execute_input":"2025-11-13T08:52:16.200333Z","iopub.status.idle":"2025-11-13T08:52:16.241166Z","shell.execute_reply.started":"2025-11-13T08:52:16.200311Z","shell.execute_reply":"2025-11-13T08:52:16.240447Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"La taille du DataFrame d'entraînement (train_df) est: {train_df.shape}\")\nprint(f\"La taille du DataFrame de test (test_df) est: {test_df.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:52:21.116144Z","iopub.execute_input":"2025-11-13T08:52:21.116407Z","iopub.status.idle":"2025-11-13T08:52:21.121138Z","shell.execute_reply.started":"2025-11-13T08:52:21.116385Z","shell.execute_reply":"2025-11-13T08:52:21.120183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cellule 3 (SIMPLIFIÉE) : Initialisation du DataLoader de Test\n\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import KFold\n# Note : Les variables globales (NUM_FOLDS, BATCH_SIZE) sont définies en Cellule 1.\n# L'initialisation du K-Fold est faite dans la cellule de Nettoyage finale (GroupKFold).\n\n# --- A. DataLoader de Test ---\n\n# 1. DataLoader pour le jeu de Test\n# targets=None est crucial car le jeu de test n'a pas de labels (cibles)\ntest_dataset = BiomassDataset(\n    df=test_df, \n    targets=None, # 🎯 ESSENTIEL : Pas de cibles pour le jeu de test\n    transform=valid_test_transform, \n)\n\ntest_loader = DataLoader(\n    test_dataset, \n    batch_size=BATCH_SIZE, \n    shuffle=False, \n    num_workers=2, \n    pin_memory=True\n)\nprint(f\"✅ DataLoader de Test créé (Taille: {len(test_dataset)}).\")\n# NOTE: Le GroupKFold (kf) est défini dans la Cellule de Nettoyage finale pour plus de sécurité.","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:52:28.598979Z","iopub.execute_input":"2025-11-13T08:52:28.599507Z","iopub.status.idle":"2025-11-13T08:52:28.605584Z","shell.execute_reply.started":"2025-11-13T08:52:28.599483Z","shell.execute_reply":"2025-11-13T08:52:28.604609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cellule 4 (FINALE) : Feature Engineering - Calcul du Pourcentage de Vert et Sec & SCALING\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.preprocessing import StandardScaler # NOUVEL IMPORT CRITIQUE\nfrom PIL import Image # Utilisation de PIL au lieu de cv2 pour la compatibilité générale\n\n# --- A. Fonctions d'Extraction (Utilise l'indice RG-NDVI) ---\n\n# Remplacement de l'approche cv2 par numpy/PIL pour la cohérence du notebook\ndef get_biomass_features(img_path):\n    \"\"\"\n    Calcule le pourcentage de pixels 'verts' (RG-NDVI > 0.1) et 'secs/jaunes' (heuristique).\n    \"\"\"\n    try:\n        # Charger l'image avec PIL et convertir en tableau numpy\n        img = np.array(Image.open(img_path).convert(\"RGB\"))\n        \n        img_float = img.astype(np.float32)\n        \n        R = img_float[:, :, 0]\n        G = img_float[:, :, 1]\n        \n        # 1. Calcul de l'indice RG-NDVI simplifié \n        epsilon = 1e-6\n        rg_ndv = (G - R) / (G + R + epsilon)\n        \n        # 2. Seuil de Classification\n        # Masque Vert : Indice positif significatif\n        mask_green_bool = rg_ndv > 0.1 \n\n        # Masque Sec : Faible NDVI + canal Rouge visible\n        mask_dry_bool = (rg_ndv < 0.05) & (rg_ndv > -0.1) & (R > 50)\n        \n        # 3. Calcul des pourcentages\n        total_pixels = img.shape[0] * img.shape[1]\n        \n        pct_green_raw = (np.sum(mask_green_bool) / total_pixels) * 100\n        pct_dry_raw = (np.sum(mask_dry_bool) / total_pixels) * 100\n        \n        return pd.Series([pct_green_raw, pct_dry_raw], index=['Pct_Green_Raw', 'Pct_Dry_Raw'])\n\n    except Exception:\n        return pd.Series([np.nan, np.nan], index=['Pct_Green_Raw', 'Pct_Dry_Raw'])\n\nprint(\"✅ Fonction de calcul de % Vert / % Sec (RG-NDVI) définie.\")\n\n\n# --- B. Application du Calcul et SCALING (Normalisation) des Features ---\n\nprint(\"\\nCalcul des pourcentages de biomasse pour le set d'entraînement et de test...\")\n\n# 1. Calcul des features brutes (Raw)\ntrain_df[['Pct_Green_Raw', 'Pct_Dry_Raw']] = train_df['full_image_path'].apply(get_biomass_features)\ntest_df[['Pct_Green_Raw', 'Pct_Dry_Raw']] = test_df['full_image_path'].apply(get_biomass_features)\n\n# 2. Imputation des NaN sur les features BRUTES (avant Scaling)\nfor df in [train_df, test_df]:\n    mean_pct_green_raw = train_df['Pct_Green_Raw'].mean()\n    mean_pct_dry_raw = train_df['Pct_Dry_Raw'].mean()\n    df['Pct_Green_Raw'] = df['Pct_Green_Raw'].fillna(mean_pct_green_raw)\n    df['Pct_Dry_Raw'] = df['Pct_Dry_Raw'].fillna(mean_pct_dry_raw)\n\n\n# --- SCALING (Normalisation Z-score) ---\nSCALER_FEATURES = ['Pct_Green_Raw', 'Pct_Dry_Raw']\nscaler = StandardScaler()\n\n# 3. Entraîner le scaler sur le TRAIN et appliquer au TRAIN/TEST\ntrain_scaled = scaler.fit_transform(train_df[SCALER_FEATURES])\ntest_scaled = scaler.transform(test_df[SCALER_FEATURES])\n\n# Création des nouvelles colonnes SCALÉES pour le Stacking\ntrain_df['Pct_Green_Scaled'] = train_scaled[:, 0]\ntrain_df['Pct_Dry_Scaled'] = train_scaled[:, 1]\ntest_df['Pct_Green_Scaled'] = test_scaled[:, 0]\ntest_df['Pct_Dry_Scaled'] = test_scaled[:, 1]\n\n\nprint(f\"✅ Colonnes 'Pct_Green_Scaled' et 'Pct_Dry_Scaled' ajoutées et normalisées (Z-score).\")\nprint(\"\\nAperçu des features normalisées (Train) :\")\nprint(train_df[['Pct_Green_Scaled', 'Pct_Dry_Scaled'] + TARGET_NAMES].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:52:49.384311Z","iopub.execute_input":"2025-11-13T08:52:49.384931Z","iopub.status.idle":"2025-11-13T08:53:13.667839Z","shell.execute_reply.started":"2025-11-13T08:52:49.384908Z","shell.execute_reply":"2025-11-13T08:53:13.667078Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cellule 5 (VERSION FINALE AVEC SMOOTHL1LOSS) : Définition de l'Architecture et de la Loss (3 Cibles)\n\nimport torch\nimport torch.nn as nn\nimport timm\nimport torch.optim as optim\nimport math\n\n# --- A. Définition des VARIABLES GLOBALES CRITIQUES ---\n\nif 'DEVICE' not in globals():\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nif 'LEARNING_RATE' not in globals(): \n    LEARNING_RATE = 2e-4 \n\n# Variables locales (cohérence avec la stratégie 3 cibles)\nNUM_TARGETS = 3 \n# Poids de la Loss ajustés pour 3 cibles (doit sommer à 1 pour l'équilibre)\nTARGET_WEIGHTS_3_CIBLES = torch.tensor([0.33, 0.33, 0.34]).float() \n\n# --- FIN A. ---\n\n\n# --- B. Définition de la Loss Function OPTIMALE (Weighted SmoothL1 Loss / Huber Loss) ---\n\nclass WeightedSmoothL1Loss(nn.Module):\n    \"\"\"\n    Loss SmoothL1 (Huber) pondérée par cible.\n    C'est une alternative très stable à la LogCosh pour la régression.\n    \"\"\"\n    def __init__(self, target_weights, beta=1.0): \n        super().__init__()\n        if len(target_weights) != NUM_TARGETS:\n            raise ValueError(f\"Les poids doivent être de taille {NUM_TARGETS} pour la stratégie des 3 cibles.\")\n        self.weights = target_weights.to(DEVICE)\n        # nn.SmoothL1Loss avec reduction='none' permet de calculer la Loss par élément\n        self.smooth_l1 = nn.SmoothL1Loss(reduction='none', beta=beta) \n        \n    def forward(self, pred, target):\n        \n        # SmoothL1(reduction='none') retourne une matrice (batch_size, num_targets)\n        loss_matrix = self.smooth_l1(pred, target)\n        \n        # Application des poids par cible (par colonne)\n        weighted_loss = loss_matrix * self.weights\n        \n        # Moyenne finale\n        total_loss = torch.mean(weighted_loss)\n        return total_loss\n# -------------------------------------------------------------------------\n\n# --- C. Définition du Modèle CNN (TÊTE COMPLEXIFIÉE) ---\n# Le code de cette classe reste inchangé car l'architecture est bonne.\n\nclass CNNBiomassModel(nn.Module):\n    \"\"\"\n    Classe de Modèle modulaire : le chemin de poids est passé à l'initialisation.\n    \"\"\"\n    def __init__(self, num_targets, model_name, pretrained_path=None): \n        super().__init__()\n        \n        # 🛑 CORRECTION MAJEURE : pretrained=False pour désactiver la recherche/le téléchargement en ligne\n        self.cnn_model = timm.create_model(model_name, pretrained=False, num_classes=0) \n        \n        # --- Gestion des poids locaux (Chargement des poids .pth) ---\n        if pretrained_path:\n            try:\n                state_dict = torch.load(pretrained_path, map_location=DEVICE)\n                # strict=False permet de charger des poids d'un modèle A sur un modèle B s'ils partagent des couches\n                self.cnn_model.load_state_dict(state_dict, strict=False)\n                print(f\"Poids du backbone chargés depuis {pretrained_path} pour {model_name}.\")\n            except Exception as e:\n                print(f\"Avertissement : Impossible de charger les poids locaux ({pretrained_path}). {e}\")\n                \n        # 3. Détermination de la taille du vecteur d'entrée\n        cnn_output_dim = self.cnn_model.num_features\n        \n        # --- Tête de Prédiction (MLP) ---\n        self.prediction_head = nn.Sequential(\n            nn.Linear(cnn_output_dim, 512), \n            nn.BatchNorm1d(512), \n            nn.ReLU(),\n            nn.Dropout(0.4), \n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_targets) \n        )\n        \n    def forward(self, images):\n        features = self.cnn_model(images)\n        output = self.prediction_head(features)\n        return output\n\n# --- D. Initialisation de la Loss pour la Cellule 6 ---\n\n# La variable 'loss_fn' est la seule variable globale nécessaire pour la cellule 6\nloss_fn = WeightedSmoothL1Loss(target_weights=TARGET_WEIGHTS_3_CIBLES, beta=1.0)\n\nprint(f\"✅ Classe CNNBiomassModel (flexible, 3 sorties) et WeightedSmoothL1Loss (Huber) définies.\")\nprint(f\"Poids de la Loss utilisés pour l'entraînement (3 cibles) : {TARGET_WEIGHTS_3_CIBLES.cpu().numpy()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:53:21.466855Z","iopub.execute_input":"2025-11-13T08:53:21.467444Z","iopub.status.idle":"2025-11-13T08:53:21.478845Z","shell.execute_reply.started":"2025-11-13T08:53:21.467422Z","shell.execute_reply":"2025-11-13T08:53:21.478062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- NOUVELLE CELLULE (FINALE SÉCURISÉE) : Intégration des Méta-données & Nettoyage ---\n\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import GroupKFold \n\nPATH_TRAIN_CSV = '/kaggle/input/csiro-biomass/train.csv'\n\n# S'assurer que les variables globales train_df, kf, GROUP_IDS sont utilisées\nglobal train_df, kf, GROUP_IDS\n# On assume que TARGET_NAMES est défini (Cellule 1) et que les features Scaled existent (Cellule 4)\n\n# --- 1. Intégration des Méta-données et Group_ID ---\nprint(\"--- 1. Intégration des Méta-données et Group_ID ---\")\n\n# 1. Chargement des Méta-données brutes pour la jointure\nmeta_df = pd.read_csv(PATH_TRAIN_CSV)\nmeta_df['ImageID'] = meta_df['sample_id'].apply(lambda x: x.split('__')[0])\n\n# 2. Création de Group_ID\nmeta_df = meta_df[['ImageID', 'State', 'Sampling_Date']].drop_duplicates(subset=['ImageID'])\nmeta_df['Sampling_Date'] = pd.to_datetime(meta_df['Sampling_Date'])\nmeta_df['Group_ID'] = meta_df['State'].astype(str) + '_' + meta_df['Sampling_Date'].dt.strftime('%Y-%m-%d')\n\n# 3. Jointure avec le DataFrame principal (train_df vient de la Cellule 2/4)\ntrain_df = train_df.merge(meta_df[['ImageID', 'State', 'Sampling_Date', 'Group_ID']], \n                          on='ImageID', \n                          how='left')\n\nprint(\"✅ Group_ID, State et Sampling_Date fusionnés dans 'train_df'.\")\n\n# 🛑 NOUVELLE VÉRIFICATION DE SÉCURITÉ IMMÉDIATE 🛑\nif 'Group_ID' not in train_df.columns:\n    print(\"❌ FATAL: 'Group_ID' MANQUANT APRÈS JOINTURE. Le DataFrame a été écrasé ou la fusion a échoué.\")\n    raise KeyError(\"Group_ID est manquant immédiatement après sa création.\")\nelse:\n    print(\"✅ VÉRIF. IMMÉDIATE: 'Group_ID' est présent dans les colonnes.\")\n\n# --- 2. Contrôle Qualité Stricte (Anti-NaN) et Préparation K-Fold ---\n\nprint(\"\\n--- 2. Contrôle Qualité Stricte et Préparation K-Fold ---\")\n\n# Les colonnes critiques CORRIGÉES\nCRITICAL_COLS_K_FOLD = TARGET_NAMES + ['Group_ID', 'ImageID', 'Pct_Green_Scaled', 'Pct_Dry_Scaled']\n\nprint(f\"Colonnes critiques vérifiées : {CRITICAL_COLS_K_FOLD}\")\n\n# 1. Vérification et Nettoyage des NaN\nnan_check = train_df[CRITICAL_COLS_K_FOLD].isnull().any(axis=1)\nnan_rows_count = nan_check.sum()\n\nif nan_rows_count > 0:\n    print(f\"⚠️ Alerte : {nan_rows_count} lignes (sur {len(train_df)}) contiennent des NaN.\")\n    \n    initial_len = len(train_df)\n    train_df = train_df.dropna(subset=CRITICAL_COLS_K_FOLD).reset_index(drop=True)\n    \n    print(f\"\\n✅ Nettoyage effectué : {initial_len - len(train_df)} lignes supprimées. Nouvelle taille : {len(train_df)}.\")\n    \nelse:\n    print(\"✅ Le train_df est propre. Aucune valeur NaN critique trouvée.\")\n\n# 2. Finalisation des Variables Globales pour K-Fold\nNUM_FOLDS = 5 \nGROUP_IDS = train_df['Group_ID'].values\nkf = GroupKFold(n_splits=NUM_FOLDS) \n\nprint(f\"✅ GROUP_IDS ({GROUP_IDS.shape}) et GroupKFold ({NUM_FOLDS} plis) prêts.\")\nprint(\"\\n--- Fin de la Préparation. train_df est prêt pour la Cellule 6 ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:53:27.546303Z","iopub.execute_input":"2025-11-13T08:53:27.546927Z","iopub.status.idle":"2025-11-13T08:53:27.570631Z","shell.execute_reply.started":"2025-11-13T08:53:27.546904Z","shell.execute_reply":"2025-11-13T08:53:27.569875Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cellule 6 (VERSION FINALE CORRIGÉE & SÉCURISÉE - SMOOTHL1) : Boucle d'Entraînement GroupKFold pour le Stacking\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import DataLoader, Subset\nfrom sklearn.metrics import mean_squared_error, r2_score\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\nimport numpy as np\nimport time\nimport timm\n\n# Assurez-vous que les classes et variables globales suivantes sont définies (Cellules 1-5) :\n# CNNBiomassModel, WeightedSmoothL1Loss, BiomassDataset, train_df, kf (GroupKFold), TARGET_NAMES,\n# train_transform, valid_test_transform, DEVICE, BATCH_SIZE, TARGET_WEIGHTS_3_CIBLES, test_loader, GROUP_IDS\n\n# --- Hyperparamètres d'Entraînement ALIGNÉS ---\nEPOCHS = 25\nLEARNING_RATE_DEFAULT = 2e-4\n\n# --- CONFIGURATION DES DEUX MODÈLES (Utilisation des chemins fournis) ---\n# NOTE: Vérifiez ces chemins localement s'ils sont dans un dataset Kaggle\nBACKBONES_CONFIG = {\n    'resnext50_32x4d': {\n        'timm_name': 'resnext50_32x4d',\n        'path': '/kaggle/input/resnext50-weight/resnext50_32x4d-7cdf4587.pth',\n        'epochs': EPOCHS,\n        'lr': LEARNING_RATE_DEFAULT\n    },\n    'resnet18': {\n        'timm_name': 'resnet18',\n        'path': '/kaggle/input/restnet-weight/resnet18-f37072fd.pth',\n        'epochs': EPOCHS,\n        'lr': 1e-4 \n    }\n}\n\n# --- Initialisation des structures de Stacking (CRITIQUE pour Cellule 7) ---\nALL_OOF_PREDS = {}\nALL_TEST_PREDS = {}\n\n# --- 1. Définition des Fonctions d'Utilité (AVEC CORRECTION ANTI-NAN) ---\n\ndef calculate_rmse(y_true, y_pred):\n    \"\"\"\n    Calcule la Root Mean Squared Error (RMSE) pour l'ensemble des cibles,\n    en nettoyant les NaN/Inf des prédictions pour éviter les ValueError de sklearn.\n    \"\"\"\n    # 1. Détacher et convertir en NumPy\n    if torch.is_tensor(y_true):\n        y_true = y_true.detach().cpu().numpy()\n        y_pred = y_pred.detach().cpu().numpy()\n        \n    # 2. 🛑 CORRECTION : Remplacer les NaN ou Inf par 0 avant sklearn 🛑\n    # Ceci empêche le \"ValueError: Input contains NaN\"\n    y_pred = np.nan_to_num(y_pred, nan=0.0, posinf=0.0, neginf=0.0)\n    \n    # 3. Calcul de la RMSE\n    rms = np.sqrt(mean_squared_error(y_true, y_pred))\n    return rms\n\ntry:\n    # On utilise la variable loss_fn définie en Cellule 5 (MAINTENANT SMOOTHL1)\n    # On s'assure que le nom de la Loss est correct dans l'affichage\n    loss_name = 'WeightedSmoothL1Loss' \n    print(f\"✅ Loss Function définie : {loss_name}.\")\nexcept NameError as e:\n    print(f\"❌ ERREUR: La Loss Function n'est pas définie. {e}\")\n    raise # Arrête l'exécution si la Loss est manquante\n\n# --- 2. Boucle Principale sur les Backbones ---\n\nNUM_FOLDS = kf.n_splits \n\n# DataFrame complet pour les cibles réelles (utile pour les métriques)\nTRUE_TARGETS_ALL = train_df[TARGET_NAMES].values\n\nfor model_key, config in BACKBONES_CONFIG.items():\n    \n    MODEL_NAME_TAG = model_key\n    LOCAL_PRETRAINED_PATH = config['path']\n    N_EPOCHS = config['epochs']\n    MODEL_LR = config['lr']\n    \n    print(f\"\\n{'='*20} DÉBUT DU BACKBONE : {MODEL_NAME_TAG} (LR: {MODEL_LR}) {'='*20}\")\n    \n    # Initialisation des résultats OOF spécifiques à ce modèle\n    OOF_PREDS = np.zeros((len(train_df), len(TARGET_NAMES)))\n    \n    # Boucle sur les plis K-Fold\n    # Utilisation de train_df.index comme indices de base pour éviter les problèmes de reset_index\n    for fold, (train_idx, val_idx) in enumerate(kf.split(train_df.index, groups=GROUP_IDS)):\n        print(f\"\\n--- PLI {fold+1}/{NUM_FOLDS} ({MODEL_NAME_TAG}) ---\")\n        start_time = time.time()\n        \n        # a. Préparation des DataLoaders\n        full_dataset = BiomassDataset(df=train_df, targets=TARGET_NAMES, transform=None) \n        \n        train_subset = Subset(full_dataset, train_idx)\n        val_subset = Subset(full_dataset, val_idx)\n        \n        # Application des transformations APRÈS la création des subsets\n        train_subset.dataset.transform = train_transform\n        val_subset.dataset.transform = valid_test_transform\n        \n        train_loader = DataLoader(train_subset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\n        val_loader = DataLoader(val_subset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\n        # b. Initialisation du Modèle et de l'Optimiseur\n        model = CNNBiomassModel(\n            num_targets=len(TARGET_NAMES), \n            model_name=config['timm_name'], \n            pretrained_path=LOCAL_PRETRAINED_PATH \n        ).to(DEVICE)\n        \n        optimizer = optim.AdamW(model.parameters(), lr=MODEL_LR, weight_decay=1e-5) \n        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=5, T_mult=1, eta_min=1e-6)\n        \n        best_val_loss = float('inf')\n        \n        # c. Boucle d'Entraînement et Validation\n        for epoch in range(N_EPOCHS):\n            # --- PHASE D'ENTRAÎNEMENT ---\n            model.train()\n            train_loss_sum = 0\n            for images, targets in train_loader:\n                images, targets = images.to(DEVICE), targets.to(DEVICE)\n                optimizer.zero_grad()\n                predictions = model(images)\n                loss = loss_fn(predictions, targets)\n                loss.backward()\n                optimizer.step()\n                train_loss_sum += loss.item() * images.size(0)\n                \n            train_loss = train_loss_sum / len(train_subset)\n            scheduler.step()\n\n            # --- PHASE DE VALIDATION ---\n            model.eval()\n            val_loss_sum = 0\n            all_val_targets = []\n            all_val_preds = []\n            \n            with torch.no_grad():\n                for images, targets in val_loader:\n                    images, targets = images.to(DEVICE), targets.to(DEVICE)\n                    predictions = model(images)\n                    loss = loss_fn(predictions, targets)\n                    val_loss_sum += loss.item() * images.size(0)\n                    all_val_targets.append(targets)\n                    all_val_preds.append(predictions)\n\n            val_loss = val_loss_sum / len(val_subset)\n            \n            # Calcul de la métrique RMSE (Maintenant résistant aux NaN/Inf)\n            val_rmse = calculate_rmse(torch.cat(all_val_targets), torch.cat(all_val_preds))\n            \n            # Affichage des métriques (MISE À JOUR DU MESSAGE)\n            print(f\"Époque {epoch+1}/{N_EPOCHS} | Train Loss SmoothL1: {train_loss:.4f} | Val Loss SmoothL1: {val_loss:.4f} | Val RMSE: {val_rmse:.4f}\")\n            \n            # Sauvegarde sur la Loss Pondérée\n            if val_loss < best_val_loss:\n                best_val_loss = val_loss\n                torch.save(model.state_dict(), f\"best_cnn_{MODEL_NAME_TAG}_fold_{fold+1}.pth\")\n                # MISE À JOUR DU MESSAGE\n                print(f\"  ➡️ Modèle sauvegardé (Loss SmoothL1: {best_val_loss:.4f})\")\n\n        # d. Enregistrement des Prédictions Out-of-Fold (OOF)\n        model.load_state_dict(torch.load(f\"best_cnn_{MODEL_NAME_TAG}_fold_{fold+1}.pth\"))\n        model.eval()\n        \n        val_preds_list = []\n        with torch.no_grad():\n            for images, _ in val_loader:\n                images = images.to(DEVICE)\n                preds = model(images).cpu().numpy()\n                val_preds_list.append(preds)\n                \n        val_preds = np.concatenate(val_preds_list, axis=0)\n        \n        # CRITICAL : S'assurer que les prédictions sont stockées dans le bon index OOF \n        OOF_PREDS[val_idx, :] = val_preds\n        \n        print(f\"Durée du pli {fold+1}: {time.time() - start_time:.2f}s\")\n\n\n    # --- 3. Stockage des Résultats OOF et Test ---\n    \n    # a. Stockage de l'OOF pour ce modèle\n    ALL_OOF_PREDS[MODEL_NAME_TAG] = OOF_PREDS\n    \n    # b. Ajout des OOF comme nouvelles features au train_df (pour le Stacking)\n    for i, target_name in enumerate(TARGET_NAMES):\n        oof_col_name = f\"OOF_PRED_{MODEL_NAME_TAG}_{target_name}\"\n        train_df[oof_col_name] = OOF_PREDS[:, i]\n    \n    # c. Génération des prédictions TEST moyennées\n    print(f\"\\n⏳ Génération des prédictions TEST moyennées pour {MODEL_NAME_TAG}...\")\n    all_test_preds_scaled = []\n    \n    for fold in range(NUM_FOLDS):\n        model_path = f\"best_cnn_{MODEL_NAME_TAG}_fold_{fold+1}.pth\"\n        \n        # Charger le modèle pour ce pli (pas de path initial ici, car il charge un .pth)\n        model_test = CNNBiomassModel(num_targets=len(TARGET_NAMES), model_name=config['timm_name'], pretrained_path=None).to(DEVICE)\n        model_test.load_state_dict(torch.load(model_path, map_location=DEVICE))\n        model_test.eval()\n        \n        # Prédire sur l'ensemble de TEST\n        fold_test_preds = []\n        with torch.no_grad():\n            # Test_loader doit être un DataLoader contenant le df_test (défini en Cellule 3)\n            # On utilise le test_loader global\n            for images in test_loader: \n                # Correction pour le test_loader: il ne renvoie que les images (pas de labels)\n                if isinstance(images, list) or isinstance(images, tuple):\n                    images = images[0]\n                    \n                images = images.to(DEVICE) \n                outputs = model_test(images).cpu().numpy()\n                fold_test_preds.append(outputs)\n                \n        all_test_preds_scaled.append(np.concatenate(fold_test_preds, axis=0))\n\n    # Calculer la moyenne et stocker\n    final_predictions_scaled_test = np.mean(all_test_preds_scaled, axis=0)\n    ALL_TEST_PREDS[MODEL_NAME_TAG] = final_predictions_scaled_test\n\n    # d. Calcul du R2 OOF final pour l'évaluation CV\n    final_r2_oof = r2_score(TRUE_TARGETS_ALL, OOF_PREDS, multioutput='variance_weighted')\n    \n    print(f\"\\n{'='*50}\")\n    print(f\"✅ RÉSULTATS FINAUX {MODEL_NAME_TAG} : OOF Features ajoutées.\")\n    print(f\"SCORE FINAL CV OOF (R2 PONDÉRÉ) : {final_r2_oof:.4f}\")\n    print(f\"{'='*50}\")\n\nprint(\"\\n🚀 TOUS LES BACKBONES ENTRAÎNÉS. PRÊT POUR LE STACKING (Cellule 7).\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-13T08:54:26.758624Z","iopub.execute_input":"2025-11-13T08:54:26.759224Z","iopub.status.idle":"2025-11-13T09:46:50.641392Z","shell.execute_reply.started":"2025-11-13T08:54:26.759200Z","shell.execute_reply":"2025-11-13T09:46:50.640430Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import lightgbm as lgb\nfrom sklearn.metrics import r2_score\nfrom sklearn.preprocessing import StandardScaler\nimport numpy as np\nimport pandas as pd\nimport joblib\nfrom sklearn.model_selection import KFold\nfrom lightgbm import early_stopping\n\n# --- AJOUT ESSENTIEL : CALCULER LES VRAIES VALEURS DE CIBLES DÉPENDANTES ---\n# Ceci garantit que toutes les cibles dans TARGET_NAMES_ALL existent dans train_df \n# pour l'étape de standardisation.\nif 'GDM_g' not in train_df.columns:\n    print(\"Calcul des VRAIES cibles GDM_g et Dry_Total_g pour l'évaluation...\")\n    \n    # GDM = Dry green vegetation + Dry clover biomass\n    train_df['GDM_g'] = train_df['Dry_Green_g'] + train_df['Dry_Clover_g']\n\n    # Total = Dry green + Dry dead + Dry clover biomass\n    train_df['Dry_Total_g'] = train_df['Dry_Green_g'] + train_df['Dry_Dead_g'] + train_df['Dry_Clover_g']\n    \n    print(\"✅ Cibles dépendantes ajoutées à train_df.\")\nelse:\n    print(\"Cibles GDM_g et Dry_Total_g déjà présentes dans train_df.\")\n# --------------------------------------------------------------------------------------\n\n\n# --- A. Variables Globales & Préparation des Features ---\nBACKBONE_1 = 'resnext50_32x4d'\nBACKBONE_2 = 'resnet18' # <-- Modèle de retour\nTARGET_NAMES_INDEPENDENT = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\nTARGET_NAMES_ALL = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\n\n# 1. Création des Features OOF/Test (NON-SCALÉES - en grammes bruts)\n# Réintroduction des deux backbones\nX_train_oof_raw = np.concatenate([ALL_OOF_PREDS[BACKBONE_1], ALL_OOF_PREDS[BACKBONE_2]], axis=1)\nX_test_oof_raw = np.concatenate([ALL_TEST_PREDS[BACKBONE_1], ALL_TEST_PREDS[BACKBONE_2]], axis=1)\n\n# 2. Features manuelles (Déjà Scalées)\nMANUAL_SCALED_COLS = ['Pct_Green_Scaled', 'Pct_Dry_Scaled']\nX_train_manual_scaled = train_df[MANUAL_SCALED_COLS].values\nX_test_manual_scaled = test_df[MANUAL_SCALED_COLS].values\n\n# 🛑 3. CONCATENATION FINALE 🛑\nX_train_combined_raw = np.concatenate([X_train_oof_raw, X_train_manual_scaled], axis=1)\nX_test_combined_raw = np.concatenate([X_test_oof_raw, X_test_manual_scaled], axis=1)\n\n# 4. Standardisation des cibles (Targets)\nY_train_raw = train_df[TARGET_NAMES_ALL].values\nTARGET_MEANS = Y_train_raw.mean(axis=0)\nTARGET_STDS = Y_train_raw.std(axis=0)\nY_train_scaled = (Y_train_raw - TARGET_MEANS) / TARGET_STDS\n\n# --- B. LightGBM Stacking K-Fold optimisé (3 CIBLES INDÉPENDANTES) ---\n\nLGB_PARAMS = {\n    'objective': 'regression_l1',\n    'metric': 'rmse',\n    # 🛑 PARAMÈTRES D'ORIGINE 🛑\n    'n_estimators': 1000, \n    'learning_rate': 0.05, \n    'feature_fraction': 0.8,\n    'bagging_fraction': 0.8,\n    'bagging_freq': 1,\n    'lambda_l1': 0.1,\n    'lambda_l2': 0.1,\n    'n_jobs': -1,\n    'seed': 42,\n    'verbose': -1,\n}\n\nN_SPLITS = 5\nkf = KFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n\nFINAL_OOF_PREDS_INDEPENDENT_SCALED = np.zeros((Y_train_scaled.shape[0], len(TARGET_NAMES_INDEPENDENT)))\nFINAL_TEST_PREDS_SUM_INDEPENDENT_SCALED = np.zeros((X_test_combined_raw.shape[0], len(TARGET_NAMES_INDEPENDENT)))\n\ntarget_index_map = {name: i for i, name in enumerate(TARGET_NAMES_ALL)}\n\n# Boucle d'entraînement sur les 3 cibles INDÉPENDANTES seulement\nfor target_idx_new, target in enumerate(TARGET_NAMES_INDEPENDENT):\n    \n    original_target_idx = target_index_map[target]\n    print(f\"--> Training LightGBM K-Fold for INDEPENDENT target: {target} (Original Index: {original_target_idx})\")\n    \n    y_train_target = Y_train_scaled[:, original_target_idx]\n    \n    oof_scaler = StandardScaler() \n\n    for fold, (train_index, val_index) in enumerate(kf.split(X_train_combined_raw)):\n        \n        X_train_fold_raw, X_val_fold_raw = X_train_combined_raw[train_index], X_train_combined_raw[val_index]\n        y_train_fold, y_val_fold = y_train_target[train_index], y_train_target[val_index]\n        \n        # SCALING ANTI-LEAKAGE\n        X_train_fold = oof_scaler.fit_transform(X_train_fold_raw)\n        X_val_fold = oof_scaler.transform(X_val_fold_raw)\n        X_test_fold_scaled = oof_scaler.transform(X_test_combined_raw) \n\n        lgb_model = lgb.LGBMRegressor(**LGB_PARAMS)\n        lgb_model.fit(\n            X_train_fold, \n            y_train_fold,\n            eval_set=[(X_val_fold, y_val_fold)], \n            eval_metric='rmse',\n            callbacks=[early_stopping(stopping_rounds=100, verbose=False)] \n        )\n\n        FINAL_OOF_PREDS_INDEPENDENT_SCALED[val_index, target_idx_new] = lgb_model.predict(X_val_fold)\n        FINAL_TEST_PREDS_SUM_INDEPENDENT_SCALED[:, target_idx_new] += lgb_model.predict(X_test_fold_scaled)\n        joblib.dump(lgb_model, f'final_lgbm_stacker_{target}_fold{fold}_reg_original.pkl')\n\nFINAL_TEST_PREDS_INDEPENDENT_SCALED = FINAL_TEST_PREDS_SUM_INDEPENDENT_SCALED / N_SPLITS \n\n# --- C. Dés-standardisation, Calcul des Dépendances et Evaluation ---\n\n# --- 1. Dés-standardisation des 3 cibles indépendantes ---\nfinal_predictions_real = np.zeros((X_test_combined_raw.shape[0], len(TARGET_NAMES_ALL)))\noof_preds_raw = np.zeros_like(Y_train_raw)\n\nfor idx_new, target in enumerate(TARGET_NAMES_INDEPENDENT):\n    idx_orig = target_index_map[target]\n    \n    final_predictions_real[:, idx_orig] = np.maximum(0, FINAL_TEST_PREDS_INDEPENDENT_SCALED[:, idx_new] * TARGET_STDS[idx_orig] + TARGET_MEANS[idx_orig])\n    oof_preds_raw[:, idx_orig] = np.maximum(0, FINAL_OOF_PREDS_INDEPENDENT_SCALED[:, idx_new] * TARGET_STDS[idx_orig] + TARGET_MEANS[idx_orig])\n\n# --- 2. Calcul des 2 cibles DÉPENDANTES (Contraintes) ---\nidx_gdm = target_index_map['GDM_g']\nidx_total = target_index_map['Dry_Total_g']\nidx_green = target_index_map['Dry_Green_g']\nidx_dead = target_index_map['Dry_Dead_g']\nidx_clover = target_index_map['Dry_Clover_g']\n\n# Calculer GDM_g: GDM = Dry Green Vegetation + Dry Clover Biomass\nfinal_predictions_real[:, idx_gdm] = final_predictions_real[:, idx_green] + final_predictions_real[:, idx_clover]\noof_preds_raw[:, idx_gdm] = oof_preds_raw[:, idx_green] + oof_preds_raw[:, idx_clover]\n\n# Calculer Dry_Total_g: Total Dry Biomass = Dry Green Vegetation + Dry Dead Material + Dry Clover Biomass\nfinal_predictions_real[:, idx_total] = final_predictions_real[:, idx_green] + final_predictions_real[:, idx_dead] + final_predictions_real[:, idx_clover]\noof_preds_raw[:, idx_total] = oof_preds_raw[:, idx_green] + oof_preds_raw[:, idx_dead] + oof_preds_raw[:, idx_clover]\n\n\n# --- 3. Évaluation (R2 OOF) ---\nfinal_r2_oof_weighted = r2_score(Y_train_raw, oof_preds_raw, multioutput='variance_weighted')\n\nprint(f\"\\nShape des prédictions finales (TEST): {final_predictions_real.shape}\")\nprint(f\"{'#'*60}\")\nprint(f\"🏆 NOUVEAU SCORE R2 OOF (LightGBM ORIGINAL) : {final_r2_oof_weighted:.5f}\")\nprint(f\"{'#'*60}\")\n\n# --- D. Création du fichier de soumission (format long) ---\nsubmission_list = []\ntry:\n    test_sample_ids = test_df['ImageID'].values\nexcept KeyError:\n    test_sample_ids = test_df['image_path'].str.replace('test/', '').str.replace('.jpg', '', regex=False).values\n\nfor i in range(final_predictions_real.shape[0]):\n    img_id = test_sample_ids[i]\n    for j, target_name in enumerate(TARGET_NAMES_ALL):\n        submission_list.append({\n            \"sample_id\": f\"{img_id}__{target_name}\",\n            \"target\": final_predictions_real[i][j]\n        })\n\nsubmission_df = pd.DataFrame(submission_list)\nsubmission_path = \"submission_original_stacking.csv\" \nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"✅ Soumission générée : {submission_path} ({len(submission_df)} lignes)\")\nprint(submission_df.head(5))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}