{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# RSNA Intracranial Aneurysm Detection — 3D ResNet (PyTorch + MONAI)\n\nThis notebook trains a **3D ResNet-18** multi-label classifier (13 locations + aneurysm present) on **full 3D volumes**.\n\n**Dataset:** RSNA Intracranial Aneurysm Detection (Kaggle)\n\n> If you see `no accelerator found`, enable GPU in **Kaggle → Settings → Accelerator → GPU**.\n","metadata":{}},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\", category=UserWarning, module=\"pydicom\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:49:40.547512Z","iopub.execute_input":"2026-01-19T04:49:40.547865Z","iopub.status.idle":"2026-01-19T04:49:40.551875Z","shell.execute_reply.started":"2026-01-19T04:49:40.547828Z","shell.execute_reply":"2026-01-19T04:49:40.551243Z"}},"outputs":[],"execution_count":2},{"cell_type":"code","source":"!pip -q install monai pydicom\n\nimport os, glob, random\nimport numpy as np\nimport pandas as pd\nimport pydicom\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\nfrom monai.transforms import Compose, Resize, ScaleIntensity\nfrom monai.networks.nets import resnet\n\nSEED = 42\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything()\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"DEVICE:\", DEVICE)\nprint(\"torch:\", torch.__version__)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:49:41.40301Z","iopub.execute_input":"2026-01-19T04:49:41.403333Z","iopub.status.idle":"2026-01-19T04:49:44.709719Z","shell.execute_reply.started":"2026-01-19T04:49:41.403306Z","shell.execute_reply":"2026-01-19T04:49:44.708818Z"}},"outputs":[{"name":"stdout","text":"DEVICE: cuda\ntorch: 2.8.0+cu126\n","output_type":"stream"}],"execution_count":3},{"cell_type":"code","source":"train_csv = '/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv'\ntest_csv  = '/kaggle/input/rsna-intracranial-aneurysm-detection/kaggle_evaluation/test.csv'\ntrain_localizers_csv = '/kaggle/input/rsna-intracranial-aneurysm-detection/train_localizers.csv'\nseg_root = '/kaggle/input/rsna-intracranial-aneurysm-detection/segmentations'\n\nSERIES_ROOT_CANDIDATES = [\n    '/kaggle/input/rsna-intracranial-aneurysm-detection/series',\n    '/kaggle/input/rsna-intracranial-aneurysm-detection/kaggle_evaluation/series'\n]\n\ndef pick_series_root(candidates):\n    for p in candidates:\n        if os.path.exists(p):\n            folders = [d for d in os.listdir(p) if os.path.isdir(os.path.join(p, d))]\n            if len(folders) > 0:\n                return p\n    return None\n\nseries_root = pick_series_root(SERIES_ROOT_CANDIDATES)\nprint(\"Series root selected:\", series_root)\nprint(\"Sample folders:\", os.listdir(series_root)[:3])\nprint(\"train_csv exists:\", os.path.exists(train_csv))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:49:49.080719Z","iopub.execute_input":"2026-01-19T04:49:49.081427Z","iopub.status.idle":"2026-01-19T04:49:57.01473Z","shell.execute_reply.started":"2026-01-19T04:49:49.08139Z","shell.execute_reply":"2026-01-19T04:49:57.014107Z"}},"outputs":[{"name":"stdout","text":"Series root selected: /kaggle/input/rsna-intracranial-aneurysm-detection/series\nSample folders: ['1.2.826.0.1.3680043.8.498.96155132589464464482407972219172224468', '1.2.826.0.1.3680043.8.498.41711215539877472420239257090976077218', '1.2.826.0.1.3680043.8.498.86837346700151751310565181236786678013']\ntrain_csv exists: True\n","output_type":"stream"}],"execution_count":4},{"cell_type":"code","source":"df = pd.read_csv(train_csv)\n\nSERIES_COL = \"SeriesInstanceUID\"\nPRES_COL   = \"Aneurysm Present\"\n\nTARGET_COLS = [\n 'Left Infraclinoid Internal Carotid Artery',\n 'Right Infraclinoid Internal Carotid Artery',\n 'Left Supraclinoid Internal Carotid Artery',\n 'Right Supraclinoid Internal Carotid Artery',\n 'Left Middle Cerebral Artery',\n 'Right Middle Cerebral Artery',\n 'Anterior Communicating Artery',\n 'Left Anterior Cerebral Artery',\n 'Right Anterior Cerebral Artery',\n 'Left Posterior Communicating Artery',\n 'Right Posterior Communicating Artery',\n 'Basilar Tip',\n 'Other Posterior Circulation',\n 'Aneurysm Present'\n]\n\nprint(\"Rows:\", len(df))\nprint(\"Columns count:\", len(df.columns))\nprint(\"Presence positives:\", int(df[PRES_COL].sum()))\nprint(df[TARGET_COLS].sum().sort_values(ascending=False).head(8))\nprint(\"Example UID:\", df[SERIES_COL].iloc[0])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:50:01.770038Z","iopub.execute_input":"2026-01-19T04:50:01.770369Z","iopub.status.idle":"2026-01-19T04:50:01.817104Z","shell.execute_reply.started":"2026-01-19T04:50:01.770343Z","shell.execute_reply":"2026-01-19T04:50:01.816441Z"}},"outputs":[{"name":"stdout","text":"Rows: 4348\nColumns count: 18\nPresence positives: 1863\nAneurysm Present                              1863\nAnterior Communicating Artery                  363\nLeft Supraclinoid Internal Carotid Artery      330\nRight Middle Cerebral Artery                   294\nRight Supraclinoid Internal Carotid Artery     278\nLeft Middle Cerebral Artery                    219\nOther Posterior Circulation                    113\nBasilar Tip                                    110\ndtype: int64\nExample UID: 1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\n","output_type":"stream"}],"execution_count":5},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\nfold = 0\n\ntrain_idx, val_idx = list(skf.split(df, df[PRES_COL]))[fold]\ntrain_df = df.iloc[train_idx].reset_index(drop=True)\nval_df   = df.iloc[val_idx].reset_index(drop=True)\n\nprint(\"train:\", train_df.shape, \"val:\", val_df.shape)\nprint(\"train presence rate:\", train_df[PRES_COL].mean())\nprint(\"val presence rate:\", val_df[PRES_COL].mean())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:50:06.459301Z","iopub.execute_input":"2026-01-19T04:50:06.460027Z","iopub.status.idle":"2026-01-19T04:50:06.472456Z","shell.execute_reply.started":"2026-01-19T04:50:06.459997Z","shell.execute_reply":"2026-01-19T04:50:06.47152Z"}},"outputs":[{"name":"stdout","text":"train: (3478, 18) val: (870, 18)\ntrain presence rate: 0.4284071305347901\nval presence rate: 0.42873563218390803\n","output_type":"stream"}],"execution_count":6},{"cell_type":"code","source":"def load_dicom_series(series_uid, series_root):\n    series_dir = os.path.join(series_root, series_uid)\n    files = glob.glob(os.path.join(series_dir, \"*.dcm\"))\n    if len(files) == 0:\n        raise FileNotFoundError(f\"No DICOMs found for {series_uid} in {series_dir}\")\n\n    dcms = [pydicom.dcmread(f) for f in files]\n\n    def sort_key(d):\n        if hasattr(d, \"ImagePositionPatient\"):\n            return float(d.ImagePositionPatient[2])\n        return float(getattr(d, \"InstanceNumber\", 0))\n\n    dcms = sorted(dcms, key=sort_key)\n\n    slices = []\n    for d in dcms:\n        img = d.pixel_array.astype(np.float32)\n        slope = float(getattr(d, \"RescaleSlope\", 1.0))\n        inter = float(getattr(d, \"RescaleIntercept\", 0.0))\n        img = img * slope + inter\n        slices.append(img)\n\n    vol = np.stack(slices, axis=0)  # (D,H,W)\n    return vol\n\n# Test one series\nuid0 = train_df[SERIES_COL].iloc[0]\nvol0 = load_dicom_series(uid0, series_root)\nprint(\"UID:\", uid0)\nprint(\"Volume shape (D,H,W):\", vol0.shape, \"dtype:\", vol0.dtype)\nprint(\"min/max:\", float(vol0.min()), float(vol0.max()))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:50:10.532698Z","iopub.execute_input":"2026-01-19T04:50:10.533236Z","iopub.status.idle":"2026-01-19T04:50:14.82745Z","shell.execute_reply.started":"2026-01-19T04:50:10.533207Z","shell.execute_reply":"2026-01-19T04:50:14.826677Z"}},"outputs":[{"name":"stdout","text":"UID: 1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\nVolume shape (D,H,W): (188, 512, 512) dtype: float32\nmin/max: 0.0 682.0\n","output_type":"stream"}],"execution_count":7},{"cell_type":"code","source":"INPUT_SIZE = (96, 96, 96)\n\nvol_tfm = Compose([\n    Resize(spatial_size=INPUT_SIZE, mode=\"trilinear\", align_corners=False),\n    ScaleIntensity()\n])\n\ndef preprocess_volume(vol):\n    # vol: numpy (D,H,W)\n    vol = vol.astype(np.float32)\n    vol = (vol - vol.mean()) / (vol.std() + 1e-6)\n\n    # اجعله (C,D,H,W) قبل Resize\n    t = torch.from_numpy(vol).unsqueeze(0)  # (1,D,H,W)\n\n    # الآن Resize يشتغل على spatial (D,H,W) بشكل صحيح\n    t = vol_tfm(t)  # (1,128,128,128)\n\n    return t\n\nx0 = preprocess_volume(vol0)\nprint(\"After preprocess shape:\", x0.shape)\nprint(\"min/max:\", float(x0.min()), float(x0.max()), \"dtype:\", x0.dtype)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:50:19.002375Z","iopub.execute_input":"2026-01-19T04:50:19.002659Z","iopub.status.idle":"2026-01-19T04:50:19.312937Z","shell.execute_reply.started":"2026-01-19T04:50:19.002636Z","shell.execute_reply":"2026-01-19T04:50:19.31234Z"}},"outputs":[{"name":"stdout","text":"After preprocess shape: torch.Size([1, 96, 96, 96])\nmin/max: 0.0 1.0 dtype: torch.float32\n","output_type":"stream"}],"execution_count":8},{"cell_type":"code","source":"class RSNADataset(Dataset):\n    def __init__(self, df, series_root, target_cols, max_tries=5):\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.target_cols = target_cols\n        self.max_tries = max_tries\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        for _ in range(self.max_tries):\n            row = self.df.iloc[idx]\n            uid = row[SERIES_COL]\n            try:\n                vol = load_dicom_series(uid, self.series_root)\n                # لو load_dicom_series بيرجع (vol, sop_uids) عندك: خليه vol, _ = load_dicom_series(...)\n                if isinstance(vol, tuple):\n                    vol = vol[0]\n\n                x = preprocess_volume(vol)  # (1,96,96,96)\n                y = torch.tensor(row[self.target_cols].values.astype(np.float32))\n                return x, y\n            except Exception:\n                idx = random.randint(0, len(self.df) - 1)\n        raise RuntimeError(\"Failed to load a valid DICOM series after retries\")\n\nBATCH_SIZE = 1\nNUM_WORKERS = 2\nPIN_MEMORY = True\n\ntrain_loader = DataLoader(\n    RSNADataset(train_df, series_root, TARGET_COLS),\n    batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=PIN_MEMORY\n)\nval_loader = DataLoader(\n    RSNADataset(val_df, series_root, TARGET_COLS),\n    batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=PIN_MEMORY\n)\n\nx, y = next(iter(train_loader))\nprint(\"x shape:\", x.shape)   # لازم: [1,1,96,96,96]\nprint(\"y shape:\", y.shape)   # لازم: [1,14]\nprint(\"x min/max:\", float(x.min()), float(x.max()))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:51:41.021941Z","iopub.execute_input":"2026-01-19T04:51:41.022689Z","iopub.status.idle":"2026-01-19T04:51:51.282991Z","shell.execute_reply.started":"2026-01-19T04:51:41.022658Z","shell.execute_reply":"2026-01-19T04:51:51.281635Z"}},"outputs":[{"name":"stdout","text":"x shape: torch.Size([1, 1, 96, 96, 96])\ny shape: torch.Size([1, 14])\nx min/max: 0.0 1.0\n","output_type":"stream"}],"execution_count":9},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef compute_auc_per_label(y_true, y_prob):\n    aucs = []\n    for i in range(y_true.shape[1]):\n        if len(np.unique(y_true[:, i])) < 2:\n            aucs.append(np.nan)\n        else:\n            aucs.append(roc_auc_score(y_true[:, i], y_prob[:, i]))\n    return np.array(aucs)\n\ndef presence_accuracy(y_true, y_prob, thr=0.5):\n    # presence هو آخر عمود\n    yt = y_true[:, -1].astype(np.int32)\n    yp = (y_prob[:, -1] >= thr).astype(np.int32)\n    return (yp == yt).mean()\n\n@torch.no_grad()\ndef evaluate_with_metrics(model, loader, thr=0.5):\n    model.eval()\n    probs_all, y_all = [], []\n\n    for x, y in loader:\n        x = x.to(DEVICE)\n        logits = model(x)\n        prob = torch.sigmoid(logits).cpu().numpy()\n        probs_all.append(prob)\n        y_all.append(y.numpy())\n\n    probs_all = np.concatenate(probs_all, axis=0)\n    y_all = np.concatenate(y_all, axis=0)\n\n    aucs = compute_auc_per_label(y_all, probs_all)\n    presence_auc = aucs[-1]\n    loc_mean_auc = np.nanmean(aucs[:-1])\n    pres_acc = presence_accuracy(y_all, probs_all, thr=thr)\n    return presence_auc, loc_mean_auc, pres_acc\n\ndef plot_history(hist):\n    # hist keys: train_loss, val_presence_auc, val_loc_auc, val_presence_acc\n    plt.figure()\n    plt.plot(hist[\"train_loss\"], label=\"train_loss\")\n    plt.legend()\n    plt.show()\n\n    plt.figure()\n    plt.plot(hist[\"val_presence_auc\"], label=\"val_presence_auc\")\n    plt.plot(hist[\"val_loc_auc\"], label=\"val_loc_auc\")\n    plt.plot(hist[\"val_presence_acc\"], label=\"val_presence_acc\")\n    plt.legend()\n    plt.show()\n\nprint(\"OK metrics + plotting ready\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:52:37.438353Z","iopub.execute_input":"2026-01-19T04:52:37.438709Z","iopub.status.idle":"2026-01-19T04:52:37.449985Z","shell.execute_reply.started":"2026-01-19T04:52:37.438676Z","shell.execute_reply":"2026-01-19T04:52:37.449394Z"}},"outputs":[{"name":"stdout","text":"OK metrics + plotting ready\n","output_type":"stream"}],"execution_count":10},{"cell_type":"code","source":"model = resnet.ResNet(\n    block=\"basic\",\n    layers=[2,2,2,2],  # ResNet18\n    block_inplanes=[64,128,256,512],\n    spatial_dims=3,\n    n_input_channels=1,\n    num_classes=len(TARGET_COLS),\n).to(DEVICE)\n\n# pos_weight لعلاج imbalance\neps = 1e-6\npos = train_df[TARGET_COLS].sum(axis=0).values + eps\nneg = len(train_df) - pos\npos_weight = torch.tensor(neg / pos, dtype=torch.float32).to(DEVICE)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4)\n\nscaler = torch.amp.GradScaler(\"cuda\", enabled=True)\n\nprint(\"Model outputs:\", len(TARGET_COLS))\nprint(\"pos_weight first 5:\", pos_weight[:5].detach().cpu().numpy())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:53:18.387491Z","iopub.execute_input":"2026-01-19T04:53:18.388154Z","iopub.status.idle":"2026-01-19T04:53:18.878819Z","shell.execute_reply.started":"2026-01-19T04:53:18.388125Z","shell.execute_reply":"2026-01-19T04:53:18.877705Z"}},"outputs":[{"name":"stdout","text":"Model outputs: 14\npos_weight first 5: [53.34375  43.589745 12.533073 15.483412 18.21547 ]\n","output_type":"stream"}],"execution_count":11},{"cell_type":"code","source":"def compute_auc_per_label(y_true, y_prob):\n    aucs = []\n    for i in range(y_true.shape[1]):\n        if len(np.unique(y_true[:, i])) < 2:\n            aucs.append(np.nan)\n        else:\n            aucs.append(roc_auc_score(y_true[:, i], y_prob[:, i]))\n    return np.array(aucs)\n\n@torch.no_grad()\ndef evaluate(model, loader):\n    model.eval()\n    probs_all, y_all = [], []\n\n    for x, y in loader:\n        x = x.to(DEVICE)\n        logits = model(x)\n        prob = torch.sigmoid(logits).cpu().numpy()\n        probs_all.append(prob)\n        y_all.append(y.numpy())\n\n    probs_all = np.concatenate(probs_all, axis=0)\n    y_all = np.concatenate(y_all, axis=0)\n\n    aucs = compute_auc_per_label(y_all, probs_all)\n    presence_auc = aucs[-1]           # Aneurysm Present\n    loc_mean_auc = np.nanmean(aucs[:-1])  # 13 locations mean\n    return presence_auc, loc_mean_auc, aucs\n\nprint(\"OK loaded eval()\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:56:00.471965Z","iopub.execute_input":"2026-01-19T04:56:00.472788Z","iopub.status.idle":"2026-01-19T04:56:00.479772Z","shell.execute_reply.started":"2026-01-19T04:56:00.472755Z","shell.execute_reply":"2026-01-19T04:56:00.479105Z"}},"outputs":[{"name":"stdout","text":"OK loaded eval()\n","output_type":"stream"}],"execution_count":12},{"cell_type":"code","source":"import json\nfrom tqdm import tqdm\n\ndef train_one_epoch(model, loader):\n    model.train()\n    total_loss = 0.0\n\n    pbar = tqdm(loader, desc=\"train\", leave=False)\n    for x, y in pbar:\n        x = x.to(DEVICE)\n        y = y.to(DEVICE)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.amp.autocast(\"cuda\", enabled=True):\n            logits = model(x)\n            loss = criterion(logits, y)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        total_loss += loss.item() * x.size(0)\n        pbar.set_postfix(loss=float(loss.item()))\n\n    return total_loss / len(loader.dataset)\n\n# ===== Training with Early Stopping =====\nEPOCHS = 15\nPATIENCE = 3          # early stopping patience\nMIN_DELTA = 1e-4      # minimum improvement\n\nbest_metric = -1e9    # we track presence_auc\nbest_epoch = -1\nno_improve = 0\n\nhistory = {\n    \"train_loss\": [],\n    \"val_presence_auc\": [],\n    \"val_loc_auc\": [],\n    \"val_presence_acc\": []\n}\n\nfor epoch in range(1, EPOCHS + 1):\n    tr_loss = train_one_epoch(model, train_loader)\n\n    pres_auc, loc_auc, pres_acc = evaluate_with_metrics(model, val_loader, thr=0.5)\n\n    history[\"train_loss\"].append(float(tr_loss))\n    history[\"val_presence_auc\"].append(float(pres_auc))\n    history[\"val_loc_auc\"].append(float(loc_auc))\n    history[\"val_presence_acc\"].append(float(pres_acc))\n\n    print(f\"Epoch {epoch:02d} | train_loss={tr_loss:.4f} | val_presence_auc={pres_auc:.4f} | val_loc_auc={loc_auc:.4f} | val_presence_acc={pres_acc:.4f}\")\n\n    # ===== Save best model (by presence AUC) =====\n    improved = (pres_auc > best_metric + MIN_DELTA) if not np.isnan(pres_auc) else False\n\n    if improved:\n        best_metric = float(pres_auc)\n        best_epoch = epoch\n        no_improve = 0\n\n        torch.save(model.state_dict(), \"best_model.pth\")\n        meta = {\n            \"best_epoch\": best_epoch,\n            \"best_val_presence_auc\": best_metric,\n            \"val_loc_auc_at_best\": float(loc_auc),\n            \"val_presence_acc_at_best\": float(pres_acc),\n            \"input_size\": list(INPUT_SIZE),\n            \"model\": \"MONAI ResNet18 3D\",\n        }\n        with open(\"best_meta.json\", \"w\") as f:\n            json.dump(meta, f, indent=2)\n\n        print(\"✅ Saved BEST model:\", meta)\n    else:\n        no_improve += 1\n        print(f\"⏳ No improvement ({no_improve}/{PATIENCE})\")\n\n    # ===== Plot every epoch (اختياري) =====\n    plot_history(history)\n\n    # ===== Early stopping =====\n    if no_improve >= PATIENCE:\n        print(f\"🛑 Early stopping at epoch {epoch}. Best epoch was {best_epoch} with val_presence_auc={best_metric:.4f}\")\n        break\n\nprint(\"Training finished.\")\nprint(\"Best epoch:\", best_epoch, \"| Best val_presence_auc:\", best_metric)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-19T04:56:23.794969Z","iopub.execute_input":"2026-01-19T04:56:23.795652Z"}},"outputs":[{"name":"stderr","text":"train:   5%|▍         | 157/3478 [09:37<4:27:27,  4.83s/it, loss=0.654]","output_type":"stream"}],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}