{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:39:36.844742Z","iopub.execute_input":"2026-09-09T01:39:36.845091Z","iopub.status.idle":"2026-09-09T01:39:40.608812Z","shell.execute_reply.started":"2026-09-09T01:39:36.845061Z","shell.execute_reply":"2026-09-09T01:39:40.607556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom torchvision import models\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import roc_auc_score, precision_recall_curve, auc, classification_report\nfrom sklearn.utils import resample","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:48:13.109202Z","iopub.execute_input":"2026-09-09T01:48:13.109925Z","iopub.status.idle":"2026-09-09T01:48:32.591696Z","shell.execute_reply.started":"2026-09-09T01:48:13.1099Z","shell.execute_reply":"2026-09-09T01:48:32.590821Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Path configuration and device set up","metadata":{}},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nbase_path = '/kaggle/input/competitions/rsna-intracranial-aneurysm-detection'\nimage_base = os.path.join(base_path, 'series')\ncsv_path = os.path.join(base_path, 'train.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:48:32.593142Z","iopub.execute_input":"2026-09-09T01:48:32.593597Z","iopub.status.idle":"2026-09-09T01:48:32.653603Z","shell.execute_reply.started":"2026-09-09T01:48:32.593573Z","shell.execute_reply":"2026-09-09T01:48:32.652784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_cols = [\n    'Left Infraclinoid Internal Carotid Artery', 'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery', 'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery', 'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery', 'Left Anterior Cerebral Artery', 'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery', 'Right Posterior Communicating Artery',\n    'Basilar Tip', 'Other Posterior Circulation', 'Aneurysm Present'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:48:32.654759Z","iopub.execute_input":"2026-09-09T01:48:32.655122Z","iopub.status.idle":"2026-09-09T01:48:32.669738Z","shell.execute_reply.started":"2026-09-09T01:48:32.655082Z","shell.execute_reply":"2026-09-09T01:48:32.669164Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CMTF-Net architecture (Dual ResNet-50 + spatial cross attention)","metadata":{}},{"cell_type":"code","source":"class SpatialCrossModalAttention(nn.Module):\n\n    ##implements Cross-Modal Transformer Fusion (CMTF) over spatial feature map tokens.\n    ##fuses feature dimensions dynamically using multi-head spatial cross-attention.\n    def __init__(self, dim=2048, heads=8):\n        super().__init__()\n        self.dim = dim\n        self.heads = heads\n        self.scale = (dim // heads) ** -0.5\n        \n        self.q = nn.Linear(dim, dim)\n        self.k = nn.Linear(dim, dim)\n        self.v = nn.Linear(dim, dim)\n        \n        self.proj_out = nn.Linear(dim, dim)\n        self.norm = nn.LayerNorm(dim)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:00.661125Z","iopub.execute_input":"2026-09-09T01:49:00.661478Z","iopub.status.idle":"2026-09-09T01:49:00.667385Z","shell.execute_reply.started":"2026-09-09T01:49:00.661449Z","shell.execute_reply":"2026-09-09T01:49:00.666693Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def forward(self, f_cta, f_mip):\n        #input features size: [B, C, H, W] -> [B, 2048, 7, 7]\n        b, c, h, w = f_cta.shape\n        \n        #reshape to spatial sequence format: [B, N, C] where N = H * W (49 spatial tokens)\n        cta_flat = f_cta.view(b, c, -1).permute(0, 2, 1) # [B, 49, 2048]\n        mip_flat = f_mip.view(b, c, -1).permute(0, 2, 1) # [B, 49, 2048]\n        \n        #project Queries, Keys, Values\n        q = self.q(cta_flat) #queries projected from CTA\n        k = self.k(mip_flat) #keys projected from MIP\n        v = self.v(mip_flat) #values projected from MIP\n        \n        #reshape into multi-headed segments: [B, heads, N, d_head]\n        q = q.view(b, -1, self.heads, c // self.heads).transpose(1, 2)\n        k = k.view(b, -1, self.heads, c // self.heads).transpose(1, 2)\n        v = v.view(b, -1, self.heads, c // self.heads).transpose(1, 2)\n        \n        #multi-head attention weights: softmax( (Q @ K^T) / sqrt(d_k) )\n        scores = torch.matmul(q, k.transpose(-2, -1)) * self.scale # [B, heads, 49, 49]\n        attn = torch.softmax(scores, dim=-1)\n        \n        #compute spatial attention context\n        context = torch.matmul(attn, v) # [B, heads, 49, d_head]\n        context = context.transpose(1, 2).contiguous().view(b, -1, c) # [B, 49, 2048]\n        \n        #feed-forward projection + transformer residual norm\n        out = self.proj_out(context)\n        out = self.norm(cta_flat + out) # Residual mapping\n        \n        #format back into spatial multi-channel tensor shape: [B, 2048, 7, 7]\n        out_tensor = out.permute(0, 2, 1).view(b, c, h, w)\n        return out_tensor, attn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:06.287209Z","iopub.execute_input":"2026-09-09T01:49:06.287751Z","iopub.status.idle":"2026-09-09T01:49:06.294542Z","shell.execute_reply.started":"2026-09-09T01:49:06.287723Z","shell.execute_reply":"2026-09-09T01:49:06.293908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CMTFNet(nn.Module):\n    \n    ##proposed CMTF-Net: Dual ResNet-50 streams fused with spatial cross-attention.\n    ##outputs 14 independent sigmoid target predictions under multi-label supervision.\n    \n    def __init__(self, num_classes=14):\n        super().__init__()\n        #load pre-trained ImageNet ResNet-50 backbones\n        resnet_cta = models.resnet50(weights='DEFAULT')\n        resnet_mip = models.resnet50(weights='DEFAULT')\n        \n        #modify first conv step of both streams to accept single-channel arrays\n        resnet_cta.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        resnet_mip.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)\n        \n        #strip backbones down to stage 4 maps (omitting standard pooling/FC)\n        self.cta_backbone = nn.Sequential(*list(resnet_cta.children())[:-2]) # outputs [B, 2048, 7, 7]\n        self.mip_backbone = nn.Sequential(*list(resnet_mip.children())[:-2]) # outputs [B, 2048, 7, 7]\n        \n        #attention and global pooling steps\n        self.transformer_fusion = SpatialCrossModalAttention(dim=2048, heads=8)\n        self.global_pool = nn.AdaptiveAvgPool2d(1)\n        \n        #multi-label diagnostic territory heads\n        self.fc_head = nn.Sequential(\n            nn.Dropout(0.4),\n            nn.Linear(2048, num_classes) # Generates 14 territory logits\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:11.585867Z","iopub.execute_input":"2026-09-09T01:49:11.586318Z","iopub.status.idle":"2026-09-09T01:49:11.592521Z","shell.execute_reply.started":"2026-09-09T01:49:11.586287Z","shell.execute_reply":"2026-09-09T01:49:11.591638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def forward(self, cta, mip):\n        #pass images into backbone stage encoders\n        f_cta = self.cta_backbone(cta) # [B, 2048, 7, 7]\n        f_mip = self.mip_backbone(mip) # [B, 2048, 7, 7]\n        \n        #apply spatial cross-attention fusion\n        fused_tensor, attn_map = self.transformer_fusion(f_cta, f_mip) # [B, 2048, 7, 7]\n        \n        #collapse features globally using GAP\n        global_features = self.global_pool(fused_tensor).squeeze(-1).squeeze(-1) # [B, 2048]\n        \n        #map features to parallel output heads\n        logits = self.fc_head(global_features)\n        return logits, attn_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:18.902567Z","iopub.execute_input":"2026-09-09T01:49:18.902976Z","iopub.status.idle":"2026-09-09T01:49:18.907568Z","shell.execute_reply.started":"2026-09-09T01:49:18.902949Z","shell.execute_reply":"2026-09-09T01:49:18.906961Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Clinical DICOM HU processing and dual-stream dataset","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nfrom PIL import Image\nfrom torchvision import transforms\nfrom torch.utils.data import Dataset\n\nclass RSNADualStreamDataset(Dataset):\n    def __init__(self, df, image_base, target_cols, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.image_base = image_base\n        self.target_cols = target_cols\n        self.transform = transform or transforms.ToTensor()\n\n        # Tries a few common column names automatically.\n        self.cta_col = self._find_column([\"cta_path\", \"cta\", \"cta_image\", \"cta_file\"])\n        self.mip_col = self._find_column([\"mip_path\", \"mip\", \"mip_image\", \"mip_file\"])\n\n    def _find_column(self, candidates):\n        for col in candidates:\n            if col in self.df.columns:\n                return col\n        raise KeyError(f\"None of these columns were found: {candidates}\")\n\n    def _resolve_path(self, path_value):\n        path_value = str(path_value)\n        if os.path.isabs(path_value):\n            return path_value\n        return os.path.join(self.image_base, path_value)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        cta_path = self._resolve_path(row[self.cta_col])\n        mip_path = self._resolve_path(row[self.mip_col])\n\n        cta = Image.open(cta_path).convert(\"RGB\")\n        mip = Image.open(mip_path).convert(\"RGB\")\n\n        cta = self.transform(cta)\n        mip = self.transform(mip)\n\n        labels = torch.tensor(\n            row[self.target_cols].fillna(0).to_numpy(dtype=\"float32\"),\n            dtype=torch.float32\n        )\n\n        return cta, mip, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:39.235741Z","iopub.execute_input":"2026-09-09T01:49:39.236148Z","iopub.status.idle":"2026-09-09T01:49:39.244021Z","shell.execute_reply.started":"2026-09-09T01:49:39.236121Z","shell.execute_reply":"2026-09-09T01:49:39.243129Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def __len__(self):\n        return len(self.df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:49:52.904278Z","iopub.execute_input":"2026-09-09T01:49:52.905014Z","iopub.status.idle":"2026-09-09T01:49:52.908802Z","shell.execute_reply.started":"2026-09-09T01:49:52.904985Z","shell.execute_reply":"2026-09-09T01:49:52.907964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_id = str(row['SeriesInstanceUID'])\n        series_path = os.path.join(self.base_path, series_id)\n        \n        # Gather DICOM slices\n        dcm_files = glob.glob(os.path.join(series_path, \"**/*.dcm\"), recursive=True)\n        if not dcm_files:\n            return torch.zeros(1, 224, 224), torch.zeros(1, 224, 224), torch.zeros(len(self.targets))\n        \n        try:\n            # Sort slice arrays into order by Z position coordinates\n            slices = [pydicom.dcmread(f) for f in dcm_files]\n            slices.sort(key=lambda x: float(x.ImagePositionPatient[2]))\n            \n            # Form raw intensity volume\n            volume_list = []\n            for s in slices:\n                img = s.pixel_array.astype(np.float32)\n                # Convert raw pixels to Hounsfield Units (HU) using DICOM headers\n                slope = float(s.RescaleSlope) if 'RescaleSlope' in s else 1.0\n                intercept = float(s.RescaleIntercept) if 'RescaleIntercept' in s else 0.0\n                volume_list.append(img * slope + intercept)\n                \n            volume = np.stack(volume_list)\n            \n            # --- Stream 1: Axial 2D CTA Slice ---\n            mid_idx = len(volume) // 2\n            cta_slice = volume[mid_idx]\n            \n            # --- Stream 2: Axial MIP Creation ---\n            # Isolate contrast enhanced vessels using [150, 600] HU clipping window\n            windowed_volume = np.clip(volume, 150.0, 600.0)\n            mip_slice = np.max(windowed_volume, axis=0) # Collapsing Z axial range to MIP projection\n            \n            # Resize and scale mappings to [0, 1] range\n            cta_resized = cv2.resize(cta_slice, (224, 224))\n            cta_norm = (cta_resized - cta_resized.min()) / (cta_resized.max() - cta_resized.min() + 1e-8)\n            \n            mip_resized = cv2.resize(mip_slice, (224, 224))\n            mip_norm = (mip_resized - mip_resized.min()) / (mip_resized.max() - mip_resized.min() + 1e-8)\n            \n            # Normalize with dataset specific stats claimed in paper\n            cta_norm = (cta_norm - self.mean) / self.std\n            mip_norm = (mip_norm - self.mean) / self.std\n            \n        except Exception as e:\n            return torch.zeros(1, 224, 224), torch.zeros(1, 224, 224), torch.zeros(len(self.targets))\n        \n        # Build 1-channel tensors: [1, 224, 224]\n        cta_tensor = torch.from_numpy(cta_norm).unsqueeze(0).float()\n        mip_tensor = torch.from_numpy(mip_norm).unsqueeze(0).float()\n        \n        labels = torch.tensor(row[self.targets].values.astype(np.float32))\n        return cta_tensor, mip_tensor, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:50:09.9822Z","iopub.execute_input":"2026-09-09T01:50:09.983008Z","iopub.status.idle":"2026-09-09T01:50:09.992063Z","shell.execute_reply.started":"2026-09-09T01:50:09.982977Z","shell.execute_reply":"2026-09-09T01:50:09.991302Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Sprint training loop with multi-label sigmoid supervision","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport pydicom\n\nfrom PIL import Image\nfrom torchvision import transforms\nfrom torch.utils.data import Dataset, DataLoader, Subset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:51:33.868101Z","iopub.execute_input":"2026-09-09T01:51:33.869052Z","iopub.status.idle":"2026-09-09T01:51:33.873095Z","shell.execute_reply.started":"2026-09-09T01:51:33.869022Z","shell.execute_reply":"2026-09-09T01:51:33.872526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"csv_path = \"/kaggle/input/competitions/rsna-intracranial-aneurysm-detection/train.csv\"\nseries_root = \"/kaggle/input/competitions/rsna-intracranial-aneurysm-detection/series\"\n\ntarget_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\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:51:52.093607Z","iopub.execute_input":"2026-09-09T01:51:52.094173Z","iopub.status.idle":"2026-09-09T01:51:52.09873Z","shell.execute_reply.started":"2026-09-09T01:51:52.094145Z","shell.execute_reply":"2026-09-09T01:51:52.097898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    ),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:52:04.254913Z","iopub.execute_input":"2026-09-09T01:52:04.255538Z","iopub.status.idle":"2026-09-09T01:52:04.260351Z","shell.execute_reply.started":"2026-09-09T01:52:04.255509Z","shell.execute_reply":"2026-09-09T01:52:04.259714Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RSNADualStreamDataset(Dataset):\n    def __init__(self, df, series_root, target_cols, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.target_cols = target_cols\n        self.transform = transform\n        self.series_col = \"SeriesInstanceUID\"\n\n    def __len__(self):\n        return len(self.df)\n\n    def _normalize_to_uint8(self, img):\n        img = img.astype(np.float32)\n        lo, hi = np.percentile(img, (1, 99))\n\n        if hi <= lo:\n            lo = float(img.min())\n            hi = float(img.max())\n\n        img = np.clip(img, lo, hi)\n        img = (img - lo) / max(hi - lo, 1e-6)\n        return (img * 255).astype(np.uint8)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:52:19.308856Z","iopub.execute_input":"2026-09-09T01:52:19.309133Z","iopub.status.idle":"2026-09-09T01:52:19.315326Z","shell.execute_reply.started":"2026-09-09T01:52:19.309111Z","shell.execute_reply":"2026-09-09T01:52:19.314415Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":" def _load_volume(self, series_uid):\n        series_uid = str(series_uid).strip()\n        series_dir = os.path.join(self.series_root, series_uid)\n\n        if not os.path.isdir(series_dir):\n            raise FileNotFoundError(f\"Missing series folder: {series_dir}\")\n\n        dcm_files = [\n            os.path.join(series_dir, f)\n            for f in os.listdir(series_dir)\n            if f.lower().endswith(\".dcm\")\n        ]\n\n        if not dcm_files:\n            raise FileNotFoundError(f\"No DICOM files found in: {series_dir}\")\n\n        slices = []\n        for path in dcm_files:\n            ds = pydicom.dcmread(path)\n            arr = ds.pixel_array.astype(np.float32)\n\n            slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n            intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n            arr = arr * slope + intercept\n\n            instance_number = getattr(ds, \"InstanceNumber\", 0)\n            slices.append((instance_number, arr))\n\n        slices.sort(key=lambda x: x[0])\n        volume = np.stack([arr for _, arr in slices], axis=0)\n        return volume","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:52:33.826733Z","iopub.execute_input":"2026-09-09T01:52:33.827392Z","iopub.status.idle":"2026-09-09T01:52:33.83476Z","shell.execute_reply.started":"2026-09-09T01:52:33.82736Z","shell.execute_reply":"2026-09-09T01:52:33.833803Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_df = pd.read_csv(csv_path)\nfull_df[target_cols] = full_df[target_cols].fillna(0).astype(np.float32)\n\nsub_df = full_df.sample(min(500, len(full_df)), random_state=42).reset_index(drop=True)\n\ndataset = RSNADualStreamDataset(\n    sub_df,\n    series_root=series_root,\n    target_cols=target_cols,\n    transform=transform,\n)\n\ndataset_size = len(dataset)\ntrain_size = min(400, dataset_size - 1)\n\ntrain_subset = Subset(dataset, range(0, train_size))\nval_subset = Subset(dataset, range(train_size, dataset_size))\n\ntrain_loader = DataLoader(train_subset, batch_size=8, shuffle=True, num_workers=0)\nval_loader = DataLoader(val_subset, batch_size=1, shuffle=False, num_workers=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:52:49.118888Z","iopub.execute_input":"2026-09-09T01:52:49.119819Z","iopub.status.idle":"2026-09-09T01:52:49.177327Z","shell.execute_reply.started":"2026-09-09T01:52:49.11979Z","shell.execute_reply":"2026-09-09T01:52:49.176743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_df = pd.read_csv(csv_path)\nfull_df[target_cols] = full_df[target_cols].fillna(0).astype(np.float32)\n\nsub_df = full_df.sample(min(500, len(full_df)), random_state=42).reset_index(drop=True)\n\ndataset = RSNADualStreamDataset(\n    sub_df,\n    series_root=series_root,\n    target_cols=target_cols,\n    transform=transform,\n)\n\ndataset_size = len(dataset)\ntrain_size = min(400, dataset_size - 1)\n\ntrain_subset = Subset(dataset, range(0, train_size))\nval_subset = Subset(dataset, range(train_size, dataset_size))\n\ntrain_loader = DataLoader(train_subset, batch_size=8, shuffle=True, num_workers=0)\nval_loader = DataLoader(val_subset, batch_size=1, shuffle=False, num_workers=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:53:03.387815Z","iopub.execute_input":"2026-09-09T01:53:03.388304Z","iopub.status.idle":"2026-09-09T01:53:03.414194Z","shell.execute_reply.started":"2026-09-09T01:53:03.388225Z","shell.execute_reply":"2026-09-09T01:53:03.413302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = CMTFNet(num_classes=len(target_cols)).to(device)\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\n\ntrain_labels = sub_df.iloc[:train_size][target_cols].to_numpy(dtype=np.float32)\npos_counts = np.sum(train_labels, axis=0)\nneg_counts = train_size - pos_counts\n\npos_weights = torch.tensor(\n    neg_counts / (pos_counts + 1e-8),\n    dtype=torch.float32\n).to(device)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:53:16.544654Z","iopub.execute_input":"2026-09-09T01:53:16.545045Z","iopub.status.idle":"2026-09-09T01:53:18.482805Z","shell.execute_reply.started":"2026-09-09T01:53:16.545018Z","shell.execute_reply":"2026-09-09T01:53:18.482245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = CMTFNet(num_classes=len(target_cols)).to(device)\noptimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-2)\n\ntrain_labels = sub_df.iloc[:train_size][target_cols].to_numpy(dtype=np.float32)\npos_counts = np.sum(train_labels, axis=0)\nneg_counts = train_size - pos_counts\n\npos_weights = torch.tensor(\n    neg_counts / (pos_counts + 1e-8),\n    dtype=torch.float32\n).to(device)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:53:37.58217Z","iopub.execute_input":"2026-09-09T01:53:37.582727Z","iopub.status.idle":"2026-09-09T01:53:38.791907Z","shell.execute_reply.started":"2026-09-09T01:53:37.582699Z","shell.execute_reply":"2026-09-09T01:53:38.791299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(sub_df[\"SeriesInstanceUID\"].iloc[0])\nprint(os.path.isdir(os.path.join(series_root, str(sub_df[\"SeriesInstanceUID\"].iloc[0]).strip())))\nprint(os.path.join(series_root, str(sub_df[\"SeriesInstanceUID\"].iloc[0]).strip()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:53:54.263564Z","iopub.execute_input":"2026-09-09T01:53:54.264467Z","iopub.status.idle":"2026-09-09T01:53:54.271647Z","shell.execute_reply.started":"2026-09-09T01:53:54.264433Z","shell.execute_reply":"2026-09-09T01:53:54.270809Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualization","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:54:22.100582Z","iopub.execute_input":"2026-09-09T01:54:22.101304Z","iopub.status.idle":"2026-09-09T01:54:22.104707Z","shell.execute_reply.started":"2026-09-09T01:54:22.101264Z","shell.execute_reply":"2026-09-09T01:54:22.104066Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport pydicom\nfrom PIL import Image\nfrom torchvision import transforms\nfrom torch.utils.data import Dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:54:34.300276Z","iopub.execute_input":"2026-09-09T01:54:34.30091Z","iopub.status.idle":"2026-09-09T01:54:34.305174Z","shell.execute_reply.started":"2026-09-09T01:54:34.30088Z","shell.execute_reply":"2026-09-09T01:54:34.304562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"csv_path = \"/kaggle/input/competitions/rsna-intracranial-aneurysm-detection/train.csv\"\nseries_root = \"/kaggle/input/competitions/rsna-intracranial-aneurysm-detection/series\"\n\ntarget_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]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:54:49.895284Z","iopub.execute_input":"2026-09-09T01:54:49.895549Z","iopub.status.idle":"2026-09-09T01:54:49.900059Z","shell.execute_reply.started":"2026-09-09T01:54:49.895527Z","shell.execute_reply":"2026-09-09T01:54:49.899456Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:55:07.450297Z","iopub.execute_input":"2026-09-09T01:55:07.450725Z","iopub.status.idle":"2026-09-09T01:55:07.455259Z","shell.execute_reply.started":"2026-09-09T01:55:07.450694Z","shell.execute_reply":"2026-09-09T01:55:07.454279Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RSNADualStreamDataset(Dataset):\n    def __init__(self, df, series_root, target_cols, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.target_cols = target_cols\n        self.transform = transform\n        self.series_col = \"SeriesInstanceUID\"\n\n    def __len__(self):\n        return len(self.df)\n\n    def _normalize_to_uint8(self, img):\n        img = img.astype(np.float32)\n        lo, hi = np.percentile(img, (1, 99))\n\n        if hi <= lo:\n            lo = float(img.min())\n            hi = float(img.max())\n\n        img = np.clip(img, lo, hi)\n        img = (img - lo) / max(hi - lo, 1e-6)\n        return (img * 255).astype(np.uint8)\n\n    def _load_volume(self, series_uid):\n        series_uid = str(series_uid).strip()\n        series_dir = os.path.join(self.series_root, series_uid)\n\n        if not os.path.isdir(series_dir):\n            raise FileNotFoundError(f\"Missing series folder: {series_dir}\")\n\n        dcm_files = [\n            os.path.join(series_dir, f)\n            for f in os.listdir(series_dir)\n            if f.lower().endswith(\".dcm\")\n        ]\n\n        if not dcm_files:\n            raise FileNotFoundError(f\"No DICOM files found in: {series_dir}\")\n\n        slices = []\n        for path in dcm_files:\n            ds = pydicom.dcmread(path)\n            arr = ds.pixel_array.astype(np.float32)\n\n            slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n            intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n            arr = arr * slope + intercept\n\n            instance_number = getattr(ds, \"InstanceNumber\", 0)\n            slices.append((instance_number, arr))\n\n        slices.sort(key=lambda x: x[0])\n        volume = np.stack([arr for _, arr in slices], axis=0)\n        return volume\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_uid = str(row[self.series_col]).strip()\n\n        volume = self._load_volume(series_uid)\n\n        center_slice = volume[len(volume) // 2]\n        mip_image = volume.max(axis=0)\n\n        cta_img = Image.fromarray(self._normalize_to_uint8(center_slice)).convert(\"RGB\")\n        mip_img = Image.fromarray(self._normalize_to_uint8(mip_image)).convert(\"RGB\")\n\n        if self.transform is not None:\n            cta_img = self.transform(cta_img)\n            mip_img = self.transform(mip_img)\n\n        labels = torch.tensor(\n            row[self.target_cols].fillna(0).to_numpy(dtype=np.float32),\n            dtype=torch.float32\n        )\n\n        return cta_img, mip_img, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:55:26.530484Z","iopub.execute_input":"2026-09-09T01:55:26.531135Z","iopub.status.idle":"2026-09-09T01:55:26.542599Z","shell.execute_reply.started":"2026-09-09T01:55:26.531105Z","shell.execute_reply":"2026-09-09T01:55:26.541831Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RSNADualStreamDataset(Dataset):\n    def __init__(self, df, series_root, target_cols, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.target_cols = target_cols\n        self.transform = transform\n        self.series_col = \"SeriesInstanceUID\"\n\n    def __len__(self):\n        return len(self.df)\n\n    def _normalize_to_uint8(self, img):\n        img = img.astype(np.float32)\n        lo, hi = np.percentile(img, (1, 99))\n\n        if hi <= lo:\n            lo = float(img.min())\n            hi = float(img.max())\n\n        img = np.clip(img, lo, hi)\n        img = (img - lo) / max(hi - lo, 1e-6)\n        return (img * 255).astype(np.uint8)\n\n    def _load_volume(self, series_uid):\n        series_uid = str(series_uid).strip()\n        series_dir = os.path.join(self.series_root, series_uid)\n\n        if not os.path.isdir(series_dir):\n            raise FileNotFoundError(f\"Missing series folder: {series_dir}\")\n\n        dcm_files = [\n            os.path.join(series_dir, f)\n            for f in os.listdir(series_dir)\n            if f.lower().endswith(\".dcm\")\n        ]\n\n        if not dcm_files:\n            raise FileNotFoundError(f\"No DICOM files found in: {series_dir}\")\n\n        slices = []\n        for path in dcm_files:\n            ds = pydicom.dcmread(path)\n            arr = ds.pixel_array.astype(np.float32)\n\n            slope = float(getattr(ds, \"RescaleSlope\", 1.0))\n            intercept = float(getattr(ds, \"RescaleIntercept\", 0.0))\n            arr = arr * slope + intercept\n\n            instance_number = getattr(ds, \"InstanceNumber\", 0)\n            slices.append((instance_number, arr))\n\n        slices.sort(key=lambda x: x[0])\n        volume = np.stack([arr for _, arr in slices], axis=0)\n        return volume\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_uid = str(row[self.series_col]).strip()\n\n        volume = self._load_volume(series_uid)\n\n        center_slice = volume[len(volume) // 2]\n        mip_image = volume.max(axis=0)\n\n        cta_img = Image.fromarray(self._normalize_to_uint8(center_slice)).convert(\"RGB\")\n        mip_img = Image.fromarray(self._normalize_to_uint8(mip_image)).convert(\"RGB\")\n\n        if self.transform is not None:\n            cta_img = self.transform(cta_img)\n            mip_img = self.transform(mip_img)\n\n        labels = torch.tensor(\n            row[self.target_cols].fillna(0).to_numpy(dtype=np.float32),\n            dtype=torch.float32\n        )\n\n        return cta_img, mip_img, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:55:47.320085Z","iopub.execute_input":"2026-09-09T01:55:47.320763Z","iopub.status.idle":"2026-09-09T01:55:47.331132Z","shell.execute_reply.started":"2026-09-09T01:55:47.320733Z","shell.execute_reply":"2026-09-09T01:55:47.330457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_df = pd.read_csv(csv_path)\nfull_df[target_cols] = full_df[target_cols].fillna(0).astype(np.float32)\n\nsub_df = full_df.sample(min(500, len(full_df)), random_state=42).reset_index(drop=True)\n\ndataset = RSNADualStreamDataset(\n    sub_df,\n    series_root=series_root,\n    target_cols=target_cols,\n    transform=transform,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:56:02.276543Z","iopub.execute_input":"2026-09-09T01:56:02.276793Z","iopub.status.idle":"2026-09-09T01:56:02.297871Z","shell.execute_reply.started":"2026-09-09T01:56:02.276772Z","shell.execute_reply":"2026-09-09T01:56:02.296861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cta_img, mip_img, labels = dataset[0]\nprint(cta_img.shape, mip_img.shape, labels.shape)\nprint(labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:56:16.530836Z","iopub.execute_input":"2026-09-09T01:56:16.531145Z","iopub.status.idle":"2026-09-09T01:56:19.567194Z","shell.execute_reply.started":"2026-09-09T01:56:16.531121Z","shell.execute_reply":"2026-09-09T01:56:19.566327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_tensor_image(t):\n    img = t.permute(1, 2, 0).cpu().numpy()\n\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n\n    img = (img * std) + mean\n    img = np.clip(img, 0, 1)\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:56:33.677303Z","iopub.execute_input":"2026-09-09T01:56:33.677593Z","iopub.status.idle":"2026-09-09T01:56:33.682131Z","shell.execute_reply.started":"2026-09-09T01:56:33.67757Z","shell.execute_reply":"2026-09-09T01:56:33.681401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 4))\n\nplt.subplot(1, 2, 1)\nplt.imshow(show_tensor_image(cta_img))\nplt.title(\"Center Slice\")\nplt.axis(\"off\")\n\nplt.subplot(1, 2, 2)\nplt.imshow(show_tensor_image(mip_img))\nplt.title(\"MIP Image\")\nplt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:56:44.962575Z","iopub.execute_input":"2026-09-09T01:56:44.962995Z","iopub.status.idle":"2026-09-09T01:56:45.263478Z","shell.execute_reply.started":"2026-09-09T01:56:44.962968Z","shell.execute_reply":"2026-09-09T01:56:45.262616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\nimport torch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T01:57:01.258064Z","iopub.execute_input":"2026-09-09T01:57:01.258702Z","iopub.status.idle":"2026-09-09T01:57:01.262325Z","shell.execute_reply.started":"2026-09-09T01:57:01.25867Z","shell.execute_reply":"2026-09-09T01:57:01.261522Z"}},"outputs":[],"execution_count":null}]}