{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"accelerator":"GPU","colab":{"gpuType":"T4","provenance":[]},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":124046,"databundleVersionId":14573278,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Bien que les images FIB/SEM soient initialement des volumes 3D de grande taille, le challenge fournit directement un ensemble d’imagettes 2D extraites autour de pixels annotés. Chaque imagette est associée à une étiquette correspondant à la classe du pixel central. Le problème est ainsi formulé comme une tâche de classification supervisée d’images 2D.","metadata":{"_uuid":"5155d3f4-105b-4a4c-9553-8b0eb9f5c9e9","_cell_guid":"882ad881-a8b8-4bc5-a705-131e9bc5d009","trusted":true,"collapsed":false,"id":"AXAmA4bgY-GW","jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom matplotlib import pyplot as plt\nimport os\nimport pickle\nimport gzip\nimport urllib\nfrom zipfile import ZipFile\n\nimport torchvision.transforms as TF\nfrom torchvision.transforms import ToTensor, ToPILImage, Grayscale\n\ndevice = (\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(device)","metadata":{"_uuid":"b4bafdd0-2f2a-4eaa-9fbb-6cc1b1ab920f","_cell_guid":"234b5cd1-8d52-464e-82b4-e32248970634","trusted":true,"collapsed":false,"id":"pLq7A-D2nPiK","outputId":"cf9790c5-679f-4d12-e3ab-e46c154460e9","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:07.945176Z","iopub.execute_input":"2026-01-21T19:13:07.945912Z","iopub.status.idle":"2026-01-21T19:13:07.951059Z","shell.execute_reply.started":"2026-01-21T19:13:07.945883Z","shell.execute_reply":"2026-01-21T19:13:07.950310Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Run to install pydot\n!pip install pydot-ng\n!pip install graphviz\n!pip install torchsummary","metadata":{"_uuid":"583e5fda-fcf8-4e97-bdec-da6566c614cc","_cell_guid":"2db71b61-0c1b-45fb-8670-9432836a4e5c","trusted":true,"collapsed":false,"id":"9WldbX4IG2it","outputId":"f977438a-d53e-45a2-b683-ad3632ba5f76","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:07.952663Z","iopub.execute_input":"2026-01-21T19:13:07.953015Z","iopub.status.idle":"2026-01-21T19:13:17.227778Z","shell.execute_reply.started":"2026-01-21T19:13:07.952988Z","shell.execute_reply":"2026-01-21T19:13:17.226823Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm import tqdm\n\n# Fonction de chargement\ndef load_data_from_folder(csv_path, img_dir):\n    df = pd.read_csv(csv_path)\n\n    data = []\n    labels = []\n\n    print(f\"Chargement de {len(df)} images depuis {img_dir}...\")\n\n    # Boucle sur chaque ligne du CSV\n    for index, row in tqdm(df.iterrows(), total=len(df)):\n        img_id = row['ID']\n        category = row['Category']\n\n       \n        filename = f\"{img_id}.png\"\n        filepath = os.path.join(img_dir, filename)\n\n        try:\n            #Ouvre l'image et converti en niveaux de gris\n            img = Image.open(filepath).convert('L')\n\n            # Transformer en tableau Numpy\n            img_array = np.array(img)\n\n            data.append(img_array)\n            labels.append(category)\n\n        except FileNotFoundError:\n            continue\n\n    # On convertit les listes en tableaux Numpy\n    return np.array(data), np.array(labels)\n\n\nbase_dir = \"/kaggle/input/2025-competition-apprentissage-i-3-d\"\n\nimg_dir_kaggle = os.path.join(base_dir, \"train\") \ncsv_file_kaggle = os.path.join(base_dir, \"train.csv\")\n\n# LAUNCH\n\nif os.path.exists(csv_file_kaggle):\n    print(\"Fichier CSV trouvé\")\n    X_train, y_train = load_data_from_folder(csv_file_kaggle, img_dir_kaggle)\n\n    print(f\"Images chargées : {X_train.shape}\") \n    print(f\"Labels chargés : {y_train.shape}\")\nelse:\n    print(f\"Fichier CSV non trouvé: {csv_file_kaggle}\")","metadata":{"_uuid":"cd2db54c-8372-427e-aa33-fd52099afca1","_cell_guid":"e19b7b04-03ce-4693-901c-c57a886a1fdf","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:17.229209Z","iopub.execute_input":"2026-01-21T19:13:17.229483Z","iopub.status.idle":"2026-01-21T19:13:57.573180Z","shell.execute_reply.started":"2026-01-21T19:13:17.229454Z","shell.execute_reply":"2026-01-21T19:13:57.572442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"N = len(X_train)\n\n# Random mix des indices x_train\nindices_melanges = np.random.permutation(N)\n\n# On applique le mix\nX_train = X_train[indices_melanges]\ny_train = y_train[indices_melanges]\n\nprint(\"Dataset mélangé\")","metadata":{"_uuid":"2af11ef8-28f8-400e-ab27-283ec6b128b3","_cell_guid":"9bd4cf98-602b-4f47-ba40-830ace2d7c06","trusted":true,"collapsed":false,"id":"SKBxGQYgc076","outputId":"e8edaba9-7d76-48d5-cd3e-9faeb78a6dc2","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:57.574263Z","iopub.execute_input":"2026-01-21T19:13:57.574699Z","iopub.status.idle":"2026-01-21T19:13:57.867508Z","shell.execute_reply.started":"2026-01-21T19:13:57.574670Z","shell.execute_reply":"2026-01-21T19:13:57.866771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nprint(f\"Total des images avant découpage : {len(X_train)}\")\n\n# Découpage automatique\n# test_size=0.05 signifie qu'on garde 15% pour la validation\nX_train_final, X_val, y_train_final, y_val = train_test_split(\n    X_train,       \n    y_train,      \n    test_size=0.05, # 5% pour la validation \n    random_state=39, # Pour que le mélange soit toujours le même\n    stratify=y_train # On conserve les proportions\n)\n\n# Affichage de la Shape\nprint(\"-\" * 30)\nprint(f\"Train set shape : data={X_train_final.shape}, label={y_train_final.shape}\")\nprint(f\"Val set shape   : data={X_val.shape}, label={y_val.shape}\")\nprint(\"-\" * 30)\n\n# Petit check pour etre sur\nprint(\"Pourcentage de chaque classe dans le set de Validation :\")\nprint(pd.Series(y_val).value_counts(normalize=True) * 100)","metadata":{"_uuid":"6c443e15-f6bd-4de8-a006-487bb2b58bef","_cell_guid":"4f2cbfe3-a346-463e-8e43-17ddb9d9d663","trusted":true,"collapsed":false,"id":"DENQVB3Qe0vJ","outputId":"5c0b39d5-07e9-492b-a841-1e95ef8543be","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:57.869368Z","iopub.execute_input":"2026-01-21T19:13:57.870126Z","iopub.status.idle":"2026-01-21T19:13:58.149065Z","shell.execute_reply.started":"2026-01-21T19:13:57.870098Z","shell.execute_reply":"2026-01-21T19:13:58.148376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset\nfrom torchvision.transforms import ToTensor\n\nclass FIBSEMDataset(Dataset):\n    def __init__(self, data, labels, transform=None):\n        self.data = data\n        self.labels = labels\n        # On applique au minimum ToTensor()\n        self.transform = transform if transform is not None else ToTensor()\n\n    def __len__(self):\n        return len(self.labels)\n\n    def __getitem__(self, idx):\n        # On récupère l'image le label à l'index idx\n        img = self.data[idx]\n        label = self.labels[idx]\n\n        # Transformation des données\n        # Si tes données sont des numpy arrays uint8 (0-255), c'est parfait.\n        img_tensor = self.transform(img)\n\n        # Le label doit être en long pour la CrossEntropyLoss\n        return img_tensor, torch.tensor(label, dtype=torch.long)","metadata":{"_uuid":"11bf46e6-3acb-4dbd-99c4-515cbcd74697","_cell_guid":"cb9bef06-617c-45d0-894b-9f315d67deae","trusted":true,"collapsed":false,"id":"6Qydy3RVcDi3","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:58.149926Z","iopub.execute_input":"2026-01-21T19:13:58.150126Z","iopub.status.idle":"2026-01-21T19:13:58.157006Z","shell.execute_reply.started":"2026-01-21T19:13:58.150108Z","shell.execute_reply":"2026-01-21T19:13:58.156349Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom torchvision.transforms import ToPILImage\nfrom torchvision import transforms\nfrom torchvision import transforms\n\n# Définition des transformations pour l'entraînement\ntrain_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)),     \n    transforms.RandomHorizontalFlip(0.5),\n    transforms.RandomVerticalFlip(0.5),\n    transforms.ToTensor(),\n    transforms.Normalize([0.5], [0.5]),\n\n])\n\nval_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)),     \n    transforms.ToTensor(),\n    transforms.Normalize([0.5], [0.5]),\n])\n\ntrain_set = FIBSEMDataset(X_train_final, y_train_final, transform=train_transforms)\nval_set = FIBSEMDataset(X_val, y_val, transform=val_transforms)\n\nclasses_names = {0: \"Autre\", 1: \"Mito\", 2: \"Réticulum\", 3: \"Mb Nucléaire\", 4: \"Mb Cellulaire\"}\n\nplt.figure(figsize=(15, 3)) # On affiche les 5 images\n\nfor class_id in range(5):\n    # On cherche l'index du premier élément qui correspond à la classe 'class_id'\n    # np.where renvoie les indices, on prend le premier [0][0]\n    idx = np.where(y_train_final == class_id)[0][0]\n\n    # On récupère l'image et le label dans le dataset\n    img_tensor, label_tensor = train_set[idx]\n\n    # Affichage dans une sous-fenêtre (1 ligne, 5 colonnes)\n    plt.subplot(1, 5, class_id + 1)\n\n    # On convertit le Tensor en image PIL pour l'affichage\n    # cmap='gray' est important pour voir le noir et blanc correct\n    plt.imshow(ToPILImage()(img_tensor), cmap='gray')\n\n    plt.title(f\"{classes_names[class_id]}\")\n    plt.axis('off') # Enlève les axes pour faire propre\n\nplt.show()","metadata":{"_uuid":"579ff8d0-d216-44ba-a953-9cde42ea3976","_cell_guid":"07ba4213-6ba8-4d7e-8b84-e337d81a371a","trusted":true,"collapsed":false,"id":"H2N3FFtgfjkZ","outputId":"8a8e24e1-0236-4f22-de7b-1dcf4d230877","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:58.157936Z","iopub.execute_input":"2026-01-21T19:13:58.158285Z","iopub.status.idle":"2026-01-21T19:13:58.453864Z","shell.execute_reply.started":"2026-01-21T19:13:58.158246Z","shell.execute_reply":"2026-01-21T19:13:58.453143Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create train & val & test sets dataloaders\nfrom torch.utils.data import DataLoader\nbs = 64\n\n# On recréer les loaders\ntrain_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=2,pin_memory=True)\n\nval_loader = DataLoader(val_set, batch_size=64, shuffle=False, num_workers=1, pin_memory=True)\n\n\n# On récupère le premier paquet d'images pour voir sa forme\nimages_batch, labels_batch = next(iter(train_loader))\n\nprint(f\"Taille d'un batch d'images : {images_batch.shape}\")\nprint(f\"Taille d'un batch de labels : {labels_batch.shape}\")","metadata":{"_uuid":"92045d11-9787-49db-9193-6f3338efc9b7","_cell_guid":"eb2ce4e7-dab5-490c-9d88-113f7de2a8e5","trusted":true,"collapsed":false,"id":"PNh3Mw0YhOQH","outputId":"3c79b70a-abd6-43bf-eda1-14a0689fe4d6","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:58.454936Z","iopub.execute_input":"2026-01-21T19:13:58.455168Z","iopub.status.idle":"2026-01-21T19:13:58.766456Z","shell.execute_reply.started":"2026-01-21T19:13:58.455146Z","shell.execute_reply":"2026-01-21T19:13:58.765474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom torchvision.transforms import ToPILImage\n\n# On crée l'itérateur\ndataiter = iter(train_loader)\n\nplt.figure(figsize=(8, 8))\n\n#  Premier Batch \nimages, labels = next(dataiter)\n\n# Image 1 du Batch 1\nplt.subplot(2, 2, 1)\nplt.imshow(ToPILImage()(images[0]), cmap='gray')\nplt.title(f\"Batch 1 - Label: {labels[0].item()}\")\nplt.axis('off')\n\n# Image 2 du Batch 1\nplt.subplot(2, 2, 2)\nplt.imshow(ToPILImage()(images[1]), cmap='gray')\nplt.title(f\"Batch 1 - Label: {labels[1].item()}\")\nplt.axis('off')\n\n# Deuxième Batch\n# On appelle next() une 2ème fois pour passer au paquet suivant\nimages, labels = next(dataiter)\n\n# Image 1 du Batch 2\nplt.subplot(2, 2, 3)\nplt.imshow(ToPILImage()(images[0]), cmap='gray')\nplt.title(f\"Batch 2 - Label: {labels[0].item()}\")\nplt.axis('off')\n\n# Image 2 du Batch 2\nplt.subplot(2, 2, 4)\nplt.imshow(ToPILImage()(images[1]), cmap='gray')\nplt.title(f\"Batch 2 - Label: {labels[1].item()}\")\nplt.axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"_uuid":"bcc7d938-029f-4a42-9c74-f2cfe4dd3922","_cell_guid":"ea2ab30b-9382-4e25-a257-87f2a1deec4f","trusted":true,"collapsed":false,"id":"rnVeeSwPh7zz","outputId":"01cbb8be-2827-46f1-d8f4-11e8ab513840","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:58.767825Z","iopub.execute_input":"2026-01-21T19:13:58.768087Z","iopub.status.idle":"2026-01-21T19:13:59.537832Z","shell.execute_reply.started":"2026-01-21T19:13:58.768057Z","shell.execute_reply":"2026-01-21T19:13:59.536798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# On ne se sert pas de ce modèle pour la suite de l'entrainemet\n# On le laisse pour montrer le processus de recherche\n\nimport torch\nimport torch.nn as nn\n\nclass CNN(nn.Module):\n    def __init__(self, num_classes=5):\n        super(CNN, self).__init__()\n\n        self.relu = nn.ReLU()\n\n        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, stride=1, padding=1)\n        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # Divise taille par 2 (128->64)\n\n        self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)\n        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # Divise taille par 2 (64->32)\n\n        self.conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1)\n        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2) # Divise taille par 2 (32->16)\n\n  \n        # Image départ 128x128 -> pool1 -> 64x64 -> pool2 -> 32x32 -> pool3 -> 16x16\n        # On a 128 canaux à la fin.\n        # Donc la taille aplatie est : 128 * 16 * 16 = 32768\n\n        self.flatten_size = 128 * 16 * 16\n\n        self.linear1 = nn.Linear(self.flatten_size, 512)\n        self.dropout = nn.Dropout(0.5) # Ajouté pour éviter le par-coeur\n        self.linear2 = nn.Linear(512, num_classes) # 5 classes\n\n    def forward(self, x):\n        # Bloc 1\n        x = self.relu(self.conv1(x))\n        x = self.pool1(x)\n\n        # Bloc 2\n        x = self.relu(self.conv2(x))\n        x = self.pool2(x)\n\n        # Bloc 3\n        x = self.relu(self.conv3(x))\n        x = self.pool3(x)\n\n        # Aplatissement\n        x = x.reshape(x.shape[0], -1)\n\n        # Dense layers\n        x = self.relu(self.linear1(x))\n        x = self.dropout(x) # Important !\n        x = self.linear2(x)\n\n        return x\n\n# Sanity Check\n# On teste avec la vraie taille de tes images (128x128) et 1 canal\nx_test = torch.randn(64, 1, 128, 128)\nmodel = CNN(num_classes=5)\nout = model(x_test)\n\nprint(f\"Input shape: {x_test.shape}\")\nprint(f\"Output shape: {out.shape}\")","metadata":{"_uuid":"55ac4666-d026-4141-8667-4a62153666df","_cell_guid":"f56ab118-9bf7-44c0-a7ad-181b82b0a83a","trusted":true,"collapsed":false,"id":"vosZ4QtRqgj9","outputId":"fd3037f9-2bb1-4788-d431-08ae64abc079","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:13:59.539819Z","iopub.execute_input":"2026-01-21T19:13:59.540113Z","iopub.status.idle":"2026-01-21T19:14:00.644955Z","shell.execute_reply.started":"2026-01-21T19:13:59.540074Z","shell.execute_reply":"2026-01-21T19:14:00.644152Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom torchvision.models import resnet18, resnet50\n\nclass ResNet18Gray(nn.Module):\n    def __init__(self, num_classes=5, pretrained=False):\n        super().__init__()\n\n        # modèle connu\n        self.model = resnet18(weights=\"DEFAULT\" if pretrained else None)\n\n        # On adapte l'entrée à 1 canal\n        old_conv = self.model.conv1\n        self.model.conv1 = nn.Conv2d(\n            in_channels=1,\n            out_channels=old_conv.out_channels,\n            kernel_size=old_conv.kernel_size,\n            stride=old_conv.stride,\n            padding=old_conv.padding,\n            bias=False\n        )\n\n        # Si pretrained=True, il faut initialiser conv1 à partir des poids RGB\n        if pretrained:\n            with torch.no_grad():\n                self.model.conv1.weight[:] = old_conv.weight.mean(dim=1, keepdim=True)\n\n        # On adapte la sortie à 5 classes\n        in_features = self.model.fc.in_features\n        self.model.fc = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        return self.model(x)","metadata":{"_uuid":"e948599b-3387-48bd-ae1b-18c05d4415f7","_cell_guid":"2312757f-3b97-4e79-8bac-201094bf8188","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.646113Z","iopub.execute_input":"2026-01-21T19:14:00.646442Z","iopub.status.idle":"2026-01-21T19:14:00.653037Z","shell.execute_reply.started":"2026-01-21T19:14:00.646410Z","shell.execute_reply":"2026-01-21T19:14:00.652317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchsummary import summary\nimport torch\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Utilisation du device : {device}\")\n\ncnn = ResNet18Gray(num_classes=5, pretrained=False).to(device)\n\nx_test = torch.randn(64, 1, 224, 224).to(device)\n\nout = cnn(x_test)\nprint(f\"Input shape:  {x_test.shape}\")\nprint(f\"Output shape: {out.shape}\")\nprint(\"ResNet-50 grayscale chargé !\")\n\n# summary\nsummary(cnn, (1, 224, 224))","metadata":{"_uuid":"89456a46-a2dc-4940-a501-ab9125d0e129","_cell_guid":"5ee21f2d-12a8-450b-9d08-52574fba4a4d","trusted":true,"collapsed":false,"id":"LH6uWY13q0BP","outputId":"e8d0a619-9689-468c-ec1f-3713078da331","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.654064Z","iopub.execute_input":"2026-01-21T19:14:00.654596Z","iopub.status.idle":"2026-01-21T19:14:00.935788Z","shell.execute_reply.started":"2026-01-21T19:14:00.654573Z","shell.execute_reply":"2026-01-21T19:14:00.935071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Envoi sur le GPU\ncnn.to(device)","metadata":{"_uuid":"e6dd35aa-9846-4851-8677-46c15cc86d0a","_cell_guid":"be16fec6-ea36-4333-a89d-dedfa1e62fca","trusted":true,"collapsed":false,"id":"y0oI5FHPrAxr","outputId":"3662d0d6-be58-440c-98b4-7e2d624ba2a9","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.936776Z","iopub.execute_input":"2026-01-21T19:14:00.937053Z","iopub.status.idle":"2026-01-21T19:14:00.943929Z","shell.execute_reply.started":"2026-01-21T19:14:00.937019Z","shell.execute_reply":"2026-01-21T19:14:00.943353Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def accuracy(loader, model, device):\n    was_training = model.training\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for x, y in loader:\n            x = x.to(device)\n            y = y.to(device)\n            pred = model(x).argmax(1)\n            correct += (pred == y).sum().item()\n            total += y.size(0)\n    if was_training:\n        model.train()\n    return correct / total\n","metadata":{"_uuid":"e57beec8-7d70-4c55-ac40-c3891ca06c0a","_cell_guid":"9a29c357-bcdf-4117-8069-6b174268fb78","trusted":true,"collapsed":false,"id":"qLt8aqC4rlie","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.947111Z","iopub.execute_input":"2026-01-21T19:14:00.947667Z","iopub.status.idle":"2026-01-21T19:14:00.957166Z","shell.execute_reply.started":"2026-01-21T19:14:00.947644Z","shell.execute_reply":"2026-01-21T19:14:00.956422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\n\n\n# Calculer combien il y a d'exemples de chaque classe\nclass_counts = np.bincount(y_train_final)\ntotal_samples = len(y_train_final)\n\nprint(f\"Nombre d'images par classe : {class_counts}\")\n\n# Calcule de poids \n# Moins il y a d'images, plus le poids est lourd\n# poids inverses (plus la classe est rare, plus son poids est grand)\nw = 1.0 / (class_counts + 1e-12)\n\n# moyenne = 1 (stable)\nw = w / w.mean()\n\nweights_tensor = torch.tensor(w, dtype=torch.float32, device=device)\nprint(\"class_counts:\", class_counts)\nprint(\"weights_tensor:\", weights_tensor)\n\ncriterion = nn.CrossEntropyLoss(weight=weights_tensor)","metadata":{"_uuid":"8ce8f503-7d1c-4660-85e3-b435ed3af3a8","_cell_guid":"a5aa84e3-8e0a-4a78-b45b-ea4ffd072977","trusted":true,"collapsed":false,"id":"71F1fsNas3bL","outputId":"9fc97299-7754-4854-b88d-30c57570bbcf","scrolled":true,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.957955Z","iopub.execute_input":"2026-01-21T19:14:00.958132Z","iopub.status.idle":"2026-01-21T19:14:00.975907Z","shell.execute_reply.started":"2026-01-21T19:14:00.958115Z","shell.execute_reply":"2026-01-21T19:14:00.975168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nimport copy\n\n# Historiques pour les graphiques\nloss_train_history = []\nloss_val_history = []\nacc_train_history = []\nacc_val_history = []\nlr_history = []\n\nbest_val_loss = float(\"inf\")\nbest_val_acc = 0.0\n\nbest_loss_state = None\nbest_acc_state  = None\n\nno_improvement = 0\npatience = 8\nmin_delta = 1e-4\nepochs=45\n\n\nwarmup_lr = 5e-3\nbase_lr = 5e-4              #  LR\nwarmup_epochs = 1           # 1 epoch is enough\nwarmup_steps = warmup_epochs * len(train_loader)\n\nglobal_step = 0\n\noptimizer = torch.optim.Adam(cnn.parameters(), lr=base_lr)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode=\"min\",\n    factor=0.5,     # lr = lr * 0.5\n    patience=3,     # attend 2 epochs sans amélioration\n    min_lr=1e-6\n)\n\n\nprint(\"Démarrage de l'entraînement...\")\nstart_time = time.time()\n\nfor epoch in range(epochs):\n\n    cnn.train() # Mode entraînement\n    running_loss = 0.0\n    \n    \n    # Boucle d'entrainement\n    for i, data in enumerate(train_loader, 0):\n        # On récupère les données\n        inputs, labels = data[0].to(device), data[1].to(device)\n\n        if global_step < warmup_steps:\n            warmup_lr = base_lr * (global_step + 1) / warmup_steps\n            optimizer.param_groups[0][\"lr\"] = warmup_lr\n\n\n        global_step += 1\n        \n        # Remise à zéro des gradients\n        optimizer.zero_grad()\n\n        # Forward (Prédiction)\n        outputs = cnn(inputs)\n\n        # Calcul de la perte \n        loss = criterion(outputs, labels)\n\n        # Backward \n        loss.backward()\n\n        # Optimization\n        optimizer.step()\n\n        # Statistique\n        running_loss += loss.item()\n\n    # Calcul de la perte moyenne sur l'époque\n    epoch_loss = running_loss / len(train_loader)\n    loss_train_history.append(epoch_loss)\n\n        \n    # Boucle de validation\n    cnn.eval() \n    running_val_loss = 0.0\n\n    with torch.no_grad(): \n        for data in val_loader:\n            inputs, labels = data[0].to(device), data[1].to(device)\n            outputs = cnn(inputs)\n            val_loss = criterion(outputs, labels)\n            running_val_loss += val_loss.item()\n    \n    # Calcul de la perte moyenne de validation\n    epoch_val_loss = running_val_loss / len(val_loader)\n    loss_val_history.append(epoch_val_loss)\n    # après calcul epoch_val_loss\n    if epoch >= warmup_epochs:\n        scheduler.step(epoch_val_loss)\n\n    current_lr = optimizer.param_groups[0][\"lr\"]\n    print(\"lr =\", current_lr)\n    lr_history.append(current_lr)\n\n    #Early stoping\n    if epoch_val_loss < best_val_loss - min_delta:\n        best_val_loss = epoch_val_loss\n        no_improvement = 0\n        best_loss_state = copy.deepcopy(cnn.state_dict())\n\n    else:\n        no_improvement += 1\n\n    \n    # Calcul de l'accuracy\n    epoch_train_acc = accuracy(train_loader, cnn, device)\n    epoch_val_acc = accuracy(val_loader, cnn, device)\n        \n    acc_train_history.append(epoch_train_acc)\n    acc_val_history.append(epoch_val_acc)\n    # Best Accuracy\n    if epoch_val_acc > best_val_acc:\n        best_val_acc = epoch_val_acc\n        best_acc_state  = copy.deepcopy(cnn.state_dict())\n\n    print(f\"Epoch [{epoch+1}] \"\n          f\"Loss Train: {epoch_loss:.4f} | Loss Val: {epoch_val_loss:.4f} | \"\n          f\"Acc Train: {epoch_train_acc*100:.2f}% | Acc Val: {epoch_val_acc*100:.2f}%\")\n    # Balanced accuracy\n\n    if no_improvement >= patience:\n        print(f\"Early stopping à epoch {epoch+1} | best val_loss={best_val_loss:.4f}\")\n        break\n    \ntotal_time = time.time() - start_time\nprint(f'Entraînement terminé en {total_time/60:.0f} minutes')","metadata":{"_uuid":"90ccc0f1-6e25-40ca-974f-81ce691356d0","_cell_guid":"8a28914f-205c-4695-9511-98edd44e1020","trusted":true,"collapsed":false,"id":"jChrdEuccm1R","outputId":"a4ca63b2-dbf9-40aa-807a-3122fe95edf9","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:14:00.976780Z","iopub.execute_input":"2026-01-21T19:14:00.977030Z","iopub.status.idle":"2026-01-21T19:39:37.064434Z","shell.execute_reply.started":"2026-01-21T19:14:00.976999Z","shell.execute_reply":"2026-01-21T19:39:37.063445Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\n# Nombre réel d'epochs effectuées\nn_epochs_done = len(loss_train_history)\nepochs_axis = np.arange(1, n_epochs_done + 1)\n\n# accuracy\nplt.figure()\nplt.plot(epochs_axis, acc_train_history[:n_epochs_done], label=\"Train Acc\")\nplt.plot(epochs_axis, acc_val_history[:n_epochs_done], label=\"Val Acc\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.title(\"Accuracy over epochs\")\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# loss \nplt.figure()\nplt.plot(epochs_axis, loss_train_history[:n_epochs_done], label=\"Train Loss\")\nplt.plot(epochs_axis, loss_val_history[:n_epochs_done], label=\"Val Loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.title(\"Loss over epochs\")\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# learning rate\nplt.figure()\nplt.plot(epochs_axis, lr_history[:n_epochs_done], label=\"LR\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Learning Rate\")\nplt.title(\"Learning rate over epochs\")\nplt.legend()\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T19:39:37.065663Z","iopub.execute_input":"2026-01-21T19:39:37.065911Z","iopub.status.idle":"2026-01-21T19:39:37.453270Z","shell.execute_reply.started":"2026-01-21T19:39:37.065882Z","shell.execute_reply":"2026-01-21T19:39:37.452638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cnn.load_state_dict(best_acc_state)\nprint(f\"Loaded best ACC model = {best_val_acc*100:.2f}%\")","metadata":{"_uuid":"484d00c5-8937-40fe-8ea2-010c3e7ce2e3","_cell_guid":"2e114b64-5cd6-40ce-9a56-138182e8eace","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:37.454260Z","iopub.execute_input":"2026-01-21T19:39:37.454652Z","iopub.status.idle":"2026-01-21T19:39:37.463709Z","shell.execute_reply.started":"2026-01-21T19:39:37.454619Z","shell.execute_reply":"2026-01-21T19:39:37.463111Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\n# Récupérer toutes les prédictions du Val Set\ny_pred = []\ny_true = []\n\ncnn.eval() # Mode évaluation\nwith torch.no_grad():\n    for inputs, labels in val_loader:\n        inputs = inputs.to(device)\n        # Prédictions\n        outputs = cnn(inputs)\n        _, predicted = torch.max(outputs, 1)\n\n        y_pred.extend(predicted.cpu().numpy())\n        y_true.extend(labels.cpu().numpy())\n\nclasses = [\"Autre\", \"Mitochondrie\", \"Réticulum\", \"Mb. Nucléaire\", \"Mb. Cellulaire\"]\n\n# Créer la matrice\ncm = confusion_matrix(y_true, y_pred)\n\n# Affichage avec Seaborn\nplt.figure(figsize=(10, 8))\ndf_cm = pd.DataFrame(cm, index=classes, columns=classes)\n\n# Heatmap : Plus c'est foncé, plus il y a d'images\nsns.heatmap(df_cm, annot=True, fmt='d', cmap='Blues')\nplt.title('Matrice de Confusion : Où le modèle se trompe-t-il ?')\nplt.ylabel('Vraie classe (Label)')\nplt.xlabel('Classe prédite par le modèle')\nplt.show()","metadata":{"_uuid":"7a66cdfb-f0a9-43b8-a6ff-72881bde4a6a","_cell_guid":"63876b30-54be-460e-9771-949d9292abe0","trusted":true,"collapsed":false,"id":"BL11bciTvkLw","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:37.464611Z","iopub.execute_input":"2026-01-21T19:39:37.464911Z","iopub.status.idle":"2026-01-21T19:39:38.631546Z","shell.execute_reply.started":"2026-01-21T19:39:37.464879Z","shell.execute_reply":"2026-01-21T19:39:38.630786Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Sauvegarde des poids du modèle\ntorch.save(cnn.state_dict(), 'fisem_model_88acc.pth')\nprint(\"Modèle sauvegardé sous 'fisem_model_88acc.pth'\")","metadata":{"_uuid":"2d99f7f8-8dc9-4155-b205-c5fcfa609644","_cell_guid":"013e6cd3-c211-4577-9e63-d1cb33cf85a2","trusted":true,"collapsed":false,"id":"dGjUWBCm3dx1","jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:38.632836Z","iopub.execute_input":"2026-01-21T19:39:38.633109Z","iopub.status.idle":"2026-01-21T19:39:38.740993Z","shell.execute_reply.started":"2026-01-21T19:39:38.633081Z","shell.execute_reply":"2026-01-21T19:39:38.740313Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nfrom tqdm import tqdm\nimport re\n\n\ndest_dir = \"/kaggle/input/2025-competition-apprentissage-i-3-d/test/\"\n\ndef load_test_data_no_csv(img_dir):\n    # Liste tous les fichiers\n    all_files = os.listdir(img_dir)\n\n    # Filtre pour ne garder que les images\n    image_files = [f for f in all_files if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg'))]\n\n    # On trie pour avoir l'ordre 1, 2, 3\n    image_files.sort(key=lambda f: int(re.sub('\\D', '', f)) if re.search('\\d', f) else f)\n\n    data = []\n    ids = []\n\n    print(f\"Chargement de {len(image_files)} images depuis {img_dir}...\")\n\n    for filename in tqdm(image_files):\n        filepath = os.path.join(img_dir, filename)\n\n        try:\n            img = Image.open(filepath).convert('L')\n\n            img = img.resize((224, 224))\n\n            img_array = np.array(img)\n\n            try:\n                img_id = int(re.search(r'\\d+', filename).group())\n            except:\n                img_id = filename\n                \n            data.append(img_array)\n            ids.append(img_id)\n\n        except Exception as e:\n            print(f\"Erreur sur {filename}: {e}\")\n            continue\n\n    # On retourne les images et les ids\n    return np.array(data), np.array(ids)\n","metadata":{"_uuid":"e3b6a17b-5467-47ab-80b3-27b319b9e727","_cell_guid":"e3df6183-0f89-4b16-956b-0279056dd683","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:38.742097Z","iopub.execute_input":"2026-01-21T19:39:38.742744Z","iopub.status.idle":"2026-01-21T19:39:38.750563Z","shell.execute_reply.started":"2026-01-21T19:39:38.742716Z","shell.execute_reply":"2026-01-21T19:39:38.749884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n\n# check du path\nif os.path.exists(dest_dir):\n    print(f\"Chemin trouvé ! 'dest_dir' pointe vers : {dest_dir}\")\n    print(f\"Il contient {len(os.listdir(dest_dir))} images.\")\n    print(\"Lancement du chargement\")\n    X_test, test_ids = load_test_data_no_csv(dest_dir)\n    print(f\"Succès ! {X_test.shape[0]} images chargées.\")\nelse:\n    print(\"ERREUR : Le chemin est introuvable.\")","metadata":{"_uuid":"b8822410-7712-44ff-8a58-21ae133e189e","_cell_guid":"3b8e8cbd-30d2-4f85-bfc0-10d8204ed4e0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:38.751550Z","iopub.execute_input":"2026-01-21T19:39:38.751865Z","iopub.status.idle":"2026-01-21T19:39:58.984966Z","shell.execute_reply.started":"2026-01-21T19:39:38.751844Z","shell.execute_reply":"2026-01-21T19:39:58.984267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\n\nval_transforms = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)),    \n    transforms.ToTensor(),\n    transforms.Normalize([0.5], [0.5]),\n])\n\n\n#On redéfinit la classe Dataset\nclass TestDataset(Dataset):\n    def __init__(self, images, ids, transform=None):\n        self.images = images\n        self.ids = ids\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.images)\n\n    def __getitem__(self, idx):\n        img = self.images[idx]\n        id_val = self.ids[idx]\n        if self.transform:\n            img = self.transform(img)\n        return img, id_val\n\n# Creation du loader\nif 'X_test' in globals():\n    test_dataset_kaggle = TestDataset(X_test, test_ids, transform=val_transforms)\n\n    test_loader_kaggle = DataLoader(\n        test_dataset_kaggle, \n        batch_size=64, \n        shuffle=False,\n        num_workers=1\n    )\n    print(\"'test_loader_kaggle' crée.\")\nelse:\n    print(\" ERREUR : La variable X_test n'existe pas.\")\n","metadata":{"_uuid":"0d82a683-5661-4439-ba6b-5f543ed1e974","_cell_guid":"27640ead-89a1-4a6f-a2f9-0dbdb4da0b11","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:58.985981Z","iopub.execute_input":"2026-01-21T19:39:58.986235Z","iopub.status.idle":"2026-01-21T19:39:59.002327Z","shell.execute_reply.started":"2026-01-21T19:39:58.986213Z","shell.execute_reply":"2026-01-21T19:39:59.001580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F \n\n# modèle en mode évaluation\ncnn.eval()\n\nall_predictions = []\nall_ids = []\n\nprint(\" Lancement des prédictions avec TTA (Moyenne de 4 vues)...\")\n\nwith torch.no_grad():\n    for images, ids in tqdm(test_loader_kaggle):\n        images = images.to(device)\n        \n        # L'image originale\n        output_1 = cnn(images)\n        \n        # Miroir Horizontal\n        img_flip_h = torch.flip(images, [3]) \n        output_2 = cnn(img_flip_h)\n        \n        # Miroir Vertical\n        img_flip_v = torch.flip(images, [2])\n        output_3 = cnn(img_flip_v)\n        \n        #Rotation 180° \n        img_flip_hv = torch.flip(images, [2, 3])\n        output_4 = cnn(img_flip_hv)\n\n        # Moyenne\n        avg_output = (output_1 + output_2 + output_3 + output_4)\n\n        # On prend la classe gagnante sur la moyenne\n        _, predicted = torch.max(avg_output.data, 1)\n\n        # Stockage\n        all_predictions.extend(predicted.cpu().numpy())\n        all_ids.extend(ids)\n\nprint(\" Prédictions TTA terminées !\")\n\n\nsubmission_df = pd.DataFrame({\n    'ID': all_ids,\n    'Category': all_predictions\n})\n","metadata":{"_uuid":"36903501-adc3-424e-b2c5-95c5bd66596c","_cell_guid":"f1946d9d-e1b8-49a9-9607-20843ae25267","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:39:59.003314Z","iopub.execute_input":"2026-01-21T19:39:59.004002Z","iopub.status.idle":"2026-01-21T19:40:19.122805Z","shell.execute_reply.started":"2026-01-21T19:39:59.003950Z","shell.execute_reply":"2026-01-21T19:40:19.121819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nsubmission_df['ID'] = submission_df['ID'].apply(lambda x: x.item() if hasattr(x, 'item') else x)\n\nprint(submission_df.head())\n\n# On sauvegarde le fichier \nsubmission_df.to_csv('submission_clean.csv', index=False)\n","metadata":{"_uuid":"dcea110a-3147-4c09-ae19-e19c12537a79","_cell_guid":"407852e7-9b3b-4072-86e9-fbc61d3c9ad8","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-21T19:40:19.124120Z","iopub.execute_input":"2026-01-21T19:40:19.124393Z","iopub.status.idle":"2026-01-21T19:40:19.144710Z","shell.execute_reply.started":"2026-01-21T19:40:19.124363Z","shell.execute_reply":"2026-01-21T19:40:19.143894Z"}},"outputs":[],"execution_count":null}]}