{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":11848,"databundleVersionId":862157,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CNN for Cancer Detection\n**Binary image classification of metastatic cancer in tissue samples using a convolutional neural network (CNN).**","metadata":{}},{"cell_type":"code","source":"# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        os.path.join(dirname, filename)\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport cv2\nfrom glob import glob\nfrom PIL import Image\nimport random\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.models import Sequential, Model\nfrom tensorflow.keras.layers import Input, Conv2D, BatchNormalization, Activation, MaxPooling2D, Dropout, Flatten, Dense, Multiply, GlobalAveragePooling2D\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define directories\ndirectory  = '../input/histopathologic-cancer-detection'  \nlabels_path = os.path.join(directory, 'train_labels.csv')\ntrain_path = os.path.join(directory, 'train')\ntest_path = os.path.join(directory, 'test')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('There are ',len(os.listdir(train_path)), 'training images and ',len(os.listdir(test_path)), 'testing images.')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# load the CSV file containing labels\nlabels = pd.read_csv(labels_path)\n\n# create a DataFrame to match training data images with labels\ntrain = pd.DataFrame({'path': glob(os.path.join(train_path, '*.tif'))}) \ntrain['id'] = train['path'].map(lambda x: os.path.splitext(os.path.basename(x))[0])  # Extract image ID to merge with training labels\ntrain = train.merge(labels, on='id')  \n\n# convert labels to strings\ntrain['label'] = train['label'].astype(str)\n\n# ensure paths are relative to train_path\ntrain['path'] = train['path'].apply(lambda x: os.path.basename(x))\n\nprint(train.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Exploration","metadata":{}},{"cell_type":"code","source":"# check for null values\nprint('There are ',train.isnull().sum().sum(), ' null values')\n# check for duplicates\nprint('There are ', train.duplicated().sum(),' duplicated images')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# randomly select one image path\nimage_path = random.choice(train['path'].values)\n# full path to the image\nfull_image_path = os.path.join(train_path, image_path)\n# Load the selected image \nselected_image = cv2.imread(full_image_path)\n\nprint('Image shape is ', selected_image.shape)\nprint('The maximum number of pixels is ', selected_image.max())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Separate cancerous and non-cancerous data\ncancerous = train[train['label'] == '1']['id'].sample(n=5).values  # Treat labels as strings\nnoncancerous = train[train['label'] == '0']['id'].sample(n=5).values\n\ncancerous_images = []\nfor id in cancerous:\n    image_path = os.path.join(train_path, id + '.tif')\n    cancerous_images.append(Image.open(image_path))\n\nnoncancerous_images = []\nfor id in noncancerous:\n    image_path = os.path.join(train_path, id + '.tif')\n    noncancerous_images.append(Image.open(image_path))\n\nplt.figure(figsize=(10, 5))\nfor i, image in enumerate(cancerous_images):\n    plt.subplot(1, 5, i+1)\n    plt.imshow(image)\n    plt.title('Cancerous')\n    plt.axis('off')\nplt.show()\n\nplt.figure(figsize=(10, 5))\nfor i, image in enumerate(noncancerous_images):\n    plt.subplot(1, 5, i+1)\n    plt.imshow(image)\n    plt.title('Non-Cancerous')\n    plt.axis('off')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Counts for each class\nlabel_counts = labels['label'].value_counts()\n\n# Calculate the percentages of each class\npositive_percentage = label_counts[1] / (label_counts[0] + label_counts[1])\nprint(f'Positive labels in training data: {positive_percentage:.2%}')\n\n# Bar plot \nplt.figure(figsize=(7, 5))\nax = label_counts.sort_index().plot(kind='bar', color=['blue', 'red'])\n\nplt.xticks([0, 1], labels=[f\"Cancer Negative N={label_counts[0]}\", f\"Cancer Positive N={label_counts[1]}\"], rotation=0)\n\nfor i, count in enumerate(label_counts):\n    percentage = count / label_counts.sum() * 100\n    ax.text(i, count + 500, f'{percentage:.2f}%', ha='center', va='bottom', fontsize=12)\n\nplt.title(f'Distribution of Training Data')\nplt.ylabel('Count')\nplt.xlabel(\" \")\nplt.tight_layout()\n\n# Save plot to kaggle output directory\n#plt.savefig('/kaggle/working/training_data_distribution.png', dpi=300)\n\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Generators with Gaussian Smoothing\nGaussian smoothing removes noise while preserving edges.","metadata":{}},{"cell_type":"code","source":"def gaussian_smooth(image):\n    # Convert image to unit8\n    image = (image * 255).astype(np.uint8)\n\n    #Convert image to RGB\n    if image.shape[-1] == 1:\n        image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)\n\n    # Gaussian smoothing (blur)\n    image = cv2.GaussianBlur(image, (3, 3), 0)\n\n    # Convert float 32 and range of [0,1]\n    return image.astype(np.float32) / 255.0\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The data augmentaion parameters have been commented out becasue the model is performing better without them. ","metadata":{}},{"cell_type":"code","source":"# Split the data into 80% training and 20% validation sets\ntrain_df, val_df = train_test_split(train, test_size=0.2, random_state=42)\n\n# Data generator for training images\ntrain_datagen = ImageDataGenerator(\n    rescale=1./255  # scale images from 0-255 to 0-1\n    #rotation_range=20,          # Degree range for random rotations\n    #width_shift_range=0.1,      # Fraction of total width for random horizontal shifts\n    #height_shift_range=0.1,     # Fraction of total height for random vertical shifts\n    #shear_range=0.1,\n    #zoom_range=0.1,\n    #horizontal_flip=True, \n    #preprocessing_function=gaussian_smooth\n    \n)\n\n# Data generator for validation images without augmentation\nvalidation_datagen = ImageDataGenerator(\n    rescale=1./255  # scale images from 0-255 to 0-1\n)\n\nbatch_size = 64\n\n# Generator for the training set\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe=train_df,\n    directory=train_path,\n    x_col='path',\n    y_col='label',\n    target_size=(96, 96), # image shape\n    batch_size=batch_size,\n    class_mode='binary',\n    shuffle=True\n)\n\n# Generator for the validation set\nvalidation_generator = validation_datagen.flow_from_dataframe(\n    dataframe=val_df,  \n    directory=train_path,\n    x_col='path',\n    y_col='label',\n    target_size=(96, 96),\n    batch_size=batch_size,\n    class_mode='binary',\n    shuffle=False\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model with Spacial Attention\nSpatial attention mechanisms help the network focus on specific regions within an image while suppressing less relevant areas. This model will also use a learning rate scheduler to decrease the learning rate during training if it detects the model isn’t improving enough.","metadata":{}},{"cell_type":"code","source":"def spatial_attention_block(input_tensor, kernel_size=7):\n    # Generates attention features through a separate pathway\n    attention = Conv2D(filters=1, kernel_size=kernel_size, padding='same', activation='sigmoid')(input_tensor)\n    #attention = BatchNormalization()(attention)\n\n    # Apply attention to input\n    refined_features = Multiply()([input_tensor, attention])\n\n    return refined_features","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"reduce_lr = ReduceLROnPlateau(\n    monitor = 'val_loss',\n    factor = 0.5, # 50% Reduction in learning Rate\n    patience = 2, # If no improvement after 2 epochs\n    verbose = 1,\n    min_lr = 1e-6 # minimum learning rate not to go below\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define input\ninputs = Input(shape=(96, 96, 3))\n\nx = Conv2D(64, (3, 3), padding='same', activation='relu', kernel_regularizer=l2(0.001))(inputs)\n# Spatial attention layer\n# Using kernel_size=9 then kernel_size=5 to capture both global and local spatial dependencies\nx = spatial_attention_block(x, kernel_size=9)\nx = spatial_attention_block(x, kernel_size=5)\nx = Dropout(0.05)(x)\nx = BatchNormalization()(x)\nx = MaxPooling2D((2, 2))(x)\n\nx = Conv2D(128, (3, 3), padding='same', activation='relu', kernel_regularizer=l2(0.001))(x)\nx = Dropout(0.1)(x)\nx = BatchNormalization()(x)\nx = MaxPooling2D((2, 2))(x)\n\nx = Conv2D(256, (3, 3), padding='same', activation='relu', kernel_regularizer=l2(0.001))(x)\nx = Dropout(0.15)(x)\nx = BatchNormalization()(x)\nx = MaxPooling2D((2, 2))(x)\n\nx = Conv2D(512, (3, 3), padding='same', activation='relu', kernel_regularizer=l2(0.001))(x)\n# 2nd spatial attention layer, allows the model to re-focus after deep feature extraction\nx = spatial_attention_block(x, kernel_size=9)\nx = spatial_attention_block(x, kernel_size=5)\nx = Dropout(0.2)(x)\nx = BatchNormalization()(x)\nx = MaxPooling2D((2, 2))(x)\n\n# Global Average Pooling to reduce spatial dimensions\nx = GlobalAveragePooling2D()(x)\n\n# Fully connected layers\nx = Dense(256)(x)\nx = Dropout(0.5)(x)\nx = BatchNormalization()(x)\nx = Activation('relu')(x)\n\n# Output layer\noutputs = Dense(1, activation='sigmoid', kernel_regularizer=l2(0.001))(x)\n\n# Build model\nmodel_spatial = Model(inputs, outputs)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_spatial.summary()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_spatial.compile(\n    optimizer=Adam(learning_rate=0.0001),\n    loss='binary_crossentropy',\n    metrics=['accuracy', 'auc']\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Keras was prematurely ending training and moving to the next epoch, most likely because Keras didn't have enough data to complete all the steps per epoch. Therefor Keras was allowed to automatically determine the number of steps_per_epoch and Validation_stpes.","metadata":{}},{"cell_type":"code","source":"#steps_per_epoch = len(train_df)//batch_size\n#validation_steps = len(val_df)//batch_size\n\nhist_spatial = model_spatial.fit(\n    train_generator,\n    #steps_per_epoch = steps_per_epoch, \n    validation_data = validation_generator,\n    #validation_steps = validation_steps,\n    epochs = 20,\n    callbacks=[reduce_lr]\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(10, 5))\n\n# accuracy plot\nplt.subplot(1, 3, 1)\nplt.plot(hist_spatial.history['accuracy'], label='Train Accuracy')\nplt.plot(hist_spatial.history['val_accuracy'], label='Validation Accuracy')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy')\nplt.ylim(0.700, 1.000)\nplt.title('Model Accuracy')\nplt.legend(loc='lower right')\n\n# loss plot\nplt.subplot(1, 3, 2)\nplt.plot(hist_spatial.history['loss'], label='Train Loss')\nplt.plot(hist_spatial.history['val_loss'], label='Validation Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.title('Model Loss')\nplt.legend(loc='upper right')\n\n# AUC plot\nplt.subplot(1, 3, 3)\nplt.plot(hist_spatial.history['auc'], label='Train AUC')\nplt.plot(hist_spatial.history['val_auc'], label='Validation AUC')\nplt.xlabel('Epoch')\nplt.ylabel('AUC')\nplt.title('ROC AUC')\nplt.legend(loc='lower right')\n\nplt.tight_layout()\n# Save plot to kaggle output directory (optional)\nplt.savefig('/kaggle/working/cnn_training_results.png', dpi=300)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Confusion Matrix","metadata":{}},{"cell_type":"code","source":"# calculations for confusion matrix\ntrue_labels = validation_generator.classes\npred_probabilities = model_spatial.predict(validation_generator, steps=len(validation_generator), verbose=1)\n\n# for binary classification get predicted classes based on probability threshold of 0.5\npred_classes = (pred_probabilities > 0.5).astype(int)\n\n# confusion matrix\ncm = confusion_matrix(true_labels, pred_classes)\n\n# plot confusion matrix\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['Non-Cancerous', 'Cancerous'])\ndisp.plot(cmap=plt.cm.Blues)\nplt.title('Confusion Matrix')\n\n# Save plot to kaggle output directory (optional)\nplt.savefig('/kaggle/working/cnn_training_confusion.png', dpi=300)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prepare Test Data for Submission","metadata":{}},{"cell_type":"code","source":"# Load test image paths\ntest_df = pd.DataFrame({'path': glob(os.path.join(test_path, '*.tif'))})\ntest_df['id'] = test_df['path'].apply(lambda x: os.path.splitext(os.path.basename(x))[0])\ntest_df['filename'] = test_df['id'] + '.tif'\nprint(test_df.head(5))\nprint(test_df.info())\nprint(test_df['path'][0])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Create the test image datagenerator\ntest_datagen = ImageDataGenerator(rescale=1/255)\ntest_generator = test_datagen.flow_from_dataframe(dataframe=test_df,\n                                                    directory=test_path,\n                                                    x_col='filename',\n                                                    y_col=None,\n                                                    target_size=(96, 96),\n                                                    batch_size=32,\n                                                    class_mode=None,\n                                                    shuffle=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Make predictions with test data\ntest_preds = model_spatial.predict(test_generator, verbose=1)\ntest_preds_probs = (test_preds > 0.5).astype(int)\nsubmission = test_df.copy()\nsubmission = submission.drop(columns=['filename', 'path'])\nsubmission['label'] = test_preds_probs\nprint(submission.head())\n\nsubmission.to_csv('/kaggle/working/submission.csv', index=False)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}