{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q monai\n!pip install -q timm\n!pip install -q pydicom\n\nprint(\"Libraries installed successfully!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-06T12:58:15.752529Z","iopub.execute_input":"2025-12-06T12:58:15.752709Z","iopub.status.idle":"2025-12-06T12:59:35.817658Z","shell.execute_reply.started":"2025-12-06T12:58:15.752693Z","shell.execute_reply":"2025-12-06T12:59:35.816704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pydicom\nimport numpy as np\nimport pandas as pd\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 sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\nfrom tqdm.notebook import tqdm\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport matplotlib.pyplot as plt\n\n# Define Paths\nBASE_PATH = \"/kaggle/input/rsna-intracranial-aneurysm-detection\"\nTRAIN_CSV = os.path.join(BASE_PATH, \"train.csv\")\nTRAIN_DIR = os.path.join(BASE_PATH, \"series\")\nSAVE_DIR = \"/kaggle/working/train_sequences/\"\n\nos.makedirs(SAVE_DIR, exist_ok=True)\n\ndf = pd.read_csv(TRAIN_CSV)\nprint(f\"Original Data Rows: {len(df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T13:00:00.306229Z","iopub.execute_input":"2025-12-06T13:00:00.306922Z","iopub.status.idle":"2025-12-06T13:00:11.757656Z","shell.execute_reply.started":"2025-12-06T13:00:00.306887Z","shell.execute_reply":"2025-12-06T13:00:11.757013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_scan_volume(study_id, base_dir=TRAIN_DIR):\n    \"\"\"Robust loader\"\"\"\n    study_path = os.path.join(base_dir, study_id)\n    dcm_files = glob.glob(os.path.join(study_path, \"**/*.dcm\"), recursive=True)\n    \n    if not dcm_files:\n        raise ValueError(f\"No DCM files for {study_id}\")\n        \n    raw_slices = [pydicom.dcmread(f) for f in dcm_files]\n    slices = [s for s in raw_slices if hasattr(s, 'ImagePositionPatient')]\n    \n    if not slices:\n        raise ValueError(f\"No valid slices for {study_id}\")\n\n    slices.sort(key=lambda x: float(x.ImagePositionPatient[2]))\n    \n    images = np.stack([s.pixel_array for s in slices])\n    images = images.astype(np.int16)\n\n    intercept = -1024 \n    slope = 1\n    for s in slices:\n        if hasattr(s, 'RescaleIntercept') and hasattr(s, 'RescaleSlope'):\n            intercept = s.RescaleIntercept\n            slope = s.RescaleSlope\n            break \n            \n    if slope != 1:\n        images = slope * images.astype(np.float64)\n        images = images.astype(np.int16)\n        \n    images += np.int16(intercept)\n    return images\n\ndef apply_window(volume, window_center=100, window_width=700):\n    \"\"\"Vascular Windowing\"\"\"\n    img_min = window_center - window_width // 2\n    img_max = window_center + window_width // 2\n    windowed_img = np.clip(volume, img_min, img_max)\n    return (windowed_img - img_min) / (img_max - img_min)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T13:00:44.956026Z","iopub.execute_input":"2025-12-06T13:00:44.956337Z","iopub.status.idle":"2025-12-06T13:00:44.963642Z","shell.execute_reply.started":"2025-12-06T13:00:44.956314Z","shell.execute_reply":"2025-12-06T13:00:44.96286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- CONFIGURATION ---\nSEQUENCE_LENGTH = 20  # How many slices in our \"video\"\nIMG_SIZE = 256        # Smaller size to fit sequence in GPU memory\n# ---------------------\n\n# 1. Select Patients (Balanced)\nuid_col = 'StudyInstanceUID' if 'StudyInstanceUID' in df.columns else df.columns[0]\npatient_group = df.groupby(uid_col)\n\nif 'Aneurysm Present' in df.columns:\n    patient_labels = patient_group['Aneurysm Present'].max()\nelse:\n    # Dummy labels if column missing (for testing)\n    patient_labels = pd.Series(0, index=df[uid_col].unique())\n\npos_patients = patient_labels[patient_labels == 1].index.tolist()\nneg_patients = patient_labels[patient_labels == 0].index.tolist()\n\n# Balance the dataset\ntarget_patients = np.concatenate([pos_patients, neg_patients[:len(pos_patients)]])\nnp.random.shuffle(target_patients)\n\n# 2. Processing Loop\nfile_paths = []\ntargets = []\n\nprint(f\"Processing {len(target_patients)} patients into 3D Sequences...\")\n\nfor patient in tqdm(target_patients):\n    try:\n        vol = load_scan_volume(patient)\n        vol = apply_window(vol)\n        \n        # EXTRACT CENTRAL SLAB\n        # Aneurysms are usually in the middle 50% of the scan\n        total_slices = vol.shape[0]\n        center_idx = total_slices // 2\n        half_seq = SEQUENCE_LENGTH // 2\n        \n        start = max(0, center_idx - half_seq)\n        end = min(total_slices, center_idx + half_seq)\n        \n        # Grab the chunk\n        slab = vol[start:end]\n        \n        # Pad if the scan is too small (rare, but safety first)\n        if slab.shape[0] < SEQUENCE_LENGTH:\n            padding = np.zeros((SEQUENCE_LENGTH - slab.shape[0], vol.shape[1], vol.shape[2]))\n            slab = np.concatenate([slab, padding], axis=0)\n            \n        # Resize each slice in the slab\n        # Result shape: (20, 256, 256)\n        slab_resized = []\n        for i in range(slab.shape[0]):\n            resized = cv2.resize(slab[i], (IMG_SIZE, IMG_SIZE))\n            slab_resized.append(resized)\n        \n        slab_final = np.stack(slab_resized)\n        \n        # Save as .npy\n        filename = f\"{patient}.npy\"\n        save_path = os.path.join(SAVE_DIR, filename)\n        np.save(save_path, slab_final.astype(np.float16))\n        \n        file_paths.append(save_path)\n        targets.append(patient_labels.loc[patient])\n            \n    except Exception as e:\n        pass\n\n# Create DataFrame\ntrain_df = pd.DataFrame({'filepath': file_paths, 'label': targets})\nprint(f\"Created {len(train_df)} sequence samples.\")\nprint(train_df['label'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T13:00:50.872321Z","iopub.execute_input":"2025-12-06T13:00:50.872858Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SequenceDataset(Dataset):\n    def __init__(self, dataframe, transform=None):\n        self.df = dataframe\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = row['filepath']\n        label = row['label']\n        \n        # Load the 3D Slab: (Seq_Len, H, W)\n        slab = np.load(path).astype(np.float32)\n        \n        # Apply transforms to EACH slice in the sequence\n        # We need to maintain consistency across the sequence\n        if self.transform:\n            # Albumentations expects (H, W, C), so we treat Time as Channels temporarily\n            # Shape: (H, W, Seq_Len)\n            slab_transposed = slab.transpose(1, 2, 0)\n            \n            augmented = self.transform(image=slab_transposed)\n            slab_aug = augmented['image'] # Returns Tensor (Seq, H, W)\n            \n            # Add Channel Dimension: (Seq, 1, H, W) for the CNN\n            slab_tensor = slab_aug.unsqueeze(1)\n            \n        else:\n            # Convert to tensor and add channel dim\n            slab_tensor = torch.tensor(slab).unsqueeze(1)\n            \n        return slab_tensor, torch.tensor(label, dtype=torch.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:03:35.213539Z","iopub.execute_input":"2025-12-06T11:03:35.214225Z","iopub.status.idle":"2025-12-06T11:03:36.808389Z","shell.execute_reply.started":"2025-12-06T11:03:35.214201Z","shell.execute_reply":"2025-12-06T11:03:36.807732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CNNLSTM(nn.Module):\n    def __init__(self, model_name='efficientnet_b0', pretrained=True):\n        super().__init__()\n        \n        # 1. Feature Extractor (CNN)\n        # in_chans=1 because we process grayscale slices\n        # num_classes=0 removes the final classification layer\n        self.cnn = timm.create_model(model_name, pretrained=pretrained, in_chans=1, num_classes=0)\n        \n        # Get output size of CNN (e.g., 1280 for B0)\n        cnn_out_size = self.cnn.num_features\n        \n        # 2. Sequence Modeler (LSTM)\n        self.lstm = nn.LSTM(\n            input_size=cnn_out_size, \n            hidden_size=256, \n            num_layers=2, \n            batch_first=True, \n            bidirectional=True\n        )\n        \n        # 3. Classifier Head\n        # Input is 256*2 because of bidirectional LSTM\n        self.fc = nn.Linear(256 * 2, 1)\n        \n    def forward(self, x):\n        # x shape: (Batch, Seq_Len, 1, Height, Width)\n        b, seq, c, h, w = x.size()\n        \n        # FLATTEN FOR CNN\n        # The CNN expects (Total_Images, 1, H, W)\n        # We merge Batch and Sequence dimensions\n        x_flat = x.view(b * seq, c, h, w)\n        \n        # Extract Features\n        features = self.cnn(x_flat) # Shape: (Batch*Seq, cnn_out_size)\n        \n        # RESHAPE FOR LSTM\n        # Shape: (Batch, Seq_Len, cnn_out_size)\n        features_seq = features.view(b, seq, -1)\n        \n        # Run LSTM\n        lstm_out, _ = self.lstm(features_seq)\n        \n        # Take the feature from the LAST time step\n        # (Or you could use max pooling across time)\n        last_hidden = lstm_out[:, -1, :]\n        \n        # Classify\n        output = self.fc(last_hidden)\n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:05:05.752789Z","iopub.execute_input":"2025-12-06T11:05:05.7533Z","iopub.status.idle":"2025-12-06T11:05:05.757788Z","shell.execute_reply.started":"2025-12-06T11:05:05.753278Z","shell.execute_reply":"2025-12-06T11:05:05.757158Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device, scaler):\n    model.train()\n    running_loss = 0.0\n    all_targets = []\n    all_preds = []\n    \n    pbar = tqdm(loader, desc=\"Training\")\n    \n    for sequences, labels in pbar:\n        # sequences shape: (Batch, 20, 1, 256, 256)\n        sequences = sequences.to(device)\n        labels = labels.to(device).unsqueeze(1)\n        \n        optimizer.zero_grad()\n        \n        with autocast():\n            outputs = model(sequences)\n            loss = criterion(outputs, labels)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item()\n        \n        all_targets.extend(labels.cpu().detach().numpy())\n        all_preds.extend(torch.sigmoid(outputs).cpu().detach().numpy())\n        pbar.set_postfix(loss=loss.item())\n        \n    epoch_loss = running_loss / len(loader)\n    epoch_auc = roc_auc_score(all_targets, all_preds) if len(set(all_targets)) > 1 else 0.5\n    return epoch_loss, epoch_auc\n\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_targets = []\n    all_preds = []\n    \n    with torch.no_grad():\n        for sequences, labels in tqdm(loader, desc=\"Validating\"):\n            sequences = sequences.to(device)\n            labels = labels.to(device).unsqueeze(1)\n            \n            with autocast():\n                outputs = model(sequences)\n                loss = criterion(outputs, labels)\n            \n            running_loss += loss.item()\n            all_targets.extend(labels.cpu().numpy())\n            all_preds.extend(torch.sigmoid(outputs).cpu().numpy())\n            \n    epoch_loss = running_loss / len(loader)\n    epoch_auc = roc_auc_score(all_targets, all_preds) if len(set(all_targets)) > 1 else 0.5\n    return epoch_loss, epoch_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:05:18.650897Z","iopub.execute_input":"2025-12-06T11:05:18.651414Z","iopub.status.idle":"2025-12-06T11:05:18.664639Z","shell.execute_reply.started":"2025-12-06T11:05:18.651389Z","shell.execute_reply":"2025-12-06T11:05:18.664039Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- HYPERPARAMETERS ---\nBATCH_SIZE = 8       # Smaller batch because sequences take more VRAM\nLEARNING_RATE = 1e-4\nEPOCHS = 10\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# --- TRANSFORMS ---\n# Note: Albumentations will apply same transform to all slices in seq\ntrain_transforms = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.Rotate(limit=10, p=0.5),\n    A.Normalize(mean=(0.5,), std=(0.5,)), # Grayscale normalization\n    ToTensorV2()\n])\n\nval_transforms = A.Compose([\n    A.Normalize(mean=(0.5,), std=(0.5,)),\n    ToTensorV2()\n])\n\n# Split & Loaders\ntrain_sub, val_sub = train_test_split(train_df, test_size=0.2, stratify=train_df['label'], random_state=42)\n\ntrain_ds = SequenceDataset(train_sub, transform=train_transforms)\nval_ds = SequenceDataset(val_sub, transform=val_transforms)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\n# Init Model\nmodel = CNNLSTM(model_name='efficientnet_b0').to(DEVICE)\noptimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE)\ncriterion = nn.BCEWithLogitsLoss()\nscaler = GradScaler()\n\nprint(f\"Starting CNN+LSTM Training on {DEVICE}...\")\n\nfor epoch in range(EPOCHS):\n    print(f\"\\n--- Epoch {epoch+1}/{EPOCHS} ---\")\n    \n    t_loss, t_auc = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE, scaler)\n    v_loss, v_auc = validate(model, val_loader, criterion, DEVICE)\n    \n    print(f\"Train Loss: {t_loss:.4f} | Train AUC: {t_auc:.4f}\")\n    print(f\"Val Loss:   {v_loss:.4f} | Val AUC:   {v_auc:.4f}\")\n    \n    torch.save(model.state_dict(), \"last_model_lstm.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T11:06:55.359345Z","iopub.execute_input":"2025-12-06T11:06:55.360123Z","iopub.status.idle":"2025-12-06T11:11:21.901912Z","shell.execute_reply.started":"2025-12-06T11:06:55.360097Z","shell.execute_reply":"2025-12-06T11:11:21.901105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_inference(model, loader, device):\n    model.eval()\n    all_preds = []\n    all_labels = []\n    \n    with torch.no_grad():\n        for sequences, labels in tqdm(loader):\n            sequences = sequences.to(device)\n            with autocast():\n                outputs = model(sequences)\n                probs = torch.sigmoid(outputs)\n            \n            all_preds.extend(probs.cpu().numpy().flatten())\n            all_labels.extend(labels.numpy().flatten())\n            \n    return all_preds, all_labels\n\n# Run on Val set\npreds, actuals = run_inference(model, val_loader, DEVICE)\n\nresults_df = pd.DataFrame({\n    'filepath': val_sub['filepath'].values,\n    'actual_label': actuals,\n    'predicted_prob': preds\n})\n\nresults_df.to_csv(\"submission_lstm.csv\", index=False)\nresults_df.head()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- INFERENCE CONFIGURATION ---\nTEST_BATCH_SIZE = 16\nMODEL_PATH = \"best_model_b4.pth\"\nOUTPUT_CSV = \"submission.csv\"\n\ndef run_inference(model, loader, device):\n    model.eval()\n    all_preds = []\n    all_labels = []\n    all_paths = []\n    \n    print(\"Running Inference...\")\n    with torch.no_grad():\n        for images, labels in tqdm(loader):\n            images = images.to(device)\n            \n            # Use Mixed Precision for speed\n            with torch.cuda.amp.autocast():\n                outputs = model(images)\n                probs = torch.sigmoid(outputs)\n            \n            all_preds.extend(probs.cpu().numpy().flatten())\n            all_labels.extend(labels.numpy().flatten())\n            \n            # We need to track which image is which, but the loader \n            # implies the order matches the dataset. \n            # (Ideally, custom datasets return IDs, but we can map back via index)\n            \n    return all_preds, all_labels\n\n# 1. Load the Best Model\nmodel = AneurysmClassifier(model_name='efficientnet_b4', pretrained=False)\nmodel.load_state_dict(torch.load(MODEL_PATH))\nmodel.to(DEVICE)\nprint(\"Best model loaded.\")\n\n# 2. Run Inference on Validation Set\n# (We reuse val_loader from the training block)\npreds, actuals = run_inference(model, val_loader, DEVICE)\n\n# 3. Save Results\n# Map predictions back to filepaths\nval_paths = val_sub['filepath'].values\n\nresults_df = pd.DataFrame({\n    'filepath': val_paths,\n    'actual_label': actuals,\n    'predicted_prob': preds\n})\n\nresults_df.to_csv(OUTPUT_CSV, index=False)\nprint(f\"Predictions saved to {OUTPUT_CSV}\")\nresults_df.head()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_errors(df, num_samples=5):\n    # Calculate Error (Absolute difference between truth and prediction)\n    df['error'] = abs(df['actual_label'] - df['predicted_prob'])\n    \n    # Sort by worst errors\n    worst_errors = df.sort_values('error', ascending=False).head(num_samples)\n    \n    plt.figure(figsize=(15, 5 * num_samples))\n    \n    for i, (_, row) in enumerate(worst_errors.iterrows()):\n        path = row['filepath']\n        true_label = int(row['actual_label'])\n        pred_prob = row['predicted_prob']\n        \n        # Load image\n        img = cv2.imread(path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        plt.subplot(num_samples, 1, i + 1)\n        plt.imshow(img)\n        plt.title(f\"True: {true_label} | Pred: {pred_prob:.4f} | Error: {row['error']:.4f}\\nFile: {os.path.basename(path)}\")\n        plt.axis('off')\n        \n    plt.tight_layout()\n    plt.show()\n\nprint(\"--- WORST MODEL FAILURES ---\")\nvisualize_errors(results_df, num_samples=5)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- DEPLOYMENT PIPELINE ---\n\ndef diagnose_patient(patient_id, model, device, base_dir=TRAIN_DIR):\n    \"\"\"\n    End-to-End pipeline: \n    Raw DICOM Folder -> 3D Volume -> 2.5D Slices -> Model -> Diagnosis\n    \"\"\"\n    print(f\"Diagnosing Patient: {patient_id}...\")\n    \n    try:\n        # 1. Load 3D Data\n        volume = load_scan_volume(patient_id, base_dir)\n        volume = apply_window(volume)\n        \n        # 2. Prepare for Model\n        model.eval()\n        model.to(device)\n        \n        # We scan the middle 70% of the brain\n        start = int(volume.shape[0] * 0.15)\n        end = int(volume.shape[0] * 0.85)\n        stride = 2 \n        \n        batch_images = []\n        \n        # Create a batch of 2.5D slices\n        for i in range(start, end, stride):\n            img_25d = make_25d_slice(volume, i)\n            \n            # Apply same transforms as validation (Resize + Normalize)\n            # We need to manually convert to format Albumentations expects (uint8 0-255)\n            img_uint8 = (img_25d * 255).astype(np.uint8)\n            \n            # Apply Albumentations (val_transforms defined in Cell 8)\n            augmented = val_transforms(image=img_uint8)\n            tensor = augmented['image']\n            batch_images.append(tensor)\n            \n        if not batch_images:\n            return 0.0, \"Scan Error (No slices)\"\n\n        # Stack into a batch tensor\n        batch_tensor = torch.stack(batch_images).to(device)\n        \n        # 3. Predict\n        with torch.no_grad():\n            with torch.cuda.amp.autocast():\n                outputs = model(batch_tensor)\n                probs = torch.sigmoid(outputs)\n        \n        # 4. Aggregation Logic\n        # If ANY slice has a high probability (> 90%), we flag the patient.\n        # Otherwise, we take the average of the top 5 suspicious slices.\n        all_scores = probs.cpu().numpy().flatten()\n        max_score = np.max(all_scores)\n        top_5_avg = np.mean(np.sort(all_scores)[-5:])\n        \n        risk_score = max_score # You can tune this logic\n        \n        return risk_score, \"High Risk\" if risk_score > 0.5 else \"Low Risk\"\n\n    except Exception as e:\n        return 0.0, f\"Error: {e}\"\n\n# --- TEST THE DEPLOYMENT ---\n# Pick a random patient from our dataframe\ntest_patient_id = train_df.iloc[0]['filepath'].split('/')[-1].split('_')[0]\n\nrisk, status = diagnose_patient(test_patient_id, model, DEVICE)\n\nprint(f\"\\nFINAL DIAGNOSIS for {test_patient_id}\")\nprint(f\"Risk Score: {risk:.4f}\")\nprint(f\"Status:     {status}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}