{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":10338,"databundleVersionId":862042,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Pneumonia Detection - RSNA Pneumonia Detection Challenge  Dataset\n## Algorithme : Ensemble(ResNet50 + GoogLeNet) - Multi-channel Preprocessing - Train/Val/test Split 80/10/10","metadata":{}},{"cell_type":"code","source":"# -- Cellule 1 : Installation\n!pip install albumentations scikit-learn tqdm pandas opencv-python-headless pydicom -q\nprint(\"OK deps\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:37.049791Z","iopub.execute_input":"2026-05-05T13:04:37.050884Z","iopub.status.idle":"2026-05-05T13:04:40.807194Z","shell.execute_reply.started":"2026-05-05T13:04:37.050787Z","shell.execute_reply":"2026-05-05T13:04:40.806306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 2 : Imports\nimport os, random, warnings\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom pathlib import Path\nfrom tqdm.notebook import tqdm\nwarnings.filterwarnings('ignore')\n \nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nimport torchvision.models as models\n \nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n \nfrom sklearn.metrics import (accuracy_score, f1_score, precision_score,\n                              recall_score, roc_auc_score, confusion_matrix,\n                              classification_report, roc_curve)\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.utils.class_weight import compute_class_weight\n \nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'PyTorch : {torch.__version__}')\nprint(f'Device  : {DEVICE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.809387Z","iopub.execute_input":"2026-05-05T13:04:40.809748Z","iopub.status.idle":"2026-05-05T13:04:40.817174Z","shell.execute_reply.started":"2026-05-05T13:04:40.809717Z","shell.execute_reply":"2026-05-05T13:04:40.816385Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 3 : Telechargement du Dataset RSNA\n# -----------------------------------------------------------------------\n# CHANGEMENT : Dataset RSNA Pneumonia Detection Challenge\n#   kaggle competitions download -c rsna-pneumonia-detection-challenge\n#\n# Structure attendue apres extraction :\n#   data/rsna/\n#     stage_2_train_labels.csv      <- patientId, x, y, width, height, Target\n#     stage_2_train_images/         <- *.dcm  (fichiers DICOM)\n#\n# Le CSV contient :\n#   Target = 0  -> Normal (pas de pneumonie)\n#   Target = 1  -> Pneumonie (une ou plusieurs bounding boxes)\n# -----------------------------------------------------------------------\nimport zipfile\n \nDATA_DIR = Path('data/rsna')\nif not DATA_DIR.exists():\n    print('Downloading RSNA dataset from Kaggle...')\n    os.system('kaggle competitions download -c rsna-pneumonia-detection-challenge -p data/rsna_zip')\n    zip_path = Path('data/rsna_zip/rsna-pneumonia-detection-challenge.zip')\n    with zipfile.ZipFile(zip_path, 'r') as z:\n        z.extractall(DATA_DIR)\n    print('Done!')\nelse:\n    print('Dataset RSNA already present')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.818122Z","iopub.execute_input":"2026-05-05T13:04:40.818537Z","iopub.status.idle":"2026-05-05T13:04:40.838408Z","shell.execute_reply.started":"2026-05-05T13:04:40.818498Z","shell.execute_reply":"2026-05-05T13:04:40.837659Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 4 : Configuration  (INCHANGE)\nSEED         = 42\nIMG_SIZE     = 224\nBATCH_SIZE   = 32\nEPOCHS       = 5\nLR           = 3e-4\nWEIGHT_DECAY = 1e-2\n \nTRAIN_SPLIT  = 0.8\nVAL_SPLIT    = 0.1\nTEST_SPLIT   = 0.1\n \nGRAD_CLIP    = 1.0\nNUM_WORKERS  = 2\nUSE_AMP      = torch.cuda.is_available()\nNUM_CLASSES  = 2\nSOTA_ACC     = None\n \nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark     = False\n \nprint('Config chargee')\nprint(f'IMG_SIZE={IMG_SIZE}  BATCH={BATCH_SIZE}  EPOCHS={EPOCHS}')\nprint(f'Split : {int(TRAIN_SPLIT*100)}% train / {int(VAL_SPLIT*100)}% val / {int(TEST_SPLIT*100)}% test')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.840296Z","iopub.execute_input":"2026-05-05T13:04:40.840681Z","iopub.status.idle":"2026-05-05T13:04:40.866130Z","shell.execute_reply.started":"2026-05-05T13:04:40.840656Z","shell.execute_reply":"2026-05-05T13:04:40.865060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 5 : Multi-Channel Preprocessing  (INCHANGE)\ndef multichannel_preprocess(img_bgr: np.ndarray, img_size: int = 224) -> np.ndarray:\n    img  = cv2.resize(img_bgr, (img_size, img_size))\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n \n    ch0 = gray.astype(np.float32) / 255.0\n \n    clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))\n    ch1   = clahe.apply(gray).astype(np.float32) / 255.0\n \n    sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)\n    sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)\n    mag    = np.sqrt(sobelx**2 + sobely**2)\n    if mag.max() > 0:\n        mag = mag / mag.max()\n    ch2 = mag.astype(np.float32)\n \n    return np.stack([ch0, ch1, ch2], axis=-1)  # [H, W, 3]\n \n \ndef visualize_multichannel(img_bgr: np.ndarray, title: str = ''):\n    mc   = multichannel_preprocess(img_bgr, IMG_SIZE)\n    orig = cv2.cvtColor(cv2.resize(img_bgr, (IMG_SIZE, IMG_SIZE)), cv2.COLOR_BGR2RGB)\n \n    fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n    images = [orig, mc[:,:,0], mc[:,:,1], mc[:,:,2]]\n    titles = ['Original', 'Canal 0\\n(Normalise)', 'Canal 1\\n(CLAHE)', 'Canal 2\\n(Sobel)']\n    cmaps  = [None, 'gray', 'gray', 'hot']\n    for ax, im, t, c in zip(axes, images, titles, cmaps):\n        ax.imshow(im, cmap=c)\n        ax.set_title(t, fontsize=11, fontweight='bold')\n        ax.axis('off')\n    plt.suptitle(f'Multi-Channel Preprocessing — {title}', fontsize=13, fontweight='bold')\n    plt.tight_layout()\n    plt.show()\n \n \nprint('Multi-Channel Preprocessing defini')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.867290Z","iopub.execute_input":"2026-05-05T13:04:40.867627Z","iopub.status.idle":"2026-05-05T13:04:40.878088Z","shell.execute_reply.started":"2026-05-05T13:04:40.867601Z","shell.execute_reply":"2026-05-05T13:04:40.877318Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 6 : Augmentations  (INCHANGE)\nMEAN = [0.5, 0.5, 0.5]\nSTD  = [0.5, 0.5, 0.5]\n \ndef get_train_transforms():\n    return A.Compose([\n        A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1,\n                           rotate_limit=10, p=0.5),\n        A.HorizontalFlip(p=0.5),\n        A.OneOf([\n            A.GaussNoise(var_limit=(0.001, 0.01)),\n            A.GaussianBlur(blur_limit=(3, 5)),\n        ], p=0.3),\n        A.RandomBrightnessContrast(brightness_limit=0.1, contrast_limit=0.1, p=0.4),\n        A.CoarseDropout(max_holes=6, max_height=IMG_SIZE//16,\n                        max_width=IMG_SIZE//16, p=0.3),\n        A.Normalize(mean=MEAN, std=STD, max_pixel_value=1.0),\n        ToTensorV2(),\n    ])\n \ndef get_val_transforms():\n    return A.Compose([\n        A.Normalize(mean=MEAN, std=STD, max_pixel_value=1.0),\n        ToTensorV2(),\n    ])\n \nprint('Augmentations definies (max_pixel_value=1.0 pour float32)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.879227Z","iopub.execute_input":"2026-05-05T13:04:40.879981Z","iopub.status.idle":"2026-05-05T13:04:40.897074Z","shell.execute_reply.started":"2026-05-05T13:04:40.879940Z","shell.execute_reply":"2026-05-05T13:04:40.896124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 7 : Dataset Multi-Channel + Train/Val/Test Split\n# -----------------------------------------------------------------------\n# CHANGEMENT : Chargement depuis les fichiers DICOM du dataset RSNA\n#\n# Le CSV stage_2_train_labels.csv peut contenir PLUSIEURS lignes\n# pour le meme patientId (une par bounding box). On déduplique par\n# patientId en gardant la valeur max de Target (si au moins une\n# bounding box -> Target=1).\n#\n# Lecture DICOM :\n#   - pydicom lit le fichier .dcm\n#   - pixel_array -> image 16-bit uint  -> normalise en uint8 BGR\n#     pour rester compatible avec multichannel_preprocess()\n# -----------------------------------------------------------------------\nimport pydicom\n \nRSNA_CSV      = DATA_DIR / 'stage_2_train_labels.csv'\nRSNA_IMG_DIR  = DATA_DIR / 'stage_2_train_images'\n \ndef dicom_to_bgr(dcm_path: str) -> np.ndarray:\n    \"\"\"Lit un fichier DICOM et retourne une image BGR uint8.\"\"\"\n    dcm   = pydicom.dcmread(dcm_path)\n    pixel = dcm.pixel_array.astype(np.float32)\n    # Normalise [0, 255]\n    pixel = (pixel - pixel.min()) / (pixel.max() - pixel.min() + 1e-8) * 255.0\n    img_gray = pixel.astype(np.uint8)\n    img_bgr  = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)\n    return img_bgr\n \n \nclass RSNAMultiChannelDataset(Dataset):\n    \"\"\"\n    Dataset RSNA Pneumonia Detection Challenge — classification binaire.\n      Label 0 : Normal  (Target == 0)\n      Label 1 : Pneumonie (Target == 1)\n    \"\"\"\n    CLASSES = {0: 'Normal', 1: 'Pneumonia'}\n \n    def __init__(self, samples, transform=None):\n        self.samples   = samples   # liste de (dcm_path, label)\n        self.transform = transform\n \n    def __len__(self):\n        return len(self.samples)\n \n    def __getitem__(self, idx):\n        path, label = self.samples[idx]\n        img_bgr = dicom_to_bgr(path)\n        mc = multichannel_preprocess(img_bgr, IMG_SIZE)\n        if self.transform:\n            mc = self.transform(image=mc)['image']\n        return mc, label\n \n \n# Chargement du CSV — dédupliquation par patientId\ndf = pd.read_csv(RSNA_CSV)\ndf_unique = df.groupby('patientId', as_index=False)['Target'].max()\n \nall_samples = []\nmissing     = 0\nfor _, row in df_unique.iterrows():\n    dcm_path = RSNA_IMG_DIR / f\"{row['patientId']}.dcm\"\n    if dcm_path.exists():\n        all_samples.append((str(dcm_path), int(row['Target'])))\n    else:\n        missing += 1\n \nif missing:\n    print(f'Avertissement : {missing} fichiers DICOM introuvables (ignores)')\n \nall_labels = [l for _, l in all_samples]\nn_normal   = sum(1 for l in all_labels if l == 0)\nn_pneumo   = sum(1 for l in all_labels if l == 1)\n \nprint(f'Total : {len(all_samples)} patients')\nprint(f'  Normal    : {n_normal} ({n_normal/len(all_samples)*100:.1f}%)')\nprint(f'  Pneumonia : {n_pneumo} ({n_pneumo/len(all_samples)*100:.1f}%)')\n \n# Step 1 : 80% train / 20% temp  (stratifie — INCHANGE)\ntrain_samples, temp_samples, train_labels, temp_labels = train_test_split(\n    all_samples, all_labels,\n    test_size=VAL_SPLIT + TEST_SPLIT,\n    random_state=SEED,\n    stratify=all_labels,\n)\n \n# Step 2 : 20% temp -> 10% val / 10% test  (INCHANGE)\nval_samples, test_samples, val_labels, test_labels = train_test_split(\n    temp_samples, temp_labels,\n    test_size=TEST_SPLIT / (VAL_SPLIT + TEST_SPLIT),\n    random_state=SEED,\n    stratify=temp_labels,\n)\n \ntrain_ds = RSNAMultiChannelDataset(train_samples, get_train_transforms())\nval_ds   = RSNAMultiChannelDataset(val_samples,   get_val_transforms())\ntest_ds  = RSNAMultiChannelDataset(test_samples,  get_val_transforms())\n \ntrain_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True,\n                      num_workers=NUM_WORKERS, pin_memory=True, drop_last=True)\nval_dl   = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False,\n                      num_workers=NUM_WORKERS, pin_memory=True)\ntest_dl  = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False,\n                      num_workers=NUM_WORKERS, pin_memory=True)\n \nn_tn = sum(1 for _, l in train_samples if l == 0)\nn_tp = sum(1 for _, l in train_samples if l == 1)\nn_vn = sum(1 for _, l in val_samples   if l == 0)\nn_vp = sum(1 for _, l in val_samples   if l == 1)\nn_sn = sum(1 for _, l in test_samples  if l == 0)\nn_sp = sum(1 for _, l in test_samples  if l == 1)\n \nprint(f'Train : {len(train_samples)} ({n_tn} Normal, {n_tp} Pneumonia)')\nprint(f'Val   : {len(val_samples)} ({n_vn} Normal, {n_vp} Pneumonia)')\nprint(f'Test  : {len(test_samples)} ({n_sn} Normal, {n_sp} Pneumonia)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:40.898295Z","iopub.execute_input":"2026-05-05T13:04:40.898674Z","iopub.status.idle":"2026-05-05T13:04:43.649976Z","shell.execute_reply.started":"2026-05-05T13:04:40.898650Z","shell.execute_reply":"2026-05-05T13:04:43.649283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 8 : Visualisation Multi-Channel\nfor label_id, label_name in RSNAMultiChannelDataset.CLASSES.items():\n    example_path = next(p for p, l in all_samples if l == label_id)\n    print(f'Exemple : {label_name}')\n    visualize_multichannel(dicom_to_bgr(example_path), title=label_name)\n \nfig, axes = plt.subplots(1, 2, figsize=(12, 4))\ncounts_train = [sum(1 for _, l in train_samples if l == c) for c in [0, 1]]\ncounts_val   = [sum(1 for _, l in val_samples   if l == c) for c in [0, 1]]\n \naxes[0].bar(['NORMAL', 'PNEUMONIA'], counts_train, color=['steelblue', 'tomato'])\naxes[0].set_title('Distribution Train', fontweight='bold')\naxes[0].set_ylabel('Images')\nfor i, v in enumerate(counts_train):\n    axes[0].text(i, v + 20, str(v), ha='center', fontweight='bold')\n \naxes[1].bar(['NORMAL', 'PNEUMONIA'], counts_val, color=['steelblue', 'tomato'])\naxes[1].set_title('Distribution Val', fontweight='bold')\nfor i, v in enumerate(counts_val):\n    axes[1].text(i, v + 5, str(v), ha='center', fontweight='bold')\n \nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:43.650796Z","iopub.execute_input":"2026-05-05T13:04:43.651399Z","iopub.status.idle":"2026-05-05T13:04:44.944579Z","shell.execute_reply.started":"2026-05-05T13:04:43.651371Z","shell.execute_reply":"2026-05-05T13:04:44.943667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 9 : Modeles ResNet50 + GoogLeNet  (INCHANGE)\ndef build_resnet50(num_classes=2):\n    model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)\n    in_f  = model.fc.in_features\n    model.fc = nn.Sequential(\n        nn.Dropout(0.4),\n        nn.Linear(in_f, 512),\n        nn.GELU(),\n        nn.Dropout(0.2),\n        nn.Linear(512, num_classes),\n    )\n    return model\n \n \ndef build_googlenet(num_classes=2):\n    model = models.googlenet(\n        weights    = models.GoogLeNet_Weights.IMAGENET1K_V1,\n        aux_logits = True,\n    )\n    model.aux_logits = False\n    model.aux1       = None\n    model.aux2       = None\n    in_f  = model.fc.in_features\n    model.fc = nn.Sequential(\n        nn.Dropout(0.4),\n        nn.Linear(in_f, 512),\n        nn.GELU(),\n        nn.Dropout(0.2),\n        nn.Linear(512, num_classes),\n    )\n    return model\n \n \nresnet    = build_resnet50(NUM_CLASSES).to(DEVICE)\ngooglenet = build_googlenet(NUM_CLASSES).to(DEVICE)\n \nn_r = sum(p.numel() for p in resnet.parameters()    if p.requires_grad)\nn_g = sum(p.numel() for p in googlenet.parameters() if p.requires_grad)\nprint(f'ResNet-50  : {n_r/1e6:.1f}M params')\nprint(f'GoogLeNet  : {n_g/1e6:.1f}M params')\nprint(f'Input      : [B, 3, {IMG_SIZE}, {IMG_SIZE}]  (multi-canal)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:44.945709Z","iopub.execute_input":"2026-05-05T13:04:44.946104Z","iopub.status.idle":"2026-05-05T13:04:46.907983Z","shell.execute_reply.started":"2026-05-05T13:04:44.946078Z","shell.execute_reply":"2026-05-05T13:04:46.907312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 10 : Ensemble Model  (INCHANGE)\nclass EnsembleModel(nn.Module):\n    def __init__(self, resnet, googlenet, fusion='mean', num_classes=2):\n        super().__init__()\n        self.resnet    = resnet\n        self.googlenet = googlenet\n        self.fusion    = fusion\n        if fusion == 'learned':\n            self.meta = nn.Sequential(\n                nn.Linear(num_classes * 2, 64),\n                nn.ReLU(),\n                nn.Linear(64, num_classes),\n            )\n \n    def forward(self, x):\n        l_r = self.resnet(x)\n        l_g = self.googlenet(x)\n        if self.fusion == 'mean':\n            return (l_r + l_g) / 2.0\n        elif self.fusion == 'learned':\n            return self.meta(torch.cat([l_r, l_g], dim=1))\n \n \nensemble = EnsembleModel(resnet, googlenet, fusion='mean').to(DEVICE)\nprint(f'Ensemble(ResNet50 + GoogLeNet) cree')\nprint(f'   Fusion : mean')\nprint(f'   Total  : {(n_r + n_g)/1e6:.1f}M params')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:46.910135Z","iopub.execute_input":"2026-05-05T13:04:46.910375Z","iopub.status.idle":"2026-05-05T13:04:46.922965Z","shell.execute_reply.started":"2026-05-05T13:04:46.910353Z","shell.execute_reply":"2026-05-05T13:04:46.922189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 11 : Loss + Optimizer + Scheduler  (INCHANGE)\ncw = compute_class_weight('balanced', classes=np.unique(all_labels), y=all_labels)\nclass_weights = torch.tensor(cw, dtype=torch.float32).to(DEVICE)\nprint(f'Poids de classe : Normal={cw[0]:.3f}, Pneumonia={cw[1]:.3f}')\n \ncriterion = nn.CrossEntropyLoss(weight=class_weights, label_smoothing=0.05)\n \noptimizer_r = AdamW(resnet.parameters(),    lr=LR, weight_decay=WEIGHT_DECAY)\noptimizer_g = AdamW(googlenet.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n \nscheduler_r = CosineAnnealingLR(optimizer_r, T_max=EPOCHS, eta_min=1e-6)\nscheduler_g = CosineAnnealingLR(optimizer_g, T_max=EPOCHS, eta_min=1e-6)\n \nscaler = torch.cuda.amp.GradScaler(enabled=USE_AMP)\n \nprint(f'CrossEntropyLoss (label_smoothing=0.05, weighted)')\nprint(f'AdamW x2  (lr={LR}, wd={WEIGHT_DECAY})')\nprint(f'CosineAnnealingLR (T_max={EPOCHS})')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:46.924038Z","iopub.execute_input":"2026-05-05T13:04:46.924632Z","iopub.status.idle":"2026-05-05T13:04:46.947131Z","shell.execute_reply.started":"2026-05-05T13:04:46.924610Z","shell.execute_reply":"2026-05-05T13:04:46.946358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 12 : Fonctions Train / Validate  (INCHANGE)\ndef train_epoch(resnet, googlenet, loader, opt_r, opt_g, criterion, scaler):\n    resnet.train()\n    googlenet.train()\n    total_loss = 0.0\n    preds_all, labels_all = [], []\n \n    pbar = tqdm(loader, desc='  [TRAIN]', leave=False)\n    for imgs, labels in pbar:\n        imgs   = imgs.float().to(DEVICE, non_blocking=True)\n        labels = labels.long().to(DEVICE, non_blocking=True)\n \n        opt_r.zero_grad(set_to_none=True)\n        opt_g.zero_grad(set_to_none=True)\n \n        with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=USE_AMP):\n            logits_r = resnet(imgs)\n            logits_g = googlenet(imgs)\n            logits_e = (logits_r + logits_g) / 2.0\n            loss = (0.3 * criterion(logits_r, labels)\n                  + 0.3 * criterion(logits_g, labels)\n                  + 0.4 * criterion(logits_e, labels))\n \n        scaler.scale(loss).backward()\n        scaler.unscale_(opt_r)\n        scaler.unscale_(opt_g)\n        nn.utils.clip_grad_norm_(resnet.parameters(),    GRAD_CLIP)\n        nn.utils.clip_grad_norm_(googlenet.parameters(), GRAD_CLIP)\n        scaler.step(opt_r)\n        scaler.step(opt_g)\n        scaler.update()\n \n        total_loss += loss.item()\n        preds_all.append(logits_e.argmax(1).detach())\n        labels_all.append(labels.detach())\n        pbar.set_postfix(loss=f'{loss.item():.4f}')\n \n    preds_all  = torch.cat(preds_all).cpu().numpy()\n    labels_all = torch.cat(labels_all).cpu().numpy()\n    return total_loss / len(loader), accuracy_score(labels_all, preds_all)\n \n \n@torch.no_grad()\ndef validate(resnet, googlenet, loader, criterion):\n    resnet.eval()\n    googlenet.eval()\n    total_loss, preds_all, labels_all, probs_all = 0.0, [], [], []\n \n    for imgs, labels in loader:\n        imgs   = imgs.float().to(DEVICE)\n        labels = labels.long().to(DEVICE)\n        with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=USE_AMP):\n            logits_r = resnet(imgs)\n            logits_g = googlenet(imgs)\n            logits_e = (logits_r + logits_g) / 2.0\n            loss     = criterion(logits_e, labels)\n        probs = F.softmax(logits_e, dim=1)[:, 1]\n        total_loss += loss.item()\n        preds_all.extend(logits_e.argmax(1).cpu().numpy())\n        labels_all.extend(labels.cpu().numpy())\n        probs_all.extend(probs.cpu().numpy())\n \n    acc = accuracy_score(labels_all, preds_all)\n    f1  = f1_score(labels_all, preds_all, average='weighted')\n    try:    auc = roc_auc_score(labels_all, probs_all)\n    except: auc = 0.0\n    return total_loss / len(loader), acc, f1, auc\n \n \nprint('train_epoch / validate definis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:46.948049Z","iopub.execute_input":"2026-05-05T13:04:46.948407Z","iopub.status.idle":"2026-05-05T13:04:46.959778Z","shell.execute_reply.started":"2026-05-05T13:04:46.948376Z","shell.execute_reply":"2026-05-05T13:04:46.958992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 13 : Boucle d'entrainement avec Progressive Unfreezing  (INCHANGE)\nos.makedirs('checkpoints', exist_ok=True)\n \nWARMUP_EPOCHS = 3\nPATIENCE      = 6\n \nbest_acc, best_state_r, best_state_g = 0.0, None, None\nhistory = []\npatience_ctr = 0\n \nprint(f'Entrainement : {EPOCHS} epochs  (warmup={WARMUP_EPOCHS}, patience={PATIENCE})')\nprint(f'   Backbone 1 : ResNet-50   ({n_r/1e6:.1f}M params)')\nprint(f'   Backbone 2 : GoogLeNet   ({n_g/1e6:.1f}M params)')\nprint(f'   Dataset    : RSNA Pneumonia Detection Challenge')\nprint(f'   Split      : Train {len(train_samples)} / Val {len(val_samples)}')\nprint('=' * 65)\n \ndef freeze_backbone(model):\n    for name, p in model.named_parameters():\n        if 'fc' not in name:\n            p.requires_grad = False\n \ndef unfreeze_all(model):\n    for p in model.parameters():\n        p.requires_grad = True\n \nfreeze_backbone(resnet)\nfreeze_backbone(googlenet)\nprint(f'Backbone gele pour {WARMUP_EPOCHS} epochs de warmup')\n \nfor epoch in range(EPOCHS):\n    if epoch == WARMUP_EPOCHS:\n        unfreeze_all(resnet)\n        unfreeze_all(googlenet)\n        for opt in [optimizer_r, optimizer_g]:\n            for g in opt.param_groups:\n                g['lr'] = LR * 0.1\n        print(f'Backbone degele. LR -> {LR*0.1:.1e}')\n \n    tr_loss, tr_acc = train_epoch(\n        resnet, googlenet, train_dl, optimizer_r, optimizer_g, criterion, scaler)\n    vl_loss, vl_acc, vl_f1, vl_auc = validate(\n        resnet, googlenet, val_dl, criterion)\n \n    if epoch >= WARMUP_EPOCHS:\n        scheduler_r.step()\n        scheduler_g.step()\n \n    history.append({\n        'epoch': epoch+1,\n        'tr_loss': tr_loss, 'tr_acc': tr_acc,\n        'vl_loss': vl_loss, 'vl_acc': vl_acc,\n        'vl_f1':   vl_f1,   'vl_auc': vl_auc,\n    })\n \n    phase = 'WU' if epoch < WARMUP_EPOCHS else 'FT'\n    lr    = optimizer_r.param_groups[0]['lr']\n    tag   = ''\n    print(f'  [{phase}] Epoch {epoch+1:02d}/{EPOCHS} | '\n          f'Tr={tr_acc:.4f}  Val={vl_acc:.4f}  '\n          f'F1={vl_f1:.4f}  AUC={vl_auc:.4f}  '\n          f'LR={lr:.2e}')\n \n    if vl_acc > best_acc:\n        best_acc     = vl_acc\n        best_state_r = {k: v.clone() for k, v in resnet.state_dict().items()}\n        best_state_g = {k: v.clone() for k, v in googlenet.state_dict().items()}\n        torch.save(best_state_r, 'checkpoints/resnet_best.pth')\n        torch.save(best_state_g, 'checkpoints/googlenet_best.pth')\n        patience_ctr = 0\n        print(f'     Nouveau record ! Acc={best_acc*100:.2f}%')\n    else:\n        if epoch >= WARMUP_EPOCHS:\n            patience_ctr += 1\n            if patience_ctr >= PATIENCE:\n                print(f'Early stopping (patience={PATIENCE})')\n                break\n \nresnet.load_state_dict(best_state_r)\ngooglenet.load_state_dict(best_state_g)\nprint(f'Entrainement termine. Meilleure Val Acc = {best_acc*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:04:46.960888Z","iopub.execute_input":"2026-05-05T13:04:46.961281Z","iopub.status.idle":"2026-05-05T13:19:53.401294Z","shell.execute_reply.started":"2026-05-05T13:04:46.961239Z","shell.execute_reply":"2026-05-05T13:19:53.400559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 14 : Courbes d'apprentissage  (INCHANGE)\nhist_df = pd.DataFrame(history)\nfig, axes = plt.subplots(1, 3, figsize=(20, 5))\n \naxes[0].plot(hist_df.epoch, hist_df.tr_loss, label='Train', color='steelblue')\naxes[0].plot(hist_df.epoch, hist_df.vl_loss, label='Val',   color='tomato')\naxes[0].set_title('Loss'); axes[0].legend(); axes[0].set_xlabel('Epoch')\n \naxes[1].plot(hist_df.epoch, hist_df.tr_acc * 100, label='Train', color='steelblue')\naxes[1].plot(hist_df.epoch, hist_df.vl_acc * 100, label='Val',   color='tomato')\naxes[1].set_title('Accuracy (%)'); axes[1].legend(); axes[1].set_xlabel('Epoch')\n \naxes[2].plot(hist_df.epoch, hist_df.vl_f1,  label='F1',  color='purple')\naxes[2].plot(hist_df.epoch, hist_df.vl_auc, label='AUC', color='orange')\naxes[2].set_title('F1 & AUC'); axes[2].legend(); axes[2].set_xlabel('Epoch')\n \nplt.suptitle('Ensemble(ResNet50 + GoogLeNet) — RSNA Pneumonia — Multi-Channel Preprocessing',\n             fontsize=13, fontweight='bold')\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:19:53.402712Z","iopub.execute_input":"2026-05-05T13:19:53.403446Z","iopub.status.idle":"2026-05-05T13:19:53.879316Z","shell.execute_reply.started":"2026-05-05T13:19:53.403416Z","shell.execute_reply":"2026-05-05T13:19:53.878638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 15 : Evaluation Finale (Test Set)  (INCHANGE)\nresnet.eval(); googlenet.eval()\nall_preds, all_probs, all_true = [], [], []\n \nwith torch.no_grad():\n    for imgs, labels in tqdm(test_dl, desc='Eval finale'):\n        imgs = imgs.float().to(DEVICE)\n        with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=USE_AMP):\n            logits_r = resnet(imgs)\n            logits_g = googlenet(imgs)\n            logits_e = (logits_r + logits_g) / 2.0\n        probs = F.softmax(logits_e, dim=1)[:, 1]\n        all_preds.extend(logits_e.argmax(1).cpu().numpy())\n        all_probs.extend(probs.cpu().numpy())\n        all_true.extend(labels.numpy())\n \nacc  = accuracy_score(all_true, all_preds)\nf1   = f1_score(all_true, all_preds, average='weighted')\nprec = precision_score(all_true, all_preds, average='weighted', zero_division=0)\nrec  = recall_score(all_true, all_preds, average='weighted', zero_division=0)\nauc  = roc_auc_score(all_true, all_probs)\n \nprint('RESULTATS FINAUX')\nprint('Ensemble : ResNet50 + GoogLeNet')\nprint('Dataset   : RSNA Pneumonia Detection Challenge')\nprint('Classes   : Normal vs Pneumonia')\nprint('Preprocessing : Multi-channel [Normalise | CLAHE | Sobel]')\nprint(f'Split : {int(TRAIN_SPLIT*100)}% Train / {int(VAL_SPLIT*100)}% Val / {int(TEST_SPLIT*100)}% Test')\nprint(f'  Accuracy  : {acc*100:.2f}%')\nprint(f'  F1-Score  : {f1*100:.2f}%')\nprint(f'  Precision : {prec*100:.2f}%')\nprint(f'  Recall    : {rec*100:.2f}%')\nprint(f'  AUC-ROC   : {auc*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:19:53.880335Z","iopub.execute_input":"2026-05-05T13:19:53.880666Z","iopub.status.idle":"2026-05-05T13:20:12.195349Z","shell.execute_reply.started":"2026-05-05T13:19:53.880641Z","shell.execute_reply":"2026-05-05T13:20:12.194527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 16 : Matrice de Confusion + Courbe ROC  (INCHANGE)\ncm = confusion_matrix(all_true, all_preds)\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n \nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0],\n            xticklabels=['Normal', 'Pneumonia'],\n            yticklabels=['Normal', 'Pneumonia'])\naxes[0].set_ylabel('Vrai label'); axes[0].set_xlabel('Prediction')\naxes[0].set_title(f'Matrice de Confusion\\nAcc={acc*100:.2f}%  AUC={auc*100:.2f}%',\n                  fontweight='bold')\n \nfpr, tpr, _ = roc_curve(all_true, all_probs)\naxes[1].plot(fpr, tpr, color='darkorange', lw=2, label=f'Ensemble ROC (AUC={auc:.4f})')\naxes[1].plot([0, 1], [0, 1], 'navy', linestyle='--', lw=1)\naxes[1].set_xlabel('False Positive Rate'); axes[1].set_ylabel('True Positive Rate')\naxes[1].set_title('Courbe ROC', fontweight='bold'); axes[1].legend(loc='lower right')\nplt.tight_layout(); plt.show()\n \nprint(classification_report(all_true, all_preds, target_names=['Normal', 'Pneumonia']))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:20:12.196695Z","iopub.execute_input":"2026-05-05T13:20:12.197108Z","iopub.status.idle":"2026-05-05T13:20:12.563465Z","shell.execute_reply.started":"2026-05-05T13:20:12.197077Z","shell.execute_reply":"2026-05-05T13:20:12.562709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 17 : Comparaison ResNet vs GoogLeNet vs Ensemble  (INCHANGE)\nresnet.eval(); googlenet.eval()\nres_indiv = {'ResNet50': [], 'GoogLeNet': [], 'Ensemble': []}\ntrue_labels = []\n \nwith torch.no_grad():\n    for imgs, labels in val_dl:\n        imgs = imgs.float().to(DEVICE)\n        with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=USE_AMP):\n            lr_ = resnet(imgs); lg_ = googlenet(imgs); le_ = (lr_ + lg_) / 2.0\n        res_indiv['ResNet50'].extend(lr_.argmax(1).cpu().numpy())\n        res_indiv['GoogLeNet'].extend(lg_.argmax(1).cpu().numpy())\n        res_indiv['Ensemble'].extend(le_.argmax(1).cpu().numpy())\n        true_labels.extend(labels.numpy())\n \nprint(f'{\"Model\":<12} {\"Accuracy\":>10} {\"F1\":>10} {\"Precision\":>12} {\"Recall\":>10}')\nprint('-' * 58)\nfor name, preds in res_indiv.items():\n    a = accuracy_score(true_labels, preds)\n    f = f1_score(true_labels, preds, average='weighted')\n    p = precision_score(true_labels, preds, average='weighted', zero_division=0)\n    r = recall_score(true_labels, preds, average='weighted')\n    tag = ' <- best' if name == 'Ensemble' else ''\n    print(f'{name:<12} {a*100:>9.2f}% {f*100:>9.2f}% {p*100:>11.2f}% {r*100:>9.2f}%{tag}')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:20:12.564677Z","iopub.execute_input":"2026-05-05T13:20:12.565065Z","iopub.status.idle":"2026-05-05T13:20:30.466426Z","shell.execute_reply.started":"2026-05-05T13:20:12.565038Z","shell.execute_reply":"2026-05-05T13:20:30.465510Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -- Cellule 18 : Sauvegarde finale  (INCHANGE)\ntorch.save({\n    'resnet_state'   : resnet.state_dict(),\n    'googlenet_state': googlenet.state_dict(),\n    'accuracy'       : acc,\n    'f1'             : f1,\n    'auc'            : auc,\n    'img_size'       : IMG_SIZE,\n    'preprocessing'  : 'multichannel [original, clahe, sobel]',\n    'split'          : f'{int(TRAIN_SPLIT*100)}/{int(VAL_SPLIT*100)}/{int(TEST_SPLIT*100)} train/val/test',\n    'dataset'        : 'RSNA Pneumonia Detection Challenge',\n    'fusion'         : 'mean',\n}, 'checkpoints/ensemble_final.pth')\n \nprint('Modele sauvegarde -> checkpoints/ensemble_final.pth')\nprint(f'   Accuracy : {acc*100:.2f}%')\nprint(f'   AUC-ROC  : {auc*100:.2f}%')\nprint(f'   F1-Score : {f1*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T13:20:30.467962Z","iopub.execute_input":"2026-05-05T13:20:30.468242Z","iopub.status.idle":"2026-05-05T13:20:30.657979Z","shell.execute_reply.started":"2026-05-05T13:20:30.468214Z","shell.execute_reply":"2026-05-05T13:20:30.656951Z"}},"outputs":[],"execution_count":null}]}