# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os, shutil
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" 
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
# unziping the train zip and test zip file after copying them to the working folder 
# remember in kaggle input folder have been given only read only acess by the admin
# hence we copy in the working folder and then complete our job
!rm -rf ./train ./cats_and_dogs_small ./train.zip ./cats_and_dogs_small_2.h5 ./cats_and_dogs_small_1.h5
# making the copy of the test and train dir from the input dir since it is read-only
!ls -lrt
# !cp /kaggle/input/dogs-vs-cats/test1.zip /kaggle/working
# !unzip /kaggle/working/test1.zip 
!cp /kaggle/input/dogs-vs-cats/train.zip /kaggle/working
!unzip /kaggle/working/train.zip
!cd /kaggle/working
# use this block wisely
# checking the contents of my working directory
# use this wisely
for dirname, _, filenames in os.walk('/kaggle/working'):
    for filename in filenames:
        print(os.path.join(dirname, filename))
        
!ls -lrt
"""
First lets make the small dataset we will be working with. Because here our goal is to work with a smaller dataset  create a new
dataset containing three subsets: a training set with 1,000 samples of each class, a validation set with 500 samples of each class, and a test set with 500 samples of each class. The original datset consists 25,000 images of dogs and cats (12,500 from each class) and is 543 MB (compressed). But since our goal is to use a smaller dataset we will resist the temptation of using it.
"""
!cd /kaggle/working
!ls -lrt
# first lets set the base paths and make the necessary directories for working out our problem
original_dataset_dir = '/kaggle/working'
base_dir = '/kaggle/working/cats_and_dogs_small'
os.mkdir(base_dir)
train_dir = os.path.join(base_dir,'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
"""
Check using the console the directories at this stage.

Do a 'cd /cats_and_dogs_small' and then run the next block.
"""
# gives you the content of the newly made folder
!ls -lrt

"""
Now go to console opnce again and then do a 'cd ..'
"""
# now lets make the cats and dogs directories in them for housing the cats and the dogs images
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)
"""
Now go and check from the console if the directories are made. How ? 
If you know unix you know how.
"""
# Now we will be copying the actual data from the base directory to the working directories
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(src, dst)
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(src, dst)
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    src = os.path.join(os.path.join(original_dataset_dir,'train'), fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(src, dst)
"""
Now lets check the length of the dat if they are being properly transferred.
"""
# checking the that data have been properly copied or not
print('total training cat images:', len(os.listdir(train_cats_dir)))
print('total training dog images:', len(os.listdir(train_dogs_dir)))
print('total validation cat images:', len(os.listdir(validation_cats_dir)))
print('total validation dog images:', len(os.listdir(validation_dogs_dir)))
print('total test cat images:', len(os.listdir(test_cats_dir)))
print('total test dog images:', len(os.listdir(test_dogs_dir)))
# Now for ease of use I will do something
# since the train folder contains too many images which we don't need I will delete it.
!rm -rf ./train
!rm -rf ./train.zip
"""
Okay Now we have the required dataset. Now I will start with the actual chapter. And before doing that I will actual save the version because saving the file commits the file in kaggle/working. And if you don't their is good chance you can loose the files.
"""
"""
# DOG vs CAT classifier using a ConvNet

We previously built a small ConvNet for MNIST dataset. We wil go ahead with a similar structure for our networkover here too only since we are dealing with a bigger images and a more complex problem , so we will make our network larger than last time by just adding another pair of Conv2D and MaxPooling2D layers. This serves both to augment the capacity of the network and to further reduce the size of the feature maps so they aren’t overly large when you reach the Flatten layer. Here, because you start from inputs of size 150 × 150 (a somewhat arbitrary choice), you end up with feature maps of size 7 × 7 just before the Flatten layer. 

Also Note The depth of the feature maps progressively increases in the network(from 32 to 128), whereas the size of the feature maps decreases (from 148 × 148 to 7 × 7). This is a pattern you’ll see in almost all convnets.

Because you’re attacking a binary-classification problem, you’ll end the network with a single unit (a Dense layer of size 1) and a sigmoid activation. This unit will encode the probability that the network is looking at one class or the other.

Okay now lets start with defining the network
"""
# defining the network
# importing the layers and models sub- module from keras library
from keras import layers,models

model = models.Sequential() # using a sequentail class to make the model

# defining the first layer which will be a Convulational layer with a feature map size 32 (filters) over a filter of 3 x 3 
# and the input shape will be that of size (150,150,3) which encodes the length breadth and channels of the input image.
model.add(layers.Conv2D(32, (3, 3), activation='relu',input_shape=(150, 150, 3)))
# just like last time the second layer will be a maxpooling layer of size (2,2) which will agressively downsample 
# the output of the conv layer
model.add(layers.MaxPooling2D((2, 2)))
# we will be repeating the same thing for a few times with a increasing 
# feature map learning new features from existing features
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
# downsampling again in the fourth layer
model.add(layers.MaxPooling2D((2, 2)))
# repeating same two steps
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
# and one final time
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

# Now lets define the classifier
# and we start with a flatten layer transforming the output of the base from a 3D Tensor to 1D tensor
model.add(layers.Flatten())
# followed by a dense layer with 512 nodes 
model.add(layers.Dense(512, activation='relu'))
# ending with sigmoid activated 1 noded classifing final layer
model.add(layers.Dense(1, activation='sigmoid'))

# checking the model Summary Now
model.summary()

"""
For the compilation step, you’ll go with the RMSprop optimizer, as usual. Because you ended the network with a single sigmoid unit, you’ll use binary crossentropy as the loss .
"""
# compiling the network

from keras import optimizers
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])
"""
# Data preprocessing

As you know by now, data should be formatted into appropriately preprocessed floatingpoint tensors before being fed into the network. Currently, the data sits on a drive as JPG files, so the steps for getting it into the network are roughly as follows:
1.  Read the picture files.
2.  Decode the JPEG content to RGB grids of pixels.
3.  Convert these into floating-point tensors.
4.  Rescale the pixel values (between 0 and 255) to the [0, 1] interval (as you know, neural networks prefer to deal with small input values).


It may seem a bit daunting, but fortunately Keras has utilities to take care of these steps automatically. Keras has a module with image-processing helper tools, located at keras.preprocessing.image. In particular, it contains the class ImageDataGenerator, which lets you quickly set up Python generators that can automatically turn image files on disk into batches of preprocessed tensors. This is what you’ll use here. 
"""
# Using ImageDataGenerator to read images from directories
# if you don't know about generators go check it in the generators in python documentation

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir,
                                                    target_size=(150, 150),
                                                    batch_size=20,
                                                    class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir,
                                                        target_size=(150, 150),
                                                        batch_size=20,
                                                        class_mode='binary')

