Introduction
So, you have built some kind of machine learning model in Keras. Now, you want to add a frontend interface so people can actually interact with your model in an approachable way. Look no further. In this tutorial, I will cover how to package your Keras model into a powerful web application using Flask.
This is the second article in a series talking about how I made and deployed a neural network to rate songs like the popular music reviewer Anthony Fantano. You can read about the process of researching and developing the neural network in the first article. The final article covers the issues I encountered deploying a Flask Web Application to Heroku.
Click here to skip to a demo of what we will be building in this article. Click here to see all my code.
Article Overview
- Cloning the Starter Code
- Mocking Up the Design
- Writing the HTML and CSS
- Creating the Model Operation Functions
- Taking in User Input
- Updating the Frontend with Javascript
- Demo and Conclusion
Cloning the Starter Code
In researching this topic myself, I stumbled upon this Github repository which promised to help “Deploy Keras Model with Flask as Web App in 10 Minutes.” While it took me more than 10 minutes to get to the finished product I wanted, this repository helped tremendously with speeding up the process. I recommend you all use it as a starting point if your web app only requires simple I/O from the user.
So, let’s go ahead and clone the starter project and check out how the code runs out of the box.
# 1. First, clone the repo
$ git clone https://github.com/mtobeiyf/keras-flask-deploy-webapp.git
$ cd keras-flask-deploy-webapp
# 2. Install Python packages
$ pip install -r requirements.txt
# 3. Run!
$ python app.py
Now that you have the Flask app running, you should be able to navigate to http://localhost:5000 and see the web app. Below is a demo of the built in functionality.
As you can see, the starter project includes basically all the deliverables for a flask web application using a keras model. If you have experience with web development and python, this may be enough for you to go off on your own and start building.
Now, I will work through my process of converting the starter code to meet my requirements.
Mocking Up The Design
I always like to start with a quick mockup before I dive into front end design. I wanted to fit the aesthetic that Anthony Fantano uses in theneedledrop. By using the same colors and fonts that Fantano uses in his videos, I settled on a mockup I was happy with.
Next, I moved on to converting this mockup into HTML and CSS.
Writing the HTML and CSS
I will walk through my code file by file for this section.
base.html
This file simply declares the meta information for your web app, including the title, google search image, and search description. It also connects the html to our css and javascript files. Additionally, I added the custom font I will be using for the project, League Gothic.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>The Needle Bot: How would Anthony Fantano rate your songs?</title>
<meta name="description" content="Using Artificial Intelligence, this bot will try to rate your songs just like Anthony Fantano of theneedledrop."/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="The Needle Bot: Rate Your Songs" />
<meta property="og:image" content="https://analyticsarora.com/wp-content/uploads/2021/06/the-needle-bot.jpeg" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='main.css') }}" />
<link rel="stylesheet" href="https://use.typekit.net/uys8pat.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.7.1.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/meyda@<VERSION>/dist/web/meyda.min.js"></script>
</head>
<body>
{% block content %}{% endblock %}
</body>
<footer>
<script src="{{ url_for('static',filename='main.js') }}"></script>
</footer>
</html>
index.html
The index file extends base.html and the code written here appears between the <body> tags in the base file. Specifically, the line {% block content %}{% endblock %} specified where the code from index.html will populate.
{% extends "base.html" %} {% block content %}
<div class="main">
<!-- Title Row with Image and App Name -->
<div class="title">
<img src="{{url_for('static', filename='melon_head.jpeg')}}" align="middle" class="melonHead"/>
<h3 class="titleText">The Needle Bot</h3>
</div>
<!-- -------- -->
<!-- File Upload Input Box -->
<div class="panel">
<input id="file-upload" class="hidden" type="file" accept="audio/*" name="audio_file"/>
<label for="file-upload" id="file-drag" class="upload-box">
<div id="upload-caption">Drop your song here or click to select (.wav)</div>
<p id="image-preview" class="hidden" />
</label>
</div>
<!-- -------- -->
<!-- Submit & Clear Buttons -->
<div style="margin-bottom: 2rem;">
<input type="button" value="Submit" class="button" onclick="submitImage();" />
<input type="button" value="Clear" class="button" onclick="clearImage();" />
</div>
<!-- -------- -->
<!-- Instructions and Loading Icon -->
<div class="text-column">
<h3 class="subText1" id='willhetext'>Will he love it? Will he hate it?<br>What will he rate it?</h3>
<h3 class="subText2 hidden" id="was-success">File Uploaded Successfully! Click submit to see results.</h3>
<div class="lds-ripple hidden" id='spinner'><div></div><div></div></div>
</div>
<!-- -------- -->
<!-- Model Results (Hidden at first) -->
<div id="image-box">
<h3 class="subText3 hidden" id="forthissong">For this song, I'm feeling a</h3>
<img id="image-display" />
<div id="pred-result" class="hidden"></div>
<h3 class="subText2 hidden" id="justopinion">Y'all know this is just my opinion, right?</h3>
<h4 class="subText4"><a href="http://google.com" target="_blank" rel="noopener noreferrer">(See how Melon Bot was made <u>here</u>)</a></h4>
</div>
<!-- -------- -->
</div>
{% endblock %}
For now, I’ll leave the html up to your own interpretation. I go into more detail about the file upload code and showing/hiding elements later in the article.
My CSS is pretty long and also self explanatory, so the code is omitted in this article. You can see all of my code in this repo.
Let’s move on to specifics of including a keras model in a flask application.
Creating the Model Operation Functions
Saving the Best Model
Before we can think about putting our model into the flask web app, we have to save it. You can save your keras model as either a single .h5 file or as a directory.
To save your model as a .h5 file do the following:
model.save("my_h5_model.h5")
To save your model as a directory, simply remove the file extension as shown:
model.save("my_h5_model")
Now, with the model saved we can move back to the flask project. I opted to contain my model, the relevant data preprocessing functions, and the predict function in a separate file from app.py . This is not necessary but it became helpful when it came time to deploy the web app to the cloud.
modeloperations.py
All the code I show here comes from the file modeloperations.py . I will explain it piece by piece. First, let’s get the imports out of the way.
import librosa
from sklearn.preprocessing import LabelEncoder
# TensorFlow and tf.keras
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.models import load_model
import numpy as np
import os
import random
from base64 import b64decode
After importing, it’s time to instantiate the model and the tensorflow session. If you used a LabelEncoder to decode the output of the model.predict() function, this is also the place to instantiate that.
config = tf.ConfigProto(
intra_op_parallelism_threads=1,
allow_soft_placement=True
)
session = tf.Session(config=config)
keras.backend.set_session(session)
model = load_model("best_fantano.h5", compile=False)
model._make_predict_function()
print('Model loaded. Start serving...')
le = LabelEncoder()
le.classes_ = np.load('static/classes.npy')
Data Preprocessing Function
Next, I wrote the function to preprocess the user inputted file and extract the necessary datapoints for my model. I am doing audio feature analysis so I need to load in the file from the tmp directory, extract the features, and finally delete the temporary file.
def extract_features_and_predict(path):
# Construct relative path
filepath = './tmp/' + path + '.wav'
# Extract Audio Features
x, sr = librosa.load(filepath)
rmse = librosa.feature.rms(y=x)
chroma_stft = librosa.feature.chroma_stft(y=x, sr=sr)
spec_cent = librosa.feature.spectral_centroid(y=x, sr=sr)
spec_bw = librosa.feature.spectral_bandwidth(y=x, sr=sr)
rolloff = librosa.feature.spectral_rolloff(y=x, sr=sr)
zcr = librosa.feature.zero_crossing_rate(x)
mfcc = librosa.feature.mfcc(y=x, sr=sr)
to_append = f'{np.mean(chroma_stft)} {np.mean(rmse)} {np.mean(spec_cent)} {np.mean(spec_bw)} {np.mean(rolloff)} {np.mean(zcr)}'
for e in mfcc:
to_append += f' {np.mean(e)}'
# Convert string to array of floats
vect = to_append.split()
for i in range(len(vect)):
vect[i] = float(vect[i])
# Make prediction
preds = model_predict(vect, model)
# Remove temporary file
os.remove(filepath)
# Return JSON formatted Predictions
responseObject = {
"result": preds.tolist(),
"errors": 'none'
}
return responseObject
Prediction Function
Once I get the features, I call my prediction function, model_predict(vect, model), which is shown below:
def model_predict(vector, model):
try:
with session.as_default():
with session.graph.as_default():
preds = model.predict(np.array([vector]))
return le.inverse_transform([np.argmax(preds)])
except Exception as ex:
print(ex)
return [2.0]
This may look verbose but the try except block is important here. Flask uses multiple threads. So, if you do not explicitly invoke the session and the session graph, your keras model will be loaded and used on different threads. If you encounter the error “Tensor is not an element of this graph“, this is likely your issue.
My model outputs a number between 1 and 10, so I default to 2 in the event of any error. You could implement better error handling that actually updates the front end.
That’s all for our modeloperations file!
Taking in User Input
Getting input from the user is basically essential to any web app with a machine learning model. The workflow that most follow is:
- Accept user input
- Send it to the backend via a post request
- Save the file in some temporary directory / an s3 bucket
- Do the actual analysis and return the results
Let’s start with accepting user input. The way to do this is using a single or multiple <input> tags and specifying the datatypes each accepts. You can also set a placeholder value to tell people what goes in each field.
<input id="file-upload" class="hidden" type="file" accept="audio/*" name="audio_file"/>
This is my input that accepts all types of audio files, including mp3 and wav. You can see that the type is set to file and I assigned the name ‘audio_file’. This name element will be important for sending the data via post if you’re sending files.
Accepting File Drag Events
Letting the user drag and drop files really makes the web app feel more polished. This can be accomplished easily with the following code. First, the HTML elements are shown:
<div class="panel">
<input id="file-upload" class="hidden" type="file" accept="audio/*" name="audio_file"/>
<label for="file-upload" id="file-drag" class="upload-box">
<div id="upload-caption">Drop your song here or click to select</div>
<p id="image-preview" class="hidden" />
</label>
</div>
Now, let’s take a look at the javascript code that powers file dragging:
//======================================================================
// Drag and drop image handling
//======================================================================
var fileDrag = document.getElementById("file-drag");
var fileSelect = document.getElementById("file-upload");
// Add event listeners
fileDrag.addEventListener("dragover", fileDragHover, false);
fileDrag.addEventListener("dragleave", fileDragHover, false);
fileDrag.addEventListener("drop", fileSelectHandler, false);
fileSelect.addEventListener("change", fileSelectHandler, false);
function fileDragHover(e) {
// prevent default behaviour
e.preventDefault();
e.stopPropagation();
fileDrag.className = e.type === "dragover" ? "upload-box dragover" : "upload-box";
}
function fileSelectHandler(e) {
// handle file selecting
var files = e.target.files || e.dataTransfer.files;
fileDragHover(e);
for (var i = 0, f; (f = files[i]); i++) {
previewFile(f);
}
}
When a file drag and drop event occurs, the previewFile(f) function is triggered, which is shown below.
function previewFile(file) {
// save a reference to the file and update the frontend
globFile = file;
var fileName = encodeURI(file.name);
imagePreview.innerText = fileName;
show(imagePreview);
hide(uploadCaption);
show(fileSuccess)
hide(willhetext)
}
In this case, I save a reference to the file object to be used in another function whenever the user hits “Submit”. I also update the front end to display the name of the file uploaded. I cover showing and hiding elements here.
Sending Files through a Post Request
Once we have gotten the file uploaded on the frontend, it is time to send it via POST to the backend.
function predictImage(file) {
var form = new FormData();
form.append('audio_file', file, "poopy.wav")
// "Content-Type": "multipart/form-data"
fetch("/predict", {
method: "POST",
headers: {
},
body: form
})
.then(resp => {
if (resp.ok)
resp.json().then(data => {
displayResult(data);
});
})
.catch(err => {
console.log("An error occured", err.message);
window.alert("Oops! Something went wrong.");
});
}
In order to send the file, I create a form data object to hold the file as a key value pair. I added the file to the form under the key ‘audio_file’. This is the same as the name that I set in the input field. I also assign a filename as the third parameter, in this case poopy.wav . This third parameter is required otherwise you won’t see the file in your endpoint code.
An important note is that I had to remove all the headers in order for my file to actually appear in my flask endpoint code. So, do not include the Content-Type header if you have multipart form data. That sounds counterintuitive so here is a quote that I thought was quite good:
you can absolutely send files via POST without using
Mark Amerymultipart/form-data
. What you can’t do is do that using an ordinary HTML form submission, without JavaScript. Setting a form to usemultipart/form-data
is the only mechanism that HTML provides to let you POST files without using JavaScript.
Saving Files to Temporary Directory
The post request gets sent off to the /predict route in the flask code. Let’s take a look at how I receive the request, extract the file, and save it to the temporary directory.
@app.route('/predict', methods=['GET', 'POST'])
def predict():
if request.method == 'POST':
file = request.files['audio_file']
random_number = random.randint(00000, 99999)
filepath = './tmp/' + str(random_number) + '.wav'
file.save(filepath)
filename = str(random_number) + '.wav'
res = extract_features_and_predict(filename)
# create a dictionary with the ID of the task
responseObject = {"status": "success", "data": res}
# return the dictionary
return jsonify(responseObject)
return None
I grab the file from request.files using the key that I set in the form data object. I then save the file to the temporary directory and pass on the file path to the data extraction / prediction function. Once the model finishes its prediction, I return the results in a JSON format to my javascript code.
Updating the Frontend with Javascript
Now, let’s dig in to how I am using Javascript to show and hide elements. Then, I will give you a full overview of what happens when the user clicks the Submit and Clear buttons.
Showing and Hiding Elements
Showing and hiding specific HTML elements is made easy with two utility functions:
function hide(el) {
// hide an element
el.classList.add("hidden");
}
function show(el) {
// show an element
el.classList.remove("hidden");
}
Calling these functions adds and removes the hidden class from a given element. The hidden class is shown below:
.hidden {
display: none;
}
Submit Function
When the user hits the submit button, the following functions are executed in the following order. First, the submit function is called:
function submitImage() {
// action for the submit button
console.log("submit");
// call the predict function of the backend
if (globFile !== null) {
if (globFile.type.split('/')[0] == 'audio') {
hide(fileSuccess)
show(spinner)
predictImage(globFile);
} else {
window.alert("Invalid File Type. Please submit an audio file");
}
} else {
window.alert("Please upload a .wav file before submitting");
}
}
The submit function performs some basic input validation, hides the label telling them their file has been uploaded, and shows them a loading icon. Then, a call is made to the predictImage(file) function.
function predictImage(file) {
var form = new FormData();
form.append('audio_file', file, "poopy.wav")
// "Content-Type": "multipart/form-data"
fetch("/predict", {
method: "POST",
headers: {
},
body: form
})
.then(resp => {
if (resp.ok)
resp.json().then(data => {
displayResult(data);
});
})
.catch(err => {
console.log("An error occured", err.message);
window.alert("Oops! Something went wrong.");
});
}
Here, we package the file into the form data as I previously explained. The endpoint will return an array with a single number in it. This is passed off to the displayResult(data) function which shows the user the model output.
function displayResult(data, errors) {
hide(spinner)
imageDisplay.src = './static/' + String(data[0]) + '.png'
if (imageDisplay.classList.contains('hidden')) {
show(imageDisplay)
}
show(justopinion)
show(forthissong)
}
The loading icon is hidden. The imageDisplay is updated to show the corresponding image for the model output, and two <p> tags are shown to explain the results. That’s it for submit!
Clear Function
The clear function resets the state of our web app back to how it looks on page load. For me, this just means showing and hiding certain elements and resetting my global file.
function clearImage() {
// reset selected files
fileSelect.value = "";
// remove image sources and hide them
imagePreview.src = "";
imageDisplay.src = "";
predResult.innerHTML = "";
globFile = null
hide(imagePreview);
hide(imageDisplay);
hide(predResult);
hide(justopinion)
hide(forthissong)
show(uploadCaption);
show(willhetext)
imageDisplay.classList.remove("loading");
}
Great! Now you should have a good understanding of how I made my flask web app for keras from start to end.
Demo and Conclusion
Let’s see the finished product of all that code! Below is a demo of my web app working.
Hopefully this article has given you some clarity on the process of creating a flask web app for keras machine learning models. Once you have created your own web application, you likely want to deploy it to the cloud so that other people can interact with it. Check out the final tutorial in this series where I detail the common issues you may encounter deploying to Heroku .
[…] focus on the research and development of the neural network. The next article in the series covers how I build a flask web application using my keras machine learning model. The final article covers the challenges I encountered deploying my web app to […]
Hello very nice web site!! Guy .. Excellent .. Superb ..
I will bookmark your website and take the feeds also?
I am satisfied to seek out a lot of helpful info here in the publish, we
want develop more strategies in this regard, thank
you for sharing. . . . . .