{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":4104,"databundleVersionId":46661,"sourceType":"competition"},{"sourceId":7866129,"sourceType":"datasetVersion","datasetId":4614938},{"sourceId":7869237,"sourceType":"datasetVersion","datasetId":4617269}],"dockerImageVersionId":30673,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nfrom glob import glob\nimport matplotlib.pyplot as plt\nimport cv2","metadata":{"execution":{"iopub.status.busy":"2026-08-04T18:54:56.955135Z","iopub.execute_input":"2026-08-04T18:54:56.955966Z","iopub.status.idle":"2026-08-04T18:54:58.062926Z","shell.execute_reply.started":"2026-08-04T18:54:56.955923Z","shell.execute_reply":"2026-08-04T18:54:58.061903Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"file_lbl=\"/kaggle/input/diabetic-retinopathy-detection/trainLabels.csv.zip\"\ndf_train=pd.read_csv(file_lbl,sep=',')\ndf_train","metadata":{"execution":{"iopub.status.busy":"2026-08-04T18:55:00.510811Z","iopub.execute_input":"2026-08-04T18:55:00.511781Z","iopub.status.idle":"2026-08-04T18:55:00.563990Z","shell.execute_reply.started":"2026-08-04T18:55:00.511755Z","shell.execute_reply":"2026-08-04T18:55:00.563018Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_DIR = \"/kaggle/input/notebooks/luisguerezebanga/eda-demo-retinopathy/preprocessed_train\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-04T18:55:06.106423Z","iopub.execute_input":"2026-08-04T18:55:06.106931Z","iopub.status.idle":"2026-08-04T18:55:06.111715Z","shell.execute_reply.started":"2026-08-04T18:55:06.106905Z","shell.execute_reply":"2026-08-04T18:55:06.110815Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(len(os.listdir(TRAIN_DIR)))\nprint(os.listdir(TRAIN_DIR)[:5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-04T18:55:09.845676Z","iopub.execute_input":"2026-08-04T18:55:09.846031Z","iopub.status.idle":"2026-08-04T18:55:10.280741Z","shell.execute_reply.started":"2026-08-04T18:55:09.846006Z","shell.execute_reply":"2026-08-04T18:55:10.279860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 🏆 Rétinopathie Diabétique — v2 (corrigée)\n# EfficientNetV2-S + CORN + Focal + Mixup (cohérent) + EMA + AMP\n# + optimisation de seuils post-hoc (QWK) + early stopping\n# Objectif : Quadratic Weighted Kappa ≈ 0.80–0.82\n# ============================================================\n\nimport os, cv2, copy, torch, numpy as np, pandas as pd, random\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom timm import create_model\nfrom sklearn.metrics import cohen_kappa_score\nfrom sklearn.model_selection import train_test_split\nfrom scipy.optimize import minimize\nfrom tqdm import tqdm\nfrom torch.cuda.amp import GradScaler, autocast\nfrom torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR\nimport warnings\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\n\n# --- 1. SEED & CONFIG ---------------------------------------------------------\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nseed_everything(42)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nIMG_SIZE = 384\nBATCH_SIZE = 24\nEPOCHS = 25              # augmenté (early stopping arrêtera avant si besoin)\nPATIENCE = 6             # early stopping sur le Kappa val\nBASE_LR = 1.5e-4\nNUM_CLASSES = 5\nEMA_DECAY = 0.998\nLABEL_SMOOTH = 0.05\nMIXUP_ALPHA = 0.2\nMIXUP_PROB = 0.5         # n'applique le mixup qu'une fois sur deux (stabilité)\n\nTRAIN_DIR = \"/kaggle/input/notebooks/luisguerezebanga/eda-demo-retinopathy/preprocessed_train\"   # sortie de ton notebook EDA/prétraitement\nCSV_PATH = \"/kaggle/input/diabetic-retinopathy-detection/trainLabels.csv.zip\"\nCKPT_PATH = \"/kaggle/working/best_model_final.pth\"\n\nprint(f\"[INFO] Device : {DEVICE} | IMG_SIZE : {IMG_SIZE}x{IMG_SIZE}\")\n\n# --- 2. DATASET --------------------------------------------------------------\nclass DRDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = os.path.join(self.img_dir, f\"{row['image']}.jpeg\")\n        img = cv2.imread(img_path)\n        if img is None:\n            raise ValueError(f\"Image corrompue : {img_path}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        if self.transform:\n            img = self.transform(img)\n        return img, row['level']\n\n# --- 3. AUGMENTATIONS (renforcées vs. v1) ------------------------------------\n# Les fonds d'œil n'ont pas d'orientation canonique -> rotation large.\n# ColorJitter compense la variabilité de qualité d'acquisition (déjà partiellement\n# corrigée par CLAHE en amont, mais utile pour la robustesse du modèle).\ntrain_tf = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.RandomResizedCrop(IMG_SIZE, scale=(0.85, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomVerticalFlip(),\n    transforms.RandomRotation(25),\n    transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.1),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\nval_tf = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406],\n                         [0.229, 0.224, 0.225])\n])\n\n# --- 4. DATA LOADING ---------------------------------------------------------\ndf = pd.read_csv(CSV_PATH)\navailable = {f.replace('.jpeg', '') for f in os.listdir(TRAIN_DIR)}\ndf = df[df['image'].isin(available)].reset_index(drop=True)\n\ndf_train, df_val = train_test_split(\n    df, test_size=0.1, random_state=42, stratify=df['level']\n)\n\ntrain_loader = DataLoader(\n    DRDataset(df_train, TRAIN_DIR, train_tf),\n    batch_size=BATCH_SIZE, shuffle=True,\n    num_workers=2, pin_memory=True, persistent_workers=False\n)\n\nval_loader = DataLoader(\n    DRDataset(df_val, TRAIN_DIR, val_tf),\n    batch_size=BATCH_SIZE, shuffle=False,\n    num_workers=2, pin_memory=True, persistent_workers=False\n)\n\n# --- 5. CLASS WEIGHTS ---------------------------------------------------------\nclass_counts = df_train['level'].value_counts().sort_index().values\nclass_weights = torch.tensor(1.0 / (class_counts + 1e-5), dtype=torch.float32)\nclass_weights = class_weights / class_weights.sum() * NUM_CLASSES\nclass_weights = class_weights.to(DEVICE)\n\n# --- 6. LOSSES ---------------------------------------------------------------\ndef corn_label_matrix(labels, num_classes):\n    \"\"\"Construit la cible binaire ordinale CORN pour un batch de labels entiers.\"\"\"\n    label_matrix = torch.zeros((labels.size(0), num_classes - 1), device=labels.device)\n    for i in range(num_classes - 1):\n        label_matrix[:, i] = (labels > i).float()\n    return label_matrix\n\nclass WeightedFocalLoss(nn.Module):\n    def __init__(self, alpha=None, gamma=1.5, label_smoothing=0.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.label_smoothing = label_smoothing\n    def forward(self, inputs, targets):\n        ce = F.cross_entropy(inputs, targets, weight=self.alpha,\n                              label_smoothing=self.label_smoothing, reduction='none')\n        pt = torch.exp(-ce)\n        return ((1 - pt) ** self.gamma * ce).mean()\n\nfocal_loss_fn = WeightedFocalLoss(alpha=class_weights, gamma=1.5, label_smoothing=LABEL_SMOOTH)\n\n# --- 7. MIXUP (cohérent avec CORN) -------------------------------------------\ndef mixup_data(x, y, alpha=0.2):\n    lam = np.random.beta(alpha, alpha)\n    index = torch.randperm(x.size(0)).to(x.device)\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    return mixed_x, y, y[index], lam\n\n# --- 8. EMA --------------------------------------------------------------\nclass ModelEMA:\n    \"\"\"Moyenne mobile exponentielle des poids — stabilise le Kappa d'une epoch\n    à l'autre par rapport au pic ponctuel d'un seul checkpoint.\"\"\"\n    def __init__(self, model, decay=0.998):\n        self.ema = copy.deepcopy(model).eval()\n        for p in self.ema.parameters():\n            p.requires_grad_(False)\n        self.decay = decay\n    @torch.no_grad()\n    def update(self, model):\n        for ema_p, p in zip(self.ema.state_dict().values(), model.state_dict().values()):\n            if ema_p.dtype.is_floating_point:\n                ema_p.mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)\n            else:\n                ema_p.copy_(p)\n\n# --- 9. MODEL ---------------------------------------------------------------\nclass EffNetCORN(nn.Module):\n    def __init__(self, num_classes=5, drop_p=0.35):\n        super().__init__()\n        self.backbone = create_model(\"tf_efficientnetv2_s\",\n                                     pretrained=True,\n                                     num_classes=num_classes)\n        in_features = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Sequential(\n            nn.Dropout(p=drop_p),\n            nn.Linear(in_features, num_classes)\n        )\n        self.corn_head = nn.Sequential(\n            nn.Dropout(p=drop_p),\n            nn.Linear(in_features, num_classes - 1)\n        )\n    def forward(self, x):\n        f = self.backbone.forward_features(x)\n        p = self.backbone.global_pool(f)\n        logits = self.backbone.classifier(p)\n        corn_logits = self.corn_head(p)\n        return logits, corn_logits\n\nmodel = EffNetCORN(NUM_CLASSES).to(DEVICE)\nema = ModelEMA(model, decay=EMA_DECAY)\noptimizer = torch.optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=1e-2)\n\nwarmup = LinearLR(optimizer, start_factor=0.1, total_iters=2)\ncosine = CosineAnnealingLR(optimizer, T_max=EPOCHS - 2, eta_min=1e-5)\nscheduler = SequentialLR(optimizer, schedulers=[warmup, cosine], milestones=[2])\n\nscaler = GradScaler()\n\n# --- 10. INFÉRENCE : score continu CORN + seuils optimisés -------------------\ndef corn_to_score(corn_logits):\n    \"\"\"Score ordinal continu = somme des probabilités binaires (0 à 4).\"\"\"\n    return torch.sigmoid(corn_logits).sum(dim=1)\n\ndef collect_scores(net, loader):\n    net.eval()\n    scores, labels = [], []\n    with torch.no_grad():\n        for imgs, lbls in loader:\n            imgs = imgs.to(DEVICE)\n            _, corn_logits = net(imgs)\n            scores.extend(corn_to_score(corn_logits).cpu().numpy())\n            labels.extend(lbls.numpy())\n    return np.array(scores), np.array(labels)\n\ndef optimize_thresholds(scores, labels, init=(0.5, 1.5, 2.5, 3.5)):\n    def neg_kappa(th):\n        th = np.sort(th)\n        preds = np.digitize(scores, th)\n        return -cohen_kappa_score(labels, preds, weights='quadratic')\n    result = minimize(neg_kappa, np.array(init), method='Nelder-Mead')\n    return np.sort(result.x), -result.fun\n\ndef apply_thresholds(scores, thresholds):\n    return np.digitize(scores, thresholds)\n\n# --- 11. TRAINING LOOP --------------------------------------------------------\nbest_kappa = -1\nepochs_no_improve = 0\nbest_thresholds = np.array([0.5, 1.5, 2.5, 3.5])\n\nprint(\"\\n🚀 Entraînement v2...\")\n\ntry:\n    for epoch in range(EPOCHS):\n        model.train()\n        running_loss = 0.0\n        optimizer.zero_grad()\n\n        print(f\"\\n🔹 Epoch {epoch+1}/{EPOCHS} | LR={optimizer.param_groups[0]['lr']:.6f}\")\n\n        for imgs, lbls in tqdm(train_loader, desc=f\"Training Epoch {epoch+1}\"):\n            imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)\n\n            use_mixup = np.random.rand() < MIXUP_PROB\n\n            with autocast():\n                if use_mixup:\n                    imgs_in, lbls_a, lbls_b, lam = mixup_data(imgs, lbls, alpha=MIXUP_ALPHA)\n                    logits, corn_logits = model(imgs_in)\n\n                    ce = lam * focal_loss_fn(logits, lbls_a) + (1 - lam) * focal_loss_fn(logits, lbls_b)\n\n                    target_a = corn_label_matrix(lbls_a, NUM_CLASSES)\n                    target_b = corn_label_matrix(lbls_b, NUM_CLASSES)\n                    corn_loss = lam * F.binary_cross_entropy_with_logits(corn_logits, target_a) \\\n                              + (1 - lam) * F.binary_cross_entropy_with_logits(corn_logits, target_b)\n                else:\n                    logits, corn_logits = model(imgs)\n                    ce = focal_loss_fn(logits, lbls)\n                    target = corn_label_matrix(lbls, NUM_CLASSES)\n                    corn_loss = F.binary_cross_entropy_with_logits(corn_logits, target)\n\n                loss = ce + 0.5 * corn_loss\n\n            scaler.scale(loss).backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n            ema.update(model)\n\n            running_loss += loss.item()\n\n        avg_loss = running_loss / len(train_loader)\n\n        # --- validation avec le modèle EMA (plus stable que le modèle brut) ---\n        scores, labels = collect_scores(ema.ema, val_loader)\n        thresholds, val_kappa = optimize_thresholds(scores, labels, init=best_thresholds)\n\n        scheduler.step()\n        print(f\"Loss = {avg_loss:.4f} | Val Kappa (EMA, seuils optimisés) = {val_kappa:.4f}\")\n\n        if val_kappa > best_kappa:\n            best_kappa = val_kappa\n            best_thresholds = thresholds\n            epochs_no_improve = 0\n            torch.save({\n                \"model_state_dict\": ema.ema.state_dict(),\n                \"thresholds\": best_thresholds,\n                \"kappa\": best_kappa,\n            }, CKPT_PATH)\n            print(f\"🔥 Nouveau meilleur modèle sauvegardé (Kappa={best_kappa:.4f}) | seuils={np.round(best_thresholds,3)}\")\n        else:\n            epochs_no_improve += 1\n            print(f\"⏳ Pas d'amélioration depuis {epochs_no_improve} epoch(s)\")\n            if epochs_no_improve >= PATIENCE:\n                print(f\"\\n🛑 Early stopping déclenché (patience={PATIENCE})\")\n                break\n\n    print(f\"\\n🏁 Entraînement terminé ! Meilleur Kappa = {best_kappa:.4f}\")\n    print(f\"Seuils optimaux : {best_thresholds}\")\n\nexcept Exception as e:\n    print(f\"\\n❌ Erreur critique : {e}\")\n    torch.save(model.state_dict(), \"/kaggle/working/model_crash.pth\")\n    raise e","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-04T18:56:44.633607Z","iopub.execute_input":"2026-08-04T18:56:44.634025Z","iopub.status.idle":"2026-08-04T21:42:48.433919Z","shell.execute_reply.started":"2026-08-04T18:56:44.633997Z","shell.execute_reply":"2026-08-04T21:42:48.432636Z"}},"outputs":[],"execution_count":null}]}