{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13851420,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":13382569,"datasetId":8491061,"databundleVersionId":14092251},{"sourceType":"datasetVersion","sourceId":15718220,"datasetId":10070427,"databundleVersionId":16658730},{"sourceType":"datasetVersion","sourceId":15724027,"datasetId":10074332,"databundleVersionId":16664937}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# CELL 1 — Environment Setup\n# ============================================================\nimport os\nos.environ[\"PYTORCH_CUDA_ALLOC_CONF\"] = \"expandable_segments:True\"\nimport warnings; warnings.filterwarnings(\"ignore\")\nprint(\"Environment ready — no MONAI required.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 2 — Imports (pure PyTorch, no MONAI)\n# ============================================================\nimport os, ast, gc\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.cuda.amp import GradScaler, autocast\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedKFold\nfrom scipy.ndimage import zoom\nfrom tqdm.notebook import tqdm\nfrom copy import deepcopy\n\ntorch.backends.cudnn.benchmark = True\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device  : {DEVICE}\")\nprint(f\"GPU     : {torch.cuda.get_device_name(0)}\")\nprint(f\"PyTorch : {torch.__version__}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 3 — Configuration\n# ============================================================\nPOS_WEIGHTS = [\n    1.34,   # Aneurysm Present\n    50.00,  # Left Infraclinoid ICA          (capped)\n    43.73,  # Right Infraclinoid ICA\n    12.79,  # Left Supraclinoid ICA\n    15.04,  # Right Supraclinoid ICA\n    18.83,  # Left MCA\n    13.86,  # Right MCA\n    10.70,  # Anterior Communicating Artery\n    50.00,  # Left ACA                       (capped)\n    50.00,  # Right ACA                      (capped)\n    46.93,  # Left Posterior Communicating\n    39.26,  # Right Posterior Communicating\n    35.60,  # Basilar Tip\n    35.60,  # Other Posterior Circulation\n]\n\nCONFIG = {\n    \"img_size\":           (96, 96, 96),\n    \"n_folds\":            5,\n    \"seed\":               42,\n    \"folds_to_run\":       [3, 4],              # ← ONLY CHANGE from Notebook 2\n\n    \"batch_size\":         2,\n    \"accumulation_steps\": 4,\n    \"epochs\":             25,\n    \"lr\":                 1e-4,\n    \"weight_decay\":       1e-5,\n    \"grad_clip\":          1.0,\n    \"ema_decay\":          0.998,\n\n    \"seg_weight\":         1.0,\n    \"cls_weight\":         0.1,\n    \"num_classes\":        14,\n\n    \"manifest\":   \"/kaggle/input/datasets/diyasilawat/rsna-aneurysm-manifest-v1/manifest.csv\",\n    \"npy_dir\":    \"/kaggle/input/datasets/sachchidanandadaki/rsna-3d-preprocessed/processed_3D_isotropic/\",\n    \"output_dir\": \"/kaggle/working/\",\n}\n\nprint(f\"Device          : {DEVICE}\")\nprint(f\"Folds to run    : {CONFIG['folds_to_run']}\")\nprint(f\"Manifest exists : {os.path.exists(CONFIG['manifest'])}\")\nprint(f\"NPY dir exists  : {os.path.exists(CONFIG['npy_dir'])}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 4 — Model Architecture (3D U-Net, pure PyTorch)\n# ============================================================\nclass ConvBlock3D(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.block = nn.Sequential(\n            nn.Conv3d(in_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm3d(out_ch),\n            nn.ReLU(inplace=True),\n            nn.Conv3d(out_ch, out_ch, 3, padding=1, bias=False),\n            nn.BatchNorm3d(out_ch),\n            nn.ReLU(inplace=True),\n        )\n    def forward(self, x):\n        return self.block(x)\n\n\nclass AneurysmNet(nn.Module):\n    \"\"\"\n    3D U-Net encoder-decoder for heatmap prediction with a\n    classification head for 14-class multi-label output.\n    Encoder bottleneck features drive both segmentation and classification.\n    \"\"\"\n    def __init__(self, num_classes=14):\n        super().__init__()\n        self.pool = nn.MaxPool3d(2)\n\n        # Encoder\n        self.enc1 = ConvBlock3D(1,   32)\n        self.enc2 = ConvBlock3D(32,  64)\n        self.enc3 = ConvBlock3D(64,  128)\n        self.enc4 = ConvBlock3D(128, 256)\n\n        # Bottleneck\n        self.bottleneck = ConvBlock3D(256, 512)\n\n        # Decoder\n        self.up4  = nn.ConvTranspose3d(512, 256, 2, stride=2)\n        self.dec4 = ConvBlock3D(512, 256)\n        self.up3  = nn.ConvTranspose3d(256, 128, 2, stride=2)\n        self.dec3 = ConvBlock3D(256, 128)\n\n        # Segmentation head\n        self.seg_head = nn.Conv3d(128, 1, 1)\n\n        # Classification head\n        self.cls_head = nn.Sequential(\n            nn.AdaptiveAvgPool3d(1),\n            nn.Flatten(),\n            nn.Linear(512, 256),\n            nn.LayerNorm(256),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes),\n        )\n\n    def forward(self, x):\n        e1 = self.enc1(x)\n        e2 = self.enc2(self.pool(e1))\n        e3 = self.enc3(self.pool(e2))\n        e4 = self.enc4(self.pool(e3))\n        b  = self.bottleneck(self.pool(e4))\n\n        cls_out = self.cls_head(b)\n\n        d4 = self.dec4(torch.cat([self.up4(b), e4], dim=1))\n        d3 = self.dec3(torch.cat([self.up3(d4), e3], dim=1))\n        seg_out = F.interpolate(self.seg_head(d3),\n                                size=x.shape[2:],\n                                mode='trilinear',\n                                align_corners=False)\n        return seg_out, cls_out\n\n\ndef verify_model():\n    m = AneurysmNet(CONFIG[\"num_classes\"]).to(DEVICE)\n    with torch.no_grad():\n        x = torch.randn(1, 1, *CONFIG[\"img_size\"]).to(DEVICE)\n        seg, cls = m(x)\n    print(f\"Seg output : {tuple(seg.shape)}\")\n    print(f\"Cls output : {tuple(cls.shape)}\")\n    assert seg.shape == (1, 1, 96, 96, 96)\n    assert cls.shape == (1, 14)\n    del m, x, seg, cls\n    gc.collect(); torch.cuda.empty_cache()\n    print(\"Model verification passed ✓\")\n\nverify_model()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 5 — Loss Functions and EMA\n# ============================================================\nclass DiceCELoss(nn.Module):\n    \"\"\"Combined Dice + BCE loss for heatmap segmentation.\"\"\"\n    def forward(self, pred, target):\n        pred_sig = torch.sigmoid(pred)\n        smooth   = 1e-5\n        inter    = (pred_sig * target).sum()\n        dice     = 1 - (2 * inter + smooth) / \\\n                   (pred_sig.sum() + target.sum() + smooth)\n        bce      = F.binary_cross_entropy_with_logits(pred, target)\n        return dice + bce\n\n\nclass FocalBCELoss(nn.Module):\n    \"\"\"\n    Focal BCE reduces easy-negative contribution and emphasises\n    hard positives — critical for rare artery location classes.\n    \"\"\"\n    def __init__(self, gamma=2.0, pos_weight=None):\n        super().__init__()\n        self.gamma      = gamma\n        self.pos_weight = pos_weight\n\n    def forward(self, pred, target):\n        bce = F.binary_cross_entropy_with_logits(\n            pred, target,\n            pos_weight=self.pos_weight,\n            reduction='none'\n        )\n        prob    = torch.sigmoid(pred)\n        p_t     = prob * target + (1 - prob) * (1 - target)\n        focal_w = (1 - p_t) ** self.gamma\n        return (focal_w * bce).mean()\n\n\nclass EMA:\n    \"\"\"Exponential moving average of model weights for stable checkpoints.\"\"\"\n    def __init__(self, model, decay=0.998):\n        self.decay  = decay\n        self.shadow = {k: v.clone().float()\n                       for k, v in model.state_dict().items()}\n\n    def update(self, model):\n        with torch.no_grad():\n            for k, v in model.state_dict().items():\n                self.shadow[k] = (self.decay * self.shadow[k]\n                                  + (1 - self.decay) * v.float())\n\n    def state_dict(self):\n        return self.shadow\n\nprint(\"DiceCELoss, FocalBCELoss, EMA defined ✓\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 6 — Dataset (pure PyTorch)\n# ============================================================\nMASK_DIR_NEW = \"/kaggle/input/datasets/diyasilawat/rsna-aneurysm-manifest-v1/processed_masks/\"\n\nclass AneurysmDataset(Dataset):\n    def __init__(self, records, augment=False, img_size=(96, 96, 96)):\n        self.records  = records\n        self.augment  = augment\n        self.img_size = img_size\n\n    def __len__(self):\n        return len(self.records)\n\n    def __getitem__(self, idx):\n        rec = self.records[idx]\n\n        # Load volume\n        image = np.load(rec['image'])\n        if image.ndim == 4:\n            image = image[0]\n\n        # Remap mask path from Notebook 1 output → current dataset location\n        mask_filename = os.path.basename(rec['label'])\n        mask_path     = os.path.join(MASK_DIR_NEW, mask_filename)\n        mask = np.load(mask_path)['mask'].astype(np.float32)\n\n        # Resize to target shape if needed\n        if image.shape != tuple(self.img_size):\n            zf    = [t / s for t, s in zip(self.img_size, image.shape)]\n            image = zoom(image, zf, order=1)\n        if mask.shape != tuple(self.img_size):\n            zf   = [t / s for t, s in zip(self.img_size, mask.shape)]\n            mask = zoom(mask, zf, order=0)\n\n        # Intensity normalisation\n        p1, p99 = np.percentile(image, [1, 99])\n        image   = np.clip(image, p1, p99)\n        image   = (image - p1) / (p99 - p1 + 1e-8)\n\n        # Augmentation — random flips along each axis\n        if self.augment:\n            for ax in range(3):\n                if np.random.random() < 0.5:\n                    image = np.flip(image, axis=ax).copy()\n                    mask  = np.flip(mask,  axis=ax).copy()\n\n        cls_label = ast.literal_eval(rec['cls'])\n\n        return {\n            'image': torch.from_numpy(image[None]).float(),\n            'label': torch.from_numpy(mask[None]).float(),\n            'cls':   torch.tensor(cls_label, dtype=torch.float32),\n        }\n\nprint(\"AneurysmDataset defined ✓\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 7 — Data Pipeline Smoke Test (2 samples)\n# ============================================================\n_df  = pd.read_csv(CONFIG[\"manifest\"]).head(2)\n_rec = _df.to_dict('records')\n_ds  = AneurysmDataset(_rec, augment=False,\n                        img_size=CONFIG[\"img_size\"])\n_dl  = DataLoader(_ds, batch_size=1, num_workers=0)\n_b   = next(iter(_dl))\n\nprint(f\"Image shape : {tuple(_b['image'].shape)}\")\nprint(f\"Label shape : {tuple(_b['label'].shape)}\")\nprint(f\"Cls shape   : {tuple(_b['cls'].shape)}\")\nprint(f\"Cls sample  : {_b['cls'][0].tolist()}\")\nprint(\"Smoke test passed ✓\")\n\ndel _df, _rec, _ds, _dl, _b\ngc.collect(); torch.cuda.empty_cache()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 8 — Single Fold Training Function\n# ============================================================\ndef train_one_fold(fold_idx, train_records, val_records):\n    print(f\"\\n{'='*60}\")\n    print(f\"  FOLD {fold_idx+1}  |  train={len(train_records)}  val={len(val_records)}\")\n    print(f\"{'='*60}\")\n\n    train_ds = AneurysmDataset(train_records, augment=True,\n                                img_size=CONFIG[\"img_size\"])\n    val_ds   = AneurysmDataset(val_records,   augment=False,\n                                img_size=CONFIG[\"img_size\"])\n    train_dl = DataLoader(train_ds, batch_size=CONFIG[\"batch_size\"],\n                          shuffle=True,  num_workers=2, pin_memory=True)\n    val_dl   = DataLoader(val_ds,   batch_size=1,\n                          shuffle=False, num_workers=2, pin_memory=True)\n\n    model = AneurysmNet(CONFIG[\"num_classes\"]).to(DEVICE)\n    ema   = EMA(model, decay=CONFIG[\"ema_decay\"])\n\n    optimizer = torch.optim.AdamW(model.parameters(),\n                                  lr=CONFIG[\"lr\"],\n                                  weight_decay=CONFIG[\"weight_decay\"])\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(\n                    optimizer, T_0=10, T_mult=1)\n    scaler    = GradScaler()\n\n    seg_loss_fn = DiceCELoss()\n    pw_tensor   = torch.tensor(POS_WEIGHTS,\n                               dtype=torch.float32).to(DEVICE)\n    cls_loss_fn = FocalBCELoss(gamma=2.0, pos_weight=pw_tensor)\n\n    best_val  = float('inf')\n    save_path = f\"{CONFIG['output_dir']}aneurysm_fold{fold_idx+1}.pth\"\n\n    for epoch in range(CONFIG[\"epochs\"]):\n        # ── Training ─────────────────────────────────────────\n        model.train()\n        total_loss = 0.0\n        optimizer.zero_grad()\n\n        pbar = tqdm(train_dl,\n                    desc=f\"F{fold_idx+1} Ep{epoch+1:02d}\",\n                    leave=False)\n        for step, batch in enumerate(pbar):\n            imgs    = batch[\"image\"].to(DEVICE)\n            seg_lbl = batch[\"label\"].to(DEVICE)\n            cls_lbl = batch[\"cls\"].to(DEVICE)\n\n            with autocast():\n                pred_seg, pred_cls = model(imgs)\n                l_seg = seg_loss_fn(pred_seg, seg_lbl)\n                l_cls = cls_loss_fn(pred_cls, cls_lbl)\n                loss  = (CONFIG[\"seg_weight\"] * l_seg\n                       + CONFIG[\"cls_weight\"] * l_cls)\n                loss  = loss / CONFIG[\"accumulation_steps\"]\n\n            scaler.scale(loss).backward()\n\n            if (step + 1) % CONFIG[\"accumulation_steps\"] == 0 \\\n               or (step + 1) == len(train_dl):\n                scaler.unscale_(optimizer)\n                torch.nn.utils.clip_grad_norm_(\n                    model.parameters(), CONFIG[\"grad_clip\"])\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                ema.update(model)\n\n            total_loss += loss.item() * CONFIG[\"accumulation_steps\"]\n            pbar.set_postfix(\n                loss=f\"{loss.item()*CONFIG['accumulation_steps']:.4f}\")\n\n        avg_train = total_loss / len(train_dl)\n\n        # ── Validation with EMA weights ───────────────────────\n        val_model = deepcopy(model)\n        val_model.load_state_dict(ema.state_dict())\n        val_model.eval()\n        val_loss = 0.0\n\n        with torch.no_grad():\n            for vb in val_dl:\n                vi = vb[\"image\"].to(DEVICE)\n                vs = vb[\"label\"].to(DEVICE)\n                vc = vb[\"cls\"].to(DEVICE)\n                ps, pc = val_model(vi)\n                val_loss += (\n                    CONFIG[\"seg_weight\"] * seg_loss_fn(ps, vs) +\n                    CONFIG[\"cls_weight\"] * cls_loss_fn(pc, vc)\n                ).item()\n\n        avg_val = val_loss / len(val_dl)\n        scheduler.step(epoch)\n\n        print(f\"  Ep {epoch+1:02d} | \"\n              f\"train={avg_train:.4f} | \"\n              f\"val={avg_val:.4f} | \"\n              f\"lr={scheduler.get_last_lr()[0]:.2e}\")\n\n        if avg_val < best_val:\n            best_val = avg_val\n            torch.save({\n                \"fold\":        fold_idx + 1,\n                \"epoch\":       epoch + 1,\n                \"model_state\": ema.state_dict(),\n                \"val_loss\":    avg_val,\n                \"config\":      CONFIG,\n                \"pos_weights\": POS_WEIGHTS,\n            }, save_path)\n            print(f\"  ✓ Checkpoint saved → val={avg_val:.4f}\")\n\n        del val_model\n        gc.collect(); torch.cuda.empty_cache()\n\n    print(f\"\\nFold {fold_idx+1} complete. Best val loss: {best_val:.4f}\")\n    return best_val","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 9 — Execute Training (Folds 0, 1, 2)\n# ============================================================\ndf = pd.read_csv(CONFIG[\"manifest\"])\ndf['_y'] = df['cls'].apply(lambda x: ast.literal_eval(x)[0])\n\nprint(f\"Total samples : {len(df)}\")\nprint(f\"Positive      : {int(df['_y'].sum())}\")\nprint(f\"Negative      : {int((df['_y']==0).sum())}\")\n\nskf    = StratifiedKFold(n_splits=CONFIG[\"n_folds\"],\n                         shuffle=True,\n                         random_state=CONFIG[\"seed\"])\nsplits = list(skf.split(df, df['_y']))\n\nfold_results = {}\n\nfor fold_idx in CONFIG[\"folds_to_run\"]:\n    train_idx, val_idx = splits[fold_idx]\n    train_records = df.iloc[train_idx].to_dict('records')\n    val_records   = df.iloc[val_idx].to_dict('records')\n\n    best = train_one_fold(fold_idx, train_records, val_records)\n    fold_results[fold_idx] = best\n\n    gc.collect(); torch.cuda.empty_cache()\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"NOTEBOOK 2 COMPLETE\")\nfor fi, best in fold_results.items():\n    print(f\"  Fold {fi+1} best val loss : {best:.4f}\")\nprint(\"=\"*60)\nprint(\"Next: download aneurysm_fold4.pth, aneurysm_fold5.pth\")\nprint(\"Upload all 5 fold .pth files as one dataset → run Notebook 4.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}