{"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":13851420,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## This notebook includes data analysis, visualization, and code for training across all 4000+ DICOM series.","metadata":{}},{"cell_type":"markdown","source":"#### Before dealing with data, let's understand the data first!\n\n- train.csv: each row consists of one brain scan information. This consist of multilabel classification telling presence of aneurysm in yes/no and last target variable indicating aneurysm present anywhere in the scan!\n\n- train_localizers.csv: This gives coordinates of aneurysms inside a scan.\n    \n    Each row = one aneurysm finding.\n    \n    Columns:\n    \n    SeriesInstanceUID → which scan.\n    \n    SOPInstanceUID → which slice (DICOM file).\n    \n    coordinates → (x, y) of the aneurysm in that slice.\n    \n    location → which artery it belongs to.\n    This is for localization: exactly where the aneurysm is inside the scan.\n\n- series/ : one series per folder, so it is a collection of series (scans) where each series consits of one brain scan. Each file in the series is .dcm file (the raw image model process.)\n\n- segmentations/ (NifTI files)\n\n    Segmentation masks of blood vessels only (not aneurysms).\n    \n    Each voxel (3D pixel) has a label (1–13) for which artery it is.\n    Useful for focusing the model only on vessels, since aneurysms only appear there.","metadata":{}},{"cell_type":"markdown","source":"### Approach 1: Multi-label classification\n\n##### detect for every 13 locations + 1 target (variable indicates if aneurysm is present anywhere) = 14 binary variables (if aneurysm is present or absent)\n","metadata":{}},{"cell_type":"code","source":"##---------------------- IMPORTING ALL LIBRARIES -------------------------\nimport pandas as pd\nimport pydicom #for dicom files\nimport numpy as np\nfrom pathlib import Path\nimport os\nimport cv2\nimport matplotlib.pyplot as plt\nimport timm\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom torchmetrics.classification import AUROC\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom torch.cuda.amp import autocast, GradScaler\nfrom torch.nn import BCEWithLogitsLoss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:40:45.39457Z","iopub.execute_input":"2025-09-25T06:40:45.39559Z","iopub.status.idle":"2025-09-25T06:40:45.406953Z","shell.execute_reply.started":"2025-09-25T06:40:45.395537Z","shell.execute_reply":"2025-09-25T06:40:45.405861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"###----------------------- COMPETITION CONSTANTS ------------------------------\n\nID_COL = 'SeriesInstanceUID'\n\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]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:40:47.367664Z","iopub.execute_input":"2025-09-25T06:40:47.367979Z","iopub.status.idle":"2025-09-25T06:40:47.373432Z","shell.execute_reply.started":"2025-09-25T06:40:47.367957Z","shell.execute_reply":"2025-09-25T06:40:47.372416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"##---------------- LOAD DATA PATHS ---------------------\n\ntrain_csv = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\"\nimages_path = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\"\ndicom_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\"\nlocalizer_csv = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T05:41:44.418768Z","iopub.execute_input":"2025-09-25T05:41:44.41907Z","iopub.status.idle":"2025-09-25T05:41:44.423305Z","shell.execute_reply.started":"2025-09-25T05:41:44.419049Z","shell.execute_reply":"2025-09-25T05:41:44.422581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"##---------------- Let's understand the data by actually loading and visualizing --------------------\n\n\nslices = [pydicom.dcmread(os.path.join(dicom_dir, file)) for file in os.listdir(dicom_dir)]\n\n#sort the 2d slices by z position. (ImagePositionPatient is metadata in dicom)\nslices.sort(key = lambda x: float(x.ImagePositionPatient[2]))\n\nthree_d_volume = np.stack([s.pixel_array for s in slices])\n\nplt.imshow(three_d_volume[len(three_d_volume) // 2], cmap = \"gray\")\nplt.title(\"Middle slice of 3d Volume (DICOM series)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:43:58.065611Z","iopub.execute_input":"2025-09-25T06:43:58.066459Z","iopub.status.idle":"2025-09-25T06:44:04.241851Z","shell.execute_reply.started":"2025-09-25T06:43:58.066427Z","shell.execute_reply":"2025-09-25T06:44:04.240598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"####------------------- Explore Tabular data provided ----------------------------\n\ntrain_df = pd.read_csv(train_csv)\n\nlocalizer_df = pd.read_csv(localizer_csv)\n\nprint(\"Train CSV shape\", train_df.shape)\nprint(\"Localizer CSV shape\", localizer_df.shape)\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:44:04.243486Z","iopub.execute_input":"2025-09-25T06:44:04.243748Z","iopub.status.idle":"2025-09-25T06:44:04.332852Z","shell.execute_reply.started":"2025-09-25T06:44:04.243727Z","shell.execute_reply":"2025-09-25T06:44:04.331932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"total_cases = train_df[\"Aneurysm Present\"].count()\nprint(\"Number of total cases\", total_cases)\npositive_cases = train_df[\"Aneurysm Present\"].sum()\nprint(\"Number of Positive cases\", positive_cases)\nneg_cases = total_cases - positive_cases\nprint(\"Number of Negative Cases:\", neg_cases)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:44:04.334148Z","iopub.execute_input":"2025-09-25T06:44:04.334512Z","iopub.status.idle":"2025-09-25T06:44:04.342135Z","shell.execute_reply.started":"2025-09-25T06:44:04.334479Z","shell.execute_reply":"2025-09-25T06:44:04.341186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#---------------------- VISUALIZING DICOM FILES IN FIRST DIRECTORY --------------------\nimport os\ndef visualize(series_path):\n    series_path = Path(series_path)\n    all_filepaths = []\n    count = 0 \n    for sub_dir in os.listdir(series_path):\n        count += 1\n        if count > 3:\n            break\n        dir_path = os.path.join(series_path, sub_dir)\n        \n        for file in os.listdir(dir_path):  #get all files of sub director\n            if file.endswith(\".dcm\"):\n                all_filepaths.append(os.path.join(dir_path, file))\n                \n    if len(all_filepaths) == 0: \n        #no files extracted\n        print(\"Ops! NO files extracted\")\n        volume = np.zeros((num_slices, image_size, image_size), dtype = np.uint8)\n        metadeta = {'age': 40, 'sex': 0, 'modality': 'CT'}\n        return volume, \n    \n    \n    rows = int(np.ceil(50 / 5))\n    plt.figure(figsize=(20, 50))\n    ### Only 50 files from the first directory\n    for i, filepath in enumerate(all_filepaths):\n        if i == 50:\n            break\n\n        ds = pydicom.dcmread(filepath, force = True)\n        img = ds.pixel_array \n        plt.subplot(rows, 5 , i + 1)\n        plt.imshow(img, cmap=\"gray\")\n    plt.show()\n        \n                    \nvisualize(images_path)        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:44:04.343598Z","iopub.execute_input":"2025-09-25T06:44:04.343872Z","iopub.status.idle":"2025-09-25T06:44:13.342503Z","shell.execute_reply.started":"2025-09-25T06:44:04.343843Z","shell.execute_reply":"2025-09-25T06:44:13.341093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#---------------------------------- IMAGE PROCESSING ---------------------------------\n\nimage_size = 512\nnum_slices = 32\n\ndef adaptive_windowing(image, modality):\n    percentile_range = (5, 95)\n\n    img_flat = image.flatten()\n    img_flat = img_flat[img_flat > 0]\n    if len(img_flat) == 0:\n        return np.zeros_like(image, dtype=np.uint8)\n    low_val = np.percentile(img_flat, percentile_range[0])\n    \n    high_val = np.percentile(img_flat, percentile_range[1])\n\n    if modality in ['CTA']:\n        window_width = (high_val - low_val) * 1.5\n        window_center = (high_val + low_val) / 2\n    elif modality in ['MRA']:\n        window_width = (high_val - low_val) * 1.2\n        window_center = high_val * 0.7\n    else:\n        window_width = high_val - low_val\n        window_center = (high_val + low_val) / 2    \n\n    img_min = window_center - window_width / 2\n    img_max = window_center + window_width / 2\n    img_windowed = np.clip(image, img_min, img_max)\n\n## normalize 0-255\n    if img_max > img_min:\n        img_normalized = ((img_windowed - img_min) / (img_max - img_min) * 255).astype(np.uint8)\n    else: \n        img_normalized = np.zeros_like(image, dtype = np.uint8)\n    return img_normalized\n\n\ndef process_dicom_series(series_path):\n\n    all_filepaths = []\n    for file in os.listdir(series_path):  #get all files of sub director\n        if file.endswith(\".dcm\"):\n            all_filepaths.append(os.path.join(series_path, file))\n                \n    if len(all_filepaths) == 0: \n        #no files extracted\n        print(\"Ops! NO files extracted\")\n        volume = np.zeros((num_slices, image_size, image_size), dtype = np.uint8)\n        metadeta = {'age': 40, 'sex': 0, 'modality': 'CT'}\n        return volume, metadata\n\n    metadata = {}\n    dicom_data = []\n    for i, filepath in enumerate(all_filepaths):\n        ds = pydicom.dcmread(filepath, force = True)\n        img = ds.pixel_array \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        instance_num = getattr(ds, 'InstanceNumber', i) # get instance number for proper slice sorting\n        if i == 0: # extract the metadata\n            metadata[\"modality\"] = getattr(ds, 'Modality', 'CT')\n            try: \n                age_str = getattr(ds, 'PatientAge', '50Y')\n                age = int(''.join(filter(str.isdigit, age_str[:3]))) or '50'\n                metadata['age'] = min(age, 100)\n            except:\n                metadata['age'] = 50\n            try:\n                sex = getattr(ds, 'PatientSex', 'M')\n                metadata['sex'] = 1 if sex == 'M' else 0\n            except:\n                metadata['sex'] = 0\n        \n        #### ------ APPLY RESCALING------------\n        if hasattr(ds, 'RescaleSlope') and hasattr(ds, 'RescaleIntercept'):\n            img = img * ds.RescaleSlope + ds.RescaleIntercept\n            \n        #input arrays shape not matching so resizing image should be done before stacking\n        img_resized = cv2.resize(img, (image_size, image_size))\n        dicom_data.append((instance_num, img_resized))\n\n    if len(dicom_data) == 0:\n        volume = np.zeros((num_slices, image_size, image_size), dtype = np.uint8)\n        return volume, metadata\n            \n    dicom_data.sort(key = lambda x: x[0])\n    raw_slices = [d[1] for d in dicom_data]\n    volume_3d = np.stack(raw_slices, axis = 0) #stacking on top of each other (row-wise)\n\n    ## apply modality specific intensity windowing\n    volume_windowed = adaptive_windowing(volume_3d, metadata['modality'])\n\n    ## Resize slices \n    processed_slices = []\n    for img in volume_windowed:\n        resized = cv2.resize(img, (image_size, image_size))\n        processed_slices.append(resized)\n    volume = np.array(processed_slices)\n\n    if len(processed_slices) > num_slices:\n        indices = np.linspace(0, len(processed_slices) - 1, num_slices).astype(int)\n        volume = volume[indices]\n    elif len(processed_slices) < num_slices:\n        pad_size = num_slices - len(processed_slices)\n        volume = np.pad(volume, ((0,pad_size), (0,0) , (0,0)), mode = 'edge')\n    return volume, metadata\n\nimg_folder = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series/1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\"\nvolume, metadata = process_dicom_series(img_folder)        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:40.29088Z","iopub.execute_input":"2025-09-25T06:47:40.291204Z","iopub.status.idle":"2025-09-25T06:47:46.129551Z","shell.execute_reply.started":"2025-09-25T06:47:40.29118Z","shell.execute_reply":"2025-09-25T06:47:46.128575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(metadata.keys())\nprint(volume.shape)\nmetadata","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:46.130977Z","iopub.execute_input":"2025-09-25T06:47:46.131298Z","iopub.status.idle":"2025-09-25T06:47:46.139077Z","shell.execute_reply.started":"2025-09-25T06:47:46.131263Z","shell.execute_reply":"2025-09-25T06:47:46.138107Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"##----------- LET'S VISUALIZE SOME PROCESSED IMAGES--------------\n\nimport matplotlib.pyplot as plt\nplt.figure(figsize = (10, 10))\nfor i in range(12):\n    plt.subplot(4, 3, i+1)\n    plt.imshow(volume[i], cmap='gray')  \nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:46.140272Z","iopub.execute_input":"2025-09-25T06:47:46.140629Z","iopub.status.idle":"2025-09-25T06:47:48.194Z","shell.execute_reply.started":"2025-09-25T06:47:46.140603Z","shell.execute_reply":"2025-09-25T06:47:48.192845Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#####----------------- CREATE RICH MULTI-CHANNEL REPRESENTATON OF 3D IMAGE------------------\n\"\"\"\nSince \"volumes\" obtained are 3D, in order to pass it in 2D network we need a smart way fro converting 3D to 2D without losing finer details \n\n\n\"\"\"\ndef create_multichannel_img(volume):\n    depth, height, width = volume.shape\n    #--------Channel 1: Adaptive maximum intensity projection---------\n    start = int(depth * 0.15)\n    end = int(depth * 0.85)\n    core_vol = volume[start: end]\n    mip = np.max(core_vol, axis = 0)\n\n    #------ Channel 2: weighted avg of high intensity slices------\n    #------------ Focusing on brightness-----------\n    slices_means = np.mean(volume, axis = (1,2))\n    top_percentile = np.percentile(slices_means, 75)\n    high_intensity = slices_means >= top_percentile\n    if np.any(high_intensity):\n        weighted_avg = np.mean(volume[high_intensity], axis = 0)\n    else:\n        weighted_avg = np.mean(volume, axis = 0)\n\n    ##-------- Channel 3: Std projection ----------\n    std_proj = np.zeros_like(volume[0])\n    window_size = min(5, depth // 4)\n    for i in range(depth - window_size + 1):\n        window_std = np.std(volume[i : i + window_size], axis = 0)\n        std_proj = np.maximum(std_proj, window_std)\n    #------- normalize all channels 0-255-----------\n    channels = []\n    for channel in [mip, weighted_avg, std_proj]:\n        if channel.max() > channel.min():\n            channel_norm = ((channel - channel.min()) / (channel.max() - channel.min()) * 255).astype(np.uint8)\n        else:  \n            channel_norm = np.zeros_like(channel, dtype = np.uint8)\n        channels.append(channel_norm)\n    return np.stack(channels, axis = -1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:49.071853Z","iopub.execute_input":"2025-09-25T06:47:49.0722Z","iopub.status.idle":"2025-09-25T06:47:49.081957Z","shell.execute_reply.started":"2025-09-25T06:47:49.072174Z","shell.execute_reply":"2025-09-25T06:47:49.080868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"vol = create_multichannel_img(volume)\n\nplt.figure(figsize=(12, 4))\n## display channel wise\n# Channel 1 (MIP)\nplt.subplot(1, 3, 1)\nplt.imshow(vol[:, :, 0], cmap=\"gray\")\nplt.title(\"Adaptive MIP\")\n\n# Channel 2 (Weighted Avg High-Intensity)\nplt.subplot(1, 3, 2)\nplt.imshow(vol[:, :, 1], cmap=\"gray\")\nplt.title(\"Weighted Avg\")\n\n# Channel 3 (Std Projection\")\nplt.subplot(1, 3, 3)\nplt.imshow(vol[:, :, 2], cmap=\"gray\")\nplt.title(\"Std Projection\")\n\nplt.show()\n\n#3 channels\nplt.figure(figsize=(6,6))\nplt.imshow(vol)  \nplt.title(\"Multi-channel projection image\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:51.838248Z","iopub.execute_input":"2025-09-25T06:47:51.838632Z","iopub.status.idle":"2025-09-25T06:47:53.033389Z","shell.execute_reply.started":"2025-09-25T06:47:51.838606Z","shell.execute_reply":"2025-09-25T06:47:53.032423Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"avail_pretrained_models = timm.list_models(pretrained=True)\nprint(len(avail_pretrained_models)) # timm currently supports 1599 models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:53.712686Z","iopub.execute_input":"2025-09-25T06:47:53.713041Z","iopub.status.idle":"2025-09-25T06:47:53.729289Z","shell.execute_reply.started":"2025-09-25T06:47:53.713014Z","shell.execute_reply":"2025-09-25T06:47:53.728403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"vol.shape ### height * width * channels ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:57.47801Z","iopub.execute_input":"2025-09-25T06:47:57.479331Z","iopub.status.idle":"2025-09-25T06:47:57.485034Z","shell.execute_reply.started":"2025-09-25T06:47:57.479299Z","shell.execute_reply":"2025-09-25T06:47:57.484188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"###-------------- EXPLORING CSV FILE---------------\ndf = pd.DataFrame(train_df)\n\nrow = df.iloc[1]\nprint(row)\nprint(\"Series Instance ID for 1st row:\",row[ID_COL])\ndf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:47:59.078221Z","iopub.execute_input":"2025-09-25T06:47:59.07896Z","iopub.status.idle":"2025-09-25T06:47:59.099128Z","shell.execute_reply.started":"2025-09-25T06:47:59.078926Z","shell.execute_reply":"2025-09-25T06:47:59.098099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# WE KNOW THAT SeriesINstanceUID IS UNIQUE REPRESENTATION OF CASES \n# SeriesINstanceUID FROM THE CSV DIRECTLY CONNECTS TO SERIES/SERIESINSTANCEID AND THIS IS HOW WE ARE GOING TO CONNECT LABELS (TARGETS) TO EACH BRAIN SCAN IMAGES (CASES)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-23T19:54:01.483467Z","iopub.execute_input":"2025-09-23T19:54:01.483697Z","iopub.status.idle":"2025-09-23T19:54:01.487736Z","shell.execute_reply.started":"2025-09-23T19:54:01.483681Z","shell.execute_reply":"2025-09-23T19:54:01.486956Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"###------------------- Model Architecture --------------------\n\nclass ClassificationModel(nn.Module):\n    def __init__(self, model_name, num_classes = 14, pretrained = True, drop_rate = 0.3, drop_path_rate = 0):\n        super().__init__()\n\n        \n        self.backbone = timm.create_model(\n            model_name,\n            pretrained = pretrained,\n            in_chans = 3, # 3 for RGB Images\n            num_classes = 0, # removes the classifier head entirely\n            exportable = True, #model made exportable for ONNX\n            drop_rate = 0, \n            drop_path_rate = 0,\n            global_pool = '' # skip global pooling. \n        )\n\n        with torch.no_grad():\n            dummy_input = torch.zeros(5, 3, image_size, 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            else: \n                num_features = features.shape[1] ## features.shape torch.Size([1, 1280]), backbone is already doing global pooling internally\n                self.needs_pool = False\n        if self.needs_pool == True:\n            self.global_pool = nn.AdaptiveAvgPool2d(1)\n        # Custom classifier\n        self.classifier = nn.Sequential(\n            nn.Linear(num_features, 512),\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) # 14 classes\n            \n        )\n\n    def forward(self, image):\n        #pass image to model\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 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        output = self.classifier(img_features)\n        return output\n\ninputs = torch.rand((5, 3, 512, 512))\nmodel = ClassificationModel(\"tf_efficientnetv2_s\")\nout = model(inputs)\nprint(out.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T06:48:19.862058Z","iopub.execute_input":"2025-09-25T06:48:19.862645Z","iopub.status.idle":"2025-09-25T06:48:34.607039Z","shell.execute_reply.started":"2025-09-25T06:48:19.862616Z","shell.execute_reply":"2025-09-25T06:48:34.605939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"## Since now we have Generated 3 channels 2D image, we need to pass this to model along with the target (y) values. So, next step is preparing data reading for training model\n\n###---------------IMAGE AUGMENTATIONS----------------------\nimage_size = 512\n## Note: transformation on train and validation data would be different\ndef apply_train_transform(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.Affine(\n            translate_percent={\"x\": 0.0625, \"y\": 0.0625},\n            scale=(0.9, 1.1),\n            rotate=(-45, 45),\n            p=0.5\n        ),\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 apply_val_transform(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#------------------------------PREPARING DATA READING FOR TRAINING-------------------------------\nimport torch\nimport pandas as pd\nfrom torch.utils.data import Dataset\nclass CustomDataset(Dataset):\n    def __init__(self, df, img_series, is_train, transform):\n        self.df = df\n        self.img_series = img_series\n        self.is_train = is_train\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        \n        row = self.df.iloc[idx]\n        instance_id = row[ID_COL] \n\n        ## you have series_id now, you can take that particular folder from series/ and preprocess it!!\n        volume, metadata = process_dicom_series(os.path.join(self.img_series, instance_id))\n        #you got 3d volume, now its time to transform it to 2D 3channels imgs\n        img = create_multichannel_img(volume)\n\n        transformed_img = self.transform(image = img)['image']\n        labels = torch.tensor(row[LABEL_COLS].values.astype(float), dtype=torch.float32)\n        return transformed_img, labels\n\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\ntrain_df = pd.read_csv(train_csv)\ntrain_df, val_df = train_test_split(\n    train_df,\n    test_size = 0.2, #20% for validation\n    random_state = 42,\n    stratify = train_df[\"Aneurysm Present\"]   # Since Aneurysm present is rare case in the dataset, number of neg cases > pos cases so this is class imbalance case. \n)\n\ntrain_transform = apply_train_transform(image_size)\nval_transform = apply_val_transform(image_size)\n\n\ntrain_data = CustomDataset(train_df, images_path, is_train = True, transform = train_transform)\nval_data = CustomDataset(val_df, images_path, is_train = False, transform = val_transform)\n\ntrain_loader = DataLoader(train_data, batch_size = 5, shuffle = True, num_workers = os.cpu_count())\nval_loader = DataLoader(val_data, batch_size = 5, shuffle = False, num_workers = os.cpu_count())\n\n\nmodel = ClassificationModel(\n    model_name = \"tf_efficientnetv2_s\",\n    num_classes = 14,\n    pretrained = True,\n    drop_rate = 0.3,\n    drop_path_rate = 0.2\n).to(device)\n\n## in the evaluation section, it's mentioned that - 13 weight is assigned to Aneurysm Present and for the rest (locations) only 1. total there are 14 labels to predict. \n\nweights = torch.ones(14).to(device)\naneurysm_idx = LABEL_COLS.index('Aneurysm Present') #these index weight to be 13\nweights[aneurysm_idx] = 13\n\n##loss function\ncriterion = BCEWithLogitsLoss(pos_weight = weights)\n## optimizer\noptimizer = torch.optim.AdamW(model.parameters(), lr = 0.001, weight_decay = 0.0003)\n\n## scheduler\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, \n    mode='max',       # we want to maximize the metric (e.g. accuracy, AUC)\n    factor=0.5,       # multiply LR by 0.5 (reduce by half)\n    patience=3,       # wait 3 epochs of no improvement before reducing LR\n    verbose=True      # print when LR is reduced\n)\n\nscaler = GradScaler(enabled = True)\nmetric_auroc = AUROC(task = \"multilabel\", num_labels = 14, average = None).to(device)\n\nbest_score = -1.0\n\nepochs = 10\nfor epoch in range(epochs):\n    model.train()\n    running_loss = 0.0\n    train_pred = []\n    train_labels = []\n\n    for i, (images, labels) in enumerate(train_loader):\n        images = images.to(device)\n        labels = labels.to(device)\n        optimizer.zero_grad()\n\n        with autocast(enabled = True):\n            outputs = model(images)\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_pred.append(torch.sigmoid(outputs).detach().cpu())\n        train_labels.append(labels.detach().cpu())\n\n    epoch_train_loss = running_loss / len(train_data)\n    train_pred = torch.cat(train_pred)\n    train_labels = torch.cat(train_labels)\n\n    #----------Calculating AUROC per class and separate main target--------------\n    train_auroc_per_class = metric_auroc(train_pred, train_labels.long())\n    train_auroc_present = train_auroc_per_class[aneurysm_idx].item()\n    sum_other_aurocs = torch.sum(train_auroc_per_class[[i for i in range(14) if i != aneurysm_idx]]).item()\n    final_score = 0.5 * (train_auroc_present + (1/13) * sum_other_aurocs)\n    \n    # auroc = torch.mean(train_auroc_per_classp[[i for i in range(14)if i != aneurysm_idx]]).item()\n    # final_score = 0.5 * (train_auroc_per_class + auroc) ALSO WORKS!!\n    print(f\"Epoch: {epoch + 1}, Train Loss: {epoch_train_loss}, Train AUROC for Present: {train_auroc_present}, Final Score: {final_score}\")\n\n    ## Now validation part!!!\n    model.eval()\n    val_preds = []\n    val_labels = []\n    val_loss = 0.0\n\n    with torch.no_grad():\n        for i, (images, labels) in enumerate(val_loader):\n            images = images.to(device)\n            labels = labels.to(device)\n\n            with autocast(enabled = True):\n                outputs = model(images)\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_data)\n        val_pred = torch.cat(val_preds)\n        val_labels = torch.cat(val_labels) ## for passing to metric auroc\n\n    \n        val_auroc_per_class = metric_auroc(val_pred, val_labels.long())\n        val_auroc_present = val_auroc_per_class[aneurysm_idx].item()\n        sum_other_aurocs = torch.sum(val_auroc_per_class[[i for i in range(14) if i != aneurysm_idx]]).item()\n        final_score = 0.5 * (val_auroc_present + (1/13) * sum_other_aurocs)\n        \n        print(f\"Epoch: {epoch + 1}, Val Loss: {epoch_val_loss}, Val AUROC for Present: {val_auroc_present}, Final Score: {final_score}\")\n\n        scheduler.step(final_score)\n        if final_score > best_score:\n            best_score = final_score\n            print(f\"Best score found! Saving Model with score {best_score}\")\n\n            torch.save({\n                'model_state_dict' : model.state_dict(),\n                'best_score' : best_score\n            }, 'best_model.pth')\n        del images, labels, outputs, loss\n        gc.collect()\n\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n    print(\"Training Complete!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-25T05:41:15.26586Z","iopub.execute_input":"2025-09-25T05:41:15.2662Z","iopub.status.idle":"2025-09-25T05:41:15.274401Z","shell.execute_reply.started":"2025-09-25T05:41:15.266173Z","shell.execute_reply":"2025-09-25T05:41:15.273414Z"}},"outputs":[],"execution_count":null}]}