{"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"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# baseline_3d_cnn_rsna.py\n# Full script: train simple 3D CNN on DICOM series and provide predict() for Kaggle RSNA gateway.\n# Copy -> paste vào notebook Kaggle, tắt Internet, Save & Run All.\n\nimport os\nimport glob\nimport math\nimport random\nfrom tqdm import tqdm\nimport numpy as np\nimport pandas as pd\nimport pydicom\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\n\n# -----------------------\n# Config\n# -----------------------\nROOT = \"/kaggle/input/rsna-intracranial-aneurysm-detection\"  # competition input\nTRAIN_CSV = os.path.join(ROOT, \"train.csv\")\nTRAIN_SERIES_DIR = \"/kaggle/input/rsna-intracranial-aneurysm-detection/train\"  # if exists (adjust)\nWORK_DIR = \"/kaggle/working\"\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nBATCH_SIZE = 8\nEPOCHS = 8\nLEARNING_RATE = 1e-4\nNUM_SLICES = 32        # depth for volume (adjustable)\nTARGET_HW = 128        # height/width to resize/crop to\nSEED = 42\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\n# -----------------------\n# Labels (as used before)\n# -----------------------\nLABEL_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present'\n]\n\n# -----------------------\n# Utilities: load dicom series -> 3D numpy volume\n# -----------------------\ndef load_series_to_volume(series_dir):\n    paths = sorted(glob.glob(os.path.join(series_dir, \"*.dcm\")))\n    if len(paths) == 0:\n        # sometimes files may have other extension; return zeros\n        return None\n    slices = []\n    for p in paths:\n        try:\n            d = pydicom.dcmread(p)\n            arr = d.pixel_array.astype(np.float32)\n            # convert via rescale if present\n            if hasattr(d, \"RescaleIntercept\") and hasattr(d, \"RescaleSlope\"):\n                arr = arr * float(d.RescaleSlope) + float(d.RescaleIntercept)\n            slices.append((int(getattr(d, \"InstanceNumber\", 0)), arr))\n        except Exception:\n            continue\n    if len(slices) == 0:\n        return None\n    slices = sorted(slices, key=lambda x: x[0])\n    volume = np.stack([s[1] for s in slices], axis=0)  # (D, H, W)\n    return volume\n\ndef center_crop_or_pad(img, target_h, target_w):\n    if img.ndim == 3:  # có channel\n        h, w = img.shape[-2:]\n    else:  # grayscale\n        h, w = img.shape\n    # pad if smaller\n    pad_h = max(0, target_h - h)\n    pad_w = max(0, target_w - w)\n    if pad_h > 0 or pad_w > 0:\n        top = pad_h//2\n        bottom = pad_h - top\n        left = pad_w//2\n        right = pad_w - left\n        img = np.pad(img, ((top,bottom),(left,right)), mode='constant', constant_values=0)\n        h, w = img.shape\n    # crop center if bigger\n    start_h = max(0, (h - target_h)//2)\n    start_w = max(0, (w - target_w)//2)\n    return img[start_h:start_h+target_h, start_w:start_w+target_w]\n\ndef resample_slices(volume, target_num):\n    D, H, W = volume.shape\n    if D == target_num:\n        return volume\n    # simple linear interpolation along depth\n    zs = np.linspace(0, D-1, target_num).astype(np.float32)\n    out = []\n    for z in zs:\n        z0 = int(np.floor(z))\n        z1 = min(D-1, z0+1)\n        if z0 == z1:\n            out.append(volume[z0])\n        else:\n            w1 = z - z0\n            w0 = 1 - w1\n            out.append(volume[z0]*w0 + volume[z1]*w1)\n    return np.stack(out, axis=0)\n\ndef normalize_volume(vol):\n    # clip extreme values then z-score per volume\n    v = vol.astype(np.float32)\n    v = np.clip(v, np.percentile(v,1), np.percentile(v,99))\n    mean = v.mean()\n    std = v.std() if v.std()>0 else 1.0\n    v = (v - mean) / std\n    return v\n\n# -----------------------\n# Dataset\n# -----------------------\nclass RSNADataset(Dataset):\n    def __init__(self, df, series_root, num_slices=NUM_SLICES, target_hw=TARGET_HW, mode='train'):\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.num_slices = num_slices\n        self.target_hw = target_hw\n        self.mode = mode\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        uid = self.df.loc[idx, \"SeriesInstanceUID\"]\n        series_dir = os.path.join(self.series_root, str(uid))\n        vol = load_series_to_volume(series_dir)\n        if vol is None:\n            # fallback zeros\n            vol = np.zeros((self.num_slices, self.target_hw, self.target_hw), dtype=np.float32)\n        else:\n            # resize each slice to target_hw x target_hw via center crop/pad\n            slices = []\n            for s in vol:\n                s2 = center_crop_or_pad(s, self.target_hw, self.target_hw)\n                slices.append(s2)\n            vol = np.stack(slices, axis=0)  # (D,H,W)\n            vol = resample_slices(vol, self.num_slices)\n        vol = normalize_volume(vol)\n        # channel-first for 3D conv: (C=1, D, H, W)\n        x = torch.from_numpy(vol).unsqueeze(0).float()\n        if self.mode == 'train' and \"Aneurysm Present\" in self.df.columns:\n            labels = self.df.loc[idx, LABEL_COLS].values.astype(np.float32)\n            y = torch.from_numpy(labels)\n            return x, y\n        else:\n            return x, str(uid)\n\n# -----------------------\n# Simple 3D CNN model\n# -----------------------\nclass Simple3DNet(nn.Module):\n    def __init__(self, in_ch=1, num_outputs=14):\n        super().__init__()\n        def conv_block(in_ch, out_ch, ks=3, stride=1, pad=1):\n            return nn.Sequential(\n                nn.Conv3d(in_ch, out_ch, kernel_size=ks, stride=stride, padding=pad),\n                nn.BatchNorm3d(out_ch),\n                nn.ReLU(inplace=True),\n                nn.MaxPool3d((1,2,2))  # pool spatial dims only\n            )\n        self.enc1 = conv_block(in_ch, 16)\n        self.enc2 = conv_block(16, 32)\n        self.enc3 = conv_block(32, 64)\n        self.enc4 = conv_block(64, 128)\n        # global pool\n        self.global_pool = nn.AdaptiveAvgPool3d((1,1,1))\n        self.fc = nn.Linear(128, num_outputs)\n\n    def forward(self, x):\n        # x: (B,1,D,H,W)\n        x = self.enc1(x)\n        x = self.enc2(x)\n        x = self.enc3(x)\n        x = self.enc4(x)\n        x = self.global_pool(x)  # (B, C,1,1,1)\n        x = x.view(x.size(0), -1)\n        x = self.fc(x)\n        return x  # logits for BCEWithLogitsLoss\n\n# -----------------------\n# Prepare train/val\n# -----------------------\ntrain_df = pd.read_csv(TRAIN_CSV)\n# if \"Aneurysm Present\" not present but 'any' exist, adapt\nif \"Aneurysm Present\" not in train_df.columns and \"any\" in train_df.columns:\n    train_df[\"Aneurysm Present\"] = train_df[\"any\"]\n# Ensure label cols exist in correct order; if not present create zeros\nfor c in LABEL_COLS:\n    if c not in train_df.columns:\n        train_df[c] = 0\n\n# split by SeriesInstanceUID\ntrain_uids, val_uids = train_test_split(train_df[\"SeriesInstanceUID\"].unique(), test_size=0.15, random_state=SEED)\ntrain_df_sub = train_df[train_df[\"SeriesInstanceUID\"].isin(train_uids)].reset_index(drop=True)\nval_df_sub = train_df[train_df[\"SeriesInstanceUID\"].isin(val_uids)].reset_index(drop=True)\n\n# Determine series root: Kaggle dataset may place DICOM series under a folder named 'train' inside input\n# Try to find folder with UID subfolders\npossible_roots = [\n    \"/kaggle/input/rsna-intracranial-aneurysm-detection/train\",\n    \"/kaggle/input/rsna-intracranial-aneurysm-detection/series\", \n    \"/kaggle/working/train\"\n]\nSERIES_ROOT = None\nfor r in possible_roots:\n    if os.path.exists(r):\n        # check first few uids\n        sample_uid = str(train_df_sub.loc[0, \"SeriesInstanceUID\"])\n        if os.path.exists(os.path.join(r, sample_uid)):\n            SERIES_ROOT = r\n            break\n# fallback to first possible existing root\nif SERIES_ROOT is None:\n    for r in possible_roots:\n        if os.path.exists(r):\n            SERIES_ROOT = r\n            break\nif SERIES_ROOT is None:\n    SERIES_ROOT = \"/kaggle/working\"  # best-effort\n\nprint(\"Using SERIES_ROOT =\", SERIES_ROOT)\n\ntrain_dataset = RSNADataset(train_df_sub, SERIES_ROOT, mode='train')\nval_dataset = RSNADataset(val_df_sub, SERIES_ROOT, mode='train')\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\n# -----------------------\n# Train\n# -----------------------\nmodel = Simple3DNet(in_ch=1, num_outputs=len(LABEL_COLS)).to(DEVICE)\noptimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)\ncriterion = nn.BCEWithLogitsLoss()\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=2, factor=0.5, verbose=True)\n\nbest_auc = 0.0\nfor epoch in range(1, EPOCHS+1):\n    model.train()\n    total_loss = 0.0\n    for xb, yb in tqdm(train_loader, desc=f\"Train E{epoch}\"):\n        xb = xb.to(DEVICE)\n        yb = yb.to(DEVICE)\n        logits = model(xb)\n        loss = criterion(logits, yb)\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item() * xb.size(0)\n    avg_loss = total_loss / len(train_loader.dataset)\n    # validation\n    model.eval()\n    all_targets = []\n    all_preds = []\n    with torch.no_grad():\n        for xb, yb in tqdm(val_loader, desc=f\"Val E{epoch}\"):\n            xb = xb.to(DEVICE)\n            yb = yb.to(DEVICE)\n            logits = model(xb)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            all_preds.append(probs)\n            all_targets.append(yb.cpu().numpy())\n    all_preds = np.vstack(all_preds)\n    all_targets = np.vstack(all_targets)\n    # compute AUC for \"Aneurysm Present\" (last column) and mean AUC across labels (if possible)\n    aucs = []\n    for i in range(all_targets.shape[1]):\n        try:\n            a = roc_auc_score(all_targets[:, i], all_preds[:, i])\n        except Exception:\n            a = float('nan')\n        aucs.append(a)\n    mean_auc = np.nanmean([a for a in aucs if not math.isnan(a)])\n    aneurysm_auc = aucs[-1]\n    print(f\"Epoch {epoch} loss={avg_loss:.4f} mean_auc={mean_auc:.4f} aneurysm_auc={aneurysm_auc:.4f}\")\n    scheduler.step(mean_auc if not math.isnan(mean_auc) else avg_loss)\n    # save best by aneurysm_auc\n    if not math.isnan(aneurysm_auc) and aneurysm_auc > best_auc:\n        best_auc = aneurysm_auc\n        torch.save(model.state_dict(), os.path.join(WORK_DIR, \"best_3dnet.pth\"))\n        print(\"Saved best model\", best_auc)\n\n# if no save happened, save last\nif not os.path.exists(os.path.join(WORK_DIR, \"best_3dnet.pth\")):\n    torch.save(model.state_dict(), os.path.join(WORK_DIR, \"last_3dnet.pth\"))\n\n# -----------------------\n# Inference predict() for Kaggle gateway\n# The RSNA gateway will place a folder /kaggle/shared/<SeriesInstanceUID> with .dcm files.\n# We'll read from that path, preprocess identical to dataset, run model, return DataFrame with LABEL_COLS (same order)\n# -----------------------\nmodel_file = os.path.join(WORK_DIR, \"best_3dnet.pth\")\nif os.path.exists(model_file):\n    model.load_state_dict(torch.load(model_file, map_location=DEVICE))\nmodel.eval()\n\ndef predict(series_instance_uids):\n    if isinstance(series_instance_uids, str):\n        uids = [series_instance_uids]\n    else:\n        uids = list(series_instance_uids)\n    results = []\n    for uid in uids:\n        series_dir = os.path.join(\"/kaggle/shared\", str(uid))\n        vol = load_series_to_volume(series_dir)\n        if vol is None:\n            vol = np.zeros((NUM_SLICES, TARGET_HW, TARGET_HW), dtype=np.float32)\n        else:\n            slices = [center_crop_or_pad(s, TARGET_HW, TARGET_HW) for s in vol]\n            vol = np.stack(slices, axis=0)\n            vol = resample_slices(vol, NUM_SLICES)\n        vol = normalize_volume(vol)\n        x = torch.from_numpy(vol).unsqueeze(0).unsqueeze(0).float().to(DEVICE)  # (1,1,D,H,W)\n        with torch.no_grad():\n            logits = model(x)\n            probs = torch.sigmoid(logits).cpu().numpy()[0]\n        probs = np.clip(probs, 0.001, 0.999)\n        row = {\"SeriesInstanceUID\": str(uid)}\n        for i, col in enumerate(LABEL_COLS):\n            row[col] = float(probs[i])\n        results.append(row)\n    return pd.DataFrame(results)\n\n# -----------------------\n# Hook into Kaggle inference server (same as your previous)\n# -----------------------\ntry:\n    import kaggle_evaluation\n    inference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        inference_server.serve()\n    else:\n        inference_server.run_local_gateway()\n        try:\n            display(pd.read_parquet('/kaggle/working/submission.parquet'))\n        except:\n            print(\"No submission file found\")\nexcept Exception as e:\n    print(\"No kaggle_evaluation available or running locally. You can still call predict(uids). Error:\", e)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-09-17T17:43:04.889133Z","iopub.execute_input":"2025-09-17T17:43:04.889949Z","iopub.status.idle":"2025-09-17T17:44:02.348763Z","shell.execute_reply.started":"2025-09-17T17:43:04.889918Z","shell.execute_reply":"2025-09-17T17:44:02.34691Z"}},"outputs":[],"execution_count":null}]}