# List all directories and sub-directories
!find ./Convolutional_Neural_Networks/dataset -type d -maxdepth 5

"""
Explanation: Table of Contents
<p><div class="lev1 toc-item"><a href="#Building-a-CNN" data-toc-modified-id="Building-a-CNN-1"><span class="toc-item-num">1&nbsp;&nbsp;</span>Building a CNN</a></div><div class="lev2 toc-item"><a href="#Prepare-the-dataset" data-toc-modified-id="Prepare-the-dataset-11"><span class="toc-item-num">1.1&nbsp;&nbsp;</span>Prepare the dataset</a></div><div class="lev2 toc-item"><a href="#Building-the-CNN" data-toc-modified-id="Building-the-CNN-12"><span class="toc-item-num">1.2&nbsp;&nbsp;</span>Building the CNN</a></div><div class="lev2 toc-item"><a href="#Fitting-the-CNN-to-the-images" data-toc-modified-id="Fitting-the-CNN-to-the-images-13"><span class="toc-item-num">1.3&nbsp;&nbsp;</span>Fitting the CNN to the images</a></div><div class="lev2 toc-item"><a href="#Making-new-predictions" data-toc-modified-id="Making-new-predictions-14"><span class="toc-item-num">1.4&nbsp;&nbsp;</span>Making new predictions</a></div><div class="lev2 toc-item"><a href="#Challenge" data-toc-modified-id="Challenge-15"><span class="toc-item-num">1.5&nbsp;&nbsp;</span>Challenge</a></div><div class="lev2 toc-item"><a href="#Better-solution" data-toc-modified-id="Better-solution-16"><span class="toc-item-num">1.6&nbsp;&nbsp;</span>Better solution</a></div>

# Building a CNN

Credit: [Deep Learning A-Z™: Hands-On Artificial Neural Networks](https://www.udemy.com/deeplearning/learn/v4/content)

- [Getting the dataset](https://www.superdatascience.com/deep-learning/)

## Prepare the dataset

- keras has an efficient support for images, but we need to prepare all the images in a special structure:
    - Separate images in training and test folder.
    - In order to differentiate the class labels, the simple trick is to make two different folders: one for dog and one for cat. Keras will understand how to differentiate the labels of our independent variables.
End of explanation
"""


# Importing the Keras libraries and packages
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Flatten
from keras.layers import Dense

# Initialising the CNN
classifier = Sequential()

# Step 1 - Convolution
classifier.add(Conv2D(32, (3, 3), input_shape = (64, 64, 3), activation = 'relu'))

# Step 2 - Pooling
classifier.add(MaxPooling2D(pool_size = (2, 2)))

# Adding a second convolutional layer
classifier.add(Conv2D(32, (3, 3), activation = 'relu'))
classifier.add(MaxPooling2D(pool_size = (2, 2)))

# Step 3 - Flattening
classifier.add(Flatten())

# Step 4 - Full connection
classifier.add(Dense(units = 128, activation = 'relu'))
classifier.add(Dense(units = 1, activation = 'sigmoid'))

# Compiling the CNN
classifier.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])

"""
Explanation: Building the CNN
End of explanation
"""


from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale = 1./255,
                                   shear_range = 0.2,
                                   zoom_range = 0.2,
                                   horizontal_flip = True)

test_datagen = ImageDataGenerator(rescale = 1./255)

training_set = train_datagen.flow_from_directory('Convolutional_Neural_Networks/dataset/training_set',
                                                 target_size = (64, 64),
                                                 batch_size = 32,
                                                 class_mode = 'binary')

test_set = test_datagen.flow_from_directory('Convolutional_Neural_Networks/dataset/test_set',
                                            target_size = (64, 64),
                                            batch_size = 32,
                                            class_mode = 'binary')

classifier.fit_generator(training_set,
                         steps_per_epoch = 8000,
                         epochs = 25,
                         validation_data = test_set,
                         validation_steps = 2000)

"""
Explanation: Fitting the CNN to the images

We only have 10,000 images, that is not enough to make great performance results. We can either get more images or use the trick of data augmentation:
Image augmentation will create many batches of our images and then each batch, it will apply some random transformation on a random selection of our images like: rotating, flipping, shifting, shearing them. Eventually, in the training process, we have more diverse images inside these batches.
Image augmentation trick can help reduce overfitting.
ImageDataGenerator
End of explanation
"""


import numpy as np
from keras.preprocessing import image
test_image = image.load_img('Convolutional_Neural_Networks/dataset/single_prediction/cat_or_dog_1.jpg', 
                            target_size = (64, 64))
test_image = image.img_to_array(test_image)
test_image = np.expand_dims(test_image, axis = 0)
result = classifier.predict(test_image)
training_set.class_indices
if result[0][0] == 1:
    prediction = 'dog'
