{"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":13762876,"sourceType":"competition"},{"sourceId":12780021,"sourceType":"datasetVersion","datasetId":8079690},{"sourceId":13037905,"sourceType":"datasetVersion","datasetId":8255679},{"sourceId":13039902,"sourceType":"datasetVersion","datasetId":8257136},{"sourceId":261278569,"sourceType":"kernelVersion"},{"sourceId":578041,"sourceType":"modelInstanceVersion","modelInstanceId":431765,"modelId":448675},{"sourceId":578244,"sourceType":"modelInstanceVersion","modelInstanceId":431852,"modelId":448675},{"sourceId":579093,"sourceType":"modelInstanceVersion","modelInstanceId":432115,"modelId":448675},{"sourceId":579097,"sourceType":"modelInstanceVersion","modelInstanceId":432118,"modelId":448675},{"sourceId":579597,"sourceType":"modelInstanceVersion","modelInstanceId":432485,"modelId":448675},{"sourceId":579599,"sourceType":"modelInstanceVersion","modelInstanceId":432486,"modelId":448675},{"sourceId":580558,"sourceType":"modelInstanceVersion","modelInstanceId":433283,"modelId":448675},{"sourceId":582469,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":434899,"modelId":448675}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Bronze Medal Solution  (Public LB = 0.70 / Private LB = 0.69)\n\nThis notebook is adapted and edited from https://www.kaggle.com/code/yosukeyama/rsna2025-32ch-img-infer-lb-0-69-share. Thank you the authors for sharing amazing works <3\n\n## Solution\n### Preprocessing & Modeling\nHere is training & preprocessing notebook: https://www.kaggle.com/code/tokkiwa/rsna25-022\n\nFollowing previous works, I converted all the DICOM series to the volume of shape (32, 384, 384). This makes the training quite faster than to use all data. \n\nIn terms of modeling, I used timm's various image networks with custom head. The head is either (1) 14-class Binary Prediction Head that predicts overall AP & Individual Presence, (2) Separated two 1-class / 13-class heads, or (3) completely separated 1-class / 13-class head (in this case, one model predicts only AP / Individual Presence, so I prepare two separate model to train). Among the timm models I tested, regnetx_040.pycls_in1k (22.1M) and tf_efficientnetv2_s.in21k_ft_in1k (21.5M) showed good performance. \n\n### Training \n\nI used the following losses: (1) BCE loss for 14 classes, (2) Focal Loss, (3)Weighted BCE loss. Models are trained with AdamW, Cosine LR Scheduler. I installed Mixup, Droppath, some augmentations (gauss noise, elastic transformation, etc). Refer to the training notebook for details.)\n\n### Ensembling\n\nI ensembled regnetx (two distinct model, ap/individual) + effnet (two distinct model, ap/individual) with simple prediction averaging. More models did not contributed to the score. \n\n","metadata":{}},{"cell_type":"markdown","source":"Here is training & preprocessing notebook:\nhttps://www.kaggle.com/code/tokkiwa/rsna25-022\n","metadata":{}},{"cell_type":"code","source":"\n# ====================================================\n# Competition constants\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]\n\n\n\n# ====================================================\n# Configuration\n# ====================================================\nclass InferenceConfig:\n    # Model settings\n    model_list = [\n        # { #path_shared_name は、古いモデルかつ２つのモデルが違うときに指定\n        #     \"model_name\": \"regnetx_040.pycls_in1k\",\n        #     \"ap_model_name\" : \"tf_efficientnetv2_s.in21k_ft_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/017d-reg-eff-mixup/1\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/017d-reg-eff-mixup/1\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": True,\n        # },\n        # {\n        #     \"model_name\": \"regnetx_040.pycls_in1k\",\n        #     \"ap_model_name\" : \"tf_efficientnetv2_s.in21k_ft_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/regnetx/1/\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-014-independent-dualmodel-weights/\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": False,\n        # },\n        {\n            \"model_name\": \"regnetx_040.pycls_in1k\",\n            \"ap_model_name\" : \"regnetx_040.pycls_in1k\",            \n            \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/regnetx/1/\",\n            \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/regnetx/1/\",\n            \"use_two_models\": True,\n            \"path_wo_name\": False,\n            \"path_shared_name\": False,\n        },\n        {\n            \"model_name\": \"tf_efficientnetv2_s.in21k_ft_in1k\",\n            \"ap_model_name\" : \"tf_efficientnetv2_s.in21k_ft_in1k\",            \n            \"model_dir\" : \"/kaggle/input/rsna25-014-independent-dualmodel-weights/\",\n            \"ap_model_dir\" : \"/kaggle/input/rsna25-014-independent-dualmodel-weights/\",\n            \"use_two_models\": True,\n            \"path_wo_name\": False,\n            \"path_shared_name\": False,\n        },\n        # {\n        #     \"model_name\": \"tf_efficientnetv2_s.in21k_ft_in1k\",           \n        #     \"model_dir\" : \"/kaggle/input/rsna2025-effnetv2-32ch\",\n        #     \"use_two_models\": False,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": False,\n        #     \"directly_load_timm\": True\n        # },\n        # {\n        #     \"model_name\": \"tf_efficientnetv2_s.in21k_ft_in1k\",           \n        #     \"model_dir\" : \"/kaggle/input/rsna25-008-withaugs\",\n        #     \"use_two_models\": False,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": False,\n        #     \"directly_load_timm\": False\n        # },\n        {\n            \"model_name\": \"regnetx_040.pycls_in1k\",           \n            \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/022-single-reg/1\",\n            \"use_two_models\": False,\n            \"path_wo_name\": True,\n            \"path_shared_name\": False,\n            \"directly_load_timm\": False\n        },\n        # {\n        #     \"model_name\": \"tiny_vit_21m_384.dist_in22k_ft_in1k\",\n        #     \"ap_model_name\" : \"tiny_vit_21m_384.dist_in22k_ft_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/default/1\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/default/1\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": False,\n        # },\n        # {\n        #     \"model_name\": \"regnetz_d8.ra3_in1k\",\n        #     \"ap_model_name\" : \"regnetz_d8.ra3_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/018a-regnetz-d8/1\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/018a-regnetz-d8/1\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": True,\n        #     \"path_shared_name\": False,\n        # },\n        #         {\n        #     \"model_name\": \"regnetx_064.pycls_in1k\",\n        #     \"ap_model_name\" : \"regnetx_064.pycls_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/018b-regnetx-064/1\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/018b-regnetx-064/1\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": True,\n        #     \"path_shared_name\": False,\n        # },\n        # {\n        #     \"model_name\": \"orig_3dresnet18_v1\",\n        #     \"ap_model_name\" : \"orig_3dresnet18_v1\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/019-3dresnet/1\",\n        #     \"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/019-3dresnet/1\",\n        #     \"use_two_models\": True,\n        #     \"path_wo_name\": True,\n        #     \"path_shared_name\": False,\n        # },\n        # {\n        #     \"model_name\": \"tf_efficientnetv2_s.in21k_ft_in1k\",\n        #     #\"ap_model_name\" : \"regnetx_064.pycls_in1k\",            \n        #     \"model_dir\" : \"/kaggle/input/rsna2025-effnetv2-32ch\",\n        #     #\"ap_model_dir\" : \"/kaggle/input/rsna25-015-dual-tinyvit/pytorch/018b-regnetx-064/1\",\n        #     \"use_two_models\": False,\n        #     \"path_wo_name\": False,\n        #     \"path_shared_name\": False,\n        # },\n\n    ]\n\n    # regnetx_040.pycls_in1k (22.1M)\n    # regnety_032.ra_in1k (19.4M)\n    # xception41.tf_in1k (27.0M)\n    # tiny_vit_21m_384.dist_in22k_ft_in1k (21.2M)\n    # resnet34.a1_in1k (21.8M)\n    # Dual-head architecture: location と presence を別々のヘッドで予測\n    use_dual_head = False\n    dual_head_hidden_dim = 512  # 各ヘッドの隠れ層サイズ\n    \n    # Two-model architecture: 2つの独立したモデルで予測\n    use_two_models = True\n    # use_two_models=True の場合:\n    # - model_name + model_dir: location予測用モデル (13クラス)\n    # - ap_model_name + ap_model_dir: presence予測用モデル (1クラス)\n    \n    size = 384\n    target_cols = LABEL_COLS\n    num_classes = len(target_cols)\n    in_chans = 32\n\n    # Albumentations Normalize must match training (scales 0..255 -> 0..1 -> (x-mean)/std per channel)\n    normalize_mean = [0.5] * in_chans\n    normalize_std = [0.5] * in_chans\n\n    # Preprocessing settings\n    target_shape = (32, 384, 384)  # (depth, height, width)\n\n    # Parallel processing settings\n    # 0: sequential processing (default for Kaggle)\n    # -1: use all available CPUs\n    # >0: use specific number of workers\n    num_workers = 0  # Set to 0 for Kaggle competition to avoid potential issues\n    enable_parallel_dicom_loading = False  # Set to True to enable parallel DICOM file loading\n    enable_parallel_slice_processing = False  # Set to True to enable parallel slice processing\n\n    # Inference settings\n    batch_size = 1\n    use_amp = True\n    use_tta = False  # TTA is prohibited due to left/right positional information\n    tta_transforms = 0\n\n    # Model paths (set to your dataset containing fold checkpoints)\n    # presenceモデルのパス\n    n_fold = 5\n    trn_fold = [0, 1, 2, 3, 4]\n\n    dual_head_hidden_dim = 512\n\n    # Ensemble weights (equal weight for all folds)\n    ensemble_weights = None  # None means equal weights\n\n    @classmethod\n    def enable_parallel_mode(cls, num_workers: int = -1):\n        \"\"\"Enable parallel processing mode for faster inference.\"\"\"\n        cls.num_workers = num_workers\n        cls.enable_parallel_dicom_loading = True\n        cls.enable_parallel_slice_processing = True\n        print(f\"Parallel mode enabled with {num_workers} workers\")\n    \n    @classmethod\n    def disable_parallel_mode(cls):\n        \"\"\"Disable parallel processing (recommended for Kaggle competition).\"\"\"\n        cls.num_workers = 0\n        cls.enable_parallel_dicom_loading = False\n        cls.enable_parallel_slice_processing = False\n        print(\"Parallel mode disabled (sequential processing)\")\n\nCFG = InferenceConfig()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-17T05:06:04.082227Z","iopub.execute_input":"2025-09-17T05:06:04.082478Z","iopub.status.idle":"2025-09-17T05:06:04.099455Z","shell.execute_reply.started":"2025-09-17T05:06:04.082454Z","shell.execute_reply":"2025-09-17T05:06:04.098789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Parallel Processing Configuration and Demo\n# ====================================================\n\n# Parallel processing can significantly speed up DICOM preprocessing\n# Especially beneficial when dealing with multiple 2D DICOM slices\n\nprint(\"Available parallel processing modes:\")\nprint(\"====================================\")\nprint(\"1. Sequential (default for Kaggle): CFG.disable_parallel_mode()\")\nprint(\"2. Parallel with specific workers:  CFG.enable_parallel_mode(num_workers=4)\")\nprint(\"3. Parallel with all CPUs:          CFG.enable_parallel_mode(num_workers=-1)\")\n\n# Example: Enable parallel mode for faster processing\n# Uncomment the line below to enable parallel processing\n# CFG.enable_parallel_mode(num_workers=4)\n\n# Example: Test different processing modes (uncomment to run benchmark)\n# Note: You need a valid DICOM series path for benchmarking\n# test_series_path = \"/path/to/test/dicom/series\"\n# if os.path.exists(test_series_path):\n#     benchmark_results = benchmark_processing_modes(test_series_path, num_runs=3)\n\nprint(\"\\nCurrent configuration:\")\nprint(f\"- num_workers: {CFG.num_workers}\")\nprint(f\"- enable_parallel_dicom_loading: {CFG.enable_parallel_dicom_loading}\")\nprint(f\"- enable_parallel_slice_processing: {CFG.enable_parallel_slice_processing}\")\n\nprint(\"\\nMemory usage considerations:\")\nprint(\"- Parallel processing uses more memory\")\nprint(\"- For Kaggle competitions, sequential mode is recommended\")\nprint(\"- For local development with sufficient RAM, parallel mode can provide 2-4x speedup\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-17T05:06:04.100681Z","iopub.execute_input":"2025-09-17T05:06:04.100863Z","iopub.status.idle":"2025-09-17T05:06:04.12078Z","shell.execute_reply.started":"2025-09-17T05:06:04.100847Z","shell.execute_reply":"2025-09-17T05:06:04.120051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pydicom\nimport cv2\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict, Optional\nfrom scipy import ndimage\nimport warnings\nimport gc\nfrom joblib import Parallel, delayed\nfrom collections import OrderedDict\nfrom tqdm.auto import tqdm\nwarnings.filterwarnings('ignore')\n\nclass DICOMPreprocessorKaggle:\n    \"\"\"\n    DICOM preprocessing system for Kaggle Code Competition\n    Converts original DICOMPreprocessor logic to single series processing\n    Enhanced with parallel processing capabilities\n    \"\"\"\n    \n    def __init__(self, target_shape: Tuple[int, int, int] = (32, 384, 384), num_workers: int = 0):\n        self.target_depth, self.target_height, self.target_width = target_shape\n        self.num_workers = num_workers  # 0: sequential, -1: all CPUs, >0: specific number\n        \n    def load_dicom_series(self, series_path: str) -> Tuple[List[pydicom.Dataset], str]:\n        \"\"\"\n        Load DICOM series\n        \"\"\"\n        series_path = Path(series_path)\n        series_name = series_path.name\n        \n        # Search for DICOM files\n        dicom_files = []\n        for root, _, files in os.walk(series_path):\n            for file in files:\n                if file.endswith('.dcm'):\n                    dicom_files.append(os.path.join(root, file))\n        \n        if not dicom_files:\n            raise ValueError(f\"No DICOM files found in {series_path}\")\n        \n        #print(f\"Found {len(dicom_files)} DICOM files in series {series_name}\")\n        \n        # Load DICOM datasets with optional parallel processing\n        if self.num_workers != 0 and len(dicom_files) > 1:\n            # Parallel loading for multiple files\n            def load_single_dicom(filepath):\n                try:\n                    return pydicom.dcmread(filepath, force=True)\n                except Exception as e:\n                    #print(f\"Failed to load {filepath}: {e}\")\n                    return None\n            \n            datasets = Parallel(n_jobs=self.num_workers)(\n                delayed(load_single_dicom)(filepath) \n                for filepath in tqdm(dicom_files, desc=\"Loading DICOM files\", leave=False)\n            )\n            # Filter out None values\n            datasets = [ds for ds in datasets if ds is not None]\n        else:\n            # Sequential loading\n            datasets = []\n            for filepath in tqdm(dicom_files, desc=\"Loading DICOM files\", leave=False):\n                try:\n                    ds = pydicom.dcmread(filepath, force=True)\n                    datasets.append(ds)\n                except Exception as e:\n                    #print(f\"Failed to load {filepath}: {e}\")\n                    continue\n        \n        if not datasets:\n            raise ValueError(f\"No valid DICOM files in {series_path}\")\n        \n        return datasets, series_name\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                    #print(\"ImagePositionPatient not found, using InstanceNumber\")\n            except Exception as e:\n                info['z_position'] = float(i)\n                #print(f\"Failed to extract position info: {e}\")\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        #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    def get_windowing_params(self, ds: pydicom.Dataset, img: np.ndarray = None) -> Tuple[Optional[float], Optional[float]]:\n        \"\"\"\n        Always use fixed mapping [0, 500] -> [0, 255] to match training preprocessing.\n        Returning non-None sentinels ensures the CT branch in apply_windowing_or_normalize is used.\n        \"\"\"\n        return \"CT\", \"CT\"\n    \n    def apply_windowing_or_normalize(self, img: np.ndarray, center: Optional[float], width: Optional[float]) -> np.ndarray:\n        \"\"\"\n        Apply windowing or statistical normalization\n        \"\"\"\n        if center is not None and width is not None:\n            # Statistical normalization with fixed range to match training\n            p1, p99 = 0, 500\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                \n                #print(f\"Applied fixed normalization: [{p1:.1f}, {p99:.1f}] → [0, 255]\")\n                return result\n            else:\n                # Fallback: min-max normalization\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    #print(f\"Applied min-max normalization: [{img_min:.1f}, {img_max:.1f}] → [0, 255]\")\n                    return result\n                else:\n                    # If image has no variation\n                    #print(\"Image has no variation, returning zeros\")\n                    return np.zeros_like(img, dtype=np.uint8)\n        \n        else:\n            # Not used in our pipeline anymore, kept for safety.\n            p1, p99 = np.percentile(img, [1, 99])\n            \n            if p99 > p1:\n                normalized = np.clip(img, p1, p99)\n                normalized = (normalized - p1) / (p99 - p1)\n                result = (normalized * 255).astype(np.uint8)\n                return result\n            else:\n                img_min, img_max = img.min(), img.max()\n                if img_max > img_min:\n                    normalized = (img - img_min) / (img_max - img_min)\n                    result = (normalized * 255).astype(np.uint8)\n                    return result\n                else:\n                    return np.zeros_like(img, dtype=np.uint8)\n    \n    def extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        \"\"\"\n        Extract 2D pixel array from DICOM and apply preprocessing (for 2D DICOM series)\n        \"\"\"\n        # Get pixel data\n        img = ds.pixel_array.astype(np.float32)\n        \n        # For 3D volume case (multiple frames) - select middle frame\n        if img.ndim == 3:\n            #print(f\"3D DICOM in 2D processing - using middle frame from shape: {img.shape}\")\n            frame_idx = img.shape[0] // 2\n            img = img[frame_idx]\n            #print(f\"Selected frame {frame_idx} from 3D DICOM\")\n        \n        # Convert color image to grayscale\n        if img.ndim == 3 and img.shape[-1] == 3:\n            img = cv2.cvtColor(img.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)\n            #print(\"Converted color image to grayscale\")\n        \n        # Disable RescaleSlope and RescaleIntercept (match training)\n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            img = img * float(slope) + float(intercept)\n            #print(f\"Applied rescaling: slope={slope}, intercept={intercept}\")\n        \n        return img\n    \n    def resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        \"\"\"\n        Resize 3D volume to target size\n        \"\"\"\n        current_shape = volume.shape\n        target_shape = (self.target_depth, self.target_height, self.target_width)\n        \n        if current_shape == target_shape:\n            return volume\n        \n        #print(f\"Resizing volume from {current_shape} to {target_shape}\")\n        \n        # 3D resizing using scipy.ndimage\n        zoom_factors = [\n            target_shape[i] / current_shape[i] for i in range(3)\n        ]\n        \n        # Resize with linear interpolation\n        resized_volume = ndimage.zoom(volume, zoom_factors, order=1, mode='nearest')\n        \n        # Clip to exact size just in case\n        resized_volume = resized_volume[:self.target_depth, :self.target_height, :self.target_width]\n        \n        # Padding if necessary\n        pad_width = [\n            (0, max(0, self.target_depth - resized_volume.shape[0])),\n            (0, max(0, self.target_height - resized_volume.shape[1])),\n            (0, max(0, self.target_width - resized_volume.shape[2]))\n        ]\n        \n        if any(pw[1] > 0 for pw in pad_width):\n            resized_volume = np.pad(resized_volume, pad_width, mode='edge')\n        \n        #print(f\"Final volume shape: {resized_volume.shape}\")\n        return resized_volume.astype(np.uint8)\n\n    def _process_slice_parallel(self, slice_data: Dict, window_center: Optional[float], window_width: Optional[float]) -> np.ndarray:\n        \"\"\"\n        Helper function to process a single slice for parallel execution\n        \"\"\"\n        ds = slice_data['dataset']\n        img = self.extract_pixel_array(ds)\n        processed_img = self.apply_windowing_or_normalize(img, window_center, window_width)\n        resized_img = cv2.resize(processed_img, (self.target_width, self.target_height))\n        return resized_img\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        try:\n            # 1. Load DICOM files\n            datasets, series_name = self.load_dicom_series(series_path)\n            \n            # Check first DICOM to determine 3D/2D\n            first_ds = datasets[0]\n            first_img = first_ds.pixel_array\n            \n            if len(datasets) == 1 and first_img.ndim == 3:\n                # Case 1: Single 3D DICOM file\n                #print(f\"Processing single 3D DICOM with shape: {first_img.shape}\")\n                return self._process_single_3d_dicom(first_ds, series_name)\n            else:\n                # Case 2: Multiple 2D DICOM files\n                #print(f\"Processing {len(datasets)} 2D DICOM files\")\n                return self._process_multiple_2d_dicoms(datasets, series_name)\n            \n        except Exception as e:\n            #print(f\"Failed to process series {series_path}: {e}\")\n            raise\n    \n    def _process_single_3d_dicom(self, ds: pydicom.Dataset, series_name: str) -> np.ndarray:\n        \"\"\"\n        Process single 3D DICOM file (for Kaggle: no file saving)\n        \"\"\"\n        # Get pixel array\n        volume = ds.pixel_array.astype(np.float32)\n        \n        # Disable RescaleSlope and RescaleIntercept (match training)\n        slope = getattr(ds, 'RescaleSlope', 1)\n        intercept = getattr(ds, 'RescaleIntercept', 0)\n        slope, intercept = 1, 0\n        if slope != 1 or intercept != 0:\n            volume = volume * float(slope) + float(intercept)\n            # #print(f\"Applied rescaling: slope={slope}, intercept={intercept}\")\n        \n        # Use fixed normalization parameters to match training\n        window_center, window_width = self.get_windowing_params(ds)\n        \n        # Apply normalization to each slice with optional parallel processing\n        if self.num_workers != 0 and volume.shape[0] > 1:\n            # Parallel processing of slices\n            def process_3d_slice(i):\n                slice_img = volume[i]\n                return self.apply_windowing_or_normalize(slice_img, window_center, window_width)\n            \n            processed_slices = Parallel(n_jobs=self.num_workers)(\n                delayed(process_3d_slice)(i) \n                for i in tqdm(range(volume.shape[0]), desc=f\"Processing 3D slices\", leave=False)\n            )\n        else:\n            # Sequential processing\n            processed_slices = []\n            for i in tqdm(range(volume.shape[0]), desc=f\"Processing 3D slices\", leave=False):\n                slice_img = volume[i]\n                processed_img = self.apply_windowing_or_normalize(slice_img, window_center, window_width)\n                processed_slices.append(processed_img)\n        \n        volume = np.stack(processed_slices, axis=0)\n        ##print(f\"3D volume shape after windowing: {volume.shape}\")\n        \n        # 3D resize\n        final_volume = self.resize_volume_3d(volume)\n        \n        ##print(f\"Successfully processed 3D DICOM series {series_name}\")\n        return final_volume\n    \n    def _process_multiple_2d_dicoms(self, datasets: List[pydicom.Dataset], series_name: str) -> np.ndarray:\n        \"\"\"\n        Process multiple 2D DICOM files (for Kaggle: no file saving)\n        \"\"\"\n        slice_info = self.extract_slice_info(datasets)\n        sorted_slices = self.sort_slices_by_position(slice_info)\n        first_img = self.extract_pixel_array(sorted_slices[0]['dataset'])\n        window_center, window_width = self.get_windowing_params(sorted_slices[0]['dataset'], first_img)\n        \n        # Process slices with optional parallel processing\n        if self.num_workers != 0 and len(sorted_slices) > 1:\n            # Parallel processing\n            #print(f\"Processing {len(sorted_slices)} slices in parallel with {self.num_workers} workers...\")\n            processed_slices = Parallel(n_jobs=self.num_workers)(\n                delayed(self._process_slice_parallel)(slice_data, window_center, window_width) \n                for slice_data in tqdm(sorted_slices, desc=f\"Processing {series_name}\", leave=False)\n            )\n        else:\n            # Sequential processing\n            #print(f\"Processing {len(sorted_slices)} slices sequentially...\")\n            processed_slices = []\n            for slice_data in tqdm(sorted_slices, desc=f\"Processing {series_name}\", leave=False):\n                processed_slices.append(self._process_slice_parallel(slice_data, window_center, window_width))\n\n        volume = np.stack(processed_slices, axis=0)\n        ##print(f\"2D slices stacked to volume shape: {volume.shape}\")\n        final_volume = self.resize_volume_3d(volume)\n        \n        ##print(f\"Successfully processed 2D DICOM series {series_name}\")\n        return final_volume\n\ndef process_dicom_series_kaggle(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384), num_workers: int = 0) -> 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        num_workers: Number of parallel workers (0: sequential, -1: all CPUs, >0: specific number)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape, num_workers=num_workers)\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] = (32, 384, 384), num_workers: int = 0) -> 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        num_workers: Number of parallel workers (0: sequential, -1: all CPUs, >0: specific number)\n    \n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        volume = process_dicom_series_kaggle(series_path, target_shape, num_workers=num_workers)\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] = (32, 384, 384), num_workers: int = 0):\n    \"\"\"\n    Test processing for single series\n    \n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n        num_workers: Number of parallel workers (0: sequential, -1: all CPUs, >0: specific number)\n    \"\"\"\n    try:\n        #print(f\"Testing single series: {series_path}\")\n        \n        # Execute processing\n        volume = process_dicom_series_safe(series_path, target_shape, num_workers=num_workers)\n        \n        # Display results\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\n        \n    except Exception as e:\n        #print(f\"✗ Failed to process series: {e}\")\n        return None","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-17T05:06:04.194474Z","iopub.execute_input":"2025-09-17T05:06:04.194724Z","iopub.status.idle":"2025-09-17T05:06:05.646238Z","shell.execute_reply.started":"2025-09-17T05:06:04.194705Z","shell.execute_reply":"2025-09-17T05:06:05.645436Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport gc\nimport json\nimport shutil\nimport warnings\nwarnings.filterwarnings('ignore')\nfrom pathlib import Path\nfrom typing import List, Dict, Optional, Tuple\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\nimport timm\n\n# Transformations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Competition API\nimport kaggle_evaluation.rsna_inference_server\n\n# DICOM preprocessor (DICOMPreprocessorKaggle class defined in previous cell)\n# In actual use, define in the same file or import appropriately\n\n# Set device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nassert torch.cuda.is_available(), \"CUDA IS DISABLED!!\"\n#print(f\"Using device: {device}\")\n\n\n# ====================================================\n# Transforms\n# ====================================================\ndef get_inference_transform():\n    \"\"\"Get inference transformation (must mirror training 'valid' transforms).\"\"\"\n    return A.Compose([\n        A.Resize(CFG.size, CFG.size),\n        A.Normalize(mean=CFG.normalize_mean, std=CFG.normalize_std),\n        ToTensorV2(),\n    ])\n\n# TTA is not used due to left/right positional information\n# def get_tta_transforms():\n#     \"\"\"TTA is prohibited for brain aneurysms due to left/right positioning\"\"\"\n#     pass\n\n# ====================================================\n# Model Loading Functions\n# ====================================================\n# Global variables\nMODELS = []\nTRANSFORM = None\nTTA_TRANSFORMS = None\n\ndef load_model_fold(fold: int, model_info) -> nn.Module:\n    \"\"\"Load a single fold model\"\"\"\n    if model_info[\"use_two_models\"]:\n        if model_info[\"path_wo_name\"]:\n            location_model_path = Path(model_info[\"model_dir\"]) / f'fold{fold}_best.pth'\n            presence_model_path = Path(model_info[\"ap_model_dir\"]) / f'fold{fold}_best.pth'\n        elif model_info[\"path_shared_name\"]:\n            # TwoModelsの場合、2つの別々のチェックポイントから読み込む\n            location_model_path = Path(model_info[\"model_dir\"]) / f'{model_info[\"model_name\"]}_fold{fold}_best.pth'\n            presence_model_path = Path(model_info[\"ap_model_dir\"]) / f'{model_info[\"model_name\"]}_fold{fold}_best.pth'\n        else:\n            # TwoModelsの場合、2つの別々のチェックポイントから読み込む\n            location_model_path = Path(model_info[\"model_dir\"]) / f'{model_info[\"model_name\"]}_fold{fold}_best.pth'\n            presence_model_path = Path(model_info[\"ap_model_dir\"]) / f'{model_info[\"ap_model_name\"]}_fold{fold}_best.pth'\n        if not location_model_path.exists():\n            raise FileNotFoundError(f\"Location model file not found: {location_model_path}\")\n        if not presence_model_path.exists():\n            raise FileNotFoundError(f\"Presence model file not found: {presence_model_path}\")\n\n        #print(f\"Loading fold {fold} location model from {location_model_path}...\")\n        #print(f\"Loading fold {fold} presence model from {presence_model_path}...\")\n\n        # Initialize TwoModels\n        model = TwoModels(\n            model_name = model_info[\"model_name\"],\n            ap_model_name = model_info[\"ap_model_name\"],\n            in_channels = CFG.in_chans,\n        )\n\n        # Load location model checkpoint\n        location_checkpoint = torch.load(location_model_path, map_location=device, weights_only=False)\n        location_state_dict = location_checkpoint['model']\n        # print(location_state_dict)\n        location_new_state_dict = OrderedDict()\n        for k, v in location_state_dict.items():\n            name = k\n            if k.startswith('module.'):\n                name = k[7:] \n            if k.startswith('model_location.'):\n                name = k[15:]  # model_location. を除去\n                location_new_state_dict[name] = v\n\n        # Load presence model checkpoint\n        presence_checkpoint = torch.load(presence_model_path, map_location=device, weights_only=False)\n        presence_state_dict = presence_checkpoint['model']\n        presence_new_state_dict = OrderedDict()\n        for k, v in presence_state_dict.items():\n            name = k\n            if k.startswith('module.'):\n                name = k[7:] \n            if k.startswith('model_presence.'):\n                name = k[15:]  # model_presence. を除去\n                presence_new_state_dict[name] = v\n\n        # Load weights into respective models\n        model.model_location.load_state_dict(location_new_state_dict)\n        model.model_presence.load_state_dict(presence_new_state_dict)\n\n    else:\n        if model_info[\"path_wo_name\"]:\n            model_path = Path(model_info[\"model_dir\"]) / f'fold{fold}_best.pth'\n        else:\n            # TwoModelsの場合、2つの別々のチェックポイントから読み込む\n            model_path = Path(model_info[\"model_dir\"]) / f'{model_info[\"model_name\"]}_fold{fold}_best.pth'\n        # Load checkpoint\n        checkpoint = torch.load(model_path, map_location=device, weights_only=False)\n        state_dict = checkpoint[\"model\"]\n\n        if model_info[\"directly_load_timm\"]:\n            model = timm.create_model(\n                    model_name = model_info[\"model_name\"],\n                    pretrained = False,\n                    in_chans = CFG.in_chans,\n                    num_classes = CFG.num_classes            \n            )\n            model.load_state_dict(state_dict)\n        else:\n            new_state_dict = OrderedDict()\n            for k, v in state_dict.items():\n                name = k\n                if k.startswith('module.'):\n                    name = k[7:]               \n                new_state_dict[name] = v\n    \n            if CFG.use_dual_head:\n                model = DualHeadModel(\n                    model_name = model_info[\"model_name\"],\n                    ap_model_name = CFG.ap_model_name,\n                    in_channels = CFG.in_chans,\n                )\n            else:\n                model = CustomModel(\n                    model_name = model_info[\"model_name\"],\n                    in_channels = CFG.in_chans,\n                    num_classes = CFG.num_classes\n                )\n\n            # Load trained weights\n            model.load_state_dict(new_state_dict)\n\n    model = model.to(device)\n    model.eval()\n\n    #print(f\"Successfully loaded fold {fold} model\")\n    return model\n\ndef load_models():\n    \"\"\"Load all fold models\"\"\"\n    global MODELS, TRANSFORM, TTA_TRANSFORMS\n\n    #print(\"Loading all fold models...\")\n    for model_info in CFG.model_list:\n        for fold in CFG.trn_fold:\n            MODELS.append(load_model_fold(fold, model_info))\n\n    if not MODELS:\n        raise ValueError(\"No models were loaded successfully\")\n\n    # Initialize transforms\n    TRANSFORM = get_inference_transform()\n    # TTA is not used due to left/right positioning\n    TTA_TRANSFORMS = None\n\n    #print(f\"Loaded {len(MODELS)} models: folds {list(MODELS.keys())}\")\n\n    # Warm up models\n    #print(\"Warming up models...\")\n    dummy_image = torch.randn(1, CFG.in_chans, CFG.size, CFG.size).to(device)\n\n    with torch.no_grad():\n        for model in MODELS:\n            _ = model(dummy_image)\n\n    #print(\"Models ready for inference!\")\n\n\n# ====================================================\n# Prediction Functions\n# ====================================================\ndef predict_single_model(model: nn.Module, image: np.ndarray) -> np.ndarray:\n    \"\"\"Make prediction with a single model (NO TTA due to left/right anatomy)\"\"\"\n\n    # Same processing as training code\n    # image shape: (D, H, W) = (32, 384, 384)\n    image = image.transpose(1, 2, 0)  # (D,H,W) -> (H,W,D) = (384, 384, 32)\n\n    # Apply same transform as training\n    transformed = TRANSFORM(image=image)\n    image_tensor = transformed['image']  # Shape: (32, 384, 384)\n    image_tensor = image_tensor.unsqueeze(0).to(device)  # (1, 32, 384, 384)\n\n    with torch.no_grad():\n        with autocast(enabled=CFG.use_amp):\n            output = model(image_tensor)\n            return torch.sigmoid(output).cpu().numpy().squeeze()\n\ndef predict_ensemble(image: np.ndarray) -> np.ndarray:\n    \"\"\"Make ensemble prediction across all folds\"\"\"\n    all_predictions = []\n    weights = []\n\n    for model in MODELS:\n        pred = predict_single_model(model, image)\n        all_predictions.append(pred)\n\n        # Use equal weights if not specified\n        # if CFG.ensemble_weights is not None:\n        #     weights.append(CFG.ensemble_weights.get(fold, 1.0))\n        # else:\n        #     weights.append(1.0)\n        weights.append(1.0)\n    # Weighted average\n    weights = np.array(weights) / np.sum(weights)\n    predictions = np.array(all_predictions)\n\n    return np.average(predictions, weights=weights, axis=0)\n\ndef _predict_inner(series_path: str) -> pl.DataFrame:\n    \"\"\"Main prediction logic (internal).\"\"\"\n    global MODELS\n\n    # Load models if not already loaded\n    if not MODELS:\n        load_models()\n\n    # Extract series ID\n    series_id = os.path.basename(series_path)\n\n    try:\n        # Process DICOM series using our preprocessor with parallel processing settings\n        num_workers = CFG.num_workers if CFG.enable_parallel_slice_processing else 0\n        volume = process_dicom_series_safe(series_path, CFG.target_shape, num_workers=num_workers)\n\n        # Make ensemble prediction\n        final_pred = predict_ensemble(volume)\n\n        # Create output dataframe\n        predictions_df = pl.DataFrame(\n            data=[[series_id] + final_pred.tolist()],\n            schema=[ID_COL] + LABEL_COLS,\n            orient='row'\n        )\n\n        # Return without ID column, as required by the API\n        return predictions_df.drop(ID_COL)\n\n    except Exception as e:\n        #print(f\"Error processing {series_id}: {e}\")\n        # Return conservative predictions\n        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions_df = pl.DataFrame(\n            data=[conservative_preds],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions_df\n\n# ====================================================\n# DICOM Processing (using DICOMPreprocessorKaggle defined in previous cell)\n# ====================================================\ndef process_dicom_series_safe(series_path: str, target_shape: Tuple[int, int, int] = (32, 384, 384), num_workers: int = 0) -> np.ndarray:\n    \"\"\"\n    Safe DICOM processing with memory cleanup\n    Uses DICOMPreprocessorKaggle defined in previous cell\n\n    Args:\n        series_path: Path to DICOM series\n        target_shape: Target volume size (depth, height, width)\n        num_workers: Number of parallel workers (0: sequential, -1: all CPUs, >0: specific number)\n\n    Returns:\n        np.ndarray: Processed volume\n    \"\"\"\n    try:\n        preprocessor = DICOMPreprocessorKaggle(target_shape=target_shape, num_workers=num_workers)\n        volume = preprocessor.process_series(series_path)\n        return volume\n    finally:\n        # Memory cleanup\n        gc.collect()\n\ndef predict_fallback(series_path: str) -> pl.DataFrame:\n    \"\"\"Fallback prediction function\"\"\"\n    #print(f\"Using fallback predictions for {os.path.basename(series_path)}\")\n\n    # Return conservative predictions\n    conservative_preds = [0.1] * len(LABEL_COLS)\n    predictions_df = pl.DataFrame(\n        data=[conservative_preds],\n        schema=LABEL_COLS,\n        orient='row'\n    )\n\n    # Clean up\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n\n    return predictions_df\n\ndef predict(series_path: str) -> pl.DataFrame:\n    \"\"\"\n    Top-level prediction function passed to the server.\n    It calls the core logic and guarantees cleanup in a `finally` block.\n    \"\"\"\n    try:\n        # Call the internal prediction logic\n        return _predict_inner(series_path)\n    except Exception as e:\n        #print(f\"Error during prediction for {os.path.basename(series_path)}: {e}\")\n        #print(\"Using fallback predictions.\")\n        # Return a fallback dataframe with the correct schema\n        conservative_preds = [0.1] * len(LABEL_COLS)\n        predictions = pl.DataFrame(\n            data=[conservative_preds],\n            schema=LABEL_COLS,\n            orient='row'\n        )\n        return predictions\n    finally:\n        # This code is required to prevent \"out of disk space\" and \"directory not empty\" errors.\n        # It deletes the shared folder and then immediately recreates it, ensuring it's\n        # empty and ready for the next prediction.\n        shared_dir = '/kaggle/shared'\n        shutil.rmtree(shared_dir, ignore_errors=True)\n        os.makedirs(shared_dir, exist_ok=True)\n        \n        # Also perform memory cleanup here\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        gc.collect()\n\n# ====================================================\n# Utility Functions for Performance Tuning\n# ====================================================\ndef benchmark_processing_modes(series_path: str, num_runs: int = 3):\n    \"\"\"\n    Benchmark different processing modes to find optimal settings.\n    \n    Args:\n        series_path: Path to a test DICOM series\n        num_runs: Number of runs for each mode\n    \"\"\"\n    import time\n    \n    print(\"Benchmarking DICOM processing modes...\")\n    print(\"=\" * 50)\n    \n    modes = [\n        (\"Sequential\", 0),\n        (\"Parallel (2 workers)\", 2),\n        (\"Parallel (4 workers)\", 4),\n        (\"Parallel (all CPUs)\", -1),\n    ]\n    \n    results = {}\n    \n    for mode_name, num_workers in modes:\n        print(f\"\\nTesting {mode_name}...\")\n        times = []\n        \n        for run in range(num_runs):\n            start_time = time.time()\n            try:\n                volume = process_dicom_series_safe(series_path, CFG.target_shape, num_workers=num_workers)\n                end_time = time.time()\n                times.append(end_time - start_time)\n                print(f\"  Run {run + 1}: {times[-1]:.2f}s\")\n            except Exception as e:\n                print(f\"  Run {run + 1}: Failed - {e}\")\n                times.append(float('inf'))\n        \n        avg_time = np.mean(times)\n        results[mode_name] = avg_time\n        print(f\"  Average: {avg_time:.2f}s\")\n    \n    print(\"\\n\" + \"=\" * 50)\n    print(\"BENCHMARK RESULTS:\")\n    print(\"=\" * 50)\n    \n    sorted_results = sorted(results.items(), key=lambda x: x[1])\n    for mode, time_taken in sorted_results:\n        if time_taken != float('inf'):\n            print(f\"{mode}: {time_taken:.2f}s\")\n        else:\n            print(f\"{mode}: Failed\")\n    \n    best_mode = sorted_results[0]\n    print(f\"\\nBest mode: {best_mode[0]} ({best_mode[1]:.2f}s)\")\n    \n    return results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-17T05:06:05.647683Z","iopub.execute_input":"2025-09-17T05:06:05.6482Z","iopub.status.idle":"2025-09-17T05:06:50.320904Z","shell.execute_reply.started":"2025-09-17T05:06:05.648179Z","shell.execute_reply":"2025-09-17T05:06:50.320349Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BasicBlock3D(nn.Module):\n    \"\"\"シンプルな3D残差ブロック\"\"\"\n    expansion = 1\n\n    def __init__(self, in_planes, planes, stride=1):\n        super(BasicBlock3D, self).__init__()\n        # 1つ目の畳み込み層\n        self.conv1 = nn.Conv3d(\n            in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False\n        )\n        self.bn1 = nn.BatchNorm3d(planes)\n        self.relu = nn.ReLU(inplace=True)\n        # 2つ目の畳み込み層\n        self.conv2 = nn.Conv3d(\n            planes, planes, kernel_size=3, stride=1, padding=1, bias=False\n        )\n        self.bn2 = nn.BatchNorm3d(planes)\n\n        # スキップ接続（ショートカット）\n        self.shortcut = nn.Sequential()\n        # ストライドが1より大きい、またはチャネル数が異なる場合、ショートカットも変換\n        if stride != 1 or in_planes != self.expansion * planes:\n            self.shortcut = nn.Sequential(\n                nn.Conv3d(\n                    in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False\n                ),\n                nn.BatchNorm3d(self.expansion * planes)\n            )\n\n    def forward(self, x):\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        out += self.shortcut(x) # 残差接続\n        out = self.relu(out)\n        return out\n\n\nclass ResNet3D(nn.Module):\n    \"\"\"\n    シンプルな3D ResNetモデル。\n    入力として [B, D, H, W] を受け取り、内部でチャネル次元を追加します。\n    \"\"\"\n    def __init__(self, block, num_blocks: List[int], num_classes: int = 14):\n        super(ResNet3D, self).__init__()\n        self.in_planes = 64\n\n        # 1. Stem: 初期畳み込み層\n        # H, Wを積極的にダウンサンプリングし、Dは緩やかに扱う\n        self.conv1 = nn.Conv3d(1, 64, kernel_size=(3, 7, 7), stride=(1, 2, 2), padding=(1, 3, 3), bias=False)\n        self.bn1 = nn.BatchNorm3d(64)\n        self.relu = nn.ReLU(inplace=True)\n        self.maxpool = nn.MaxPool3d(kernel_size=(3, 3, 3), stride=(2, 2, 2), padding=(1, 1, 1))\n\n        # 2. 4つの残差レイヤー\n        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)\n        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)\n        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)\n\n        # 3. Head: 分類層\n        # プーリングで特徴量をベクトル化\n        self.avgpool = nn.AdaptiveAvgPool3d((1, 1, 1))\n        # 全結合層でクラス分類\n        self.fc = nn.Linear(512 * block.expansion, num_classes)\n\n    def _make_layer(self, block, planes, num_blocks, stride):\n        strides = [stride] + [1] * (num_blocks - 1)\n        layers = []\n        for s in strides:\n            layers.append(block(self.in_planes, planes, s))\n            self.in_planes = planes * block.expansion\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        # 入力: [B, D, H, W] (例: [16, 32, 384, 384])\n        # Conv3dが要求する形式 [B, C, D, H, W] にするためにチャネル次元(C=1)を追加\n        x = x.unsqueeze(1) # -> [B, 1, D, H, W]\n\n        # Stem\n        out = self.relu(self.bn1(self.conv1(x)))\n        out = self.maxpool(out)\n\n        # ResNet Layers\n        out = self.layer1(out)\n        out = self.layer2(out)\n        out = self.layer3(out)\n        out = self.layer4(out)\n\n        # Head\n        out = self.avgpool(out)\n        out = torch.flatten(out, 1)\n        out = self.fc(out)\n\n        return out\n\ndef ResNet18_3D(num_classes=14):\n    \"\"\"ResNet-18相当の3Dモデルを返すヘルパー関数\"\"\"\n    return ResNet3D(BasicBlock3D, [2, 2, 2, 2], num_classes=num_classes)\n    \nclass CustomModel(nn.Module):\n    def __init__(self, model_name, pretrained=False, in_channels=3, num_classes=14):\n        super().__init__()\n        if model_name == \"orig_3dresnet18_v1\":\n            #custom model mode\n            self.model = ResNet18_3D(num_classes = num_classes)\n        else:\n            self.model = timm.create_model(\n                model_name,\n                pretrained=False,\n                in_chans=in_channels,\n                num_classes=num_classes,\n            )\n\n        # EfficientNetV2の場合、入力畳み込み層は 'conv_stem'\n        # in_channelsが3以外の場合、timmは自動で重みを調整しないことがあるため、手動で調整（オプション）\n        # if in_channels != 3 and pretrained:\n        #     original_conv = self.model.conv_stem\n        #     new_conv = nn.Conv2d(in_channels, original_conv.out_channels,\n        #                          kernel_size=original_conv.kernel_size, stride=original_conv.stride,\n        #                          padding=original_conv.padding, bias=original_conv.bias)\n        #     # 元の重みを平均して新しいチャネルにコピー (簡易的な方法)\n        #     with torch.no_grad():\n        #         new_conv.weight[:, :3, :, :] = original_conv.weight.clone()\n        #         if in_channels > 3:\n        #             new_conv.weight[:, 3:, :, :] = torch.mean(original_conv.weight, dim=1, keepdim=True).repeat(1, in_channels - 3, 1, 1)\n        #     self.model.conv_stem = new_conv\n\n    def forward(self, x):\n        return self.model(x)\n\nclass DualHeadModel(nn.Module):\n    \"\"\"\n    Dual-head architecture: 13個のlocation予測と1個のpresence予測を別々のヘッドで行う\n    \"\"\"\n    def __init__(self, model_name, pretrained=False, in_channels=3):\n        super().__init__()\n        # バックボーンモデル（分類層なし）\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            in_chans=in_channels,\n            num_classes=0,  # 分類層を削除\n        )\n\n        # バックボーンの出力次元を取得\n        self.feature_dim = self.backbone.num_features\n\n        # Location prediction head (13 locations)\n        self.location_head = nn.Sequential(\n            nn.Linear(self.feature_dim, CFG.dual_head_hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(0),\n            nn.Linear(CFG.dual_head_hidden_dim, 13)\n        )\n\n        # Presence prediction head (1 presence)\n        self.presence_head = nn.Sequential(\n            nn.Linear(self.feature_dim, CFG.dual_head_hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(0),\n            nn.Linear(CFG.dual_head_hidden_dim, 1)\n        )\n\n    def forward(self, x):\n        # 共通特徴抽出\n        features = self.backbone(x)\n\n        # 各ヘッドで予測\n        location_logits = self.location_head(features)  # (batch, 13)\n        presence_logits = self.presence_head(features)  # (batch, 1)\n\n        # 結合して従来の形式と同じ出力にする\n        return torch.cat([location_logits, presence_logits], dim=1)  # (batch, 14)\n\nclass TwoModels(nn.Module):\n    \"\"\"\n    2つの独立したモデルをカプセル化するラッパー。\n    - model_location: 13クラスの位置を予測\n    - model_presence: 1クラスの有無を予測\n    \"\"\"\n    def __init__(self, model_name, ap_model_name, pretrained=False, in_channels=3):\n        super().__init__()\n        self.model_location = CustomModel(model_name, pretrained, in_channels, num_classes=13)\n        self.model_presence = CustomModel(ap_model_name, pretrained, in_channels, num_classes=1)\n\n    def forward(self, x):\n        location_logits = self.model_location(x)\n        presence_logits = self.model_presence(x)\n        # 評価のために出力を結合\n        return torch.cat([location_logits, presence_logits], dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-17T05:06:50.321634Z","iopub.execute_input":"2025-09-17T05:06:50.321826Z","iopub.status.idle":"2025-09-17T05:06:50.340705Z","shell.execute_reply.started":"2025-09-17T05:06:50.321811Z","shell.execute_reply":"2025-09-17T05:06:50.339993Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# Main Execution\n# ====================================================\n\n# Load models at startup\nload_models()\nprint(\"Successfully loaded all folds\")\n\n# for model in MODELS:\n#     out = model(torch.zeros((1,32,384,384), device = \"cuda\"))\n#     print(out.shape)\n#     print(\"sanity checking the models... \")\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-09-17T05:06:50.342322Z","iopub.execute_input":"2025-09-17T05:06:50.342535Z","iopub.status.idle":"2025-09-17T05:07:14.271419Z","shell.execute_reply.started":"2025-09-17T05:06:50.342519Z","shell.execute_reply":"2025-09-17T05:07:14.270825Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}