{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"},{"sourceId":9864328,"sourceType":"datasetVersion","datasetId":6054580},{"sourceId":169572,"sourceType":"modelInstanceVersion","modelInstanceId":142732,"modelId":164031}],"dockerImageVersionId":30787,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Pneumonia Detection Using ResNet-50\n\n## Introduction\n\nPneumonia is a serious respiratory infection that can cause inflammation of the air sacs in the lungs, leading to symptoms such as cough, chest pain, fever, and difficulty breathing. Early detection is critical for effective treatment, as pneumonia can progress rapidly and lead to severe complications if left untreated.\n\n### ResNet-50 Architecture\n\nResNet-50 is a deep convolutional neural network that uses residual learning to enable the training of very deep networks. The main innovation is the introduction of skip connections or residual connections, which allow gradients to flow more easily through the network during backpropagation.\n\nFor more details on ResNet architecture, refer to: [Introduction to ResNet-18](https://www.kaggle.com/code/iamtapendu/introduction-to-resnet-18)\n\n### Dataset Overview\n\n**Goal**: Build an algorithm to identify pneumonia in chest X-rays by locating lung opacities.\n\n- **Total Rows**: 30,227\n- **Unique Patients**: 26,684\n\n**Distribution**:\n- No Lung Opacity / Not Normal: 11,821 samples\n- Normal: 8,851 samples\n- Lung Opacity: 9,555 samples","metadata":{}},{"cell_type":"markdown","source":"## 1. Environment Setup and Configuration","metadata":{}},{"cell_type":"code","source":"import os\nimport time\nimport warnings\n\n# Only set log level, DON'T hide GPU\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, ReduceLROnPlateau, TensorBoard\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.utils import plot_model\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport json\nfrom datetime import datetime\n\nfrom sklearn.model_selection import train_test_split, StratifiedKFold\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.metrics import (\n    classification_report, confusion_matrix, roc_curve, auc,\n    f1_score, precision_recall_curve, average_precision_score\n)\n\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:26:50.991884Z","iopub.execute_input":"2026-01-13T17:26:50.992600Z","iopub.status.idle":"2026-01-13T17:27:14.008748Z","shell.execute_reply.started":"2026-01-13T17:26:50.992564Z","shell.execute_reply":"2026-01-13T17:27:14.008000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seeds(seed=123):\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n\nset_seeds()\n\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\"GPUs available: {len(gpus)}\")\n        print(\"GPU devices:\", gpus)\n        \n        from tensorflow.keras import mixed_precision\n        mixed_precision.set_global_policy('mixed_float16')\n        print(\"Mixed precision enabled\")\n    except RuntimeError as e:\n        print(f\"GPU configuration error: {e}\")\n        print(\"Falling back to CPU\")\nelse:\n    print(\"No GPU detected. Using CPU.\")\n    print(\"Note: Training will be slower on CPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.010133Z","iopub.execute_input":"2026-01-13T17:27:14.010598Z","iopub.status.idle":"2026-01-13T17:27:14.555106Z","shell.execute_reply.started":"2026-01-13T17:27:14.010569Z","shell.execute_reply":"2026-01-13T17:27:14.554251Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_PATH = '/kaggle/input/rsna-pneumonia-processed-dataset/Training/Images/'\nMODEL_PATH = '/kaggle/input/pneumonia-detection-using-resnet50/tensorflow2/model-v1.1/2/lung_detection_2.keras'\nTRAIN_METADATA_PATH = '/kaggle/input/rsna-pneumonia-processed-dataset/stage2_train_metadata.csv'\nTEST_METADATA_PATH = '/kaggle/input/rsna-pneumonia-processed-dataset/stage2_test_metadata.csv'\n\nCONFIG = {\n    'model_version': '2.0',\n    'image_size': (512, 512),\n    'batch_size': 8,\n    'learning_rate': 1e-5,\n    'epochs': 24,\n    'val_split': 0.1,\n    'random_state': 123,\n    'n_folds': 5,\n    'patience': 10,\n    'reduce_lr_patience': 3,\n    'min_lr': 1e-7\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.556549Z","iopub.execute_input":"2026-01-13T17:27:14.556853Z","iopub.status.idle":"2026-01-13T17:27:14.572863Z","shell.execute_reply.started":"2026-01-13T17:27:14.556826Z","shell.execute_reply":"2026-01-13T17:27:14.571999Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Data Loading and Exploration","metadata":{}},{"cell_type":"code","source":"train_metadata = pd.read_csv(TRAIN_METADATA_PATH)\ntest_metadata = pd.read_csv(TEST_METADATA_PATH)\n\nprint(\"Train Metadata Shape:\", train_metadata.shape)\nprint(\"Test Metadata Shape:\", test_metadata.shape)\nprint(\"\\nTrain Metadata Info:\")\ntrain_metadata.info()\nprint(\"\\nTest Metadata Info:\")\ntest_metadata.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.574517Z","iopub.execute_input":"2026-01-13T17:27:14.574762Z","iopub.status.idle":"2026-01-13T17:27:14.757331Z","shell.execute_reply.started":"2026-01-13T17:27:14.574738Z","shell.execute_reply":"2026-01-13T17:27:14.756454Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_metadata.describe().style.background_gradient()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.758774Z","iopub.execute_input":"2026-01-13T17:27:14.759135Z","iopub.status.idle":"2026-01-13T17:27:14.864388Z","shell.execute_reply.started":"2026-01-13T17:27:14.759091Z","shell.execute_reply":"2026-01-13T17:27:14.863407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_metadata.describe(include='O')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.865595Z","iopub.execute_input":"2026-01-13T17:27:14.866027Z","iopub.status.idle":"2026-01-13T17:27:14.916335Z","shell.execute_reply.started":"2026-01-13T17:27:14.865996Z","shell.execute_reply":"2026-01-13T17:27:14.915512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f'Total rows: {train_metadata.shape[0]}')\nprint(f'Unique patients: {train_metadata[\"patientId\"].nunique()}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.917290Z","iopub.execute_input":"2026-01-13T17:27:14.917537Z","iopub.status.idle":"2026-01-13T17:27:14.927886Z","shell.execute_reply.started":"2026-01-13T17:27:14.917512Z","shell.execute_reply":"2026-01-13T17:27:14.927158Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(14, 10))\nsns.set_palette('rocket_r')\n\nplt.subplot(221)\nclass_counts = train_metadata['class'].value_counts()\nplt.pie(class_counts, autopct='%4.2f%%', labels=class_counts.index)\nplt.title('Class Distribution')\n\nplt.subplot(222)\nsns.boxplot(data=train_metadata, x='class', y='age')\nplt.title('Age Distribution by Class')\nplt.xticks(rotation=15)\n\nplt.subplot(223)\nsns.countplot(data=train_metadata, x='class', hue='sex')\nplt.title('Gender Distribution by Class')\nplt.xticks(rotation=15)\nplt.legend(title='Sex')\n\nplt.subplot(224)\nsns.countplot(data=train_metadata, x='class', hue='position')\nplt.title('Position Distribution by Class')\nplt.xticks(rotation=15)\nplt.legend(title='Position')\n\nplt.suptitle('Data Overview', fontsize=20)\nplt.tight_layout()\nplt.savefig('data_overview.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:14.929000Z","iopub.execute_input":"2026-01-13T17:27:14.929319Z","iopub.status.idle":"2026-01-13T17:27:17.127518Z","shell.execute_reply.started":"2026-01-13T17:27:14.929292Z","shell.execute_reply":"2026-01-13T17:27:17.126642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Data Preprocessing","metadata":{}},{"cell_type":"code","source":"train_metadata_clean = train_metadata.drop(['x', 'y', 'width', 'height'], axis=1)\ntrain_metadata_clean = train_metadata_clean.drop_duplicates()\n\nprint(f\"Cleaned dataset shape: {train_metadata_clean.shape}\")\nprint(f\"Removed {train_metadata.shape[0] - train_metadata_clean.shape[0]} duplicate entries\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.128522Z","iopub.execute_input":"2026-01-13T17:27:17.128801Z","iopub.status.idle":"2026-01-13T17:27:17.153798Z","shell.execute_reply.started":"2026-01-13T17:27:17.128775Z","shell.execute_reply":"2026-01-13T17:27:17.152861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_patient_id, val_patient_id, train_target, val_target = train_test_split(\n    train_metadata_clean.patientId,\n    train_metadata_clean['Target'],\n    test_size=CONFIG['val_split'],\n    stratify=train_metadata_clean['Target'],\n    random_state=CONFIG['random_state']\n)\n\nprint(f\"Training samples: {len(train_patient_id)}\")\nprint(f\"Validation samples: {len(val_patient_id)}\")\nprint(f\"\\nTraining set distribution:\")\nprint(train_target.value_counts())\nprint(f\"\\nValidation set distribution:\")\nprint(val_target.value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.156375Z","iopub.execute_input":"2026-01-13T17:27:17.157044Z","iopub.status.idle":"2026-01-13T17:27:17.173867Z","shell.execute_reply.started":"2026-01-13T17:27:17.157015Z","shell.execute_reply":"2026-01-13T17:27:17.173031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_weights = compute_class_weight(\n    'balanced',\n    classes=np.unique(train_metadata_clean.Target),\n    y=train_metadata_clean.Target\n)\n\nclass_weight_dict = {i: class_weights[i] for i in range(len(class_weights))}\nprint(\"Class weights:\", class_weight_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.174796Z","iopub.execute_input":"2026-01-13T17:27:17.175066Z","iopub.status.idle":"2026-01-13T17:27:17.183939Z","shell.execute_reply.started":"2026-01-13T17:27:17.175042Z","shell.execute_reply":"2026-01-13T17:27:17.183015Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Data Augmentation and Generator","metadata":{}},{"cell_type":"code","source":"data_augmentation = keras.Sequential([\n    layers.RandomFlip(\"horizontal\"),\n    layers.RandomRotation(0.1),\n    layers.RandomZoom(0.1),\n    layers.RandomContrast(0.1),\n])\n\nclass DataGenerator(keras.utils.Sequence):\n    def __init__(self, patient_id, target_class, batch_size=32, size=(512, 512), \n                 shuffle=True, augment=False, **kwargs):\n        super().__init__(**kwargs)\n        \n        self.patient_id = patient_id\n        self.target_class = target_class\n        self.batch_size = batch_size\n        self.size = size\n        self.shuffle = shuffle\n        self.augment = augment\n        \n        assert len(self.patient_id) == len(self.target_class), \\\n            \"Patient IDs and targets must have the same length\"\n        \n        self.indexes = np.arange(len(self.patient_id))\n        \n        if self.shuffle:\n            self.on_epoch_end()\n\n    def __len__(self):\n        return int(np.floor(len(self.patient_id) / self.batch_size))\n\n    def __getitem__(self, index):\n        batch_indices = self.indexes[index * self.batch_size : (index + 1) * self.batch_size]\n        \n        images = []\n        targets = []\n        \n        for idx in batch_indices:\n            img = cv2.imread(IMG_PATH + self.patient_id[idx] + '.png', 1)\n            img = cv2.resize(img, self.size)\n            img = img / 255.0\n            \n            if self.augment:\n                img = data_augmentation(np.expand_dims(img, 0), training=True)[0]\n            \n            images.append(img)\n            targets.append(self.target_class[idx])\n        \n        return np.array(images), np.array(targets)\n\n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indexes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.184890Z","iopub.execute_input":"2026-01-13T17:27:17.185298Z","iopub.status.idle":"2026-01-13T17:27:17.478153Z","shell.execute_reply.started":"2026-01-13T17:27:17.185267Z","shell.execute_reply":"2026-01-13T17:27:17.477259Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data = DataGenerator(\n    patient_id=train_patient_id.tolist(),\n    target_class=train_target.tolist(),\n    batch_size=CONFIG['batch_size'],\n    size=CONFIG['image_size'],\n    augment=True\n)\n\nval_data = DataGenerator(\n    patient_id=val_patient_id.tolist(),\n    target_class=val_target.tolist(),\n    batch_size=CONFIG['batch_size'],\n    size=CONFIG['image_size'],\n    augment=False\n)\n\nprint(f\"Training batches: {len(train_data)}\")\nprint(f\"Validation batches: {len(val_data)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.479253Z","iopub.execute_input":"2026-01-13T17:27:17.479512Z","iopub.status.idle":"2026-01-13T17:27:17.486743Z","shell.execute_reply.started":"2026-01-13T17:27:17.479488Z","shell.execute_reply":"2026-01-13T17:27:17.485895Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Model Architecture","metadata":{}},{"cell_type":"code","source":"def build_model(input_shape=(512, 512, 3), num_classes=1):\n    image_input = layers.Input(shape=input_shape)\n    \n    resnet = ResNet50(\n        weights='imagenet',\n        include_top=False,\n        pooling='avg',\n        input_shape=input_shape\n    )\n    \n    x = resnet(image_input)\n    x = layers.Flatten()(x)\n    x = layers.Dense(1024, activation='relu')(x)\n    x = layers.Dense(num_classes, activation='sigmoid')(x)\n    \n    model = models.Model(inputs=image_input, outputs=x)\n    \n    return model\n\nmodel = build_model(input_shape=(*CONFIG['image_size'], 3))\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:17.487763Z","iopub.execute_input":"2026-01-13T17:27:17.487998Z","iopub.status.idle":"2026-01-13T17:27:19.219826Z","shell.execute_reply.started":"2026-01-13T17:27:17.487976Z","shell.execute_reply":"2026-01-13T17:27:19.219131Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Model Architecture Visualization","metadata":{}},{"cell_type":"code","source":"plot_model(\n    model,\n    to_file='model_architecture.png',\n    show_shapes=True,\n    show_layer_names=True,\n    rankdir='TB',\n    expand_nested=True,\n    dpi=150\n)\n\nplot_model(\n    model,\n    to_file='model_architecture_detailed.png',\n    show_shapes=True,\n    show_dtype=False,\n    show_layer_names=True,\n    show_layer_activations=True,\n    rankdir='TB',\n    expand_nested=True,\n    dpi=150\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:19.220737Z","iopub.execute_input":"2026-01-13T17:27:19.220981Z","iopub.status.idle":"2026-01-13T17:27:26.756927Z","shell.execute_reply.started":"2026-01-13T17:27:19.220957Z","shell.execute_reply":"2026-01-13T17:27:26.755854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"layer_info = []\nfor layer in model.layers:\n    layer_info.append({\n        'Layer Name': layer.name,\n        'Layer Type': layer.__class__.__name__,\n        'Output Shape': str(layer.output.shape),\n        'Parameters': layer.count_params()\n    })\n\ndf_layers = pd.DataFrame(layer_info)\nprint(df_layers.to_string(index=False))\ndf_layers.to_csv('model_architecture_table.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:26.758253Z","iopub.execute_input":"2026-01-13T17:27:26.758530Z","iopub.status.idle":"2026-01-13T17:27:26.772951Z","shell.execute_reply.started":"2026-01-13T17:27:26.758504Z","shell.execute_reply":"2026-01-13T17:27:26.771997Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.patches as mpatches\n\nfig, ax = plt.subplots(figsize=(12, 8))\n\nlayers_data = [\n    ('Input', '512×512×3', '#3498db'),\n    ('ResNet-50\\n(Pre-trained)', '2048', '#e67e22'),\n    ('Flatten', '2048', '#2ecc71'),\n    ('Dense', '1024', '#e74c3c'),\n    ('Output Dense', '1', '#9b59b6')\n]\n\nfor i, (name, shape, color) in enumerate(layers_data):\n    rect = mpatches.FancyBboxPatch(\n        (0.3, i), 0.4, 0.8,\n        boxstyle=\"round,pad=0.1\",\n        facecolor=color,\n        edgecolor='black',\n        linewidth=2\n    )\n    ax.add_patch(rect)\n    \n    ax.text(0.5, i + 0.4, f'{name}\\n{shape}',\n            ha='center', va='center',\n            fontsize=12, fontweight='bold',\n            color='white')\n    \n    if i < len(layers_data) - 1:\n        ax.arrow(0.5, i + 0.85, 0, 0.1,\n                head_width=0.05, head_length=0.05,\n                fc='black', ec='black', linewidth=2)\n\nax.set_xlim(0, 1)\nax.set_ylim(-0.5, len(layers_data))\nax.axis('off')\nax.set_title('ResNet-50 Pneumonia Detection Model', fontsize=16, fontweight='bold', pad=20)\n\nplt.tight_layout()\nplt.savefig('model_architecture_custom.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:26.774071Z","iopub.execute_input":"2026-01-13T17:27:26.774414Z","iopub.status.idle":"2026-01-13T17:27:27.598052Z","shell.execute_reply.started":"2026-01-13T17:27:26.774371Z","shell.execute_reply":"2026-01-13T17:27:27.597105Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Load Pre-trained Model and Fine-tune","metadata":{}},{"cell_type":"code","source":"model = models.load_model(MODEL_PATH)\n\nmodel.layers[0].trainable = False\nfor layer in model.layers[1].layers[:-50]:\n    layer.trainable = False\n\ntrainable_params = sum([tf.size(w).numpy() for w in model.trainable_weights])\ntotal_params = sum([tf.size(w).numpy() for w in model.weights])\n\nprint(f\"Trainable parameters: {trainable_params:,}\")\nprint(f\"Total parameters: {total_params:,}\")\nprint(f\"Trainable percentage: {100 * trainable_params / total_params:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:27.599417Z","iopub.execute_input":"2026-01-13T17:27:27.599808Z","iopub.status.idle":"2026-01-13T17:27:39.419438Z","shell.execute_reply.started":"2026-01-13T17:27:27.599766Z","shell.execute_reply":"2026-01-13T17:27:39.418558Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=CONFIG['learning_rate']),\n    loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),\n    metrics=['accuracy', tf.keras.metrics.AUC(name='auc')]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:39.420614Z","iopub.execute_input":"2026-01-13T17:27:39.420894Z","iopub.status.idle":"2026-01-13T17:27:39.442189Z","shell.execute_reply.started":"2026-01-13T17:27:39.420867Z","shell.execute_reply":"2026-01-13T17:27:39.441526Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Callbacks Configuration","metadata":{}},{"cell_type":"code","source":"class MetricsLogger(keras.callbacks.Callback):\n    def __init__(self):\n        super().__init__()\n        self.epoch_times = []\n        \n    def on_epoch_begin(self, epoch, logs=None):\n        self.epoch_start_time = time.time()\n    \n    def on_epoch_end(self, epoch, logs=None):\n        epoch_time = time.time() - self.epoch_start_time\n        self.epoch_times.append(epoch_time)\n        print(f\"\\nEpoch {epoch + 1} completed in {epoch_time:.2f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:39.444491Z","iopub.execute_input":"2026-01-13T17:27:39.444730Z","iopub.status.idle":"2026-01-13T17:27:39.449670Z","shell.execute_reply.started":"2026-01-13T17:27:39.444707Z","shell.execute_reply":"2026-01-13T17:27:39.448880Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"checkpoint_loss = ModelCheckpoint(\n    'best_model_loss.keras',\n    monitor='val_loss',\n    verbose=1,\n    save_best_only=True,\n    mode='min',\n    save_weights_only=False\n)\n\ncheckpoint_auc = ModelCheckpoint(\n    'best_model_auc.keras',\n    monitor='val_auc',\n    verbose=1,\n    save_best_only=True,\n    mode='max',\n    save_weights_only=False\n)\n\nearly_stopping = EarlyStopping(\n    monitor='val_loss',\n    patience=CONFIG['patience'],\n    restore_best_weights=True,\n    verbose=1\n)\n\nlr_scheduler = ReduceLROnPlateau(\n    monitor='val_loss',\n    factor=0.5,\n    patience=CONFIG['reduce_lr_patience'],\n    min_lr=CONFIG['min_lr'],\n    verbose=1\n)\n\ntensorboard = TensorBoard(\n    log_dir='./logs',\n    histogram_freq=1,\n    write_graph=True\n)\n\nmetrics_logger = MetricsLogger()\n\ncallbacks = [checkpoint_loss, checkpoint_auc, early_stopping, lr_scheduler, tensorboard, metrics_logger]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:39.450921Z","iopub.execute_input":"2026-01-13T17:27:39.451631Z","iopub.status.idle":"2026-01-13T17:27:39.467031Z","shell.execute_reply.started":"2026-01-13T17:27:39.451591Z","shell.execute_reply":"2026-01-13T17:27:39.466363Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Model Training","metadata":{}},{"cell_type":"code","source":"training_start_time = time.time()\n\nhistory = model.fit(\n    train_data,\n    validation_data=val_data,\n    epochs=CONFIG['epochs'],\n    class_weight=class_weight_dict,\n    callbacks=callbacks,\n    verbose=1\n)\n\ntraining_duration = time.time() - training_start_time\nprint(f\"\\nTotal training time: {training_duration / 60:.2f} minutes\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T17:27:39.468303Z","iopub.execute_input":"2026-01-13T17:27:39.468908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Training Visualization","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n\naxes[0, 0].plot(history.history['loss'], label='Train Loss', linewidth=2)\naxes[0, 0].plot(history.history['val_loss'], label='Validation Loss', linewidth=2)\naxes[0, 0].set_title('Model Loss', fontsize=14, fontweight='bold')\naxes[0, 0].set_ylabel('Loss')\naxes[0, 0].set_xlabel('Epoch')\naxes[0, 0].legend()\naxes[0, 0].grid(True, alpha=0.3)\n\naxes[0, 1].plot(history.history['accuracy'], label='Train Accuracy', linewidth=2)\naxes[0, 1].plot(history.history['val_accuracy'], label='Validation Accuracy', linewidth=2)\naxes[0, 1].set_title('Model Accuracy', fontsize=14, fontweight='bold')\naxes[0, 1].set_ylabel('Accuracy')\naxes[0, 1].set_xlabel('Epoch')\naxes[0, 1].legend()\naxes[0, 1].grid(True, alpha=0.3)\n\naxes[1, 0].plot(history.history['auc'], label='Train AUC', linewidth=2)\naxes[1, 0].plot(history.history['val_auc'], label='Validation AUC', linewidth=2)\naxes[1, 0].set_title('Model AUC', fontsize=14, fontweight='bold')\naxes[1, 0].set_ylabel('AUC')\naxes[1, 0].set_xlabel('Epoch')\naxes[1, 0].legend()\naxes[1, 0].grid(True, alpha=0.3)\n\naxes[1, 1].plot(metrics_logger.epoch_times, linewidth=2, marker='o')\naxes[1, 1].set_title('Epoch Training Time', fontsize=14, fontweight='bold')\naxes[1, 1].set_ylabel('Time (seconds)')\naxes[1, 1].set_xlabel('Epoch')\naxes[1, 1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig('training_history.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Comprehensive Model Evaluation","metadata":{}},{"cell_type":"code","source":"def comprehensive_evaluation(model, val_data):\n    y_true = []\n    y_pred_prob = []\n    \n    print(\"Generating predictions...\")\n    for i in range(len(val_data)):\n        imgs, labels = val_data[i]\n        predictions = model.predict(imgs, verbose=0)\n        y_true.extend(labels)\n        y_pred_prob.extend(predictions.flatten())\n    \n    y_true = np.array(y_true)\n    y_pred_prob = np.array(y_pred_prob)\n    y_pred = (y_pred_prob > 0.5).astype(int)\n    \n    print(\"\\nClassification Report:\")\n    print(classification_report(y_true, y_pred, target_names=['Normal', 'Pneumonia'], digits=4))\n    \n    cm = confusion_matrix(y_true, y_pred)\n    \n    fpr, tpr, _ = roc_curve(y_true, y_pred_prob)\n    roc_auc = auc(fpr, tpr)\n    \n    precision, recall, _ = precision_recall_curve(y_true, y_pred_prob)\n    avg_precision = average_precision_score(y_true, y_pred_prob)\n    \n    return {\n        'y_true': y_true,\n        'y_pred': y_pred,\n        'y_pred_prob': y_pred_prob,\n        'confusion_matrix': cm,\n        'fpr': fpr,\n        'tpr': tpr,\n        'roc_auc': roc_auc,\n        'precision': precision,\n        'recall': recall,\n        'avg_precision': avg_precision\n    }\n\neval_results = comprehensive_evaluation(model, val_data)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n\nsns.heatmap(eval_results['confusion_matrix'], annot=True, fmt='d', cmap='Blues',\n            xticklabels=['Normal', 'Pneumonia'],\n            yticklabels=['Normal', 'Pneumonia'],\n            ax=axes[0, 0])\naxes[0, 0].set_title('Confusion Matrix', fontsize=14, fontweight='bold')\naxes[0, 0].set_ylabel('True Label')\naxes[0, 0].set_xlabel('Predicted Label')\n\naxes[0, 1].plot(eval_results['fpr'], eval_results['tpr'], linewidth=2,\n                label=f'ROC curve (AUC = {eval_results[\"roc_auc\"]:.4f})')\naxes[0, 1].plot([0, 1], [0, 1], 'k--', linewidth=1, label='Random Classifier')\naxes[0, 1].set_xlim([0.0, 1.0])\naxes[0, 1].set_ylim([0.0, 1.05])\naxes[0, 1].set_xlabel('False Positive Rate')\naxes[0, 1].set_ylabel('True Positive Rate')\naxes[0, 1].set_title('ROC Curve', fontsize=14, fontweight='bold')\naxes[0, 1].legend(loc='lower right')\naxes[0, 1].grid(True, alpha=0.3)\n\naxes[1, 0].plot(eval_results['recall'], eval_results['precision'], linewidth=2,\n                label=f'AP = {eval_results[\"avg_precision\"]:.4f}')\naxes[1, 0].set_xlabel('Recall')\naxes[1, 0].set_ylabel('Precision')\naxes[1, 0].set_title('Precision-Recall Curve', fontsize=14, fontweight='bold')\naxes[1, 0].legend(loc='lower left')\naxes[1, 0].grid(True, alpha=0.3)\n\naxes[1, 1].hist(eval_results['y_pred_prob'][eval_results['y_true'] == 0],\n                bins=50, alpha=0.7, label='Normal', color='blue')\naxes[1, 1].hist(eval_results['y_pred_prob'][eval_results['y_true'] == 1],\n                bins=50, alpha=0.7, label='Pneumonia', color='red')\naxes[1, 1].axvline(x=0.5, color='black', linestyle='--', linewidth=2, label='Threshold')\naxes[1, 1].set_xlabel('Predicted Probability')\naxes[1, 1].set_ylabel('Frequency')\naxes[1, 1].set_title('Prediction Distribution', fontsize=14, fontweight='bold')\naxes[1, 1].legend()\naxes[1, 1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.savefig('evaluation_metrics.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. Threshold Optimization","metadata":{}},{"cell_type":"code","source":"def find_optimal_threshold(y_true, y_pred_prob, metric='f1'):\n    thresholds = np.arange(0.1, 0.9, 0.01)\n    scores = []\n    \n    for thresh in thresholds:\n        y_pred = (y_pred_prob > thresh).astype(int)\n        if metric == 'f1':\n            scores.append(f1_score(y_true, y_pred))\n    \n    optimal_idx = np.argmax(scores)\n    return thresholds[optimal_idx], scores[optimal_idx], thresholds, scores\n\noptimal_threshold, optimal_f1, all_thresholds, all_scores = find_optimal_threshold(\n    eval_results['y_true'],\n    eval_results['y_pred_prob']\n)\n\nprint(f\"Optimal threshold: {optimal_threshold:.4f}\")\nprint(f\"F1 score at optimal threshold: {optimal_f1:.4f}\")\n\nplt.figure(figsize=(10, 6))\nplt.plot(all_thresholds, all_scores, linewidth=2)\nplt.axvline(x=optimal_threshold, color='red', linestyle='--', linewidth=2,\n            label=f'Optimal = {optimal_threshold:.4f}')\nplt.xlabel('Threshold')\nplt.ylabel('F1 Score')\nplt.title('F1 Score vs Classification Threshold', fontsize=14, fontweight='bold')\nplt.legend()\nplt.grid(True, alpha=0.3)\nplt.savefig('threshold_optimization.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_pred_optimized = (eval_results['y_pred_prob'] > optimal_threshold).astype(int)\n\nprint(\"\\nClassification Report with Optimized Threshold:\")\nprint(classification_report(eval_results['y_true'], y_pred_optimized,\n                          target_names=['Normal', 'Pneumonia'], digits=4))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Grad-CAM Visualization","metadata":{}},{"cell_type":"code","source":"def generate_gradcam(model, img_array, layer_name='conv5_block3_out'):\n    grad_model = tf.keras.models.Model(\n        [model.inputs],\n        [model.get_layer(layer_name).output, model.output]\n    )\n    \n    with tf.GradientTape() as tape:\n        conv_outputs, predictions = grad_model(img_array)\n        loss = predictions[:, 0]\n    \n    grads = tape.gradient(loss, conv_outputs)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n    \n    conv_outputs = conv_outputs[0]\n    heatmap = conv_outputs @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    \n    return heatmap.numpy()\n\ndef overlay_gradcam(img, heatmap, alpha=0.4):\n    heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))\n    heatmap = np.uint8(255 * heatmap)\n    heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)\n    \n    superimposed_img = heatmap * alpha + img * 255 * (1 - alpha)\n    superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)\n    \n    return superimposed_img","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_imgs, sample_labels = val_data[0]\nsample_predictions = model.predict(sample_imgs, verbose=0)\n\nfig, axes = plt.subplots(2, 4, figsize=(16, 8))\naxes = axes.flatten()\n\nfor i in range(min(4, len(sample_imgs))):\n    img = sample_imgs[i]\n    label = sample_labels[i]\n    pred_prob = sample_predictions[i][0]\n    pred_class = int(pred_prob > optimal_threshold)\n    \n    axes[i].imshow(img)\n    axes[i].set_title(f'True: {label} | Pred: {pred_class} ({pred_prob:.2%})')\n    axes[i].axis('off')\n    \n    heatmap = generate_gradcam(model, np.expand_dims(img, 0))\n    gradcam_img = overlay_gradcam(img, heatmap)\n    \n    axes[i + 4].imshow(gradcam_img)\n    axes[i + 4].set_title('Grad-CAM Visualization')\n    axes[i + 4].axis('off')\n\nplt.tight_layout()\nplt.savefig('gradcam_visualization.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 14. Error Analysis","metadata":{}},{"cell_type":"code","source":"def analyze_errors(model, val_data, threshold=0.5):\n    false_positives = []\n    false_negatives = []\n    \n    for i in range(len(val_data)):\n        imgs, labels = val_data[i]\n        predictions = model.predict(imgs, verbose=0).flatten()\n        \n        for j, (img, label, pred) in enumerate(zip(imgs, labels, predictions)):\n            pred_class = int(pred > threshold)\n            if pred_class == 1 and label == 0:\n                false_positives.append((img, pred, label))\n            elif pred_class == 0 and label == 1:\n                false_negatives.append((img, pred, label))\n    \n    return false_positives, false_negatives\n\nfalse_positives, false_negatives = analyze_errors(model, val_data, optimal_threshold)\n\nprint(f\"False Positives: {len(false_positives)}\")\nprint(f\"False Negatives: {len(false_negatives)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if len(false_positives) > 0:\n    fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n    fig.suptitle('False Positives', fontsize=16, fontweight='bold')\n    axes = axes.flatten()\n    \n    for i in range(min(8, len(false_positives))):\n        img, pred, label = false_positives[i]\n        axes[i].imshow(img)\n        axes[i].set_title(f'Confidence: {pred:.2%}')\n        axes[i].axis('off')\n    \n    for i in range(min(8, len(false_positives)), 8):\n        axes[i].axis('off')\n    \n    plt.tight_layout()\n    plt.savefig('false_positives.png', dpi=300, bbox_inches='tight')\n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if len(false_negatives) > 0:\n    fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n    fig.suptitle('False Negatives', fontsize=16, fontweight='bold')\n    axes = axes.flatten()\n    \n    for i in range(min(8, len(false_negatives))):\n        img, pred, label = false_negatives[i]\n        axes[i].imshow(img)\n        axes[i].set_title(f'Confidence: {pred:.2%}')\n        axes[i].axis('off')\n    \n    for i in range(min(8, len(false_negatives)), 8):\n        axes[i].axis('off')\n    \n    plt.tight_layout()\n    plt.savefig('false_negatives.png', dpi=300, bbox_inches='tight')\n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 15. Sample Predictions Visualization","metadata":{}},{"cell_type":"code","source":"for batch_idx in range(4):\n    imgs, labels = val_data[batch_idx]\n    pred_probs = np.squeeze(model.predict(imgs, verbose=0))\n    pred_classes = (pred_probs > optimal_threshold).astype(int)\n    \n    fig, axes = plt.subplots(1, 8, figsize=(16, 2))\n    \n    for i, (img, label, pred_cls, pred_prob) in enumerate(zip(imgs, labels, pred_classes, pred_probs)):\n        axes[i].imshow(img)\n        axes[i].set_xticks([])\n        axes[i].set_yticks([])\n        \n        color = 'green' if pred_cls == label else 'red'\n        axes[i].set_title(\n            f'True: {label}\\nPred: {pred_cls} ({pred_prob:.2%})',\n            fontsize=11,\n            color=color\n        )\n    \n    plt.tight_layout()\n    plt.savefig(f'predictions_batch_{batch_idx}.png', dpi=300, bbox_inches='tight')\n    plt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 16. Cross-Validation Analysis","metadata":{}},{"cell_type":"code","source":"def cross_validate_model(patient_ids, targets, n_splits=5):\n    skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=CONFIG['random_state'])\n    cv_scores = {\n        'accuracy': [],\n        'auc': [],\n        'f1': []\n    }\n    \n    for fold, (train_idx, val_idx) in enumerate(skf.split(patient_ids, targets)):\n        print(f\"\\nFold {fold + 1}/{n_splits}\")\n        \n        fold_train_ids = [patient_ids.iloc[i] for i in train_idx]\n        fold_train_targets = [targets.iloc[i] for i in train_idx]\n        fold_val_ids = [patient_ids.iloc[i] for i in val_idx]\n        fold_val_targets = [targets.iloc[i] for i in val_idx]\n        \n        fold_train_data = DataGenerator(\n            patient_id=fold_train_ids,\n            target_class=fold_train_targets,\n            batch_size=CONFIG['batch_size'],\n            size=CONFIG['image_size'],\n            augment=True\n        )\n        \n        fold_val_data = DataGenerator(\n            patient_id=fold_val_ids,\n            target_class=fold_val_targets,\n            batch_size=CONFIG['batch_size'],\n            size=CONFIG['image_size'],\n            augment=False\n        )\n        \n        fold_model = build_model(input_shape=(*CONFIG['image_size'], 3))\n        fold_model.compile(\n            optimizer=tf.keras.optimizers.Adam(learning_rate=CONFIG['learning_rate']),\n            loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),\n            metrics=['accuracy', tf.keras.metrics.AUC(name='auc')]\n        )\n        \n        fold_model.fit(\n            fold_train_data,\n            validation_data=fold_val_data,\n            epochs=10,\n            verbose=0\n        )\n        \n        y_true = []\n        y_pred_prob = []\n        \n        for i in range(len(fold_val_data)):\n            imgs, labels = fold_val_data[i]\n            predictions = fold_model.predict(imgs, verbose=0)\n            y_true.extend(labels)\n            y_pred_prob.extend(predictions.flatten())\n        \n        y_true = np.array(y_true)\n        y_pred_prob = np.array(y_pred_prob)\n        y_pred = (y_pred_prob > 0.5).astype(int)\n        \n        from sklearn.metrics import accuracy_score\n        \n        fold_accuracy = accuracy_score(y_true, y_pred)\n        fold_auc = auc(*roc_curve(y_true, y_pred_prob)[:2])\n        fold_f1 = f1_score(y_true, y_pred)\n        \n        cv_scores['accuracy'].append(fold_accuracy)\n        cv_scores['auc'].append(fold_auc)\n        cv_scores['f1'].append(fold_f1)\n        \n        print(f\"Fold {fold + 1} - Accuracy: {fold_accuracy:.4f}, AUC: {fold_auc:.4f}, F1: {fold_f1:.4f}\")\n    \n    return cv_scores","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 17. Model Export for Production","metadata":{}},{"cell_type":"code","source":"model.save('pneumonia_detector_final.h5')\nmodel.save('pneumonia_detector_final.keras')\n\nprint(\"Model saved in H5 and Keras formats\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"converter = tf.lite.TFLiteConverter.from_keras_model(model)\nconverter.optimizations = [tf.lite.Optimize.DEFAULT]\ntflite_model = converter.convert()\n\nwith open('pneumonia_detector.tflite', 'wb') as f:\n    f.write(tflite_model)\n\ntflite_size = len(tflite_model) / (1024 * 1024)\nprint(f\"TFLite model size: {tflite_size:.2f} MB\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tf.saved_model.save(model, 'pneumonia_detector_savedmodel')\nprint(\"Model exported in SavedModel format\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 18. Comprehensive Results Report","metadata":{}},{"cell_type":"code","source":"test_loss, test_accuracy, test_auc = model.evaluate(val_data, steps=len(val_data), verbose=1)\n\nprint(f\"\\nFinal Validation Metrics:\")\nprint(f\"Loss: {test_loss:.4f}\")\nprint(f\"Accuracy: {test_accuracy:.4f}\")\nprint(f\"AUC: {test_auc:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cm = eval_results['confusion_matrix']\ntn, fp, fn, tp = cm.ravel()\n\nsensitivity = tp / (tp + fn)\nspecificity = tn / (tn + fp)\nppv = tp / (tp + fp)\nnpv = tn / (tn + fn)\n\nresults = {\n    'metadata': {\n        'model_version': CONFIG['model_version'],\n        'architecture': 'ResNet50',\n        'timestamp': datetime.now().isoformat(),\n        'training_duration_minutes': training_duration / 60\n    },\n    'dataset': {\n        'total_samples': len(train_metadata_clean),\n        'training_samples': len(train_patient_id),\n        'validation_samples': len(val_patient_id),\n        'class_distribution': train_metadata_clean['Target'].value_counts().to_dict()\n    },\n    'hyperparameters': CONFIG,\n    'performance': {\n        'validation_loss': float(test_loss),\n        'validation_accuracy': float(test_accuracy),\n        'validation_auc': float(test_auc),\n        'roc_auc': float(eval_results['roc_auc']),\n        'average_precision': float(eval_results['avg_precision']),\n        'optimal_threshold': float(optimal_threshold),\n        'optimal_f1_score': float(optimal_f1)\n    },\n    'clinical_metrics': {\n        'sensitivity_recall': float(sensitivity),\n        'specificity': float(specificity),\n        'positive_predictive_value': float(ppv),\n        'negative_predictive_value': float(npv)\n    },\n    'confusion_matrix': {\n        'true_negatives': int(tn),\n        'false_positives': int(fp),\n        'false_negatives': int(fn),\n        'true_positives': int(tp)\n    },\n    'error_analysis': {\n        'false_positives_count': len(false_positives),\n        'false_negatives_count': len(false_negatives)\n    },\n    'training_history': {\n        'epochs_completed': len(history.history['loss']),\n        'final_train_loss': float(history.history['loss'][-1]),\n        'final_val_loss': float(history.history['val_loss'][-1]),\n        'best_val_loss': float(min(history.history['val_loss'])),\n        'best_val_auc': float(max(history.history['val_auc']))\n    }\n}\n\nwith open('model_results.json', 'w') as f:\n    json.dump(results, f, indent=2)\n\nprint(\"\\nResults saved to model_results.json\")\nprint(json.dumps(results, indent=2))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"summary_df = pd.DataFrame([\n    {'Metric': 'Validation Accuracy', 'Value': f\"{test_accuracy:.4f}\"},\n    {'Metric': 'Validation AUC', 'Value': f\"{test_auc:.4f}\"},\n    {'Metric': 'ROC AUC', 'Value': f\"{eval_results['roc_auc']:.4f}\"},\n    {'Metric': 'Optimal Threshold', 'Value': f\"{optimal_threshold:.4f}\"},\n    {'Metric': 'F1 Score (Optimal)', 'Value': f\"{optimal_f1:.4f}\"},\n    {'Metric': 'Sensitivity/Recall', 'Value': f\"{sensitivity:.4f}\"},\n    {'Metric': 'Specificity', 'Value': f\"{specificity:.4f}\"},\n    {'Metric': 'PPV', 'Value': f\"{ppv:.4f}\"},\n    {'Metric': 'NPV', 'Value': f\"{npv:.4f}\"},\n    {'Metric': 'Training Time (min)', 'Value': f\"{training_duration / 60:.2f}\"}\n])\n\nsummary_df.to_csv('model_summary.csv', index=False)\nprint(\"\\nModel Summary:\")\nprint(summary_df.to_string(index=False))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 19. Final Model Persistence","metadata":{}},{"cell_type":"code","source":"import pickle\n\nmodel_artifacts = {\n    'config': CONFIG,\n    'optimal_threshold': optimal_threshold,\n    'class_weights': class_weight_dict,\n    'results': results,\n    'history': history.history\n}\n\nwith open('model_artifacts.pkl', 'wb') as f:\n    pickle.dump(model_artifacts, f)\n\nprint(\"Model artifacts saved\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}