{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":52254,"databundleVersionId":9674523,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CT DICOM Preprocessing Pipeline\nComplete preprocessing for abdominal trauma CT classification","metadata":{}},{"cell_type":"code","source":"!pip install segmentation-models-pytorch\nimport os\nimport glob\nimport numpy as np\nimport pydicom\nimport cv2\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nfrom tqdm.notebook import tqdm\nimport segmentation_models_pytorch as smp\n\n# --- 1. CONFIGURATION ---\nCONFIG = {\n    \"INPUT_DIR\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection/train_images\",\n    \"IMG_SIZE\": 256,\n    \"BATCH_SIZE\": 16,      # Good for RTX 3060 / P100\n    \"ACCUM_STEPS\": 2,      # Effective Batch = 32\n    \"LR\": 1e-4,\n    \"EPOCHS\": 30,\n    \"DEVICE\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"NUM_WORKERS\": 2       # Uses CPU to load DICOMs while GPU trains\n}\n\n# --- 2. PREPROCESSING UTILS (ON-THE-FLY) ---\ndef get_windowing(image, slope, intercept, window_center, window_width):\n    \"\"\"Apply CT windowing to raw pixels.\"\"\"\n    img_hu = image * slope + intercept\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    img_window = np.clip(img_hu, img_min, img_max)\n    return img_window\n\ndef load_dicom_volume(series_path):\n    \"\"\"Reads DICOMs, selects a random chunk, and preprocesses.\"\"\"\n    dicom_files = sorted(glob.glob(os.path.join(series_path, \"*.dcm\")))\n    \n    if not dicom_files:\n        return None\n\n    # --- EFFICIENCY HACK ---\n    # Instead of loading 500 slices, pick 3 random consecutive slices (2.5D)\n    # This makes it FAST enough for on-the-fly training.\n    if len(dicom_files) < 3: \n        return None\n    \n    start_idx = np.random.randint(0, len(dicom_files) - 3)\n    selected_files = dicom_files[start_idx : start_idx+3]\n    \n    processed_chunk = []\n    \n    for f in selected_files:\n        try:\n            ds = pydicom.dcmread(f)\n            slope = float(getattr(ds, 'RescaleSlope', 1))\n            intercept = float(getattr(ds, 'RescaleIntercept', 0))\n            pixel_data = ds.pixel_array.astype(np.float32)\n            \n            # Soft Tissue Window (Abdomen)\n            img = get_windowing(pixel_data, slope, intercept, 40, 400)\n            \n            # Normalize to [0, 1]\n            img = (img - (40 - 200)) / 400 \n            \n            # Resize\n            img = cv2.resize(img, (CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE']))\n            processed_chunk.append(img)\n        except:\n            return None\n\n    if len(processed_chunk) != 3: \n        return None\n\n    # Stack -> (256, 256, 3)\n    return np.dstack(processed_chunk)\n\n# --- 3. DIRECT DATASET (NO SAVING TO DISK) ---\nclass DirectTraumaDataset(Dataset):\n    def __init__(self, root_dir):\n        self.root_dir = root_dir\n        # Get list of all patient folders\n        self.patient_ids = sorted(os.listdir(root_dir))\n        # Filter out non-folders if any\n        self.patient_ids = [p for p in self.patient_ids if os.path.isdir(os.path.join(root_dir, p))]\n        \n    def __len__(self):\n        return len(self.patient_ids)\n    \n    def __getitem__(self, idx):\n        patient_id = self.patient_ids[idx]\n        patient_path = os.path.join(self.root_dir, patient_id)\n        \n        # Get first available series (simplified for training loop)\n        series_list = os.listdir(patient_path)\n        if not series_list:\n            return torch.zeros((3, CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE'])), torch.zeros((1, CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE']))\n            \n        series_id = series_list[0]\n        series_path = os.path.join(patient_path, series_id)\n        \n        # Load Data (CPU Intense)\n        image = load_dicom_volume(series_path)\n        \n        if image is None:\n            # Fallback if load fails\n            image = np.zeros((CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE'], 3), dtype=np.float32)\n            \n        # To Tensor (C, H, W)\n        image = torch.from_numpy(image).permute(2, 0, 1).float()\n        \n        # Dummy Mask (Replace with real labels later)\n        mask = torch.zeros((1, CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE'])).float()\n        \n        return image, mask\n\n# --- 4. TRAINING LOOP ---\ndef train_on_the_fly():\n    print(f\"🚀 Initializing On-the-Fly Pipeline on {CONFIG['DEVICE']}...\")\n    \n    # Dataset\n    dataset = DirectTraumaDataset(CONFIG['INPUT_DIR'])\n    print(f\"Found {len(dataset)} patients.\")\n    \n    loader = DataLoader(\n        dataset, \n        batch_size=CONFIG['BATCH_SIZE'], \n        shuffle=True, \n        num_workers=CONFIG['NUM_WORKERS'], \n        pin_memory=True\n    )\n    \n    # --- FIX: CHANGED ENCODER TO 'mobilenet_v2' ---\n    model = smp.Unet(\n        encoder_name=\"mobilenet_v2\",      # Supported & Fast\n        encoder_weights=\"imagenet\", \n        in_channels=3, \n        classes=1\n    ).to(CONFIG['DEVICE'])\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    criterion = nn.BCEWithLogitsLoss()\n    scaler = GradScaler()\n\n    # Loop\n    for epoch in range(CONFIG['EPOCHS']):\n        model.train()\n        epoch_loss = 0\n        pbar = tqdm(loader, desc=f\"Epoch {epoch+1}\")\n        \n        for i, (img, mask) in enumerate(pbar):\n            img, mask = img.to(CONFIG['DEVICE']), mask.to(CONFIG['DEVICE'])\n            \n            # Mixed Precision Step\n            with autocast():\n                pred = model(img)\n                loss = criterion(pred, mask) / CONFIG['ACCUM_STEPS']\n            \n            scaler.scale(loss).backward()\n            \n            if (i + 1) % CONFIG['ACCUM_STEPS'] == 0:\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            epoch_loss += loss.item() * CONFIG['ACCUM_STEPS']\n            pbar.set_postfix(loss=loss.item() * CONFIG['ACCUM_STEPS'])\n            \n        print(f\"Epoch {epoch+1} Complete. Avg Loss: {epoch_loss/len(loader):.4f}\")\n\n# Run\nif __name__ == \"__main__\":\n    train_on_the_fly()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T20:29:33.922943Z","iopub.execute_input":"2025-11-24T20:29:33.923413Z","iopub.status.idle":"2025-11-24T20:30:22.639757Z","shell.execute_reply.started":"2025-11-24T20:29:33.923377Z","shell.execute_reply":"2025-11-24T20:30:22.637837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install nibabel pydicom segmentation-models-pytorch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:08:06.044918Z","iopub.execute_input":"2025-11-24T19:08:06.045198Z","iopub.status.idle":"2025-11-24T19:08:09.609603Z","shell.execute_reply.started":"2025-11-24T19:08:06.045168Z","shell.execute_reply":"2025-11-24T19:08:09.608887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport pydicom\nimport nibabel as nib\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.notebook import tqdm\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nCONFIG = {\n    \"ROOT_DIR\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection\",\n    \"IMG_SIZE\": 128,\n    \"NUM_SLICES\": 32,      # Depth of 3D chunk\n    \"BATCH_SIZE\": 8,       # Optimized for 2x T4 GPUs\n    \"LR\": 1e-4,\n    \"EPOCHS\": 30,\n    \"DEVICE\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"NUM_WORKERS\": 4\n}\n\n# ==========================================\n# 2. CUSTOM LOSS (The Fix for Blank Masks)\n# ==========================================\nclass DiceBCELoss(nn.Module):\n    def __init__(self, weight=None, size_average=True):\n        super(DiceBCELoss, self).__init__()\n\n    def forward(self, inputs, targets, smooth=1):\n        # Flatten\n        inputs = inputs.view(-1)\n        targets = targets.view(-1)\n        \n        # BCE Loss\n        bce = F.binary_cross_entropy_with_logits(inputs, targets, reduction='mean')\n        \n        # Dice Loss\n        inputs = torch.sigmoid(inputs)\n        intersection = (inputs * targets).sum()                            \n        dice = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)  \n        \n        # Combined: 30% BCE (Overall accuracy) + 70% Dice (Overlap focus)\n        return 0.3 * bce + 0.7 * dice\n\n# ==========================================\n# 3. DATASET LOGIC\n# ==========================================\ndef find_valid_pairs(root_dir):\n    print(\"🔍 Scanning dataset for matching pairs...\")\n    img_root = os.path.join(root_dir, \"train_images\")\n    seg_root = os.path.join(root_dir, \"segmentations\")\n    \n    # Get available NIfTI masks\n    available_masks = {f.split('.')[0]: os.path.join(seg_root, f) for f in os.listdir(seg_root) if f.endswith('.nii')}\n    \n    valid_pairs = []\n    patients = os.listdir(img_root)\n    \n    for pid in tqdm(patients, desc=\"Matching Series\"):\n        patient_dir = os.path.join(img_root, pid)\n        if not os.path.isdir(patient_dir): continue\n        \n        for series_id in os.listdir(patient_dir):\n            if series_id in available_masks:\n                valid_pairs.append({\n                    'img_path': os.path.join(patient_dir, series_id),\n                    'mask_path': available_masks[series_id]\n                })\n                \n    print(f\"✅ Found {len(valid_pairs)} matched series.\")\n    return valid_pairs\n\nclass TraumaVolumeDataset(Dataset):\n    def __init__(self, pairs_list, img_size=128, num_slices=32):\n        self.pairs = pairs_list\n        self.img_size = img_size\n        self.num_slices = num_slices\n\n    def __len__(self):\n        return len(self.pairs)\n\n    def load_volume_and_mask(self, img_path, mask_path):\n        # 1. Load DICOMs (Sorted)\n        dicom_files = glob.glob(os.path.join(img_path, \"*.dcm\"))\n        dicom_files.sort(key=lambda x: int(pydicom.dcmread(x, stop_before_pixels=True).InstanceNumber))\n        \n        # 2. Load NIfTI Mask\n        nii = nib.load(mask_path)\n        mask_data = nii.get_fdata() \n        # Fix Orientation: NIfTI (H, W, D) -> (D, H, W)\n        mask_data = np.transpose(mask_data, (2, 1, 0)) \n        mask_data = np.rot90(mask_data, k=1, axes=(1,2))\n        mask_data = np.flip(mask_data, axis=1)\n\n        # 3. SMART CHUNKING (Crucial Fix)\n        # Don't just pick a random chunk (likely empty). Pick a chunk with organs.\n        total_slices = len(dicom_files)\n        best_start = 0\n        max_pixels = -1\n        \n        if total_slices > self.num_slices:\n            # Try 10 random spots, keep the one with most mask content\n            for _ in range(10):\n                start = np.random.randint(0, total_slices - self.num_slices)\n                chunk_sum = np.sum(mask_data[start : start + self.num_slices])\n                if chunk_sum > max_pixels:\n                    max_pixels = chunk_sum\n                    best_start = start\n                    if max_pixels > 500: break # Found a good chunk, stop looking\n            \n            files_chunk = dicom_files[best_start : best_start + self.num_slices]\n            mask_chunk = mask_data[best_start : best_start + self.num_slices]\n        else:\n            files_chunk = dicom_files\n            mask_chunk = mask_data\n\n        # 4. Process Images\n        img_vol = []\n        for f in files_chunk:\n            ds = pydicom.dcmread(f)\n            img = ds.pixel_array.astype(np.float32)\n            slope = float(getattr(ds, 'RescaleSlope', 1))\n            intercept = float(getattr(ds, 'RescaleIntercept', 0))\n            img = img * slope + intercept\n            img = np.clip(img, -40, 240) # Abdominal Window\n            img = (img + 40) / 280\n            img = cv2.resize(img, (self.img_size, self.img_size))\n            img_vol.append(img)\n\n        # 5. Process Mask\n        mask_vol = []\n        for i in range(len(mask_chunk)):\n            m = cv2.resize(mask_chunk[i], (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n            mask_vol.append(m)\n            \n        img_vol = np.array(img_vol)\n        mask_vol = np.array(mask_vol)\n        mask_vol = (mask_vol > 0).astype(np.float32) # Binary\n\n        # Padding\n        if img_vol.shape[0] < self.num_slices:\n            pad = self.num_slices - img_vol.shape[0]\n            img_vol = np.pad(img_vol, ((0, pad), (0,0), (0,0)), 'constant')\n            mask_vol = np.pad(mask_vol, ((0, pad), (0,0), (0,0)), 'constant')\n            \n        return img_vol, mask_vol\n\n    def __getitem__(self, idx):\n        try:\n            pair = self.pairs[idx]\n            img, mask = self.load_volume_and_mask(pair['img_path'], pair['mask_path'])\n        except:\n            img = np.zeros((self.num_slices, self.img_size, self.img_size), dtype=np.float32)\n            mask = np.zeros_like(img)\n        return torch.from_numpy(img).unsqueeze(0).float(), torch.from_numpy(mask).unsqueeze(0).float()\n\n# ==========================================\n# 4. MODEL (MobileNetV3-UNet)\n# ==========================================\nclass SEBlock3D(nn.Module):\n    def __init__(self, in_channels, reduction=4):\n        super().__init__()\n        self.pool = nn.AdaptiveAvgPool3d(1)\n        self.fc = nn.Sequential(\n            nn.Linear(in_channels, in_channels // reduction, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Linear(in_channels // reduction, in_channels, bias=False),\n            nn.Hardsigmoid()\n        )\n    def forward(self, x):\n        b, c, _, _, _ = x.size()\n        y = self.pool(x).view(b, c)\n        y = self.fc(y).view(b, c, 1, 1, 1)\n        return x * y\n\nclass MobileNetBlock3D(nn.Module):\n    def __init__(self, in_ch, out_ch, kernel_size, stride, expand_ratio, use_se=True):\n        super().__init__()\n        self.use_res_connect = (stride == 1 and in_ch == out_ch)\n        hidden_dim = int(round(in_ch * expand_ratio))\n        layers = []\n        if expand_ratio != 1:\n            layers.extend([nn.Conv3d(in_ch, hidden_dim, 1, 1, 0, bias=False), nn.BatchNorm3d(hidden_dim), nn.Hardswish()])\n        pad = (kernel_size - 1) // 2\n        layers.extend([\n            nn.Conv3d(hidden_dim, hidden_dim, kernel_size, stride, pad, groups=hidden_dim, bias=False),\n            nn.BatchNorm3d(hidden_dim),\n            nn.Hardswish()\n        ])\n        if use_se: layers.append(SEBlock3D(hidden_dim))\n        layers.extend([nn.Conv3d(hidden_dim, out_ch, 1, 1, 0, bias=False), nn.BatchNorm3d(out_ch)])\n        self.conv = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return x + self.conv(x) if self.use_res_connect else self.conv(x)\n\nclass MobileNetV3UNet3D(nn.Module):\n    def __init__(self, in_channels=1, num_classes=1):\n        super().__init__()\n        self.stem = nn.Sequential(nn.Conv3d(in_channels, 16, 3, stride=2, padding=1, bias=False), nn.BatchNorm3d(16), nn.Hardswish())\n        self.layer1 = MobileNetBlock3D(16, 24, 3, 1, 2) \n        self.layer2 = MobileNetBlock3D(24, 40, 5, 2, 4)\n        self.layer3 = MobileNetBlock3D(40, 80, 5, 2, 4)\n        self.layer4 = MobileNetBlock3D(80, 112, 5, 2, 4)\n        \n        self.up1 = nn.ConvTranspose3d(112, 80, kernel_size=2, stride=2)\n        self.dec1 = nn.Sequential(nn.Conv3d(160, 80, 3, 1, 1), nn.BatchNorm3d(80), nn.ReLU(inplace=True))\n        self.up2 = nn.ConvTranspose3d(80, 40, kernel_size=2, stride=2)\n        self.dec2 = nn.Sequential(nn.Conv3d(80, 40, 3, 1, 1), nn.BatchNorm3d(40), nn.ReLU(inplace=True))\n        self.up3 = nn.ConvTranspose3d(40, 24, kernel_size=2, stride=2)\n        self.dec3 = nn.Sequential(nn.Conv3d(48, 24, 3, 1, 1), nn.BatchNorm3d(24), nn.ReLU(inplace=True))\n        self.up4 = nn.ConvTranspose3d(24, 16, kernel_size=2, stride=2)\n        self.final = nn.Conv3d(16, num_classes, 1)\n\n    def forward(self, x):\n        x0 = self.stem(x); x1 = self.layer1(x0); x2 = self.layer2(x1); x3 = self.layer3(x2); x4 = self.layer4(x3)\n        d1 = self.dec1(torch.cat([self.up1(x4), x3], dim=1))\n        d2 = self.dec2(torch.cat([self.up2(d1), x2], dim=1))\n        d3_up = self.up3(d2)\n        if d3_up.shape != x1.shape: d3_up = F.interpolate(d3_up, size=x1.shape[2:], mode='nearest')\n        d3 = self.dec3(torch.cat([d3_up, x1], dim=1))\n        out = self.final(self.up4(d3))\n        if out.shape[2:] != x.shape[2:]: out = F.interpolate(out, size=x.shape[2:], mode='nearest')\n        return out\n\n# ==========================================\n# 5. PIPELINE EXECUTION\n# ==========================================\ndef extract_roi(image, mask, padding=5):\n    rows = np.any(mask, axis=1)\n    cols = np.any(mask, axis=0)\n    if not np.any(rows) or not np.any(cols): return image, None\n    y_min, y_max = np.where(rows)[0][[0, -1]]\n    x_min, x_max = np.where(cols)[0][[0, -1]]\n    h, w = image.shape[:2]\n    y_min = max(0, y_min - padding); y_max = min(h, y_max + padding)\n    x_min = max(0, x_min - padding); x_max = min(w, x_max + padding)\n    return image[y_min:y_max, x_min:x_max], (x_min, y_min, x_max, y_max)\n\ndef run_pipeline():\n    print(f\"🚀 STARTING PIPELINE (Smart Chunking + Dice Loss) | GPUs: {torch.cuda.device_count()}\")\n    \n    # 1. Data Setup\n    valid_pairs = find_valid_pairs(CONFIG['ROOT_DIR'])\n    if not valid_pairs: print(\"❌ No Pairs Found!\"); return\n    \n    train_pairs, val_pairs = train_test_split(valid_pairs, test_size=0.2, random_state=42)\n    train_loader = DataLoader(TraumaVolumeDataset(train_pairs), batch_size=CONFIG['BATCH_SIZE'], shuffle=True, num_workers=CONFIG['NUM_WORKERS'])\n    val_loader = DataLoader(TraumaVolumeDataset(val_pairs), batch_size=CONFIG['BATCH_SIZE'], shuffle=False, num_workers=CONFIG['NUM_WORKERS'])\n    \n    # 2. Model Setup\n    model = MobileNetV3UNet3D(in_channels=1, num_classes=1)\n    if torch.cuda.device_count() > 1: model = nn.DataParallel(model)\n    model = model.to(CONFIG['DEVICE'])\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    criterion = DiceBCELoss() # <--- THE CRITICAL FIX\n    scaler = torch.amp.GradScaler('cuda')\n    history = []\n    \n    # 3. Training Loop\n    for epoch in range(CONFIG['EPOCHS']):\n        model.train(); t_loss = 0\n        pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}\")\n        \n        for img, mask in pbar:\n            img, mask = img.to(CONFIG['DEVICE']), mask.to(CONFIG['DEVICE'])\n            with torch.amp.autocast('cuda'):\n                pred = model(img)\n                loss = criterion(pred, mask)\n            \n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer); torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            scaler.step(optimizer); scaler.update(); optimizer.zero_grad()\n            t_loss += loss.item()\n            pbar.set_postfix(loss=loss.item())\n            \n        avg_loss = t_loss/len(train_loader)\n        history.append(avg_loss)\n        print(f\"Ep {epoch+1} | Avg Loss: {avg_loss:.4f}\")\n\n    # 4. Final Visualization\n    print(\"\\n🖼️ VISUALIZING PREDICTIONS...\")\n    img, _ = val_loader.dataset[0]\n    model.eval()\n    with torch.no_grad():\n        # Predict on GPU\n        logit = model(img.unsqueeze(0).to(CONFIG['DEVICE']))\n        pred_mask = torch.sigmoid(logit).cpu().numpy()[0, 0]\n\n    # Show Slice 16\n    mid = 16\n    sl_img = img[0, mid].numpy()\n    sl_mask = pred_mask[mid]\n    \n    # Binarize mask for cropping (>0.5)\n    binary_mask = (sl_mask > 0.5).astype(int)\n    crop, bbox = extract_roi(sl_img, binary_mask)\n    \n    plt.figure(figsize=(16, 5))\n    ax1 = plt.subplot(1,3,1); ax1.imshow(sl_img, cmap='gray'); ax1.set_title(\"Input CT\")\n    if bbox: ax1.add_patch(patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0], bbox[3]-bbox[1], ec='r', fc='none', lw=2))\n    \n    plt.subplot(1,3,2); plt.imshow(sl_mask, cmap='jet'); plt.title(\"AI Probability Map\")\n    \n    plt.subplot(1,3,3)\n    if crop is not None: plt.imshow(crop, cmap='gray'); plt.title(\"Final Extracted Organ\")\n    else: plt.text(0.5,0.5,\"No Organ Found\",ha='center'); plt.title(\"Crop Failed\")\n    \n    plt.show()\n\nif __name__ == \"__main__\":\n    run_pipeline()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T19:08:14.427782Z","iopub.execute_input":"2025-11-24T19:08:14.42807Z","iopub.status.idle":"2025-11-24T19:55:41.946508Z","shell.execute_reply.started":"2025-11-24T19:08:14.428046Z","shell.execute_reply":"2025-11-24T19:55:41.945547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport pydicom\nimport nibabel as nib\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport seaborn as sns\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.notebook import tqdm\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\n# ==========================================\n# 1. CONFIGURATION\n# ==========================================\nCONFIG = {\n    \"ROOT_DIR\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection\",\n    \"SAVE_DIR\": \"/kaggle/working/processed_crops\",\n    \"IMG_SIZE\": 128,\n    \"NUM_SLICES\": 32,      # Depth of 3D chunk\n    \"BATCH_SIZE\": 8,       # 8 Total (Split 4 per GPU on 2x T4)\n    \"LR\": 1e-4,\n    \"EPOCHS\": 100,\n    \"DEVICE\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"NUM_WORKERS\": 4\n}\n\nos.makedirs(CONFIG['SAVE_DIR'], exist_ok=True)\n\n# ==========================================\n# 2. LOSS & UTILS\n# ==========================================\nclass DiceBCELoss(nn.Module):\n    def __init__(self, weight=None, size_average=True):\n        super(DiceBCELoss, self).__init__()\n\n    def forward(self, inputs, targets, smooth=1):\n        inputs = inputs.view(-1)\n        targets = targets.view(-1)\n        bce = F.binary_cross_entropy_with_logits(inputs, targets, reduction='mean')\n        inputs = torch.sigmoid(inputs)\n        intersection = (inputs * targets).sum()                            \n        dice = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)  \n        return 0.3 * bce + 0.7 * dice\n\ndef find_valid_pairs(root_dir):\n    print(\"🔍 Scanning dataset...\")\n    img_root = os.path.join(root_dir, \"train_images\")\n    seg_root = os.path.join(root_dir, \"segmentations\")\n    available_masks = {f.split('.')[0]: os.path.join(seg_root, f) for f in os.listdir(seg_root) if f.endswith('.nii')}\n    valid_pairs = []\n    if not os.path.exists(img_root): return []\n    \n    for pid in tqdm(os.listdir(img_root), desc=\"Matching\"):\n        p_dir = os.path.join(img_root, pid)\n        if not os.path.isdir(p_dir): continue\n        for sid in os.listdir(p_dir):\n            if sid in available_masks:\n                valid_pairs.append({'img_path': os.path.join(p_dir, sid), 'mask_path': available_masks[sid]})\n    print(f\"✅ Found {len(valid_pairs)} pairs.\")\n    return valid_pairs\n\n# ==========================================\n# 3. DATASET (Smart Chunking)\n# ==========================================\nclass TraumaVolumeDataset(Dataset):\n    def __init__(self, pairs_list, img_size=128, num_slices=32):\n        self.pairs = pairs_list\n        self.img_size = img_size\n        self.num_slices = num_slices\n\n    def __len__(self): return len(self.pairs)\n\n    def load_volume_and_mask(self, img_path, mask_path):\n        dicom_files = glob.glob(os.path.join(img_path, \"*.dcm\"))\n        # Sorting is critical for 3D consistency\n        dicom_files.sort(key=lambda x: int(pydicom.dcmread(x, stop_before_pixels=True).InstanceNumber))\n        \n        nii = nib.load(mask_path)\n        mask_data = nii.get_fdata() \n        mask_data = np.transpose(mask_data, (2, 1, 0)) \n        mask_data = np.rot90(mask_data, k=1, axes=(1,2))\n        mask_data = np.flip(mask_data, axis=1)\n\n        # Smart Chunking: Find a chunk with content\n        total_slices = len(dicom_files)\n        best_start = 0\n        max_pixels = -1\n        \n        if total_slices > self.num_slices:\n            for _ in range(10): # Search 10 times for a good spot\n                start = np.random.randint(0, total_slices - self.num_slices)\n                chunk_sum = np.sum(mask_data[start : start + self.num_slices])\n                if chunk_sum > max_pixels:\n                    max_pixels = chunk_sum\n                    best_start = start\n                    if max_pixels > 500: break\n            files_chunk = dicom_files[best_start : best_start + self.num_slices]\n            mask_chunk = mask_data[best_start : best_start + self.num_slices]\n        else:\n            files_chunk = dicom_files\n            mask_chunk = mask_data\n\n        img_vol = []\n        for f in files_chunk:\n            ds = pydicom.dcmread(f)\n            img = ds.pixel_array.astype(np.float32)\n            slope = float(getattr(ds, 'RescaleSlope', 1))\n            intercept = float(getattr(ds, 'RescaleIntercept', 0))\n            img = img * slope + intercept\n            img = np.clip(img, -40, 240)\n            img = (img + 40) / 280\n            img = cv2.resize(img, (self.img_size, self.img_size))\n            img_vol.append(img)\n\n        mask_vol = []\n        for i in range(len(mask_chunk)):\n            m = cv2.resize(mask_chunk[i], (self.img_size, self.img_size), interpolation=cv2.INTER_NEAREST)\n            mask_vol.append(m)\n            \n        img_vol = np.array(img_vol)\n        mask_vol = np.array(mask_vol)\n        mask_vol = (mask_vol > 0).astype(np.float32)\n\n        if img_vol.shape[0] < self.num_slices:\n            pad = self.num_slices - img_vol.shape[0]\n            img_vol = np.pad(img_vol, ((0, pad), (0,0), (0,0)), 'constant')\n            mask_vol = np.pad(mask_vol, ((0, pad), (0,0), (0,0)), 'constant')\n            \n        return img_vol, mask_vol\n\n    def __getitem__(self, idx):\n        try:\n            pair = self.pairs[idx]\n            img, mask = self.load_volume_and_mask(pair['img_path'], pair['mask_path'])\n        except:\n            img = np.zeros((self.num_slices, self.img_size, self.img_size), dtype=np.float32)\n            mask = np.zeros_like(img)\n        return torch.from_numpy(img).unsqueeze(0).float(), torch.from_numpy(mask).unsqueeze(0).float()\n\n# ==========================================\n# 4. MODEL (MobileNetV3-UNet 3D)\n# ==========================================\n\nclass SEBlock3D(nn.Module):\n    def __init__(self, in_channels, reduction=4):\n        super().__init__()\n        self.pool = nn.AdaptiveAvgPool3d(1)\n        self.fc = nn.Sequential(\n            nn.Linear(in_channels, in_channels // reduction, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Linear(in_channels // reduction, in_channels, bias=False),\n            nn.Hardsigmoid()\n        )\n    def forward(self, x):\n        b, c, _, _, _ = x.size()\n        y = self.pool(x).view(b, c)\n        y = self.fc(y).view(b, c, 1, 1, 1)\n        return x * y\n\nclass MobileNetBlock3D(nn.Module):\n    def __init__(self, in_ch, out_ch, kernel_size, stride, expand_ratio, use_se=True):\n        super().__init__()\n        self.use_res_connect = (stride == 1 and in_ch == out_ch)\n        hidden_dim = int(round(in_ch * expand_ratio))\n        layers = []\n        if expand_ratio != 1:\n            layers.extend([nn.Conv3d(in_ch, hidden_dim, 1, 1, 0, bias=False), nn.BatchNorm3d(hidden_dim), nn.Hardswish()])\n        pad = (kernel_size - 1) // 2\n        layers.extend([\n            nn.Conv3d(hidden_dim, hidden_dim, kernel_size, stride, pad, groups=hidden_dim, bias=False),\n            nn.BatchNorm3d(hidden_dim),\n            nn.Hardswish()\n        ])\n        if use_se: layers.append(SEBlock3D(hidden_dim))\n        layers.extend([nn.Conv3d(hidden_dim, out_ch, 1, 1, 0, bias=False), nn.BatchNorm3d(out_ch)])\n        self.conv = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return x + self.conv(x) if self.use_res_connect else self.conv(x)\n\nclass MobileNetV3UNet3D(nn.Module):\n    def __init__(self, in_channels=1, num_classes=1):\n        super().__init__()\n        self.stem = nn.Sequential(nn.Conv3d(in_channels, 16, 3, stride=2, padding=1, bias=False), nn.BatchNorm3d(16), nn.Hardswish())\n        self.layer1 = MobileNetBlock3D(16, 24, 3, 1, 2) \n        self.layer2 = MobileNetBlock3D(24, 40, 5, 2, 4)\n        self.layer3 = MobileNetBlock3D(40, 80, 5, 2, 4)\n        self.layer4 = MobileNetBlock3D(80, 112, 5, 2, 4)\n        self.up1 = nn.ConvTranspose3d(112, 80, kernel_size=2, stride=2)\n        self.dec1 = nn.Sequential(nn.Conv3d(160, 80, 3, 1, 1), nn.BatchNorm3d(80), nn.ReLU(inplace=True))\n        self.up2 = nn.ConvTranspose3d(80, 40, kernel_size=2, stride=2)\n        self.dec2 = nn.Sequential(nn.Conv3d(80, 40, 3, 1, 1), nn.BatchNorm3d(40), nn.ReLU(inplace=True))\n        self.up3 = nn.ConvTranspose3d(40, 24, kernel_size=2, stride=2)\n        self.dec3 = nn.Sequential(nn.Conv3d(48, 24, 3, 1, 1), nn.BatchNorm3d(24), nn.ReLU(inplace=True))\n        self.up4 = nn.ConvTranspose3d(24, 16, kernel_size=2, stride=2)\n        self.final = nn.Conv3d(16, num_classes, 1)\n\n    def forward(self, x):\n        x0 = self.stem(x); x1 = self.layer1(x0); x2 = self.layer2(x1); x3 = self.layer3(x2); x4 = self.layer4(x3)\n        d1 = self.dec1(torch.cat([self.up1(x4), x3], dim=1))\n        d2 = self.dec2(torch.cat([self.up2(d1), x2], dim=1))\n        d3_up = self.up3(d2)\n        if d3_up.shape != x1.shape: d3_up = F.interpolate(d3_up, size=x1.shape[2:], mode='nearest')\n        d3 = self.dec3(torch.cat([d3_up, x1], dim=1))\n        out = self.final(self.up4(d3))\n        if out.shape[2:] != x.shape[2:]: out = F.interpolate(out, size=x.shape[2:], mode='nearest')\n        return out\n\n# ==========================================\n# 5. CROP UTILS\n# ==========================================\ndef extract_roi_2d(image, mask, padding=5):\n    rows = np.any(mask, axis=1); cols = np.any(mask, axis=0)\n    if not np.any(rows) or not np.any(cols): return image, None\n    y_min, y_max = np.where(rows)[0][[0, -1]]; x_min, x_max = np.where(cols)[0][[0, -1]]\n    h, w = image.shape[:2]\n    y_min = max(0, y_min - padding); y_max = min(h, y_max + padding)\n    x_min = max(0, x_min - padding); x_max = min(w, x_max + padding)\n    return image[y_min:y_max, x_min:x_max], (x_min, y_min, x_max, y_max)\n\ndef extract_roi_3d(volume, mask, padding=5):\n    rows = np.any(mask, axis=(0, 2)); cols = np.any(mask, axis=(0, 1)); depth = np.any(mask, axis=(1, 2))\n    if not np.any(rows) or not np.any(cols): return volume\n    y_min, y_max = np.where(rows)[0][[0, -1]]; x_min, x_max = np.where(cols)[0][[0, -1]]; z_min, z_max = np.where(depth)[0][[0, -1]]\n    h, w = volume.shape[1:]; d = volume.shape[0]\n    y_min = max(0, y_min - padding); y_max = min(h, y_max + padding)\n    x_min = max(0, x_min - padding); x_max = min(w, x_max + padding)\n    z_min = max(0, z_min); z_max = min(d, z_max)\n    return volume[z_min:z_max, y_min:y_max, x_min:x_max]\n\n# ==========================================\n# 6. TRAINING PIPELINE\n# ==========================================\ndef run_pipeline():\n    print(f\"🚀 STARTING TRAINING | GPUs: {torch.cuda.device_count()}\")\n    valid_pairs = find_valid_pairs(CONFIG['ROOT_DIR'])\n    if not valid_pairs: print(\"❌ No Pairs Found!\"); return None, None\n\n    train_pairs, val_pairs = train_test_split(valid_pairs, test_size=0.2, random_state=42)\n    train_loader = DataLoader(TraumaVolumeDataset(train_pairs), batch_size=CONFIG['BATCH_SIZE'], shuffle=True, num_workers=CONFIG['NUM_WORKERS'])\n    val_loader = DataLoader(TraumaVolumeDataset(val_pairs), batch_size=CONFIG['BATCH_SIZE'], shuffle=False, num_workers=CONFIG['NUM_WORKERS'])\n    \n    # --- MODEL SETUP WITH MULTI-GPU ---\n    model = MobileNetV3UNet3D(in_channels=1, num_classes=1)\n    if torch.cuda.device_count() > 1: \n        print(f\"🔥 DataParallel Enabled on {torch.cuda.device_count()} GPUs\")\n        model = nn.DataParallel(model)\n    model = model.to(CONFIG['DEVICE'])\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    criterion = DiceBCELoss()\n    scaler = torch.amp.GradScaler('cuda')\n    \n    # --- TRAINING LOOP ---\n    for epoch in range(CONFIG['EPOCHS']):\n        model.train(); t_loss = 0\n        for img, mask in tqdm(train_loader, desc=f\"Epoch {epoch+1}\"):\n            img, mask = img.to(CONFIG['DEVICE']), mask.to(CONFIG['DEVICE'])\n            with torch.amp.autocast('cuda'):\n                pred = model(img)\n                loss = criterion(pred, mask)\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer); torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n            scaler.step(optimizer); scaler.update(); optimizer.zero_grad()\n            t_loss += loss.item()\n        print(f\"Ep {epoch+1} | Loss: {t_loss/len(train_loader):.4f}\")\n\n    # --- VISUALIZATION ---\n    print(\"\\n🖼️ VISUALIZING VALIDATION...\")\n    img, _ = val_loader.dataset[0]\n    model.eval()\n    with torch.no_grad():\n        pred_mask = torch.sigmoid(model(img.unsqueeze(0).to(CONFIG['DEVICE']))).cpu().numpy()[0, 0]\n    \n    mid = 16\n    crop, bbox = extract_roi_2d(img[0, mid].numpy(), (pred_mask[mid]>0.5).astype(int))\n    plt.figure(figsize=(12, 4))\n    plt.subplot(1,3,1); plt.imshow(img[0, mid], cmap='gray'); plt.title(\"Input\")\n    plt.subplot(1,3,2); plt.imshow(pred_mask[mid], cmap='jet'); plt.title(\"Prediction\")\n    plt.subplot(1,3,3); \n    if crop is not None: plt.imshow(crop, cmap='gray'); plt.title(\"Crop\")\n    plt.show()\n    \n    return model, val_loader\n\n# ==========================================\n# 7. CROP GENERATION\n# ==========================================\ndef generate_crops(model, val_loader):\n    print(f\"💾 SAVING CROPS TO: {CONFIG['SAVE_DIR']}\")\n    model.eval()\n    count = 0\n    with torch.no_grad():\n        for i, (img_tensor, _) in tqdm(enumerate(val_loader), total=len(val_loader)):\n            img_tensor = img_tensor.to(CONFIG['DEVICE'])\n            # Prediction\n            logits = model(img_tensor)\n            pred_masks = (torch.sigmoid(logits) > 0.5).cpu().numpy()\n            imgs_np = img_tensor.cpu().numpy()\n            \n            for b in range(imgs_np.shape[0]):\n                # Extract 3D ROI for each patient in batch\n                cropped = extract_roi_3d(imgs_np[b, 0], pred_masks[b, 0])\n                np.save(os.path.join(CONFIG['SAVE_DIR'], f\"crop_{count}.npy\"), cropped)\n                count += 1\n    print(f\"✅ Done! Saved {count} files.\")\n\nif __name__ == \"__main__\":\n    # 1. Train & Get Model\n    trained_model, validation_loader = run_pipeline()\n    \n    # 2. Generate Crops (Only if training succeeded)\n    if trained_model is not None:\n        generate_crops(trained_model, validation_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-24T20:29:23.714839Z","iopub.execute_input":"2025-11-24T20:29:23.715483Z","iopub.status.idle":"2025-11-24T20:29:24.694857Z","shell.execute_reply.started":"2025-11-24T20:29:23.715389Z","shell.execute_reply":"2025-11-24T20:29:24.693671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models\nfrom sklearn.metrics import roc_auc_score, roc_curve, auc, confusion_matrix, classification_report\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom tqdm.notebook import tqdm\nimport warnings\n\nwarnings.filterwarnings('ignore')\n\n# ==========================================\n# 1. PRODUCTION CONFIGURATION\n# ==========================================\nCONFIG = {\n    # Paths\n    \"ROOT_DIR\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection\",\n    \"CROP_DIR\": \"/kaggle/working/processed_crops_labeled\", # Where your .npy files are\n    \"CSV_PATH\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection/train_2024.csv\",\n    \n    # Model Params\n    \"IMG_SIZE\": 256,\n    \"SEQ_LEN\": 24,         # Depth (Slices per patient)\n    \n    # Hardware & Training\n    \"DEVICE\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n    \"BATCH_SIZE\": 8,       # Fits on 2x T4\n    \"ACCUM_STEPS\": 4,      # Gradient Accumulation (Virtual batch = 32)\n    \"LR\": 3e-4,\n    \"EPOCHS\": 100,\n    \"PATIENCE\": 3,         # Early stopping\n    \"NUM_WORKERS\": 4\n}\n\nprint(f\"✅ Configuration Loaded. Device: {CONFIG['DEVICE']}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:09:16.763675Z","iopub.execute_input":"2025-11-22T17:09:16.764425Z","iopub.status.idle":"2025-11-22T17:09:16.773348Z","shell.execute_reply.started":"2025-11-22T17:09:16.764394Z","shell.execute_reply":"2025-11-22T17:09:16.772713Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport pydicom\nimport torch\nfrom tqdm.notebook import tqdm\n\n# ==========================================\n# CONFIGURATION\n# ==========================================\nPRE_CONFIG = {\n    \"ROOT_DIR\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection\",\n    \"SAVE_DIR\": \"/kaggle/working/processed_crops_labeled\", # Must match training CROP_DIR\n    \"CSV_PATH\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection/train_2024.csv\",\n    \"IMG_SIZE\": 256,\n    \"SEQ_LEN\": 24\n}\n\nos.makedirs(PRE_CONFIG['SAVE_DIR'], exist_ok=True)\n\ndef preprocess_patient(patient_dir):\n    # 1. Load DICOMs and Sort\n    files = sorted(glob.glob(os.path.join(patient_dir, \"*.dcm\")), \n                   key=lambda x: int(pydicom.dcmread(x, stop_before_pixels=True).InstanceNumber))\n    \n    if len(files) < 2: return None\n\n    # 2. Smart Sampling (Standardize Depth to 24)\n    indices = np.linspace(0, len(files)-1, PRE_CONFIG['SEQ_LEN']).astype(int)\n    selected_files = [files[i] for i in indices]\n    \n    vol_stack = []\n    for f in selected_files:\n        try:\n            ds = pydicom.dcmread(f)\n            img = ds.pixel_array.astype(np.float32)\n            \n            # Windowing (Abdominal)\n            slope = float(getattr(ds, 'RescaleSlope', 1))\n            intercept = float(getattr(ds, 'RescaleIntercept', 0))\n            img = img * slope + intercept\n            img = np.clip(img, -40, 240)\n            img = (img + 40) / 280 # Normalize\n            \n            # Resize\n            img = cv2.resize(img, (PRE_CONFIG['IMG_SIZE'], PRE_CONFIG['IMG_SIZE']))\n            vol_stack.append(img)\n        except:\n            vol_stack.append(np.zeros((PRE_CONFIG['IMG_SIZE'], PRE_CONFIG['IMG_SIZE']), dtype=np.float32))\n            \n    return np.array(vol_stack, dtype=np.float16)\n\ndef run_preprocessing():\n    print(f\"💾 GENERATING DATA -> {PRE_CONFIG['SAVE_DIR']}\")\n    \n    df = pd.read_csv(PRE_CONFIG['CSV_PATH'])\n    patient_ids = df['patient_id'].unique()\n    \n    # Limit to 100 patients for testing (Remove [:100] for full run)\n    count = 0\n    for pid in tqdm(patient_ids[:100], desc=\"Processing\"):\n        save_path = os.path.join(PRE_CONFIG['SAVE_DIR'], f\"{pid}.npy\")\n        \n        # Skip if already exists\n        if os.path.exists(save_path): \n            count += 1\n            continue\n            \n        p_path = os.path.join(PRE_CONFIG['ROOT_DIR'], \"train_images\", str(pid))\n        if not os.path.exists(p_path): continue\n        \n        series = os.listdir(p_path)\n        if not series: continue\n        \n        # Process first series\n        vol = preprocess_patient(os.path.join(p_path, series[0]))\n        if vol is not None:\n            np.save(save_path, vol)\n            count += 1\n\n    print(f\"✅ Generated {count} files. You can now run training.\")\n\nif __name__ == \"__main__\":\n    run_preprocessing()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T10:47:27.00806Z","iopub.execute_input":"2025-11-22T10:47:27.008727Z","iopub.status.idle":"2025-11-22T10:53:21.804566Z","shell.execute_reply.started":"2025-11-22T10:47:27.0087Z","shell.execute_reply":"2025-11-22T10:53:21.803656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 2. DATASET (Robust & Filtered)\n# ==========================================\nclass CachedTraumaDataset(Dataset):\n    def __init__(self, crop_dir, csv_path, split='train'):\n        self.crop_dir = crop_dir\n        self.df = pd.read_csv(csv_path)\n        \n        # Create a set of valid IDs from the CSV for filtering\n        valid_ids = set(self.df['patient_id'].unique())\n        \n        # Scan folder for .npy files\n        all_files = glob.glob(os.path.join(crop_dir, \"*.npy\"))\n        \n        # Filter: Only keep files that exist in the CSV\n        self.files = []\n        for f in all_files:\n            try:\n                pid = int(os.path.basename(f).replace('.npy', ''))\n                if pid in valid_ids:\n                    self.files.append(f)\n            except: continue\n                \n        print(f\"   📂 {split.upper()}: Found {len(self.files)} valid patient volumes.\")\n\n        # Set index for fast label lookup\n        self.df = self.df.set_index('patient_id')\n        \n        # Target: Any Injury (Binary)\n        cols = ['bowel_injury', 'extravasation_injury', 'kidney_low', 'kidney_high', \n                'liver_low', 'liver_high', 'spleen_low', 'spleen_high']\n        self.df['target'] = self.df[cols].max(axis=1)\n        \n        # Train/Val Split (80/20)\n        split_idx = int(len(self.files) * 0.8)\n        if split == 'train': \n            self.files = self.files[:split_idx]\n        else: \n            self.files = self.files[split_idx:]\n\n    def __len__(self):\n        return len(self.files)\n\n    def __getitem__(self, idx):\n        path = self.files[idx]\n        pid = int(os.path.basename(path).replace('.npy',''))\n        \n        # Load 3D Volume\n        vol = np.load(path).astype(np.float32)\n        \n        # Input Shape: (1, Seq, H, W)\n        vol = torch.tensor(vol).unsqueeze(0) \n        \n        # Get Label\n        label = self.df.loc[pid, 'target']\n        return vol, torch.tensor(label, dtype=torch.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:09:24.415118Z","iopub.execute_input":"2025-11-22T17:09:24.415718Z","iopub.status.idle":"2025-11-22T17:09:24.424085Z","shell.execute_reply.started":"2025-11-22T17:09:24.415694Z","shell.execute_reply":"2025-11-22T17:09:24.42333Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 3. CLASSIFICATION MODEL (2.5D) - MobileNetV2\n# ==========================================\nclass TraumaClassifierMobileNetV2(nn.Module):\n    def __init__(self, hidden_dim=256):\n        super().__init__()\n        \n        # A. Backbone (MobileNetV2)\n        # Using weights='DEFAULT' loads ImageNet weights\n        self.backbone = models.mobilenet_v2(weights='DEFAULT')\n        self.features = self.backbone.features\n        \n        # MobileNetV2 output channels = 1280 (vs 576 in V3-Small)\n        self.pool = nn.AdaptiveAvgPool2d((1, 1))\n        \n        # B. Aggregator (Bi-LSTM)\n        # Input size must match backbone output channels (1280)\n        self.lstm = nn.LSTM(\n            input_size=1280, \n            hidden_size=hidden_dim, \n            num_layers=1, \n            batch_first=True, \n            bidirectional=True\n        )\n        \n        # C. Head\n        self.head = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(hidden_dim * 2, 64),\n            nn.ReLU(),\n            nn.Linear(64, 1)\n        )\n\n    def forward(self, x):\n        # Input: (Batch, 1, Seq, H, W)\n        b, c, s, h, w = x.shape\n        \n        # 1. CNN Phase (Fold time into batch)\n        x = x.view(b * s, 1, h, w)\n        x = x.repeat(1, 3, 1, 1) # 1 channel -> 3 channels\n        \n        x = self.features(x)       # (B*S, 1280, 7, 7)\n        x = self.pool(x)           # (B*S, 1280, 1, 1)\n        x = x.view(b, s, -1)       # Unfold: (B, S, 1280)\n        \n        # 2. RNN Phase\n        x, _ = self.lstm(x)        # (B, S, Hidden*2)\n        \n        # 3. Aggregation (Max Pool over time)\n        x, _ = torch.max(x, dim=1) # (B, Hidden*2)\n        \n        return self.head(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:09:27.694461Z","iopub.execute_input":"2025-11-22T17:09:27.695015Z","iopub.status.idle":"2025-11-22T17:09:27.701625Z","shell.execute_reply.started":"2025-11-22T17:09:27.694992Z","shell.execute_reply":"2025-11-22T17:09:27.700952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 4. TRAINING ENGINE\n# ==========================================\ndef train_production_model():\n    print(f\"🚀 STARTING TRAINING (MobileNetV2) on {CONFIG['DEVICE']}\")\n    \n    # 1. Load Data\n    train_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'train')\n    val_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'val')\n    \n    if len(train_ds) == 0:\n        print(\"❌ Error: No training data found. Did you run the preprocessing step?\")\n        return None, None, None\n    \n    train_loader = DataLoader(train_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=True, num_workers=CONFIG['NUM_WORKERS'])\n    val_loader = DataLoader(val_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=False, num_workers=CONFIG['NUM_WORKERS'])\n    \n    # 2. Model & Optimizer\n    # --- UPDATED TO USE MOBILENET V2 CLASS ---\n    model = TraumaClassifierMobileNetV2().to(CONFIG['DEVICE'])\n    \n    if torch.cuda.device_count() > 1:\n        model = nn.DataParallel(model)\n        \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    criterion = nn.BCEWithLogitsLoss()\n    scaler = torch.amp.GradScaler('cuda') # AMP\n    \n    # 3. Loop\n    history = {'train_loss': [], 'val_loss': [], 'val_auc': []}\n    best_loss = float('inf')\n    patience_c = 0\n    \n    for epoch in range(CONFIG['EPOCHS']):\n        model.train()\n        t_loss = 0\n        \n        pbar = tqdm(train_loader, desc=f\"Ep {epoch+1}\")\n        for i, (X, y) in enumerate(pbar):\n            X, y = X.to(CONFIG['DEVICE']), y.to(CONFIG['DEVICE'])\n            \n            with torch.amp.autocast('cuda'):\n                pred = model(X).squeeze()\n                loss = criterion(pred, y) / CONFIG['ACCUM_STEPS']\n            \n            scaler.scale(loss).backward()\n            \n            if (i + 1) % CONFIG['ACCUM_STEPS'] == 0:\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            t_loss += loss.item() * CONFIG['ACCUM_STEPS']\n            pbar.set_postfix(loss=loss.item() * CONFIG['ACCUM_STEPS'])\n            \n        # Validation\n        model.eval()\n        v_loss = 0\n        preds, targets = [], []\n        \n        with torch.no_grad():\n            for X, y in val_loader:\n                X, y = X.to(CONFIG['DEVICE']), y.to(CONFIG['DEVICE'])\n                with torch.amp.autocast('cuda'):\n                    out = model(X).squeeze()\n                    v_loss += criterion(out, y).item()\n                    preds.extend(torch.sigmoid(out).cpu().numpy())\n                    targets.extend(y.cpu().numpy())\n        \n        avg_t = t_loss / len(train_loader)\n        avg_v = v_loss / len(val_loader)\n        try: auc_score = roc_auc_score(targets, preds)\n        except: auc_score = 0.5\n        \n        history['train_loss'].append(avg_t)\n        history['val_loss'].append(avg_v)\n        history['val_auc'].append(auc_score)\n        \n        print(f\"Ep {epoch+1} | Train: {avg_t:.4f} | Val: {avg_v:.4f} | AUC: {auc_score:.4f}\")\n        \n        # Patience Check\n        if avg_v < best_loss:\n            best_loss = avg_v\n            patience_c = 0\n            torch.save(model.state_dict(), \"best_model_mobilenetv2.pth\")\n        else:\n            patience_c# filepath: c:\\Users\\sarma\\Downloads\\ct-dicom-preprocessing-e45610 (1).ipynb\n# ==========================================\n# 4. TRAINING ENGINE\n# ==========================================\ndef train_production_model():\n    print(f\"🚀 STARTING TRAINING (MobileNetV2) on {CONFIG['DEVICE']}\")\n    \n    # 1. Load Data\n    train_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'train')\n    val_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'val')\n    \n    if len(train_ds) == 0:\n        print(\"❌ Error: No training data found. Did you run the preprocessing step?\")\n        return None, None, None\n    \n    train_loader = DataLoader(train_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=True, num_workers=CONFIG['NUM_WORKERS'])\n    val_loader = DataLoader(val_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=False, num_workers=CONFIG['NUM_WORKERS'])\n    \n    # 2. Model & Optimizer\n    # --- UPDATED TO USE MOBILENET V2 CLASS ---\n    model = TraumaClassifierMobileNetV2().to(CONFIG['DEVICE'])\n    \n    if torch.cuda.device_count() > 1:\n        model = nn.DataParallel(model)\n        \n    optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    criterion = nn.BCEWithLogitsLoss()\n    scaler = torch.amp.GradScaler('cuda') # AMP\n    \n    # 3. Loop\n    history = {'train_loss': [], 'val_loss': [], 'val_auc': []}\n    best_loss = float('inf')\n    patience_c = 0\n    \n    for epoch in range(CONFIG['EPOCHS']):\n        model.train()\n        t_loss = 0\n        \n        pbar = tqdm(train_loader, desc=f\"Ep {epoch+1}\")\n        for i, (X, y) in enumerate(pbar):\n            X, y = X.to(CONFIG['DEVICE']), y.to(CONFIG['DEVICE'])\n            \n            with torch.amp.autocast('cuda'):\n                pred = model(X).squeeze()\n                loss = criterion(pred, y) / CONFIG['ACCUM_STEPS']\n            \n            scaler.scale(loss).backward()\n            \n            if (i + 1) % CONFIG['ACCUM_STEPS'] == 0:\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n                \n            t_loss += loss.item() * CONFIG['ACCUM_STEPS']\n            pbar.set_postfix(loss=loss.item() * CONFIG['ACCUM_STEPS'])\n            \n        # Validation\n        model.eval()\n        v_loss = 0\n        preds, targets = [], []\n        \n        with torch.no_grad():\n            for X, y in val_loader:\n                X, y = X.to(CONFIG['DEVICE']), y.to(CONFIG['DEVICE'])\n                with torch.amp.autocast('cuda'):\n                    out = model(X).squeeze()\n                    v_loss += criterion(out, y).item()\n                    preds.extend(torch.sigmoid(out).cpu().numpy())\n                    targets.extend(y.cpu().numpy())\n        \n        avg_t = t_loss / len(train_loader)\n        avg_v = v_loss / len(val_loader)\n        try: auc_score = roc_auc_score(targets, preds)\n        except: auc_score = 0.5\n        \n        history['train_loss'].append(avg_t)\n        history['val_loss'].append(avg_v)\n        history['val_auc'].append(auc_score)\n        \n        print(f\"Ep {epoch+1} | Train: {avg_t:.4f} | Val: {avg_v:.4f} | AUC: {auc_score:.4f}\")\n        \n        # Patience Check\n        if avg_v < best_loss:\n            best_loss = avg_v\n            patience_c = 0\n            torch.save(model.state_dict(), \"best_model_mobilenetv2.pth\")\n        else:\n            patience_c += 1\n            if patience_c >= CONFIG['PATIENCE']:\n                print(\"🛑 Early Stopping Triggered\")\n                break\n                \n    return history, targets, preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:09:29.811737Z","iopub.execute_input":"2025-11-22T17:09:29.812458Z","iopub.status.idle":"2025-11-22T17:09:29.824059Z","shell.execute_reply.started":"2025-11-22T17:09:29.812431Z","shell.execute_reply":"2025-11-22T17:09:29.823302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 5. EXECUTION & REPORTING\n# ==========================================\ndef generate_report(history, y_true, y_pred):\n    sns.set_style(\"whitegrid\")\n    plt.figure(figsize=(18, 5))\n    \n    # A. Learning Curve\n    plt.subplot(1, 3, 1)\n    plt.plot(history['train_loss'], label='Train Loss', marker='o')\n    plt.plot(history['val_loss'], label='Val Loss', marker='o')\n    plt.title(\"Loss History\")\n    plt.xlabel(\"Epochs\"); plt.ylabel(\"BCE Loss\"); plt.legend()\n    \n    # B. ROC Curve\n    plt.subplot(1, 3, 2)\n    fpr, tpr, _ = roc_curve(y_true, y_pred)\n    roc_auc = auc(fpr, tpr)\n    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'AUC = {roc_auc:.3f}')\n    plt.plot([0, 1], [0, 1], color='navy', linestyle='--')\n    plt.title(\"ROC Curve\")\n    plt.legend()\n    \n    # C. Confusion Matrix\n    plt.subplot(1, 3, 3)\n    y_bin = (np.array(y_pred) > 0.5).astype(int)\n    cm = confusion_matrix(y_true, y_bin)\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')\n    plt.title(\"Confusion Matrix\"); plt.xlabel(\"Predicted\"); plt.ylabel(\"Actual\")\n    \n    plt.tight_layout()\n    plt.show()\n    \n    print(\"\\n📊 CLASSIFICATION REPORT:\")\n    print(classification_report(y_true, y_bin))\n\nif __name__ == \"__main__\":\n    # 1. Train\n    hist, y_true, y_prob = train_production_model()\n    \n    # 2. Report (Only if training happened)\n    if hist is not None:\n        generate_report(hist, y_true, y_prob)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:09:32.746745Z","iopub.execute_input":"2025-11-22T17:09:32.747345Z","iopub.status.idle":"2025-11-22T17:10:13.795456Z","shell.execute_reply.started":"2025-11-22T17:09:32.74732Z","shell.execute_reply":"2025-11-22T17:10:13.794733Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dont use it for now","metadata":{}},{"cell_type":"code","source":"#pip install timm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:19:12.457479Z","iopub.execute_input":"2025-11-22T17:19:12.458454Z","iopub.status.idle":"2025-11-22T17:20:31.835303Z","shell.execute_reply.started":"2025-11-22T17:19:12.458418Z","shell.execute_reply":"2025-11-22T17:20:31.834465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import glob\n# import numpy as np\n# import pandas as pd\n# import torch\n# import torch.nn as nn\n# import torch.optim as optim\n# from torch.utils.data import Dataset, DataLoader\n# import timm\n# from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix\n# import matplotlib.pyplot as plt\n# import seaborn as sns\n# from tqdm.notebook import tqdm\n# import warnings\n\n# warnings.filterwarnings('ignore')\n\n# # ==========================================\n# # 1. CONFIGURATION\n# # ==========================================\n# CONFIG = {\n#     \"CROP_DIR\": \"/kaggle/working/processed_crops_labeled\", \n#     \"CSV_PATH\": \"/kaggle/input/rsna-2023-abdominal-trauma-detection/train_2024.csv\", \n#     \"IMG_SIZE\": 224,       \n#     \"SEQ_LEN\": 24,         \n#     \"BATCH_SIZE\": 8,       \n#     \"ACCUM_STEPS\": 4,      \n#     \"LR\": 1e-4,\n#     \"EPOCHS\": 30,\n#     \"PATIENCE\": 4,         \n#     \"DEVICE\": \"cuda\" if torch.cuda.is_available() else \"cpu\",\n#     \"NUM_WORKERS\": 4\n# }\n\n# # ==========================================\n# # 2. DATASET (ROBUST FIX)\n# # ==========================================\n# class CachedTraumaDataset(Dataset):\n#     def __init__(self, crop_dir, csv_path, split='train'):\n#         self.files = glob.glob(os.path.join(crop_dir, \"*.npy\"))\n#         self.df = pd.read_csv(csv_path)\n        \n#         # Create Target Column\n#         injury_cols = [\n#             'bowel_injury', 'extravasation_injury', \n#             'kidney_low', 'kidney_high', \n#             'liver_low', 'liver_high', \n#             'spleen_low', 'spleen_high'\n#         ]\n#         self.df['injury_label'] = self.df[injury_cols].max(axis=1)\n#         self.labels = self.df.set_index('patient_id')['injury_label'].to_dict()\n        \n#         # --- ROBUST FILE FILTERING ---\n#         self.valid_files = []\n#         for f in self.files:\n#             try:\n#                 filename = os.path.basename(f) # e.g. \"10004_21057.npy\" or \"11832.npy\"\n#                 clean_name = filename.replace('.npy', '')\n                \n#                 # Handle both \"PID_SID\" and \"PID\" formats\n#                 if '_' in clean_name:\n#                     patient_id = int(clean_name.split('_')[0])\n#                 else:\n#                     patient_id = int(clean_name)\n                    \n#                 if patient_id in self.labels:\n#                     self.valid_files.append(f)\n#             except ValueError:\n#                 continue # Skip weird files\n        \n#         # Split\n#         split_idx = int(len(self.valid_files) * 0.8)\n#         if split == 'train': self.files = self.valid_files[:split_idx]\n#         else: self.files = self.valid_files[split_idx:]\n\n#     def __len__(self): return len(self.files)\n\n#     def __getitem__(self, idx):\n#         path = self.files[idx]\n#         filename = os.path.basename(path)\n#         clean_name = filename.replace('.npy', '')\n        \n#         # Robust ID Extraction\n#         if '_' in clean_name:\n#             patient_id = int(clean_name.split('_')[0])\n#         else:\n#             patient_id = int(clean_name)\n        \n#         # Load Volume\n#         vol = np.load(path).astype(np.float32)\n        \n#         # Normalize & Resize (2.5D Stack)\n#         vol = torch.tensor(vol).unsqueeze(0).unsqueeze(0) # (1, 1, D, H, W)\n#         vol = torch.nn.functional.interpolate(vol, size=(CONFIG['SEQ_LEN'], CONFIG['IMG_SIZE'], CONFIG['IMG_SIZE']), mode='trilinear', align_corners=False)\n#         vol = vol.squeeze(0) # (1, 24, 224, 224)\n        \n#         label = self.labels.get(patient_id, 0.0)\n#         return vol, torch.tensor(label, dtype=torch.float32)\n\n# # ==========================================\n# # 3. MODEL: EfficientNet-Lite0 + LSTM\n# # ==========================================\n# class EfficientNetLiteLSTM(nn.Module):\n#     def __init__(self, hidden_dim=256):\n#         super().__init__()\n#         # Backbone\n#         self.backbone = timm.create_model('tf_efficientnet_lite0', pretrained=True, features_only=True)\n#         self.feature_dim = 320 \n#         self.pool = nn.AdaptiveAvgPool2d((1, 1))\n        \n#         # Aggregator\n#         self.lstm = nn.LSTM(\n#             input_size=self.feature_dim, \n#             hidden_size=hidden_dim, \n#             num_layers=1, \n#             batch_first=True, \n#             bidirectional=True\n#         )\n        \n#         # Head\n#         self.head = nn.Sequential(\n#             nn.Dropout(0.3),\n#             nn.Linear(hidden_dim * 2, 64),\n#             nn.ReLU(),\n#             nn.Linear(64, 1)\n#         )\n\n#     def forward(self, x):\n#         b, c, s, h, w = x.shape\n#         x = x.view(b * s, 1, h, w)\n#         x = x.repeat(1, 3, 1, 1) # RGB\n        \n#         features = self.backbone(x)[-1] \n#         x = self.pool(features)     \n#         x = x.view(b, s, -1)        \n        \n#         x, _ = self.lstm(x)         \n#         x, _ = torch.max(x, dim=1)  \n#         return self.head(x)\n\n# # ==========================================\n# # 4. TRAINING ENGINE\n# # ==========================================\n# def train_efficientnet():\n#     print(f\"🚀 STARTING EFFICIENTNET-LITE TRAINING | Device: {CONFIG['DEVICE']}\")\n    \n#     if not os.path.exists(CONFIG['CROP_DIR']):\n#         print(\"❌ No crops found.\"); return None, None\n\n#     train_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'train')\n#     val_ds = CachedTraumaDataset(CONFIG['CROP_DIR'], CONFIG['CSV_PATH'], 'val')\n    \n#     if len(train_ds) == 0:\n#         print(\"❌ Dataset is empty after filtering. Check CSV IDs vs Filenames.\")\n#         return None, None\n\n#     train_loader = DataLoader(train_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=True, num_workers=CONFIG['NUM_WORKERS'])\n#     val_loader = DataLoader(val_ds, batch_size=CONFIG['BATCH_SIZE'], shuffle=False, num_workers=CONFIG['NUM_WORKERS'])\n    \n#     model = EfficientNetLiteLSTM().to(CONFIG['DEVICE'])\n#     if torch.cuda.device_count() > 1: model = nn.DataParallel(model)\n    \n#     optimizer = optim.AdamW(model.parameters(), lr=CONFIG['LR'])\n    \n#     # Class Balancing\n#     pos_weight = torch.tensor([3.0]).to(CONFIG['DEVICE'])\n#     criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n    \n#     scaler = torch.amp.GradScaler('cuda')\n    \n#     best_auc = 0\n#     patience_c = 0\n    \n#     for epoch in range(CONFIG['EPOCHS']):\n#         model.train()\n#         t_loss = 0\n        \n#         pbar = tqdm(train_loader, desc=f\"Ep {epoch+1}\")\n#         for i, (img, label) in enumerate(pbar):\n#             img, label = img.to(CONFIG['DEVICE']), label.to(CONFIG['DEVICE'])\n            \n#             with torch.amp.autocast('cuda'):\n#                 pred = model(img).squeeze()\n#                 loss = criterion(pred, label) / CONFIG['ACCUM_STEPS']\n            \n#             scaler.scale(loss).backward()\n            \n#             if (i+1) % CONFIG['ACCUM_STEPS'] == 0:\n#                 scaler.step(optimizer)\n#                 scaler.update()\n#                 optimizer.zero_grad()\n            \n#             t_loss += loss.item() * CONFIG['ACCUM_STEPS']\n#             pbar.set_postfix(loss=loss.item() * CONFIG['ACCUM_STEPS'])\n            \n#         # Validation\n#         model.eval()\n#         preds, targets = [], []\n#         with torch.no_grad():\n#             for img, label in val_loader:\n#                 img, label = img.to(CONFIG['DEVICE']), label.to(CONFIG['DEVICE'])\n#                 with torch.amp.autocast('cuda'):\n#                     out = model(img).squeeze()\n#                     preds.extend(torch.sigmoid(out).cpu().numpy())\n#                     targets.extend(label.cpu().numpy())\n        \n#         try: auc_val = roc_auc_score(targets, preds)\n#         except: auc_val = 0.5\n        \n#         print(f\"Ep {epoch+1} | Loss: {t_loss/len(train_loader):.4f} | Val AUC: {auc_val:.4f}\")\n        \n#         if auc_val > best_auc:\n#             best_auc = auc_val\n#             patience_c = 0\n#             torch.save(model.state_dict(), \"efficientnet_best.pth\")\n#         else:\n#             patience_c += 1\n#             if patience_c >= CONFIG['PATIENCE']:\n#                 print(\"🛑 Early Stopping!\"); break\n                \n#     return targets, preds\n\n# if __name__ == \"__main__\":\n#     y_true, y_pred = train_efficientnet()\n    \n#     if y_true is not None:\n#         y_bin = (np.array(y_pred) > 0.5).astype(int)\n#         print(\"\\n📊 Final Report:\")\n#         print(classification_report(y_true, y_bin))\n        \n#         plt.figure(figsize=(6,5))\n#         sns.heatmap(confusion_matrix(y_true, y_bin), annot=True, fmt='d', cmap='Blues')\n#         plt.title(\"Confusion Matrix\")\n#         plt.xlabel(\"Predicted\"); plt.ylabel(\"Actual\")\n#         plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T17:35:57.021909Z","iopub.execute_input":"2025-11-22T17:35:57.022753Z","iopub.status.idle":"2025-11-22T17:37:00.369223Z","shell.execute_reply.started":"2025-11-22T17:35:57.022724Z","shell.execute_reply":"2025-11-22T17:37:00.368413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:26:56.542253Z","iopub.execute_input":"2025-11-22T09:26:56.542941Z","iopub.status.idle":"2025-11-22T09:26:56.626013Z","shell.execute_reply.started":"2025-11-22T09:26:56.542916Z","shell.execute_reply":"2025-11-22T09:26:56.625342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}