else:
    prediction = 'cat'
    
print (prediction)

"""
Explanation: Making new predictions
End of explanation
"""


from tensorflow.contrib.keras.api.keras.layers import Dropout
from tensorflow.contrib.keras.api.keras.models import Sequential
from tensorflow.contrib.keras.api.keras.layers import Conv2D
from tensorflow.contrib.keras.api.keras.layers import MaxPooling2D
from tensorflow.contrib.keras.api.keras.layers import Flatten
from tensorflow.contrib.keras.api.keras.layers import Dense
from tensorflow.contrib.keras.api.keras.callbacks import Callback
from tensorflow.contrib.keras.api.keras.preprocessing.image import ImageDataGenerator
from tensorflow.contrib.keras import backend
import os
 
 
class LossHistory(Callback):
    def __init__(self):
        super().__init__()
        self.epoch_id = 0
        self.losses = ''
 
    def on_epoch_end(self, epoch, logs={}):
        self.losses += "Epoch {}: accuracy -> {:.4f}, val_accuracy -> {:.4f}\n"\
            .format(str(self.epoch_id), logs.get('acc'), logs.get('val_acc'))
        self.epoch_id += 1
 
    def on_train_begin(self, logs={}):
        self.losses += 'Training begins...\n'
 
script_dir = os.path.dirname(__file__)
training_set_path = os.path.join(script_dir, '../dataset/training_set')
test_set_path = os.path.join(script_dir, '../dataset/test_set')
 
# Initialising the CNN
classifier = Sequential()
 
# Step 1 - Convolution
input_size = (128, 128)
classifier.add(Conv2D(32, (3, 3), input_shape=(*input_size, 3), activation='relu'))
 
# Step 2 - Pooling
classifier.add(MaxPooling2D(pool_size=(2, 2)))  # 2x2 is optimal
 
# Adding a second convolutional layer
classifier.add(Conv2D(32, (3, 3), activation='relu'))
classifier.add(MaxPooling2D(pool_size=(2, 2)))
 
# Adding a third convolutional layer
classifier.add(Conv2D(64, (3, 3), activation='relu'))
classifier.add(MaxPooling2D(pool_size=(2, 2)))
 
# Step 3 - Flattening
classifier.add(Flatten())
 
# Step 4 - Full connection
classifier.add(Dense(units=64, activation='relu'))
classifier.add(Dropout(0.5))
classifier.add(Dense(units=1, activation='sigmoid'))
 
# Compiling the CNN
classifier.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
 
# Part 2 - Fitting the CNN to the images
batch_size = 32
train_datagen = ImageDataGenerator(rescale=1. / 255,
                                   shear_range=0.2,
                                   zoom_range=0.2,
                                   horizontal_flip=True)
 
test_datagen = ImageDataGenerator(rescale=1. / 255)
 
training_set = train_datagen.flow_from_directory(training_set_path,
                                                 target_size=input_size,
                                                 batch_size=batch_size,
                                                 class_mode='binary')
 
test_set = test_datagen.flow_from_directory(test_set_path,
                                            target_size=input_size,
                                            batch_size=batch_size,
                                            class_mode='binary')
 
# Create a loss history
history = LossHistory()
 
classifier.fit_generator(training_set,
                         steps_per_epoch=8000/batch_size,
                         epochs=90,
                         validation_data=test_set,
                         validation_steps=2000/batch_size,
                         workers=12,
                         max_q_size=100,
                         callbacks=[history])
 
 
# Save model
model_backup_path = os.path.join(script_dir, '../dataset/cat_or_dogs_model.h5')
classifier.save(model_backup_path)
print("Model saved to", model_backup_path)
 
# Save loss history to file
loss_history_path = os.path.join(script_dir, '../loss_history.log')
myFile = open(loss_history_path, 'w+')
myFile.write(history.losses)
myFile.close()
 
backend.clear_session()
print("The model class indices are:", training_set.class_indices)

"""
Explanation: Challenge
Evaluation was already made during the training with the validation set, therefore k-Fold Cross Validation is not needed.
Then the techniques to improve and tune a CNN model are the same as for ANNs. So here is the challenge:
Apply the techniques you've learnt and use your architect power to make a CNN that will give you the gold medal:
- Bronze medal: Test set accuracy between 80% and 85%
- Silver medal: Test set accuracy between 85% and 90%
- Gold medal: Test set accuracy over 90%!!
Rules:
- Use the same training set
- Dropout allowed
- Customized SGD allowed
- Specific seeds not allowed
Better solution

Prediction Challenge Solution - Result: 90%
End of explanation
"""


