End-to-end CNN using TensorFlow
Tags: deeplearning, python, machinelearning, datascience
Published: 2020-12-04 11:31:40

# Table of content.
1. Data gathering.
2. Build a CNN model.
 - Getting our data ready.
 - Building a model.
 - Training a model.
 - Saving and reloading a trained model.
 - Testing Fun.
3. Build Gui using Tkinter.
4. Build a Flask app.
5. Deploy your Flask app into the Heroku.

You can find the complete code repo and the Jupyter notebook below
- {% github Vivek2509/Dog_VS_Cat_Flask_web_app no-readme%}
- [Jupuyer notebook](https://vivek2509.github.io/World_of_ML/deep%20learning/cnn/2020/11/21/DOG_VS_CAT.html)

Take a look at what we are going to build
[Dog_VS_Cat](https://dog-vs-cat.herokuapp.com/)
![Web_demo](https://dev-to-uploads.s3.amazonaws.com/i/ocxzg3edni03vumi56ac.png)


## 1. Data gathering
Data is gathered from [kaggle](https://www.kaggle.com/c/dogs-vs-cats)
If you want to use [Google-colab](https://colab.research.google.com/) make sure you download and upload the dataset into Google drive.

## 2. Build the CNN model.
For building a convolution neural network we use a prebuild model from [TensorFlow-Hub](https://tfhub.dev/).

After download, we can see that data is in the below structure

```
|-test
    |-image

|-train
    |-Dog 
       |-image1
       |-image2
    |-Cat
       |-image1
       |-image2

```
Before we jump into Jupyter notebook we have to make our data ready.

> **If you want to use this structure you can use TensorFlow** [*image_dataset_from_directory*](https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/image_dataset_from_directory)


### Getting our data ready
Here we make a function to rename all images and then move all images into a single folder after that we create a CSV file with filename and labels.

*A rename function*
```
import os
# 
def rename_image(path,name):
    '''
    Take the path of the folder and the name of what you want to rename.
    '''
    for count, filename in enumerate(os.listdir(path)):
        dst = name + '.' + str(count) + ".jpeg"
        src = path + filename
        dst = path + dst
        os.rename(src,dst)
    print("image change is :",count)

```
Above function take path and name that you want to change. 
`os.listdir` make a list of all files from that folder.
`dst` is used for the destination file and `src` for the source file.
here we add `name + count` for a unique name and then file extension.
now our loop goes through all images and change the name that you give in the function argument.


Before calling this function let's make a list of the label from that folder
```
label = []
for filename in os.listdir('path-to/train/'):
    label.append(filename)
```
This function is to take the filename from that path we provide and append it to the label list.

Now we make another function to rename all images from both folders
```
def file_name():
    path='path-to/train/'
    for i, name in enumerate(label):
        real_path = path + label[i] + '/'
        rename_image(real_path,name)

#call fun
file_name()
```
The above function is to call our previous rename function and applies to the subfolder as we gave labels also.

Now you can see that all images are renamed. let's move all images into a new folder with the help of the python `replace` function.

```
def move_files(old,new):
    for filename in os.listdir(old):
        os.replace(str(old) + str(filename), str(new) + str(filename))

def file_change(old_path,new_path):
    for i, name in enumerate(label):
        real_path = old_path + label[i] + '/'
        move_files(real_path,new_path)
    print("Suceess")

file_change(old_path='path-to/train/',new_path='DATA/FULL_DATA/')
```
move_files takes an old and new path then moves a file from old to new.
for change files from more than one folder, we provide all folder names (our folder names stored in label list) to file_change after that function call that `move_file` function and move images from subfolders also.

Now all our images are in one folder let's make CSV file from that. Before that, we have to convert data into DataFrame then save it to a CSV file.

*create CSV file*
```
import pandas as pd
filenames=os.listdir("DATA/FULL_DATA/")
categories=[]
for f_name in filenames:
    category = f_name.split('.')[0]
    for i, filename in enumerate(label):
        if category == filename:
            categories.append(filename)

#create pandas DataFrame
df=pd.DataFrame({
    'filename':filenames,
    'labels':categories
})

#save to csv
df.to_csv('data.csv',index=False)
```
In the above code, we have 2 lists `categories` and `label` now we make Pandas Data Frame from that. Then, we save that Data Frame into CSV with Pandas`to_csv`function.

Now our data is ready with the CSV file and filename accordingly let's build the CNN model. 

### Build a model  
> Now you can use this [Jupyter notebook](https://vivek2509.github.io/World_of_ML/deep%20learning/cnn/2020/11/21/DOG_VS_CAT.html) for complete code.

**Steps**
- Import tools.
```
import tensorflow as tf
import tensorflow_hub as hub
```
if you don't have this library use `pip install` to install it.

- Read CSV file.
```
import pandas as pd
labels_csv = pd.read_csv("/path-to-your-csv-file/Dog_vs_cat.csv")
```
The above code generates pandas DataFrame

- Make a full filename
```
filenames = ["path-to-our-new-full-image-folder/" + fname for fname in labels_csv["filename"]]

# Check the first 15
filenames[:15]
```

- Let's prepare our labels.
```
import numpy as np
labels = labels_csv["category"].to_numpy() 
labels
```
The above code converts our category into a numpy array.

- Turn label into an array of booleans.
```
unique_category = np.unique(labels)
boolean_labels = [label == unique_category for label in labels]
```
Now, this code converts our label into a category like here we have 2 labels so for the first item it generates an array of [True, False] if our label is 0.
 
- Preprocessing Images.
```
IMG_SIZE = 224

# Function
def process_image(image_path, image_size=IMG_SIZE):
  """
  Takes an image file path and turns the image into a Tensor.
  """
  # Read in an image file
  image = tf.io.read_file(image_path)
  # Turn the jpg image into numerical Tensor with 3 colour channel(RGB)
  image = tf.image.decode_jpeg(image,channels=3)
  # Convert the color channel values to (0-1) values
  image = tf.image.convert_image_dtype(image,tf.float32)
  # Resize the image to (224,224)
  image = tf.image.resize(image, size=[image_size,image_size])

  return image
```
The above function takes an image path. 
After that with the help of `tf.io.read_file` fun read an image then convert it into tensors here tensor values are from `0 to 255`. Now convert that into (0,1) and lastly resize it to `224, 224` sizes.

- Turning our data into Batches.
```
def get_image_lable(image_path,label):
  """
  Takes an image file path name and the label,
  processes the image and return a tuple (image, label).
  """
  image = process_image(image_path)
  
  return image, label
```
here fun takes an image path and return with its label.

- Let's make a function to turn all of the data into batches!
```
BATCH_SIZE = 32

# Function to convert data into batches
def create_data_batches(X,y=None, batch_size=BATCH_SIZE,valid_data=False):
  """
  Creates batches of data of image (X) and label (y) pairs.
  Shuffle the data if it's training data but doesn't shuffle if it's validation data.
  """
  # If data is valid dataset (NO SHUFFLE)
  if valid_data:
    print("Creating valid data batches.........")
    data = tf.data.Dataset.from_tensor_slices((tf.constant(X),
                                               tf.constant(y)))
    data_batch = data.map(get_image_lable).batch(batch_size)
    return data_batch

  else:
    print("Creating train data batches.........")
    # Turn filepaths and labels into Tensors
    data = tf.data.Dataset.from_tensor_slices((tf.constant(X),
                                               tf.constant(y)))
    # Shuffling pathname and labels before mapping image processor fun
    data = data.shuffle(buffer_size=len(X))
    data_batch = data.map(get_image_lable).batch(batch_size)

    return data_batch
```
This will create batches from our data.
It is good practice to convert your data into batches.

- create batches
```
train_data = create_data_batches(X_train, y_train)
val_data = create_data_batches(X_val, y_val, valid_data=True)
```
create our batches with shuffle if it's train data and not shuffle if it's valid data.

- Creating our own validation set.
```
X = filenames
y = boolean_labels

from sklearn.model_selection import train_test_split

# Into train and valid 
X_train, X_val, y_train, y_val = train_test_split(X,y,                               test_size=0.2,random_state=42)

```
It is good practice to always divided our data into train and valid. Generally, the split size is around 70-30 or 80-20 ratio.
that 70% is used to train our model and 30 is for validation.

- Turning our data into Batches and Visualizing Data Batches
```
import matplotlib.pyplot as plt
# Create fun for viewing in a data batch
def show_images(images, labels):
  """
  Displays a plot of 25 images and their labels from a data batch.
  """
  plt.figure(figsize=(20, 20))
  for i in range(25):
    # Subplot
    ax = plt.subplot(5,5,i+1)
    plt.imshow(images[i])
    plt.title(unique_category[labels[i].argmax()])
    plt.axis("Off")


train_images, train_labels = next(train_data.as_numpy_iterator())
show_images(train_images,train_labels)
```
Now we can see our train images with its label.

let's recap what we have done until now.
>Read our data then create labels and filename after that convert data into batches with the right size and label.

Now let's build a model

- Building a model
```
INPUT_SHAPE = [None, IMG_SIZE, IMG_SIZE, 3] # Batch, height, width, Colour_chanels

# Setup output shape of the model
OUTPUT_SHAPE = len(unique_category)

# Setup model URL
MODEL_URL = "https://tfhub.dev/google/imagenet/mobilenet_v2_130_224/classification/4"
```
here we define our model URL with input shape and output shape

```
def create_model(input_shape=INPUT_SHAPE,output_shape=OUTPUT_SHAPE, model_url=MODEL_URL):
  print("Building model with:", model_url)

  # Setup the model
  model = tf.keras.Sequential(
                   [hub.KerasLayer(model_url),
                    tf.keras.layers.Dense(units=output_shape,
                    activation="softmax")]
)

  # Compile the model
  model.compile(
      loss = tf.keras.losses.BinaryCrossentropy(),
      optimizer = tf.keras.optimizers.Adam(),
      metrics = ["accuracy"]
  )

  # Build the model
  model.build(input_shape)

  return model
```
This will compile our model with `loss = BinaryCrossentropy()` as our problem is a binary classification. If we have more than 2 labels then we use `CategoricalCrossentropy()`.

![model.summary()](https://dev-to-uploads.s3.amazonaws.com/i/0bgjzofuspsgeaehsdmk.png)

- Early stopping [callback](https://www.tensorflow.org/api_docs/python/tf/keras/callbacks/EarlyStopping).
```
early_stopping = tf.keras.callbacks.EarlyStopping(monitor="val_accuracy",
                                                  patience = 3)
```
If you set patience=3 means that your training will stop after 3 epochs if it's not improving. with this callback, your model is not overfitting. 
If your model is overfitting it only remembers training data and it fails on valid data. So try to reduce the model from overfitting.

- Train a model
```
def train_model():
  # Fit the model
  model.fit(x=train_data,
            epochs= 100,
            validation_data=val_data,
            validation_freq = 1,
            callbacks = [early_stopping])
  return model

model = train_model()
```
![model.fit](https://dev-to-uploads.s3.amazonaws.com/i/ptqezl6t2zs4xjtzcalj.png)
here we see that after some epoch our training is stopped because of that callback we gave.

- Making and evaluating prediction using a trained model.
```
predictions = model.predict(val_data, verbose=1)
predictions[0]
```
here we see that predictions give a probability of label into an array of two values. If you add them then the total sum is = 1.

```
np.argmax(prediction[0])
```
`np.argmax` takes an array and returns a new array with an integer value.If original value is [0.3, 0.7] then argmax return [0,1]. 

It means it returns 1 if its original value if is greater than 0.5 

- Saving a trained model.
```
model.save('model.h5')
```
This will save our model. so we can use our model without retraining. 

- Predict on custom data.

```
def test_data(path):
  demo = imread(path)
  demo = tf.image.convert_image_dtype(demo,tf.float32)
  demo = tf.image.resize(demo,size=[224,224])
  demo = np.expand_dims(demo,axis=0)

  pred = model.predict(demo)
  result = unique_category[np.argmax(pred)]

  return result
```
The above function is the same as we used to preprocess our image. additionally here we have to provide `expand_dims` which expands the dimension of our data. Because our model is trained with input shape = [32,224,224,3] and here we have only 1 image so we have to take our image which currently has [224,224,3] to convert to [1,224,224,3] same as our model input shape.


As of now, we have a train our CNN model and evaluating using valid data and test with some test data with a test path.

Now let's build a GUI using Tkinter so we can directly load our image and get back the result.

## 3. Build a GUI using Tkinter.

Read more [Tkinter](https://docs.python.org/3/library/tkinter.html)

**Steps**
- Create a python file
- Import libraries 
- Import model
- Init a Gui
- Build a function to classify an image 
- Upload function

```
#import lib
import tkinter as tk
from tkinter import filedialog
from tkinter import *
from matplotlib.pyplot import imread
import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
from PIL import ImageTk, Image

#import model
print('_________________________________')
print('..........Start loading..........')

model = tf.keras.models.load_model('model.h5', custom_objects={
                                   "KerasLayer": hub.KerasLayer})

print('_________________________________')
print('...........Model Loaded..........')
print('_________________________________')

# dictionary to label all traffic signs class.
labels = ['Cat', 'Dog']

# initialise GUI
top = tk.Tk()
top.geometry('800x600')
top.title('Dog_VS_Cat Classification')
top.configure(background='#CDCDCD')
label = Label(top, background='#CDCDCD', font=('arial', 15, 'bold'))
sign_image = Label(top)

#To classify an image
def classify(file_path):
    global label_packed
    image = imread(file_path)
    image = tf.image.convert_image_dtype(image, tf.float32)
    image = tf.image.resize(image, size=[224, 224])
    image = np.expand_dims(image, axis=0)
    pred = model.predict(image)
    sign = labels[np.argmax(pred)]
    print(sign)
    label.configure(foreground='#011638', text=sign)

#Button
def show_classify_button(file_path):
    classify_b = Button(top, text="Classify Image",
                        command=lambda: classify(file_path),
                        padx=10, pady=5)
    classify_b.configure(background='#364156', foreground='white',
                         font=('arial', 10, 'bold'))
    classify_b.place(relx=0.79, rely=0.46)

#Upload image
def upload_image():
    try:
        file_path = filedialog.askopenfilename()
        uploaded = Image.open(file_path)
        uploaded.thumbnail(((top.winfo_width()/2.25),
                            (top.winfo_height()/2.25)))
        im = ImageTk.PhotoImage(uploaded)
        sign_image.configure(image=im)
        sign_image.image = im
        label.configure(text='')
        show_classify_button(file_path)
    except:
        pass

upload = Button(top, text="Upload an image",
                command=upload_image, padx=10, pady=5)
upload.configure(background='#364156', foreground='white',
                 font=('arial', 10, 'bold'))
upload.pack(side=BOTTOM, pady=50)
sign_image.pack(side=BOTTOM, expand=True)
label.pack(side=BOTTOM, expand=True)
heading = Label(top, text="Image Classification",
                pady=20, font=('arial', 20, 'bold'))
heading.configure(background='#CDCDCD', foreground='#364156')
heading.pack()
top.mainloop()

```

**Demo**
![Tkinter Demo](https://dev-to-uploads.s3.amazonaws.com/i/ya5vq7vw5j3zxca6oum4.png)

Now we have used our model in Gui it's time to make it public so everyone can use it.

For this, we have to build a web app that can get an image from the user's device and store it then predict using our model, and give the result back to the user's device.

## Build a Flask app.

> Use below repo for complete code 
{% github Vivek2509/Dog_VS_Cat_Flask_web_app no-readme%}

*File structure of flask app*
```
|-static
    |-js
    |-uploads
|-templates
    |-base.html
    |-index.html
|-app.py
|-model.h5

```

**Steps In app.py**

- Import libraries 
```
import os
import numpy as np
from matplotlib.pyplot import imread
import tensorflow as tf
import tensorflow_hub as hub

# Flask utils
from flask import Flask, redirect, url_for, request, render_template
from werkzeug.utils import secure_filename

```

- Define a flask app
```
# Define a flask app
app = Flask(__name__)

STATIC_FOLDER = 'static'
# Path to the folder where we'll store the upload before prediction
UPLOAD_FOLDER = STATIC_FOLDER + '/uploads'

labels = ['Cat', 'Dog']

```

- Load model same as we did in GUI.
```
def load__model():
    print('[INFO] : Model loading ................')
    model = tf.keras.models.load_model('model.h5', 
                                      custom_objects{
                                     "KerasLayer": hub.KerasLayer}
    )

    return model


model = load__model()
print('[INFO] : Model loaded ................')

```

- Function to preprocess image and predict image
```
def preprocessing_image(path):
    img = imread(path)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, size=[224, 224])
    img = np.expand_dims(img, axis=0)

    return img

def predict(model, fullpath):
    image = preprocessing_image(fullpath)
    pred = model.predict(image)

    return pred

```

- Define route
```
@app.route('/', methods=['GET'])
def index():
    # Main page
    return render_template('index.html')


@app.route('/predict', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        # Get the file from post request
        file = request.files['file']
        fullname = os.path.join(UPLOAD_FOLDER, file.filename)
        file.save(fullname)

        # Make prediction
        pred = predict(model, fullname)
        result = labels[np.argmax(pred)]

        return result
    return None



if __name__ == '__main__':
    app.run(debug=True)
```
We create our predict function on the `predict` route. So when the
form button is clicked our data is fetched from that.

**Templates**

Index.html
```
{% extends "base.html" %} {% block content %}

<h2>Dog_VS_Cat Classifier</h2>

<div>
  <form id="upload-file" method="post" enctype="multipart/form-data">
    <label for="imageUpload" class="upload-label"> Choose... </label>
    <input
      type="file"
      name="file"
      id="imageUpload"
      accept=".png, .jpg, .jpeg"
    />
  </form>

  <div class="image-section" style="display: none">
    <div class="img-preview">
      <div id="imagePreview"></div>
    </div>
    <div>
      <button type="button" class="btn btn-primary btn-lg" id="btn-predict">
        Predict!
      </button>
    </div>
  </div>

  <div class="loader" style="display: none"></div>

  <h3 id="result">
    <span> </span>
  </h3>
</div>

{% endblock %}

```

Run from Command line
```
flask run
```
This will run our flask app on localhost

**Demo**
![Flask_demo](https://dev-to-uploads.s3.amazonaws.com/i/6937r06oli4nghmq0tlh.png)

Now you can see on the address bar that currently, our flask app runs on localhost so not everyone can use it.

For making a model deploy into production we use Heroku as a platform as a service (PaaS). where we can store our model and build flask API to fetch prediction on images. and everyone can use it.


## Deploy your Flask app into the Heroku.

- Login into Heroku account. If you don't have any make it [here](https://www.heroku.com/). You can read more about how to use it on [docs](https://devcenter.heroku.com/articles/getting-started-with-python)
- Install Heroku CLI. Read more about [here](https://devcenter.heroku.com/articles/heroku-cli)
- Login into heruko using CLI
```
heroku login
```
- Creating Apps from the CLI
```
heroku create name-of-your-app
```
We have to add 2 files to our project to work on Heroku.
- requirements.txt
> here we add all our libraries that are used to build our entire project. Make sure you add the right version number also.
- Procfile
> here we can add our app start point with guicorn. so you have to install guicorn as well.

*Procfile*
```
web: gunicorn app:app
```

*requirements.txt*
```
flask==1.1.1
tensorflow-cpu==2.3.0
tensorflow_hub==0.10.0
matplotlib==3.3.3
numpy==1.18.0
gunicorn==20.0.4

```

After adding this file we have to init our project with git. Make sure use install git on the local machine. you can download it [here](https://git-scm.com/).

- Init
```
git init
```
- Add all file 
```
git add .
```
- Commit the change
```
git commit -am "Deploy our flask app"
```
- Push to Heroku for build
```
git push heroku master
```
And it's done. you see your web app link after some build message.

## Contact 
Hey Readers, thank you for your time. If you liked the blog, don’t forget to appreciate it.

**| Ai/ML Enthusiast | Blogger | Web Dev |**

If you have any queries or suggestions feel free to contact me 

[`Twitter`](https://twitter.com/Vivek2509_) [`Linkedin`](https://www.linkedin.com/in/vivek2509/) [`Github`](https://www.github.com/Vivek2509)

**Happy deep learning and have a great time learning how to make machines smarter.**