{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13747926,"sourceType":"competition"},{"sourceId":12990154,"sourceType":"datasetVersion","datasetId":8222214},{"sourceId":13056635,"sourceType":"datasetVersion","datasetId":8250046}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"EXPERIMENT_NAME = \"022-singlemodel-revisited\"\n\nimport os\nimport gc\nimport random\nimport warnings\nfrom pathlib import Path\nfrom typing import *\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport timm\ntry:\n    from timm.scheduler import CosineLRScheduler\nexcept Exception:\n    from timm.scheduler.cosine_lr import CosineLRScheduler\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim.lr_scheduler import MultiStepLR, ReduceLROnPlateau\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nimport wandb\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom sklearn.model_selection import StratifiedKFold # GroupKFoldの代わりにこちらを使用\nfrom sklearn.metrics import roc_auc_score\nfrom scipy import ndimage\nfrom tqdm.notebook import tqdm\nfrom torch.distributions.beta import Beta\nfrom typing import Tuple\n\nwarnings.filterwarnings('ignore')\n\ndef set_seed(seed=42):\n    \"\"\"Set all random seeds for reproducibility\"\"\"\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:39.017532Z","iopub.execute_input":"2025-09-16T12:49:39.017778Z","iopub.status.idle":"2025-09-16T12:49:52.497414Z","shell.execute_reply.started":"2025-09-16T12:49:39.017751Z","shell.execute_reply":"2025-09-16T12:49:52.496536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nif \"/content\" in sys.path:\n    IS_RUNNING_IN_COLAB = True\n    from google.colab import drive\n    drive.mount('/content/drive')\n    EXPORT_DIR = f\"/content/drive/MyDrive/kaggle/RSNA25/{EXPERIMENT_NAME}/\"\n    os.makedirs(EXPORT_DIR, exist_ok = True)\n    assert os.path.exists(EXPORT_DIR), \"The EXPORT_DIR does not exist. Please check the path.\"\n    !pip install pydicom\nelse:\n    IS_RUNNING_IN_COLAB = False\n    EXPORT_DIR = \"./\"\nprint(\"The environment is\", \"colab\" if IS_RUNNING_IN_COLAB else \"kaggle\")\nimport pydicom","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:52.499395Z","iopub.execute_input":"2025-09-16T12:49:52.499786Z","iopub.status.idle":"2025-09-16T12:49:52.96201Z","shell.execute_reply.started":"2025-09-16T12:49:52.499767Z","shell.execute_reply":"2025-09-16T12:49:52.961402Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# model zoo:\n- regnetx_040.pycls_in1k (22.1M) ... Very Good\n- regnety_032.ra_in1k (19.4M) ... TBC\n- xception41.tf_in1k (27.0M) ... Bad\n- tiny_vit_21m_384.dist_in22k_ft_in1k (21.2M) ... Not Bad\n- resnet34.a1_in1k (21.8M) ... Bad\n- tf_efficientnetv2_s.in21k_ft_in1k (21.5M) ... Very Good\n- convnext_nano.in12k (22.5M) ... Bad\n- mambaout_tiny.in1k (26.5M) ... Bad\n- caformer_s18.sail_in22k_ft_in1k_384 (26.3M)\n- \n## Regnet Family\n- regnetz_d8.ra3_in1k (23.4M. IN1K Top1 = 84.054)\n- regnetz_040_h.ra3_in1k (28.9M. IN1K Top1 = 84.496)\n- regnetx_064.pycls_in1k (26.21M. IN1K Top1 = 79.064)\n\n\n# TODO:\n- Mixup / CutMix (Done)\n- unbalanced data handling (over sampling, Focal Loss, etc) ","metadata":{}},{"cell_type":"code","source":"class Config:\n    # データ\n    data_dir = \"/kaggle/input/rsna-intracranial-aneurysm-detection\" \n    if IS_RUNNING_IN_COLAB:\n        train_csv_path = os.path.join(tokkiwa_rsna25_minimum_path, \"train.csv\")\n        train_localizer_csv_path = os.path.join(tokkiwa_rsna25_minimum_path, \"train_localizer.csv\")\n        cache_dir = os.path.join(tokkiwa_rsna25_32x384x384_precached_path, \"cache_32x384x384\")\n    else:\n        cache_dir = os.path.join(\"/kaggle/input/rsna25-32x384x384-precached/cache_32x384x384\")\n        train_csv_path = os.path.join(data_dir, \"train.csv\")\n        train_localizer_csv_path = os.path.join(data_dir, \"train_localizer.csv\")\n    train_image_dir = os.path.join(data_dir, \"series\")\n\n    # 入力形状\n    image_height = 384\n    image_width = 384\n    num_slices = 32\n    in_channels = num_slices\n\n    # モデル\n    # Aneurysm Present のみの1クラス学習モード\n    pred_only_ap = False\n    \n    # Dual-head architecture: location と presence を別々のヘッドで予測\n    use_dual_head = False\n    dual_head_hidden_dim = 512  # 各ヘッドの隠れ層サイズ\n    dual_head_dropout = 0.3\n\n    use_seq_head = False\n\n    # Two-model architecture: 2つの独立したモデルで予測\n    use_two_models = False\n    model_name = \"regnetx_040.pycls_in1k\"\n    ap_model_name = None\n\n\n    pretrained = True\n    num_classes = 14  # 13 locations + 1 presence\n\n\n    # 学習設定\n    num_folds = 5\n    target_fold = 0\n    seed = 42\n    epochs = 10\n    batch_size = 16\n    learning_rate = 1e-4\n    optimizer_weight_decay = 1e-4\n    # scheduler_step / scheduler_gamma は未使用になるが残しておく（後方互換）\n\n    scheduler_type = \"cosinelr\" #[\"multisteplr\", \"cosinelr\", \"reducelr\"]\n    scheduler_step = [8,9] #multisteplr 用\n    scheduler_gamma = 0.1  #multisteplr 用\n    scheduler_patience = 3 #reducelr用\n    mixed_precision = True\n\n    optimizer_type = \"adamw\" #[\"adam\", \"adamw\", \"radam\"]\n\n    # timm CosineLRScheduler 設定（エポック単位）\n    cosine_cycle_mul = 1.0\n    cosine_cycle_decay = 0.5\n    cosine_min_lr = 1e-7\n    cosine_warmup_t = 5             # ウォームアップエポック数\n    cosine_warmup_lr_init = 1e-6     # ウォームアップ初期LR\n    cosine_t_in_epochs = True        # epoch 単位で step\n\n    # 正則化 / 不均衡対策\n    drop_rate = 0.2\n    drop_path_rate = 0.1\n    use_pos_weight = True\n    pos_weight_clip_min = 1.0\n    pos_weight_clip_max = 20.0\n    grad_clip_norm = 1.0\n    early_stopping_patience = 3\n    early_stopping_delta = 1e-5\n\n    # 追加: オーグメンテーション設定（軽め）\n    aug_gauss_noise_p = 0.2\n    aug_gauss_var_limit = (1.0, 10.0)   # 0-255スケール前提の軽いノイズ\n    aug_elastic_p = 0.2\n    aug_elastic_alpha = 10.0\n    aug_elastic_sigma = 5.0\n    aug_elastic_alpha_affine = 5.0\n\n    use_separate_loss = True\n    separate_present_weight = 1.0 #present が高いほうがスコアは高いはず（重くする？) \n    separate_location_weight = 1.0 \n\n    # Mixup\n    mixup_alpha=0.7\n    mixup_prob=0.5\n\n    # 追加: Focal Loss 設定    \n    use_focal_loss = False\n    focal_gamma = 2.0  # alpha は pos_weight から自動算出（利用時）\n\n    # ターゲット列・ラベル列\n    target_col = \"Aneurysm Present\"\n    all_label_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',\n        'Right Anterior Cerebral Artery', 'Left Posterior Communicating Artery',\n        'Right Posterior Communicating Artery', 'Basilar Tip',\n        'Other Posterior Circulation', 'Aneurysm Present',\n    ]\n\n    # データローダ\n    num_workers = 4\n    pin_memory = True\n    valid_shuffle = False\n\n    # 前処理（DICOM→ボリューム）\n    intensity_min = 0.0      # 正規化下限（推論と一致）\n    intensity_max = 500.0    # 正規化上限（推論と一致)\n\n    # キャッシュ\n    use_cache = True\n\n    # Normalize の統一（Albumentations）\n    normalize_mean = [0.5] * in_channels\n    normalize_std = [0.5] * in_channels\n\n    # ロギング\n    DEBUG = True\n    time_profiling = True\n    generate_cache_only = False        # True にすると訓練せずキャッシュ生成のみ実行\n    cache_only_missing = True          # 既に存在する .npy をスキップ\n    log_individual_aucs = True\n    use_wandb = True\n    experiment_name = EXPERIMENT_NAME\n\ndef effective_num_classes(cfg):\n    \"\"\"pred_only_apモードの場合は1クラス、通常は14クラス\"\"\"\n    return 1 if cfg.pred_only_ap else cfg.num_classes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:52.962726Z","iopub.execute_input":"2025-09-16T12:49:52.962979Z","iopub.status.idle":"2025-09-16T12:49:52.973589Z","shell.execute_reply.started":"2025-09-16T12:49:52.962957Z","shell.execute_reply":"2025-09-16T12:49:52.972878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"config_dict = {key: getattr(Config, key) for key in dir(Config) \n               if not key.startswith('__') and not callable(getattr(Config, key))}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:52.974258Z","iopub.execute_input":"2025-09-16T12:49:52.974611Z","iopub.status.idle":"2025-09-16T12:49:52.993775Z","shell.execute_reply.started":"2025-09-16T12:49:52.974593Z","shell.execute_reply":"2025-09-16T12:49:52.993053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Device configuration\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB\")\n    print(f\"CUDA version: {torch.version.cuda}\")\n    torch.cuda.empty_cache()\nif not Config.generate_cache_only and not torch.cuda.is_available():\n    raise RuntimeError(\"CUDA is not available! This code requires GPU.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:52.994592Z","iopub.execute_input":"2025-09-16T12:49:52.995074Z","iopub.status.idle":"2025-09-16T12:49:53.106471Z","shell.execute_reply.started":"2025-09-16T12:49:52.995037Z","shell.execute_reply":"2025-09-16T12:49:53.105585Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if Config.use_wandb:\n    if IS_RUNNING_IN_COLAB:\n      from google.colab import userdata\n      api_key = userdata.get('wandb_api')\n    else:\n      from kaggle_secrets import UserSecretsClient\n      user_secrets = UserSecretsClient()\n      api_key = user_secrets.get_secret(\"wandb_api\")\n    wandb.login(key=api_key)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:53.107203Z","iopub.execute_input":"2025-09-16T12:49:53.107418Z","iopub.status.idle":"2025-09-16T12:49:59.079915Z","shell.execute_reply.started":"2025-09-16T12:49:53.107401Z","shell.execute_reply":"2025-09-16T12:49:59.079192Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DICOMPreprocessorKaggle:\n    def __init__(self, target_shape: Tuple[int, int, int] = (32, 384, 384),\n                 intensity_min: float = 0.0, intensity_max: float = 500.0):\n        self.target_depth, self.target_height, self.target_width = target_shape\n        self.intensity_min = float(intensity_min)\n        self.intensity_max = float(intensity_max)\n\n    def load_dicom_series(self, series_path: str) -> List[pydicom.Dataset]:\n        series_path = Path(series_path)\n        dicom_files = []\n        for root, _, files in os.walk(series_path):\n            for f in files:\n                if f.endswith(\".dcm\"):\n                    dicom_files.append(os.path.join(root, f))\n        if not dicom_files:\n            raise FileNotFoundError(f\"No DICOM files found in {series_path}\")\n        datasets = []\n        for fp in dicom_files:\n            try:\n                ds = pydicom.dcmread(fp, force=True)\n                datasets.append(ds)\n            except Exception:\n                continue\n        if not datasets:\n            raise RuntimeError(f\"No valid DICOM files in {series_path}\")\n        return datasets\n\n    def _extract_slice_info(self, datasets: List[pydicom.Dataset]) -> List[Dict]:\n        infos = []\n        for i, ds in enumerate(datasets):\n            inst = getattr(ds, \"InstanceNumber\", i)\n            try:\n                pos = getattr(ds, \"ImagePositionPatient\", None)\n                z = float(pos[2]) if (pos is not None and len(pos) >= 3) else float(inst)\n            except Exception:\n                z = float(i)\n            infos.append({\"dataset\": ds, \"z\": z})\n        return sorted(infos, key=lambda x: x[\"z\"])\n\n    def _extract_pixel_array(self, ds: pydicom.Dataset) -> np.ndarray:\n        img = ds.pixel_array.astype(np.float32)\n        # 3Dマルチフレームの場合は中央スライスを使用（推論と同様の簡略化）\n        if img.ndim == 3:\n            img = img[img.shape[0] // 2]\n        # RGBならグレースケール化\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        # 推論と合わせて RescaleSlope/Intercept は無効化（slope=1, intercept=0 相当）\n        return img\n\n    def _normalize_fixed_range(self, img: np.ndarray) -> np.ndarray:\n        # 0〜500 を [0,255] に線形マッピング（推論と同等）\n        p1, p99 = self.intensity_min, self.intensity_max\n        if p99 <= p1:\n            # フォールバック: min-max\n            imin, imax = img.min(), img.max()\n            if imax > imin:\n                img = (img - imin) / (imax - imin)\n            else:\n                img = np.zeros_like(img, dtype=np.float32)\n            return (img * 255).astype(np.uint8)\n        img = np.clip(img, p1, p99)\n        img = (img - p1) / (p99 - p1 + 1e-7)\n        return (img * 255).astype(np.uint8)\n\n    def _resize_volume_3d(self, volume: np.ndarray) -> np.ndarray:\n        # volume: (D,H,W)\n        td, th, tw = self.target_depth, self.target_height, self.target_width\n        if volume.shape == (td, th, tw):\n            return volume\n        zoom_factors = [td / volume.shape[0], th / volume.shape[1], tw / volume.shape[2]]\n        resized = ndimage.zoom(volume, zoom_factors, order=1, mode=\"nearest\")\n        # クリップ/パディング\n        resized = resized[:td, :th, :tw]\n        if resized.shape != (td, th, tw):\n            pad = [(0, td - resized.shape[0]), (0, th - resized.shape[1]), (0, tw - resized.shape[2])]\n            resized = np.pad(resized, pad, mode=\"edge\")\n        return resized.astype(np.uint8)\n\n    def process_series(self, series_path: str) -> np.ndarray:\n        datasets = self.load_dicom_series(series_path)\n        # 単一3D DICOM（多フレーム）ならそのまま処理\n        first = datasets[0].pixel_array\n        if len(datasets) == 1 and first.ndim == 3:\n            vol = first.astype(np.float32)\n            # slope/intercept 無効化（推論と同等）\n            slices = [self._normalize_fixed_range(vol[i]) for i in range(vol.shape[0])]\n            volume = np.stack(slices, axis=0)\n            return self._resize_volume_3d(volume)\n\n        # 複数2D DICOM\n        sorted_infos = self._extract_slice_info(datasets)\n        proc = []\n        for info in sorted_infos:\n            img = self._extract_pixel_array(info[\"dataset\"])\n            img = self._normalize_fixed_range(img)\n            img = cv2.resize(img, (self.target_width, self.target_height), interpolation=cv2.INTER_LINEAR)\n            proc.append(img)\n        if not proc:\n            return np.zeros((self.target_depth, self.target_height, self.target_width), dtype=np.uint8)\n        volume = np.stack(proc, axis=0)\n        return self._resize_volume_3d(volume)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.082523Z","iopub.execute_input":"2025-09-16T12:49:59.08302Z","iopub.status.idle":"2025-09-16T12:49:59.100924Z","shell.execute_reply.started":"2025-09-16T12:49:59.083002Z","shell.execute_reply":"2025-09-16T12:49:59.100031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_dicom_patient_info(series_uid: str, dicom_base_dir: str) -> Tuple[str, str]:\n    \"\"\"\n    Extract StudyInstanceUID and PatientID from DICOM metadata.\n    (User provided function, modified to take base directory)\n    \"\"\"\n    try:\n        dicom_dir = os.path.join(dicom_base_dir, str(series_uid))\n        if os.path.exists(dicom_dir):\n            dcm_files = [f for f in os.listdir(dicom_dir) if f.endswith('.dcm')]\n            if dcm_files:\n                ds = pydicom.dcmread(\n                    os.path.join(dicom_dir, dcm_files[0]),\n                    stop_before_pixels=True,\n                    force=True\n                )\n                study_uid = getattr(ds, 'StudyInstanceUID', None)\n                patient_id = getattr(ds, 'PatientID', None)\n                \n                # study_uidとpatient_idの両方を返すように調整\n                fallback_id = f\"fallback_{series_uid[:16]}\"\n                return study_uid or fallback_id, patient_id or fallback_id\n    except Exception:\n        pass\n\n    fallback_id = f\"fallback_{series_uid[:16]}\"\n    return fallback_id, fallback_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.101881Z","iopub.execute_input":"2025-09-16T12:49:59.102105Z","iopub.status.idle":"2025-09-16T12:49:59.126165Z","shell.execute_reply.started":"2025-09-16T12:49:59.102087Z","shell.execute_reply":"2025-09-16T12:49:59.125343Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_dicom_series(series_path: str,\n                         target_shape: Tuple[int, int, int] = (Config.num_slices, Config.image_height, Config.image_width)) -> np.ndarray:\n    pre = DICOMPreprocessorKaggle(target_shape=target_shape,\n                                  intensity_min=Config.intensity_min,\n                                  intensity_max=Config.intensity_max)\n    return pre.process_series(series_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.127035Z","iopub.execute_input":"2025-09-16T12:49:59.127319Z","iopub.status.idle":"2025-09-16T12:49:59.141863Z","shell.execute_reply.started":"2025-09-16T12:49:59.127294Z","shell.execute_reply":"2025-09-16T12:49:59.14106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_transforms(data_type='train'):\n    if data_type == 'train':\n        # 左右反転(HorizontalFlip)は解剖学的な位置関係を壊すため使用しない\n        return A.Compose([\n            A.Resize(Config.image_height, Config.image_width), # 念のためリサイズ層を入れる\n            # 軽いノイズ＆弾性変形\n            A.GaussNoise(var_limit=Config.aug_gauss_var_limit, mean=0, per_channel=True, p=Config.aug_gauss_noise_p),\n            A.ElasticTransform(alpha=Config.aug_elastic_alpha,\n                               sigma=Config.aug_elastic_sigma,\n                               alpha_affine=Config.aug_elastic_alpha_affine,\n                               border_mode=cv2.BORDER_CONSTANT,\n                               p=Config.aug_elastic_p),\n            A.Rotate(limit=15, p=0.5, border_mode=cv2.BORDER_CONSTANT),\n            A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=0, p=0.5, border_mode=cv2.BORDER_CONSTANT),\n            A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n            A.Normalize(mean=[0.5] * Config.in_channels, std=[0.5] * Config.in_channels), # [0, 1]範囲の画像に対する標準化\n            ToTensorV2(),\n        ])\n    elif data_type == 'valid':\n        return A.Compose([\n            A.Resize(Config.image_height, Config.image_width),\n            A.Normalize(mean=[0.5] * Config.in_channels, std=[0.5] * Config.in_channels),\n            ToTensorV2(),\n        ])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.142686Z","iopub.execute_input":"2025-09-16T12:49:59.143266Z","iopub.status.idle":"2025-09-16T12:49:59.163872Z","shell.execute_reply.started":"2025-09-16T12:49:59.143248Z","shell.execute_reply":"2025-09-16T12:49:59.163334Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BrainAneurysmDataset(Dataset):\n    def __init__(self, df, transforms=None):\n        self.df = df.copy()\n        self.series_paths = self.df['series_path'].values\n        # 欠損は0埋め\n        self.df[Config.all_label_cols] = self.df[Config.all_label_cols].fillna(0)\n        \n        # pred_only_apモードの場合はAneurysm Presentのみ、通常は全ラベル\n        if Config.pred_only_ap:\n            self.labels = self.df[Config.target_col].values.astype(np.float32).reshape(-1, 1)\n        else:\n            self.labels = self.df[Config.all_label_cols].values.astype(np.float32)\n        self.transforms = transforms\n\n        if Config.use_cache:\n            try:\n                os.makedirs(Config.cache_dir, exist_ok=True)\n            except Exception as e:\n                print(e)\n                pass\n\n    def __len__(self):\n        return len(self.df)\n\n    def _cache_path(self, series_path: str) -> Path:\n        # 既定は .npz（圧縮保存）\n        key = os.path.basename(series_path)\n        return Path(Config.cache_dir) / f\"{key}.npz\"\n\n    def _legacy_cache_path_npy(self, series_path: str) -> Path:\n        # 後方互換で .npy も参照\n        key = os.path.basename(series_path)\n        return Path(Config.cache_dir) / f\"{key}.npy\"\n\n    def __getitem__(self, idx):\n        series_path = self.series_paths[idx]\n\n        # --- キャッシュ読み込み（.npz 優先、なければ .npy） ---\n        volume = None\n        if Config.use_cache:\n            npz_path = self._cache_path(series_path)\n            npy_path = self._legacy_cache_path_npy(series_path)\n            if npz_path.exists():\n                # print(npz_path, \"Cache hit\")\n                try:\n                    with np.load(npz_path) as data:\n                        volume = data[\"volume\"]\n                except Exception:\n                    volume = None\n            elif volume is None and npy_path.exists():\n                try:\n                    volume = np.load(npy_path)\n                except Exception:\n                    volume = None\n            # else:\n            #     print(npz_path, \"Cache miss\")\n\n        # --- DICOM前処理 ---\n        if volume is None:\n            volume = process_dicom_series(series_path)  # (D,H,W)=(32,384,384)\n            if Config.use_cache:\n                try:\n                    # 圧縮保存（キーは \"volume\"）\n                    np.savez_compressed(self._cache_path(series_path), volume=volume)\n                except Exception:\n                    pass\n\n        # Albumentationsは(H,W,C)想定 → (H,W,D)へ\n        volume = volume.transpose(1, 2, 0)  # (H,W,D)\n\n        if self.transforms:\n            augmented = self.transforms(image=volume)\n            image_tensor = augmented['image']  # (D,H,W)\n        else:\n            # フォールバック: Tensor化のみ (0-1正規化)\n            img = torch.from_numpy(volume.astype(np.float32) / 255.0)  # (H,W,D)\n            image_tensor = img.permute(2, 0, 1).contiguous()  # (D,H,W)\n\n        label_tensor = torch.from_numpy(self.labels[idx])\n        return image_tensor, label_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.164694Z","iopub.execute_input":"2025-09-16T12:49:59.164932Z","iopub.status.idle":"2025-09-16T12:49:59.186249Z","shell.execute_reply.started":"2025-09-16T12:49:59.164915Z","shell.execute_reply":"2025-09-16T12:49:59.185479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FocalLoss(nn.Module):\n    \"\"\"Focal Loss for multi-label with logits.\n    - gamma: focusing parameter\n    - alpha: tensor of per-class weights (optional). If None, will not reweight positives.\n    - pos_weight: like BCEWithLogitsLoss pos_weight, boosts positive examples (tensor per class) (optional).\n    Note: If both alpha and pos_weight are provided, both are applied multiplicatively to the positive term.\n    \"\"\"\n    def __init__(self, gamma: float = 2.0, alpha: Optional[torch.Tensor] = None, pos_weight: Optional[torch.Tensor] = None, reduction: str = 'mean'):\n        super().__init__()\n        self.gamma = gamma\n        self.register_buffer('alpha', alpha if alpha is not None else None)\n        self.register_buffer('pos_weight', pos_weight if pos_weight is not None else None)\n        self.reduction = reduction\n\n    def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:\n        # BCE with logits components\n        # sigmoid & probabilities\n        prob = torch.sigmoid(logits)\n        pt = torch.where(targets > 0.5, prob, 1.0 - prob)\n        # focal weight\n        focal_weight = (1.0 - pt).clamp(min=1e-6).pow(self.gamma)\n\n        # BCE term per element\n        bce = nn.functional.binary_cross_entropy_with_logits(logits, targets, reduction='none')\n\n        # optional class weights\n        if self.pos_weight is not None:\n            # emulate BCEWithLogitsLoss(pos_weight): weight positive examples\n            pos_w = self.pos_weight.view(1, -1)\n            weight = torch.where(targets > 0.5, pos_w, torch.ones_like(pos_w))\n            bce = bce * weight\n\n        if self.alpha is not None:\n            a = self.alpha.view(1, -1)\n            bce = bce * torch.where(targets > 0.5, a, 1 - a)\n\n        loss = focal_weight * bce\n        if self.reduction == 'mean':\n            return loss.mean()\n        elif self.reduction == 'sum':\n            return loss.sum()\n        else:\n            return loss","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.187067Z","iopub.execute_input":"2025-09-16T12:49:59.187313Z","iopub.status.idle":"2025-09-16T12:49:59.20158Z","shell.execute_reply.started":"2025-09-16T12:49:59.187297Z","shell.execute_reply":"2025-09-16T12:49:59.201026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultiLabelMixup:\n    \"\"\"\n    マルチラベル分類タスクのためのMixupを適用するクラス。\n\n    Args:\n        alpha (float, optional): ベータ分布のパラメータα。デフォルトは 1.0。\n                                 α=1.0の場合、一様分布からサンプリングされる。\n                                 値が小さいほど、0または1に近い極端な混合比率が選ばれやすくなる。\n        p (float, optional): 各バッチでMixupを適用する確率。デフォルトは 0.5。\n    \"\"\"\n    def __init__(self, alpha: float = 1.0, p: float = 0.5):\n        assert alpha > 0, \"alphaは正の値である必要があります。\"\n        assert 0.0 <= p <= 1.0, \"pは0から1の間の値である必要があります。\"\n        self.alpha = alpha\n        self.p = p\n\n    def __call__(self, x: torch.Tensor, y: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:\n        \"\"\"\n        入力されたバッチに対してMixup処理を適用します。\n\n        Args:\n            x (torch.Tensor): 入力画像テンソル。形状は [b, c, h, w]。\n            y (torch.Tensor): 入力ラベルテンソル（ハードラベル）。形状は [b, class]。\n\n        Returns:\n            Tuple[torch.Tensor, torch.Tensor]: Mixup適用後の画像テンソルとラベルテンソル（ソフトラベル）。\n        \"\"\"\n        # 確率pに基づいてMixupを適用するか判断\n        if torch.rand(1).item() >= self.p:\n            return x, y\n\n        # 混合比率ラムダをベータ分布からサンプリング\n        # .to(x.device)で、テンソルがCPU/GPUのどちらにあっても対応可能\n        lam = Beta(self.alpha, self.alpha).sample().to(x.device)\n\n        # バッチ内のインデックスをランダムにシャッフル\n        batch_size = x.size(0)\n        index = torch.randperm(batch_size, device=x.device)\n\n        # 画像のMixup\n        # xとシャッフル後のxをラムダで線形補間\n        # (b, c, h, w)のテンソルにブロードキャストするために形状を調整\n        mixed_x = lam.view(-1, 1, 1, 1) * x + (1 - lam).view(-1, 1, 1, 1) * x[index, :]\n\n        # ラベルのMixup\n        # yとシャッフル後のyをラムダで線形補間\n        # (b, class)のテンソルにブロードキャストするために形状を調整\n        mixed_y = lam.view(-1, 1) * y + (1 - lam).view(-1, 1) * y[index, :]\n\n        return mixed_x, mixed_y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.202355Z","iopub.execute_input":"2025-09-16T12:49:59.202565Z","iopub.status.idle":"2025-09-16T12:49:59.220235Z","shell.execute_reply.started":"2025-09-16T12:49:59.20255Z","shell.execute_reply":"2025-09-16T12:49:59.219617Z"}},"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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.220918Z","iopub.execute_input":"2025-09-16T12:49:59.221157Z","iopub.status.idle":"2025-09-16T12:49:59.235748Z","shell.execute_reply.started":"2025-09-16T12:49:59.221113Z","shell.execute_reply":"2025-09-16T12:49:59.235111Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CustomModel(nn.Module):\n    def __init__(self, model_name, pretrained=True, 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=pretrained,\n                in_chans=in_channels,\n                num_classes=num_classes,\n                drop_rate=Config.drop_rate,\n                drop_path_rate=Config.drop_path_rate,\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\n\nclass DualHeadModel(nn.Module):\n    \"\"\"\n    Dual-head architecture: 13個のlocation予測と1個のpresence予測を別々のヘッドで行う\n    \"\"\"\n    def __init__(self, model_name, pretrained=True, 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            drop_rate=Config.drop_rate,\n            drop_path_rate=Config.drop_path_rate,\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, Config.dual_head_hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(Config.dual_head_dropout),\n            nn.Linear(Config.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, Config.dual_head_hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(Config.dual_head_dropout),\n            nn.Linear(Config.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=True, 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)\n        \nclass SequentialHeadModel(nn.Module):\n    def __init__(self, model_name, pretrained=True, in_channels=3, num_locations=13):\n        super().__init__()\n        # 1. 共有バックボーン（分類層は削除）\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            in_chans=in_channels,\n            num_classes=0,  # 分類層なし\n        )\n        feature_dim = self.backbone.num_features\n\n        # 2. 位置予測ヘッド (13クラス)\n        self.location_head = nn.Linear(feature_dim, num_locations)\n\n        # 3. 有無予測ヘッド (13次元 -> 1次元)\n        # 位置予測ヘッドの出力ロジットを受け取る\n        self.presence_head = nn.Sequential(\n            nn.Linear(num_locations, 64), # 中間層\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 1) # 最終的な有無予測\n        )\n\n    def forward(self, x):\n        # 共有バックボーンで画像特徴を抽出\n        features = self.backbone(x) # -> [Batch, feature_dim]\n\n        # 位置予測ヘッドで各位置のロジットを計算\n        location_logits = self.location_head(features) # -> [Batch, 13]\n\n        # 位置ロジットを有無予測ヘッドに入力\n        # detach()しないことで、勾配がlocation_headやbackboneに流れるようにする\n        presence_logits = self.presence_head(location_logits) # -> [Batch, 1]\n\n        # 評価のために従来の形式通りに結合して返す\n        return torch.cat([location_logits, presence_logits], dim=1) # -> [Batch, 14]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.236458Z","iopub.execute_input":"2025-09-16T12:49:59.236698Z","iopub.status.idle":"2025-09-16T12:49:59.256615Z","shell.execute_reply.started":"2025-09-16T12:49:59.236676Z","shell.execute_reply":"2025-09-16T12:49:59.256029Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_cache():\n    \"\"\"\n    訓練を行わず、train.csv の全 series_path に対して DICOM 前処理を行い\n    Config.cache_dir に (D,H,W)=(num_slices,image_height,image_width) の .npz を保存する。\n    \"\"\"\n    try:\n        os.makedirs(Config.cache_dir, exist_ok=True)\n    except Exception as e:\n        print(e)\n        pass\n\n    df = pd.read_csv(Config.train_csv_path)\n    if Config.DEBUG:\n        print(f\"Loaded {len(df)} rows from {Config.train_csv_path}\")\n\n    df['series_path'] = df['SeriesInstanceUID'].apply(\n        lambda sid: os.path.join(Config.train_image_dir, str(sid))\n    )\n\n    pre = DICOMPreprocessorKaggle(\n        target_shape=(Config.num_slices, Config.image_height, Config.image_width),\n        intensity_min=Config.intensity_min,\n        intensity_max=Config.intensity_max,\n    )\n\n    errors = 0\n    for series_path in tqdm(df['series_path'].values, desc=\"Generating cache\"):\n        base = os.path.basename(series_path)\n        npz_path = Path(Config.cache_dir) / f\"{base}.npz\"\n        npy_path = Path(Config.cache_dir) / f\"{base}.npy\"\n\n        # 既存スキップ（.npz があればスキップ、後方互換で .npy があってもスキップ可能）\n        if Config.cache_only_missing and (npz_path.exists() or npy_path.exists()):\n            continue\n        try:\n            volume = pre.process_series(series_path)  # (D,H,W) uint8\n            np.savez_compressed(npz_path, volume=volume)\n        except Exception as e:\n            errors += 1\n            if Config.DEBUG:\n                print(f\"[WARN] Failed: {series_path} -> {e}\")\n\n    print(f\"Cache generation completed. Errors: {errors}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.257406Z","iopub.execute_input":"2025-09-16T12:49:59.257629Z","iopub.status.idle":"2025-09-16T12:49:59.276386Z","shell.execute_reply.started":"2025-09-16T12:49:59.257613Z","shell.execute_reply":"2025-09-16T12:49:59.275706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_fn(model, dataloader, optimizer, criterion, scaler, device, \n             optimizer_location=None, optimizer_presence=None, scaler_location=None, scaler_presence=None, mixup_fn  =None):\n    model.train()\n    total_loss = 0\n    total_present_loss = 0\n    total_location_loss = 0\n    progress_bar = tqdm(dataloader, desc=\"Training\", leave=False)\n    \n    # 分離損失またはdual-head用のBCE\n    if Config.use_separate_loss or Config.use_dual_head or Config.use_two_models:\n        present_bce = nn.BCEWithLogitsLoss().to(device)\n        location_bce = nn.BCEWithLogitsLoss().to(device)\n\n    for images, labels in progress_bar:\n        images = images.to(device)\n        labels = labels.to(device).to(torch.float)\n\n        #mixup\n        if mixup_fn is not None:\n            images, labels = mixup_fn(images, labels)     \n\n        if not Config.use_two_models:\n            optimizer.zero_grad()\n\n        with autocast(enabled=Config.mixed_precision):\n            if Config.use_two_models:\n                # 2つのモデルを完全に独立して訓練\n                \n                # Location modelの訓練\n                optimizer_location.zero_grad()\n                location_logits = model.module.model_location(images)\n                location_loss = location_bce(location_logits, labels[:, :13])\n                scaler_location.scale(location_loss).backward()\n                if Config.grad_clip_norm is not None and Config.grad_clip_norm > 0:\n                    scaler_location.unscale_(optimizer_location)\n                    torch.nn.utils.clip_grad_norm_(model.module.model_location.parameters(), Config.grad_clip_norm)\n                scaler_location.step(optimizer_location)\n                scaler_location.update()\n                \n                # Presence modelの訓練\n                optimizer_presence.zero_grad()\n                presence_logits = model.module.model_presence(images)\n                present_loss = present_bce(presence_logits, labels[:, -1:])\n                scaler_presence.scale(present_loss).backward()\n                if Config.grad_clip_norm is not None and Config.grad_clip_norm > 0:\n                    scaler_presence.unscale_(optimizer_presence)\n                    torch.nn.utils.clip_grad_norm_(model.module.model_presence.parameters(), Config.grad_clip_norm)\n                scaler_presence.step(optimizer_presence)\n                scaler_presence.update()\n                \n                # 統計用の合計損失\n                loss = (Config.separate_present_weight * present_loss + \n                       Config.separate_location_weight * location_loss)\n                \n                total_present_loss += present_loss.item()\n                total_location_loss += location_loss.item()\n                outputs = torch.cat([location_logits, presence_logits], dim=1) # for compatibility\n            else:\n                outputs = model(images)\n                if Config.pred_only_ap:\n                    # Aneurysm Presentのみの学習\n                    loss = criterion(outputs, labels)\n                elif Config.use_dual_head:\n                    # Dual-head: location と presence で別々の損失\n                    present_loss = present_bce(outputs[:, -1:], labels[:, -1:])\n                    location_loss = location_bce(outputs[:, :13], labels[:, :13])\n                    loss = (Config.separate_present_weight * present_loss + \n                           Config.separate_location_weight * location_loss)\n                    total_present_loss += present_loss.item()\n                    total_location_loss += location_loss.item()\n                elif Config.use_separate_loss:\n                    # Present (最後の列) と Location (最初の13列) で分離\n                    present_loss = present_bce(outputs[:, -1:], labels[:, -1:])\n                    location_loss = location_bce(outputs[:, :13], labels[:, :13])\n                    loss = (Config.separate_present_weight * present_loss + \n                           Config.separate_location_weight * location_loss)\n                    total_present_loss += present_loss.item()\n                    total_location_loss += location_loss.item()\n                else:\n                    loss = criterion(outputs, labels)\n\n        if not Config.use_two_models:\n            scaler.scale(loss).backward()\n            # 勾配クリッピング\n            if Config.grad_clip_norm is not None and Config.grad_clip_norm > 0:\n                scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(model.parameters(), Config.grad_clip_norm)\n            scaler.step(optimizer)\n            scaler.update()\n\n        total_loss += loss.item()\n        progress_bar.set_postfix(loss=loss.item())\n\n    avg_loss = total_loss / len(dataloader)\n    avg_present_loss = total_present_loss / len(dataloader) if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) else 0.0\n    avg_location_loss = total_location_loss / len(dataloader) if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) else 0.0\n    \n    return avg_loss, avg_present_loss, avg_location_loss\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.277063Z","iopub.execute_input":"2025-09-16T12:49:59.277288Z","iopub.status.idle":"2025-09-16T12:49:59.293169Z","shell.execute_reply.started":"2025-09-16T12:49:59.277274Z","shell.execute_reply":"2025-09-16T12:49:59.292343Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def valid_fn(model, dataloader, criterion_weighted, device):\n    model.eval()\n    total_weighted_loss = 0.0\n    total_unweighted_loss = 0.0\n    total_present_loss = 0.0\n    total_location_loss = 0.0\n    all_preds = []\n    all_labels = []\n\n    # unweighted BCE（pos_weight=None）を別で用意\n    unweighted_bce = nn.BCEWithLogitsLoss(pos_weight=None).to(device)\n    \n    # 分離損失またはdual-head用のBCE\n    if Config.use_separate_loss or Config.use_dual_head or Config.use_two_models:\n        present_bce = nn.BCEWithLogitsLoss().to(device)\n        location_bce = nn.BCEWithLogitsLoss().to(device)\n\n    with torch.no_grad():\n        for images, labels in dataloader:\n            images = images.to(device)\n            labels = labels.to(device)\n\n            with autocast(enabled=Config.mixed_precision):\n                outputs = model(images)\n                \n                if Config.use_two_models:\n                    # 2モデルの場合、criterion_weightedは使えないので再計算\n                    present_loss = present_bce(outputs[:, -1:], labels[:, -1:])\n                    location_loss = location_bce(outputs[:, :13], labels[:, :13])\n                    weighted_loss = (Config.separate_present_weight * present_loss + \n                                     Config.separate_location_weight * location_loss)\n                    unweighted_loss = weighted_loss # unweighted版は別途計算が必要だが、ここでは簡易的に同じ値\n                else:\n                    weighted_loss = criterion_weighted(outputs, labels)\n                    unweighted_loss = unweighted_bce(outputs, labels)\n                \n                # 分離損失またはdual-headの計算\n                if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) and not Config.pred_only_ap:\n                    # Present (最後の列)\n                    present_loss = present_bce(outputs[:, -1:], labels[:, -1:])\n                    # Location (最初の13列)\n                    location_loss = location_bce(outputs[:, :13], labels[:, :13])\n                    total_present_loss += present_loss.item()\n                    total_location_loss += location_loss.item()\n\n            total_weighted_loss += weighted_loss.item()\n            total_unweighted_loss += unweighted_loss.item()\n\n            all_preds.append(torch.sigmoid(outputs).float().cpu().numpy())\n            all_labels.append(labels.float().cpu().numpy())\n\n    avg_weighted = total_weighted_loss / len(dataloader)\n    avg_unweighted = total_unweighted_loss / len(dataloader)\n    avg_present_loss = total_present_loss / len(dataloader) if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) else 0.0\n    avg_location_loss = total_location_loss / len(dataloader) if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) else 0.0\n\n    # 競技メトリクス\n    cv_score = ap_auc = avg_ind_auc = 0.5\n    individual_aucs = [0.5] * 14\n    \n    if len(all_preds) > 0:\n        y_pred = np.concatenate(all_preds, axis=0)\n        y_true = np.concatenate(all_labels, axis=0)\n        \n        if Config.pred_only_ap:\n            # Aneurysm Presentのみの場合、そのAUCをcv_scoreとする\n            try:\n                if len(np.unique(y_true)) > 1:\n                    cv_score = ap_auc = roc_auc_score(y_true, y_pred)\n                else:\n                    cv_score = ap_auc = 0.5\n            except:\n                cv_score = ap_auc = 0.5\n            avg_ind_auc = 0.5\n            individual_aucs = [0.5] * 13  # 13個のlocationは評価しない\n        else:\n            cv_score, ap_auc, avg_ind_auc, individual_aucs = calculate_competition_metric(y_true, y_pred)\n        \n        if Config.DEBUG:\n            pos_rate = (y_true.mean(axis=0))\n            pred_std = (y_pred.std(axis=0))\n            print(\"valid pos_rate:\", np.round(pos_rate, 4))\n            print(\"valid pred_std:\", np.round(pred_std, 4))\n\n    return (avg_weighted, avg_unweighted, cv_score, ap_auc, avg_ind_auc, \n            individual_aucs, avg_present_loss, avg_location_loss)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.293843Z","iopub.execute_input":"2025-09-16T12:49:59.29406Z","iopub.status.idle":"2025-09-16T12:49:59.309082Z","shell.execute_reply.started":"2025-09-16T12:49:59.294045Z","shell.execute_reply":"2025-09-16T12:49:59.308364Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef calculate_competition_metric(y_true, y_pred):\n    \"\"\"Calculate competition metric: weighted multilabel AUC ROC\"\"\"\n    individual_aucs = []\n    \n    # Calculate AUC for first 13 classes\n    for i in range(13):\n        try:\n            if len(np.unique(y_true[:, i])) > 1:\n                auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n            else:\n                auc = 0.5\n            individual_aucs.append(auc)\n        except:\n            print(\"failed to calcualte roc_auc\")\n            individual_aucs.append(0.5)\n    \n    # Calculate AUC for Aneurysm Present\n    try:\n        if len(np.unique(y_true[:, 13])) > 1:\n            aneurysm_present_auc = roc_auc_score(y_true[:, 13], y_pred[:, 13])\n        else:\n            aneurysm_present_auc = 0.5\n    except:\n        print(\"failed to calcualte roc_auc\")\n        aneurysm_present_auc = 0.5\n    \n    # Final score\n    avg_individual = np.mean(individual_aucs)\n    final_score = (aneurysm_present_auc + avg_individual) / 2\n    \n    return final_score, aneurysm_present_auc, avg_individual, individual_aucs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.309929Z","iopub.execute_input":"2025-09-16T12:49:59.310171Z","iopub.status.idle":"2025-09-16T12:49:59.33182Z","shell.execute_reply.started":"2025-09-16T12:49:59.310145Z","shell.execute_reply":"2025-09-16T12:49:59.331152Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_training(fold_index: Optional[int] = None, group_name: Optional[str] = None):\n    # --- データ読み込み ---\n    df = pd.read_csv(Config.train_csv_path)\n    if Config.DEBUG:\n        print(\"Train df loaded. Keys = \", df.columns)\n\n    # series_path 列\n    df['series_path'] = df['SeriesInstanceUID'].apply(lambda sid: os.path.join(Config.train_image_dir, str(sid)))\n\n    # --- StratifiedKFold ---\n    print(f\"Creating StratifiedKFold split based on '{Config.target_col}'...\")\n    skf = StratifiedKFold(n_splits=Config.num_folds, shuffle=True, random_state=Config.seed)\n    df['fold'] = -1\n    for fold_idx, (_, valid_indices) in enumerate(skf.split(df, df[Config.target_col])):\n        df.loc[valid_indices, 'fold'] = fold_idx\n\n    # --- 対象フォールド ---\n    target_fold_number = fold_index if fold_index is not None else Config.target_fold\n    train_df = df[df['fold'] != target_fold_number].reset_index(drop=True)\n    valid_df = df[df['fold'] == target_fold_number].reset_index(drop=True)\n\n    # --- W&B ---\n    if Config.use_wandb:\n        grp = group_name if group_name is not None else Config.experiment_name\n        run = wandb.init(\n            config=config_dict,\n            entity=\"tokkiwa-rsna25\",\n            project=\"RSNA25\",\n            group=grp,\n            name=f\"fold{target_fold_number+1}\",\n            reinit=True,\n        )\n\n    # --- データローダー ---\n    train_dataset = BrainAneurysmDataset(train_df, transforms=get_transforms('train'))\n    valid_dataset = BrainAneurysmDataset(valid_df, transforms=get_transforms('valid'))\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=Config.batch_size,\n        shuffle=True,\n        num_workers=Config.num_workers,\n        pin_memory=Config.pin_memory\n    )\n    valid_loader = DataLoader(\n        valid_dataset,\n        batch_size=Config.batch_size,\n        shuffle=Config.valid_shuffle,\n        num_workers=Config.num_workers,\n        pin_memory=Config.pin_memory\n    )\n\n    # --- pos_weight を推定（学習データのクラス不均衡対応）---\n    if Config.use_pos_weight:\n        labels_np = train_dataset.labels\n        pos = labels_np.sum(axis=0)\n        neg = (labels_np.shape[0] - pos)\n        pw = np.clip((neg + 1e-6) / (pos + 1e-6), Config.pos_weight_clip_min, Config.pos_weight_clip_max)\n        pos_weight = torch.tensor(pw, dtype=torch.float32, device=device)\n        if Config.DEBUG:\n            if Config.pred_only_ap:\n                print(\"pos_weight (Aneurysm Present only):\", np.round(pw, 2))\n            else:\n                print(\"pos_weight (first 14):\", np.round(pw[:14], 2))\n    else:\n        pos_weight = None\n\n    if Config.use_two_models:\n        model = TwoModels(Config.model_name, Config.ap_model_name, pretrained=Config.pretrained,\n                          in_channels=Config.in_channels)\n        print(f\"Independent Dual Model mode. Model names are {Config.model_name} and {Config.ap_model_name}\")\n    elif Config.use_dual_head:\n        model = DualHeadModel(Config.model_name, pretrained=Config.pretrained, \n                             in_channels=Config.in_channels)\n        print(f\"Dual Head mode. Model name is {Config.model_name}\")\n    elif Config.use_seq_head:\n        model = SequentialHeadModel(Config.model_name, pretrained=Config.pretrained, \n                             in_channels=Config.in_channels)  \n        print(f\"Sequential Head mode. Model name is {Config.model_name}\")\n    else:\n        model = CustomModel(Config.model_name, pretrained=Config.pretrained, \n                           in_channels=Config.in_channels, \n                           num_classes=effective_num_classes(Config))\n        print(f\"Single Head mode. Model name is {Config.model_name}\")\n    model = nn.DataParallel(model)\n    model.to(device)\n\n    # --- 損失関数の選択（BCE、Focal、または分離損失） ---\n    if Config.pred_only_ap:\n        # Aneurysm Presentのみの学習\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        print(\"Using pred_only_ap mode: Aneurysm Present only\")\n    elif Config.use_two_models:\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) # validation用\n        print(\"Using two-model architecture: Separate models for Present and Location\")\n    elif Config.use_dual_head:\n        # Dual-head モードでは、criterion は validation 用のみ使用\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        print(\"Using dual-head architecture: Separate heads for Present and Location\")\n    elif Config.use_separate_loss:\n        # 分離損失モードでは、criterion は validation 用のみ使用\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        print(\"Using separate loss mode: Present loss + Location loss\")\n    elif Config.use_focal_loss:\n        # alpha を 0-1 に落とす（pos_weight の大きいクラスほど alpha を高く）\n        if pos_weight is not None:\n            # 正規化: alpha = pw / (pw + 1) in (0,1)\n            alpha = (pos_weight / (pos_weight + 1.0)).detach()\n        else:\n            alpha = None\n        criterion = FocalLoss(gamma=Config.focal_gamma, alpha=alpha, pos_weight=pos_weight)\n        print(\"Using Focal Loss\")\n    else:\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        print(\"Using standard BCE with logits loss\")\n\n    if Config.optimizer_type == \"adam\":\n        optimizer = optim.Adam(model.parameters(), lr=Config.learning_rate, weight_decay=Config.optimizer_weight_decay)\n    elif Config.optimizer_type == \"adamw\":\n        optimizer = optim.AdamW(model.parameters(), lr=Config.learning_rate, weight_decay=Config.optimizer_weight_decay)\n    elif Config.optimizer_type == \"radam\":\n        optimizer = optim.AdamW(model.parameters(), lr=Config.learning_rate, weight_decay=Config.optimizer_weight_decay)\n    else:\n        raise ValueError(f\"Unsupported Optimizer {Config.optimizer_type}\")\n    # 2つのモデルの場合は、それぞれに独立したオプティマイザーとスケジューラーを作成\n    if Config.use_two_models:\n        if Config.optimizer_type == \"adam\":\n            optimizer_location = optim.Adam(\n                model.module.model_location.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n            optimizer_presence = optim.Adam(\n                model.module.model_presence.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n        elif Config.optimizer_type == \"adamw\":\n            optimizer_location = optim.AdamW(\n                model.module.model_location.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n            optimizer_presence = optim.AdamW(\n                model.module.model_presence.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n        elif Config.optimizer_type == \"radam\":\n            optimizer_location = optim.RAdam(\n                model.module.model_location.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n            optimizer_presence = optim.RAdam(\n                model.module.model_presence.parameters(),\n                lr=Config.learning_rate,\n                weight_decay=Config.optimizer_weight_decay\n            )\n        else:      \n            raise ValueError(f\"Unsupported Optimizer {Config.optimizer_type}\")\n        # Location model用のスケジューラー\n        if Config.scheduler_type == \"cosinelr\":\n            try:\n                scheduler_location = CosineLRScheduler(\n                    optimizer_location,\n                    t_initial=Config.epochs,\n                    lr_min=Config.cosine_min_lr,\n                    warmup_t=Config.cosine_warmup_t,\n                    warmup_lr_init=Config.cosine_warmup_lr_init,\n                    t_in_epochs=Config.cosine_t_in_epochs,\n                    t_mul=Config.cosine_cycle_mul,\n                    decay_rate=Config.cosine_cycle_decay,\n                )\n            except TypeError:\n                scheduler_location = CosineLRScheduler(\n                    optimizer_location,\n                    t_initial=Config.epochs,\n                    lr_min=Config.cosine_min_lr,\n                    warmup_t=Config.cosine_warmup_t,\n                    warmup_lr_init=Config.cosine_warmup_lr_init,\n                    t_in_epochs=Config.cosine_t_in_epochs,\n                    cycle_mul=Config.cosine_cycle_mul,\n                    cycle_decay=Config.cosine_cycle_decay,\n                )\n            scheduler_location.step(0)\n            \n            # Presence model用のスケジューラー\n            try:\n                scheduler_presence = CosineLRScheduler(\n                    optimizer_presence,\n                    t_initial=Config.epochs,\n                    lr_min=Config.cosine_min_lr,\n                    warmup_t=Config.cosine_warmup_t,\n                    warmup_lr_init=Config.cosine_warmup_lr_init,\n                    t_in_epochs=Config.cosine_t_in_epochs,\n                    t_mul=Config.cosine_cycle_mul,\n                    decay_rate=Config.cosine_cycle_decay,\n                )\n            except TypeError:\n                scheduler_presence = CosineLRScheduler(\n                    optimizer_presence,\n                    t_initial=Config.epochs,\n                    lr_min=Config.cosine_min_lr,\n                    warmup_t=Config.cosine_warmup_t,\n                    warmup_lr_init=Config.cosine_warmup_lr_init,\n                    t_in_epochs=Config.cosine_t_in_epochs,\n                    cycle_mul=Config.cosine_cycle_mul,\n                    cycle_decay=Config.cosine_cycle_decay,\n                )\n            scheduler_presence.step(0)\n        elif Config.scheduler_type == \"multisteplr\":\n                scheduler_presence = MultiStepLR(\n                    optimizer_presence,\n                    milestones = Config.scheduler_step,\n                    gamma = Config.scheduler_gamma,\n                )\n                scheduler_location = MultiStepLR(\n                    optimizer_location,\n                    milestones = Config.scheduler_step,\n                    gamma = Config.scheduler_gamma,\n                )\n        elif Config.scheduler_type == \"reducelr\":\n                scheduler_presence = ReduceLROnPlateau(\n                    optimizer_presence,\n                    patience = Config.scheduler_patience,\n                )\n                scheduler_location = ReduceLROnPlateau(\n                    optimizer_location,\n                    patience = Config.scheduler_patience,\n                )\n        else:\n            raise ValueError(f\"Unsupported Scheduler {Config.scheduler_type}\")\n        # 各モデル用のスケーラー\n        scaler_location = GradScaler(enabled=Config.mixed_precision)\n        scaler_presence = GradScaler(enabled=Config.mixed_precision)\n    else:\n        optimizer_location = optimizer_presence = None\n        scheduler_location = scheduler_presence = None\n        scaler_location = scaler_presence = None\n\n    # --- timm CosineLRScheduler に変更（引数差異に両対応） ---\n    if Config.scheduler_type == \"cosinelr\":\n        try:\n            scheduler = CosineLRScheduler(\n                optimizer,\n                t_initial=Config.epochs,\n                lr_min=Config.cosine_min_lr,\n                warmup_t=Config.cosine_warmup_t,\n                warmup_lr_init=Config.cosine_warmup_lr_init,\n                t_in_epochs=Config.cosine_t_in_epochs,\n                t_mul=Config.cosine_cycle_mul,\n                decay_rate=Config.cosine_cycle_decay,\n            )\n        except TypeError:\n            scheduler = CosineLRScheduler(\n                optimizer,\n                t_initial=Config.epochs,\n                lr_min=Config.cosine_min_lr,\n                warmup_t=Config.cosine_warmup_t,\n                warmup_lr_init=Config.cosine_warmup_lr_init,\n                t_in_epochs=Config.cosine_t_in_epochs,\n                cycle_mul=Config.cosine_cycle_mul,\n                cycle_decay=Config.cosine_cycle_decay,\n            )\n        # 初期化（ウォームアップ開始LRを反映）\n        scheduler.step(0)\n    elif Config.scheduler_type == \"multisteplr\":\n        scheduler = MultiStepLR(\n            optimizer,\n            milestones = Config.scheduler_step,\n            gamma = Config.scheduler_gamma,\n        )\n    elif Config.scheduler_type == \"reducelr\":\n        scheduler = ReduceLROnPlateau(\n            optimizer_presence,\n            patience = Config.scheduler_patience,\n        )\n    else:\n        raise ValueError(f\"Unsupported Scheduler {Config.scheduler_type}\")\n\n    mixup_fn = MultiLabelMixup(\n        alpha=Config.mixup_alpha,\n        p=Config.mixup_prob,\n    )\n    #timmとかのmixupは多ラベルに対応していないので、自作\n\n    scaler = GradScaler(enabled=Config.mixed_precision)\n    # --- 訓練 ---\n    best_weighted_loss = float('inf')\n    best_unweighted_loss = float('inf')\n    best_cv_score = float('-inf')\n    epochs_no_improve = 0\n    for epoch in range(Config.epochs):\n        print(f\"--- Epoch {epoch+1}/{Config.epochs} ---\")\n        train_loss, train_present_loss, train_location_loss = train_fn(\n            model, train_loader, optimizer, criterion, scaler, device,\n            optimizer_location=optimizer_location,\n            optimizer_presence=optimizer_presence,\n            scaler_location=scaler_location,\n            scaler_presence=scaler_presence,\n            mixup_fn = mixup_fn\n        )\n        (valid_weighted_loss, valid_unweighted_loss, cv_score, ap_auc, avg_ind_auc, \n         individual_aucs, valid_present_loss, valid_location_loss) = valid_fn(model, valid_loader, criterion, device)\n\n        # エポック単位のスケジューラ更新（t_in_epochs=True）\n        if Config.use_two_models:\n            if Config.scheduler_type == \"reducelr\":\n                scheduler_location.step(valid_location_loss)\n                scheduler_presence.step(valid_present_loss)   \n            else:\n                scheduler_location.step(epoch + 1)\n                scheduler_presence.step(epoch + 1)\n        else:\n            if Config.scheduler_type == \"reducelr\":\n                scheduler.step(valid_weighted_loss)\n            else:\n                scheduler.step(epoch + 1)\n\n        # ログ出力\n        log_str = f\"Train Loss: {train_loss:.4f}\"\n        if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) and not Config.pred_only_ap:\n            log_str += f\" (Present: {train_present_loss:.4f}, Location: {train_location_loss:.4f})\"\n        log_str += f\", Valid (weighted) BCE: {valid_weighted_loss:.4f}, Valid (unweighted) BCE: {valid_unweighted_loss:.4f}\"\n        if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) and not Config.pred_only_ap:\n            log_str += f\" (Present: {valid_present_loss:.4f}, Location: {valid_location_loss:.4f})\"\n        \n        if Config.pred_only_ap:\n            log_str += f\", AP AUC: {ap_auc:.4f}\"\n        else:\n            log_str += f\", CV: {cv_score:.4f} (AP: {ap_auc:.4f}, IND13: {avg_ind_auc:.4f})\"\n        print(log_str)\n\n        # CVのベストを更新\n        # if cv_score > best_cv_score:\n        #     best_cv_score = cv_score\n\n        # improved_weighted = valid_weighted_loss < best_weighted_loss - Config.early_stopping_delta\n        # improved_unweighted = valid_unweighted_loss < best_unweighted_loss - Config.early_stopping_delta\n        improved_cv = best_cv_score - Config.early_stopping_delta < cv_score\n        # latest の保存（毎エポック）\n        latest_path = EXPORT_DIR + f\"fold{target_fold_number}_latest.pth\"\n        torch.save({'model': model.module.state_dict(), 'epoch': epoch}, latest_path)\n\n        # best の保存（weighted 基準）\n        if improved_cv:\n            best_cv_score = cv_score\n            best_path = EXPORT_DIR + f\"fold{target_fold_number}_best.pth\"\n            torch.save({'model': model.module.state_dict(), 'epoch': epoch}, best_path)\n            print(f\"Model saved to {best_path} (Best CV)\")\n            epochs_no_improve = 0\n        else:\n            epochs_no_improve += 1\n\n        if epochs_no_improve >= Config.early_stopping_patience:\n            print(f\"Early stopping triggered at epoch {epoch+1}\")\n            break\n\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n        if Config.use_wandb:\n            log_dict = {\n                \"train_loss\": train_loss,\n                \"valid_weighted_bce\": valid_weighted_loss,\n                \"valid_unweighted_bce\": valid_unweighted_loss,\n                \"cv_score\": cv_score,\n                \"ap_auc\": ap_auc,\n                \"avg_ind_auc\": avg_ind_auc,\n                \"epochs_no_improve\": epochs_no_improve,\n            }\n            \n            # 学習率のログ（2つのモデルの場合は両方記録）\n            if Config.use_two_models:\n                log_dict[\"lr_location\"] = optimizer_location.param_groups[0]['lr']\n                log_dict[\"lr_presence\"] = optimizer_presence.param_groups[0]['lr']\n            else:\n                log_dict[\"lr\"] = optimizer.param_groups[0]['lr']\n            \n            # 分離損失またはdual-headのログ\n            if (Config.use_separate_loss or Config.use_dual_head or Config.use_two_models) and not Config.pred_only_ap:\n                log_dict.update({\n                    \"train_present_loss\": train_present_loss,\n                    \"train_location_loss\": train_location_loss,\n                    \"valid_present_loss\": valid_present_loss,\n                    \"valid_location_loss\": valid_location_loss,\n                })\n            \n            # 個別AUCのログ\n            if Config.log_individual_aucs and not Config.pred_only_ap:\n                label_names = [\n                    'auc_left_infraclinoid_ica', 'auc_right_infraclinoid_ica',\n                    'auc_left_supraclinoid_ica', 'auc_right_supraclinoid_ica',\n                    'auc_left_mca', 'auc_right_mca',\n                    'auc_anterior_communicating', 'auc_left_aca',\n                    'auc_right_aca', 'auc_left_pcom',\n                    'auc_right_pcom', 'auc_basilar_tip',\n                    'auc_other_posterior',# 'auc_aneurysm_present'\n                ]\n                print(individual_aucs)\n                for i, label_name in enumerate(label_names):\n                    log_dict[label_name] = individual_aucs[i]\n            \n            wandb.log(log_dict)\n    if Config.use_wandb:\n        wandb.finish()\n\n    return best_cv_score, cv_score\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.332687Z","iopub.execute_input":"2025-09-16T12:49:59.332958Z","iopub.status.idle":"2025-09-16T12:49:59.365581Z","shell.execute_reply.started":"2025-09-16T12:49:59.332937Z","shell.execute_reply":"2025-09-16T12:49:59.365045Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_all_folds():\n    # グループ名は experiment_name をそのまま使用\n    group = Config.experiment_name\n    print(f\"W&B group: {group}\")\n    fold_cv_best_scores = []\n    fold_cv_latest_scores = []\n    for fdx in range(Config.num_folds):\n        print(\"=\" * 40)\n        print(f\"Start training fold {fdx+1}/{Config.num_folds}\")\n        best_score, latest_score = run_training(fold_index=fdx, group_name=group)\n        fold_cv_best_scores.append(float(best_score))\n        fold_cv_latest_scores.append(float(latest_score))\n        # 明示的にキャッシュ解放\n        gc.collect()\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\n    if len(fold_cv_best_scores) > 0:\n        avg_best_cv = float(np.mean(fold_cv_best_scores))\n        std_best_cv = float(np.std(fold_cv_best_scores))\n        print(f\"全フォルド平均CV (best): {avg_best_cv:.6f} ± {std_best_cv:.6f}\")\n    if len(fold_cv_latest_scores) > 0:\n        avg_latest_cv = float(np.mean(fold_cv_latest_scores))\n        std_latest_cv = float(np.std(fold_cv_latest_scores))\n        print(f\"全フォルド平均CV (latest): {avg_latest_cv:.6f} ± {std_latest_cv:.6f}\")        \n\n\n# --- メイン実行 ---\nif __name__ == \"__main__\":\n    # 全フォルド or 単一フォルド\n    run_all = getattr(Config, 'run_all_folds', True)\n    if Config.generate_cache_only:\n        print(\"Cache-only mode enabled. Generating caches without training...\")\n        generate_cache()\n    else:\n        if run_all:\n            run_all_folds()\n        else:\n            run_training()  # 単一フォルド（Config.target_fold）","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-16T12:49:59.366261Z","iopub.execute_input":"2025-09-16T12:49:59.366426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}