{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":"gpu","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"}],"dockerImageVersionId":30787,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# RSNA Pneumonia detection Challenge: Data augmentation\n\nIn this notebook we'll explore how to use data augmentation in our predictions using the pneumonia detection challenge from dicom files.\n\nCreating prediction model for medicine or any other field of computer vision has the limitation of the data size, usually we are limited to few training examples with the hope of generalizing our model, this is not enough and often lead to overfitting, meaning we have to some data manipulation to improve our model ability to generalize, this is what we'll see in this notebook","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nimport pydicom\nimport numpy as np\nimport os\nfrom sklearn.model_selection import train_test_split\nimport pandas as pd\nimport matplotlib.pyplot as plt","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:24:11.006540Z","iopub.execute_input":"2024-11-23T19:24:11.006825Z","iopub.status.idle":"2024-11-23T19:24:23.254243Z","shell.execute_reply.started":"2024-11-23T19:24:11.006797Z","shell.execute_reply":"2024-11-23T19:24:23.253526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_DIR = '/kaggle/input/rsna-pneumonia-detection-challenge/stage_2_train_images/'\ndf = pd.read_csv('/kaggle/input/rsna-pneumonia-detection-challenge/stage_2_train_labels.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:25:33.914949Z","iopub.execute_input":"2024-11-23T19:25:33.915562Z","iopub.status.idle":"2024-11-23T19:25:33.980784Z","shell.execute_reply.started":"2024-11-23T19:25:33.915528Z","shell.execute_reply":"2024-11-23T19:25:33.980073Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Let's create the functions to read the dicom images and map with the labels that are store in the csv file. We also prepare our dataset for training separating between training and validation datasets.","metadata":{}},{"cell_type":"code","source":"def read_dicom(file_path):\n    \"\"\"\n    Read a DICOM file and return as a tensor.\n    \"\"\"\n    dicom = pydicom.dcmread(file_path)\n    image = dicom.pixel_array\n    # Normalize pixel values to [0, 1]\n    image = (image - image.min()) / (image.max() - image.min())\n    return image\n\ndef create_dicom_dataset(base_dir, df, image_size, batch_size=20):\n    \"\"\"\n    Create a tf.data.Dataset from DICOM files with corresponding labels.\n    \"\"\"\n    def process_path(file_path, label):\n        # Read the DICOM file\n        image = tf.py_function(\n            lambda x: read_dicom(x.numpy().decode('utf-8')), \n            [file_path], \n            tf.float32\n        )\n        \n        # Ensure the image has the correct shape\n        image = tf.ensure_shape(image, (None, None))\n        \n        # Resize and add channel dimension\n        image = tf.image.resize(image[..., tf.newaxis], image_size)\n        \n        return image, label\n\n    # Create full file paths\n    file_paths = [os.path.join(base_dir, f\"{id}.dcm\") for id in df['patientId']]\n    labels = df['Target'].values\n\n    # Create dataset\n    dataset = tf.data.Dataset.from_tensor_slices((file_paths, labels))\n    \n    # Map the processing function\n    dataset = dataset.map(process_path, num_parallel_calls=tf.data.AUTOTUNE)\n    \n    # Batch and prefetch\n    dataset = dataset.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    \n    return dataset\n\ndef prepare_dataset(df, base_dir, image_size, batch_size=20, val_split=0.2):\n    \"\"\"\n    Prepare training and validation datasets.\n    \"\"\"\n    # Split into train and validation\n    train_df, val_df = train_test_split(\n        df, \n        test_size=val_split, \n        stratify=df['Target'],\n        random_state=42\n    )\n    \n    # Create datasets\n    train_dataset = create_dicom_dataset(\n        base_dir, \n        train_df, \n        image_size=image_size, \n        batch_size=batch_size\n    )\n    \n    val_dataset = create_dicom_dataset(\n        base_dir, \n        val_df, \n        image_size=image_size, \n        batch_size=batch_size\n    )\n    \n    return train_dataset, val_dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:25:51.853642Z","iopub.execute_input":"2024-11-23T19:25:51.853991Z","iopub.status.idle":"2024-11-23T19:25:51.862984Z","shell.execute_reply.started":"2024-11-23T19:25:51.853959Z","shell.execute_reply":"2024-11-23T19:25:51.862108Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Set augmentation functions and plot with labels so we can have a clear picture of what's going on in each step","metadata":{}},{"cell_type":"code","source":"def apply_single_augmentation(image, aug_name, aug_layer):\n    \"\"\"\n    Apply a single augmentation to an image\n    \"\"\"\n    return aug_layer(tf.expand_dims(image, 0), training=True)[0]\n\ndef demo_augmentation(image, num_augmentations=5):\n    \"\"\"\n    Display original and labeled augmented versions of an image\n    \"\"\"\n    # Calculate the number of rows and columns for the subplot grid\n    n_cols = 3  # 3 images per row\n    n_rows = (num_augmentations + 4) // 3  # Round up to include all augmentations plus original\n    \n    plt.figure(figsize=(15, 5 * n_rows))\n    \n    # Show original\n    plt.subplot(n_rows, n_cols, 1)\n    plt.imshow(tf.squeeze(image), cmap='gray')\n    plt.title('Original', pad=10, fontsize=12)\n    plt.axis('off')\n    \n    # Show each type of augmentation\n    for idx, (aug_name, aug_layer) in enumerate(augmentation_layers.items(), start=1):\n        plt.subplot(n_rows, n_cols, idx + 1)\n        augmented = apply_single_augmentation(image, aug_name, aug_layer)\n        plt.imshow(tf.squeeze(augmented), cmap='gray')\n        plt.title(aug_name, pad=10, fontsize=12)\n        plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\ndef show_batch_augmentations(dataset, num_images=4):\n    \"\"\"\n    Show labeled augmentations for multiple images from a batch\n    \"\"\"\n    # Get a batch of images\n    for images, _ in dataset.take(1):\n        break\n    \n    print(f'Images per batch: {len(images)}')\n    print(f'\\nShowing {len(augmentation_layers)} different types of augmentations for each image:')\n    print('- ' + '\\n- '.join(augmentation_layers.keys()))\n    \n    # Show augmentations for specified number of images\n    for i in range(min(num_images, len(images))):\n        print(f'\\nAugmentations for Image {i+1}:')\n        demo_augmentation(images[i])\n\ndef create_combined_augmentation(image, num_combinations=3):\n    \"\"\"\n    Create and display combinations of multiple augmentations\n    \"\"\"\n    plt.figure(figsize=(15, 5))\n    \n    # Show original\n    plt.subplot(1, 4, 1)\n    plt.imshow(tf.squeeze(image), cmap='gray')\n    plt.title('Original', pad=10, fontsize=12)\n    plt.axis('off')\n    \n    # Create combined augmentation model\n    combined_aug = tf.keras.Sequential(list(augmentation_layers.values()))\n    \n    # Show different combinations\n    for i in range(num_combinations):\n        augmented = combined_aug(tf.expand_dims(image, 0), training=True)[0]\n        plt.subplot(1, 4, i + 2)\n        plt.imshow(tf.squeeze(augmented), cmap='gray')\n        plt.title(f'Combined\\nAugmentation {i+1}', pad=10, fontsize=12)\n        plt.axis('off')\n    \n    plt.tight_layout()\n    plt.show()\n\ndef run_augmentation_demo(train_dataset):\n    \"\"\"\n    Run the complete augmentation demonstration with labels\n    \"\"\"\n    print(\"Demonstrating individual augmentations...\")\n    show_batch_augmentations(train_dataset, num_images=2)\n    \n    print(\"\\nDemonstrating combined augmentations...\")\n    for images, _ in train_dataset.take(1):\n        create_combined_augmentation(images[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:26:05.017064Z","iopub.execute_input":"2024-11-23T19:26:05.017748Z","iopub.status.idle":"2024-11-23T19:26:05.029454Z","shell.execute_reply.started":"2024-11-23T19:26:05.017716Z","shell.execute_reply":"2024-11-23T19:26:05.028235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create separate augmentation layers for clear labeling\n# Define fill mode.\nFILL_MODE = 'nearest'\naugmentation_layers = {\n    'Horizontal Flip': tf.keras.layers.RandomFlip('horizontal'),\n    'Rotation': tf.keras.layers.RandomRotation(0.2, fill_mode=FILL_MODE),\n    'Zoom': tf.keras.layers.RandomZoom(0.2, fill_mode=FILL_MODE),\n    'Translation': tf.keras.layers.RandomTranslation(0.2,0.2, fill_mode=FILL_MODE),\n    'Brightness': tf.keras.layers.RandomBrightness(0.2),\n    'Contrast': tf.keras.layers.RandomContrast(0.2)\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:26:13.081462Z","iopub.execute_input":"2024-11-23T19:26:13.082105Z","iopub.status.idle":"2024-11-23T19:26:13.883142Z","shell.execute_reply.started":"2024-11-23T19:26:13.082070Z","shell.execute_reply":"2024-11-23T19:26:13.882237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Assuming you have your DataFrame 'df' and BASE_DIR defined\ntrain_dataset, val_dataset = prepare_dataset(\n    df=df,\n    base_dir=BASE_DIR,\n    image_size=(300, 300),\n    batch_size=20\n)\n\n# Run the augmentation demonstration\nrun_augmentation_demo(train_dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:26:18.400025Z","iopub.execute_input":"2024-11-23T19:26:18.400883Z","iopub.status.idle":"2024-11-23T19:26:24.132955Z","shell.execute_reply.started":"2024-11-23T19:26:18.400847Z","shell.execute_reply":"2024-11-23T19:26:24.132078Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we can see not all augmentations makes sense in the context of X-ray, this is critical to keep in mind since data augmentation can hurt our model performance if we do it blindly.","metadata":{}},{"cell_type":"code","source":"def create_model(input_shape):\n  '''Builds a CNN for image binary classification'''\n\n  model = tf.keras.models.Sequential([\n      tf.keras.Input(input_shape),\n      # This will rescale the image to [0,1]\n      tf.keras.layers.Rescaling(1./255),\n      # This is the first convolution\n      tf.keras.layers.Conv2D(16, (3,3), activation='relu'),\n      tf.keras.layers.MaxPooling2D(2, 2),\n      # The second convolution\n      tf.keras.layers.Conv2D(32, (3,3), activation='relu'),\n      tf.keras.layers.MaxPooling2D(2,2),\n      # The third convolution\n      tf.keras.layers.Conv2D(64, (3,3), activation='relu'),\n      tf.keras.layers.MaxPooling2D(2,2),\n      # Flatten the results to feed into a DNN\n      tf.keras.layers.Flatten(),\n      # 512 neuron hidden layer\n      tf.keras.layers.Dense(512, activation='relu'),\n      # Only 1 output neuron. It will contain a value from 0-1 where 0 for one class ('horses') and 1 for the other ('humans')\n      tf.keras.layers.Dense(1, activation='sigmoid')\n      ])\n\n  return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:26:51.730956Z","iopub.execute_input":"2024-11-23T19:26:51.731798Z","iopub.status.idle":"2024-11-23T19:26:51.738125Z","shell.execute_reply.started":"2024-11-23T19:26:51.731763Z","shell.execute_reply":"2024-11-23T19:26:51.737213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_model_with_augmentation(base_model, input_shape):\n    \"\"\"\n    Create a model that combines augmentation layers with the base model\n    \n    Args:\n        base_model: The pre-created base model\n        input_shape: Input shape of the images (height, width, channels)\n    \n    Returns:\n        tf.keras.Model: Model with augmentation layers\n    \"\"\"\n    # Create the augmentation sequence\n    augmentation_sequence = tf.keras.Sequential([\n        tf.keras.layers.Input(shape=input_shape, name='input_layer')\n    ] + list(augmentation_layers.values()), name='augmentation_sequence')\n    \n    # Create the full model\n    model_with_aug = tf.keras.Sequential([\n        augmentation_sequence,\n        base_model\n    ])\n    \n    # Compile the model\n    model_with_aug.compile(\n        loss='binary_crossentropy',\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),\n        metrics=['accuracy']\n    )\n    \n    return model_with_aug\n\ndef print_model_summary(model):\n    \"\"\"\n    Print a detailed summary of the model architecture including augmentation layers\n    \"\"\"\n    print(\"\\nModel Architecture:\")\n    print(\"=\" * 50)\n    print(\"\\nAugmentation Layers:\")\n    for name in augmentation_layers.keys():\n        print(f\"- {name}\")\n    print(\"\\nFull Model Summary:\")\n    print(\"-\" * 50)\n    model.summary()\n\ndef setup_complete_model(input_shape):\n    \"\"\"\n    Set up both the base model and augmented model\n    \"\"\"\n    # Create base model\n    model_without_aug = create_model(input_shape)  # Your existing create_model function\n    \n    # Create model with augmentation\n    model_with_aug = create_model_with_augmentation(\n        base_model=model_without_aug,\n        input_shape=input_shape\n    )\n    \n    return model_without_aug, model_with_aug","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:31:30.587679Z","iopub.execute_input":"2024-11-23T19:31:30.588454Z","iopub.status.idle":"2024-11-23T19:31:30.595673Z","shell.execute_reply.started":"2024-11-23T19:31:30.588419Z","shell.execute_reply":"2024-11-23T19:31:30.594729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Setup both models\nmodel_without_aug, model_with_aug = setup_complete_model(input_shape=(300, 300, 1))\n\n# Print the model summary to verify the architecture\nprint_model_summary(model_with_aug)\n\n# Train the model\nhistory = model_with_aug.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=5,\n    callbacks=[\n        tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)\n    ]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:31:33.219684Z","iopub.execute_input":"2024-11-23T19:31:33.220528Z","iopub.status.idle":"2024-11-23T19:53:06.651562Z","shell.execute_reply.started":"2024-11-23T19:31:33.220491Z","shell.execute_reply":"2024-11-23T19:53:06.650547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_loss_acc(history):\n    '''Plots the training and validation loss and accuracy from a history object'''\n    acc = history.history['accuracy']\n    val_acc = history.history['val_accuracy']\n    loss = history.history['loss']\n    val_loss = history.history['val_loss']\n\n    epochs = range(len(acc))\n\n    fig, ax = plt.subplots(1,2, figsize=(12, 6))\n    ax[0].plot(epochs, acc, 'bo', label='Training accuracy')\n    ax[0].plot(epochs, val_acc, 'b', label='Validation accuracy')\n    ax[0].set_title('Training and validation accuracy')\n    ax[0].set_xlabel('epochs')\n    ax[0].set_ylabel('accuracy')\n    ax[0].legend()\n\n    ax[1].plot(epochs, loss, 'bo', label='Training Loss')\n    ax[1].plot(epochs, val_loss, 'b', label='Validation Loss')\n    ax[1].set_title('Training and validation loss')\n    ax[1].set_xlabel('epochs')\n    ax[1].set_ylabel('loss')\n    ax[1].legend()\n\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:53:15.110114Z","iopub.execute_input":"2024-11-23T19:53:15.110494Z","iopub.status.idle":"2024-11-23T19:53:15.117445Z","shell.execute_reply.started":"2024-11-23T19:53:15.110466Z","shell.execute_reply":"2024-11-23T19:53:15.116507Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot training results\nplot_loss_acc(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:53:17.545721Z","iopub.execute_input":"2024-11-23T19:53:17.546548Z","iopub.status.idle":"2024-11-23T19:53:17.985861Z","shell.execute_reply.started":"2024-11-23T19:53:17.546512Z","shell.execute_reply":"2024-11-23T19:53:17.985006Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The results shows accuracy of around 0.68 in the training set and similar for validation dataset, but there are no improvements accross epochs, even we only set to 5 as we want to evaluate first the behaviour before training a larger model.","metadata":{}},{"cell_type":"code","source":"# Setup both models\nmodel_without_aug, model_with_aug = setup_complete_model(input_shape=(300, 300, 1))\n\n    # Compile the model\nmodel_without_aug.compile(\n    loss='binary_crossentropy',\n    optimizer=tf.keras.optimizers.RMSprop(learning_rate=1e-3),\n    metrics=['accuracy']\n)\n\nhistory = model_without_aug.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=5,\n    callbacks=[\n        tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True)\n    ]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:53:38.806803Z","iopub.execute_input":"2024-11-23T19:53:38.807395Z","iopub.status.idle":"2024-11-23T20:13:14.749400Z","shell.execute_reply.started":"2024-11-23T19:53:38.807360Z","shell.execute_reply":"2024-11-23T20:13:14.748614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot training results\nplot_loss_acc(history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-23T19:53:06.653362Z","iopub.execute_input":"2024-11-23T19:53:06.653750Z","iopub.status.idle":"2024-11-23T19:53:07.131939Z","shell.execute_reply.started":"2024-11-23T19:53:06.653711Z","shell.execute_reply":"2024-11-23T19:53:07.130713Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we can see the data augmentation actually hurt the model performance, this can be explain as we blindly assign data augmented features to the image, we also need to account for the fact that this are X-ray and some features might be needed and some other won't.","metadata":{}},{"cell_type":"markdown","source":"**To do**\n\n- Use the same evaluation metric than the competition\n- Run a test with the testing data\n- Use winning results from previous competitions","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}}]}