{"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":"gpu","dataSources":[{"sourceId":36363,"databundleVersionId":4050810,"sourceType":"competition"},{"sourceId":4264054,"sourceType":"datasetVersion","datasetId":2406209}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"133e18fa","cell_type":"markdown","source":"# RSNA 2022 Cervical Spine Fracture Detection\n## 3D Volume Analysis with EfficientNet\n\n**Project Overview:**\n- Convert 2D DICOM slices → 3D volumes\n- Detect cervical fractures (C1-C7) using EfficientNet\n- Multi-label classification approach\n\n**Architecture:** 2.5D EfficientNetB3 with temporal aggregation","metadata":{}},{"id":"43f8ec89","cell_type":"markdown","source":"---\n## Step 1: Install Dependencies\n\nRun this cell, then **restart kernel** (press 0 twice on Kaggle)","metadata":{}},{"id":"56bbbf21","cell_type":"code","source":"# ============================================================\n# INSTALLATION - Run once, then restart kernel\n# ============================================================\nimport subprocess, sys\n\nprint(\"Step 1/4: Fixing numpy compatibility...\")\nsubprocess.run([sys.executable, '-m', 'pip', 'install', '-q', '--force-reinstall', \n                'numpy==1.26.4'], capture_output=True)\nprint(\"✓ Numpy 1.26.4 installed\")\n\nprint(\"Step 2/4: Rebuilding scientific packages...\")\npackages_rebuild = ['scikit-learn', 'scipy', 'matplotlib', 'pandas', 'opencv-python']\nfor pkg in packages_rebuild:\n    print(f\"  Rebuilding {pkg}...\")\n    subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', '--force-reinstall', \n                    '--no-cache-dir', pkg], capture_output=True)\nprint(\"✓ Scientific packages rebuilt\")\n\nprint(\"Step 3/4: Installing DICOM packages...\")\npackages_dicom = ['pylibjpeg', 'pylibjpeg-libjpeg', 'pylibjpeg-openjpeg', 'python-gdcm', 'nibabel']\nfor pkg in packages_dicom:\n    subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', pkg], capture_output=True)\nprint(\"✓ DICOM packages installed\")\n\nprint(\"Step 4/4: Verifying installation...\")\ntry:\n    import numpy as np\n    print(f\"  numpy: {np.__version__}\")\nexcept:\n    print(\"  ⚠️ numpy import failed\")\n\nprint('\\n' + '='*50)\nprint('✅ INSTALLATION COMPLETE!')\nprint('='*50)\nprint('\\n🔄 NOW RESTART KERNEL:')\nprint('   Kaggle: Press 0 twice')\nprint('   Colab: Runtime → Restart runtime')\nprint('\\n⏭️  After restart: Skip this cell, run Step 2')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T14:57:32.666716Z","iopub.execute_input":"2026-01-14T14:57:32.667002Z","iopub.status.idle":"2026-01-14T14:59:40.313661Z","shell.execute_reply.started":"2026-01-14T14:57:32.666961Z","shell.execute_reply":"2026-01-14T14:59:40.312861Z"}},"outputs":[],"execution_count":null},{"id":"10b4d8da","cell_type":"markdown","source":"---\n## Step 2: Import Libraries & Configure\n\n**⚠️ START HERE after kernel restart!**","metadata":{}},{"id":"64f63a1d","cell_type":"code","source":"# ============================================================\n# CRITICAL: Set environment BEFORE importing TensorFlow\n# ============================================================\nimport os\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\nos.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'\n\n# ============================================================\n# IMPORTS\n# ============================================================\nimport numpy as np\nimport pandas as pd\nimport cv2\nimport glob\nimport gc\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# DICOM\nimport pydicom\ntry:\n    import pylibjpeg\n    print('✓ DICOM decompression libraries ready')\nexcept ImportError:\n    raise ImportError('Run Step 1 first, then restart kernel!')\n\n# Segmentation\ntry:\n    import nibabel as nib\n    NIBABEL_OK = True\n    print('✓ Segmentation (nibabel) ready')\nexcept ImportError:\n    NIBABEL_OK = False\n    print('⚠️ nibabel not found - segmentation disabled')\n\n# TensorFlow - with memory management\nimport tensorflow as tf\n\n# CRITICAL: Limit GPU memory growth to prevent OOM\ngpus = tf.config.list_physical_devices('GPU')\nif gpus:\n    try:\n        for gpu in gpus:\n            tf.config.experimental.set_memory_growth(gpu, True)\n        print(f'✓ GPU memory growth enabled for {len(gpus)} GPU(s)')\n    except RuntimeError as e:\n        print(f'⚠️ GPU config error: {e}')\n\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.applications import EfficientNetB0  # Smallest variant\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau\n\n# Sklearn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\n\n# Scipy\nfrom scipy import ndimage\n\n# Visualization\nimport matplotlib.pyplot as plt\n\n# ============================================================\n# CONFIGURATION - ULTRA MEMORY-EFFICIENT\n# ============================================================\n# Kaggle paths\nif os.path.exists('/kaggle/input'):\n    for d in os.listdir('/kaggle/input'):\n        if 'rsna' in d.lower():\n            base = f'/kaggle/input/{d}'\n            if os.path.exists(f'{base}/train_images'):\n                KAGGLE_INPUT = base\n                break\n    else:\n        KAGGLE_INPUT = '/kaggle/input/rsna-2022-cervical-spine-fracture-detection'\nelse:\n    KAGGLE_INPUT = '/kaggle/input/rsna-2022-cervical-spine-fracture-detection'\n\nTRAIN_IMAGES = f'{KAGGLE_INPUT}/train_images'\nTRAIN_CSV = f'{KAGGLE_INPUT}/train.csv'\nSEGMENTATIONS_DIR = f'{KAGGLE_INPUT}/segmentations'\nBBOXES_CSV = f'{KAGGLE_INPUT}/train_bounding_boxes.csv'\n\n# ULTRA LOW MEMORY SETTINGS\nIMG_SIZE = 96           # Reduced from 128\nNUM_SLICES = 8          # Reduced from 16\nBATCH_SIZE = 1          # Minimum batch size\nEPOCHS = 10\nLEARNING_RATE = 1e-4\n\n# Feature flags\nUSE_SEGMENTATION = NIBABEL_OK and os.path.exists(SEGMENTATIONS_DIR)\nUSE_BBOXES = os.path.exists(BBOXES_CSV)\n\n# Seeds\nnp.random.seed(42)\ntf.random.set_seed(42)\n\n# Clear memory\ngc.collect()\ntf.keras.backend.clear_session()\n\n# ============================================================\n# VERIFY SETUP\n# ============================================================\nprint('\\n' + '='*50)\nprint('ENVIRONMENT (ULTRA LOW MEMORY MODE)')\nprint('='*50)\nprint(f'TensorFlow: {tf.__version__}')\nprint(f'GPU: {len(tf.config.list_physical_devices(\"GPU\"))} available')\nprint(f'Dataset: {os.path.exists(TRAIN_CSV)}')\nprint(f'Image size: {IMG_SIZE}x{IMG_SIZE}')\nprint(f'Slices: {NUM_SLICES}')\nprint(f'Batch size: {BATCH_SIZE}')\nprint('='*50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:08.586319Z","iopub.execute_input":"2026-01-14T15:00:08.586868Z","iopub.status.idle":"2026-01-14T15:00:31.374562Z","shell.execute_reply.started":"2026-01-14T15:00:08.586844Z","shell.execute_reply":"2026-01-14T15:00:31.373927Z"}},"outputs":[],"execution_count":null},{"id":"920082b1","cell_type":"markdown","source":"---\n## Step 3: Load Dataset","metadata":{}},{"id":"a92d3bd6","cell_type":"code","source":"# ============================================================\n# LOAD DATA\n# ============================================================\ntrain_df = pd.read_csv(TRAIN_CSV)\n\n# Fracture columns\nFRACTURE_COLS = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7']\n\n# Add patient_overall if missing\nif 'patient_overall' not in train_df.columns:\n    train_df['patient_overall'] = train_df[FRACTURE_COLS].max(axis=1)\n\n# Label columns for model\nLABEL_COLS = FRACTURE_COLS + ['patient_overall']\n\nprint(f'Dataset: {len(train_df)} patients')\nprint(f'\\nFracture distribution:')\nfor col in LABEL_COLS:\n    n = train_df[col].sum()\n    pct = 100 * n / len(train_df)\n    print(f'  {col:15s}: {n:4d} ({pct:5.1f}%)')\n\n# Load bounding boxes\nbboxes_df = pd.read_csv(BBOXES_CSV) if USE_BBOXES else None\nif bboxes_df is not None:\n    print(f'\\nBounding boxes: {len(bboxes_df)} annotations')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:31.375719Z","iopub.execute_input":"2026-01-14T15:00:31.376194Z","iopub.status.idle":"2026-01-14T15:00:31.402926Z","shell.execute_reply.started":"2026-01-14T15:00:31.376174Z","shell.execute_reply":"2026-01-14T15:00:31.402218Z"}},"outputs":[],"execution_count":null},{"id":"d5504aa8","cell_type":"markdown","source":"---\n## Step 4: DICOM Loading Functions","metadata":{}},{"id":"d4cd9ebe","cell_type":"code","source":"# ============================================================\n# DICOM LOADING WITH CACHING\n# ============================================================\n_cache = {}  # Global cache\n\ndef load_dicom_volume(study_id, num_slices=NUM_SLICES):\n    \"\"\"\n    Load DICOM series and create 3D volume.\n    Returns: (H, W, D) numpy array or None\n    \"\"\"\n    # Check cache\n    if study_id in _cache:\n        return _cache[study_id].copy()\n    \n    patient_dir = os.path.join(TRAIN_IMAGES, str(study_id))\n    if not os.path.exists(patient_dir):\n        return None\n    \n    # Get DICOM files\n    dcm_files = glob.glob(os.path.join(patient_dir, '**/*.dcm'), recursive=True)\n    if not dcm_files:\n        return None\n    \n    # Load slices\n    slices = []\n    for f in dcm_files:\n        try:\n            ds = pydicom.dcmread(f)\n            arr = ds.pixel_array.astype(np.float32)\n            pos = float(ds.ImagePositionPatient[2]) if hasattr(ds, 'ImagePositionPatient') else 0\n            slices.append((pos, arr))\n        except:\n            continue\n    \n    if len(slices) < 5:\n        return None\n    \n    # Sort by position\n    slices.sort(key=lambda x: x[0])\n    \n    # Stack into volume\n    arrays = [s[1] for s in slices]\n    volume = np.stack(arrays, axis=-1)\n    \n    # Resample to target slices\n    if volume.shape[2] != num_slices:\n        indices = np.linspace(0, volume.shape[2]-1, num_slices, dtype=int)\n        volume = volume[:, :, indices]\n    \n    # Cache (limit size)\n    if len(_cache) < 100:\n        _cache[study_id] = volume.copy()\n    \n    return volume\n\ndef load_segmentation(study_id):\n    \"\"\"Load NIfTI segmentation mask.\"\"\"\n    if not USE_SEGMENTATION:\n        return None\n    \n    for ext in ['.nii', '.nii.gz']:\n        path = os.path.join(SEGMENTATIONS_DIR, f'{study_id}{ext}')\n        if os.path.exists(path):\n            try:\n                return nib.load(path).get_fdata()\n            except:\n                return None\n    return None\n\n# Test\nprint('Testing DICOM loading...')\ntest_id = train_df['StudyInstanceUID'].iloc[0]\ntest_vol = load_dicom_volume(test_id)\nif test_vol is not None:\n    print(f'✓ Loaded volume: {test_vol.shape}')\nelse:\n    print('✗ Failed to load test volume')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:31.403723Z","iopub.execute_input":"2026-01-14T15:00:31.404021Z","iopub.status.idle":"2026-01-14T15:00:37.157398Z","shell.execute_reply.started":"2026-01-14T15:00:31.404003Z","shell.execute_reply":"2026-01-14T15:00:37.156573Z"}},"outputs":[],"execution_count":null},{"id":"500a7c6f","cell_type":"markdown","source":"---\n## Step 5: Preprocessing","metadata":{}},{"id":"24485d72","cell_type":"code","source":"# ============================================================\n# PREPROCESSING FUNCTIONS\n# ============================================================\n\ndef apply_windowing(volume, center=40, width=400):\n    \"\"\"Apply CT bone windowing.\"\"\"\n    low = center - width // 2\n    high = center + width // 2\n    volume = np.clip(volume, low, high)\n    volume = (volume - low) / (high - low)\n    return volume\n\ndef resize_volume(volume, target_size=IMG_SIZE):\n    \"\"\"Resize volume to target size.\"\"\"\n    h, w, d = volume.shape\n    \n    # Resize each slice\n    resized = np.zeros((target_size, target_size, d), dtype=np.float32)\n    for i in range(d):\n        resized[:, :, i] = cv2.resize(volume[:, :, i], (target_size, target_size))\n    \n    return resized\n\ndef preprocess_volume(volume, study_id=None):\n    \"\"\"\n    Full preprocessing pipeline.\n    Returns: (H, W, D) normalized volume\n    \"\"\"\n    if volume is None:\n        return None\n    \n    # Apply CT windowing\n    volume = apply_windowing(volume)\n    \n    # Resize\n    volume = resize_volume(volume, IMG_SIZE)\n    \n    # Ensure [0, 1] range\n    volume = np.clip(volume, 0, 1)\n    \n    return volume.astype(np.float32)\n\n# Test preprocessing\nif test_vol is not None:\n    preprocessed = preprocess_volume(test_vol)\n    print(f'Preprocessed shape: {preprocessed.shape}')\n    print(f'Value range: [{preprocessed.min():.3f}, {preprocessed.max():.3f}]')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:37.158223Z","iopub.execute_input":"2026-01-14T15:00:37.158489Z","iopub.status.idle":"2026-01-14T15:00:37.176947Z","shell.execute_reply.started":"2026-01-14T15:00:37.158465Z","shell.execute_reply":"2026-01-14T15:00:37.176331Z"}},"outputs":[],"execution_count":null},{"id":"64bd9aaf","cell_type":"markdown","source":"---\n## Step 6: Build 3D EfficientNet Model\n\n**Architecture:** Process slices through EfficientNetB3, aggregate with LSTM","metadata":{}},{"id":"c3579cf7","cell_type":"code","source":"# ============================================================\n# 3D EFFICIENTNET MODEL - ULTRA MEMORY OPTIMIZED\n# ============================================================\n\n# Clear memory before building\ntf.keras.backend.clear_session()\ngc.collect()\n\ndef build_model(input_shape=(IMG_SIZE, IMG_SIZE, NUM_SLICES), num_classes=8):\n    \"\"\"\n    Build 2.5D EfficientNet model - memory optimized version.\n    Uses EfficientNetB0 (smallest) with reduced architecture.\n    \"\"\"\n    # Input\n    inputs = layers.Input(shape=input_shape, name='volume_input')\n    \n    # EfficientNet backbone (B0 - smallest)\n    backbone = EfficientNetB0(\n        include_top=False,\n        weights='imagenet',\n        input_shape=(input_shape[0], input_shape[1], 3),\n        pooling='avg'\n    )\n    backbone.trainable = False  # Freeze for initial training\n    \n    # Process slices - use fewer feature extractions\n    slice_features = []\n    for i in range(input_shape[2]):\n        # Extract slice\n        s = layers.Lambda(lambda x, idx=i: x[:, :, :, idx:idx+1])(inputs)\n        # Convert to RGB\n        s_rgb = layers.Concatenate()([s, s, s])\n        # Extract features\n        features = backbone(s_rgb)\n        slice_features.append(features)\n    \n    # Stack features\n    x = layers.Lambda(lambda x: tf.stack(x, axis=1))(slice_features)\n    \n    # Simple LSTM (smaller)\n    x = layers.LSTM(64, return_sequences=False)(x)\n    \n    # Minimal classification head\n    x = layers.Dense(128, activation='relu')(x)\n    x = layers.Dropout(0.5)(x)\n    \n    # Output\n    outputs = layers.Dense(num_classes, activation='sigmoid', name='output')(x)\n    \n    model = models.Model(inputs, outputs, name='EfficientNet3D')\n    return model, backbone\n\n# Build model\nprint('Building ultra-memory-optimized model...')\nprint(f'  Image size: {IMG_SIZE}x{IMG_SIZE}')\nprint(f'  Slices: {NUM_SLICES}')\nprint(f'  Batch size: {BATCH_SIZE}')\n\nmodel, backbone = build_model()\n\nprint(f'\\n✓ Model built: {model.count_params():,} parameters')\nprint(f'  Input: {model.input_shape}')\nprint(f'  Output: {model.output_shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:37.178982Z","iopub.execute_input":"2026-01-14T15:00:37.179343Z","iopub.status.idle":"2026-01-14T15:00:42.401739Z","shell.execute_reply.started":"2026-01-14T15:00:37.179312Z","shell.execute_reply":"2026-01-14T15:00:42.401091Z"}},"outputs":[],"execution_count":null},{"id":"097051f7","cell_type":"markdown","source":"---\n## Step 7: Data Generator","metadata":{}},{"id":"35dfe047","cell_type":"code","source":"# ============================================================\n# DATA GENERATOR\n# ============================================================\n\nclass DataGenerator(keras.utils.Sequence):\n    \"\"\"Efficient data generator for 3D volumes.\"\"\"\n    \n    def __init__(self, df, batch_size=BATCH_SIZE, shuffle=True, augment=False):\n        self.df = df.reset_index(drop=True)\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.augment = augment\n        self.indices = np.arange(len(df))\n        self.on_epoch_end()\n    \n    def __len__(self):\n        return int(np.ceil(len(self.df) / self.batch_size))\n    \n    def __getitem__(self, idx):\n        start = idx * self.batch_size\n        end = min(start + self.batch_size, len(self.df))\n        batch_idx = self.indices[start:end]\n        \n        X, y = [], []\n        \n        for i in batch_idx:\n            row = self.df.iloc[i]\n            study_id = row['StudyInstanceUID']\n            \n            # Load and preprocess\n            vol = load_dicom_volume(study_id)\n            vol = preprocess_volume(vol, study_id)\n            \n            if vol is not None:\n                # Augmentation\n                if self.augment:\n                    vol = self._augment(vol)\n                \n                X.append(vol)\n                y.append([row[c] for c in LABEL_COLS])\n        \n        if len(X) == 0:\n            # Return dummy batch if all failed\n            X = np.zeros((1, IMG_SIZE, IMG_SIZE, NUM_SLICES), dtype=np.float32)\n            y = np.zeros((1, 8), dtype=np.float32)\n        else:\n            X = np.array(X, dtype=np.float32)\n            y = np.array(y, dtype=np.float32)\n        \n        return X, y\n    \n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n    \n    def _augment(self, vol):\n        \"\"\"Simple augmentation.\"\"\"\n        # Random flip\n        if np.random.rand() > 0.5:\n            vol = np.flip(vol, axis=1)\n        \n        # Random intensity shift\n        if np.random.rand() > 0.5:\n            shift = np.random.uniform(-0.05, 0.05)\n            vol = np.clip(vol + shift, 0, 1)\n        \n        return vol\n\n# Split data\ntrain_df_split, val_df_split = train_test_split(\n    train_df, test_size=0.15, random_state=42,\n    stratify=train_df['patient_overall']\n)\n\nprint(f'Training: {len(train_df_split)} patients')\nprint(f'Validation: {len(val_df_split)} patients')\n\n# Create generators\ntrain_gen = DataGenerator(train_df_split, shuffle=True, augment=True)\nval_gen = DataGenerator(val_df_split, shuffle=False, augment=False)\n\nprint(f'\\nTrain batches: {len(train_gen)}')\nprint(f'Val batches: {len(val_gen)}')\n\n# Test\nprint('\\nTesting generator...')\nX_test, y_test = train_gen[0]\nprint(f'Batch shape: X={X_test.shape}, y={y_test.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:42.402522Z","iopub.execute_input":"2026-01-14T15:00:42.402868Z","iopub.status.idle":"2026-01-14T15:00:46.487558Z","shell.execute_reply.started":"2026-01-14T15:00:42.402843Z","shell.execute_reply":"2026-01-14T15:00:46.486805Z"}},"outputs":[],"execution_count":null},{"id":"e2bc923d","cell_type":"markdown","source":"---\n## Step 8: Compile & Train\n\n**Note:** Using `run_eagerly=True` for GPU compatibility","metadata":{}},{"id":"2aa96347","cell_type":"code","source":"# ============================================================\n# COMPILE MODEL\n# ============================================================\n\n# Compile with eager execution for stability\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=LEARNING_RATE),\n    loss='binary_crossentropy',\n    metrics=[\n        'accuracy',\n        keras.metrics.AUC(name='auc', multi_label=True),\n        keras.metrics.Precision(name='precision'),\n        keras.metrics.Recall(name='recall')\n    ],\n    run_eagerly=True  # CRITICAL: Prevents layout optimizer errors\n)\n\nprint('✓ Model compiled with eager execution (stable mode)')\n\n# Callbacks\ncallbacks = [\n    ModelCheckpoint(\n        'best_model.h5',\n        monitor='val_auc',\n        save_best_only=True,\n        mode='max',\n        verbose=1\n    ),\n    EarlyStopping(\n        monitor='val_loss',\n        patience=7,\n        restore_best_weights=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-7,\n        verbose=1\n    )\n]\n\nprint(f'\\nTraining config:')\nprint(f'  Batch size: {BATCH_SIZE}')\nprint(f'  Epochs: {EPOCHS}')\nprint(f'  Learning rate: {LEARNING_RATE}')\nprint(f'  Mode: Eager execution (GPU-stable)')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:46.488579Z","iopub.execute_input":"2026-01-14T15:00:46.488997Z","iopub.status.idle":"2026-01-14T15:00:46.510658Z","shell.execute_reply.started":"2026-01-14T15:00:46.488974Z","shell.execute_reply":"2026-01-14T15:00:46.509947Z"}},"outputs":[],"execution_count":null},{"id":"1ae2fbac","cell_type":"code","source":"# ============================================================\n# TRAIN MODEL\n# ============================================================\n\nprint('='*50)\nprint('STARTING TRAINING')\nprint('='*50)\nprint('\\n⏳ This will take 2-4 hours on Kaggle GPU\\n')\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS,\n    callbacks=callbacks,\n    verbose=1\n)\n\nprint('\\n' + '='*50)\nprint('TRAINING COMPLETE!')\nprint('='*50)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-14T15:00:46.511421Z","iopub.execute_input":"2026-01-14T15:00:46.511653Z"}},"outputs":[],"execution_count":null},{"id":"bfd7a78b","cell_type":"markdown","source":"---\n## Step 9: Evaluate Model","metadata":{}},{"id":"6e55a0b7","cell_type":"code","source":"# ============================================================\n# EVALUATION\n# ============================================================\n\n# Plot training history\nfig, axes = plt.subplots(2, 2, figsize=(14, 10))\n\n# Accuracy\naxes[0,0].plot(history.history['accuracy'], label='Train')\naxes[0,0].plot(history.history['val_accuracy'], label='Val')\naxes[0,0].set_title('Accuracy')\naxes[0,0].legend()\naxes[0,0].grid(True, alpha=0.3)\n\n# Loss\naxes[0,1].plot(history.history['loss'], label='Train')\naxes[0,1].plot(history.history['val_loss'], label='Val')\naxes[0,1].set_title('Loss')\naxes[0,1].legend()\naxes[0,1].grid(True, alpha=0.3)\n\n# AUC\naxes[1,0].plot(history.history['auc'], label='Train')\naxes[1,0].plot(history.history['val_auc'], label='Val')\naxes[1,0].set_title('AUC')\naxes[1,0].legend()\naxes[1,0].grid(True, alpha=0.3)\n\n# Precision/Recall\naxes[1,1].plot(history.history['precision'], label='Train Precision')\naxes[1,1].plot(history.history['recall'], label='Train Recall')\naxes[1,1].plot(history.history['val_precision'], label='Val Precision', linestyle='--')\naxes[1,1].plot(history.history['val_recall'], label='Val Recall', linestyle='--')\naxes[1,1].set_title('Precision & Recall')\naxes[1,1].legend()\naxes[1,1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig('training_history.png', dpi=150)\nplt.show()\n\n# Final evaluation\nprint('\\nEvaluating on validation set...')\nresults = model.evaluate(val_gen, verbose=1)\n\nprint(f'\\n{\"=\"*50}')\nprint('FINAL RESULTS')\nprint(f'{\"=\"*50}')\nprint(f'Loss:      {results[0]:.4f}')\nprint(f'Accuracy:  {results[1]:.4f}')\nprint(f'AUC:       {results[2]:.4f}')\nprint(f'Precision: {results[3]:.4f}')\nprint(f'Recall:    {results[4]:.4f}')\nprint(f'{\"=\"*50}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"0dd869dc","cell_type":"code","source":"# ============================================================\n# PER-VERTEBRA ANALYSIS\n# ============================================================\n\nprint('Generating per-vertebra predictions...')\n\ny_true, y_pred = [], []\nfor i in range(len(val_gen)):\n    X, y = val_gen[i]\n    pred = model.predict(X, verbose=0)\n    y_true.append(y)\n    y_pred.append(pred)\n\ny_true = np.vstack(y_true)\ny_pred = np.vstack(y_pred)\n\nprint('\\nPer-Vertebra AUC Scores:')\nprint('-' * 40)\nfor i, label in enumerate(LABEL_COLS):\n    try:\n        auc = roc_auc_score(y_true[:, i], y_pred[:, i])\n        print(f'{label:15s}: {auc:.4f}')\n    except:\n        print(f'{label:15s}: N/A')\n\ntry:\n    macro_auc = roc_auc_score(y_true, y_pred, average='macro')\n    print(f'\\n{\"Macro Average\":15s}: {macro_auc:.4f}')\nexcept:\n    pass","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"c4493be8","cell_type":"markdown","source":"---\n## Step 10: Save Model","metadata":{}},{"id":"a22acc16","cell_type":"code","source":"# ============================================================\n# SAVE MODEL\n# ============================================================\n\n# Save complete model\nmodel.save('rsna_fracture_model.h5')\nprint('✓ Model saved: rsna_fracture_model.h5')\n\n# Save weights\nmodel.save_weights('rsna_model_weights.h5')\nprint('✓ Weights saved: rsna_model_weights.h5')\n\n# Save training history\npd.DataFrame(history.history).to_csv('training_log.csv', index=False)\nprint('✓ History saved: training_log.csv')\n\n# Save config\nimport json\nconfig = {\n    'img_size': IMG_SIZE,\n    'num_slices': NUM_SLICES,\n    'batch_size': BATCH_SIZE,\n    'epochs': EPOCHS,\n    'learning_rate': LEARNING_RATE,\n    'val_auc': float(results[2]),\n    'val_accuracy': float(results[1])\n}\nwith open('config.json', 'w') as f:\n    json.dump(config, f, indent=2)\nprint('✓ Config saved: config.json')\n\nprint('\\n' + '='*50)\nprint('ALL FILES SAVED!')\nprint('='*50)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"374fd20c","cell_type":"markdown","source":"---\n## Optional: Fine-tune Model\n\nAfter initial training, unfreeze EfficientNet layers for fine-tuning","metadata":{}},{"id":"0f241ac5","cell_type":"code","source":"# ============================================================\n# FINE-TUNING (Optional)\n# ============================================================\n\n# Unfreeze backbone\nbackbone.trainable = True\n\n# Freeze early layers (keep first 100 frozen)\nfor layer in backbone.layers[:100]:\n    layer.trainable = False\n\n# Recompile with lower learning rate\nmodel.compile(\n    optimizer=keras.optimizers.Adam(learning_rate=1e-5),\n    loss='binary_crossentropy',\n    metrics=[\n        'accuracy',\n        keras.metrics.AUC(name='auc', multi_label=True),\n        keras.metrics.Precision(name='precision'),\n        keras.metrics.Recall(name='recall')\n    ],\n    run_eagerly=True\n)\n\nprint('Fine-tuning with unfrozen backbone...')\nprint(f'Trainable params: {sum(np.prod(v.shape) for v in model.trainable_variables):,}')\n\n# Continue training\nhistory_ft = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=5,\n    callbacks=callbacks,\n    verbose=1\n)\n\n# Save fine-tuned model\nmodel.save('rsna_fracture_model_finetuned.h5')\nprint('\\n✓ Fine-tuned model saved!')","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}