"""
Let’s look at the output of one of these generators: it yields batches of 150 × 150 RGB images (shape (20, 150, 150, 3)) and binary labels (shape (20,)). There are 20 samples in each batch (the batch size). Note that the generator yields these batches indefinitely: it loops endlessly over the images in the target folder. For this reason, you need to break the iteration loop at some point.
"""
# checking the shape of the output from this generators
for data_batch, labels_batch in train_generator:
    print('data batch shape:', data_batch.shape)
    print('labels batch shape:', labels_batch.shape)
    break
"""
Let’s fit the model to the data using the generator. You do so using the fit_generator method, the equivalent of fit for data generators like this one. It expects as its first argument a Python generator that will yield batches of inputs and targets indefinitely, like this one does. Because the data is being generated endlessly, the Keras model needs to know how many samples to draw from the generator before declaring an epoch over. This is the role of the steps_per_epoch argument: after having drawn steps_per_epoch batches from the generator—that is, after having run for steps_per_epoch gradient descent steps—the fitting process will go to the next epoch. 

Just remember it is nothing but the number of batches and batch size is the number of samples in the batches. The relationship actually goes as train_length // batch_size

In this case, batches are 20 samples, so it will take 100 batches until you see your target of 2,000 samples.

When using fit_generator, you can pass a validation_data argument, much as with the fit method. It’s important to note that this argument is allowed to be a data generator, but it could also be a tuple of Numpy arrays. If you pass a generator as validation_data, then this generator is expected to yield batches of validation data endlessly; thus you should also specify the validation_steps argument, which tells the process how many batches to draw from the validation generator for evaluation. 
"""
# Fitting the model using a batch generator
history = model.fit_generator(train_generator,
                              steps_per_epoch=100,
                              epochs=30,
                              validation_data=validation_generator,
                              validation_steps=50)
# now we will be doing something we haven't done previously
# its a simple practice but it is a good practice.
# So lets save our model
model.save('cats_and_dogs_small_1.h5')
"""
Let’s plot the loss and accuracy of the model over the training and validation data during training.
"""
# Displaying curves of loss and accuracy during training

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
"""
These plots are characteristic of overfitting. The training accuracy increases linearly over time, until it reaches nearly 100%, whereas the validation accuracy stalls at 70–72%. The validation loss reaches its minimum after only five epochs and then stalls, whereas the training loss keeps decreasing linearly until it reaches nearly 0. Because you have relatively few training samples (2,000), overfitting will be your number-one concern. You already know about a number of techniques that can help mitigate overfitting, such as dropout and weight decay (L2 regularization). We’re now going to work with a new one, specific to computer vision and used almost universally when processing images with deep-learning models: data augmentation. 
"""
"""
# Using data augmentation
Overfitting is caused by having too few samples to learn from, rendering you unable to train a model that can generalize to new data. Given infinite data, your mode would be exposed to every possible aspect of the data distribution at hand: you would
never overfit. Data augmentation takes the approach of generating more training data from existing training samples, by augmenting the samples via a number of random transformations that yield believable-looking images. The goal is that at training time, your model will never see the exact same picture twice. This helps expose the model to more aspects of the data and generalize better. In Keras, this can be done by configuring a number of random transformations to be performed on the images read by the ImageDataGenerator instance. Let’s get started with an example. 
"""
datagen = ImageDataGenerator(rotation_range=40,
                             width_shift_range=0.2,
                             height_shift_range=0.2,
                             shear_range=0.2,
                             zoom_range=0.2,
                             horizontal_flip=True,
                             fill_mode='nearest')
