{"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":"code","source":"import os\nimport csv\nimport copy\nimport time\nimport random\nfrom pathlib import Path\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim import lr_scheduler\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision\nfrom torchvision import models, transforms\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    classification_report,\n    confusion_matrix,\n    precision_score,\n    recall_score,\n    f1_score,\n    accuracy_score,\n    roc_auc_score,\n)\n\nfrom tqdm.auto import tqdm\n\nprint(\"Torch version      :\", torch.__version__)\nprint(\"Torchvision version:\", torchvision.__version__)\nprint(\"CUDA available     :\", torch.cuda.is_available())\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:55:28.621017Z","iopub.execute_input":"2026-05-07T03:55:28.621760Z","iopub.status.idle":"2026-05-07T03:55:37.979950Z","shell.execute_reply.started":"2026-05-07T03:55:28.621731Z","shell.execute_reply":"2026-05-07T03:55:37.979284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 1 · Configuration\n# =============================================================================\n\ndef resolve_rsna_dataset():\n    candidate_dirs = [\n        Path(\"/kaggle/input/competitions/rsna-pneumonia-detection-challenge\"),\n    ]\n\n    for path in candidate_dirs:\n        if (\n            path.exists()\n            and (path / \"stage_2_train_images\").exists()\n            and (path / \"stage_2_train_labels.csv\").exists()\n        ):\n            return path\n\n    raise FileNotFoundError(\n        \"Dataset RSNA introuvable. \"\n        \"Ajoute 'rsna-pneumonia-detection-challenge' au notebook.\"\n    )\n\nDATA_DIR = resolve_rsna_dataset()\n\nOUTPUT_DIR = Path(\"/kaggle/working/outputs_rsna\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\nBATCH_SIZE  = 8\nNUM_WORKERS = 0\nNUM_EPOCHS  = 5\nLR          = 1e-4\nSTEP_SIZE   = 2\nGAMMA_LR    = 0.1\nINPUT_SIZE  = 224\nNUM_CLASSES = 2\n\nTRAIN_SIZE  = 0.8\nVAL_SIZE    = 0.1\nTEST_SIZE   = 0.1\n\nSEED = 42\n\nCLASS_NAMES  = [\"NORMAL\", \"PNEUMONIA\"]\nCLASS_TO_IDX = {\"NORMAL\": 0, \"PNEUMONIA\": 1}\n\nDEVICE = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\nprint(\"Device  :\", DEVICE)\nprint(\"DATA_DIR:\", DATA_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:55:37.981247Z","iopub.execute_input":"2026-05-07T03:55:37.981674Z","iopub.status.idle":"2026-05-07T03:55:37.991378Z","shell.execute_reply.started":"2026-05-07T03:55:37.981646Z","shell.execute_reply":"2026-05-07T03:55:37.990656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 2 · Reproductibilité\n# =============================================================================\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seed(SEED)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:55:37.992160Z","iopub.execute_input":"2026-05-07T03:55:37.992467Z","iopub.status.idle":"2026-05-07T03:55:38.021011Z","shell.execute_reply.started":"2026-05-07T03:55:37.992436Z","shell.execute_reply":"2026-05-07T03:55:38.020468Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 3 · Construction DataFrame RSNA\n# =============================================================================\n\nlabels_csv = DATA_DIR / \"stage_2_train_labels.csv\"\nclass_info_csv = DATA_DIR / \"stage_2_detailed_class_info.csv\"\nimages_dir = DATA_DIR / \"stage_2_train_images\"\n\nlabels_df = pd.read_csv(labels_csv)\nclass_df  = pd.read_csv(class_info_csv)\n\n# RSNA :\n# Target = 1 -> PNEUMONIA\n# Target = 0 -> NORMAL\n\npatient_targets = labels_df.groupby(\"patientId\")[\"Target\"].max().reset_index()\n\npatient_targets[\"class_name\"] = patient_targets[\"Target\"].map({\n    0: \"NORMAL\",\n    1: \"PNEUMONIA\"\n})\n\npatient_targets[\"label\"] = patient_targets[\"Target\"]\n\nrecords = []\n\nfor _, row in patient_targets.iterrows():\n\n    patient_id = row[\"patientId\"]\n\n    img_path = images_dir / f\"{patient_id}.dcm\"\n\n    if not img_path.exists():\n        continue\n\n    records.append({\n        \"path\": str(img_path),\n        \"filename\": f\"{patient_id}.dcm\",\n        \"study_id\": patient_id,\n        \"class_name\": row[\"class_name\"],\n        \"label\": int(row[\"label\"]),\n    })\n\nct_df = pd.DataFrame(records)\n\nprint(\"\\nTotal images :\", len(ct_df))\nprint(ct_df[\"class_name\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:55:38.021931Z","iopub.execute_input":"2026-05-07T03:55:38.022556Z","iopub.status.idle":"2026-05-07T03:56:56.208795Z","shell.execute_reply.started":"2026-05-07T03:55:38.022531Z","shell.execute_reply":"2026-05-07T03:56:56.207896Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 4 · Split 80/10/10\n# =============================================================================\n\nstudy_df = ct_df[[\"study_id\", \"label\"]].drop_duplicates()\n\ntrain_studies, temp_studies = train_test_split(\n    study_df,\n    test_size=(VAL_SIZE + TEST_SIZE),\n    random_state=SEED,\n    stratify=study_df[\"label\"],\n)\n\nval_studies, test_studies = train_test_split(\n    temp_studies,\n    test_size=TEST_SIZE / (VAL_SIZE + TEST_SIZE),\n    random_state=SEED,\n    stratify=temp_studies[\"label\"],\n)\n\ntrain_df = ct_df[\n    ct_df[\"study_id\"].isin(train_studies[\"study_id\"])\n].reset_index(drop=True)\n\nval_df = ct_df[\n    ct_df[\"study_id\"].isin(val_studies[\"study_id\"])\n].reset_index(drop=True)\n\ntest_df = ct_df[\n    ct_df[\"study_id\"].isin(test_studies[\"study_id\"])\n].reset_index(drop=True)\n\nprint(f\"\\nTrain : {len(train_df)}\")\nprint(f\"Val   : {len(val_df)}\")\nprint(f\"Test  : {len(test_df)}\")\n\nprint(\"\\nTrain :\", train_df[\"class_name\"].value_counts().to_dict())\nprint(\"Val   :\", val_df[\"class_name\"].value_counts().to_dict())\nprint(\"Test  :\", test_df[\"class_name\"].value_counts().to_dict())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.210873Z","iopub.execute_input":"2026-05-07T03:56:56.211244Z","iopub.status.idle":"2026-05-07T03:56:56.265629Z","shell.execute_reply.started":"2026-05-07T03:56:56.211212Z","shell.execute_reply":"2026-05-07T03:56:56.264850Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 5 · DICOM Reader\n# =============================================================================\n\nimport pydicom\n\ndef read_dicom(path):\n    dicom = pydicom.dcmread(path)\n    image = dicom.pixel_array.astype(np.float32)\n\n    image -= image.min()\n\n    if image.max() != 0:\n        image /= image.max()\n\n    image = (image * 255).astype(np.uint8)\n\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.266590Z","iopub.execute_input":"2026-05-07T03:56:56.266898Z","iopub.status.idle":"2026-05-07T03:56:56.820380Z","shell.execute_reply.started":"2026-05-07T03:56:56.266854Z","shell.execute_reply":"2026-05-07T03:56:56.819650Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 6 · Multi-channel Preprocessing\n# =============================================================================\n\ndef gamma_correction(img_uint8: np.ndarray, gamma: float = 0.5):\n\n    inv_gamma = 1.0 / gamma\n\n    lut = np.array(\n        [((i / 255.0) ** inv_gamma) * 255 for i in range(256)],\n        dtype=np.uint8\n    )\n\n    return cv2.LUT(img_uint8, lut)\n\ndef sobel_magnitude(img_uint8: np.ndarray):\n\n    sobelx = cv2.Sobel(img_uint8, cv2.CV_32F, 1, 0, ksize=3)\n    sobely = cv2.Sobel(img_uint8, cv2.CV_32F, 0, 1, ksize=3)\n\n    mag = np.sqrt(sobelx ** 2 + sobely ** 2)\n\n    p99 = np.percentile(mag, 99)\n\n    mag = np.clip(mag / (p99 + 1e-7), 0.0, 1.0)\n\n    return mag.astype(np.float32)\n\ndef build_multichannel(path_str: str, img_size: int = INPUT_SIZE):\n\n    img = read_dicom(path_str)\n\n    if img is None:\n        return np.zeros((img_size, img_size, 3), dtype=np.float32)\n\n    img = cv2.resize(\n        img,\n        (img_size, img_size),\n        interpolation=cv2.INTER_AREA\n    )\n\n    denoised = cv2.bilateralFilter(\n        img,\n        d=9,\n        sigmaColor=75,\n        sigmaSpace=75\n    )\n\n    clahe = cv2.createCLAHE(\n        clipLimit=2.0,\n        tileGridSize=(8, 8)\n    )\n\n    ch0 = clahe.apply(denoised).astype(np.float32) / 255.0\n\n    ch1 = sobel_magnitude(denoised)\n\n    ch2 = gamma_correction(\n        denoised,\n        gamma=0.5\n    ).astype(np.float32) / 255.0\n\n    return np.stack([ch0, ch1, ch2], axis=-1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.821286Z","iopub.execute_input":"2026-05-07T03:56:56.821529Z","iopub.status.idle":"2026-05-07T03:56:56.830074Z","shell.execute_reply.started":"2026-05-07T03:56:56.821506Z","shell.execute_reply":"2026-05-07T03:56:56.829377Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 7 · Transforms\n# =============================================================================\n\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\n\ntrain_aug = transforms.Compose([\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.ColorJitter(brightness=0.1, contrast=0.1),\n])\n\nbase_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.831817Z","iopub.execute_input":"2026-05-07T03:56:56.832402Z","iopub.status.idle":"2026-05-07T03:56:56.847148Z","shell.execute_reply.started":"2026-05-07T03:56:56.832369Z","shell.execute_reply":"2026-05-07T03:56:56.846433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 8 · Dataset\n# =============================================================================\n\nclass PneumoniaDataset(Dataset):\n\n    def __init__(self, df: pd.DataFrame, augment: bool = False):\n        self.df = df.reset_index(drop=True)\n        self.augment = augment\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n\n        row = self.df.loc[idx]\n\n        img_path = row[\"path\"]\n        label = int(row[\"label\"])\n\n        multichannel = build_multichannel(img_path)\n\n        pil_img = Image.fromarray(\n            (multichannel * 255).astype(np.uint8)\n        )\n\n        if self.augment:\n            pil_img = train_aug(pil_img)\n\n        tensor = base_transform(pil_img)\n\n        return tensor, label\n\n    def get_filename(self, idx):\n        return self.df.loc[idx, \"filename\"]\n\n    def get_filepath(self, idx):\n        return self.df.loc[idx, \"path\"]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.848164Z","iopub.execute_input":"2026-05-07T03:56:56.848569Z","iopub.status.idle":"2026-05-07T03:56:56.859585Z","shell.execute_reply.started":"2026-05-07T03:56:56.848537Z","shell.execute_reply":"2026-05-07T03:56:56.858737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 9 · DataLoaders\n# =============================================================================\n\ntrain_dataset = PneumoniaDataset(train_df, augment=True)\nval_dataset   = PneumoniaDataset(val_df, augment=False)\ntest_dataset  = PneumoniaDataset(test_df, augment=False)\n\ndataset_sizes = {\n    \"train\": len(train_dataset),\n    \"val\": len(val_dataset),\n    \"test\": len(test_dataset),\n}\n\ndataloaders = {\n    \"train\": DataLoader(\n        train_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=True,\n        num_workers=NUM_WORKERS,\n        pin_memory=torch.cuda.is_available()\n    ),\n\n    \"val\": DataLoader(\n        val_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=torch.cuda.is_available()\n    ),\n\n    \"test\": DataLoader(\n        test_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=torch.cuda.is_available()\n    ),\n}\n\nprint(\"\\nDataset sizes :\", dataset_sizes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.860487Z","iopub.execute_input":"2026-05-07T03:56:56.860721Z","iopub.status.idle":"2026-05-07T03:56:56.873512Z","shell.execute_reply.started":"2026-05-07T03:56:56.860700Z","shell.execute_reply":"2026-05-07T03:56:56.872619Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 10 · Visualisation\n# =============================================================================\n\ndef visualize_channels(dataset: PneumoniaDataset, idx: int = 0):\n\n    row = dataset.df.loc[idx]\n\n    raw = read_dicom(row[\"path\"])\n\n    raw = cv2.resize(raw, (INPUT_SIZE, INPUT_SIZE))\n\n    denoised = cv2.bilateralFilter(\n        raw,\n        d=9,\n        sigmaColor=75,\n        sigmaSpace=75\n    )\n\n    clahe = cv2.createCLAHE(\n        clipLimit=2.0,\n        tileGridSize=(8, 8)\n    )\n\n    ch0_vis = clahe.apply(denoised)\n\n    ch1_vis = (\n        sobel_magnitude(denoised) * 255\n    ).astype(np.uint8)\n\n    ch2_vis = gamma_correction(\n        denoised,\n        gamma=0.5\n    )\n\n    fig, axes = plt.subplots(1, 4, figsize=(16, 4))\n\n    for ax, im, title in zip(\n        axes,\n        [raw, ch0_vis, ch1_vis, ch2_vis],\n        [\"Original\", \"CLAHE\", \"Sobel\", \"Gamma\"]\n    ):\n        ax.imshow(im, cmap=\"gray\")\n        ax.set_title(title)\n        ax.axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n\nvisualize_channels(train_dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:56.874639Z","iopub.execute_input":"2026-05-07T03:56:56.875152Z","iopub.status.idle":"2026-05-07T03:56:57.429377Z","shell.execute_reply.started":"2026-05-07T03:56:56.875128Z","shell.execute_reply":"2026-05-07T03:56:57.428617Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 11 · Modèle ResNet18\n# =============================================================================\n\ndef build_model(num_classes: int = NUM_CLASSES):\n\n    model = models.resnet18(\n        weights=models.ResNet18_Weights.DEFAULT\n    )\n\n    num_ftrs = model.fc.in_features\n\n    model.fc = nn.Linear(num_ftrs, num_classes)\n\n    return model.to(DEVICE)\n\ndef plot_history(train_values, val_values, metric_name):\n\n    plt.figure(figsize=(8, 5))\n\n    plt.plot(\n        range(1, len(train_values)+1),\n        train_values,\n        label=f\"Train {metric_name}\"\n    )\n\n    plt.plot(\n        range(1, len(val_values)+1),\n        val_values,\n        label=f\"Val {metric_name}\"\n    )\n\n    plt.title(f\"{metric_name} — ResNet18 Multi-channel\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(metric_name)\n\n    plt.legend()\n\n    plt.tight_layout()\n\n    plt.savefig(\n        OUTPUT_DIR / f\"{metric_name.lower()}_resnet18.png\",\n        dpi=150\n    )\n\n    plt.close()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:57.430427Z","iopub.execute_input":"2026-05-07T03:56:57.430940Z","iopub.status.idle":"2026-05-07T03:56:57.437165Z","shell.execute_reply.started":"2026-05-07T03:56:57.430897Z","shell.execute_reply":"2026-05-07T03:56:57.436433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 12 · Entraînement\n# =============================================================================\n\ndef train_model(\n    model,\n    criterion,\n    optimizer,\n    scheduler,\n    num_epochs=NUM_EPOCHS\n):\n\n    since = time.time()\n\n    best_wts = copy.deepcopy(model.state_dict())\n\n    best_acc = 0.0\n\n    train_loss_h = []\n    val_loss_h = []\n\n    train_acc_h = []\n    val_acc_h = []\n\n    for epoch in range(num_epochs):\n\n        print(f\"\\nEpoch {epoch+1}/{num_epochs}\")\n\n        for phase in [\"train\", \"val\"]:\n\n            if phase == \"train\":\n                model.train()\n            else:\n                model.eval()\n\n            running_loss = 0.0\n            running_correct = 0\n\n            for inputs, labels in tqdm(\n                dataloaders[phase],\n                desc=phase,\n                leave=False\n            ):\n\n                inputs = inputs.to(DEVICE)\n                labels = labels.to(DEVICE)\n\n                optimizer.zero_grad()\n\n                with torch.set_grad_enabled(phase == \"train\"):\n\n                    logits = model(inputs)\n\n                    loss = criterion(logits, labels)\n\n                    _, preds = torch.max(logits, 1)\n\n                    if phase == \"train\":\n                        loss.backward()\n                        optimizer.step()\n\n                running_loss += loss.item() * inputs.size(0)\n\n                running_correct += (\n                    preds == labels\n                ).sum().item()\n\n            if phase == \"train\":\n                scheduler.step()\n\n            epoch_loss = running_loss / dataset_sizes[phase]\n\n            epoch_acc = running_correct / dataset_sizes[phase]\n\n            print(\n                f\"{phase} Loss: {epoch_loss:.4f} \"\n                f\"Acc: {epoch_acc:.4f}\"\n            )\n\n            if phase == \"train\":\n                train_loss_h.append(epoch_loss)\n                train_acc_h.append(epoch_acc)\n\n            else:\n                val_loss_h.append(epoch_loss)\n                val_acc_h.append(epoch_acc)\n\n                if epoch_acc > best_acc:\n                    best_acc = epoch_acc\n                    best_wts = copy.deepcopy(model.state_dict())\n\n    print(\n        f\"\\nTraining complete in \"\n        f\"{(time.time()-since)/60:.1f} min\"\n    )\n\n    print(f\"Best val Acc: {best_acc:.4f}\")\n\n    model.load_state_dict(best_wts)\n\n    plot_history(train_loss_h, val_loss_h, \"Loss\")\n    plot_history(train_acc_h, val_acc_h, \"Accuracy\")\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:57.438125Z","iopub.execute_input":"2026-05-07T03:56:57.438513Z","iopub.status.idle":"2026-05-07T03:56:57.456180Z","shell.execute_reply.started":"2026-05-07T03:56:57.438479Z","shell.execute_reply":"2026-05-07T03:56:57.455555Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 13 · Export probabilités\n# =============================================================================\n\ndef export_probabilities(\n    model,\n    dataset,\n    csv_path,\n    labels_csv_path\n):\n\n    model.eval()\n\n    rows = []\n    label_rows = []\n\n    loader = DataLoader(\n        dataset,\n        batch_size=1,\n        shuffle=False,\n        num_workers=0\n    )\n\n    correct = 0\n\n    with torch.no_grad():\n\n        for i, (inputs, labels) in enumerate(loader):\n\n            inputs = inputs.to(DEVICE)\n            labels = labels.to(DEVICE)\n\n            logits = model(inputs)\n\n            probs = torch.softmax(logits, dim=1)\n\n            _, predicted = torch.max(logits, 1)\n\n            rows.append({\n                \"filename\": dataset.get_filename(i),\n                \"prob_Normal\": probs[0][0].item(),\n                \"prob_Pneumonia\": probs[0][1].item(),\n                \"predicted_label\": predicted.item(),\n                \"true_label\": labels.item(),\n            })\n\n            label_rows.append({\n                \"filename\": dataset.get_filename(i),\n                \"label\": labels.item()\n            })\n\n            correct += (\n                predicted == labels\n            ).sum().item()\n\n    pd.DataFrame(rows).to_csv(csv_path, index=False)\n\n    pd.DataFrame(label_rows).to_csv(\n        labels_csv_path,\n        index=False\n    )\n\n    print(f\"Export accuracy : {correct/len(dataset):.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:57.458361Z","iopub.execute_input":"2026-05-07T03:56:57.458558Z","iopub.status.idle":"2026-05-07T03:56:57.471805Z","shell.execute_reply.started":"2026-05-07T03:56:57.458540Z","shell.execute_reply":"2026-05-07T03:56:57.471177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 14 · Training\n# =============================================================================\n\nmodel = build_model(NUM_CLASSES)\n\ncriterion = nn.CrossEntropyLoss()\n\noptimizer = optim.Adam(\n    model.parameters(),\n    lr=LR\n)\n\nscheduler = lr_scheduler.StepLR(\n    optimizer,\n    step_size=STEP_SIZE,\n    gamma=GAMMA_LR\n)\n\nmodel = train_model(\n    model,\n    criterion,\n    optimizer,\n    scheduler,\n    num_epochs=NUM_EPOCHS\n)\n\nexport_probabilities(\n    model,\n    dataset=test_dataset,\n    csv_path=OUTPUT_DIR / \"resnet18_rsna_test.csv\",\n    labels_csv_path=OUTPUT_DIR / \"test_labels.csv\",\n)\n\ntorch.save(\n    model.state_dict(),\n    OUTPUT_DIR / \"resnet18_rsna_multichannel.pth\"\n)\n\nprint(\"Model saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T03:56:57.472760Z","iopub.execute_input":"2026-05-07T03:56:57.473055Z","iopub.status.idle":"2026-05-07T04:50:59.023929Z","shell.execute_reply.started":"2026-05-07T03:56:57.473025Z","shell.execute_reply":"2026-05-07T04:50:59.023147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 15 · Évaluation finale\n# =============================================================================\n\ndef show_metrics(labels, predictions, prob_matrix, classes):\n\n    accuracy = accuracy_score(labels, predictions)\n\n    precision = precision_score(\n        labels,\n        predictions,\n        zero_division=0\n    )\n\n    recall = recall_score(\n        labels,\n        predictions,\n        zero_division=0\n    )\n\n    f1 = f1_score(\n        labels,\n        predictions,\n        zero_division=0\n    )\n\n    auc = roc_auc_score(\n        labels,\n        prob_matrix[:, 1]\n    )\n\n    print(\"\\n──────── FINAL RESULTS ────────\")\n\n    print(f\"Accuracy  : {accuracy:.4f}\")\n    print(f\"Precision : {precision:.4f}\")\n    print(f\"Recall    : {recall:.4f}\")\n    print(f\"F1-Score  : {f1:.4f}\")\n    print(f\"AUC       : {auc:.4f}\")\n\n    print(\"\\nClassification Report:\\n\")\n\n    print(\n        classification_report(\n            labels,\n            predictions,\n            target_names=classes,\n            digits=4,\n            zero_division=0\n        )\n    )\n\n    matrix = confusion_matrix(labels, predictions)\n\n    plt.figure(figsize=(5, 4))\n\n    plt.imshow(matrix, cmap=\"Blues\")\n\n    plt.title(\"Confusion Matrix — ResNet18 RSNA\")\n\n    plt.xlabel(\"Predicted\")\n    plt.ylabel(\"True\")\n\n    plt.xticks(range(len(classes)), classes)\n    plt.yticks(range(len(classes)), classes)\n\n    for i in range(matrix.shape[0]):\n        for j in range(matrix.shape[1]):\n\n            plt.text(\n                j,\n                i,\n                matrix[i, j],\n                ha=\"center\",\n                va=\"center\",\n                color=(\n                    \"white\"\n                    if matrix[i, j] > matrix.max()/2\n                    else \"black\"\n                )\n            )\n\n    plt.tight_layout()\n\n    plt.savefig(\n        OUTPUT_DIR / \"confusion_matrix.png\",\n        dpi=150\n    )\n\n    plt.show()\n\n    metric_names = [\n        \"Accuracy\",\n        \"Precision\",\n        \"Recall\",\n        \"F1-Score\",\n        \"AUC\"\n    ]\n\n    metric_vals = [\n        accuracy,\n        precision,\n        recall,\n        f1,\n        auc\n    ]\n\n    plt.figure(figsize=(7, 4))\n\n    bars = plt.bar(metric_names, metric_vals)\n\n    plt.ylim(0, 1.05)\n\n    plt.title(\"Metrics — ResNet18 RSNA\")\n\n    plt.ylabel(\"Score\")\n\n    for bar, val in zip(bars, metric_vals):\n\n        plt.text(\n            bar.get_x() + bar.get_width()/2,\n            val + 0.01,\n            f\"{val:.3f}\",\n            ha=\"center\"\n        )\n\n    plt.tight_layout()\n\n    plt.savefig(\n        OUTPUT_DIR / \"metrics_bar.png\",\n        dpi=150\n    )\n\n    plt.show()\n\n    summary = pd.DataFrame({\n        \"Metric\": metric_names,\n        \"Value\": metric_vals\n    })\n\n    return summary","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T04:51:31.511838Z","iopub.execute_input":"2026-05-07T04:51:31.512326Z","iopub.status.idle":"2026-05-07T04:51:31.523170Z","shell.execute_reply.started":"2026-05-07T04:51:31.512297Z","shell.execute_reply":"2026-05-07T04:51:31.522506Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# 16 · Calcul final\n# =============================================================================\n\ndf_probs = pd.read_csv(\n    OUTPUT_DIR / \"resnet18_rsna_test.csv\"\n)\n\ndf_labels = pd.read_csv(\n    OUTPUT_DIR / \"test_labels.csv\"\n)\n\nprobs = df_probs[\n    [\"prob_Normal\", \"prob_Pneumonia\"]\n].values.astype(np.float64)\n\nlabels = df_labels[\"label\"].astype(int).values\n\npreds = np.argmax(probs, axis=1).astype(int)\n\nsummary = show_metrics(\n    labels,\n    preds,\n    probs,\n    CLASS_NAMES\n)\n\nsummary.to_csv(\n    OUTPUT_DIR / \"final_metrics_summary.csv\",\n    index=False\n)\n\nprint(\"Saved :\", OUTPUT_DIR / \"final_metrics_summary.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-07T04:51:54.100032Z","iopub.execute_input":"2026-05-07T04:51:54.100483Z","iopub.status.idle":"2026-05-07T04:51:54.515633Z","shell.execute_reply.started":"2026-05-07T04:51:54.100445Z","shell.execute_reply":"2026-05-07T04:51:54.514980Z"}},"outputs":[],"execution_count":null}]}