{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13441085},{"sourceType":"datasetVersion","sourceId":12780021,"datasetId":8079690,"databundleVersionId":13404554},{"sourceType":"kernelVersion","sourceId":258223905}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":84.2572,"end_time":"2025-08-16T12:16:24.888146","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-08-16T12:15:00.630946","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import sys\nimport os\nimport multiprocessing\nfrom pathlib import Path\nfrom typing import List, Dict, Optional, Tuple\nimport shutil\nimport gc\nimport inspect\n# Data\nimport json\nimport polars as pl\nimport pandas as pd\n\n# Maths\nimport numpy as np\n# Image\nimport pydicom\nimport cv2\nfrom scipy import ndimage\nfrom scipy.ndimage import zoom\n# ML/DL\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.cuda.amp import autocast\n# Train\nfrom torch.utils.data import DataLoader\nimport torch.optim as optim\nimport timm\nfrom tqdm import tqdm\n\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport warnings\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:19:47.162361Z","iopub.execute_input":"2025-08-26T14:19:47.162533Z","iopub.status.idle":"2025-08-26T14:20:34.730927Z","shell.execute_reply.started":"2025-08-26T14:19:47.162517Z","shell.execute_reply":"2025-08-26T14:20:34.73013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Seeding for reproducibility\ndef set_global_seed(seed: int):\n    import random\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n    try:\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n    except Exception:\n        pass\n    \n# Set device\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"DEVICE:\", DEVICE)\n\n# ====================================================\n# Competition constants\n# ====================================================\nSERIES_ROOT_TRAIN = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"\nTRAIN_CSV         = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\"\nLOCALIZER_CSV     = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv\"\n\nID_COL = 'SeriesInstanceUID'\nLABEL_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present',\n]\nTARGET_COL = 'Aneurysm Present'\n\nDEBUG = False\nTRAIN = False\n\nTARGET_SIZE = (32, 384, 384) \n\nMODEL_DIR = '/kaggle/input/efficientnetv2-s'#''/kaggle/input/rsna2025-effnetv2-32ch'\nMODEL_NAME = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n\nset_global_seed(42)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:34.73167Z","iopub.execute_input":"2025-08-26T14:20:34.731917Z","iopub.status.idle":"2025-08-26T14:20:34.812646Z","shell.execute_reply.started":"2025-08-26T14:20:34.731892Z","shell.execute_reply":"2025-08-26T14:20:34.811928Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Processing","metadata":{}},{"cell_type":"code","source":"class DICOMPreprocessorKaggle:\n    \"\"\"\n    DICOM preprocessing system for Kaggle Code Competition\n    Converts original DICOMPreprocessor logic to single series processing\n    \"\"\"\n    \n    def __init__(self, target_shape: Tuple[int, int, int] = TARGET_SIZE):\n        self.target_depth, self.target_height, self.target_width = target_shape\n\n    def _process_slice(self, img): #TODO\n        img_target_shape = (self.target_height, self.target_width)\n        if img_target_shape[0] > np.shape(img)[0] or img_target_shape[1] > np.shape(img)[1]:\n            interpolation = cv2.INTER_CUBIC  # better for upscaling\n        else:\n            interpolation = cv2.INTER_AREA   # better for downscaling\n        image = cv2.resize(img, img_target_shape, interpolation=interpolation)\n        return np.array(image).astype(np.float32)\n    \n    def extract_slice_info(self, datasets: List[pydicom.Dataset]) -> List[Dict]:\n        \"\"\"\n        Extract position information for each slice\n        \"\"\"\n        slice_info = []\n        \n        for i, ds in enumerate(datasets):\n            info = {\n                'dataset': ds,\n                'index': i,\n                'instance_number': getattr(ds, 'InstanceNumber', i),\n            }\n            \n            # Get z-coordinate from ImagePositionPatient\n            try:\n                position = getattr(ds, 'ImagePositionPatient', None)\n                if position is not None and len(position) >= 3:\n                    info['z_position'] = float(position[2])\n                else:\n                    # Fallback: use InstanceNumber\n                    info['z_position'] = float(info['instance_number'])\n            except Exception as e:\n                info['z_position'] = float(i)\n            \n            slice_info.append(info)\n        \n        return slice_info\n    \n    def sort_slices_by_position(self, slice_info: List[Dict]) -> List[Dict]:\n        \"\"\"\n        Sort slices by z-coordinate\n        \"\"\"\n        # Sort by z-coordinate\n        sorted_slices = sorted(slice_info, key=lambda x: x['z_position'])\n        \n        if DEBUG:\n            print(f\"Sorted {len(sorted_slices)} slices by z-position\")\n            print(f\"Z-range: {sorted_slices[0]['z_position']:.2f} to {sorted_slices[-1]['z_position']:.2f}\")\n        \n        return sorted_slices\n\n    \n    def process_series(self, series_path: str) -> np.ndarray:\n        \"\"\"\n        Process DICOM series and return as NumPy array (for Kaggle: no file saving)\n        \"\"\"\n        volume_target_shape = (self.target_depth, self.target_height, self.target_width)\n        dicom_files =  sorted(os.listdir(series_path))\n        \n        if len(dicom_files) == 1:\n            dcm = pydicom.dcmread(os.path.join(series_path, dicom_files[0]))\n            volume = dcm.pixel_array  # (D, H, W)\n            processed_slices = [self._process_slice(slice_) for slice_ in volume]\n        else:\n            slices = [pydicom.dcmread(os.path.join(series_path, f)).pixel_array\n                    for f in dicom_files]\n            slices_info = self.extract_slice_info(slices)\n            sorted_slices = self.sort_slices_by_position(slices_info)\n            processed_slices = [self._process_slice(slice_['dataset']) for slice_ in sorted_slices]\n        volume = np.array(processed_slices, dtype=np.float32)\n        volume = volume.astype(np.float32)\n        volume = (volume - volume.min()) / (volume.max() - volume.min() + 1e-8)\n        factors = [t / s for s, t in zip(volume.shape, volume_target_shape)]\n        if DEBUG:\n            print(series_path,len(dicom_files), volume.shape, flush = True)\n        resized_vol = zoom(volume, zoom=factors, order=1)\n        \n        return resized_vol\n    \n\ndef process_dicom_series_kaggle(series_path: str, target_shape: Tuple[int, int, int] = TARGET_SIZE) -> np.ndarray:\n    \"\"\"\n    DICOM processing function for Kaggle inference (single series)\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n    return preprocessor.process_series(series_path)\n\n# Safe processing function with memory cleanup\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = TARGET_SIZE) -> np.ndarray:\n    \"\"\"\n    Safe DICOM processing with memory cleanup\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        volume = process_dicom_series_kaggle(series_path, target_shape)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\n# Test function\ndef test_single_series(series_path: str, target_shape: Tuple[int, int, int] = TARGET_SIZE):\n    \"\"\"\n    Test processing for single series\n    \"\"\"\n    if DEBUG:\n        print(f\"Testing single series: {series_path}\")\n    \n    # Execute processing\n    volume = process_dicom_series_safe(series_path, target_shape)\n    \n    # Display results\n    if DEBUG:\n        print(f\"  Successfully processed series\")\n        print(f\"  Volume shape: {volume.shape}\")\n        print(f\"  Volume dtype: {volume.dtype}\")\n        print(f\"  Volume range: [{volume.min()}, {volume.max()}]\")\n    \n    return volume","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":1.252437,"end_time":"2025-08-16T12:15:06.010441","exception":false,"start_time":"2025-08-16T12:15:04.758004","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:34.813879Z","iopub.execute_input":"2025-08-26T14:20:34.814096Z","iopub.status.idle":"2025-08-26T14:20:34.879287Z","shell.execute_reply.started":"2025-08-26T14:20:34.814078Z","shell.execute_reply":"2025-08-26T14:20:34.87866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset\nimport numpy as np\n\nTRANSFORM = A.Compose([\n    A.RandomBrightnessContrast(p=0.2),\n    A.ShiftScaleRotate(p=0.2),\n    A.GaussianBlur(p=0.1),\n    A.Normalize(mean=0.0, std=1.0),\n    ToTensorV2()\n])\n\n\nclass MyDataset(Dataset):\n    def __init__(self, df, label_cols, target_shape, transform=TRANSFORM, series_path = SERIES_ROOT_TRAIN):\n        \"\"\"\n        df : DataFrame avec au moins:\n            - 'series_path' : chemin dossier DICOM\n            - colonnes de labels (multi + binaire)\n        label_cols : liste dans l'ordre des colonnes de labels (multi + dernier=binaire)\n        target_shape : (depth, height, width)\n        transform : éventuelle transformation Torch/Albumentations\n        \"\"\"\n        self.df = df.reset_index(drop=True).replace(['', None, 'nan', 'NaN'], 0)\n        self.label_cols = label_cols\n        self.target_shape = target_shape\n        self.transform = transform\n        self.preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n        self.series_path = series_path\n        self.extract_labels_and_paths()\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # --- Prétraitement du volume DICOM ---\n        vol = self.preprocessor.process_series(row['series_path'])  # NumPy [D,H,W]\n        # Conversion en tenseur float32\n        vol_tensor = torch.from_numpy(vol).float()  # [D,H,W] \n        if self.transform:\n            # Si ton transform attend HWC -> permuter\n            vol_tensor = vol_tensor.permute(1, 2, 0)  # [H,W,C]\n            vol_tensor = self.transform(image=vol_tensor.numpy())[\"image\"]\n        \n        # --- Extraction des labels ---\n        vals = row[self.label_cols].to_numpy(dtype=np.float32, na_value=0.0)\n        labels_all = torch.tensor(vals, dtype=torch.float32)\n        label_binary = labels_all[-1]   # dernier = anévrisme 0/1\n        label_multi = torch.argmax(labels_all[:-1]).long()  # index classe fine\n\n        return vol_tensor, label_multi, label_binary\n        \n    def extract_labels_and_paths(self):\n        \"\"\"\n        Extrait labels_class, labels_binary et mes_paths\n        à partir d'un DataFrame fusionné.\n    \n        Args:\n            dicom_dir (str): Chemin racine contenant les fichiers DICOM.\n    \n        Returns:\n            tuple: (mes_paths, labels_class, labels_binary)\n        \"\"\"\n        # --- 1) labels_class ---\n        labels_class = []\n        for _, row in self.df.iterrows():\n            sub_labels = row[self.label_cols[:-1]]\n            if sub_labels.max() == 1:\n                idx = sub_labels[sub_labels == 1].index[0]\n                class_index = self.label_cols[:-1].index(idx)\n            else:\n                class_index = -1  # ou autre valeur sentinelle\n            labels_class.append(class_index)\n    \n        # --- 2) labels_binary ---\n        labels_binary = self.df['Aneurysm Present'].astype(int).tolist()\n        \n        # --- 3) mes_paths ---\n        mes_paths = [\n            os.path.join(self.series_path, str(uid))\n            for uid in self.df[\"SeriesInstanceUID\"]\n        ]\n        self.df[\"series_path\"] = mes_paths\n        return mes_paths, labels_class, labels_binary\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:34.879958Z","iopub.execute_input":"2025-08-26T14:20:34.880154Z","iopub.status.idle":"2025-08-26T14:20:34.902466Z","shell.execute_reply.started":"2025-08-26T14:20:34.880137Z","shell.execute_reply":"2025-08-26T14:20:34.901934Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Process","metadata":{}},{"cell_type":"markdown","source":"## Build model","metadata":{}},{"cell_type":"code","source":"# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\nclass CFG:\n    seed = 42\n    debug = False\n    train = False\n\n    # DICOM\n    target_shape = TARGET_SIZE\n    threshold = 0.2\n\n    # Modèle\n    backbone_name = \"tf_efficientnetv2_s.in21k_ft_in1k\"\n    in_chans = 32\n    num_classes = 13\n\n    model_params = {\n        \"backbone_name\": backbone_name,\n        \"in_chans\": in_chans,\n        \"num_classes\": num_classes,\n        \"pretrained\": False\n    }\n\n    backbone_params = {\n        \"drop_rate\": 0.2,\n        \"drop_path_rate\": 0.2,\n        \"global_pool\": \"avg\",\n        \"act_layer\": \"silu\",\n        \"output_stride\": 32\n    }\n\n    # Entraînement\n    batch_size = 15\n    num_workers = multiprocessing.cpu_count()\n    lr = 1e-4\n    weight_decay = 1e-5\n    alpha = 1.0\n    epochs = 3\n    n_folds = 1\n    ensemble_weights = {0: 1.0}\n    use_amp = True\n\n    # Chemins\n    model_dir = MODEL_DIR\n    data_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection\"\n    train_csv = f\"{data_dir}/train.csv\"\n    series_root = f\"{data_dir}/series\"\n\n    # Labels\n    label_cols = LABEL_COLS\n\n\n# ====================================================\n# Modèle hiérarchique\n# ====================================================\nclass EfficientNetV2Hierarchical(nn.Module):\n    def __init__(self, backbone_name, in_chans, num_classes, pretrained=False, **backbone_kwargs):\n        super().__init__()\n        self.backbone = timm.create_model(\n            backbone_name,\n            pretrained=pretrained,\n            num_classes=0,\n            in_chans=in_chans,\n            **backbone_kwargs\n        )\n        in_features = self.backbone.num_features\n        self.head_binary = nn.Linear(in_features, 1)\n        self.head_multiclass = nn.Linear(in_features, num_classes)\n\n    def forward(self, x):\n        feats = self.backbone(x)\n        out_bin = self.head_binary(feats)\n        out_multi = torch.softmax(self.head_multiclass(feats), dim=1)\n        return out_bin, out_multi\n\n# ====================================================\n# Chargement modèle\n# ====================================================\ndef load_hierarchical_model(model_path, backbone_name, in_chans, num_classes, device=DEVICE):\n    model = EfficientNetV2Hierarchical(\n        backbone_name=backbone_name,\n        in_chans=in_chans,\n        num_classes=num_classes,\n        pretrained=False\n    )\n    checkpoint = torch.load(model_path, map_location=device)\n    model.load_state_dict(checkpoint['model'])\n    model.to(device)\n    model.eval()\n    return model\n\n# ====================================================\n# Inférence hiérarchique\n# ====================================================\ndef hierarchical_inference(model, images, threshold=0.5):\n    \"\"\"\n    images: tensor [B, C, H, W]\n    Retourne: score binaire, prediction multiclasses ou None\n    \"\"\"\n    with torch.no_grad():\n        out_bin, out_multi = model(images)\n        prob_bin = torch.sigmoid(out_bin)                # probas en [0, 1]\n        has_aneurysm = (prob_bin >= threshold).squeeze(1)\n        \n        preds_multi = []\n        for i in range(images.size(0)):\n            if has_aneurysm[i]:\n                preds_multi.append(out_multi[i])\n            else:\n                preds_multi.append(None)\n                \n    return out_bin.cpu(), preds_multi","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:34.903491Z","iopub.execute_input":"2025-08-26T14:20:34.903732Z","iopub.status.idle":"2025-08-26T14:20:35.124764Z","shell.execute_reply.started":"2025-08-26T14:20:34.903707Z","shell.execute_reply":"2025-08-26T14:20:35.123909Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train model","metadata":{}},{"cell_type":"code","source":"def train_model(model, dataloader, optimizer, device, epochs=10, alpha=1.0):\n    scaler = torch.cuda.amp.GradScaler()  # précision mixte\n    bce_loss = nn.BCELoss()\n    ce_loss = nn.CrossEntropyLoss()\n\n    model.to(device)\n    model.train()\n    \n    for epoch in range(epochs):\n        running_loss = 0.0\n        loop = tqdm(dataloader, desc=f\"Epoch [{epoch+1}/{epochs}]\", leave=True)\n        \n        for images, y_multi, y_bin in loop:\n            images = images.to(device, non_blocking=True)\n            y_multi = y_multi.to(device, non_blocking=True)\n            y_bin = y_bin.float().to(device, non_blocking=True)\n\n            optimizer.zero_grad(set_to_none=True)\n            \n            with torch.cuda.amp.autocast():  # précision mixte\n                out_bin, out_multi = model(images)\n                loss_bin = bce_loss(out_bin.squeeze(1), y_bin)\n                loss_multi = ce_loss(out_multi, y_multi)\n                loss = loss_bin + alpha * loss_multi\n\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n            running_loss += loss.item() * images.size(0)\n            loop.set_postfix(loss=loss.item())\n\n        epoch_loss = running_loss / len(dataloader.dataset)\n        print(f\"Epoch {epoch+1} — Loss: {epoch_loss:.4f}\")\n        \n        # Libération mémoire intermédiaire\n        torch.cuda.empty_cache()\n\ndef train_model_hierarchical(model, dataloader, optimizer, device, epochs=10, alpha=1.0):\n    scaler = torch.cuda.amp.GradScaler()\n    bce_loss = nn.BCEWithLogitsLoss()\n    ce_loss = nn.CrossEntropyLoss()\n\n    model.to(device)\n\n    for epoch in range(epochs):\n        model.train()\n        running_loss = 0.0\n        loop = tqdm(dataloader, desc=f\"Epoch [{epoch+1}/{epochs}]\", leave=True)\n\n        for images, y_multi, y_bin in loop:\n            images = images.to(device, non_blocking=True)\n            y_multi = y_multi.to(device, non_blocking=True)\n            y_bin = y_bin.float().to(device, non_blocking=True)\n\n            optimizer.zero_grad(set_to_none=True)\n\n            with torch.cuda.amp.autocast():\n                out_bin, out_multi = model(images)\n                loss_bin = bce_loss(out_bin.squeeze(1), y_bin)\n\n                # --- Sélectionner uniquement les cas positifs pour la perte multi‑classe ---\n                pos_mask = (y_bin == 1)\n                if pos_mask.any():\n                    loss_multi = ce_loss(out_multi[pos_mask], y_multi[pos_mask])\n                else:\n                    loss_multi = torch.tensor(0.0, device=device)\n\n                loss = loss_bin + alpha * loss_multi\n\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n            running_loss += loss.item() * images.size(0)\n            loop.set_postfix(loss=loss.item())\n\n        epoch_loss = running_loss / len(dataloader.dataset)\n        print(f\"Epoch {epoch+1} — Loss: {epoch_loss:.4f}\")\n        if False:\n            # Sauvegarde intermédiaire\n            cfg_dict = {k: getattr(CFG, k) for k in dir(CFG) if not k.startswith(\"__\") and not callable(getattr(CFG, k))}\n            torch.save({\n            'epoch': epoch + 1,\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'cfg': cfg_dict\n            }, f\"checkpoint_epoch_{epoch}.pth\")\n            torch.cuda.empty_cache()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.125473Z","iopub.execute_input":"2025-08-26T14:20:35.12571Z","iopub.status.idle":"2025-08-26T14:20:35.137395Z","shell.execute_reply.started":"2025-08-26T14:20:35.125692Z","shell.execute_reply":"2025-08-26T14:20:35.136662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    # Hypothèse : tes données pré‑chargées\n    # images: Tensor [N, 32, 384, 384]\n    # labels: Tensor one‑hot [N, 14] (13 classes fines + 1 binaire)\n    # Ici juste du fake pour l’exemple\n    N = 64\n    \n    train_df = pd.read_csv(TRAIN_CSV)\n    # cta_df = train_df[train_df['Modality'] == \"CTA\"].sample(n=10, random_state=42)\n    dataset = MyDataset(train_df, label_cols=LABEL_COLS, target_shape=TARGET_SIZE, series_path = SERIES_ROOT_TRAIN)\n    dataloader = DataLoader(\n        dataset,\n        batch_size=15,          # petit batch pour la mémoire\n        shuffle=True,\n        num_workers=os.cpu_count(),         # ajuster selon le CPU\n        pin_memory=True        # accélère le transfert vers GPU\n    )\n    \n    model = EfficientNetV2Hierarchical(\n        backbone_name=\"tf_efficientnetv2_s.in21k_ft_in1k\",\n        in_chans=32,\n        num_classes=13,\n        pretrained=False\n    )\n    \n    optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)\n    \n    train_model_hierarchical(\n        model,\n        dataloader,\n        optimizer,\n        device=DEVICE,\n        epochs=3,\n        alpha=1.0    # pondération de la perte multi‑classe\n    )\n    \n    # Sauvegarde\n    torch.save({\n    'model': model.state_dict(),\n    'cfg': CFG.model_params,\n    'epoch': 3\n    }, \"hierarchical_model.pth\")\n    print(\"Modèle entraîné et sauvegardé avec succès.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.138233Z","iopub.execute_input":"2025-08-26T14:20:35.138519Z","iopub.status.idle":"2025-08-26T14:20:35.158475Z","shell.execute_reply.started":"2025-08-26T14:20:35.138494Z","shell.execute_reply":"2025-08-26T14:20:35.157979Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_models():\n    global MODELS\n    MODELS = {}\n\n    valid_params = inspect.signature(EfficientNetV2Hierarchical.__init__).parameters\n    allowed_keys = set(valid_params.keys()) - {'self'}\n    filtered_params = {k: v for k, v in CFG.model_params.items() if k in allowed_keys}\n\n    for fold in range(CFG.n_folds):\n        model_path = f\"{CFG.model_dir}/hierarchical_model.pth\"\n        model = EfficientNetV2Hierarchical(**filtered_params, **CFG.backbone_params).to(DEVICE)\n        state_dict = torch.load(model_path, map_location=DEVICE)\n        model.load_state_dict(state_dict['model'], strict=False)\n        model.eval()\n        MODELS[fold] = model\n\n    print(\"Modèle chargé avec succès.\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.159241Z","iopub.execute_input":"2025-08-26T14:20:35.159468Z","iopub.status.idle":"2025-08-26T14:20:35.178294Z","shell.execute_reply.started":"2025-08-26T14:20:35.159441Z","shell.execute_reply":"2025-08-26T14:20:35.177467Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prediction","metadata":{}},{"cell_type":"code","source":"# ====================================================\n# DICOM Processing\n# ====================================================\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = CFG.target_shape) -> np.ndarray:\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        gc.collect()\n\n# ====================================================\n# Prediction Functions\n# ====================================================\ndef predict_single_model(model: nn.Module, volume: np.ndarray) -> Dict:\n    assert isinstance(volume, np.ndarray), f\"Expected np.ndarray, got {type(volume)}\"\n\n    image = volume.transpose(1, 2, 0)  # (D,H,W) → (H,W,D)\n    transformed = TRANSFORM(image=image)\n    image_tensor = transformed['image'].unsqueeze(0).to(DEVICE)\n\n    with torch.no_grad():\n        with autocast(enabled=CFG.use_amp):\n            out_bin, out_multi = model(image_tensor)\n            prob_bin = torch.sigmoid(out_bin).item()\n            prob_multi = torch.softmax(out_multi, dim=1).squeeze(0).tolist()\n\n            if prob_bin >= CFG.threshold:\n                return {\n                    \"aneurysm_score\": prob_bin,\n                    \"class_prediction\": prob_multi\n                }\n            else:\n                return {\n                    \"aneurysm_score\": prob_bin,\n                    \"class_prediction\": [0.1] * CFG.num_classes\n                }\n\ndef predict_ensemble(volume: np.ndarray) -> Dict:\n    all_preds = []\n    weights = []\n\n    for fold, model in MODELS.items():\n        pred = predict_single_model(model, volume)\n        all_preds.append(pred)\n        weights.append(CFG.ensemble_weights.get(fold, 1.0))\n\n    weights = np.array(weights) / np.sum(weights)\n    aneurysm_scores = np.array([p[\"aneurysm_score\"] for p in all_preds])\n    class_preds = np.array([p[\"class_prediction\"] for p in all_preds])\n\n    final_score = float(np.average(aneurysm_scores, weights=weights))\n    final_class = list(np.average(class_preds, weights=weights, axis=0))\n\n    return {\n        \"aneurysm_score\": final_score,\n        \"class_prediction\": final_class\n    }\n\ndef predict_fallback(series_path: str) -> pl.DataFrame:\n    conservative_preds = [0.0] * len(CFG.label_cols)\n    predictions_df = pl.DataFrame(\n        data=[conservative_preds],\n        schema=CFG.label_cols,\n        orient='row'\n    )\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    return predictions_df\n\ndef _predict_inner(series_path: str) -> pl.DataFrame:\n    if not MODELS:\n        load_models()\n\n    series_id = os.path.basename(series_path)\n    volume = process_dicom_series_safe(series_path, CFG.target_shape)\n    prediction = predict_ensemble(volume)\n\n    row = [series_id] + prediction[\"class_prediction\"] + [prediction[\"aneurysm_score\"]]\n    schema = [ID_COL] + CFG.label_cols\n    predictions_df = pl.DataFrame(data=[row], schema=schema, orient='row')\n\n    return predictions_df.drop(ID_COL)\n\ndef predict(series_path: str) -> pl.DataFrame:\n    try:\n        predictions = _predict_inner(series_path)\n    except Exception as e:\n        print(f\"⚠️ Erreur sur {series_path}: {e}\")\n        predictions = predict_fallback(series_path)\n\n    shared_dir = '/kaggle/shared'\n    shutil.rmtree(shared_dir, ignore_errors=True)\n    os.makedirs(shared_dir, exist_ok=True)\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    gc.collect()\n\n    return predictions\n\ndef predict_batch(images: torch.Tensor, model: nn.Module) -> List[Dict]:\n    images = images.to(DEVICE)\n    with torch.no_grad():\n        out_bin, out_multi = model(images)\n        prob_bin = torch.sigmoid(out_bin).squeeze(1)\n        has_aneurysm = (prob_bin >= CFG.threshold)\n\n        results = []\n        for i in range(images.size(0)):\n            score = prob_bin[i].item()\n            if has_aneurysm[i]:\n                class_scores = out_multi[i].softmax(dim=0).tolist()\n            else:\n                class_scores = [0.1] * CFG.num_classes\n\n            results.append({\n                \"aneurysm_score\": score,\n                \"class_prediction\": class_scores\n            })\n\n    return results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.179973Z","iopub.execute_input":"2025-08-26T14:20:35.180305Z","iopub.status.idle":"2025-08-26T14:20:35.204025Z","shell.execute_reply.started":"2025-08-26T14:20:35.180281Z","shell.execute_reply":"2025-08-26T14:20:35.203193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if DEBUG:\n    load_models()\n    serie_name = \"1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\"\n    test_path = os.path.join(CFG.series_root, serie_name)\n    volume = process_dicom_series_safe(test_path)\n    print(\"Shape:\", volume.shape)\n    print(\"Min/Max:\", volume.min(), volume.max())\n\n    test_path = os.path.join(CFG.series_root, serie_name)\n    pred_df = predict(test_path)\n    print(pred_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.204731Z","iopub.execute_input":"2025-08-26T14:20:35.204967Z","iopub.status.idle":"2025-08-26T14:20:35.222114Z","shell.execute_reply.started":"2025-08-26T14:20:35.204935Z","shell.execute_reply":"2025-08-26T14:20:35.221571Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if DEBUG:\n    train_df = pd.read_csv(TRAIN_CSV)\n    sample_df = train_df.sample(n=5, random_state=42)\n\n    dataset = MyDataset(\n        sample_df,\n        label_cols=LABEL_COLS,\n        target_shape=CFG.target_shape,\n        series_path=SERIES_ROOT_TRAIN\n    )\n\n    dataloader = DataLoader(\n        dataset,\n        batch_size=5,\n        shuffle=True,\n        num_workers=0\n    )\n\n    model = EfficientNetV2Hierarchical(**CFG.model_params, **CFG.backbone_params).to(DEVICE)\n    model.load_state_dict(torch.load(f\"{CFG.model_dir}/hierarchical_model.pth\", map_location=DEVICE)['model'])\n    model.eval()\n\n    for images, _, _ in dataloader:\n        result = predict_batch(images, model)\n        print(\"🔍 Résultat de prédiction sur batch d'entraînement :\")\n        print(result)\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.222807Z","iopub.execute_input":"2025-08-26T14:20:35.222962Z","iopub.status.idle":"2025-08-26T14:20:35.236796Z","shell.execute_reply.started":"2025-08-26T14:20:35.222949Z","shell.execute_reply":"2025-08-26T14:20:35.236129Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Evaluation","metadata":{}},{"cell_type":"code","source":"if DEBUG:\n    shared_dir = '/kaggle/shared'\n    shutil.rmtree(shared_dir, ignore_errors=True)\n    os.makedirs(shared_dir, exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.237462Z","iopub.execute_input":"2025-08-26T14:20:35.237665Z","iopub.status.idle":"2025-08-26T14:20:35.254054Z","shell.execute_reply.started":"2025-08-26T14:20:35.23765Z","shell.execute_reply":"2025-08-26T14:20:35.253283Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Main Execution\n# ====================================================\n\n# Load models at startup\nload_models()\n\n# Initialize the inference server with our main `predict` function.\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\n# Check if the notebook is running in the competition environment or a local session.\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    \n    submission_df = pl.read_parquet('/kaggle/working/submission.parquet')\n    display(submission_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-26T14:20:35.254788Z","iopub.execute_input":"2025-08-26T14:20:35.254967Z","iopub.status.idle":"2025-08-26T14:20:58.507067Z","shell.execute_reply.started":"2025-08-26T14:20:35.254953Z","shell.execute_reply":"2025-08-26T14:20:58.506426Z"}},"outputs":[],"execution_count":null}]}