"""
<div style="display: block; height: 500px; overflow:hidden; text-align:center">
     <img src="https://imgur.com/dhGIBOt.jpg" style="top: 0px;border-radius: 20px; ">
</div>
"""
"""
# 1. Imports
"""
#
import numpy as np
import pandas as pd
import random

# image
from PIL import Image

# folder
import os
import glob

# visu
import matplotlib.pyplot as plt
plt.rc('image', cmap='gray')

# sklearn
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split

#tensorflow
from tensorflow.keras import Sequential
from tensorflow.keras import layers
from tensorflow.keras.callbacks import EarlyStopping
from tensorflow.keras.utils import to_categorical
"""
# 2. Loading images
"""
"""
There are four shape categories. The images are loaded in a numpy array as matrix and associated categories are loaded in an independent array.
"""
categories = ["circle", "square", "star", "triangle"]
"""
All images are of size 200 x 200. Because of memory limitation in Kaggle, keeping 200 x 200 is not possible. Let's divide the height and width by two.
"""
im_width = 100
im_height = 100
%%time

data = []
target = []

for cat in categories:
    filelist = glob.glob('/kaggle/input/four-shapes/shapes/' + cat + '/*.png')
    target.extend([cat for _ in filelist])
    data.extend([np.array(Image.open(fname).resize((im_width, im_height))) for fname in filelist])
#
data_array = np.stack(data, axis=0)
"""
So we have 14970 tensor images of width 100 and height 100, each pixel being defined by Black (0) or white (255):
"""
data_array.shape
"""
Let's have a look at several random shape images and associated label of our dataset:
"""
fig = plt.figure(figsize=(20,15))
gs = fig.add_gridspec(4, 4)
#
for line in range(0, 3):
    for row in range(0, 3):
        num_image = random.randint(0, data_array.shape[0])
        ax = fig.add_subplot(gs[line, row])
        ax.axis('off');
        ax.set_title(target[num_image])
        ax.imshow(data_array[num_image]);
"""
# 3. Train test split
<b> Let's split the dataset in a train set to train model and a test set for evaluation.</b> We will build the train set with 80% of the dataset and the test set with the 20% remaining. We keep the class repartition by setting the parameter `stratify` to `target` (which is the list containing the labels).
"""
pd.DataFrame(target).value_counts()/len(target)
X_train, X_test, y_train, y_test = train_test_split(data_array, np.array(target), test_size=0.2, stratify=target)
pd.DataFrame(y_train).value_counts()/len(y_train)
pd.DataFrame(y_test).value_counts()/len(y_test)
"""
# 4. Preparing the data
"""
"""
## Normalization
"""
"""
To ease the convergence of the algorithm, it is usefull to normalize the data. See here what are the maximum and minimum values in the data, and normalize it accordingly (the resulting image intensities should be between 0 and 1).
"""
print(X_train.max())
print(X_train.min())
X_test_norm = np.round((X_test/255), 3).copy()
X_train_norm = np.round((X_train/255), 3).copy()
print(X_train_norm.max())
print(X_train_norm.min())
"""
Here again, we can check the normalised pictures randomly:
"""
fig = plt.figure(figsize=(20,15))
gs = fig.add_gridspec(4, 4)
#
for line in range(0, 3):
    for row in range(0, 3):
        num_image = random.randint(0, X_train_norm.shape[0])
        ax = fig.add_subplot(gs[line, row])
        ax.axis('off');
        ax.set_title(y_train[num_image])
        ax.imshow(X_train_norm[num_image]);
