{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":11848,"databundleVersionId":862157,"sourceType":"competition"},{"sourceId":344693,"sourceType":"modelInstanceVersion","modelInstanceId":288148,"modelId":308923},{"sourceId":344695,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":288150,"modelId":308923}],"dockerImageVersionId":31011,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Histopathologic Cancer Detection Project\n\nUniversity of Colorado Boulder\n\nDTSA 5511 Week 5\n\n## 1. Introduction \n\nThis notebook tackles the Histopathologic Cancer Detection challenge, where the goal is to indentify metastatic cancer in images and classify cancerous and non-cancerous cells based on small 32x32px patches in a larger 96x96 pixel image dataset.\n\nI aim to develop and compare two convolutional neural network (CNN) architectures. The first will be a baseline CNN with standard convolutional, pooling, and dense layers. The second architecture will introduce batch normalization to improve peformance and compare how the model performs when the layer activations are normalized. In theory, the batch normalization will help the model focus on the most important patterns needed to recognize cancer while being able to essentially ignore the variation in cell-staining intensity and color birghtness in the images, resulting in higher accuracy.\n\n### 1.1 Problem Statement\n\nMetastatic cancer detection from pathology scans is critical to cancer diagnosis. This project attempts to develop a more efficient way for pathologists to identify metastatic cells than the time-consuming and error-prone method of manually reviewing slides.\n\nThe task is a binary image classification problem with inputs of small image patches from larger pathology scans and outputs of binary predictions (0 or 1) indicating the presence of tumor tissue. A label of 0 is negative for cancer, while a label of 1 is positive for cancer.\n\n### 1.2 Data Description\n\nThe dataset is a modified version of the PatchCamelyon (PCam) benchmark dataset where images are small patches taken from histopathology scans. In this dataset, a positive label (1) indicates that the center 32x32px region contains at least one pixel of tumor tissue. The outer region provides context for models that don't use zero-padding, and tumor tissue in the outer region does not influence the label. It is important to note that this dataset does not contain duplicate images, which differs from the original PatchCamelyon dataset.","metadata":{"_uuid":"ef30d602-9a58-44f2-8ee9-424bc94e089d","_cell_guid":"57ad46dc-4c70-4edb-a09b-f19958aad8cb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## 2. Setup\n\nNote: I decided not to use the accelerator T4s because my implementation of ImageDataGenerator was not optimized for parallelization, which resulted in low GPU usage (<5%). Being that this is a homework assignment, the optimization is not super important initially, but I would like to revisit this and refactor some things. The time spent setting up the compute ended up not being worth the time saved, but I kept it in this notebook in case someone (possibly me) would like to improve it. Also, it serves as a good reference for later use. Can I make this text smaller?","metadata":{"_uuid":"e293681b-fff2-4883-8f5f-9523b246c5f8","_cell_guid":"3f8f6b9b-5285-423f-bbe4-8b4453690430","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# Basic Imports\nimport os\nimport numpy as np\nimport pandas as pd\n\n# Plotting Tools\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# TensorFlow\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras import layers, models, callbacks\n\n# sklearn stuff\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc","metadata":{"_uuid":"d0aa2e4c-43ca-4fb9-b283-c411ac916041","_cell_guid":"fb8a518d-0d1d-4d6b-b9f6-c88ad2dca85f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:29.654675Z","iopub.execute_input":"2026-08-16T20:05:29.654991Z","iopub.status.idle":"2026-08-16T20:05:43.039023Z","shell.execute_reply.started":"2026-08-16T20:05:29.654947Z","shell.execute_reply":"2026-08-16T20:05:43.038357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# setting random seed for use throughout the notebook\nRANDOM_SEED = 19\nnp.random.seed(RANDOM_SEED)\ntf.random.set_seed(RANDOM_SEED)","metadata":{"_uuid":"0edbd027-21a2-4868-b676-ff76145ebb0e","_cell_guid":"16cb5e20-55c0-4cf3-a18d-eb6f44d5c7d0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:43.040307Z","iopub.execute_input":"2026-08-16T20:05:43.041052Z","iopub.status.idle":"2026-08-16T20:05:43.045484Z","shell.execute_reply.started":"2026-08-16T20:05:43.041031Z","shell.execute_reply":"2026-08-16T20:05:43.044603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# check for available GPUs\ngpus = tf.config.list_physical_devices('GPU')\nprint(f\"Num GPUs Available: {len(gpus)}\")","metadata":{"_uuid":"955b1c66-27e8-441b-8381-48c3fb6b4a1c","_cell_guid":"c95a3a78-6d90-4013-b084-b585d44b46c4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:43.046476Z","iopub.execute_input":"2026-08-16T20:05:43.046758Z","iopub.status.idle":"2026-08-16T20:05:43.847427Z","shell.execute_reply.started":"2026-08-16T20:05:43.046733Z","shell.execute_reply":"2026-08-16T20:05:43.846335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# set up strategy for use with two NVIDIA T4s, if available\nif gpus:\n\n    # configure memory growth to avoid memory allocation issues\n    for gpu in gpus:\n        tf.config.experimental.set_memory_growth(gpu, True)\n    \n    # setup MirroredStrategy on the T4s\n    strategy = tf.distribute.MirroredStrategy()\n    print(f\"Using MirroredStrategy with {strategy.num_replicas_in_sync} T4 GPUs\")\n    \n    # enable mixed precision for optimzied training\n    tf.keras.mixed_precision.set_global_policy('mixed_float16')\n\nelse: # use defaults\n    strategy = tf.distribute.OneDeviceStrategy(device=\"/cpu:0\")\n    print(\"No GPUs available, using Kaggle standard CPU\")\n    tf.keras.mixed_precision.set_global_policy('float32')","metadata":{"_uuid":"cf1a6a98-2d20-4544-b1d6-fd0ba884774b","_cell_guid":"8b26de34-2def-4027-93a0-6057408acbf4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:43.848365Z","iopub.execute_input":"2026-08-16T20:05:43.848665Z","iopub.status.idle":"2026-08-16T20:05:44.001328Z","shell.execute_reply.started":"2026-08-16T20:05:43.848637Z","shell.execute_reply":"2026-08-16T20:05:44.000325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# load data\nTRAIN_DIR = '/kaggle/input/histopathologic-cancer-detection/train/'\nTEST_DIR = '/kaggle/input/histopathologic-cancer-detection/test/'\nTRAIN_LABELS_PATH = '/kaggle/input/histopathologic-cancer-detection/train_labels.csv'\n\ntrain_labels = pd.read_csv(TRAIN_LABELS_PATH)\nprint(f\"Training labels shape: {train_labels.shape}\")\nprint(train_labels.head())","metadata":{"_uuid":"95e30a8e-4c6c-4d6b-ae22-f02bb3560f99","_cell_guid":"7b7ba2d5-c566-41da-a815-edca583effd2","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:44.003304Z","iopub.execute_input":"2026-08-16T20:05:44.003597Z","iopub.status.idle":"2026-08-16T20:05:44.393622Z","shell.execute_reply.started":"2026-08-16T20:05:44.003579Z","shell.execute_reply":"2026-08-16T20:05:44.392938Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# check class distribution\nclass_distribution = train_labels['label'].value_counts(normalize=True) * 100\nprint(f\"\\nClass distribution (%):\\n{class_distribution}\")","metadata":{"_uuid":"737e04ac-bcb2-45fe-9333-7a7c1621855c","_cell_guid":"ce482bbf-2a70-42fd-8eb5-df9b3bee1ae9","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:44.394419Z","iopub.execute_input":"2026-08-16T20:05:44.394678Z","iopub.status.idle":"2026-08-16T20:05:44.405395Z","shell.execute_reply.started":"2026-08-16T20:05:44.394651Z","shell.execute_reply":"2026-08-16T20:05:44.404805Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. EDA","metadata":{"_uuid":"bb3313d3-80e5-4d10-8f51-f451fdb3c7c0","_cell_guid":"5866561c-5ac1-4913-a2c6-c3c668d3162b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# sample images from each class\npos_samples = train_labels[train_labels['label'] == 1].sample(2)\nneg_samples = train_labels[train_labels['label'] == 0].sample(2)\nsample_df = pd.concat([pos_samples, neg_samples])\n\n# add file extension to path & convert to string for Keras\nsample_df['filename'] = sample_df['id'] + '.tif'\nsample_df['label_str'] = sample_df['label'].astype(str)\n\n# plot images\nplt.figure(figsize=(15, 8))\nfor i, (_, row) in enumerate(sample_df.iterrows()):\n    img_path = os.path.join(TRAIN_DIR, row['filename'])\n    img = plt.imread(img_path)/255   \n    plt.subplot(2, 4, i+1)\n    plt.imshow(img)\n    plt.title(f\"Label: {row['label']}\")\n    plt.axis('off')\nplt.tight_layout()\nplt.show()\n\n# plot dist\nplt.figure(figsize=(8, 6))\nsns.countplot(x='label', data=train_labels)\nplt.title('Training Labels Distribution')\nplt.xlabel('Label (0 = No Cancer, 1 = Cancer)')\nplt.ylabel('Count')\nplt.show()","metadata":{"_uuid":"70b89494-421d-4149-a94c-94b81c66dae0","_cell_guid":"bd7975bf-72be-4f7c-a315-f57eba734e69","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:44.406186Z","iopub.execute_input":"2026-08-16T20:05:44.406513Z","iopub.status.idle":"2026-08-16T20:05:45.131230Z","shell.execute_reply.started":"2026-08-16T20:05:44.406495Z","shell.execute_reply":"2026-08-16T20:05:45.130514Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### EDA Results\nThe data exhibits a class imbalance of 59.5% negative to 40.5% positive for cancer. I do not believe that this is a significant class imbalance that warrants up or down sampling for this task. All images have standard dimensions, but from a cursory view of some samples, the variation in color and amount of nonnegative space varies wildly.","metadata":{}},{"cell_type":"markdown","source":"## 4. Preprocessing","metadata":{"_uuid":"10e8fa6b-4004-486b-99de-4d42dcab6086","_cell_guid":"eaf424fa-218a-4ae2-8ed5-bee8b2458991","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"train_data = train_labels.copy() # uncomment for full run\n\n# add file extension to path & convert to string for Keras\ntrain_data['filename'] = train_data['id'] + '.tif'\ntrain_data['label_str'] = train_data['label'].astype(str)","metadata":{"_uuid":"732286c4-ef38-4e08-a0e6-6b173e40fb89","_cell_guid":"fcaf19ec-42e7-4731-993e-344bd82fdbb7","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:45.132084Z","iopub.execute_input":"2026-08-16T20:05:45.132374Z","iopub.status.idle":"2026-08-16T20:05:45.218193Z","shell.execute_reply.started":"2026-08-16T20:05:45.132328Z","shell.execute_reply":"2026-08-16T20:05:45.217214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# set batch size to match strategy\nif gpus:\n    replicas = strategy.num_replicas_in_sync\n    BATCH_SIZE = 128 * replicas # should be fine\n    print(f\"Using batch size of {BATCH_SIZE} with {replicas} GPU(s)\")\nelse:\n    BATCH_SIZE = 64\n    print(f\"Using default batch size of {BATCH_SIZE}\")\n\n# size of the images\nIMG_SIZE = (32, 32)","metadata":{"_uuid":"f42f3137-5dc5-4fac-a77c-1042db799e97","_cell_guid":"581602d5-0a6c-47e5-adad-7c7bbc3f48c0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:45.219201Z","iopub.execute_input":"2026-08-16T20:05:45.219517Z","iopub.status.idle":"2026-08-16T20:05:45.224668Z","shell.execute_reply.started":"2026-08-16T20:05:45.219483Z","shell.execute_reply":"2026-08-16T20:05:45.223962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_center(img):\n    # center coordinates\n    h, w = img.shape[0], img.shape[1]\n    center_h, center_w = h // 2, w // 2\n    offset = 16  # 32/2 pixels\n    \n    # get center 32x32 region\n    center_img = img[center_h-offset:center_h+offset, center_w-offset:center_w+offset, :]\n    return center_img","metadata":{"_uuid":"c64a2d96-7188-4587-8817-2e87db477923","_cell_guid":"51e7b05f-fcc2-4995-b4b9-bff9bb511e18","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:05:45.225409Z","iopub.execute_input":"2026-08-16T20:05:45.225992Z","iopub.status.idle":"2026-08-16T20:05:45.237095Z","shell.execute_reply.started":"2026-08-16T20:05:45.225972Z","shell.execute_reply":"2026-08-16T20:05:45.236335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Extracting the center 32x32 pixels removes context of the larger image, but improves training time by quite a bit. ","metadata":{}},{"cell_type":"code","source":"datagen = ImageDataGenerator(\n    rescale=1./255,\n    preprocessing_function=extract_center,\n    validation_split=0.2\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-16T20:05:45.237769Z","iopub.execute_input":"2026-08-16T20:05:45.238074Z","iopub.status.idle":"2026-08-16T20:05:45.249078Z","shell.execute_reply.started":"2026-08-16T20:05:45.238059Z","shell.execute_reply":"2026-08-16T20:05:45.248157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_generator = datagen.flow_from_dataframe(\n    dataframe=train_data,\n    directory=TRAIN_DIR,\n    x_col='filename',\n    y_col='label_str',\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    subset='training',\n    shuffle=True\n)\n\nprint(f\"Training generator batches: {len(train_generator)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-16T20:05:45.249790Z","iopub.execute_input":"2026-08-16T20:05:45.250605Z","iopub.status.idle":"2026-08-16T20:17:13.384119Z","shell.execute_reply.started":"2026-08-16T20:05:45.250579Z","shell.execute_reply":"2026-08-16T20:17:13.383147Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"validation_generator = datagen.flow_from_dataframe(\n    dataframe=train_data,\n    directory=TRAIN_DIR,\n    x_col='filename',\n    y_col='label_str',\n    target_size=IMG_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode='binary',\n    subset='validation',\n    shuffle=False\n)\n\n\nprint(f\"Validation generator batches: {len(validation_generator)}\")","metadata":{"_uuid":"f2c3e253-ee91-45df-8d82-215b42b2d61c","_cell_guid":"e682e8c4-0cf5-4001-adb2-ce3ed0f38f10","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:17:13.385041Z","iopub.execute_input":"2026-08-16T20:17:13.385802Z","iopub.status.idle":"2026-08-16T20:22:41.106232Z","shell.execute_reply.started":"2026-08-16T20:17:13.385779Z","shell.execute_reply":"2026-08-16T20:22:41.105493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# calculate steps for training\nsteps_per_epoch = int(np.ceil(train_generator.samples / train_generator.batch_size))\nvalidation_steps = int(np.ceil(validation_generator.samples / validation_generator.batch_size))\n\nprint(f\"Steps per epoch: {steps_per_epoch}\")\nprint(f\"Validation steps: {validation_steps}\")","metadata":{"_uuid":"b9017ce7-713c-4e9c-bcf7-5fa3ff9897bb","_cell_guid":"2e5546cc-63f6-4c32-b2a2-b30638f29f6e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:22:41.109086Z","iopub.execute_input":"2026-08-16T20:22:41.109275Z","iopub.status.idle":"2026-08-16T20:22:41.114324Z","shell.execute_reply.started":"2026-08-16T20:22:41.109260Z","shell.execute_reply":"2026-08-16T20:22:41.113647Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Model Development - Base CNN","metadata":{"_uuid":"286100dd-e386-4c3d-ae72-8dcb8f16250e","_cell_guid":"a66cd4a8-2add-4df5-bcd9-24ecfab016bc","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"Base architecture with 2 convolutional blocks, relu activation function, consistent padding, and possibility for more efficient training with parallelized strategy on NVIDIA T4s.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def create_baseline_model():\n        model = models.Sequential([\n            \n            # 1st convolutional block\n            layers.Input(shape=(32, 32, 3)),\n            layers.Conv2D(64, (3, 3), padding='same'),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # 2nd convolutional block\n            layers.Conv2D(128, (3, 3), padding='same'),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # 3rd convolutional block\n            layers.Conv2D(256, (3, 3), padding='same'),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # flatten and dense layers\n            layers.Flatten(),\n            layers.Dense(256),\n            layers.Activation('relu'),\n            #__ dropout(0.5) is a destructive overfitting fixer, which disables half\n            #of the parameters when overfitting is detected\n            layers.Dropout(0.5),\n            \n            layers.Dense(1, activation='sigmoid', dtype='float32')\n        ])\n        #__ adam optimizer and reduceLROnPlateu work together to multiply the learning rate by 0.2 when improvement stops (+1-2 accuracy points)\n        model.compile(\n            optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n            loss='binary_crossentropy',\n            metrics=['accuracy', tf.keras.metrics.AUC()]\n        )\n        \n        return model\n    \n    baseline_model = create_baseline_model()\n    baseline_model.name = \"Baseline\"  \n\nprint(\"Baseline CNN Model Summary:\")\nbaseline_model.summary()","metadata":{"_uuid":"ab16b81a-39c9-40ca-9f6a-1813e59d8973","_cell_guid":"412c2bc2-b617-426d-869f-c2898309bbfb","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:22:41.115115Z","iopub.execute_input":"2026-08-16T20:22:41.115470Z","iopub.status.idle":"2026-08-16T20:22:42.368532Z","shell.execute_reply.started":"2026-08-16T20:22:41.115442Z","shell.execute_reply":"2026-08-16T20:22:42.367979Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"## 6. Model Development - CNN with Batch Normalization","metadata":{"_uuid":"0b0866d0-c833-486a-b550-95ecb5ec6485","_cell_guid":"9c7f8a0a-281f-4715-be16-93ab45d14f00","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"\"Experimental\" architecture with the same architecture as the baseline model but added batch normalization to allow improved learning, overfitting reduction, and mitigating possible weighting imbalances due to color intensity in the images.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    def create_batchnorm_model():\n        model = models.Sequential([\n\n#__ added batch normalization to each convolutional block, +6 accuracy points\n#__ also fixed pooling to (2,2) with 3 pooling stages so that chunk pattern recognition\n#is guaranteed instead of memorized\n            # 1st convolutional block with batch normalization\n            layers.Input(shape=(32, 32, 3)),\n            layers.Conv2D(64, (3, 3), padding='same'),\n            layers.BatchNormalization(),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # 2nd convolutional block with batch normalization\n            layers.Conv2D(128, (3, 3), padding='same'),\n            layers.BatchNormalization(),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # 3rd convolutional block with batch normalization\n            layers.Conv2D(256, (3, 3), padding='same'),\n            layers.BatchNormalization(),\n            layers.Activation('relu'),\n            layers.MaxPooling2D((2, 2)),\n            \n            # flatten and dense layers\n            layers.Flatten(),\n            layers.Dense(256),\n            layers.BatchNormalization(),\n            layers.Activation('relu'),\n            layers.Dropout(0.5),\n\n            layers.Dense(1, activation='sigmoid', dtype='float32')  \n        ])\n        \n        model.compile(\n            optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n            loss='binary_crossentropy',\n            metrics=['accuracy', tf.keras.metrics.AUC()]\n        )\n        \n        return model\n    \n    batchnorm_model = create_batchnorm_model()\n    batchnorm_model.name = \"BatchNorm\"  \n\nprint(\"BatchNorm CNN Model Summary:\")\nbatchnorm_model.summary()","metadata":{"_uuid":"cb95b114-f78b-4c18-8e62-8c7def841d9c","_cell_guid":"f525a7fe-7b12-4c40-a5bc-cbaf5304bd91","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:22:42.369222Z","iopub.execute_input":"2026-08-16T20:22:42.369745Z","iopub.status.idle":"2026-08-16T20:22:42.478163Z","shell.execute_reply.started":"2026-08-16T20:22:42.369726Z","shell.execute_reply":"2026-08-16T20:22:42.477594Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Model Training and Evaluation","metadata":{"_uuid":"52fa5347-b505-44e0-ab30-3f723760990c","_cell_guid":"ac7ee51b-561b-4a5a-866e-115bfc756832","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# callbacks \ncallbacks_list = [\n    #__ add early stopping (+0.05 accuracy points)\n    callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=5,\n        restore_best_weights=True\n    ),\n    callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.2,\n        patience=3,\n        min_lr=1e-6\n    ),\n    # save best model\n    callbacks.ModelCheckpoint(\n        filepath='best_baseline_model.keras',\n        monitor='val_accuracy',\n        save_best_only=True,\n        verbose=1\n    )\n]\n\n# callbacks for the BatchNorm model\nbatchnorm_callbacks = callbacks_list.copy()\n\n# ModelCheckpoint for BatchNorm\nbatchnorm_callbacks[-1] = callbacks.ModelCheckpoint(\n    filepath='best_batchnorm_model.keras',\n    monitor='val_accuracy',\n    save_best_only=True,\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-16T20:22:42.478782Z","iopub.execute_input":"2026-08-16T20:22:42.479044Z","iopub.status.idle":"2026-08-16T20:22:42.484647Z","shell.execute_reply.started":"2026-08-16T20:22:42.479028Z","shell.execute_reply":"2026-08-16T20:22:42.484058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# train baseline model \nprint(\"\\nTraining Baseline CNN Model...\")\nbaseline_history = baseline_model.fit(\n    train_generator,\n    epochs=50,\n    steps_per_epoch=steps_per_epoch,\n    validation_data=validation_generator,\n    validation_steps=validation_steps,\n    callbacks=callbacks_list,\n    verbose=1\n)","metadata":{"_uuid":"1a5474f8-0cc5-4c3b-aae9-815f0e8a1bd5","_cell_guid":"d6b147b7-d84f-4a08-8eb9-5e5c340ce479","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-08-16T20:22:42.485511Z","iopub.execute_input":"2026-08-16T20:22:42.486048Z","execution_failed":"2026-08-17T02:13:13.196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# train batch normalization model\nprint(\"\\nTraining BatchNorm CNN Model...\")\nbatchnorm_history = batchnorm_model.fit(\n    train_generator,\n    epochs=50,\n    steps_per_epoch=steps_per_epoch,\n    validation_data=validation_generator,\n    validation_steps=validation_steps,\n    callbacks=batchnorm_callbacks,\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.206Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. Results Comparison","metadata":{"_uuid":"7acda2e5-ff8c-4363-9aa1-214571faee8f","_cell_guid":"629c5a97-4bee-4cca-88b9-fa89450850c2","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# load models & evaluate on validation data\nbaseline_model = tf.keras.models.load_model('/kaggle/working/best_baseline_model.keras')\nbatchnorm_model = tf.keras.models.load_model('/kaggle/working/best_batchnorm_model.keras')\n\nbaseline_results = baseline_model.evaluate(validation_generator, verbose=1)\nbatchnorm_results = batchnorm_model.evaluate(validation_generator, verbose=1)\n\nmetric_names = baseline_model.metrics_names\nmetric_names[1] = 'accuracy'","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.208Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# vizz\ndef visualize_model_performance(model, generator, results):\n        \n    generator.reset()\n    prediction_batch_size = BATCH_SIZE * 2\n    prediction_steps = (generator.samples + prediction_batch_size - 1) // prediction_batch_size\n    \n    # new generator with larger batch size for prediction\n    temp_generator = datagen.flow_from_dataframe(\n        dataframe=train_data,\n        directory=TRAIN_DIR,\n        x_col='filename',\n        y_col='label_str',\n        target_size=IMG_SIZE,\n        batch_size=prediction_batch_size,\n        class_mode='binary',\n        subset='validation',\n        shuffle=False\n    )\n    \n    # predict\n    y_pred = model.predict(temp_generator, steps=prediction_steps, verbose=1)\n    y_pred_classes = (y_pred > 0.5).astype(int).flatten()\n    \n    # get labels\n    y_true = generator.classes[:len(y_pred_classes)]\n    \n    # confusion matrix\n    cm = confusion_matrix(y_true, y_pred_classes)\n\n    plt.figure(figsize=(6, 5))\n    sns.heatmap(\n        cm, \n        annot=True, \n        fmt='d', \n        cmap='Blues',\n        xticklabels=['Non-tumor', 'Tumor'],\n        yticklabels=['Non-tumor', 'Tumor']\n    )\n    plt.title(f'Confusion Matrix - {model.name}')\n    plt.ylabel('True Label')\n    plt.xlabel('Predicted Label')\n    plt.tight_layout()\n    plt.show()\n    \n    # ROC AUC\n    fpr, tpr, _ = roc_curve(y_true, y_pred)\n    roc_auc = auc(fpr, tpr)\n    \n    plt.figure(figsize=(6, 5))\n    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc})')\n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('False Positive Rate')\n    plt.ylabel('True Positive Rate')\n    plt.title(f'ROC Curve - {model.name}')\n    plt.legend(loc=\"lower right\")\n    plt.tight_layout()\n    plt.show()\n    \n    print(\"\\nClassification Report:\")\n    print(classification_report(y_true, y_pred_classes))\n    \n    return roc_auc","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.209Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get additional metrics (AUC)\nbaseline_auc = visualize_model_performance(baseline_model, validation_generator, baseline_results)\nbatchnorm_auc = visualize_model_performance(batchnorm_model, validation_generator, batchnorm_results)","metadata":{"_uuid":"b572ab82-60b2-4f93-b9b2-efe02ffa7f37","_cell_guid":"b5885c80-52d4-448b-ad17-d6747b7ccc6e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-08-17T02:13:13.211Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"From the results, the batch norm model performs much better. Something that stood out to me was the difference in false positive rate between the models as shown in the ROC curve and the confusion matrix. The batch norm model classified more false negatives and fewer false positives than the baseline model. This may not be the best for medical applications with dire consequences for misdiagnoses.","metadata":{}},{"cell_type":"code","source":"# results table\ncomparison_df = pd.DataFrame({\n    'Model': ['Baseline CNN', 'BatchNorm CNN'],\n    'Validation Loss': [baseline_results[0], batchnorm_results[0]],\n    'Validation Accuracy': [baseline_results[1], batchnorm_results[1]],\n    #'AUC': [baseline_results[2], batchnorm_results[2]],\n    'AUC': [baseline_auc, batchnorm_auc]\n})\n\nprint(\"\\nModel Performance Comparison:\")\nprint(comparison_df)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.212Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# compare metrics\nmetrics = ['Validation Loss', 'Validation Accuracy', 'AUC']\nplt.figure(figsize=(12, 5))\n\nfor i, metric in enumerate(metrics):\n    plt.subplot(1, 3, i+1)\n    plt.bar(['Baseline', 'BatchNorm'], comparison_df[metric])\n    plt.title(metric)\n    plt.ylim(0 if metric == 'Validation Loss' else 0.5, 1.0)\n    \n    for j, v in enumerate(comparison_df[metric]):\n        plt.text(j, v + 0.02, f\"{v}\", ha='center')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.220Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Validation loss, validation accuracy, and AUC are all improved from the baseline model when batch normalization is introduced. However, the confusion matrices above show an increase in problematic false negatives. The trade off is more correctly classified images overall.","metadata":{}},{"cell_type":"markdown","source":"## 9. Submission","metadata":{"_uuid":"a74b2c66-39fa-4c6e-865d-0ce2be735713","_cell_guid":"e15a4e72-da1f-4d8c-9406-6920881d53e4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"if baseline_results[1] > batchnorm_results[1]:\n    best_model = baseline_model\n    print(f\"Using Baseline model for submission (accuracy: {baseline_results[1]})\")\nelse:\n    best_model = batchnorm_model\n    print(f\"Using BatchNorm model for submission (accuracy: {batchnorm_results[1]})\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# test data generator\ntest_datagen = ImageDataGenerator(rescale=1./255)\n\n# test file names DF\ntest_files = os.listdir(TEST_DIR)\ntest_df = pd.DataFrame({\n    'id': [os.path.splitext(file)[0] for file in test_files],\n    'filename': test_files\n})","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# make test data gen\nsubmission_batch_size = BATCH_SIZE * 2 \n\ntest_generator = test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    directory=TEST_DIR,\n    x_col='filename',\n    y_col=None,  # no labels for test data\n    target_size=IMG_SIZE,\n    batch_size=submission_batch_size,\n    class_mode=None,\n    shuffle=False\n)","metadata":{"_uuid":"40795dd7-a02d-4132-8826-ccc744ca8986","_cell_guid":"10a6a275-f4fd-4027-bc1c-37211c498d47","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-08-17T02:13:13.228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# predict\nprint(\"\\nGenerating predictions for submission...\")\npredictions = best_model.predict(\n    test_generator,\n    verbose=1\n)\npredicted_classes = (predictions > 0.5).astype(int).flatten()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# save\nsubmission_df = pd.DataFrame({\n    'id': test_df['id'][:len(predicted_classes)],\n    'label': predicted_classes\n})\n\nsubmission_path = 'submission.csv'\nsubmission_df.to_csv(submission_path, index=False)\nprint(f\"Submission saved to {submission_path}\")\nprint(f\"Sample of submission file:\\n{submission_df.head()}\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-17T02:13:13.229Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Conclusion and Future Directions\n\n### Summary\nIn this project, I completed the challenge of detecting metastatic cancer by developing convolutional neural networks that learn from the center 32x32 pixels of larger 96x96 pixel histopathology scans. The core focus was to compare an simple architecture with standard convolutional, pooling, and dense layers to the same architecture with batch normalization added after each convolutional and dense layer.\n\n### Results\nComparing that architectures revealed interesting results. The batchnorm model achieved higher final validation accuracy of 0.91 compared to the baseline model's 0.85 accuracy. Additionally, the architecture with batch normalization achieved a higher AUC (0.97) than the baseline model (0.93). Both models successfully learned to distinguish between cancerous cells with great accuracy.\n\nThese results confirm the theoretical advantages of batch normalization in medical imaging tasks. By normalizing activations within each batch, the model better handled the variation in tissue staining and color intensity.\n\n### Significance\nThe results from this project exhibit that simple modifications to model architecture can significantly improve performance in classification tasks, at least in this type task using medical imaging.\n\n### Future Work\nIn future work, various model improvements may be attempted such as implementing data augmentation that does not lose information from the center 32x32 pixel region to improve generalization of the model and trying different activation functions. Moreover, it is possibly worthwhile to develop explainable AI methods to assists in understanding model decisions by marking the model's areas of attention.\n\n\nIf you made it this far, I appreciate constructive input!","metadata":{"_uuid":"edfa436e-ec4f-4d7c-805b-f083bad46ef5","_cell_guid":"b9a44eb0-a4f7-4616-820c-1e194084563d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}}]}