{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":[{"sourceType":"competition","sourceId":13451,"datasetId":654585,"databundleVersionId":1188070}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"d3db0814","cell_type":"markdown","source":"# RSNA Intracranial Hemorrhage Detection — Single-Notebook Kaggle Pipeline\n\nThis notebook is fully self-contained for:\n- environment setup\n- reproducible config\n- loading RSNA competition data from `/kaggle/input`\n- preprocessing and 3-slice CT context dataset\n- fold training with mixed precision\n- validation + error analysis plots\n- test inference + submission generation\n- artifact export to `/kaggle/working`\n\n> Run top-to-bottom in Kaggle. Adjust config in the next section for faster dry runs or full training.","metadata":{}},{"id":"2c47edc2-05ec-4e2e-b385-7e6ec3db2c2c","cell_type":"code","source":"# Optional: uncomment if a package is missing in Kaggle runtime\n!pip -q install timm pydicom\n\nimport os\nimport gc\nimport json\nimport math\nimport time\nimport random\nimport warnings\nfrom dataclasses import dataclass, asdict\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport cv2\nimport pydicom\n\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.metrics import roc_auc_score, confusion_matrix\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\n\nimport timm\n\nwarnings.filterwarnings(\"ignore\")\nprint(\"torch:\", torch.__version__)\nprint(\"timm:\", timm.__version__)\nprint(\"cuda:\", torch.cuda.is_available())\nprint(\"gpu count:\", torch.cuda.device_count())\nif torch.cuda.is_available():\n    print(\"device:\", torch.cuda.get_device_name(0))\n\n!ls /kaggle/input/rsna-intracranial-hemorrhage-detection","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:27:20.132852Z","iopub.execute_input":"2026-02-24T16:27:20.133059Z","iopub.status.idle":"2026-02-24T16:27:38.054937Z","shell.execute_reply.started":"2026-02-24T16:27:20.133038Z","shell.execute_reply":"2026-02-24T16:27:38.054182Z"}},"outputs":[],"execution_count":null},{"id":"71937ea3-fd43-4fc4-b0ee-2d0478e24042","cell_type":"code","source":"!ls /kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:42:08.370138Z","iopub.execute_input":"2026-02-24T16:42:08.370788Z","iopub.status.idle":"2026-02-24T16:42:08.619273Z","shell.execute_reply.started":"2026-02-24T16:42:08.370758Z","shell.execute_reply":"2026-02-24T16:42:08.618585Z"}},"outputs":[],"execution_count":null},{"id":"c36ad4fb","cell_type":"markdown","source":"## 1) Environment Setup in Kaggle Notebook\n\nThis notebook assumes Kaggle paths:\n- `/kaggle/input/rsna-intracranial-hemorrhage-detection`\n- outputs in `/kaggle/working`","metadata":{}},{"id":"674d73a3","cell_type":"code","source":"@dataclass\nclass CFG:\n    # Paths\n    competition_root: str = \"/kaggle/input/rsna-intracranial-hemorrhage-detection\"\n    working_dir: str = \"/kaggle/working/rsna_single_notebook\"\n\n    # Data\n    image_size: int = 384\n    in_chans: int = 3\n    num_classes: int = 6\n    classes: tuple = (\"any\", \"epidural\", \"intraparenchymal\", \"intraventricular\", \"subarachnoid\", \"subdural\")\n\n    # Folds\n    n_folds: int = 5\n    train_folds: tuple = (0,)   # change to (0,1,2,3,4) for full run\n\n    # Model/Train\n    model_name: str = \"tf_efficientnet_b2_ns\"\n    pretrained: bool = True\n    epochs: int = 3             # increase for full training\n    batch_size: int = 24\n    num_workers: int = 4\n    lr: float = 1e-4\n    weight_decay: float = 1e-4\n    grad_clip: float = 5.0\n    use_amp: bool = True\n\n    # Inference\n    tta: bool = True\n\n    # Repro\n    seed: int = 3407\n\ncfg = CFG()\ncfg.batch_size = 16\ncfg.num_workers = 2\nPath(cfg.working_dir).mkdir(parents=True, exist_ok=True)\nprint(asdict(cfg))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:27:42.818675Z","iopub.execute_input":"2026-02-24T16:27:42.819161Z","iopub.status.idle":"2026-02-24T16:27:42.826291Z","shell.execute_reply.started":"2026-02-24T16:27:42.819135Z","shell.execute_reply":"2026-02-24T16:27:42.825633Z"}},"outputs":[],"execution_count":null},{"id":"55f53a56","cell_type":"code","source":"def seed_everything(seed=3407):\n    random.seed(seed)\n    np.random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nseed_everything(cfg.seed)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:27:46.137306Z","iopub.execute_input":"2026-02-24T16:27:46.137555Z","iopub.status.idle":"2026-02-24T16:27:46.147984Z","shell.execute_reply.started":"2026-02-24T16:27:46.137537Z","shell.execute_reply":"2026-02-24T16:27:46.147196Z"}},"outputs":[],"execution_count":null},{"id":"02baf28c","cell_type":"markdown","source":"## 2) Load Dataset from Kaggle Input","metadata":{}},{"id":"5b68a952-42a4-4258-8bd5-7f8de2f98c67","cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:27:48.796588Z","iopub.execute_input":"2026-02-24T16:27:48.796841Z","iopub.status.idle":"2026-02-24T16:27:48.800528Z","shell.execute_reply.started":"2026-02-24T16:27:48.796822Z","shell.execute_reply":"2026-02-24T16:27:48.799889Z"}},"outputs":[],"execution_count":null},{"id":"10bf7cfd","cell_type":"code","source":"# Correct root\nroot = \"/kaggle/input/rsna-intracranial-hemorrhage-detection/rsna-intracranial-hemorrhage-detection\"\n\ntrain_csv = root + \"/stage_2_train.csv\"\nsample_sub_csv = root + \"/stage_2_sample_submission.csv\"\ntrain_img_dir = root + \"/stage_2_train_images\"\ntest_img_dir = root + \"/stage_2_test_images\"\n\nrequired = [train_csv, sample_sub_csv, train_img_dir, test_img_dir]\n\ntrain_raw = pd.read_csv(train_csv)\nsample_sub = pd.read_csv(sample_sub_csv)\n\nprint(\"train_raw shape:\", train_raw.shape)\nprint(\"sample_sub shape:\", sample_sub.shape)\nprint(train_raw.head(2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:42:51.879672Z","iopub.execute_input":"2026-02-24T16:42:51.880529Z","iopub.status.idle":"2026-02-24T16:42:55.823247Z","shell.execute_reply.started":"2026-02-24T16:42:51.880494Z","shell.execute_reply":"2026-02-24T16:42:55.822576Z"}},"outputs":[],"execution_count":null},{"id":"c8398f82","cell_type":"markdown","source":"## 3) Data Validation and Quick Profiling","metadata":{}},{"id":"ae2facda","cell_type":"code","source":"# Convert train labels from long to wide format\ntrain_raw[[\"SOPInstanceUID\", \"label_type\"]] = train_raw[\"ID\"].str.rsplit(\"_\", n=1, expand=True)\nlabel_df = train_raw.pivot_table(index=\"SOPInstanceUID\", columns=\"label_type\", values=\"Label\", aggfunc=\"first\").reset_index()\nfor c in cfg.classes:\n    if c not in label_df.columns:\n        label_df[c] = 0.0\nlabel_df = label_df[[\"SOPInstanceUID\", *cfg.classes]]\n\nprint(\"label_df:\", label_df.shape)\nprint(\"missing labels:\", label_df.isna().sum().sum())\n\nclass_means = label_df[list(cfg.classes)].mean().sort_values(ascending=False)\nplt.figure(figsize=(8, 3))\nclass_means.plot(kind=\"bar\")\nplt.title(\"Class prevalence\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:42:58.602456Z","iopub.execute_input":"2026-02-24T16:42:58.603038Z","iopub.status.idle":"2026-02-24T16:43:40.364094Z","shell.execute_reply.started":"2026-02-24T16:42:58.603012Z","shell.execute_reply":"2026-02-24T16:43:40.363497Z"}},"outputs":[],"execution_count":null},{"id":"5b896552-5c0a-4829-b5c5-a53213351bbd","cell_type":"markdown","source":"2️⃣ Prepare Train Data (Pivot + Patient ID)","metadata":{}},{"id":"ab163db9-775f-4109-a193-8ba44bd9ed77","cell_type":"code","source":"# Split ID into Image and Class\ntrain_raw[['Image', 'Class']] = train_raw['ID'].str.rsplit('_', n=1, expand=True)\n\n# Use pivot_table instead of pivot (important!)\ntrain_df = train_raw.pivot_table(\n    index='Image',\n    columns='Class',\n    values='Label',\n    aggfunc='max'   # or 'mean' — max is safer\n).reset_index()\n\n# Ensure correct column order\ntrain_df = train_df[['Image'] + list(cfg.classes)]\n\n# Add patient id\ntrain_df['patient_id'] = train_df['Image'].apply(lambda x: x.split('_')[0])\n\nprint(\"train_df shape:\", train_df.shape)\ntrain_df.head()\ntrain_df.tail()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:43:40.365376Z","iopub.execute_input":"2026-02-24T16:43:40.365599Z","iopub.status.idle":"2026-02-24T16:44:00.874567Z","shell.execute_reply.started":"2026-02-24T16:43:40.365581Z","shell.execute_reply":"2026-02-24T16:44:00.874019Z"}},"outputs":[],"execution_count":null},{"id":"3d6201cc-9075-4c7a-b174-45f9c1730405","cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold\n\nskf = StratifiedKFold(n_splits=cfg.n_folds, shuffle=True, random_state=cfg.seed)\n\ntrain_df['fold'] = -1\n\n# Stratify using \"any\" column (best choice)\nfor fold, (train_idx, val_idx) in enumerate(\n    skf.split(train_df, train_df['any'])\n):\n    train_df.loc[val_idx, 'fold'] = fold\n\ntrain_df['fold'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:44:00.875344Z","iopub.execute_input":"2026-02-24T16:44:00.875921Z","iopub.status.idle":"2026-02-24T16:44:01.010748Z","shell.execute_reply.started":"2026-02-24T16:44:00.875895Z","shell.execute_reply":"2026-02-24T16:44:01.010163Z"}},"outputs":[],"execution_count":null},{"id":"9c562a74-fdf8-4f42-bca0-2d7e6e78c26e","cell_type":"code","source":"def window_image(img, WL, WW):\n    lower = WL - WW // 2\n    upper = WL + WW // 2\n    img = np.clip(img, lower, upper)\n    img = (img - lower) / (upper - lower)\n    return img\n\ndef load_dicom(path):\n    dcm = pydicom.dcmread(path)\n    img = dcm.pixel_array.astype(np.float32)\n\n    # Apply rescale slope & intercept\n    if hasattr(dcm, \"RescaleSlope\"):\n        img = img * dcm.RescaleSlope\n    if hasattr(dcm, \"RescaleIntercept\"):\n        img = img + dcm.RescaleIntercept\n\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:44:01.011568Z","iopub.execute_input":"2026-02-24T16:44:01.011825Z","iopub.status.idle":"2026-02-24T16:44:01.016954Z","shell.execute_reply.started":"2026-02-24T16:44:01.011801Z","shell.execute_reply":"2026-02-24T16:44:01.016215Z"}},"outputs":[],"execution_count":null},{"id":"db2e00ae-1348-48a5-a9d8-9bce98bc85a5","cell_type":"code","source":"class RSNADataset(Dataset):\n    def __init__(self, df, img_dir, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.is_test = is_test\n        self.label_cols = list(cfg.classes)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_id = row['Image']\n        path = os.path.join(self.img_dir, img_id + \".dcm\")\n\n        if not os.path.exists(path):\n            img = np.zeros((cfg.image_size, cfg.image_size, 3), dtype=np.float32)\n            img = torch.tensor(img).permute(2, 0, 1)\n\n            if self.is_test:\n                return img\n            label = torch.tensor(row[self.label_cols].values.astype(np.float32))\n            return img, label\n\n        img = load_dicom(path)\n\n        img1 = window_image(img, 40, 80)\n        img2 = window_image(img, 80, 200)\n        img3 = window_image(img, 600, 2800)\n\n        img = np.stack([img1, img2, img3], axis=-1)\n        img = cv2.resize(img, (cfg.image_size, cfg.image_size))\n        img = np.transpose(img, (2, 0, 1))\n\n        img = torch.tensor(img, dtype=torch.float32)\n\n        if self.is_test:\n            return img\n\n        label = torch.tensor(row[self.label_cols].values.astype(np.float32))\n        return img, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:53:12.018438Z","iopub.execute_input":"2026-02-24T16:53:12.019093Z","iopub.status.idle":"2026-02-24T16:53:12.026767Z","shell.execute_reply.started":"2026-02-24T16:53:12.019058Z","shell.execute_reply":"2026-02-24T16:53:12.026135Z"}},"outputs":[],"execution_count":null},{"id":"b99e63c9-eed8-439b-9774-ed5c81ba1713","cell_type":"code","source":"class SliceModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.model = timm.create_model(\n            cfg.model_name,\n            pretrained=cfg.pretrained,\n            num_classes=cfg.num_classes,\n            in_chans=cfg.in_chans\n        )\n\n    def forward(self, x):\n        return self.model(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:53:12.028124Z","iopub.execute_input":"2026-02-24T16:53:12.02834Z","iopub.status.idle":"2026-02-24T16:53:12.042813Z","shell.execute_reply.started":"2026-02-24T16:53:12.028313Z","shell.execute_reply":"2026-02-24T16:53:12.042121Z"}},"outputs":[],"execution_count":null},{"id":"9aa1d5e2-ce5d-47af-8baa-89fff7f17388","cell_type":"code","source":"class SequenceModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.cnn = timm.create_model(\n            cfg.model_name,\n            pretrained=cfg.pretrained,\n            num_classes=0,\n            in_chans=cfg.in_chans\n        )\n\n        self.feature_dim = self.cnn.num_features\n\n        self.fc1 = nn.Linear(self.feature_dim, 512)\n\n        self.bigru = nn.GRU(\n            input_size=512,\n            hidden_size=256,\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True\n        )\n\n        self.fc_out = nn.Linear(512, cfg.num_classes)\n\n    def forward(self, x):\n        feat = self.cnn(x)\n        feat = self.fc1(feat)\n\n        seq = feat.unsqueeze(1)\n        gru_out, _ = self.bigru(seq)\n        gru_out = gru_out.squeeze(1)\n\n        combined = feat + gru_out\n        out = self.fc_out(combined)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:53:12.043704Z","iopub.execute_input":"2026-02-24T16:53:12.043939Z","iopub.status.idle":"2026-02-24T16:53:12.05693Z","shell.execute_reply.started":"2026-02-24T16:53:12.043921Z","shell.execute_reply":"2026-02-24T16:53:12.056425Z"}},"outputs":[],"execution_count":null},{"id":"9b5502d0-e127-4bbc-8286-78e7e5e179a4","cell_type":"code","source":"criterion = nn.BCEWithLogitsLoss()\n\ndef train_one_epoch(model, loader, optimizer, scaler, device):\n    model.train()\n    total_loss = 0\n\n    for imgs, labels in loader:\n        imgs = imgs.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n\n        with autocast(enabled=cfg.use_amp):\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n\n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.grad_clip)\n        scaler.step(optimizer)\n        scaler.update()\n\n        total_loss += loss.item()\n\n    return total_loss / len(loader)\n\n\ndef validate(model, loader, device):\n    model.eval()\n    preds, targets = [], []\n\n    with torch.no_grad():\n        for imgs, labels in loader:\n            imgs = imgs.to(device)\n            outputs = model(imgs)\n\n            preds.append(torch.sigmoid(outputs).cpu().numpy())\n            targets.append(labels.numpy())\n\n    preds = np.vstack(preds)\n    targets = np.vstack(targets)\n\n    auc = roc_auc_score(targets, preds, average=\"macro\")\n    return auc, preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:53:12.05777Z","iopub.execute_input":"2026-02-24T16:53:12.058036Z","iopub.status.idle":"2026-02-24T16:53:12.074306Z","shell.execute_reply.started":"2026-02-24T16:53:12.057994Z","shell.execute_reply":"2026-02-24T16:53:12.073779Z"}},"outputs":[],"execution_count":null},{"id":"2b0d6b21-1a01-4375-a277-74012bd397d2","cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold\n\nskf = StratifiedKFold(n_splits=cfg.n_folds, shuffle=True, random_state=cfg.seed)\n\ntrain_df['fold'] = -1\n\nfor fold, (train_idx, val_idx) in enumerate(\n    skf.split(train_df, train_df['any'])\n):\n    train_df.loc[val_idx, 'fold'] = fold\n\ndevice = torch.device(\"cuda\")\n\nfor fold in cfg.train_folds:\n    print(f\"\\n========== Fold {fold} ==========\")\n\n    train_data = train_df[train_df.fold != fold]\n    val_data = train_df[train_df.fold == fold]\n\n    train_ds = RSNADataset(train_data, train_img_dir)\n    val_ds = RSNADataset(val_data, train_img_dir)\n\n    train_loader = DataLoader(train_ds,\n                              batch_size=cfg.batch_size,\n                              shuffle=True,\n                              num_workers=cfg.num_workers,\n                              pin_memory=True)\n\n    val_loader = DataLoader(val_ds,\n                            batch_size=cfg.batch_size,\n                            shuffle=False,\n                            num_workers=cfg.num_workers,\n                            pin_memory=True)\n\n    model1 = SliceModel().to(device)\n    model2 = SequenceModel().to(device)\n\n    optimizer1 = torch.optim.AdamW(model1.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\n    optimizer2 = torch.optim.AdamW(model2.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\n\n    scaler1 = GradScaler()\n    scaler2 = GradScaler()\n\n    for epoch in range(cfg.epochs):\n        loss1 = train_one_epoch(model1, train_loader, optimizer1, scaler1, device)\n        loss2 = train_one_epoch(model2, train_loader, optimizer2, scaler2, device)\n\n        auc1, _ = validate(model1, val_loader, device)\n        auc2, _ = validate(model2, val_loader, device)\n\n        print(f\"Epoch {epoch+1} | \"\n              f\"M1 Loss {loss1:.4f} AUC {auc1:.4f} | \"\n              f\"M2 Loss {loss2:.4f} AUC {auc2:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-24T16:53:12.075601Z","iopub.execute_input":"2026-02-24T16:53:12.075825Z","iopub.status.idle":"2026-02-24T17:00:01.550031Z","shell.execute_reply.started":"2026-02-24T16:53:12.075806Z","shell.execute_reply":"2026-02-24T17:00:01.549049Z"}},"outputs":[],"execution_count":null}]}