{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":126777,"databundleVersionId":15314950}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nfrom scipy.spatial.distance import cosine\nimport matplotlib.pyplot as plt\nfrom tqdm.auto import tqdm\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 1. PRÉPARATION DES DONNÉES (CORRECTION OVERFITTING)\")\nprint(\"=\"*50)\n\n# 1. Scan du disque\ntoutes_les_images = glob.glob('/kaggle/input/**/*.png', recursive=True)\ntoutes_les_images += glob.glob('/kaggle/input/**/*.jpg', recursive=True)\nimage_map = {os.path.basename(p): p for p in toutes_les_images}\n\n# 2. Lecture du CSV\ncsv_path = glob.glob('/kaggle/input/**/train.csv', recursive=True)[0]\nfull_df = pd.read_csv(csv_path)\n\ncol_fichier = [c for c in full_df.columns if c in ['filename', 'image', 'image_name']][0]\nlabel_col = [c for c in full_df.columns if c in ['ground_truth', 'jaguar_id', 'identity', 'label']][0]\n\nfull_df['img_path'] = full_df[col_fichier].apply(lambda x: image_map.get(x))\nfull_df = full_df.dropna(subset=['img_path']).reset_index(drop=True)\n\n# 3. Encodage\nle = LabelEncoder()\nfull_df['label_idx'] = le.fit_transform(full_df[label_col])\nnum_classes = len(le.classes_)\n\n# 4. Split\ntrain_df, val_df = train_test_split(\n    full_df, test_size=0.2, stratify=full_df['label_idx'], random_state=42\n)\n\n# 5. Pipeline avec Augmentation Renforcée\ndef process_path(file_path, label):\n    img = tf.io.read_file(file_path)\n    img = tf.image.decode_image(img, channels=3, expand_animations=False)\n    img = tf.image.resize(img, [224, 224])\n    img = preprocess_input(img) \n    return img, label\n\ndef heavy_augment(img, label):\n    img = tf.image.random_flip_left_right(img)\n    img = tf.image.random_brightness(img, max_delta=0.1)\n    img = tf.image.random_contrast(img, lower=0.9, upper=1.1)\n    # Ajout d'un léger zoom/crop aléatoire\n    img = tf.image.resize(tf.image.central_crop(img, central_fraction=0.9), [224, 224])\n    return img, label\n\nbatch_size = 32\n\ntrain_dataset = tf.data.Dataset.from_tensor_slices((train_df['img_path'].values, train_df['label_idx'].values))\ntrain_dataset = train_dataset.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_dataset = train_dataset.map(heavy_augment, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_dataset = train_dataset.shuffle(buffer_size=1000).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nval_dataset = tf.data.Dataset.from_tensor_slices((val_df['img_path'].values, val_df['label_idx'].values))\nval_dataset = val_dataset.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\nval_dataset = val_dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 2. MODÈLE ROBUSTE AVEC DROPOUT\")\nprint(\"=\"*50)\n\n# On charge ResNet50 pré-entraîné\nbase_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))\n\n# STRATÉGIE : On gèle d'abord le corps du modèle pour ne pas \"polluer\" les poids ImageNet\nbase_model.trainable = False \n\nx = GlobalAveragePooling2D()(base_model.output)\nx = Dropout(0.5)(x)  # Grosse dose de Dropout pour contrer l'overfitting\nx = Dense(512, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.01))(x)\nx = Dropout(0.3)(x)\npredictions = Dense(num_classes, activation='softmax')(x)\n\nmodel = Model(inputs=base_model.input, outputs=predictions)\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 3. ENTRAÎNEMENT EN DEUX PHASES\")\nprint(\"=\"*50)\n\n# Callbacks intelligents\nearly_stop = EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True, verbose=1)\nreduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2, patience=2, min_lr=1e-6, verbose=1)\n\nprint(\"Phase 1 : Entraînement de la tête (Transfer Learning)...\")\nhistory1 = model.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=5,\n    callbacks=[early_stop, reduce_lr]\n)\n\nprint(\"\\nPhase 2 : Fine-tuning (Dégel partiel)...\")\n# On dégel les 20 dernières couches du ResNet\nbase_model.trainable = True\nfor layer in base_model.layers[:-20]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), # LR très bas pour le fine-tuning\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nhistory2 = model.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=10,\n    callbacks=[early_stop, reduce_lr]\n)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 4. SAUVEGARDE ET SUBMISSION\")\nprint(\"=\"*50)\n\nmodel.save('/kaggle/working/resnet50_jaguar_robust.h5')\n\n# Pour l'extraction, on prend la sortie du GlobalAveragePooling\nfeature_extractor = Model(inputs=model.input, outputs=model.layers[-4].output) \n\ntest_csv_path = glob.glob('/kaggle/input/**/test.csv', recursive=True)[0]\ntest_df = pd.read_csv(test_csv_path)\nimages_uniques = set(test_df['query_image']).union(set(test_df['gallery_image']))\n\nembeddings_dict = {}\nprint(\"Extraction des caractéristiques robustes...\")\nfor img_name in tqdm(images_uniques):\n    img_path = image_map.get(img_name)\n    if img_path:\n        img = tf.keras.utils.load_img(img_path, target_size=(224, 224))\n        img_array = tf.keras.utils.img_to_array(img)\n        img_array = np.expand_dims(img_array, axis=0)\n        img_array = preprocess_input(img_array)\n        emb = feature_extractor.predict(img_array, verbose=0)\n        # Normalisation L2 de l'embedding pour une meilleure similarité cosinus\n        emb = emb[0] / np.linalg.norm(emb[0])\n        embeddings_dict[img_name] = emb\n\nprint(\"Calcul des similarités...\")\nsimilarites = []\nfor index, row in tqdm(test_df.iterrows(), total=len(test_df)):\n    q_img = row['query_image']\n    g_img = row['gallery_image']\n    if q_img in embeddings_dict and g_img in embeddings_dict:\n        dist = cosine(embeddings_dict[q_img], embeddings_dict[g_img])\n        similarites.append(1 - dist)\n    else:\n        similarites.append(0.5)\n\nsoumission_df = pd.DataFrame({'row_id': test_df['row_id'], 'similarity': similarites})\nsoumission_df.to_csv('/kaggle/working/submission.csv', index=False)\n\n# Fusion des historiques pour le graphe\nh_loss = history1.history['loss'] + history2.history['loss']\nh_val_loss = history1.history['val_loss'] + history2.history['val_loss']\n\nplt.figure(figsize=(10, 4))\nplt.plot(h_loss, label='Train Loss')\nplt.plot(h_val_loss, label='Val Loss')\nplt.title('Courbe de Perte corrigée')\nplt.legend()\nplt.show()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-17T21:11:21.525614Z","iopub.execute_input":"2026-04-17T21:11:21.526471Z","iopub.status.idle":"2026-04-17T22:03:28.043202Z","shell.execute_reply.started":"2026-04-17T21:11:21.526440Z","shell.execute_reply":"2026-04-17T22:03:28.042368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 0. INSTALLATION DE HUGGING FACE\n# ==========================================\n!pip install transformers -q\n\nimport os\nimport glob\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\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nfrom transformers import ViTForImageClassification\nfrom scipy.spatial.distance import cosine\nfrom tqdm.auto import tqdm\nimport matplotlib.pyplot as plt\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"⚙️ 1. PRÉPARATION DES DONNÉES (PyTorch)\")\nprint(\"=\"*50)\n\n# Scan du disque Kaggle\ntoutes_les_images = glob.glob('/kaggle/input/**/*.png', recursive=True)\ntoutes_les_images += glob.glob('/kaggle/input/**/*.jpg', recursive=True)\nimage_map = {os.path.basename(p): p for p in toutes_les_images}\n\ncsv_path = glob.glob('/kaggle/input/**/train.csv', recursive=True)[0]\nfull_df = pd.read_csv(csv_path)\n\n# Encodage des 31 jaguars\nle = LabelEncoder()\nlabel_col = [c for c in full_df.columns if c in ['ground_truth', 'jaguar_id', 'identity', 'label']][0]\nfull_df['label_idx'] = le.fit_transform(full_df[label_col])\nnum_classes = len(le.classes_)\n\n# Association Image <-> Chemin\ncol_fichier = [c for c in full_df.columns if c in ['filename', 'image', 'image_name']][0]\nfull_df['img_path'] = full_df[col_fichier].apply(lambda x: image_map.get(x))\nfull_df = full_df.dropna(subset=['img_path']).reset_index(drop=True)\n\n# Split Stratifié 80/20\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, stratify=full_df['label_idx'], random_state=42)\n\n# Définition du Dataset PyTorch\nclass JaguarDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_path = self.df.loc[idx, 'img_path']\n        label = self.df.loc[idx, 'label_idx']\n        image = Image.open(img_path).convert('RGB')\n        if self.transform:\n            image = self.transform(image)\n        return image, label\n\n# Prétraitement mathématique spécifique exigé par le ViT\ntrain_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) \n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])\n])\n\ntrain_loader = DataLoader(JaguarDataset(train_df, train_transform), batch_size=32, shuffle=True, num_workers=2)\nval_loader = DataLoader(JaguarDataset(val_df, val_transform), batch_size=32, shuffle=False, num_workers=2)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\" Utilisation de l'accélérateur : {device}\")\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 2. CRÉATION DU VISION TRANSFORMER (Hugging Face)\")\nprint(\"=\"*50)\n\n# ViTForImageClassification coupe automatiquement la tête d'origine et met la nôtre (num_classes)\nmodel = ViTForImageClassification.from_pretrained(\n    'google/vit-base-patch16-224-in21k',\n    num_labels=num_classes\n)\nmodel.to(device)\ncriterion = nn.CrossEntropyLoss()\n\n# Fonctions d'entraînement standards PyTorch\ndef train_epoch(model, loader, optimizer):\n    model.train()\n    total_loss, correct, total = 0, 0, 0\n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(images).logits\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n        _, preds = torch.max(outputs, 1)\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n    return total_loss / len(loader), correct / total\n\ndef eval_epoch(model, loader):\n    model.eval()\n    total_loss, correct, total = 0, 0, 0\n    with torch.no_grad():\n        for images, labels in loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images).logits\n            loss = criterion(outputs, labels)\n            total_loss += loss.item()\n            _, preds = torch.max(outputs, 1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n    return total_loss / len(loader), correct / total\n\nprint(\"\\nPHASE 1 : WARM-UP (Transformer Gelé, 3 Époques)\")\n# On gèle le cerveau (le Transformer)\nfor param in model.vit.parameters():\n    param.requires_grad = False\n\noptimizer_warmup = torch.optim.Adam(model.classifier.parameters(), lr=1e-3)\n\ntrain_losses, val_losses = [], []\nfor epoch in range(3):\n    t_loss, t_acc = train_epoch(model, train_loader, optimizer_warmup)\n    v_loss, v_acc = eval_epoch(model, val_loader)\n    train_losses.append(t_loss); val_losses.append(v_loss)\n    print(f\"Époque {epoch+1}/3 - Loss: {t_loss:.4f} | Val Acc: {v_acc:.4f}\")\n\nprint(\"\\nPHASE 2 : FINE-TUNING GLOBAL (Modèle Dégelé, 7 Époques)\")\n# On dégèle le cerveau pour un apprentissage profond\nfor param in model.vit.parameters():\n    param.requires_grad = True\n\n# Le Learning Rate est divisé par 100 pour ne pas \"casser\" l'intelligence pré-entraînée\noptimizer_finetune = torch.optim.Adam(model.parameters(), lr=1e-5)\n\nfor epoch in range(7):\n    t_loss, t_acc = train_epoch(model, train_loader, optimizer_finetune)\n    v_loss, v_acc = eval_epoch(model, val_loader)\n    train_losses.append(t_loss); val_losses.append(v_loss)\n    print(f\"Époque {epoch+1}/7 - Loss: {t_loss:.4f} | Val Acc: {v_acc:.4f}\")\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 3. GÉNÉRATION DE SUBMISSION_VIT.CSV\")\nprint(\"=\"*50)\n\n# Sauvegarde des poids du modèle\ntorch.save(model.state_dict(), '/kaggle/working/vit_jaguar.pth')\n\ntest_csv_path = glob.glob('/kaggle/input/**/test.csv', recursive=True)[0]\ntest_df = pd.read_csv(test_csv_path)\nimages_uniques = set(test_df['query_image']).union(set(test_df['gallery_image']))\n\nmodel.eval()\nembeddings_dict = {}\n\nprint(\"Étape A : Extraction de la signature [CLS] par le Transformer...\")\nwith torch.no_grad():\n    for img_name in tqdm(images_uniques):\n        img_path = image_map.get(img_name)\n        if img_path:\n            image = Image.open(img_path).convert('RGB')\n            tensor = val_transform(image).unsqueeze(0).to(device)\n            \n            # On passe l'image dans le corps du ViT (model.vit) pour éviter la tête de classification\n            outputs = model.vit(tensor)\n            \n            # MAGIE DU ViT : Le vecteur global de l'image est toujours le token à l'index 0 (le CLS Token)\n            cls_token_embedding = outputs.last_hidden_state[0, 0, :].cpu().numpy()\n            embeddings_dict[img_name] = cls_token_embedding\n\nprint(\"Étape B : Calcul de la Similarité Cosinus...\")\nsimilarites = []\nfor index, row in test_df.iterrows():\n    q_img = row['query_image']\n    g_img = row['gallery_image']\n    \n    if q_img in embeddings_dict and g_img in embeddings_dict:\n        distance = cosine(embeddings_dict[q_img], embeddings_dict[g_img])\n        sim_normalisee = 1 - (distance / 2.0)\n        similarites.append(sim_normalisee)\n    else:\n        similarites.append(0.5)\n\nsoumission_df = pd.DataFrame({'row_id': test_df['row_id'], 'similarity': similarites})\nchemin_soumission = '/kaggle/working/submission_vit.csv'\nsoumission_df.to_csv(chemin_soumission, index=False)\nprint(f\" Fichier prêt pour Kaggle : {chemin_soumission}\")\n\n# Graphique final pour votre mémoire\nplt.figure(figsize=(8, 5))\nplt.plot(train_losses, label='Train Loss', color='blue')\nplt.plot(val_losses, label='Val Loss', color='orange')\nplt.axvline(x=2.5, color='red', linestyle='--', label='Dégel (Unfreezing)')\nplt.title('Apprentissage Progressif - Vision Transformer')\nplt.xlabel('Époques totales')\nplt.ylabel('Erreur (Loss)')\nplt.legend()\nplt.grid(True, alpha=0.3)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-15T23:08:20.830481Z","iopub.execute_input":"2026-04-15T23:08:20.830965Z","iopub.status.idle":"2026-04-16T00:11:21.606185Z","shell.execute_reply.started":"2026-04-15T23:08:20.830930Z","shell.execute_reply":"2026-04-16T00:11:21.605344Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n# 🔇 Rend TensorFlow silencieux sur les faux avertissements CUDA\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' \n\nimport tensorflow as tf\nfrom tensorflow.keras.applications import EfficientNetB0\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nimport pandas as pd\nimport numpy as np\nimport glob\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nimport matplotlib.pyplot as plt\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 1. PRÉPARATION DES DONNÉES (Natif Keras)\")\nprint(\"=\"*50)\n\n# Scan du disque\ntoutes_les_images = glob.glob('/kaggle/input/**/*.png', recursive=True)\ntoutes_les_images += glob.glob('/kaggle/input/**/*.jpg', recursive=True)\nimage_map = {os.path.basename(p): p for p in toutes_les_images}\ncsv_path = glob.glob('/kaggle/input/**/train.csv', recursive=True)[0]\nfull_df = pd.read_csv(csv_path)\n\n# Encodage pour 31 jaguars\nle = LabelEncoder()\nlabel_col = [c for c in full_df.columns if c in ['ground_truth', 'jaguar_id', 'identity', 'label']][0]\nfull_df['label_idx'] = le.fit_transform(full_df[label_col]) \nnum_classes = len(le.classes_)\n\n# Split avec mapping\ncol_fichier = [c for c in full_df.columns if c in ['filename', 'image', 'image_name']][0]\nfull_df['img_path'] = full_df[col_fichier].apply(lambda x: image_map.get(x))\nfull_df = full_df.dropna(subset=['img_path']).reset_index(drop=True)\n\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, stratify=full_df['label_idx'], random_state=42)\n\n# Pipeline tf.data optimisé\ndef process_path(file_path, label):\n    img = tf.io.read_file(file_path)\n    img = tf.image.decode_image(img, channels=3, expand_animations=False)\n    img = tf.image.resize(img, [224, 224])\n    # Prétraitement spécifique et natif pour EfficientNet (crucial !)\n    img = preprocess_input(img)\n    return img, label\n\ndef augment(img, label):\n    img = tf.image.random_brightness(img, max_delta=0.2)\n    img = tf.image.random_contrast(img, lower=0.8, upper=1.2)\n    return img, label\n\nbatch_size = 32\n\ntrain_ds = tf.data.Dataset.from_tensor_slices((train_df['img_path'].values, train_df['label_idx'].values))\ntrain_ds = train_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds = train_ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds = train_ds.shuffle(1000).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nval_ds = tf.data.Dataset.from_tensor_slices((val_df['img_path'].values, val_df['label_idx'].values))\nval_ds = val_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\nval_ds = val_ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nprint(\"\\n PHASE 1 : WARM-UP (Base EfficientNet Gelée)\")\n\n# Chargement natif d'EfficientNetB0\nbase_model = EfficientNetB0(weights='imagenet', include_top=False, pooling='avg')\n\n#  ON GÈLE LE MODÈLE DE BASE\nbase_model.trainable = False \n\ninputs = tf.keras.Input(shape=(224, 224, 3))\nx = base_model(inputs)\noutputs = tf.keras.layers.Dense(num_classes, activation='softmax')(x)\n\nmodel = tf.keras.Model(inputs, outputs)\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\n# Entraînement court de la tête de classification\nhistory_warmup = model.fit(train_ds, validation_data=val_ds, epochs=5)\n\nprint(\"\\n PHASE 2 : FINE-TUNING GLOBAL (Base Dégelée)\")\n\n#  ON DÉGÈLE LE MODÈLE\nbase_model.trainable = True \n\n# Recompilation avec un taux d'apprentissage très faible pour ne pas détruire les poids pré-entraînés\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), \n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nearly_stop = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True, verbose=1)\n\n# Entraînement profond de tout le réseau\nhistory_finetuning = model.fit(\n    train_ds, \n    validation_data=val_ds, \n    epochs=15, \n    callbacks=[early_stop]\n)\n\nprint(\"\\n✅ Stratégie terminée avec succès !\")\n\n# Sauvegarde\nmodel.save('/kaggle/working/efficientnet_jaguar.h5')\nprint(\" Modèle sauvegardé dans : /kaggle/working/efficientnet_jaguar.h5\")\n\n# Visualisation des performances combinées\ntotal_loss = history_warmup.history['loss'] + history_finetuning.history['loss']\ntotal_val_loss = history_warmup.history['val_loss'] + history_finetuning.history['val_loss']\ntotal_acc = history_warmup.history['accuracy'] + history_finetuning.history['accuracy']\ntotal_val_acc = history_warmup.history['val_accuracy'] + history_finetuning.history['val_accuracy']\n\nplt.figure(figsize=(12, 5))\n\nplt.subplot(1, 2, 1)\nplt.plot(total_loss, label='Train Loss')\nplt.plot(total_val_loss, label='Val Loss')\nplt.axvline(x=4.5, color='r', linestyle='--', label='Dégel (Unfreezing)')\nplt.title('Apprentissage Progressif EfficientNet : Loss')\nplt.legend()\n\nplt.subplot(1, 2, 2)\nplt.plot(total_acc, label='Train Accuracy')\nplt.plot(total_val_acc, label='Val Accuracy')\nplt.axvline(x=4.5, color='r', linestyle='--', label='Dégel')\nplt.title('Apprentissage Progressif EfficientNet : Accuracy')\nplt.legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T00:16:00.409623Z","iopub.execute_input":"2026-04-16T00:16:00.410790Z","iopub.status.idle":"2026-04-16T00:48:25.475792Z","shell.execute_reply.started":"2026-04-16T00:16:00.410693Z","shell.execute_reply":"2026-04-16T00:48:25.475050Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n# 🔇 Rend TensorFlow silencieux sur les faux avertissements CUDA\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' \n\nimport glob\nimport pandas as pd\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\nimport matplotlib.pyplot as plt\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 1. PRÉPARATION DES DONNÉES\")\nprint(\"=\"*50)\n\n# Scan du disque\ntoutes_les_images = glob.glob('/kaggle/input/**/*.png', recursive=True)\ntoutes_les_images += glob.glob('/kaggle/input/**/*.jpg', recursive=True)\nimage_map = {os.path.basename(p): p for p in toutes_les_images}\n\ncsv_path = glob.glob('/kaggle/input/**/train.csv', recursive=True)[0]\nfull_df = pd.read_csv(csv_path)\n\n# Encodage pour vos 31 jaguars (C'est ICI que num_classes est créé !)\nle = LabelEncoder()\nlabel_col = [c for c in full_df.columns if c in ['ground_truth', 'jaguar_id', 'identity', 'label']][0]\nfull_df['label_idx'] = le.fit_transform(full_df[label_col])\nnum_classes = len(le.classes_) \nprint(f\"Nombre de classes détectées : {num_classes}\")\n\n# Split avec mapping\ncol_fichier = [c for c in full_df.columns if c in ['filename', 'image', 'image_name']][0]\nfull_df['img_path'] = full_df[col_fichier].apply(lambda x: image_map.get(x))\nfull_df = full_df.dropna(subset=['img_path']).reset_index(drop=True)\n\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, stratify=full_df['label_idx'], random_state=42)\n\n# Pipelines tf.data\ndef process_path(file_path, label):\n    img = tf.io.read_file(file_path)\n    img = tf.image.decode_image(img, channels=3, expand_animations=False)\n    img = tf.image.resize(img, [224, 224])\n    img = img / 255.0 \n    return img, label\n\ndef augment(img, label):\n    img = tf.image.random_brightness(img, max_delta=0.1)\n    img = tf.image.random_flip_left_right(img) # Data augmentation simple\n    return img, label\n\nbatch_size = 32\n\ntrain_ds = tf.data.Dataset.from_tensor_slices((train_df['img_path'].values, train_df['label_idx'].values))\ntrain_ds = train_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds = train_ds.map(augment, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds = train_ds.shuffle(1000).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nval_ds = tf.data.Dataset.from_tensor_slices((val_df['img_path'].values, val_df['label_idx'].values))\nval_ds = val_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\"2. CRÉATION DU CNN SUR-MESURE AVEC DROPOUT\")\nprint(\"=\"*50)\n\nmodel = models.Sequential([\n    # BLOC CONVOLUTIF 1\n    layers.Input(shape=(224, 224, 3)),\n    layers.Conv2D(32, kernel_size=(3, 3), activation='relu', padding='same'),\n    layers.MaxPooling2D(pool_size=(2, 2)),\n    \n    # BLOC CONVOLUTIF 2\n    layers.Conv2D(64, kernel_size=(3, 3), activation='relu', padding='same'),\n    layers.MaxPooling2D(pool_size=(2, 2)),\n\n    # BLOC CONVOLUTIF 3\n    layers.Conv2D(128, kernel_size=(3, 3), activation='relu', padding='same'),\n    layers.MaxPooling2D(pool_size=(2, 2)),\n\n    # PARTIE DENSE & CLASSIFICATION\n    layers.Flatten(),\n    layers.Dense(256, activation='relu'),\n    \n    #  LA COUCHE DE RÉGULARISATION\n    layers.Dropout(0.5),\n    \n    layers.Dense(num_classes, activation='softmax')\n])\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nmodel.summary()\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 3. ENTRAÎNEMENT DU MODÈLE\")\nprint(\"=\"*50)\n\n# Un modèle from scratch a besoin de plus de temps pour apprendre, on augmente les époques\nearly_stop = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1)\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=25,\n    callbacks=[early_stop]\n)\n\n# Visualisation pour votre rapport\nplt.figure(figsize=(12, 5))\nplt.subplot(1, 2, 1)\nplt.plot(history.history['loss'], label='Train Loss', color='blue')\nplt.plot(history.history['val_loss'], label='Val Loss', color='orange')\nplt.title('CNN From Scratch : Perte (Loss)')\nplt.legend()\n\nplt.subplot(1, 2, 2)\nplt.plot(history.history['accuracy'], label='Train Accuracy', color='blue')\nplt.plot(history.history['val_accuracy'], label='Val Accuracy', color='orange')\nplt.title('CNN From Scratch : Précision (Accuracy)')\nplt.legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T12:59:35.175323Z","iopub.execute_input":"2026-04-16T12:59:35.175984Z","iopub.status.idle":"2026-04-16T13:41:06.683743Z","shell.execute_reply.started":"2026-04-16T12:59:35.175951Z","shell.execute_reply":"2026-04-16T13:41:06.682975Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' \n\nimport glob\nimport pandas as pd\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 1. PRÉPARATION DES DONNÉES\")\nprint(\"=\"*50)\n\ntoutes_les_images = glob.glob('/kaggle/input/**/*.png', recursive=True)\ntoutes_les_images += glob.glob('/kaggle/input/**/*.jpg', recursive=True)\nimage_map = {os.path.basename(p): p for p in toutes_les_images}\n\ncsv_path = glob.glob('/kaggle/input/**/train.csv', recursive=True)[0]\nfull_df = pd.read_csv(csv_path)\n\n# C'est ici que la variable manquante est calculée !\nle = LabelEncoder()\nlabel_col = [c for c in full_df.columns if c in ['ground_truth', 'jaguar_id', 'identity', 'label']][0]\nfull_df['label_idx'] = le.fit_transform(full_df[label_col])\nnum_classes = len(le.classes_) \n\ncol_fichier = [c for c in full_df.columns if c in ['filename', 'image', 'image_name']][0]\nfull_df['img_path'] = full_df[col_fichier].apply(lambda x: image_map.get(x))\nfull_df = full_df.dropna(subset=['img_path']).reset_index(drop=True)\n\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, stratify=full_df['label_idx'], random_state=42)\n\ndef process_path(file_path, label):\n    img = tf.io.read_file(file_path)\n    img = tf.image.decode_image(img, channels=3, expand_animations=False)\n    img = tf.image.resize(img, [224, 224])\n    img = img / 255.0 \n    return img, label\n\nbatch_size = 32\n\ntrain_ds = tf.data.Dataset.from_tensor_slices((train_df['img_path'].values, train_df['label_idx'].values))\ntrain_ds = train_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_ds = train_ds.shuffle(1000).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nval_ds = tf.data.Dataset.from_tensor_slices((val_df['img_path'].values, val_df['label_idx'].values))\nval_ds = val_ds.map(process_path, num_parallel_calls=tf.data.AUTOTUNE).batch(batch_size).prefetch(tf.data.AUTOTUNE)\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 2. CRÉATION DU MODÈLE TEMP0REL (LSTM sur Image)\")\nprint(\"=\"*50)\n\nmodel = models.Sequential([\n    layers.Input(shape=(224, 224, 3)),\n    \n    # Écrasement des dimensions : (224, 224, 3) -> (224, 672)\n    layers.Reshape((224, 224 * 3)),\n\n    # Lecture de l'image ligne par ligne de haut en bas\n    layers.LSTM(128, return_sequences=False),\n    \n    layers.Dropout(0.5),\n    layers.Dense(256, activation='relu'),\n    layers.Dropout(0.3),\n    layers.Dense(num_classes, activation='softmax')\n])\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n    loss='sparse_categorical_crossentropy',\n    metrics=['accuracy']\n)\n\nmodel.summary()\n\nprint(\"\\n\" + \"=\"*50)\nprint(\" 3. ENTRAÎNEMENT\")\nprint(\"=\"*50)\n\nearly_stop = EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True)\n\nhistory = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=10,\n    callbacks=[early_stop]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T14:04:07.642479Z","iopub.execute_input":"2026-04-16T14:04:07.642949Z","iopub.status.idle":"2026-04-16T14:37:06.229557Z","shell.execute_reply.started":"2026-04-16T14:04:07.642925Z","shell.execute_reply":"2026-04-16T14:37:06.228847Z"}},"outputs":[],"execution_count":null}]}