{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":29653,"databundleVersionId":2420395,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport glob\nimport pandas as pd\nimport numpy as np\nimport pydicom\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings('ignore')\n\n\n\nBASE_PATH = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification'\nif not os.path.exists(BASE_PATH):\n    \n    BASE_PATH = '/kaggle/input/competitions/rsna-miccai-brain-tumor-radiogenomic-classification'\n\nTRAIN_DIR = os.path.join(BASE_PATH, 'train')\nTRAIN_LABELS = os.path.join(BASE_PATH, 'train_labels.csv')\n\n\ndf = pd.read_csv(TRAIN_LABELS)\nprint(f\"Original dataset size: {len(df)} patients\")\n\n# Remove known bad cases per Kaggle rules\nbad_cases = [109, 123, 709]\ndf = df[~df['BraTS21ID'].isin(bad_cases)].reset_index(drop=True)\nprint(f\"Dataset size after removing bad cases: {len(df)} patients\")\n\n# Format IDs to 5-digit strings to match folder names (e.g., 0 -> '00000')\ndf['folder_id'] = df['BraTS21ID'].apply(lambda x: str(x).zfill(5))\n\n\nplt.figure(figsize=(6, 4))\nsns.countplot(data=df, x='MGMT_value', palette='viridis')\nplt.title(\"Class Distribution: MGMT Methylation\")\nplt.xlabel(\"MGMT_value (0 = Unmethylated, 1 = Methylated)\")\nplt.ylabel(\"Patient Count\")\nplt.show()\n\nprint(\"\\nClass Counts:\")\nprint(df['MGMT_value'].value_counts(normalize=True) * 100)\n\n\nprint(\"\\nScanning slice counts for a sample of 20 patients...\")\nsample_df = df.sample(20, random_state=42)\nmodalities = ['FLAIR', 'T1w', 'T1wCE', 'T2w']\nslice_counts = {mod: [] for mod in modalities}\n\nfor folder_id in sample_df['folder_id']:\n    for mod in modalities:\n        mod_path = os.path.join(TRAIN_DIR, folder_id, mod)\n        num_slices = len(glob.glob(os.path.join(mod_path, '*.dcm')))\n        slice_counts[mod].append(num_slices)\n\n\nfor mod in modalities:\n    counts = slice_counts[mod]\n    print(f\"{mod} - Min slices: {min(counts)}, Max slices: {max(counts)}, Avg slices: {np.mean(counts):.1f}\")\n\n\ndef plot_patient_scans(patient_id):\n    patient_dir = os.path.join(TRAIN_DIR, patient_id)\n    fig, axes = plt.subplots(1, 4, figsize=(20, 5))\n    fig.suptitle(f\"Patient {patient_id} - Middle Slices for Each Modality\", fontsize=16)\n    \n    for i, mod in enumerate(modalities):\n        mod_path = os.path.join(patient_dir, mod)\n        # Get all dicom files and sort them numerically by Image-X.dcm\n        dicom_files = sorted(glob.glob(os.path.join(mod_path, '*.dcm')), \n                             key=lambda x: int(os.path.basename(x).split('-')[1].split('.')[0]))\n        \n        if len(dicom_files) > 0:\n            # Pick the middle slice (where the brain is usually most visible)\n            middle_idx = len(dicom_files) // 2\n            dcm = pydicom.dcmread(dicom_files[middle_idx])\n            image = dcm.pixel_array\n            \n            axes[i].imshow(image, cmap='gray')\n            axes[i].set_title(f\"{mod} (Slice {middle_idx}/{len(dicom_files)})\")\n            axes[i].axis('off')\n        else:\n            axes[i].set_title(f\"{mod} (Missing)\")\n            axes[i].axis('off')\n            \n    plt.show()\n\n# Visualize a random patient from the dataset\nrandom_patient = df['folder_id'].iloc[0]\nplot_patient_scans(random_patient)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T08:35:41.305788Z","iopub.execute_input":"2026-04-26T08:35:41.306046Z","iopub.status.idle":"2026-04-26T08:35:49.732772Z","shell.execute_reply.started":"2026-04-26T08:35:41.306022Z","shell.execute_reply":"2026-04-26T08:35:49.731597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install monai\nimport os\nimport glob\nimport pydicom\nimport numpy as np\nimport cv2\nimport pandas as pd\nfrom tqdm import tqdm\nfrom joblib import Parallel, delayed\n\n\nBASE_PATH = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification'\nif not os.path.exists(BASE_PATH):\n    BASE_PATH = '/kaggle/input/competitions/rsna-miccai-brain-tumor-radiogenomic-classification'\n\nTRAIN_DIR = os.path.join(BASE_PATH, 'train')\nTRAIN_LABELS = os.path.join(BASE_PATH, 'train_labels.csv')\n\n# Create a folder in Kaggle's working directory to store the fast-loading numpy arrays\nSAVE_DIR = '/kaggle/working/processed_train_3d'\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n# Target 3D Volume Shape: Depth (Z) = 64, Height (Y) = 128, Width (X) = 128\nZ_DIM, Y_DIM, X_DIM = 64, 128, 128\nMODALITIES = ['FLAIR', 'T1w', 'T1wCE', 'T2w']\n\n\ndf = pd.read_csv(TRAIN_LABELS)\nbad_cases = [109, 123, 709]\ndf = df[~df['BraTS21ID'].isin(bad_cases)].reset_index(drop=True)\ndf['folder_id'] = df['BraTS21ID'].apply(lambda x: str(x).zfill(5))\n\n\ndef load_and_resize_modality(patient_dir, modality):\n    \"\"\"Loads all DICOMs for one modality, resizes 2D slices, and samples Z-axis to fixed depth.\"\"\"\n    mod_path = os.path.join(patient_dir, modality)\n    dicom_files = sorted(glob.glob(os.path.join(mod_path, '*.dcm')), \n                         key=lambda x: int(os.path.basename(x).split('-')[1].split('.')[0]))\n    \n    if len(dicom_files) == 0:\n        # If a modality is missing completely, return an empty zero array\n        return np.zeros((Z_DIM, Y_DIM, X_DIM), dtype=np.float32)\n    \n    # 1. Sample the Z-axis (Depth) evenly to reach exactly Z_DIM (64 slices)\n    # This is MUCH faster than mathematical 3D interpolation and preserves anatomy\n    indices = np.linspace(0, len(dicom_files) - 1, Z_DIM).astype(int)\n    sampled_files = [dicom_files[i] for i in indices]\n    \n    volume3d = []\n    for filepath in sampled_files:\n        dicom = pydicom.dcmread(filepath)\n        img = dicom.pixel_array.astype(np.float32)\n        \n        # 2. Resize 2D slice to 128x128\n        if img.shape != (Y_DIM, X_DIM):\n            img = cv2.resize(img, (X_DIM, Y_DIM), interpolation=cv2.INTER_LINEAR)\n        \n        volume3d.append(img)\n        \n    volume3d = np.array(volume3d) # Shape: (64, 128, 128)\n    \n    # 3. Normalize pixel values to [0, 1] for this specific volume\n    if np.max(volume3d) > 0:\n        volume3d = volume3d / np.max(volume3d)\n        \n    return volume3d\n\ndef process_patient(folder_id):\n    \"\"\"Processes all 4 modalities for a single patient and saves as .npy\"\"\"\n    patient_dir = os.path.join(TRAIN_DIR, folder_id)\n    \n    # We will stack the modalities as channels: (4, 64, 128, 128)\n    patient_volume = []\n    \n    for mod in MODALITIES:\n        mod_vol = load_and_resize_modality(patient_dir, mod)\n        patient_volume.append(mod_vol)\n        \n    patient_volume = np.stack(patient_volume, axis=0) # Shape: (4, 64, 128, 128)\n    \n    \n    save_path = os.path.join(SAVE_DIR, f\"{folder_id}.npy\")\n    np.save(save_path, patient_volume.astype(np.float16)) # Float16 saves disk space & RAM!\n\n\nprint(f\"Starting conversion for {len(df)} patients...\")\nprint(\"Converting 3D volumes to shape: (4 Channels, 64 Depth, 128 Height, 128 Width)\")\n\n\n_ = Parallel(n_jobs=-1)(delayed(process_patient)(folder_id) for folder_id in tqdm(df['folder_id'], total=len(df)))\n\nprint(\"\\nProcessing Complete! Checking output...\")\nsample_files = os.listdir(SAVE_DIR)\nprint(f\"Successfully saved {len(sample_files)} patient files to {SAVE_DIR}.\")\n\n\nif len(sample_files) > 0:\n    sample_npy = np.load(os.path.join(SAVE_DIR, sample_files[0]))\n    print(f\"Sample Array Shape: {sample_npy.shape} | Data Type: {sample_npy.dtype}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T08:55:10.067180Z","iopub.execute_input":"2026-04-26T08:55:10.067928Z","iopub.status.idle":"2026-04-26T09:03:39.290087Z","shell.execute_reply.started":"2026-04-26T08:55:10.067892Z","shell.execute_reply":"2026-04-26T09:03:39.289275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport time\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 torch.optim.lr_scheduler import CosineAnnealingLR\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import f1_score, accuracy_score\nfrom monai.networks.nets import DenseNet121\n\n# --- 1. Configurations ---\nSAVE_DIR = '/kaggle/working/processed_train_3d'\nTRAIN_LABELS = '/kaggle/input/rsna-miccai-brain-tumor-radiogenomic-classification/train_labels.csv'\n\n# Fallback path if the first one doesn't exist\nif not os.path.exists(TRAIN_LABELS):\n    TRAIN_LABELS = '/kaggle/input/competitions/rsna-miccai-brain-tumor-radiogenomic-classification/train_labels.csv'\n\nBATCH_SIZE = 4  # Lowered to 4 to ensure DenseNet fits in T4 GPU Memory\nEPOCHS = 15\nLEARNING_RATE = 3e-4\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(f\"Training on Device: {DEVICE}\")\n\n# --- 2. Prepare Data & Split ---\ndf = pd.read_csv(TRAIN_LABELS)\nbad_cases = [109, 123, 709] # Remove corrupted cases\ndf = df[~df['BraTS21ID'].isin(bad_cases)].reset_index(drop=True)\ndf['folder_id'] = df['BraTS21ID'].apply(lambda x: str(x).zfill(5))\n\n# Stratified Train/Val Split (80% Train, 20% Val)\ntrain_df, val_df = train_test_split(df, test_size=0.2, stratify=df['MGMT_value'], random_state=42)\nprint(f\"Training Samples: {len(train_df)} | Validation Samples: {len(val_df)}\")\n\n# --- 3. Custom PyTorch Dataset ---\nclass BrainMRIDataset(Dataset):\n    def __init__(self, dataframe, data_dir, is_train=True):\n        self.df = dataframe.reset_index(drop=True)\n        self.data_dir = data_dir\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        folder_id = self.df.loc[idx, 'folder_id']\n        label = self.df.loc[idx, 'MGMT_value']\n        \n        # Load the pre-processed numpy array\n        npy_path = os.path.join(self.data_dir, f\"{folder_id}.npy\")\n        volume = np.load(npy_path).astype(np.float32) # Convert float16 back to float32\n        \n        # Simple Data Augmentation for training\n        if self.is_train and np.random.rand() > 0.5:\n            axis = np.random.choice([1, 2, 3]) # Flip along Z, Y, or X axis\n            volume = np.flip(volume, axis=axis).copy()\n            \n        return torch.tensor(volume), torch.tensor([label], dtype=torch.float32)\n\ntrain_dataset = BrainMRIDataset(train_df, SAVE_DIR, is_train=True)\nval_dataset = BrainMRIDataset(val_df, SAVE_DIR, is_train=False)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\n# --- 4. EMA (Exponential Moving Average) ---\nclass EMA:\n    \"\"\"Maintains a shadow copy of weights for better generalization\"\"\"\n    def __init__(self, model, decay=0.999):\n        self.model = model\n        self.decay = decay\n        self.shadow = {k: v.clone().detach() for k, v in model.state_dict().items()}\n\n    def update(self):\n        with torch.no_grad():\n            for k, v in self.model.state_dict().items():\n                self.shadow[k] = self.shadow[k] * self.decay + v * (1.0 - self.decay)\n\n    def apply_shadow(self):\n        self.model.load_state_dict(self.shadow)\n\n# --- 5. Model, Loss, Optimizer, Scheduler ---\n# 3D DenseNet121 from MONAI\nmodel = DenseNet121(spatial_dims=3, in_channels=4, out_channels=1).to(DEVICE)\nema = EMA(model)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-2)\n\n# Cosine Annealing Scheduler: Slowly drops LR to prevent jumping out of good solutions\nscheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)\n\n# --- 6. Training Loop ---\nbest_val_acc = 0.0\nbest_val_f1 = 0.0\n\nprint(\"\\nStarting Training with 3D DenseNet121 & Cosine LR Scheduler...\")\nprint(\"-\" * 85)\nprint(f\"{'Epoch':<6} | {'LR':<10} | {'Train Loss':<12} | {'Val Loss':<10} | {'Val Acc':<9} | {'Val F1':<8}\")\nprint(\"-\" * 85)\n\nfor epoch in range(EPOCHS):\n    start_time = time.time()\n    \n    # -- Training Phase --\n    model.train()\n    train_loss = 0.0\n    for inputs, labels in train_loader:\n        inputs, labels = inputs.to(DEVICE), labels.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model(inputs)\n        loss = criterion(outputs, labels)\n        \n        loss.backward()\n        optimizer.step()\n        ema.update() # Update EMA weights\n        \n        train_loss += loss.item() * inputs.size(0)\n        \n    train_loss /= len(train_loader.dataset)\n    \n    # -- Step the Scheduler --\n    current_lr = optimizer.param_groups[0]['lr']\n    scheduler.step()\n    \n    # -- Validation Phase --\n    model.eval()\n    val_loss = 0.0\n    all_preds = []\n    all_targets = []\n    \n    with torch.no_grad():\n        for inputs, labels in val_loader:\n            inputs, labels = inputs.to(DEVICE), labels.to(DEVICE)\n            \n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * inputs.size(0)\n            \n            # Apply Sigmoid and threshold at 0.5\n            preds = torch.sigmoid(outputs).cpu().numpy() > 0.5\n            all_preds.extend(preds)\n            all_targets.extend(labels.cpu().numpy())\n            \n    val_loss /= len(val_loader.dataset)\n    val_acc = accuracy_score(all_targets, all_preds)\n    val_f1 = f1_score(all_targets, all_preds, zero_division=0)\n    \n    print(f\"{epoch+1:<6} | {current_lr:<10.6f} | {train_loss:<12.4f} | {val_loss:<10.4f} | {val_acc:<9.4f} | {val_f1:<8.4f}\")\n    \n    # Save best model based on Accuracy\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        best_val_f1 = val_f1\n        ema.apply_shadow() # Save the smooth EMA weights\n        torch.save(model.state_dict(), \"/kaggle/working/best_densenet_ema.pth\")\n\nprint(\"-\" * 85)\nprint(f\"Training Complete! Best Validation Accuracy: {best_val_acc:.4f} | Best F1: {best_val_f1:.4f}\")\nprint(\"Model saved to: /kaggle/working/best_densenet_ema.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T09:31:52.296480Z","iopub.execute_input":"2026-04-26T09:31:52.297340Z","iopub.status.idle":"2026-04-26T09:44:06.280382Z","shell.execute_reply.started":"2026-04-26T09:31:52.297301Z","shell.execute_reply":"2026-04-26T09:44:06.279511Z"}},"outputs":[],"execution_count":null}]}