{"cells":[{"metadata":{},"cell_type":"markdown","source":"This kernel is a follow-up from the notebook presenting a CNN for classification of cacti in aerial imagery (https://www.kaggle.com/frlemarchand/simple-cnn-using-keras). While this previous work aimed to simply use a CNN for a fairly easy binary task, the same architecture will not be used as easily on such large images as for this current cancer detection task. Of course, some code will be similar."},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\nimport pandas as pd\n\nimport tensorflow as tf\nfrom tensorflow import keras\n\nimport os\nfrom shutil import copyfile, move\nfrom tqdm import tqdm\nimport h5py\nimport random\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Dense, Flatten, Dropout, Activation\nfrom tensorflow.keras.layers import BatchNormalization, GlobalAveragePooling2D\nfrom tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping\nfrom tensorflow.keras.applications import VGG16","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"dataset_df = pd.read_csv(\"../input/train_labels.csv\")\ndataset_df[\"filename\"] = [item.id+\".tif\" for idx, item in dataset_df.iterrows()]\ndataset_df[\"groundtruth\"] = [\"cancerous\" if item.label==1 else \"healthy\" for idx, item in dataset_df.iterrows()]\ndataset_df.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We create the training and validation sets. The training set is composed of 80% of the dataset and the validation set contains the 20% left. It is important to have a large enough validation set as some of our training conditions (for example, early stopping) relies on the performance on the validation set."},{"metadata":{"trusted":true},"cell_type":"code","source":"training_sample_percentage = 0.8\ntraining_sample_size = int(len(dataset_df)*training_sample_percentage)\nvalidation_sample_size = len(dataset_df)-training_sample_size\n\ntraining_df = dataset_df.sample(n=training_sample_size)\nvalidation_df = dataset_df[~dataset_df.index.isin(training_df.index)]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Load the dataset"},{"metadata":{},"cell_type":"markdown","source":"While for smaller datasets, it is okay to open and load images into memory, we reach a grand total of 220025 images here. It also means that copying the files into well ordered folders becomes a bit of a long mess. Therefore, we can use the *flow_from_dataframe* method from *ImageDataGenerator* to associate the existing labels from the \"train_labels.csv\" file with the images provided: Simple and extremely efficient.\nPlease note that we can here afford to play around with data augmentation due to the nature of the dataset. Indeed, only the 32x32-pixel area in the centre of our 96x96-pixel images can contain cancerous cells, as described in the documentation."},{"metadata":{"trusted":true},"cell_type":"code","source":"training_batch_size = 64\nvalidation_batch_size = 64\ntarget_size = (96,96)\n\ntrain_datagen = ImageDataGenerator(\n    rescale=1. / 255,\n    horizontal_flip=True,\n    vertical_flip=True,\n    zoom_range=0.2, \n    width_shift_range=0.1,\n    height_shift_range=0.1\n)\n\ntrain_generator = train_datagen.flow_from_dataframe(\n    dataframe = training_df,\n    x_col='filename',\n    y_col='groundtruth',\n    directory='../input/train/',\n    target_size=target_size,\n    batch_size=training_batch_size,\n    shuffle=True,\n    class_mode='binary')\n\n\nvalidation_datagen = ImageDataGenerator(rescale=1. / 255)\nvalidation_generator = validation_datagen.flow_from_dataframe(\n    dataframe = validation_df,\n    x_col='filename',\n    y_col='groundtruth',\n    directory='../input/train/',\n    target_size=target_size,\n    shuffle=False,\n    batch_size=validation_batch_size,\n    class_mode='binary')\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Even though the approach has already been decided, it can be insightful to visually assess the images constituting the dataset. Moreover, it is the perfect opportunity to check whether any data augmentation may have generated strange-looking images."},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_random_samples(generator):\n    generator_size = len(generator)\n    index=random.randint(0,generator_size-1)\n    image,label = generator.__getitem__(index)\n\n    sample_number = 10\n    fig = plt.figure(figsize = (20,sample_number))\n    for i in range(0,sample_number):\n        ax = fig.add_subplot(2, 5, i+1)\n        ax.imshow(image[i])\n        if label[i]==0:\n            ax.set_title(\"Cancerous cells\")\n        elif label[i]==1:\n            ax.set_title(\"Healthy cells\")\n    plt.tight_layout()\n    plt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plot_random_samples(validation_generator)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Create the model: Use a pretrained VGG16"},{"metadata":{},"cell_type":"markdown","source":"The VGG16 is placed at the beginning of our model, which pre-initialised weights based on the training on the ImageNet dataset. A layer is added at the very end of the model to learn the classification between the two classes. Also, I decided to unfreeze the last layers' weights of the imported VGG16. The type of images in cancer research we are using are very specific and not represented in ImageNet.\nMore reading can be done on transfer learning strategies there: https://towardsdatascience.com/transfer-learning-from-pre-trained-models-f2393f124751\n\nWhile I initially attempted to use a ResNet50 for this kernel, it turned out that I could not freeze only a part of the layers without leading to very unexpected behaviours. As this kernel was written to demonstrate this very particular transfer learning strategy, I went for a VGG16 despite that the ResNet50 could reach results around 96-97% of accuracy by leaving all the layers trainable but taking longer to train.\n\nMore reading about the different pretrained architectures can be found here: https://towardsdatascience.com/neural-network-architectures-156e5bad51ba"},{"metadata":{"trusted":true},"cell_type":"code","source":"input_shape = (96, 96, 3)\npretrained_layers = VGG16(weights='imagenet',include_top = False, input_shape=input_shape)\npretrained_layers.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We freeze all the layers except the 8 last, before checking the \"trainable\" status of the all the layers in our VGG16."},{"metadata":{"trusted":true},"cell_type":"code","source":"for layer in pretrained_layers.layers[:-8]:\n    layer.trainable = False\n\nfor layer in pretrained_layers.layers:\n    print(layer, layer.trainable)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We proceed to create the model by adding the pretrained VGG16 and then our bottleneck layers which will finish with a binary classification."},{"metadata":{"trusted":true},"cell_type":"code","source":"dropout_dense_layer = 0.6\n\nmodel = Sequential()\nmodel.add(pretrained_layers)\n    \nmodel.add(GlobalAveragePooling2D())\nmodel.add(Dense(256, use_bias=False))\nmodel.add(BatchNormalization())\nmodel.add(Activation('relu'))\nmodel.add(Dropout(dropout_dense_layer))\n\nmodel.add(Dense(1))\nmodel.add(Activation('sigmoid'))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.summary()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The learning rate is set at 0.001, which could be considered as high. However, one of the callback functions checks whether the loss on the validation is going down and will automatically decrease the learning rate if the loss stagnates."},{"metadata":{"trusted":true},"cell_type":"code","source":"model.compile(loss=keras.losses.binary_crossentropy,\n              optimizer=keras.optimizers.Adam(lr=0.001),\n              metrics=['accuracy'])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"callbacks = [ReduceLROnPlateau(monitor='val_loss', patience=1, verbose=1, factor=0.5),\n             EarlyStopping(monitor='val_loss', patience=5),\n             ModelCheckpoint(filepath='best_model.h5', monitor='val_loss', save_best_only=True)]\n\ntrain_step_size = train_generator.n // train_generator.batch_size\nvalidation_step_size = validation_generator.n // validation_generator.batch_size","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"epochs = 20\nhistory = model.fit_generator(train_generator,\n          steps_per_epoch = train_step_size,\n          validation_data= validation_generator,\n          validation_steps = validation_step_size,\n          epochs=epochs,\n          verbose=1,\n          shuffle=True,\n          callbacks=callbacks)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Plotting performance during training"},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(history.history['acc'])\nplt.plot(history.history['val_acc'])\nplt.title('Accuracy over epochs')\nplt.ylabel('Acc')\nplt.xlabel('Epoch')\nplt.legend(['Train', 'Validation'], loc='best')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.plot(history.history['loss'])\nplt.plot(history.history['val_loss'])\nplt.title('Loss over epochs')\nplt.ylabel('Loss')\nplt.xlabel('Epoch')\nplt.legend(['Train', 'Validation'], loc='best')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Load the best model and classify images from the test set"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.load_weights(\"best_model.h5\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Once again, we use the ImageDataGenerator to load our images. This time, I only used the `flow_from_directory` as we do not need to associate labels to the images."},{"metadata":{"trusted":true},"cell_type":"code","source":"src=\"../input/test\"\n\ntest_folder=\"../test_folder\"\ndst = test_folder+\"/test\"\nos.mkdir(test_folder)\nos.mkdir(dst)\n\nfile_list =  os.listdir(src)\nwith tqdm(total=len(file_list)) as pbar:\n    for filename in file_list:\n        pbar.update(1)\n        copyfile(src+\"/\"+filename,dst+\"/\"+filename)\n        \ntest_datagen = ImageDataGenerator(\n    rescale=1. / 255)\n\ntest_generator = test_datagen.flow_from_directory(\n    directory=test_folder,\n    target_size=target_size,\n    batch_size=1,\n    shuffle=False,\n    class_mode='binary'\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"pred=model.predict_generator(test_generator,verbose=1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"csv_file = open(\"sample_submission.csv\",\"w\")\ncsv_file.write(\"id,label\\n\")\nfor filename, prediction in zip(test_generator.filenames,pred):\n    name = filename.split(\"/\")[1].replace(\".tif\",\"\")\n    csv_file.write(str(name)+\",\"+str(prediction[0])+\"\\n\")\ncsv_file.close()","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}