<jupyter_start><jupyter_text>Flower Classification using Convolutional Neural Network <NAME> 7th September, 2020  IntroductionImage Classification is one of the core problem statement being solved with Deep Learning methods. By definition, image classification is to extract patterns or information from multiple groups of images. The resulting features are used to decide the group/class of a new input image. Convolutional Neural Networks (CNN) have been the go-to for image classification, among many other functionalities for it's effciency and effectiveness. To see how CNNs work, visit [here](https://analyticsindiamag.com/convolutional-neural-network-image-classification-overview/).   Flower Classification ProblemWe will be using the Kaggle [dataset](https://www.kaggle.com/ayaanmustafa/flower) which consists images of 3 classes of flowers, namely, Lotus, Rose and Sunflower. The dataset consists of images being distributed in training and validation folders. The images are stored in folders named as their classes.   ApproachWe will implement a 3-layer convolutional network using the Keras framework. We will take advantage of the [ImageDataGenerator](https://keras.io/api/preprocessing/image/imagedatagenerator-class) class in Keras, given the folder structure of the dataset. The class will allow on-the-go data augmentation and can be used as a data pipeline for the CNN model directly.   Importing necessary libraries<jupyter_code>import os
import tensorflow as tf 
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
import matplotlib.pyplot as plt
import numpy as np
import cv2
from keras.preprocessing import image

K.set_image_data_format('channels_first') # Setting channels first at keras backend<jupyter_output><empty_output><jupyter_text>Iterating over the training folder to check the number of samples in each class.<jupyter_code>train_data_path = "/archive/flower_images/training"
for folder in os.listdir(train_data_path):
  path = train_data_path + "//" + folder
  print(folder + " " +  str(len(os.listdir(path))))<jupyter_output>lotus 71
rose 75
sunflower 126<jupyter_text>As we see above, the training data has lower number of samples contrary to what might give a satisfactory result with CNNs. That is where data augmentation will come handy. The ImageDataGenerator class will do the heavy lifting here. [ImageDataGenerator](https://keras.io/api/preprocessing/image/imagedatagenerator-class) accepts parameters such:- rotation_range: value between 0-180. A range from 0 to value passed to which the image is randomly roatated.- width_shift_range: Translates image horizontally. Value is fraction of total width.- height_shift_range: Translates image vertically. Value is fraction of total height.- shear_range: Random [shear transformations](https://en.wikipedia.org/wiki/Shear_mapping).- zoom_range: Randomly zooms images.- rescale: Rescaling factorSee the documentation for more options to the class.<jupyter_code>train_datagen = ImageDataGenerator(
    rotation_range=40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.3,
    zoom_range=0.2,
    rescale=1./255,
    horizontal_flip=True
)

test_datagen = ImageDataGenerator(rescale=1./255)<jupyter_output><empty_output><jupyter_text>We define a CNN model with 3 layers of convloutions paired with max pooling. The convolutions are followed by a fully-connected layer and the output layer. The input size is defined (3, 150, 150). We are using categorical crossentropy as loss function. And RMSProp as optimizer.<jupyter_code>model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=(3, 150, 150)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2), padding='valid', data_format='channels_first'))

model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2), padding='valid', data_format='channels_first'))

model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2), padding='valid', data_format='channels_first'))
model.add(Flatten())  # this converts our 3D feature maps to 1D feature vectors
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(3))
model.add(Activation('sigmoid'))

model.compile(loss='categorical_crossentropy',
              optimizer='rmsprop',
              metrics=['accuracy'])
model.summary()
batch_size = 25<jupyter_output><empty_output><jupyter_text>We take advantage of [flow_from_directory](https://keras.io/api/preprocessing/image/flowfromdirectory-method) method to generate a data piepline directly for the CNN model. We use the class mode as categorical as we used categorical crossentropy as the loss function. The train and validation generator can be passed directly to the training function, thus letting Keras do the heavy lifting.<jupyter_code>train_generator = train_datagen.flow_from_directory(
        '/archive/flower_images/training', 
        target_size=(150, 150),  
        batch_size=batch_size,
        class_mode='categorical')

