{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom collections import defaultdict\nfrom typing import List, Dict, Optional, Tuple\nfrom IPython.display import display\n\n# Data handling\nimport numpy as np\nimport polars as pl\nimport pandas as pd\n\n# Medical imaging\nimport pydicom\nimport cv2\n\n# ML/DL\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.cuda.amp import autocast, GradScaler\nimport timm\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Metrics\nfrom torchmetrics.classification import AUROC\n\n# Utilities\nfrom tqdm.notebook import tqdm\nfrom sklearn.model_selection import train_test_split\n\n# --- Import Dataset and DataLoader ---\nfrom torch.utils.data import Dataset, DataLoader\n\n# IMPORTANT: Set the multiprocessing start method at the very beginning.\n# try:\n#     torch.multiprocessing.set_start_method('spawn', force=True)\n# except RuntimeError:\n#     pass\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# --- 2. Constants and Configuration ---\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]\n\nLOCATION_COLS = [col for col in LABEL_COLS if col != 'Aneurysm Present']\nNUM_LOCATION_CLASSES = len(LOCATION_COLS)\nNUM_TOTAL_CLASSES = len(LABEL_COLS)\n\nTRAIN_MODEL_NAME = 'tf_efficientnetv2_s'\n\nclass TrainingConfig:\n    model_name = TRAIN_MODEL_NAME\n    \n    image_size = 512\n    num_slices_sample = 32\n    use_windowing = True\n    \n    epochs = 10\n    batch_size = 8\n    learning_rate = 1e-4\n    weight_decay = 1e-5\n    \n    use_amp = True\n    \n    augment_prob = 0.5\n    \n    aneurysm_present_weight = 13.0\n    other_location_weight = 1.0\n\nCFG = TrainingConfig()\n\n# --- Paths ---\nDATA_DIR = '/kaggle/input/rsna-intracranial-aneurysm-detection'\nTRAIN_CSV_PATH = os.path.join(DATA_DIR, 'train.csv')\nTRAIN_LOCALIZERS_CSV_PATH = os.path.join(DATA_DIR, 'train_localizers.csv')\nSERIES_DIR = os.path.join(DATA_DIR, 'series')\nSEGMENTATIONS_DIR = os.path.join(DATA_DIR, 'segmentations')\n\n# --- 3. Model Architecture (UPDATED: Removed metadata input) ---\nclass MultiBackboneModel(nn.Module):\n    def __init__(self, model_name, num_classes=14, pretrained=True, \n                 drop_rate=0.3, drop_path_rate=0.2):\n        super().__init__()\n        \n        self.model_name = model_name\n        \n        if 'swin' in model_name:\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                img_size=CFG.image_size,\n                num_classes=0,\n                global_pool=''\n            )\n        else:\n            self.backbone = timm.create_model(\n                model_name, \n                pretrained=pretrained,\n                in_chans=3,\n                drop_rate=drop_rate,\n                drop_path_rate=drop_path_rate,\n                num_classes=0,\n                global_pool=''\n            )\n        \n        with torch.no_grad():\n            dummy_input = torch.zeros(1, 3, CFG.image_size, CFG.image_size)\n            features = self.backbone(dummy_input)\n            \n            if len(features.shape) == 4:\n                num_features = features.shape[1]\n                self.needs_pool = True\n            elif len(features.shape) == 3:\n                num_features = features.shape[-1]\n                self.needs_pool = False\n                self.needs_seq_pool = True\n            else:\n                num_features = features.shape[1]\n                self.needs_pool = False\n                self.needs_seq_pool = False\n        \n        print(f\"Model {model_name}: detected {num_features} features, output shape: {features.shape}\")\n        \n        if self.needs_pool:\n            self.global_pool = nn.AdaptiveAvgPool2d(1)\n        \n        # REMOVED: self.meta_fc as age/sex metadata are not available in test set\n        \n        self.classifier = nn.Sequential(\n            nn.Linear(num_features, 512), # UPDATED: Input dimension changed from num_features + 32\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(512, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(256, num_classes)\n        )\n        \n    def forward(self, image): # UPDATED: Removed meta argument\n        img_features = self.backbone(image)\n        \n        if hasattr(self, 'needs_pool') and self.needs_pool:\n            img_features = self.global_pool(img_features)\n            img_features = img_features.flatten(1)\n        elif hasattr(self, 'needs_seq_pool') and self.needs_seq_pool:\n            img_features = img_features.mean(dim=1)\n        elif len(img_features.shape) == 4:\n            img_features = F.adaptive_avg_pool2d(img_features, 1).flatten(1)\n        elif len(img_features.shape) == 3:\n            img_features = img_features.mean(dim=1)\n        \n        # REMOVED: meta_features processing and concatenation\n        # combined = torch.cat([img_features, meta_features], dim=1)\n        output = self.classifier(img_features) # UPDATED: Only image features passed\n        \n        return output\n\n# --- 4. DICOM Processing Functions (UPDATED: Removed age/sex extraction) ---\ndef apply_dicom_windowing(img: np.ndarray, window_center: float, window_width: float) -> np.ndarray:\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img = np.clip(img, img_min, img_max)\n    img = (img - img_min) / (img_max - img_min + 1e-7)\n    return (img * 255).astype(np.uint8)\n\ndef get_windowing_params(modality: str) -> Tuple[float, float]:\n    windows = {\n        'CT': (40, 80),\n        'CTA': (50, 350),\n        'MRA': (600, 1200),\n        'MRI': (40, 80),\n    }\n    return windows.get(modality, (40, 80))\n\ndef process_dicom_series(series_path: str, num_slices_sample: int, image_size: int, use_windowing: bool) -> Tuple[np.ndarray, Dict]:\n    series_path = Path(series_path)\n    \n    all_filepaths = []\n    for root, _, files in os.walk(series_path):\n        for file in files:\n            if file.endswith('.dcm'):\n                all_filepaths.append(os.path.join(root, file))\n    all_filepaths.sort()\n    \n    # Initialize metadata. Only 'modality' is relevant and available in test set.\n    metadata = {'modality': 'CT'} \n\n    if len(all_filepaths) == 0:\n        volume = np.zeros((num_slices_sample, image_size, image_size), dtype=np.uint8)\n        return volume, metadata \n    \n    slices = []\n    \n    # Attempt to read modality from the first DICOM file\n    try:\n        first_ds = pydicom.dcmread(all_filepaths[0], force=True)\n        metadata['modality'] = getattr(first_ds, 'Modality', 'CT')\n    except Exception as e:\n        # print(f\"WARNING: Error reading modality from first DICOM file in {series_path}: {e}. Using default 'CT'.\")\n        pass # metadata remains {'modality': 'CT'}\n\n    # Now, iterate through all DICOM files to extract pixel data\n    for filepath in all_filepaths:\n        try:\n            ds = pydicom.dcmread(filepath, force=True)\n            img = ds.pixel_array.astype(np.float32)\n            \n            if img.ndim == 3:\n                if img.shape[-1] == 3:\n                    img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_BGR2GRAY).astype(np.float32)\n                else:\n                    img = img[:, :, 0]\n            \n            if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n                img = img * ds.RescaleSlope + ds.RescaleIntercept\n            \n            if use_windowing:\n                window_center, window_width = get_windowing_params(metadata['modality'])\n                img = apply_dicom_windowing(img, window_center, window_width)\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    img = ((img - img_min) / (img_max - img_min) * 255).astype(np.uint8)\n                else:\n                    img = np.zeros_like(img, dtype=np.uint8)\n            \n            img = cv2.resize(img, (image_size, image_size))\n            slices.append(img)\n            \n        except Exception as e:\n            # print(f\"Error processing slice {filepath} in series {series_path}: {e}\")\n            continue # Skip problematic slice\n    \n    if len(slices) == 0:\n        volume = np.zeros((num_slices_sample, image_size, image_size), dtype=np.uint8)\n    else:\n        volume = np.array(slices)\n        if len(slices) > num_slices_sample:\n            indices = np.linspace(0, len(slices) - 1, num_slices_sample).astype(int)\n            volume = volume[indices]\n        elif len(slices) < num_slices_sample:\n            pad_size = num_slices_sample - len(slices)\n            volume = np.pad(volume, ((0, pad_size), (0, 0), (0, 0)), mode='edge')\n    \n    return volume, metadata\n\n# --- 5. Transform Functions (No changes here) ---\n\ndef get_train_transforms(image_size):\n    return A.Compose([\n        A.Resize(image_size, image_size),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.05, rotate_limit=15, p=0.5),\n        A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n        A.GaussNoise(p=0.2),\n        A.OneOf([\n            A.MotionBlur(p=0.2),\n            A.MedianBlur(blur_limit=3, p=0.1),\n            A.Blur(blur_limit=3, p=0.1),\n        ], p=0.2),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\ndef get_val_transforms(image_size):\n    return A.Compose([\n        A.Resize(image_size, image_size),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\n# --- Custom Dataset Class (UPDATED: Removed metadata handling) ---\nclass RSNADataset(Dataset):\n    def __init__(self, df: pd.DataFrame, series_dir: str, is_train: bool, transform: A.Compose):\n        self.df = df\n        self.series_dir = series_dir\n        self.is_train = is_train\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    # UPDATED: No longer returns meta_tensor\n    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: \n        row = self.df.iloc[idx]\n        series_uid = row[ID_COL]\n        \n        volume, metadata = process_dicom_series( # metadata still contains 'modality' for windowing\n            os.path.join(self.series_dir, series_uid),\n            num_slices_sample=CFG.num_slices_sample,\n            image_size=CFG.image_size,\n            use_windowing=CFG.use_windowing\n        )\n        \n        # Ensure volume is not empty before attempting to process slices\n        if volume.shape[0] == 0:\n            # Fallback for completely failed series: return dummy data\n            transformed_image = torch.zeros(3, CFG.image_size, CFG.image_size, dtype=torch.float32)\n            labels = torch.zeros(NUM_TOTAL_CLASSES, dtype=torch.float32)\n            return transformed_image, labels # UPDATED: No meta_tensor returned\n\n        middle_slice = volume[CFG.num_slices_sample // 2] if CFG.num_slices_sample > 0 else volume[0]\n        if volume.shape[0] == 1 and CFG.num_slices_sample > 1:\n            middle_slice = volume[0]\n        elif CFG.num_slices_sample == 0: \n             middle_slice = volume[0]\n        else:\n            middle_slice = volume[CFG.num_slices_sample // 2]\n        \n        mip = np.max(volume, axis=0)\n        std_proj = np.std(volume, axis=0).astype(np.float32)\n        \n        if std_proj.max() > std_proj.min():\n            std_proj = ((std_proj - std_proj.min()) / (std_proj.max() - std_proj.min()) * 255).astype(np.uint8)\n        else:\n            std_proj = np.zeros_like(std_proj, dtype=np.uint8)\n        \n        image = np.stack([middle_slice, mip, std_proj], axis=-1)\n\n        transformed_image = self.transform(image=image)['image']\n\n        # REMOVED: age_normalized and sex processing\n\n        labels = torch.tensor(row[LABEL_COLS].values.astype(float), dtype=torch.float32)\n\n        return transformed_image, labels # UPDATED: No meta_tensor returned\n\n# --- Main Execution Block ---\nif __name__ == '__main__':\n    print(\"--- 1. Loading and Inspecting CSV Data ---\")\n\n    try:\n        train_df_raw = pd.read_csv(TRAIN_CSV_PATH)\n        merged_df = train_df_raw\n        print(f\"Loaded {len(merged_df)} rows from train.csv\")\n        print(f\"DataFrame head:\\n{merged_df.head()}\")\n        print(f\"Columns used for labels: {LABEL_COLS}\")\n\n    except FileNotFoundError as e:\n        print(f\"Error loading CSV files: {e}. Please ensure '{DATA_DIR}' exists and contains the CSVs.\")\n        sys.exit(1)\n\n    train_df, val_df = train_test_split(\n        merged_df, \n        test_size=0.2, \n        random_state=42, \n        stratify=merged_df['Aneurysm Present']\n    )\n    print(f\"Train samples: {len(train_df)}, Validation samples: {len(val_df)}\")\n\n    train_transforms = get_train_transforms(CFG.image_size)\n    val_transforms = get_val_transforms(CFG.image_size)\n\n    train_dataset = RSNADataset(train_df, SERIES_DIR, is_train=True, transform=train_transforms)\n    val_dataset = RSNADataset(val_df, SERIES_DIR, is_train=False, transform=val_transforms)\n\n    train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, num_workers=os.cpu_count() // 2 or 1)\n    val_loader = DataLoader(val_dataset, batch_size=CFG.batch_size, shuffle=False, num_workers=os.cpu_count() // 2 or 1)\n    \n    print(f\"Train loader has {len(train_loader)} batches, Val loader has {len(val_loader)} batches.\")\n\n    model = MultiBackboneModel( # No change to instantiation, internal init is updated\n        model_name=CFG.model_name,\n        num_classes=NUM_TOTAL_CLASSES,\n        pretrained=True,\n        drop_rate=0.3,\n        drop_path_rate=0.2\n    ).to(device)\n\n    pos_weight = torch.ones(NUM_TOTAL_CLASSES).to(device)\n    aneurysm_present_idx = LABEL_COLS.index('Aneurysm Present')\n    pos_weight[aneurysm_present_idx] = CFG.aneurysm_present_weight\n\n    criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CFG.learning_rate, weight_decay=CFG.weight_decay)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=3, verbose=True)\n\n    scaler = GradScaler(enabled=CFG.use_amp)\n\n    metric_auroc = AUROC(task=\"multilabel\", num_labels=NUM_TOTAL_CLASSES, average=None).to(device)\n\n    best_val_score = -1.0\n    \n    print(\"\\n--- Starting Training Loop ---\")\n    for epoch in range(CFG.epochs):\n        model.train()\n        running_loss = 0.0\n        train_preds = []\n        train_labels = []\n\n        # UPDATED: Loop over only two items: images, labels\n        for i, (images, labels) in tqdm(enumerate(train_loader), total=len(train_loader), desc=f\"Epoch {epoch+1} Training\"):\n            images = images.to(device)\n            labels = labels.to(device)\n\n            optimizer.zero_grad()\n\n            with autocast(enabled=CFG.use_amp):\n                outputs = model(images) # UPDATED: Only image passed\n                loss = criterion(outputs, labels)\n            \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n            running_loss += loss.item() * images.size(0)\n            train_preds.append(torch.sigmoid(outputs).detach().cpu())\n            train_labels.append(labels.detach().cpu())\n\n        epoch_train_loss = running_loss / len(train_dataset)\n        train_preds = torch.cat(train_preds)\n        train_labels = torch.cat(train_labels)\n        \n        train_auroc_per_class = metric_auroc(train_preds, train_labels.long())\n        train_auroc_ap = train_auroc_per_class[aneurysm_present_idx].item()\n        train_auroc_loc_avg = torch.mean(train_auroc_per_class[[i for i in range(NUM_TOTAL_CLASSES) if i != aneurysm_present_idx]]).item()\n        train_comp_score = 0.5 * (train_auroc_ap + (1/13) * train_auroc_loc_avg)\n\n        print(f\"Epoch {epoch+1} | Train Loss: {epoch_train_loss:.4f} | Train AUC_AP: {train_auroc_ap:.4f} | Train Avg AUC_Loc: {train_auroc_loc_avg:.4f} | Train Comp Score: {train_comp_score:.4f}\")\n\n        model.eval()\n        val_preds = []\n        val_labels = []\n        val_loss = 0.0\n\n        with torch.no_grad():\n            # UPDATED: Loop over only two items: images, labels\n            for i, (images, labels) in tqdm(enumerate(val_loader), total=len(val_loader), desc=f\"Epoch {epoch+1} Validation\"):\n                images = images.to(device)\n                labels = labels.to(device)\n\n                with autocast(enabled=CFG.use_amp):\n                    outputs = model(images) # UPDATED: Only image passed\n                    loss = criterion(outputs, labels)\n                \n                val_loss += loss.item() * images.size(0)\n                val_preds.append(torch.sigmoid(outputs).cpu())\n                val_labels.append(labels.cpu())\n\n        epoch_val_loss = val_loss / len(val_dataset)\n        val_preds = torch.cat(val_preds)\n        val_labels = torch.cat(val_labels)\n\n        val_auroc_per_class = metric_auroc(val_preds, val_labels.long())\n        val_auroc_ap = val_auroc_per_class[aneurysm_present_idx].item()\n        val_auroc_loc_avg = torch.mean(val_auroc_per_class[[i for i in range(NUM_TOTAL_CLASSES) if i != aneurysm_present_idx]]).item()\n        current_val_score = 0.5 * (val_auroc_ap + (1/13) * val_auroc_loc_avg)\n\n        print(f\"Epoch {epoch+1} | Val Loss: {epoch_val_loss:.4f} | Val AUC_AP: {val_auroc_ap:.4f} | Val Avg AUC_Loc: {val_auroc_loc_avg:.4f} | Val Comp Score: {current_val_score:.4f}\")\n\n        scheduler.step(current_val_score)\n\n        if current_val_score > best_val_score:\n            best_val_score = current_val_score\n            print(f\"New best model found! Saving model with score: {best_val_score:.4f}\")\n            \n            torch.save({\n                'model_state_dict': model.state_dict(),\n                'best_score': best_val_score,\n                'model_config': {'model_name': CFG.model_name, 'num_classes': NUM_TOTAL_CLASSES},\n                'training_config': {\n                    'image_size': CFG.image_size,\n                    'num_slices': CFG.num_slices_sample,\n                    'use_windowing': CFG.use_windowing,\n                    'num_classes': NUM_TOTAL_CLASSES\n                }\n            }, f'{CFG.model_name}_best.pth')\n            \n        del images, labels, outputs, loss # UPDATED: Removed metas from del\n        gc.collect()\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\n    print(\"\\n--- Training Complete ---\")\n    print(f\"Best validation competition score achieved: {best_val_score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T15:33:55.943914Z","iopub.execute_input":"2025-07-31T15:33:55.944682Z","iopub.status.idle":"2025-08-01T03:29:50.192297Z","shell.execute_reply.started":"2025-07-31T15:33:55.94464Z","shell.execute_reply":"2025-08-01T03:29:50.189262Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ===============================================================================================","metadata":{}},{"cell_type":"code","source":"# import pydicom\n\n# # Replace with your actual file path\n# file_path = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647/1.2.826.0.1.3680043.8.498.10124807242473374136099471315028464450.dcm\"\n# ds = pydicom.dcmread(file_path)\n\n# # Print all metadata\n# print(ds)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-31T09:59:11.786707Z","iopub.execute_input":"2025-07-31T09:59:11.787725Z","iopub.status.idle":"2025-07-31T09:59:11.809669Z","shell.execute_reply.started":"2025-07-31T09:59:11.787685Z","shell.execute_reply":"2025-07-31T09:59:11.80903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}