{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"title","cell_type":"markdown","source":"# Apprentissage profond par ensemble, interprétable et prenant en compte l'incertitude pour la détection multi-étiquettes des hémorragies intracrâniennes sur scanner cérébral sans injection — ICH (Sekkat Hamza.)\n\n**Réimplémentation complète** : ResNet-50 × Deep Ensemble × Grad-CAM × UQ  \nDonnées : RSNA Intracranial Hemorrhage Detection (Kaggle)  \n\n| Paramètre article | Valeur article | Valeur notebook |\n|---|---|---|\n| ensemble_size | 5 | 3 (rapide) → mettre 5 |\n| n_folds | 3 | 2 (rapide) → mettre 3 |\n| max_epochs | 30 | 5 (rapide) → mettre 30 |\n| image_size | 224 | 224 |\n| max_studies | 752 803 | 200 (sous-échantillon) |\n\n> ⚠️ **Session → Restart & Run All** avant d'exécuter.","metadata":{}},{"id":"s0","cell_type":"markdown","source":"## 0. Installation et imports","metadata":{}},{"id":"imports","cell_type":"code","source":"import sys, subprocess, os, math, random, json\nfrom dataclasses import dataclass\nfrom typing import Tuple\n\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"pydicom\"], check=False)\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport pydicom\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import resnet50, ResNet50_Weights\nfrom sklearn.metrics import roc_auc_score, average_precision_score\nfrom sklearn.model_selection import GroupKFold\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Torch\", torch.__version__, \"| device:\", DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T12:56:17.392765Z","iopub.execute_input":"2026-06-28T12:56:17.393422Z"}},"outputs":[],"execution_count":null},{"id":"s1","cell_type":"markdown","source":"## 1. Configuration","metadata":{}},{"id":"config","cell_type":"code","source":"LABELS = [\"epidural\", \"intraparenchymal\", \"intraventricular\",\n          \"subarachnoid\", \"subdural\", \"any\"]\nN_LABELS = len(LABELS)\n\n@dataclass\nclass Config:\n    image_size: int = 224\n    # Table 1 de l'article : brain / subdural / bone\n    windows: Tuple[Tuple[int, int], ...] = ((40, 80), (80, 200), (600, 2800))\n    ensemble_size: int = 5    # article = 5\n    n_folds: int = 3          # article = 3\n    max_epochs: int = 30      # article = 30\n    batch_size: int = 64      # 2x GPU T4\n    lr: float = 1e-4\n    weight_decay: float = 1e-4\n    patience_es: int = 5\n    num_workers: int = 4\n    seed: int = 42\n    pretrained: bool = True\n    max_studies: int = 10000  # toutes les données stage_2\n    ece_bins: int = 15\n\nCFG = Config()\n\ndef set_seed(s):\n    random.seed(s); np.random.seed(s)\n    torch.manual_seed(s); torch.cuda.manual_seed_all(s)\nset_seed(CFG.seed)\nprint(\"Config OK\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:22.211208Z","iopub.execute_input":"2026-06-28T03:48:22.211422Z","iopub.status.idle":"2026-06-28T03:48:22.220664Z","shell.execute_reply.started":"2026-06-28T03:48:22.211402Z","shell.execute_reply":"2026-06-28T03:48:22.219939Z"}},"outputs":[],"execution_count":null},{"id":"99130b52-d56e-4c0f-8094-6b01ce4a0b43","cell_type":"code","source":"import torch\nprint(f\"GPUs disponibles : {torch.cuda.device_count()}\")\nfor i in range(torch.cuda.device_count()):\n    print(f\"  GPU {i} : {torch.cuda.get_device_name(i)}\")\nprint(f\"DEVICE : {DEVICE}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:22.221723Z","iopub.execute_input":"2026-06-28T03:48:22.22225Z","iopub.status.idle":"2026-06-28T03:48:22.232231Z","shell.execute_reply.started":"2026-06-28T03:48:22.222227Z","shell.execute_reply":"2026-06-28T03:48:22.23162Z"}},"outputs":[],"execution_count":null},{"id":"s2","cell_type":"markdown","source":"## 2. Détection automatique des chemins RSNA","metadata":{}},{"id":"paths","cell_type":"code","source":"def find_rsna_paths():\n    candidates = [\n        \"/kaggle/input/rsna-intracranial-hemorrhage-detection\",\n        \"/kaggle/input/competitions/rsna-intracranial-hemorrhage-detection\",\n        \"/kaggle/input/competitions/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection\",\n    ]\n    for base in candidates:\n        if not os.path.exists(base): continue\n        for root, dirs, files in os.walk(base):\n            for f in files:\n                if \"stage_2_train\" in f and f.endswith(\".csv\"):\n                    csv = os.path.join(root, f)\n                    dcm_dir = os.path.join(root, \"stage_2_train\")\n                    if not os.path.exists(dcm_dir): dcm_dir = root\n                    print(f\"CSV      : {csv}\")\n                    print(f\"DICOM dir: {dcm_dir}\")\n                    return csv, dcm_dir\n    raise FileNotFoundError(\"Dataset RSNA introuvable. Ajoute-le via + Add Data.\")\n\nCSV_PATH, DICOM_DIR = find_rsna_paths()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:22.233733Z","iopub.execute_input":"2026-06-28T03:48:22.234028Z","iopub.status.idle":"2026-06-28T03:48:22.24596Z","shell.execute_reply.started":"2026-06-28T03:48:22.234007Z","shell.execute_reply":"2026-06-28T03:48:22.245188Z"}},"outputs":[],"execution_count":null},{"id":"s3","cell_type":"markdown","source":"## 3. Chargement du manifest RSNA","metadata":{}},{"id":"manifest","cell_type":"code","source":"from concurrent.futures import ThreadPoolExecutor\n\ndef build_rsna_manifest(csv_path, dicom_dir, max_studies=CFG.max_studies):\n    raw = pd.read_csv(csv_path)\n    raw[[\"pre\", \"img\", \"sub\"]] = raw[\"ID\"].str.rsplit(\"_\", n=2, expand=True)\n    raw[\"img\"] = raw[\"pre\"] + \"_\" + raw[\"img\"]\n    wide = raw.pivot_table(index=\"img\", columns=\"sub\", values=\"Label\").reset_index()\n    # Sous-échantillonnage avant lecture\n    wide = wide.sample(min(max_studies * 3, len(wide)), random_state=CFG.seed).reset_index(drop=True)\n    \n    def check_exists(r):\n        p = os.path.join(dicom_dir, r[\"img\"] + \".dcm\")\n        if not os.path.exists(p): return None\n        return {\"path\": p, \"study_id\": r[\"img\"][:10],\n                **{st: int(r.get(st, 0)) for st in LABELS}}\n    \n    with ThreadPoolExecutor(max_workers=8) as ex:\n        results = list(ex.map(check_exists, [wide.iloc[i] for i in range(len(wide))]))\n    \n    rows = [r for r in results if r is not None]\n    man = pd.DataFrame(rows)\n    keep = man[\"study_id\"].drop_duplicates().sample(\n        min(max_studies, man[\"study_id\"].nunique()), random_state=CFG.seed)\n    return man[man[\"study_id\"].isin(keep)].reset_index(drop=True)\n\ndf = build_rsna_manifest(CSV_PATH, DICOM_DIR)\nprint(\"Manifest:\", df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:22.246909Z","iopub.execute_input":"2026-06-28T03:48:22.247587Z","iopub.status.idle":"2026-06-28T03:48:56.974522Z","shell.execute_reply.started":"2026-06-28T03:48:22.247563Z","shell.execute_reply":"2026-06-28T03:48:56.973804Z"}},"outputs":[],"execution_count":null},{"id":"s4","cell_type":"markdown","source":"## 4. Prétraitement : HU → 3 fenêtres (Table 1)","metadata":{}},{"id":"preproc","cell_type":"code","source":"def window_image(hu, center, width):\n    low, high = center - width/2.0, center + width/2.0\n    return ((np.clip(hu, low, high) - low) / (high - low + 1e-8)).astype(np.float32)\n\ndef dicom_to_hu(path):\n    ds = pydicom.dcmread(path)\n    arr = ds.pixel_array.astype(np.float32)\n    slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n    intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n    return arr * slope + intercept\n\ndef load_hu(path):\n    return np.load(path).astype(np.float32) if path.endswith(\".npy\") else dicom_to_hu(path)\n\ndef make_three_window(hu, cfg):\n    return np.stack([window_image(hu, c, w) for (c, w) in cfg.windows], axis=-1)\n\n# Aperçu\nex = load_hu(df.iloc[int(np.argmax(df[\"any\"].values))][\"path\"])\nimg3 = make_three_window(ex, CFG)\nfig, ax = plt.subplots(1, 4, figsize=(13, 3.2))\nax[0].imshow(ex, cmap=\"gray\"); ax[0].set_title(\"HU brut\")\nfor i, name in enumerate([\"brain (40/80)\", \"subdural (80/200)\", \"bone (600/2800)\"]):\n    ax[i+1].imshow(img3[..., i], cmap=\"gray\"); ax[i+1].set_title(name)\nfor a in ax: a.axis(\"off\")\nplt.suptitle(\"Table 1 — CT window settings\", fontsize=11, fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:56.975575Z","iopub.execute_input":"2026-06-28T03:48:56.976251Z","iopub.status.idle":"2026-06-28T03:48:57.335448Z","shell.execute_reply.started":"2026-06-28T03:48:56.976203Z","shell.execute_reply":"2026-06-28T03:48:57.334855Z"}},"outputs":[],"execution_count":null},{"id":"s5","cell_type":"markdown","source":"## 5. Dataset PyTorch","metadata":{}},{"id":"dataset","cell_type":"code","source":"class ICHDataset(Dataset):\n    def __init__(self, frame, cfg, train=False):\n        self.df = frame.reset_index(drop=True); self.cfg = cfg; self.train = train\n    def __len__(self): return len(self.df)\n    def __getitem__(self, i):\n        row = self.df.iloc[i]\n        hu = load_hu(row[\"path\"])\n        img = make_three_window(hu, self.cfg)\n        t = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0)\n        t = F.interpolate(t, size=(self.cfg.image_size, self.cfg.image_size),\n                          mode=\"bilinear\", align_corners=False).squeeze(0)\n        if self.train and random.random() < 0.5:   # flip horizontal (article §2.2)\n            t = torch.flip(t, dims=[2])\n        y = torch.tensor([float(row[l]) for l in LABELS], dtype=torch.float32)\n        return t, y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:57.336341Z","iopub.execute_input":"2026-06-28T03:48:57.336658Z","iopub.status.idle":"2026-06-28T03:48:57.343549Z","shell.execute_reply.started":"2026-06-28T03:48:57.336625Z","shell.execute_reply":"2026-06-28T03:48:57.342881Z"}},"outputs":[],"execution_count":null},{"id":"s6","cell_type":"markdown","source":"## 6. Modèle ResNet-50 multi-label (§2.4)","metadata":{}},{"id":"model","cell_type":"code","source":"class ResNet50MultiLabel(nn.Module):\n    def __init__(self, n_labels=N_LABELS, pretrained=True):\n        super().__init__()\n        w = ResNet50_Weights.IMAGENET1K_V2 if pretrained else None\n        self.backbone = resnet50(weights=w)\n        self.backbone.fc = nn.Linear(self.backbone.fc.in_features, n_labels)\n    def forward(self, x): return self.backbone(x)\n\ndef compute_pos_weights(frame, any_boost=1.5):\n    w = []\n    for l in LABELS:\n        pos = max(int(frame[l].sum()), 1); neg = len(frame) - pos\n        w.append(neg / pos)\n    w = torch.tensor(w, dtype=torch.float32)\n    w[LABELS.index(\"any\")] *= any_boost\n    return w","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:57.344425Z","iopub.execute_input":"2026-06-28T03:48:57.344737Z","iopub.status.idle":"2026-06-28T03:48:57.355915Z","shell.execute_reply.started":"2026-06-28T03:48:57.344716Z","shell.execute_reply":"2026-06-28T03:48:57.355202Z"}},"outputs":[],"execution_count":null},{"id":"s7","cell_type":"markdown","source":"## 7. Entraînement d'un membre (§2.4)","metadata":{}},{"id":"train","cell_type":"code","source":"def safe_auc(y, p):\n    return roc_auc_score(y, p) if len(np.unique(y)) > 1 else float(\"nan\")\n\ndef train_member(train_df, val_df, cfg, seed):\n    set_seed(seed)\n    model = ResNet50MultiLabel(pretrained=cfg.pretrained).to(DEVICE)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=compute_pos_weights(train_df).to(DEVICE))\n    opt = torch.optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\n    sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, mode=\"min\", factor=0.5, patience=1)\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=(DEVICE == \"cuda\"))\n    tl = DataLoader(ICHDataset(train_df, cfg, True), batch_size=cfg.batch_size,\n                    shuffle=True, num_workers=cfg.num_workers, pin_memory=True)\n    vl = DataLoader(ICHDataset(val_df, cfg, False), batch_size=cfg.batch_size,\n                    shuffle=False, num_workers=cfg.num_workers, pin_memory=True)\n    best_auc, best_state, no_imp = -1, None, 0\n    history = {\"train_loss\": [], \"val_loss\": [], \"val_auc\": [], \"val_ece\": []}\n    for ep in range(cfg.max_epochs):\n        model.train(); tloss = 0; nb = 0\n        for x, y in tl:\n            x, y = x.to(DEVICE), y.to(DEVICE)\n            opt.zero_grad(set_to_none=True)\n            with torch.amp.autocast(\"cuda\", enabled=(DEVICE == \"cuda\")):\n                loss = criterion(model(x), y)\n            scaler.scale(loss).backward(); scaler.step(opt); scaler.update()\n            tloss += loss.item() * x.size(0); nb += x.size(0)\n        model.eval(); vloss, P, G = 0, [], []\n        with torch.no_grad():\n            for x, y in vl:\n                x, y = x.to(DEVICE), y.to(DEVICE)\n                lo = model(x); vloss += criterion(lo, y).item() * x.size(0)\n                P.append(torch.sigmoid(lo).cpu().numpy()); G.append(y.cpu().numpy())\n        vloss /= len(vl.dataset); P = np.concatenate(P); G = np.concatenate(G)\n        macro = np.nanmean([safe_auc(G[:, i], P[:, i]) for i in range(N_LABELS)])\n        ece_val = np.nanmean([ece(P[:, i], G[:, i], cfg.ece_bins) for i in range(N_LABELS)])\n        sched.step(vloss)\n        history[\"train_loss\"].append(tloss / nb)\n        history[\"val_loss\"].append(vloss)\n        history[\"val_auc\"].append(macro)\n        history[\"val_ece\"].append(ece_val)\n        print(f\"  ep{ep+1}/{cfg.max_epochs}  train_loss={tloss/nb:.3f}  \"\n              f\"val_loss={vloss:.3f}  macroAUC={macro:.3f}  ECE={ece_val:.3f}\")\n        if macro > best_auc:\n            best_auc, no_imp = macro, 0\n            best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}\n        else:\n            no_imp += 1\n            if no_imp >= cfg.patience_es: break\n    model.load_state_dict(best_state)\n    return model, history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:57.356928Z","iopub.execute_input":"2026-06-28T03:48:57.357254Z","iopub.status.idle":"2026-06-28T03:48:57.371849Z","shell.execute_reply.started":"2026-06-28T03:48:57.357228Z","shell.execute_reply":"2026-06-28T03:48:57.371101Z"}},"outputs":[],"execution_count":null},{"id":"s8","cell_type":"markdown","source":"## 8. Métriques UQ complètes (§2.5)","metadata":{}},{"id":"metrics","cell_type":"code","source":"# ── Entropie binaire ────────────────────────────────────────────────────────\ndef binary_entropy(p):\n    p = np.clip(p, 1e-7, 1-1e-7)\n    return -(p*np.log(p) + (1-p)*np.log(1-p))\n\n# ── Eq.1 : moyenne, Eq.2 : information mutuelle ────────────────────────────\ndef ensemble_uncertainty(member_probs):\n    mean_p = member_probs.mean(0)                   # Eq.1\n    std_p  = member_probs.std(0)\n    pred_H = binary_entropy(mean_p)                 # H(p̄) — incertitude totale\n    exp_H  = binary_entropy(member_probs).mean(0)   # E[H(p_m)] — aléatoire\n    mi     = pred_H - exp_H                         # Eq.2 — epistémique\n    margin = 1.0 - np.abs(2*mean_p - 1.0)\n    return {\"mean\": mean_p, \"std\": std_p,\n            \"predictive_entropy\": pred_H,\n            \"expected_entropy\": exp_H,\n            \"mutual_information\": mi,\n            \"margin\": margin}\n\n# ── Calibration ────────────────────────────────────────────────────────────\ndef calibration_bins(p, y, bins=15):\n    edges = np.linspace(0, 1, bins+1)\n    conf_bins, acc_bins, cnt_bins = [], [], []\n    for i in range(bins):\n        m = (p > edges[i]) & (p <= edges[i+1])\n        if m.sum():\n            conf_bins.append(p[m].mean())\n            acc_bins.append(y[m].mean())\n            cnt_bins.append(m.sum())\n        else:\n            conf_bins.append((edges[i]+edges[i+1])/2)\n            acc_bins.append(float(\"nan\"))\n            cnt_bins.append(0)\n    return np.array(conf_bins), np.array(acc_bins), np.array(cnt_bins)\n\ndef ece(p, y, bins=15):\n    edges = np.linspace(0, 1, bins+1); e = 0\n    for i in range(bins):\n        m = (p > edges[i]) & (p <= edges[i+1])\n        if m.sum(): e += m.mean() * abs(y[m].mean() - p[m].mean())\n    return float(e)\n\ndef mce(p, y, bins=15):\n    \"\"\"Maximum Calibration Error\"\"\"\n    edges = np.linspace(0, 1, bins+1); errs = []\n    for i in range(bins):\n        m = (p > edges[i]) & (p <= edges[i+1])\n        if m.sum(): errs.append(abs(y[m].mean() - p[m].mean()))\n    return float(max(errs)) if errs else 0.0\n\ndef brier(p, y): return float(np.mean((p-y)**2))\n\ndef nll(p, y):\n    p = np.clip(p, 1e-7, 1-1e-7)\n    return float(-np.mean(y*np.log(p) + (1-y)*np.log(1-p)))\n\n# ── Métriques seuil ────────────────────────────────────────────────────────\ndef threshold_metrics(p, y, thr):\n    pred = (p >= thr).astype(int)\n    tp = int(((pred==1)&(y==1)).sum()); tn = int(((pred==0)&(y==0)).sum())\n    fp = int(((pred==1)&(y==0)).sum()); fn = int(((pred==0)&(y==1)).sum())\n    se = tp/(tp+fn+1e-8); sp = tn/(tn+fp+1e-8); pr = tp/(tp+fp+1e-8)\n    f1 = 2*pr*se/(pr+se+1e-8)\n    return {\"sensitivity\": se, \"specificity\": sp, \"precision\": pr, \"f1\": f1,\n            \"tp\": tp, \"tn\": tn, \"fp\": fp, \"fn\": fn}\n\ndef best_threshold(p, y, grid=np.linspace(0.05, 0.95, 19)):\n    return float(grid[int(np.argmax([threshold_metrics(p, y, t)[\"f1\"] for t in grid]))])\n\n# ── Temperature scaling par classe (§2.5) ──────────────────────────────────\ndef fit_temperature(logits, targets):\n    T = torch.ones(N_LABELS, requires_grad=True)\n    opt = torch.optim.LBFGS([T], lr=0.05, max_iter=200)\n    bce = nn.BCEWithLogitsLoss()\n    def closure():\n        opt.zero_grad(); l = bce(logits/T.clamp(min=1e-2), targets); l.backward(); return l\n    opt.step(closure)\n    return T.detach().clamp(min=1e-2)\n\nprint(\"Métriques OK\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:57.374017Z","iopub.execute_input":"2026-06-28T03:48:57.37427Z","iopub.status.idle":"2026-06-28T03:48:57.391555Z","shell.execute_reply.started":"2026-06-28T03:48:57.374248Z","shell.execute_reply":"2026-06-28T03:48:57.390769Z"}},"outputs":[],"execution_count":null},{"id":"s9","cell_type":"markdown","source":"## 9. Boucle principale : CV study-level × deep ensemble","metadata":{}},{"id":"experiment","cell_type":"code","source":"def collect_logits(model, loader):\n    model.eval(); L, Y = [], []\n    with torch.no_grad():\n        for x, y in loader:\n            L.append(model(x.to(DEVICE)).cpu().numpy()); Y.append(y.numpy())\n    return np.concatenate(L), np.concatenate(Y)\n\ndef run_experiment(df, cfg):\n    gkf = GroupKFold(n_splits=cfg.n_folds)\n    folds = []; store = {}; all_histories = []\n    for fold, (trv, te) in enumerate(gkf.split(df, groups=df[\"study_id\"].values)):\n        print(f\"\\n{'='*50}\")\n        print(f\"FOLD {fold}\")\n        print(f\"{'='*50}\")\n        trv_df, test_df = df.iloc[trv], df.iloc[te]\n        inner = GroupKFold(n_splits=4)\n        itr, ival = next(inner.split(trv_df, groups=trv_df[\"study_id\"].values))\n        train_df, val_df = trv_df.iloc[itr], trv_df.iloc[ival]\n        vl = DataLoader(ICHDataset(val_df, cfg, False), batch_size=cfg.batch_size,\n                        shuffle=False, num_workers=cfg.num_workers)\n        tl = DataLoader(ICHDataset(test_df, cfg, False), batch_size=cfg.batch_size,\n                        shuffle=False, num_workers=cfg.num_workers)\n        members, vP, tP, fold_hist = [], [], [], []\n        vY = tY = None\n        for m in range(cfg.ensemble_size):\n            print(f\"\\n-- Membre {m+1}/{cfg.ensemble_size} --\")\n            mod, hist = train_member(train_df, val_df, cfg,\n                                     seed=cfg.seed + 1000*fold + m)\n            members.append(mod); fold_hist.append(hist)\n            vl_logits, vY = collect_logits(mod, vl)\n            T = fit_temperature(torch.tensor(vl_logits), torch.tensor(vY)).numpy()\n            vP.append(1/(1+np.exp(-(vl_logits/T))))\n            tl_logits, tY = collect_logits(mod, tl)\n            tP.append(1/(1+np.exp(-(tl_logits/T))))\n        vP, tP = np.stack(vP), np.stack(tP)  # (M, N, C)\n        vuq, tuq = ensemble_uncertainty(vP), ensemble_uncertainty(tP)\n        thr = [best_threshold(vuq[\"mean\"][:, c], vY[:, c]) for c in range(N_LABELS)]\n        per = {}\n        for c, lab in enumerate(LABELS):\n            p, y = tuq[\"mean\"][:, c], tY[:, c]\n            per[lab] = {\n                \"roc_auc\": safe_auc(y, p),\n                \"ap\": average_precision_score(y, p) if y.sum() else float(\"nan\"),\n                \"ece\": ece(p, y, cfg.ece_bins),\n                \"mce\": mce(p, y, cfg.ece_bins),\n                \"brier\": brier(p, y),\n                \"nll\": nll(p, y),\n                \"thr\": thr[c],\n                **threshold_metrics(p, y, thr[c])\n            }\n        macro = np.nanmean([per[l][\"roc_auc\"] for l in LABELS])\n        print(f\"\\nFold {fold} macro ROC-AUC = {macro:.4f}\")\n        folds.append({\"fold\": fold, \"macro\": macro, \"per\": per})\n        all_histories.append(fold_hist)\n        store = {\"members\": members, \"test_df\": test_df,\n                 \"tuq\": tuq, \"tY\": tY, \"thr\": thr,\n                 \"vP\": vP, \"tP\": tP, \"vY\": vY}\n    return folds, store, all_histories\n\nFOLDS, STORE, HISTORIES = run_experiment(df, CFG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T03:48:57.39257Z","iopub.execute_input":"2026-06-28T03:48:57.392936Z","iopub.status.idle":"2026-06-28T05:17:35.816291Z","shell.execute_reply.started":"2026-06-28T03:48:57.392902Z","shell.execute_reply":"2026-06-28T05:17:35.815286Z"}},"outputs":[],"execution_count":null},{"id":"s10","cell_type":"markdown","source":"## 10. Fig. 3 — Convergence : train/val loss, macro AUC, macro ECE","metadata":{}},{"id":"fig3","cell_type":"code","source":"fig, axes = plt.subplots(1, 4, figsize=(16, 4))\nkeys = [\"train_loss\", \"val_loss\", \"val_auc\", \"val_ece\"]\ntitles = [\"A. Training loss\", \"B. Validation loss\",\n          \"C. Validation macro ROC-AUC\", \"D. Validation macro ECE\"]\nfor ax, key, title in zip(axes, keys, titles):\n    all_curves = []\n    for fold_hist in HISTORIES:\n        for h in fold_hist:\n            vals = h[key]\n            ax.plot(range(1, len(vals)+1), vals, color=\"steelblue\", alpha=0.3, lw=0.8)\n            all_curves.append(vals)\n    # Moyenne\n    min_len = min(len(c) for c in all_curves)\n    mat = np.array([c[:min_len] for c in all_curves])\n    ax.plot(range(1, min_len+1), mat.mean(0), color=\"darkred\", lw=2, label=\"Mean\")\n    ax.fill_between(range(1, min_len+1),\n                    mat.mean(0)-mat.std(0),\n                    mat.mean(0)+mat.std(0), alpha=0.15, color=\"darkred\")\n    ax.set_title(title, fontsize=10, fontweight=\"bold\")\n    ax.set_xlabel(\"Epoch\"); ax.legend(fontsize=8)\nplt.suptitle(\"Fig 3. Convergence et stabilité de l'ensemble\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:35.81788Z","iopub.execute_input":"2026-06-28T05:17:35.818127Z","iopub.status.idle":"2026-06-28T05:17:36.372762Z","shell.execute_reply.started":"2026-06-28T05:17:35.818099Z","shell.execute_reply":"2026-06-28T05:17:36.372159Z"}},"outputs":[],"execution_count":null},{"id":"s11","cell_type":"markdown","source":"## 11. Résultats agrégés — Table 2 & Table 3","metadata":{}},{"id":"results","cell_type":"code","source":"def aggregate(folds):\n    out = {}\n    for lab in LABELS:\n        for met in [\"roc_auc\", \"ap\", \"ece\", \"mce\", \"brier\", \"nll\",\n                    \"sensitivity\", \"specificity\", \"precision\", \"f1\"]:\n            vals = [f[\"per\"][lab][met] for f in folds]\n            vals = [v for v in vals if not (isinstance(v, float) and math.isnan(v))]\n            out.setdefault(lab, {})[met] = (\n                np.mean(vals) if vals else float(\"nan\"),\n                np.std(vals) if vals else float(\"nan\"))\n    return out\n\nAGG = aggregate(FOLDS)\nmacro = [f[\"macro\"] for f in FOLDS]\nprint(f\"Macro ROC-AUC = {np.mean(macro):.4f} ± {np.std(macro):.4f}\\n\")\n\n# Table 2\ntbl2 = pd.DataFrame({\n    lab: {\n        \"ROC-AUC\": f\"{AGG[lab]['roc_auc'][0]:.4f}±{AGG[lab]['roc_auc'][1]:.4f}\",\n        \"AP\":      f\"{AGG[lab]['ap'][0]:.4f}±{AGG[lab]['ap'][1]:.4f}\",\n    } for lab in LABELS}).T\nprint(\"=== Table 2 : Discrimination ===\")\ndisplay(tbl2)\n\n# Table 3\ntbl3 = pd.DataFrame({\n    lab: {\n        \"Sensitivity\": f\"{AGG[lab]['sensitivity'][0]:.3f}±{AGG[lab]['sensitivity'][1]:.3f}\",\n        \"Specificity\": f\"{AGG[lab]['specificity'][0]:.3f}±{AGG[lab]['specificity'][1]:.3f}\",\n        \"Precision\":   f\"{AGG[lab]['precision'][0]:.3f}±{AGG[lab]['precision'][1]:.3f}\",\n        \"F1\":          f\"{AGG[lab]['f1'][0]:.3f}±{AGG[lab]['f1'][1]:.3f}\",\n        \"ECE\":         f\"{AGG[lab]['ece'][0]:.4f}±{AGG[lab]['ece'][1]:.4f}\",\n        \"MCE\":         f\"{AGG[lab]['mce'][0]:.4f}±{AGG[lab]['mce'][1]:.4f}\",\n        \"Brier\":       f\"{AGG[lab]['brier'][0]:.4f}±{AGG[lab]['brier'][1]:.4f}\",\n        \"NLL\":         f\"{AGG[lab]['nll'][0]:.4f}±{AGG[lab]['nll'][1]:.4f}\",\n    } for lab in LABELS}).T\nprint(\"\\n=== Table 3 : Threshold + Calibration ===\")\ndisplay(tbl3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:36.37395Z","iopub.execute_input":"2026-06-28T05:17:36.374288Z","iopub.status.idle":"2026-06-28T05:17:36.399829Z","shell.execute_reply.started":"2026-06-28T05:17:36.374264Z","shell.execute_reply":"2026-06-28T05:17:36.399029Z"}},"outputs":[],"execution_count":null},{"id":"s12","cell_type":"markdown","source":"## 12. Fig. 8 — Calibration : ECE, Brier, NLL","metadata":{}},{"id":"fig8","cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))\nmetrics_cal = [(\"ece\", \"A. Expected calibration error\", \"ECE\"),\n               (\"brier\", \"B. Brier score\", \"Brier score\"),\n               (\"nll\", \"C. Negative log-likelihood\", \"NLL\")]\nx = np.arange(N_LABELS)\nfor ax, (met, title, ylabel) in zip(axes, metrics_cal):\n    means = [AGG[l][met][0] for l in LABELS]\n    stds  = [AGG[l][met][1] for l in LABELS]\n    bars = ax.bar(x, means, yerr=stds, capsize=4, color=\"steelblue\", alpha=0.8)\n    ax.set_xticks(x); ax.set_xticklabels(\n        [l.replace(\"intra\", \"\\nintra\") for l in LABELS], fontsize=8, rotation=0)\n    ax.set_title(title, fontsize=10, fontweight=\"bold\")\n    ax.set_ylabel(ylabel)\nplt.suptitle(\"Fig 8. Calibration et scoring probabiliste\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:36.401247Z","iopub.execute_input":"2026-06-28T05:17:36.401446Z","iopub.status.idle":"2026-06-28T05:17:36.764109Z","shell.execute_reply.started":"2026-06-28T05:17:36.401427Z","shell.execute_reply":"2026-06-28T05:17:36.763527Z"}},"outputs":[],"execution_count":null},{"id":"s13","cell_type":"markdown","source":"## 13. Reliability diagrams (diagrammes de fiabilité) — §2.7","metadata":{}},{"id":"reliability","cell_type":"code","source":"tuq, tY, thr = STORE[\"tuq\"], STORE[\"tY\"], STORE[\"thr\"]\n\nfig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p, y = tuq[\"mean\"][:, c], tY[:, c]\n    conf_b, acc_b, cnt_b = calibration_bins(p, y, CFG.ece_bins)\n    # Perfect calibration\n    ax.plot([0, 1], [0, 1], \"k--\", lw=1, label=\"Perfect\")\n    # Gap (shaded)\n    mask = ~np.isnan(acc_b)\n    ax.bar(conf_b[mask], acc_b[mask], width=1/CFG.ece_bins,\n           alpha=0.5, color=\"steelblue\", label=\"Accuracy\")\n    ax.bar(conf_b[mask], conf_b[mask], width=1/CFG.ece_bins,\n           alpha=0.3, color=\"orange\", label=\"Confidence\")\n    ece_v = AGG[lab][\"ece\"][0]\n    mce_v = AGG[lab][\"mce\"][0]\n    ax.set_title(f\"{lab}\\nECE={ece_v:.4f}  MCE={mce_v:.4f}\",\n                 fontsize=9, fontweight=\"bold\")\n    ax.set_xlabel(\"Confidence\"); ax.set_ylabel(\"Accuracy\")\n    ax.set_xlim(0, 1); ax.set_ylim(0, 1)\n    ax.legend(fontsize=7)\nplt.suptitle(\"Reliability diagrams — calibration par étiquette\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:36.76502Z","iopub.execute_input":"2026-06-28T05:17:36.765412Z","iopub.status.idle":"2026-06-28T05:17:37.737471Z","shell.execute_reply.started":"2026-06-28T05:17:36.765379Z","shell.execute_reply":"2026-06-28T05:17:37.736587Z"}},"outputs":[],"execution_count":null},{"id":"s14","cell_type":"markdown","source":"## 14. Fig. 9 — Désaccord ensemble vs probabilité prédite","metadata":{}},{"id":"fig9","cell_type":"code","source":"fig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p_mean = tuq[\"mean\"][:, c]\n    p_std  = tuq[\"std\"][:, c]\n    y      = tY[:, c]\n    # Négatifs (violet) / Positifs (or)\n    ax.scatter(p_mean[y==0], p_std[y==0], s=2, alpha=0.3, c=\"purple\", label=\"Neg\")\n    ax.scatter(p_mean[y==1], p_std[y==1], s=2, alpha=0.3, c=\"gold\", label=\"Pos\")\n    ax.set_xlabel(\"Mean ensemble probability\")\n    ax.set_ylabel(\"Ensemble probability std\")\n    ax.set_title(lab, fontsize=9, fontweight=\"bold\")\n    ax.legend(fontsize=7, markerscale=3)\nplt.suptitle(\"Fig 9. Désaccord ensemble vs probabilité prédite\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:37.738586Z","iopub.execute_input":"2026-06-28T05:17:37.738927Z","iopub.status.idle":"2026-06-28T05:17:38.772087Z","shell.execute_reply.started":"2026-06-28T05:17:37.738903Z","shell.execute_reply":"2026-06-28T05:17:38.771445Z"}},"outputs":[],"execution_count":null},{"id":"s15","cell_type":"markdown","source":"## 15. Fig. 10 — Entropie prédictive : correct vs erreur","metadata":{}},{"id":"fig10","cell_type":"code","source":"fig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p = tuq[\"mean\"][:, c]; y = tY[:, c]\n    H = tuq[\"predictive_entropy\"][:, c]\n    pred = (p >= thr[c]).astype(int)\n    correct = pred == y\n    bins = np.linspace(0, 0.7, 20)\n    ax.hist(H[correct],  bins=bins, alpha=0.6, color=\"steelblue\", label=\"Correct\",  density=False)\n    ax.hist(H[~correct], bins=bins, alpha=0.6, color=\"orange\",    label=\"Erreur\",   density=False)\n    ax.set_title(lab, fontsize=9, fontweight=\"bold\")\n    ax.set_xlabel(\"Predictive entropy\")\n    ax.set_ylabel(\"Count\")\n    ax.legend(fontsize=7)\nplt.suptitle(\"Fig 10. Entropie prédictive stratifiée par justesse\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:38.772977Z","iopub.execute_input":"2026-06-28T05:17:38.773318Z","iopub.status.idle":"2026-06-28T05:17:40.178682Z","shell.execute_reply.started":"2026-06-28T05:17:38.773294Z","shell.execute_reply":"2026-06-28T05:17:40.178068Z"}},"outputs":[],"execution_count":null},{"id":"s16","cell_type":"markdown","source":"## 16. Fig. 11 — Information mutuelle : correct vs erreur","metadata":{}},{"id":"fig11","cell_type":"code","source":"fig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p = tuq[\"mean\"][:, c]; y = tY[:, c]\n    MI = tuq[\"mutual_information\"][:, c]\n    pred = (p >= thr[c]).astype(int)\n    correct = pred == y\n    bins = np.linspace(0, 0.5, 20)\n    ax.hist(MI[correct],  bins=bins, alpha=0.6, color=\"steelblue\", label=\"Correct\", density=False)\n    ax.hist(MI[~correct], bins=bins, alpha=0.6, color=\"orange\",    label=\"Erreur\",  density=False)\n    ax.set_title(lab, fontsize=9, fontweight=\"bold\")\n    ax.set_xlabel(\"Mutual information\")\n    ax.set_ylabel(\"Count\")\n    ax.legend(fontsize=7)\nplt.suptitle(\"Fig 11. Information mutuelle stratifiée par justesse\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:40.179623Z","iopub.execute_input":"2026-06-28T05:17:40.179874Z","iopub.status.idle":"2026-06-28T05:17:41.206084Z","shell.execute_reply.started":"2026-06-28T05:17:40.179852Z","shell.execute_reply":"2026-06-28T05:17:41.205296Z"}},"outputs":[],"execution_count":null},{"id":"s17","cell_type":"markdown","source":"## 17. Fig. 12 — Std ensemble : correct vs erreur","metadata":{}},{"id":"fig12","cell_type":"code","source":"fig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p = tuq[\"mean\"][:, c]; y = tY[:, c]\n    S = tuq[\"std\"][:, c]\n    pred = (p >= thr[c]).astype(int)\n    correct = pred == y\n    bins = np.linspace(0, 0.45, 20)\n    ax.hist(S[correct],  bins=bins, alpha=0.6, color=\"steelblue\", label=\"Correct\", density=False)\n    ax.hist(S[~correct], bins=bins, alpha=0.6, color=\"orange\",    label=\"Erreur\",  density=False)\n    ax.set_title(lab, fontsize=9, fontweight=\"bold\")\n    ax.set_xlabel(\"Ensemble std prob\")\n    ax.set_ylabel(\"Count\")\n    ax.legend(fontsize=7)\nplt.suptitle(\"Fig 12. Std ensemble stratifiée par justesse\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:41.207336Z","iopub.execute_input":"2026-06-28T05:17:41.20763Z","iopub.status.idle":"2026-06-28T05:17:42.211203Z","shell.execute_reply.started":"2026-06-28T05:17:41.207608Z","shell.execute_reply":"2026-06-28T05:17:42.210333Z"}},"outputs":[],"execution_count":null},{"id":"s18","cell_type":"markdown","source":"## 18. Fig. 13 — Risk–Coverage (§3.7)","metadata":{}},{"id":"fig13","cell_type":"code","source":"def risk_coverage(p, y, thr, entropy, covs=np.linspace(0.1, 1.0, 19)):\n    order = np.argsort(entropy)   # certain → incertain\n    err = ((p[order] >= thr).astype(int) != y[order]).astype(int)\n    n = len(y)\n    return covs, np.array([err[:max(int(round(c*n)), 1)].mean() for c in covs])\n\nplt.figure(figsize=(8, 5))\nfor c, lab in enumerate(LABELS):\n    covs, risk = risk_coverage(tuq[\"mean\"][:, c], tY[:, c], thr[c],\n                               tuq[\"predictive_entropy\"][:, c])\n    plt.plot(covs, risk, marker=\"o\", ms=3, label=lab)\nplt.xlabel(\"Couverture (prédictions certaines en premier)\")\nplt.ylabel(\"Risque sélectif (taux d'erreur)\")\nplt.title(\"Fig 13. Risk–Coverage par étiquette\", fontweight=\"bold\")\nplt.legend(fontsize=8); plt.grid(alpha=0.3)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:42.212488Z","iopub.execute_input":"2026-06-28T05:17:42.213357Z","iopub.status.idle":"2026-06-28T05:17:42.412438Z","shell.execute_reply.started":"2026-06-28T05:17:42.213329Z","shell.execute_reply":"2026-06-28T05:17:42.411787Z"}},"outputs":[],"execution_count":null},{"id":"s19","cell_type":"markdown","source":"## 19. Déciles d'incertitude (§2.7)","metadata":{}},{"id":"deciles","cell_type":"code","source":"fig, axes = plt.subplots(2, 3, figsize=(14, 8))\nfor ax, (c, lab) in zip(axes.flat, enumerate(LABELS)):\n    p = tuq[\"mean\"][:, c]; y = tY[:, c]\n    H = tuq[\"predictive_entropy\"][:, c]\n    pred = (p >= thr[c]).astype(int)\n    correct = (pred == y).astype(float)\n    # Découpages en 10 déciles\n    decile_edges = np.percentile(H, np.linspace(0, 100, 11))\n    dec_acc, dec_f1s = [], []\n    for i in range(10):\n        mask = (H >= decile_edges[i]) & (H < decile_edges[i+1])\n        if mask.sum() < 2:\n            dec_acc.append(float(\"nan\")); dec_f1s.append(float(\"nan\")); continue\n        dec_acc.append(correct[mask].mean())\n        tm = threshold_metrics(p[mask], y[mask], thr[c])\n        dec_f1s.append(tm[\"f1\"])\n    x = np.arange(1, 11)\n    ax.plot(x, dec_acc, marker=\"o\", label=\"Accuracy\", color=\"steelblue\")\n    ax.plot(x, dec_f1s, marker=\"s\", label=\"F1\", color=\"darkorange\")\n    ax.set_xlabel(\"Décile d'incertitude\")\n    ax.set_ylabel(\"Performance\")\n    ax.set_title(lab, fontsize=9, fontweight=\"bold\")\n    ax.set_ylim(0, 1.05); ax.legend(fontsize=7)\nplt.suptitle(\"Performance par décile d'incertitude (entropie)\", fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:42.413304Z","iopub.execute_input":"2026-06-28T05:17:42.413725Z","iopub.status.idle":"2026-06-28T05:17:43.182794Z","shell.execute_reply.started":"2026-06-28T05:17:42.413701Z","shell.execute_reply":"2026-06-28T05:17:43.182107Z"}},"outputs":[],"execution_count":null},{"id":"s20","cell_type":"markdown","source":"## 20. Grad-CAM + incertitude spatiale — Éq.3 & Éq.4","metadata":{}},{"id":"gradcam","cell_type":"code","source":"class GradCAM:\n    def __init__(self, model):\n        self.model = model.eval()\n        self.layer = model.backbone.layer4[-1]\n        self.a = self.g = None\n        self.layer.register_forward_hook(\n            lambda m, i, o: setattr(self, \"a\", o.detach()))\n        self.layer.register_full_backward_hook(\n            lambda m, gi, go: setattr(self, \"g\", go[0].detach()))\n    def __call__(self, x, cls):\n        self.model.zero_grad(set_to_none=True)\n        self.model(x)[0, cls].backward()\n        w = self.g.mean((2, 3), keepdim=True)\n        cam = F.relu((w*self.a).sum(1, keepdim=True))\n        cam = F.interpolate(cam, size=x.shape[2:], mode=\"bilinear\",\n                            align_corners=False)\n        cam = cam.squeeze().cpu().numpy()\n        cam -= cam.min(); cam /= cam.max()+1e-8\n        return cam\n\ndef ensemble_gradcam(members, x, cls):\n    \"\"\"Eq.3 : mean, Eq.4 : std\"\"\"\n    cams = np.stack([GradCAM(m)(x, cls) for m in members], 0)\n    return cams.mean(0), cams.std(0)\n\nprint(\"GradCAM OK\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:43.183766Z","iopub.execute_input":"2026-06-28T05:17:43.184228Z","iopub.status.idle":"2026-06-28T05:17:43.192268Z","shell.execute_reply.started":"2026-06-28T05:17:43.184203Z","shell.execute_reply":"2026-06-28T05:17:43.191524Z"}},"outputs":[],"execution_count":null},{"id":"s21","cell_type":"markdown","source":"## 21. Fig. 14 — Panneaux qualitatifs UQ/XAI (§3.8)","metadata":{}},{"id":"fig14","cell_type":"code","source":"members = STORE[\"members\"]\ntest_df = STORE[\"test_df\"].reset_index(drop=True)\ntuq_d   = STORE[\"tuq\"]\n\ndef get_case(label_idx, example_type):\n    \"\"\"Sélectionne un cas selon le type (TP confiant, FN incertain, etc.)\"\"\"\n    p  = tuq_d[\"mean\"][:, label_idx]\n    y  = tY[:, label_idx]\n    H  = tuq_d[\"predictive_entropy\"][:, label_idx]\n    th = thr[label_idx]\n    pred = (p >= th).astype(int)\n    if example_type == \"confident_tp\":\n        mask = (pred==1) & (y==1) & (H < 0.05)\n    elif example_type == \"uncertain_tp\":\n        mask = (pred==1) & (y==1) & (H > 0.3)\n    elif example_type == \"uncertain_fp\":\n        mask = (pred==1) & (y==0) & (H > 0.2)\n    elif example_type == \"uncertain_fn\":\n        mask = (pred==0) & (y==1) & (H > 0.5)\n    elif example_type == \"high_disagreement\":\n        mask = tuq_d[\"mutual_information\"][:, label_idx] > 0.1\n    else:\n        mask = np.ones(len(p), dtype=bool)\n    idxs = np.where(mask)[0]\n    return int(idxs[0]) if len(idxs) > 0 else int(np.argmax(H))\n\ndef plot_uq_panel(row_idx, label_idx, example_type, ax_row):\n    \"\"\"Affiche un panel UQ/XAI pour un cas donné (Fig.14)\"\"\"\n    path = test_df.iloc[row_idx][\"path\"]\n    hu   = load_hu(path)\n    img3 = make_three_window(hu, CFG)\n    x = torch.from_numpy(img3).permute(2, 0, 1).unsqueeze(0)\n    x = F.interpolate(x, size=(CFG.image_size, CFG.image_size),\n                      mode=\"bilinear\", align_corners=False).to(DEVICE)\n    mean_cam, unc_cam = ensemble_gradcam(members, x, label_idx)\n    brain = img3[..., 0]\n    brain_rs = np.array(F.interpolate(\n        torch.from_numpy(brain)[None, None],\n        size=(CFG.image_size, CFG.image_size),\n        mode=\"bilinear\", align_corners=False)[0, 0])\n    # Métriques UQ\n    p   = float(tuq_d[\"mean\"][row_idx, label_idx])\n    H   = float(tuq_d[\"predictive_entropy\"][row_idx, label_idx])\n    MI  = float(tuq_d[\"mutual_information\"][row_idx, label_idx])\n    std = float(tuq_d[\"std\"][row_idx, label_idx])\n    mrg = float(tuq_d[\"margin\"][row_idx, label_idx])\n    y_true = int(tY[row_idx, label_idx])\n    # Affichage sur 3 axes\n    ax_row[0].imshow(brain_rs, cmap=\"gray\"); ax_row[0].axis(\"off\")\n    ax_row[0].set_title(f\"{LABELS[label_idx]}\\n{example_type}\", fontsize=7)\n    ax_row[1].imshow(brain_rs, cmap=\"gray\")\n    ax_row[1].imshow(mean_cam, cmap=\"jet\", alpha=0.5); ax_row[1].axis(\"off\")\n    ax_row[1].set_title(\"Grad-CAM (Éq.3)\", fontsize=7)\n    im = ax_row[2].imshow(unc_cam, cmap=\"jet\"); ax_row[2].axis(\"off\")\n    ax_row[2].set_title(\"Unc. spatiale (Éq.4)\", fontsize=7)\n    # Texte UQ\n    txt = (f\"Target={y_true}  p={p:.3f}\\n\"\n           f\"H={H:.3f}  MI={MI:.3f}\\n\"\n           f\"std={std:.3f}  margin={mrg:.3f}\")\n    ax_row[2].text(1.05, 0.5, txt, transform=ax_row[2].transAxes,\n                   fontsize=6.5, va=\"center\",\n                   bbox=dict(boxstyle=\"round\", facecolor=\"lightyellow\", alpha=0.8))\n\n# Cas à afficher : (label_idx, example_type)\ncases = [\n    (LABELS.index(\"any\"),              \"confident_tp\"),\n    (LABELS.index(\"subdural\"),         \"uncertain_tp\"),\n    (LABELS.index(\"subarachnoid\"),     \"uncertain_fp\"),\n    (LABELS.index(\"any\"),              \"uncertain_fn\"),\n    (LABELS.index(\"intraparenchymal\"), \"high_disagreement\"),\n]\n\nfig, axes = plt.subplots(len(cases), 3, figsize=(11, 3.5 * len(cases)))\nif len(cases) == 1: axes = axes[None, :]\nfor row, (lab_idx, ex_type) in enumerate(cases):\n    ri = get_case(lab_idx, ex_type)\n    plot_uq_panel(ri, lab_idx, ex_type, axes[row])\n\nplt.suptitle(\"Fig 14. Exemples qualitatifs UQ/XAI\", fontsize=12, fontweight=\"bold\")\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T05:17:43.193266Z","iopub.execute_input":"2026-06-28T05:17:43.193542Z","iopub.status.idle":"2026-06-28T05:17:45.383031Z","shell.execute_reply.started":"2026-06-28T05:17:43.19351Z","shell.execute_reply":"2026-06-28T05:17:45.382036Z"}},"outputs":[],"execution_count":null},{"id":"s22","cell_type":"markdown","source":"## 22. Synthèse\n\nCe notebook reproduit **intégralement** le pipeline de Sekkat Hamza. :\n\n| Section article | Cellule notebook |\n|---|---|\n| §2.2 — CT windowing (Table 1) | Cellule 4 |\n| §2.4 — ResNet-50 + BCE pondérée + AdamW | Cellules 6-7 |\n| §2.5 — Eq.1 (mean), Eq.2 (MI), temp. scaling | Cellule 8 |\n| §2.6 — Eq.3 (Grad-CAM moyen), Eq.4 (unc. spatiale) | Cellule 20 |\n| §3.1 — Fig.3 convergence | Cellule 10 |\n| §3.2 — Table 2 discrimination | Cellule 11 |\n| §3.3 — Table 3 threshold | Cellule 11 |\n| §3.4 — Fig.8 calibration + reliability diagrams | Cellules 12-13 |\n| §3.5 — Fig.9 désaccord vs probabilité | Cellule 14 |\n| §3.6 — Fig.10-12 entropie/MI/std vs justesse | Cellules 15-17 |\n| §3.7 — Fig.13 risk–coverage | Cellule 18 |\n| §3.7 — Déciles d'incertitude | Cellule 19 |\n| §3.8 — Fig.14 panneaux UQ/XAI | Cellule 21 |\n\n**Pour reproduire l'article complet :** `ensemble_size=5`, `n_folds=3`, `max_epochs=30`, `max_studies=5000`.","metadata":{}}]}