"""
These are just a few of the options available (for more, see the Keras documentation).

* rotation_range is a value in degrees (0–180), a range within which to randomly rotate pictures.
* width_shift and height_shift are ranges (as a fraction of total width orheight) within which to randomly translate pictures vertically or horizontally.
* shear_range is for randomly applying shearing transformations.
* zoom_range is for randomly zooming inside pictures.
* horizontal_flip is for randomly flipping half the images horizontally—relevant when there are no assumptions of horizontal asymmetry (for example,real-world pictures).
* fill_mode is the strategy used for filling in newly created pixels, which canappear after a rotation or a width/height shift.

Let’s look at the augmented images

"""
# checking the augmented images by plotting them using matplotlib
# after converting them into tensors and then augmenting them
from keras.preprocessing import image

fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]

img_path = fnames[20]
img = image.load_img(img_path, target_size=(150, 150))

x = image.img_to_array(img)
x = x.reshape((1,) + x.shape)

i = 0
for batch in datagen.flow(x, batch_size=1):
    plt.figure(i)
    imgplot = plt.imshow(image.array_to_img(batch[0]))
    i += 1
    if i % 4 == 0:
        break
plt.show()
"""
I guess now we all understand what data augmentation stands for.

Now if we train a new network using this data-augmentation configuration, the network will never see the same input twice. But the inputs it sees are still heavily intercorrelated, because they come from a small number of original images—you can’t produce new information, you can only remix existing information. As such, this may not be enough to completely get rid of overfitting. To further fight overfitting, you’ll also add a Dropout layer to your model, right before the densely connected classifier.

So come on lets configure a new network again with data augmentation.
"""
# defining the new network

model = models.Sequential() # calling the sequential class

# making the convnet just as before
model.add(layers.Conv2D(32, (3, 3), activation='relu',
                        input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
# model.add(layers.BatchNormalization())
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))

# defining the classifier same as before
model.add(layers.Flatten())
# adding the dropout layer as discussed 
model.add(layers.Dropout(0.5))
# adding a batch normalization layer to normalize
# the flactuations in the output analysis curve
model.add(layers.BatchNormalization())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])

# checking in the model summary

model.summary()
"""
Let’s train the network using data augmentation and dropout.
"""
import math
BATCH_SIZE=40

TRAINING_SIZE = 2000

VALIDATION_SIZE = 1000

# We take the ceiling because we do not drop the remainder of the batch
compute_steps_per_epoch = lambda x: int(math.ceil(1. * x / BATCH_SIZE))

steps_per_epoch = compute_steps_per_epoch(TRAINING_SIZE)
val_steps = compute_steps_per_epoch(VALIDATION_SIZE)
print(steps_per_epoch,val_steps)
# creating the data augmentation generator classes

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

# note that we don't apply data augmentation to the test because that will be useless

test_datagen = ImageDataGenerator(rescale=1./255)

# and defining the training generator

train_generator = train_datagen.flow_from_directory(train_dir,
                                                    target_size=(150, 150),
                                                    batch_size=32,
                                                    class_mode='binary')

# and defining the validation generator

validation_generator = test_datagen.flow_from_directory(validation_dir,
                                                        target_size=(150, 150),
                                                        batch_size=32,
                                                        class_mode='binary')

# training the new model

history = model.fit_generator(train_generator,
                              steps_per_epoch=50,
                              epochs=100,
                              validation_data=validation_generator,
                              validation_steps=25)
"""
Let’s save the model
"""
# saving the second model

model.save('cats_and_dogs_small_2.h5')
"""
And let’s plot the results again. 
"""
# ploting the graphs 

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()
"""
By using regularization techniques even further, and by tuning the network’s parameters (such as the number of filters per convolution layer, or the number of layers in the network), you may be able to get an even better accuracy, likely up to 86% or 87%. So keep trying, also try using batch normalization, it may prove good in order to reduce the fluctuations in the analysis curve.
"""