"""
## Target encoding
"""
"""
Here we convert targets. First, from string to numerical values, each category becoming an integer, from 0 to 3 (as there are four different shape categories):
"""
display(np.array(y_train).shape)
display(np.unique(y_train))
display(np.array(y_test).shape)
display(np.unique(y_test))
"""
Fitting the encoder on train set:
"""
encoder = LabelEncoder().fit(y_train)
"""
Applying on both train and test set:
"""
y_train_cat = encoder.transform(y_train)
y_test_cat = encoder.transform(y_test)
"""
And now, we convert the result to one-hot encoded target so that they can be used to train a classification neural network. We use `to_categorical` from tensorflow library:
"""
y_train_oh = to_categorical(y_train_cat)
y_test_oh = to_categorical(y_test_cat)
pd.DataFrame(y_test_oh).head()
"""
## Expanding dimension for the correct model intput dim
"""
"""
<b>The deep learning model needs a 4 dimensions tensor to work with. Here we have grayscale pictures with no channel. It means the matrices of our black and white pictures are of shape 3. We need to add an extra dimension so algorithm can accept it.</b>
"""
X_train_norm = X_train_norm.reshape(-1, 100, 100, 1)
X_test_norm = X_test_norm.reshape(-1, 100, 100, 1)
X_train_norm.shape
"""
# 5. Convolutionnal neural network
"""
"""
Now, let's define the Convolutional Neural Network.

<b>The CNN that is composed of:</b>

* a Conv2D layer with 32 filters, a kernel size of (3, 3), the relu activation function, a padding equal to same and the correct input_shape
* a MaxPooling2D layer with a pool size of (2, 2)
* a Conv2D layer with 64 filters, a kernel size of (3, 3), the relu activation function, and a padding equal to same
* a MaxPooling2D layer with a pool size of (2, 2)
* a Conv2D layer with 128 filters, a kernel size of (3, 3), the relu activation function, and a padding equal to same
* a MaxPooling2D layer with a pool size of (3, 3)
* a Flatten layer
* a dense function with 120 neurons with the relu activation function
* a dense function with 60 neurons with the relu activation function
* a dropout layer (with a rate of 0.5), to regularize the network
* a dense function related to the task: multiclassification of 4 classes
"""
def initialize_model():
    model = Sequential()
    model.add(layers.Conv2D(32, (3, 3), activation="relu", input_shape=(im_height, im_width, 1), padding='same'))
    model.add(layers.MaxPool2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(64, (3, 3), activation="relu", padding='same'))
    model.add(layers.MaxPool2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(128, (3, 3), activation="relu", padding='same'))
    model.add(layers.MaxPool2D(pool_size=(3, 3)))
    model.add(layers.Flatten())
    model.add(layers.Dense(120, activation='relu'))
    model.add(layers.Dense(60, activation='relu'))
    model.add(layers.Dropout(rate=0.2))
    model.add(layers.Dense(4, activation='softmax'))

    return model
model = initialize_model()
model.summary()
def compile_model(model):
    model.compile(optimizer='adam',
                  loss='categorical_crossentropy',
                  metrics="accuracy")
    return model
"""
Here I set an early stopping after 5 epochs and set the parameter `restore_best_weights` to `True` so that the weights of best score on monitored metric - here `val_accuracy` (accuracy on test set) - are restored when training stops. This way the model has the best accuracy possible on unseen data.
"""
model = initialize_model()
model = compile_model(model)
es = EarlyStopping(patience=5, monitor='val_accuracy', restore_best_weights=True)

history = model.fit(X_train_norm, y_train_oh,
                    batch_size=16,
                    epochs=1000,
                    validation_split=0.3,
                    callbacks=[es])
"""
# 6. Results
"""
"""
<b>So we have a wonderful 100% accuracy for this shape recognition algorithm!<span style="font-size:50pt">🔥🚀</span></b>
"""
def plot_history(history, title='', axs=None, exp_name=""):
    if axs is not None:
        ax1, ax2 = axs
    else:
        f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
    
    if len(exp_name) > 0 and exp_name[0] != '_':
        exp_name = '_' + exp_name
    ax1.plot(history.history['loss'], label='train' + exp_name)
    ax1.plot(history.history['val_loss'], label='val' + exp_name)
    ax1.set_ylim(-0.1, 0.1)
    ax1.set_title('loss')
    ax1.legend()

    ax2.plot(history.history['accuracy'], label='train accuracy'  + exp_name)
    ax2.plot(history.history['val_accuracy'], label='val accuracy'  + exp_name)
    ax2.set_ylim(0.9, 1.1)
    ax2.set_title('Accuracy')
    ax2.legend()
    return (ax1, ax2)

plot_history(history, title='', axs=None, exp_name="");
"""
<b>We can check the accuracy:</b>
"""
model.evaluate(X_test_norm, y_test_oh, verbose=0)
"""
<b>And make predictions on test set to show, for random images of the test set, that the predicted label for each images is the good one:</b>
"""
predictions = model.predict(X_test_norm)
fig = plt.figure(figsize=(20,15))
gs = fig.add_gridspec(4, 4)
#
for line in range(0, 3):
    for row in range(0, 3):
        num_image = random.randint(0, X_test_norm.shape[0])
        ax = fig.add_subplot(gs[line, row])
        ax.axis('off');
        ax.set_title("Predicted: " + categories[list(np.round(predictions[num_image])).index(1)])
        ax.imshow(X_test_norm[num_image]);
fig.suptitle("Predicted label for the displayed shapes", fontsize=25, x=0.42);
"""
<b>Thank you for reading 🙏🏻 If you like please upvote. If you have any suggestion of improvment or if you notice some mistakes please feel free to comment</b><br><br>
<b style="color:royalblue">V. Bonnet</b>
"""