validation_generator = test_datagen.flow_from_directory(
        '/archive/flower_images/validation',
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode='categorical')
steps_train = train_generator.n // batch_size
steps_valid = validation_generator.n // batch_size<jupyter_output><empty_output><jupyter_text>We will be saving the model only if it improves loss on the validation dataset. The ModelCheckpoint is one of the many callbacks in Keras to interrupt training and saving the weights. See [documentation](https://keras.io/api/callbacks/).<jupyter_code>checkpoint =  tf.keras.callbacks.ModelCheckpoint('/best_model.hdf5', save_best_only = True, monitor = 'val_loss', verbose = 1)<jupyter_output><empty_output><jupyter_text>The model.fit_generator method is used to train the model on the dataset. The fit_generator method accepts the generator object (as implemented above). Note: model.fit method also accepts generator object and is normally implemented. See [documentation](https://keras.io/api/models/model_training_apis/fit-method).<jupyter_code>history = model.fit_generator(
        train_generator,
        steps_per_epoch=steps_train,
        epochs=10,
        validation_data=validation_generator,
        validation_steps=steps_valid,
        callbacks=[checkpoint])<jupyter_output>WARNING:tensorflow:From <ipython-input-10-60dd36babf9e>:7: Model.fit_generator (from tensorflow.python.keras.engine.training) is deprecated and will be removed in a future version.
Instructions for updating:
Please use Model.fit, which supports generators.
Epoch 1/10
10/10 [==============================] - ETA: 0s - loss: 1.1078 - accuracy: 0.4634
Epoch 00001: val_loss improved from inf to 1.18535, saving model to /content/best_model.hdf5
10/10 [==============================] - 107s 11s/step - loss: 1.1078 - accuracy: 0.4634 - val_loss: 1.1854 - val_accuracy: 0.1400
Epoch 2/10
10/10 [==============================] - ETA: 0s - loss: 0.8506 - accuracy: 0.5854
Epoch 00002: val_loss improved from 1.18535 to 0.70961, saving model to /content/best_model.hdf5
10/10 [==============================] - 7s 695ms/step - loss: 0.8506 - accuracy: 0.5854 - val_loss: 0.7096 - val_accuracy: 0.7200
Epoch 3/10
10/10 [==============================] - ETA: 0s - loss: 0.5792 - accuracy: 0.8008
Epoch 000[...]<jupyter_text>The highest validation accuracy obtained is 0.98. The result is satisfactory given the amount of data we were provided.  Visualizing the loss and accuracy graph, we will get more idea on how our model performed.<jupyter_code>H = history.history
fig = plt.figure(figsize = (20, 7))
plt.subplot(121)
plt.plot(H['accuracy'], label = 'acc')
plt.plot(H['val_accuracy'], label = 'val_acc')
plt.grid()
plt.legend()

plt.subplot(122)
plt.plot(H['loss'], label = 'loss')
plt.plot(H['val_loss'], label = 'val_loss')
plt.grid()
plt.legend()<jupyter_output><empty_output><jupyter_text>We can test our model on randomly picked images now. I have downloaded one image from each class from [Pixabay](https://pixabay.com/). We resize the image using Keras preprocessing module to our input size, i.e. (150, 150). The images are stored in /images folder.<jupyter_code>model.load_weights('/best_model.hdf5')
class_list = ["Lotus", "Rose", "Sunflower"]

fig = plt.figure(figsize = (15, 15))
for temp, img in enumerate(os.listdir('/images')):
  plt.subplot(f'33{temp+1}')
  temp += 1

  img_path = '/images' + '//' + img

  i_cv = cv2.imread(img_path, 1)

  img_width, img_height = 150, 150
  i = image.load_img(img_path, target_size = (img_width, img_height))
  i = image.img_to_array(i)
  i = np.expand_dims(i, axis = 0)

  preds = model.predict(i)
  preds = zip(class_list, list(preds[0]))
  preds = sorted(list(preds), key = lambda z: z[1], reverse = True)[:1]
  plt.imshow(i_cv)
  plt.title(f'{preds[0][0]}: {round(preds[0][1] * 100, 2)}%') 

plt.tight_layout()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11685049688762696, 'fraction_numerical': 0.03308943977285137, 'mean_word_length': 5.289835164835165, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 37, 'https://': 9, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '629526', 'license': 'Apache-2.0', 'n_tokens_mistral': 3037, 'n_tokens_neox': 2767, 'n_words': 863, 'path': 'FlowerClassificationUsingCNN.ipynb', 'repo_name': 'tahazanzibarwala/Flower-classification-using-CNN', 'size': '9168'}