ML Zoomcamp 2023 – Deploying Machine Learning Models– Part 4

  1. Serving the churn model with Flask
    1. Wrapping the predict script into a Flask app
    2. Querying it with ‘requests’
    3. Preparing for production: gunicorn
    4. Running it on Windows with waitress

Serving the churn model with Flask

Now we want to serve the churn model with Flask.

Wrapping the predict script into a Flask app

First, we need to convert all the code from Jupyter Notebook into .py scripts. The following snippet displays the code for “train.py”. The prediction section is separated from the training part since that will be the responsibility of the web service.

import pickle
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.feature_extraction import DictVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

# Setting model parameters
C = 1.0
n_splits = 5

output_file = f'model_C={C}.bin'

# Data preparation
df = pd.read_csv('data-week-3.csv')

df.columns = df.columns.str.lower().str.replace(' ', '_')

categorical_columns = list(df.dtypes[df.dtypes == 'object'].index)

for c in categorical_columns:
    df[c] = df[c].str.lower().str.replace(' ', '_')

df.totalcharges = pd.to_numeric(df.totalcharges, errors='coerce')
df.totalcharges = df.totalcharges.fillna(0)

df.churn = (df.churn == 'yes').astype(int)

# Data splitting
df_full_train, df_test = train_test_split(df, test_size=0.2, random_state=1)

numerical = ['tenure', 'monthlycharges', 'totalcharges']

categorical = ['gender', 'seniorcitizen', 'partner', 'dependents',
       'phoneservice', 'multiplelines', 'internetservice',
       'onlinesecurity', 'onlinebackup', 'deviceprotection', 'techsupport',
       'streamingtv', 'streamingmovies', 'contract', 'paperlessbilling',
       'paymentmethod']

# Training
def train(df_train, y_train, C=1.0):
    dicts = df_train[categorical + numerical].to_dict(orient='records')

    dv = DictVectorizer(sparse=False)
    X_train = dv.fit_transform(dicts)

    model = LogisticRegression(C=C, max_iter=1000)
    model.fit(X_train, y_train)

    return dv, model

def predict(df, dv, model):
     dicts = df[categorical + numerical].to_dict(orient='records')

     X = dv.fit_transform(dicts)
     y_pred = model.predict_proba(X)[:,1]

     return y_pred

# Validation
print(f'doing validation with C={C}')

kfold = KFold(n_splits=n_splits, shuffle=True, random_state=1)  
    
scores = []

fold = 0

for train_idx, val_idx in kfold.split(df_full_train):
    df_train = df_full_train.iloc[train_idx]
    df_val = df_full_train.iloc[val_idx]

    y_train = df_train.churn.values
    y_val = df_val.churn.values

    dv, model = train(df_train, y_train, C=C)
    y_pred = predict(df_val, dv, model)

    auc = roc_auc_score(y_val, y_pred)
    scores.append(auc)

    print(f'auc on fold {fold} is {auc}')
    fold += 1

print('validation result:')
print('C=%s %.3f +- %.3f' % (C, np.mean(scores), np.std(scores)))
# Output: C=1.0 0.841 +- 0.008

# Train the final model
print('train the final model')

dv, model = train(df_full_train, df_full_train.churn.values, C=1.0)
y_pred = predict(df_test, dv, model)
y_test = df_test.churn.values

auc = roc_auc_score(y_test, y_pred)

print(f'auc={auc}')
# Output: 0.8572386167896259

# Saving the model with Pickle
with open(output_file, 'wb') as f_out:
    pickle.dump((dv, model), f_out)

print(f'the model is saved to {output_file}')

The following snippet covers to the prediction part (predict.py). There’s a key distinction this time: we won’t be using the GET method as we did in our previous simple sample from the last article. Instead, we’ll use the POST method since we need to send information to the web service.

import pickle
from flask import Flask
from flask import request
from flask import jsonify

model_file = 'model_C=1.0.bin'

with open(model_file, 'rb') as f_in:
    dv, model = pickle.load(f_in)

app = Flask('churn')

@app.route('/predict', methods=['POST'])
def predict():
    # json = Python dictionary
    customer = request.get_json()

    X = dv.transform([customer])
    model.predict_proba(X)
    y_pred = model.predict_proba(X)[0,1] 
    churn = y_pred >= 0.5

    result = {
        # the next line raises an error so we need to change it
        #'churn_probability': y_pred,
        'churn_probability': float(y_pred),
        # the next line raises an error so we need to change it
        #'churn': churn
        'churn': bool(churn)
    }

    return jsonify(result) 

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=9696)

This time we cannot test it in the browser, because browser sends an GET request.
Otherwise we’ll get an error:
127.0.0.1 – – [06/Oct/2023 17:23:04] “GET /predict HTTP/1.1” 405 –

Querying it with ‘requests’

To test our implementation, we use “predict-test.py,” which is implemented in the following snippet. In this script, we use the requests library to send our request to the web service. As previously mentioned, we use the POST method. The POST method of requests takes two arguments. ‘url’ points to the web service, and ‘json’ represents our customer information, which needs to be in JSON format. Depending on the result, the script will determine whether to send a promotion email or not.

import requests

url = 'http://localhost:9696/predict'

customer_id = 'xyz-123'
customer = {
    "gender": "female",
    "seniorcitizen": 0,
    "partner": "yes",
    "dependents": "no",
    "phoneservice": "no",
    "multiplelines": "no_phone_service",
    "internetservice": "dsl",
    "onlinesecurity": "no",
    "onlinebackup": "yes",
    "deviceprotection": "no",
    "techsupport": "no",
    "streamingtv": "no",
    "streamingmovies": "no",
    "contract": "month-to-month",
    "paperlessbilling": "yes",
    "paymentmethod": "electronic_check",
    "tenure": 1,
    "monthlycharges": 29.85,
    "totalcharges": 29.85
}

response = requests.post(url, json=customer).json()
print(response)

if response['churn'] == True:
    print('sending promo email to %s' % customer_id)
else:
    print('not sending promo email to %s' % customer_id)

Preparing for production: gunicorn

Flask is not recommended for use in production, but there are several alternatives, such as Gunicorn, which is suitable for Linux, Unix, and MacOS. You can also use Gunicorn in the Windows Subsystem for Linux (WSL). To use Gunicorn on the mentioned operating systems, it needs to be installed first with the command ‘pip install gunicorn‘. However, please note that Gunicorn is not compatible with Windows.

After installing Gunicorn, you can start the web service by running the command ‘gunicorn --bind 0.0.0.0:9696 predict:app‘. This command binds the web service to port 9696 on localhost and uses the ‘predict’ function from our app which we’ve implemented in ‘predict.py’.

Now, we can send a request to our web service using the command ‘python predict-test.py‘. The web service responds to this request, and the evaluation result looks like:

{'churn': True, 'churn_probability': 0.6363584152721401}
sending promo email to xyz-123

Running it on Windows with waitress

As mentioned before we cannot use gunicorn on Windows. But there is an alternative called waitress which is similar to gunicorn. Before the first use it also needs to be installed with ‘pip install waitress‘.

After installing Waitress, you can start the web service by running the command ‘waitress --listen=0.0.0.0:9696 predict:app‘. This command binds the web service to port 9696 on localhost and uses the ‘predict’ function from our app which we’ve implemented in ‘predict.py’.

Now, we can send the request to the web service using the command ‘python predict-test.py‘. The web service responds to this request, and the evaluation result looks like:

{'churn': True, 'churn_probability': 0.6363584152721401}
sending promo email to xyz-123

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.