{"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":"# ============================================================\n# 🚀 TRAINING PIPELINE — v6 (final)\n# EfficientNetV2-S + CORN + Focal + Mixup (cohérent) + EMA + AMP\n# + seuils optimisés (QWK) + early stopping + suivi détaillé du temps\n# Prétraitement Ben Graham déjà appliqué sur disque (preprocess_train_v3.py)\n# ============================================================\n\nimport os, copy, time, torch, numpy as np, pandas as pd, cv2, 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.model_selection import train_test_split, StratifiedKFold\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# ⚠️ Nom du Utility Script Kaggle réellement attaché à ce notebook.\n# Vérifie avant de lancer : print(open(preprocess_utils.__file__).read())\n# doit bien afficher corn_label_matrix / corn_to_score / optimize_thresholds.\nfrom preprocess_utils import corn_label_matrix, corn_to_score, optimize_thresholds, apply_thresholds, NUM_CLASSES\n\n# --- OUTIL DE TIMING -------------------------------------------------------\ndef log_step(msg):\n    print(f\"\\n[{time.strftime('%H:%M:%S')}] {msg}\")\n\nclass Timer:\n    \"\"\"Context manager : print automatique du temps écoulé à la sortie du bloc.\"\"\"\n    def __init__(self, label):\n        self.label = label\n    def __enter__(self):\n        self.t0 = time.time()\n        print(f\"[{time.strftime('%H:%M:%S')}] ▶️  {self.label}...\")\n        return self\n    def __exit__(self, *args):\n        dt = time.time() - self.t0\n        print(f\"[{time.strftime('%H:%M:%S')}] ✅ {self.label} terminé en {dt:.1f}s ({dt/60:.1f} min)\")\n\n# --- CONFIG ------------------------------------------------------------\ndef seed_everything(seed=42):\n    random.seed(seed); os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed); torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.benchmark = True\nseed_everything(42)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nIMG_SIZE = 384\nBATCH_SIZE = 32\nEPOCHS = 20\nPATIENCE = 5\nBASE_LR = 1.5e-4\nEMA_DECAY = 0.998\nLABEL_SMOOTH = 0.05\nMIXUP_ALPHA = 0.2\nMIXUP_PROB = 0.5\nNUM_WORKERS = 4\n\nTRAIN_DIR = \"/kaggle/input/notebooks/luisguerezebanga/preprocess-train/preprocessed_train\"\n# ⚠️ Remplace <NOM-DE-TON-DATASET> par le slug exact — visible dans le panneau\n# \"Input\" à droite une fois le dataset ajouté (Add Input), ou en survolant le\n# dossier pour copier son chemin complet. Vérifie aussi que le sous-dossier\n# s'appelle bien \"preprocessed_train\" à l'intérieur du dataset (parfois Kaggle\n# imbrique un niveau de dossier supplémentaire selon comment tu l'as publié).\nCSV_PATH = \"/kaggle/input/diabetic-retinopathy-detection/trainLabels.csv.zip\"\nCKPT_PATH = \"/kaggle/working/best_model_final.pth\"\n\nUSE_KFOLD = False\nN_FOLDS = 3\n\nlog_step(f\"CONFIG | Device={DEVICE} | USE_KFOLD={USE_KFOLD} | BATCH_SIZE={BATCH_SIZE} | IMG_SIZE={IMG_SIZE}\")\n\n# --- AUGMENTATIONS -----------------------------------------------------\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], [0.229, 0.224, 0.225])\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], [0.229, 0.224, 0.225])\n])\n\nclass DRDataset(Dataset):\n    def __init__(self, df, img_dir, transform):\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 = cv2.imread(os.path.join(self.img_dir, f\"{row['image']}.jpeg\"))\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        return self.transform(img), row['level']\n\n# --- LOSSES ----------------------------------------------------------------\nclass WeightedFocalLoss(nn.Module):\n    def __init__(self, alpha=None, gamma=1.5, label_smoothing=0.0):\n        super().__init__()\n        self.alpha, self.gamma, self.label_smoothing = alpha, gamma, 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\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    return lam * x + (1 - lam) * x[index, :], y, y[index], lam\n\nclass ModelEMA:\n    def __init__(self, model, decay=0.998):\n        self.ema = copy.deepcopy(model).eval()\n        for p in self.ema.parameters(): p.requires_grad_(False)\n        self.decay = decay\n    @torch.no_grad()\n    def update(self, model):\n        for ep, p in zip(self.ema.state_dict().values(), model.state_dict().values()):\n            if ep.dtype.is_floating_point:\n                ep.mul_(self.decay).add_(p.detach(), alpha=1 - self.decay)\n            else:\n                ep.copy_(p)\n\n# --- MODEL -------------------------------------------------------------------\nclass EffNetCORN(nn.Module):\n    def __init__(self, num_classes=NUM_CLASSES, drop_p=0.35):\n        super().__init__()\n        self.backbone = create_model(\"tf_efficientnetv2_s\", pretrained=True, num_classes=num_classes)\n        in_features = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Sequential(nn.Dropout(drop_p), nn.Linear(in_features, num_classes))\n        self.corn_head = nn.Sequential(nn.Dropout(drop_p), nn.Linear(in_features, num_classes - 1))\n    def forward(self, x):\n        f = self.backbone.forward_features(x)\n        p = self.backbone.global_pool(f)\n        return self.backbone.classifier(p), self.corn_head(p)\n\ndef collect_scores(net, loader):\n    net.eval()\n    scores, labels = [], []\n    with torch.no_grad():\n        for imgs, lbls in loader:\n            _, corn_logits = net(imgs.to(DEVICE))\n            scores.extend(corn_to_score(corn_logits).cpu().numpy())\n            labels.extend(lbls.numpy())\n    return np.array(scores), np.array(labels)\n\n# --- BOUCLE D'ENTRAÎNEMENT ---------------------------------------------------\ndef train_one_split(df_train, df_val, tag=\"single\"):\n    with Timer(f\"[{tag}] Préparation (class weights, dataloaders, modèle)\"):\n        class_counts = df_train['level'].value_counts().sort_index().values\n        class_weights = torch.tensor(1.0 / (class_counts + 1e-5), dtype=torch.float32)\n        class_weights = (class_weights / class_weights.sum() * NUM_CLASSES).to(DEVICE)\n        focal_loss_fn = WeightedFocalLoss(alpha=class_weights, gamma=1.5, label_smoothing=LABEL_SMOOTH)\n\n        train_loader = DataLoader(DRDataset(df_train, TRAIN_DIR, train_tf), batch_size=BATCH_SIZE,\n                                   shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\n        val_loader = DataLoader(DRDataset(df_val, TRAIN_DIR, val_tf), batch_size=BATCH_SIZE,\n                                 shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n\n        model = EffNetCORN(NUM_CLASSES).to(DEVICE)\n        ema = ModelEMA(model, decay=EMA_DECAY)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=BASE_LR, weight_decay=1e-2)\n        warmup = LinearLR(optimizer, start_factor=0.1, total_iters=2)\n        cosine = CosineAnnealingLR(optimizer, T_max=EPOCHS - 2, eta_min=1e-5)\n        scheduler = SequentialLR(optimizer, schedulers=[warmup, cosine], milestones=[2])\n        scaler = GradScaler()\n\n    log_step(f\"[{tag}] {len(df_train)} images train | {len(df_val)} images val | \"\n              f\"{len(train_loader)} itérations/epoch\")\n\n    best_kappa, epochs_no_improve = -1, 0\n    best_thresholds = np.array([0.5, 1.5, 2.5, 3.5])\n    split_t0 = time.time()\n\n    for epoch in range(EPOCHS):\n        epoch_t0 = time.time()\n        log_step(f\"[{tag}] === Epoch {epoch+1}/{EPOCHS} | LR={optimizer.param_groups[0]['lr']:.6f} ===\")\n\n        # --- phase entraînement ---\n        train_t0 = time.time()\n        model.train()\n        running_loss = 0.0\n        for imgs, lbls in tqdm(train_loader, desc=f\"[{tag}] Epoch {epoch+1} - train\"):\n            imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)\n            use_mixup = np.random.rand() < MIXUP_PROB\n            with autocast():\n                if use_mixup:\n                    imgs_in, lbls_a, lbls_b, lam = mixup_data(imgs, lbls, MIXUP_ALPHA)\n                    logits, corn_logits = model(imgs_in)\n                    ce = lam * focal_loss_fn(logits, lbls_a) + (1 - lam) * focal_loss_fn(logits, lbls_b)\n                    ta, tb = corn_label_matrix(lbls_a), corn_label_matrix(lbls_b)\n                    corn_loss = lam * F.binary_cross_entropy_with_logits(corn_logits, ta) \\\n                              + (1 - lam) * F.binary_cross_entropy_with_logits(corn_logits, tb)\n                else:\n                    logits, corn_logits = model(imgs)\n                    ce = focal_loss_fn(logits, lbls)\n                    corn_loss = F.binary_cross_entropy_with_logits(corn_logits, corn_label_matrix(lbls))\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); scaler.update(); optimizer.zero_grad()\n            ema.update(model)\n            running_loss += loss.item()\n        train_dt = time.time() - train_t0\n        avg_loss = running_loss / len(train_loader)\n        print(f\"[{time.strftime('%H:%M:%S')}] ✅ [{tag}] Phase train terminée en {train_dt:.1f}s \"\n              f\"({train_dt/60:.1f} min) | Loss={avg_loss:.4f}\")\n\n        # --- phase validation ---\n        val_t0 = time.time()\n        scores, labels = collect_scores(ema.ema, val_loader)\n        val_dt = time.time() - val_t0\n        print(f\"[{time.strftime('%H:%M:%S')}] ✅ [{tag}] Phase validation (inférence) terminée en {val_dt:.1f}s\")\n\n        # --- optimisation des seuils ---\n        th_t0 = time.time()\n        thresholds, val_kappa = optimize_thresholds(scores, labels, init=best_thresholds)\n        th_dt = time.time() - th_t0\n        print(f\"[{time.strftime('%H:%M:%S')}] ✅ [{tag}] Optimisation des seuils terminée en {th_dt:.2f}s | \"\n              f\"Kappa={val_kappa:.4f}\")\n\n        scheduler.step()\n\n        if val_kappa > best_kappa:\n            best_kappa, best_thresholds, epochs_no_improve = val_kappa, thresholds, 0\n            save_t0 = time.time()\n            torch.save({\"model_state_dict\": ema.ema.state_dict(),\n                        \"thresholds\": best_thresholds, \"kappa\": best_kappa, \"tag\": tag}, CKPT_PATH)\n            print(f\"[{time.strftime('%H:%M:%S')}] 🔥 [{tag}] Nouveau meilleur modèle sauvegardé \"\n                  f\"(Kappa={best_kappa:.4f}) en {time.time()-save_t0:.1f}s\")\n        else:\n            epochs_no_improve += 1\n            print(f\"[{time.strftime('%H:%M:%S')}] ⏳ [{tag}] Pas d'amélioration depuis {epochs_no_improve} epoch(s)\")\n\n        epoch_dt = time.time() - epoch_t0\n        elapsed_total = time.time() - split_t0\n        print(f\"[{time.strftime('%H:%M:%S')}] ⏱️  [{tag}] Epoch {epoch+1} : {epoch_dt:.1f}s total \"\n              f\"| Cumulé depuis début du split : {elapsed_total/60:.1f} min\")\n\n        if epochs_no_improve >= PATIENCE:\n            log_step(f\"[{tag}] 🛑 Early stopping déclenché (patience={PATIENCE})\")\n            break\n\n    split_dt = time.time() - split_t0\n    log_step(f\"[{tag}] Split terminé en {split_dt/60:.1f} min | Meilleur Kappa={best_kappa:.4f}\")\n    return best_kappa, best_thresholds\n\n# --- EXÉCUTION -----------------------------------------------------------\nif __name__ == \"__main__\":\n    run_t0 = time.time()\n    log_step(\"DÉBUT DU RUN\")\n\n    with Timer(\"Chargement CSV + filtrage des images disponibles\"):\n        df = pd.read_csv(CSV_PATH)\n        available = {f.replace('.jpeg', '') for f in os.listdir(TRAIN_DIR)}\n        df = df[df['image'].isin(available)].reset_index(drop=True)\n    log_step(f\"{len(df)} images disponibles sur disque après filtrage\")\n\n    if not USE_KFOLD:\n        with Timer(\"Split train/val stratifié\"):\n            df_train, df_val = train_test_split(df, test_size=0.1, random_state=42, stratify=df['level'])\n        kappa, thresholds = train_one_split(df_train, df_val, tag=\"split\")\n        log_step(f\"🏁 RUN TERMINÉ | Kappa={kappa:.4f} | seuils={thresholds} | \"\n                  f\"Durée totale={(time.time()-run_t0)/60:.1f} min\")\n    else:\n        skf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=42)\n        results = []\n        for fold, (tr_idx, va_idx) in enumerate(skf.split(df['image'], df['level']), 1):\n            kappa, thresholds = train_one_split(df.iloc[tr_idx], df.iloc[va_idx], tag=f\"fold{fold}\")\n            results.append((fold, kappa, thresholds))\n        for fold, kappa, thresholds in results:\n            print(f\"Fold {fold}: Kappa={kappa:.4f}\")\n        log_step(f\"🏁 RUN TERMINÉ | Kappa moyen={np.mean([k for _, k, _ in results]):.4f} | \"\n                  f\"Durée totale={(time.time()-run_t0)/60:.1f} min\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T16:38:58.131851Z","iopub.execute_input":"2026-08-09T16:38:58.132314Z","iopub.status.idle":"2026-08-09T18:55:43.843993Z","shell.execute_reply.started":"2026-08-09T16:38:58.132265Z","shell.execute_reply":"2026-08-09T18:55:43.842734Z"}},"outputs":[],"execution_count":null}]}