Productionize a Machine Learning model with Flask and Heroku | by GreekDataGuy | Towards Data Science

Coming from a software development background I never thought twice about productionizing my models. But for anyone just getting into coding, as a data scientist, it may not be obvious.
For a model to be used in the real world, you’ll need to productionize it.
In this post we’ll cover a few things:1. creating an ML model2. wrapping it in a Flask API3. deploying that API onto Heroku
This tutorial is not about training a model so we’ll train a really simple model on the Boston housing dataset without regards to how well it works.
# import datasetfrom sklearn.datasets import load_boston# load datadata = load_boston()import pandas as pd# load into a dataframedf = pd.DataFrame(data['data'], columns=data['feature_names'].tolist())df['label'] = data['target']df.head()
label on the left represents Median value of owner-occupied homes in $1000’s average home value in thousands of dollars in each area. The idea behind this dataset is to use values in the other columns to predict average home value.
We’ll just use RM, average number of rooms per dwelling to make our predictions, fully knowing this will not create an accurate model.
# shuffle datashuffled_df = df.sample(frac=1)# split datasplit = int(len(shuffled_df) * .7)train_df = shuffled_df[:split]test_df = shuffled_df[split:]# stock function for extracting X,y columns from dfdef X_and_y_from_df(df, y_column, X_columns = []):    X = {}    for feature in X_columns:        X[feature] = df[feature].tolist()    y = df[y_column].tolist()    return X, y# extract X and yX_train, y_train = X_and_y_from_df(train_df, 'label', ['RM'])X_test, y_test = X_and_y_from_df(test_df, 'label', ['RM'])# reshapeimport numpy as npX_train_arr = np.array(X_train['RM'])X_train_arr = X_train_arr.reshape(X_train_arr.shape[0],1)X_test_arr = np.array(X_test['RM'])X_test_arr = X_test_arr.reshape(X_test_arr.shape[0],1)# train modelfrom sklearn.linear_model import LinearRegressionmodel = LinearRegression()model.fit(X_arr, y_train)# predict resultsy_pred = model.predict(X_test_arr)
I’ve predicted results here but frankly, we don’t care. We could build a much more accurate model if we spent more time on the problem.
Pickle what?!
Pickling converts the object into a byte stream which can be stored, transferred, and converted back to the original model at a later time. Pickles are one of the ways python lets you save just about any object out of the box.
import picklepickl = {'model': model}pickle.dump( pickl, open( 'model_file' + ".p", "wb" ) )
Boom. Our model is now saved as a pickle in the same directory as our jupyter notebook.
Flask is a micro frameworks for Python. It comes with none of the excess cruft of other frameworks so its perfect for building a simple API.
We’re going to run a few commands on the command line to get things started.
Create a directory to store the appmkdir FlaskAPI
cd into the directorycd FlaskAPI
Create a virtual environmentpython3 -m venv vir_env
Activate the virtual environmentsource vir_env/bin/activate
Install all the packages we’ll needpip3 install flask gunicorn sklearn
Save a list of packages and versions that production will need to installpip3 freeze > requirements.txt
Inside the last directory we created, create a few files and another directory with the command line.
touch app.pytouch Procfiletouch wsgi.pymkdir models
Open the FlaskAPI directory in your favorite code editor. I like sublime so I’ll launch it in the current directory with sublime ..
Copy the following into app.py .
import flaskfrom flask import Flask, jsonify, requestimport jsonapp = Flask(__name__)@app.route('/predict', methods=['GET'])def predict(): response = json.dumps({'response': 'yahhhh!'}) return response, 200if __name__ == '__main__': application.run(debug=True)
And copy the following into wsgi.py
from app import app as applicationif __name__ == "__main__":    application.run()
Now start the app! In the same directory run gunicorn --bind 0.0.0.0:8080 wsgi:application -w 1
And you should see...
Try making a request to it from another terminal window with curl -X GET http://0.0.0.0:8080/predict
Success. We have a Flask API running locally.
Time to add our machine learning model. Grab your model_file.p pickle and drop it into the /models/ directory in the app we made so your file tree now looks like the left.
Add this function to app.py
def load_models():    file_name = "models/model_file.p"    with open(file_name, 'rb') as pickled:        data = pickle.load(pickled)        model = data['model']    return model
And modify your predict() function so it looks like this
def predict():    # stub input features    x = 5.963    # load model    model = load_models()    prediction = model.predict([[x]])[0]    response = json.dumps({'response': prediction})    return response, 200
You’ll also need to add import json and import pickle to the top of app.py .
app.py now looks like
import flaskfrom flask import Flask, jsonify, requestimport jsonimport pickleapp = Flask(__name__)def load_models():    file_name = "models/model_file.p"    with open(file_name, 'rb') as pickled:       data = pickle.load(pickled)       model = data['model']    return model@app.route('/predict', methods=['GET'])def predict():    # stub input features    x = 5.963    # load model    model = load_models()    prediction = model.predict([[x]])[0]    response = json.dumps({'response': prediction})    return response, 200if __name__ == '__main__':    application.run(debug=True)
Note that we’re mocking an input value rather than actually sending it to the app and parsing the response — that’s coming.
Let’s test the app again to ensure the model is working behind our API.
Stop the web server and restart with gunicorn — bind 0.0.0.0:8080 wsgi:application -w 1
Now make another request, curl -X GET http://0.0.0.0:8080/predict
Perfect. Our model is predicting a label based on an inputted feature inside our API!
Now let’s get our API to accept input features from a request.
Add this to predict()
# parse input features from requestrequest_json = request.get_json()x = float(request_json['input'])
Now app.py looks like
import flaskfrom flask import Flask, jsonify, requestimport jsonimport pickleapp = Flask(__name__)def load_models():    file_name = "models/model_file.p"    with open(file_name, 'rb') as pickled:        data = pickle.load(pickled)        model = data['model']    return model@app.route('/predict', methods=['GET'])def predict():    # parse input features from request    request_json = request.get_json()    x = float(request_json['input'])     # load model    model = load_models()    prediction = model.predict([[x]])[0]    response = json.dumps({'response': prediction})    return response, 200if __name__ == '__main__':    application.run(debug=True)
Also add this to Procfile, which actually does have no extension.
web: gunicorn app:app --preload
Make another request. This time we’ve modified it slightly so our API knows it’s getting a JSON.
curl -X GET http://0.0.0.0:8080/predict -H "Content-Type: application/json" -d '{"input":"7"}'
Again, success! We have a working API with our model inside.
Add git to the app by running git init on the command line.
Then run nano .gitignore and add the following lines,
vir_env__pycache__/.DS_Store
Then press ctrl+x, y, then enter to save and close the file on mac. This will prevent pushing unnecessary files to Heroku.
Continue adding files to git by running git add . -A and git commit -m 'first commit' on the command line.
Create an account with Heroku. Then login to Heroku with heroku login on the command line, then create an app with heroku create flask-ml-api-123 . flask-ml-api-123 is the app name I picked. You’ll need to find a name that no one else has already taken.
Now push our app to heroku with git push heroku master.
While deploying you’ll see the URL given to your app from Heroku. This is where you’ll send your API request.
In my case it’s https://flask-ml-api-123.herokuapp.com/ .
Now make a request there
curl -X GET https://flask-ml-api-123.herokuapp.com/predict -H “Content-Type: application/json” -d '{"input”:”9"}'
Now we’re talking! We’ve trained a model, wrapped it in an API, and deployed it to Heroku. Obviously there are a lot more things we should add like authentication. But this should get you started!