{"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":14774,"databundleVersionId":875431,"sourceType":"competition"},{"sourceId":13479521,"sourceType":"datasetVersion","datasetId":8557821}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# BLOC : MODÈLE HYBRIDE ResNet50 + DeiT-Basic (entraînement end-to-end)\nimport torch\nimport torch.nn as nn\nfrom torchvision import models\nimport timm\n\nclass HybridResNet50_DeitBasic(nn.Module):\n    def __init__(self, num_classes=5):\n        super().__init__()\n        # 1. ResNet50 (features locaux)\n        self.resnet = models.resnet50(weights='IMAGENET1K_V1')\n        self.resnet.fc = nn.Identity()  # Supprime la couche finale → sortie [B, 2048]\n        \n        # 2. DeiT-Basic (contexte global)\n        # DeiT-Basic = deit_base_patch16_224\n        self.deit = timm.create_model('deit_base_patch16_224', pretrained=True, num_classes=0)  # [B, 768]\n        \n        # 3. Fusion simple (concaténation)\n        self.fusion = nn.Linear(2048 + 768, 512)\n        self.head = nn.Sequential(\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n    \n    def forward(self, x):\n        # Extraire features ResNet50\n        res_feat = self.resnet(x)  # [B, 2048]\n        \n        # Extraire token [CLS] de DeiT (déjà 1D car num_classes=0)\n        deit_feat = self.deit(x)   # [B, 768]\n        \n        # Fusion\n        fused = torch.cat([res_feat, deit_feat], dim=1)  # [B, 2816]\n        return self.head(self.fusion(fused))  # [B, 5]\n\n# Test rapide\nmodel = HybridResNet50_DeitBasic()\nx = torch.randn(2, 3, 224, 224)\nprint(\"✅ Shape sortie :\", model(x).shape)  # [2, 5]\nprint(\"✅ Nombre de paramètres :\", sum(p.numel() for p in model.parameters()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-30T20:03:44.238607Z","iopub.execute_input":"2026-01-30T20:03:44.238829Z","iopub.status.idle":"2026-01-30T20:03:46.439389Z","shell.execute_reply.started":"2026-01-30T20:03:44.238811Z","shell.execute_reply":"2026-01-30T20:03:46.438681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# BLOC 2 : DATASET + DATALOADERS POUR RESNET50 + DEIT-BASIC\n\nfrom pathlib import Path\nimport pandas as pd\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nimport torch\n\n# Chemins\nTRAIN_DIR = Path(\"/kaggle/input/aptos-2019/aptos2019/processed/train\")\nVAL_DIR = Path(\"/kaggle/input/aptos-2019/aptos2019/processed/validation\")\nCSV_PATH = \"/kaggle/input/aptos2019-blindness-detection/train.csv\"\n\n# Dataset\nclass APTOSDataset(Dataset):\n    def __init__(self, img_dir, csv_path, transform=None):\n        self.img_dir = Path(img_dir)\n        self.transform = transform\n        df = pd.read_csv(csv_path)\n        self.samples = df[['id_code', 'diagnosis']].values.tolist()\n    \n    def __len__(self):\n        return len(self.samples)\n    \n    def __getitem__(self, idx):\n        img_name, label = self.samples[idx]\n        img_path = self.img_dir / f\"{img_name}.png\"\n        if not img_path.exists():\n            img_path = Path(\"/kaggle/input/aptos2019-blindness-detection/train_images\") / f\"{img_name}.png\"\n        image = Image.open(img_path).convert('RGB')\n        if self.transform:\n            image = self.transform(image)\n        return image, torch.tensor(label, dtype=torch.long)\n\n# Transforms (224x224)\nfrom torchvision import transforms\ntrain_tf = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomRotation(10),\n    transforms.ColorJitter(0.1, 0.1),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\nval_tf = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Créer les datasets\ntrain_ds = APTOSDataset(TRAIN_DIR, CSV_PATH, transform=train_tf)\nval_ds = APTOSDataset(VAL_DIR, CSV_PATH, transform=val_tf)\n\n# DataLoader (batch_size réduit pour modèle lourd)\ntrain_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_ds, batch_size=8, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(\"✅ Datasets et DataLoaders prêts\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-30T20:03:58.276928Z","iopub.execute_input":"2026-01-30T20:03:58.277218Z","iopub.status.idle":"2026-01-30T20:03:58.567148Z","shell.execute_reply.started":"2026-01-30T20:03:58.277196Z","shell.execute_reply":"2026-01-30T20:03:58.566519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel = HybridResNet50_DeitBasic().to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)  # lr plus faible (modèle lourd)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)\n\nbest_acc = 0.0\nprint(\"🚀 Entraînement ResNet50 + DeiT-Basic (end-to-end)...\")\n\nfor epoch in range(20):\n    model.train()\n    running_loss = 0.0\n    \n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        optimizer.step()\n        running_loss += loss.item()\n    \n    # Validation\n    model.eval()\n    correct = total = 0\n    with torch.no_grad():\n        for imgs, labels in val_loader:\n            imgs, labels = imgs.to(device), labels.to(device)\n            preds = model(imgs).argmax(1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n    \n    val_acc = correct / total\n    scheduler.step()\n    \n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save(model.state_dict(), '/kaggle/working/resnet50_deit_basic_best.pth')\n        print(f\"⭐ Nouveau meilleur modèle ! Val Acc: {val_acc:.4f}\")\n    \n    print(f\"Époque {epoch+1}/20 | Loss: {running_loss/len(train_loader):.4f} | Val Acc: {val_acc:.4f}\")\n\nprint(f\"\\n✅ Entraînement terminé. Meilleure précision : {best_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-30T20:04:05.621008Z","iopub.execute_input":"2026-01-30T20:04:05.621287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Passer en mode évaluation\nmodel.eval()\n\n# Collecter toutes les prédictions\nall_preds, all_labels = [], []\nwith torch.no_grad():\n    for imgs, labels in val_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        preds = model(imgs).argmax(1)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\n# Afficher le rapport\nfrom sklearn.metrics import classification_report\n\nprint(\"\\n📊 RAPPORT DE CLASSIFICATION – Modèle Hybride ResNet50 + DeiT-Basic\")\nprint(classification_report(\n    all_labels,\n    all_preds,\n    target_names=['0-No DR', '1-Mild', '2-Moderate', '3-Severe', '4-Proliferative'],\n    digits=4\n))\n\n# Précision globale (optionnel)\nimport numpy as np\nfinal_acc = np.mean(np.array(all_preds) == np.array(all_labels))\nprint(f\"\\n🎯 Précision globale : {final_acc:.4f} ({100 * final_acc:.2f}%)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T21:04:17.075149Z","iopub.execute_input":"2026-01-14T21:04:17.075944Z","iopub.status.idle":"2026-01-14T21:08:32.303592Z","shell.execute_reply.started":"2026-01-14T21:04:17.075902Z","shell.execute_reply":"2026-01-14T21:08:32.302514Z"}},"outputs":[],"execution_count":null}]}