{"cells":[{"metadata":{},"cell_type":"markdown","source":"In this notebook, our task is to classify each image into four disease categories or a fifth category indicating a healthy leaf. The training dataset contains 21,367 labeled images. A further description of the task and the dataset is available [here](https://www.kaggle.com/c/cassava-leaf-disease-classification)."},{"metadata":{},"cell_type":"markdown","source":"## Imports"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import glob\n\nimport numpy as np\nimport pandas as pd \nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\nfrom sklearn.utils.class_weight import compute_class_weight\nimport seaborn as sns\nimport tensorflow as tf\nfrom tensorflow.keras.layers.experimental import preprocessing\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.applications import EfficientNetB3\nfrom tensorflow.keras.callbacks import ModelCheckpoint","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Constants"},{"metadata":{"trusted":true},"cell_type":"code","source":"TRAIN_DATA_PATH = '../input/cassava-leaf-disease-classification/train.csv'\nTRAIN_IMAGES_PATH = '../input/cassava-leaf-disease-classification/train_images'\nMODEL_WEIGHTS_PATH = '../input/tfkerasefficientnetimagenetnotop/efficientnetb3_notop.h5'\nSPLIT_TEST_SIZE = 0.2\nIMG_SIZE = 512\nBATCH_SIZE = 8\nTOP_DROPOUT_RATE = 0.2\nDROPOUT_RATE = 0.4","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Load data"},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"df = pd.read_csv(TRAIN_DATA_PATH)\nprint(df.shape)\ndf.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"labels = df.label.unique()\nlabels_number = len(labels)\nprint(f'Labels: {labels}')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## EDA"},{"metadata":{},"cell_type":"markdown","source":"We check if the training dataset is balanced."},{"metadata":{"trusted":true},"cell_type":"code","source":"sns.countplot(df.label)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data preparation"},{"metadata":{},"cell_type":"markdown","source":"Split the dataset in training and validation sets."},{"metadata":{"trusted":true},"cell_type":"code","source":"df['path'] = f'{TRAIN_IMAGES_PATH}/' + df['image_id']\ntrain_data_df, val_data_df = train_test_split(\n    df, test_size = SPLIT_TEST_SIZE, random_state=1\n)\ntrain_data_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_path_images = train_data_df.path.values\ntrain_labels = train_data_df.label.values\nval_path_images = val_data_df.path.values\nval_labels = val_data_df.label.values","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Build `tf` datasets with the path of the images and the target's value."},{"metadata":{"trusted":true},"cell_type":"code","source":"ds_train = tf.data.Dataset.from_tensor_slices((train_path_images, train_labels))\nds_valid = tf.data.Dataset.from_tensor_slices((val_path_images, val_labels))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We apply a function to the datasets to preprocess the images."},{"metadata":{"trusted":true},"cell_type":"code","source":"def process_data_train(image_path, label):\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    return img, label\n\ndef process_data_valid(image_path, label):\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE,IMG_SIZE])\n    return img, label","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Set `num_parallel_calls` so multiple images are processed in parallel.\nAUTOTUNE = tf.data.experimental.AUTOTUNE\nds_train = ds_train.map(process_data_train, num_parallel_calls=AUTOTUNE)\nds_valid = ds_valid.map(process_data_valid, num_parallel_calls=AUTOTUNE)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Visualize some images in the training dataset."},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(10,10))\nfor i, (image, label) in enumerate(ds_train.take(9)):\n    ax = plt.subplot(3, 3, i + 1)\n    plt.imshow(image.numpy().astype(\"uint8\"))\n    plt.title(f'Label: {label}')\n    plt.axis(\"off\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"ds_train = ds_train.shuffle(buffer_size=1000)\nds_train = ds_train.batch(BATCH_SIZE)\nds_train = ds_train.prefetch(buffer_size=AUTOTUNE)\n\nds_valid = ds_valid.batch(BATCH_SIZE)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Data augmentation"},{"metadata":{},"cell_type":"markdown","source":"We use preprocessing layers of keras for image augmentation."},{"metadata":{"trusted":true},"cell_type":"code","source":"img_augmentation = Sequential(\n    [\n        preprocessing.RandomCrop(height=IMG_SIZE, width=IMG_SIZE),\n        preprocessing.RandomFlip('horizontal_and_vertical'),\n        preprocessing.RandomRotation(0.25),\n        preprocessing.RandomZoom((-0.2, 0)),\n        preprocessing.RandomContrast(factor=0.1),\n        preprocessing.RandomTranslation(height_factor=0.1, width_factor=0.1),\n    ],\n    name=\"img_augmentation\",\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(10,10))\nfor i in range(9):\n    ax = plt.subplot(3, 3, i + 1)\n    aug_img = img_augmentation(tf.expand_dims(image, axis=0))\n    plt.imshow(aug_img[0].numpy().astype(\"uint8\"))\n    plt.title(f'Label: {label}')\n    plt.axis(\"off\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Build the model and compile it"},{"metadata":{},"cell_type":"markdown","source":"We initialize the model with pre-trained ImageNet weights, and we fine-tune it with EfficientNet on our own dataset. A description of the EfficienNet architecture is available in this [notebook](https://nbviewer.jupyter.org/github/victorviro/Deep_learning_python/blob/master/EfficientNet.ipynb)."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Transfer learning from pre-trained weights\ninputs = layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = img_augmentation(inputs)\nbase_model = EfficientNetB3(include_top=False, input_tensor=x, weights=None, \n                            drop_connect_rate=DROPOUT_RATE)\nbase_model.load_weights(MODEL_WEIGHTS_PATH)\n\n# Freeze the pretrained weights\nfor layer in base_model.layers:\n    layer.trainable = False\n\n# Rebuild top\nx = layers.GlobalAveragePooling2D(name=\"avg_pool\")(base_model.output)\nx = layers.BatchNormalization()(x)\n\ntop_dropout_rate = TOP_DROPOUT_RATE\nx = layers.Dropout(top_dropout_rate, name=\"top_dropout\")(x)\noutputs = layers.Dense(labels_number, activation=\"softmax\", name=\"pred\")(x)\nmodel = tf.keras.Model(inputs, outputs, name=\"EfficientNet\")    ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Since the new top layers will be initialized randomly, it will make large errors, at least during the first few epochs, so\nthere will be large error gradients that may wreck the reused weights. To avoid this, one approach is to freeze the reused layers during the first few epochs, giving the new layers some time to learn reasonable weights. To do this, we simply set every layer’s `trainable` attribute to `False` and compile the model. For this step, a relatively large learning rate (`1e-2`) can be used."},{"metadata":{"trusted":true},"cell_type":"code","source":"optimizer = tf.keras.optimizers.Adam(learning_rate=1e-2)\nmodel.compile(\n    optimizer=optimizer, \n    loss=\"sparse_categorical_crossentropy\", \n    metrics=[\"sparse_categorical_accuracy\"]\n)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Train the model"},{"metadata":{"trusted":true},"cell_type":"code","source":"epochs = 3\nhist = model.fit(ds_train, epochs=epochs, validation_data=ds_valid)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_hist(hist):\n    plt.plot(hist.history[\"sparse_categorical_accuracy\"])\n    plt.plot(hist.history[\"val_sparse_categorical_accuracy\"])\n    plt.title(\"model accuracy\")\n    plt.ylabel(\"accuracy\")\n    plt.xlabel(\"epoch\")\n    plt.legend([\"train\", \"validation\"], loc=\"upper left\")\n    plt.show()\n\nplot_hist(hist)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The second step is to unfreeze a number of layers and continue training to fine-tune the reused layers for the new task. For this step, a smaller learning rate is used. In this example we show unfreezing all layers, but depending on specific dataset it may be desireble to only unfreeze a fraction of all layers. When we use pretrained weights on a dataset that is more different from ImageNet, this fine-tuning step can be crucial as the feature extractor also needs to be adjusted by a considerable amount."},{"metadata":{"trusted":true},"cell_type":"code","source":"for layer in model.layers:\n    if not isinstance(layer, layers.BatchNormalization):\n        layer.trainable = True","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"optimizer = tf.keras.optimizers.Adam(learning_rate=1e-4)\nmodel.compile(\n    optimizer=optimizer, \n    loss=\"sparse_categorical_crossentropy\", \n    metrics=[\"sparse_categorical_accuracy\"]\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"class_weight =  dict(zip(labels, compute_class_weight('balanced', labels, train_labels)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true},"cell_type":"code","source":"model_checkpoint = ModelCheckpoint(filepath='best_model.h5', monitor='val_loss', \n                                   save_best_only=True)\nepochs = 4\nhist = model.fit(ds_train, epochs=epochs, validation_data=ds_valid, \n                 class_weight = class_weight, callbacks=[model_checkpoint])\n","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def plot_hist(hist):\n    plt.plot(hist.history[\"sparse_categorical_accuracy\"])\n    plt.plot(hist.history[\"val_sparse_categorical_accuracy\"])\n    plt.title(\"model accuracy\")\n    plt.ylabel(\"accuracy\")\n    plt.xlabel(\"epoch\")\n    plt.legend([\"train\", \"validation\"], loc=\"upper left\")\n    plt.show()\n\n\nplot_hist(hist)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Model evaluation"},{"metadata":{"trusted":true},"cell_type":"code","source":"model.load_weights(\"best_model.h5\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Evaluate the model on the validation set"},{"metadata":{"trusted":true},"cell_type":"code","source":"# Compute predictions in the validation set\nval_predictions = model.predict(ds_valid)\ny_val_predictions = np.argmax(val_predictions, axis=-1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# Compute metrics to evaluate the model\nclassification_metrics = classification_report(val_labels, y_val_predictions)\n# Compute the confusion matrix\nerror_matrix = confusion_matrix(val_labels, y_val_predictions)#, normalize='true'\nprint(f'Classification metrics:\\n{classification_metrics}')\n# Plot the confusion matrix\nplt.figure(figsize=(10,5))\nax = plt.axes()\nsns.heatmap(error_matrix, annot=True, fmt=\"d\")\nax.set_title('Confusion matrix Validation set')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"plt.figure(figsize=(10,10))\nfor imgs_batch, labels_batch in ds_valid.take(1):\n    for i in range(BATCH_SIZE):\n        image = imgs_batch[i]\n        ax = plt.subplot(3, 3, i + 1)\n        plt.imshow(image.numpy().astype(\"uint8\"))\n        plt.title(f'Label: {labels_batch[i]}; Predicted: {y_val_predictions[i]}')\n        plt.axis(\"off\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Predictions in test dataset"},{"metadata":{"trusted":true},"cell_type":"code","source":"test_images = glob.glob('../input/cassava-leaf-disease-classification/test_images/*.jpg')\nprint(test_images)\ndf_test = pd.DataFrame(np.array(test_images), columns=['Path'])\ndf_test.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_ds = tf.data.Dataset.from_tensor_slices((df_test.Path.values))\n\ndef process_test_img(image_path):\n    img = tf.io.read_file(image_path)\n    img = tf.image.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE,IMG_SIZE])\n    return img\n    \ntest_ds = test_ds.map(process_test_img, num_parallel_calls=AUTOTUNE).batch(BATCH_SIZE)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_predictions = model.predict(test_ds)\ny_test_predictions = np.argmax(test_predictions, axis=-1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_test['image_id'] = df_test.Path.str.split('/').str[-1]\ndf_test['label'] = y_test_predictions\ndf_test= df_test[['image_id','label']]\ndf_test.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_test.to_csv('submission.csv', index=False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## References"},{"metadata":{},"cell_type":"markdown","source":"- [Keras: Image classification via fine-tuning with EfficientNet](https://keras.io/examples/vision/image_classification_efficientnet_fine_tuning/)\n\n- [Kaggle notebook reference](https://www.kaggle.com/frlemarchand/efficientnet-aug-tf-keras-for-cassava-diseases)"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}