Skip to main content

TST, HongKong

Serving your SciKit Learn Model as a Prediction API

Github Repository

Prediction Pipeline

import matplotlib.pyplot as plt
import numpy as np
import os
import pandas as pd
import pickle
import scipy as sc

from sklearn.pipeline import make_pipeline
from sklearn.base import BaseEstimator, TransformerMixin
from skimage import io, color, transform, feature
# import test image
# test_img = io.imread('assets/lion.jpg', as_gray=True)
test_img = io.imread('assets/human.jpg', as_gray=True)

Image Resizing

# the model was trained with 80x80 px images => resize inputs accordingly
test_img_resized = (transform.resize(test_img, (80, 80)) * 255).astype(np.uint8)
plt.imshow(test_img_resized, cmap='gray')

ScikitImage Prediction Pipeline

Feature Extraction

# extract features with optimized hyper parameters
feature_vector = feature.hog(
test_img_resized,
orientations=11,
pixels_per_cell=(8, 8),
cells_per_block=(2, 2)
).reshape(1, -1)

# ValueError: Expected 2D array, got 1D array instead:
# Reshape your data either using array.reshape(-1, 1) if your data has a single feature
# or array.reshape(1, -1) if it contains a single sample.

Model Prediction

normalizer = pickle.load(open('model/animal_model_deployment_scaler.pkl', 'rb'))
model = pickle.load(open('model/animal_model_deployment.pkl', 'rb'))
model.get_params()
feature_vector_scaled = normalizer.transform(feature_vector)
model.predict(feature_vector_scaled)
# array(['human'], dtype='<U8')
decision_values = model.decision_function(feature_vector_scaled)
z_scores = sc.stats.zscore(decision_values.flatten())
probabilities = (sc.special.softmax(z_scores) * 100).round(2)

labels = model.classes_

probabilities_df = pd.DataFrame(probabilities, columns=['probability [%]'], index=labels)
probabilities_df.sort_values(by='probability [%]', ascending=False)[:5]

Top 5 Predictions:

probability [%]
human44.86
rabbit6.42
pigeon6.30
tiger5.64
eagle5.22
# https://stackoverflow.com/questions/52644035/how-to-show-a-pandas-dataframe-into-a-existing-flask-html-table
probabilities_dict = probabilities_df.sort_values(by='probability [%]', ascending=False)[:5].to_dict()
probabilities_dict.values()

# dict_values([{'human': 44.86, 'tiger': 6.42, 'rabbit': 6.3, 'monkey': 5.64, 'eagle': 5.22}])
plt.figure(figsize=(12,5))
plt.barh(labels, probabilities)
plt.ylabel('Target Classes')
plt.xlabel('Probability [%]')
plt.title('Prediction Probability')
plt.grid()
plt.savefig('assets/Scikit_Image_Model_Deployment_09.webp')

ScikitImage Prediction Pipeline

Building the Prediction Pipeline

def prediction_pipeline(img_path, normalizer, model):
img = io.imread(img_path, as_gray=True)
img_resized = (transform.resize(img, (80, 80)) * 255).astype(np.uint8)

feature_vector = feature.hog(
img_resized,
orientations=11,
pixels_per_cell=(8, 8),
cells_per_block=(2, 2)
).reshape(1, -1)

feature_vector_scaled = normalizer.transform(feature_vector)
model.predict(feature_vector_scaled)

decision_values = model.decision_function(feature_vector_scaled)
z_scores = sc.stats.zscore(decision_values.flatten())
probabilities = (sc.special.softmax(z_scores) * 100).round(2)

labels = model.classes_

probabilities_df = pd.DataFrame(probabilities, columns=['probability [%]'], index=labels)
top5predictions = probabilities_df.sort_values(
by='probability [%]', ascending=False
)[:5].to_dict().values()

return top5predictions
# test pipeline
prediction_pipeline('assets/human.jpg', normalizer, model)

# dict_values([{'human': 44.86, 'tiger': 6.42, 'rabbit': 6.3, 'monkey': 5.64, 'eagle': 5.22}])
results = prediction_pipeline('assets/human.jpg', normalizer, model)
list(list(results)[0].items())[0]

Docker Deployment

app.py

from flask import Flask, render_template, request, redirect, url_for
import os
import pickle
import numpy as np
import pandas as pd
import scipy as sc
from sklearn.base import BaseEstimator, TransformerMixin
from skimage import io, color, transform, feature

PORT = 3000

BASE_PATH = os.getcwd()
UPLOAD_PATH = os.path.join(BASE_PATH,'static/uploads/')
MODELS_PATH = os.path.join(BASE_PATH,'static/models/')

LIVE_MODEL = os.path.join(MODELS_PATH,'animal_model_deployment.pkl')
LIVE_SCALER = os.path.join(MODELS_PATH,'animal_model_deployment_scaler.pkl')

model = pickle.load(open(LIVE_MODEL,'rb'))
normalizer = pickle.load(open(LIVE_SCALER,'rb'))

app = Flask(__name__)



@app.errorhandler(404)
def error404(error):
error_title = "404 Not Found"
error_message = "The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist."
return render_template("error.html", title=error_title, message=error_message)

@app.errorhandler(405)
def error405(error):
error_title = "405 Method Not Allowed"
error_message = 'The request method is known by the server but is not supported by the target resource.'
return render_template("error.html", title=error_title, message=error_message)

@app.errorhandler(500)
def error500(error):
error_title = "500 Internal Server Error"
error_message='This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.'
return render_template("error.html", title=error_title, message=error_message)



@app.route('/', methods=['GET','POST'])
def index():
# upload file
if request.method == "POST":
upload_file = request.files['image']
extension = upload_file.filename.split('.')[-1]
if extension.lower() in ['png', 'jpg', 'jpeg']:
file_path = os.path.join(UPLOAD_PATH,upload_file.filename)
upload_file.save(file_path)
else:
print('ERROR :: File extension not allowed,', extension)
return render_template('upload.html', fileupload=False, extexception=True, extension=extension)
print('INFO :: File uploaded', upload_file.filename)
# run prediction
results = prediction_pipeline(file_path, normalizer, model)
img_width = calc_width(file_path)
print('INFO :: Prediction results', results)
return render_template(
'upload.html',
fileupload=True,
extexception=False,
image='/static/uploads/'+upload_file.filename,
data=list(list(results)[0].items()),
width=img_width
)
else:
return render_template('upload.html', fileupload=False, extexception=False)




def prediction_pipeline(img_path, normalizer, model):
img = io.imread(img_path, as_gray=True)
img_resized = (transform.resize(img, (80, 80)) * 255).astype(np.uint8)

feature_vector = feature.hog(
img_resized,
orientations=11,
pixels_per_cell=(8, 8),
cells_per_block=(2, 2)
).reshape(1, -1)

feature_vector_scaled = normalizer.transform(feature_vector)
model.predict(feature_vector_scaled)

decision_values = model.decision_function(feature_vector_scaled)
z_scores = sc.stats.zscore(decision_values.flatten())
probabilities = (sc.special.softmax(z_scores) * 100).round(2)

labels = model.classes_

probabilities_df = pd.DataFrame(probabilities, columns=['probability [%]'], index=labels)
top5predictions = probabilities_df.sort_values(
by='probability [%]', ascending=False
)[:5].to_dict().values()

return top5predictions


@app.route('/about/')
def about():
return render_template('about.html')


def calc_width(path):
img = io.imread(path)
height,width,_ = img.shape
aspect_ratio = width/height

max_height = 335
max_width = 360
optimal_width = max_height * aspect_ratio

if optimal_width <= max_width:
return optimal_width
else:
return max_width


if __name__ == 'main':
app.run(host="localhost", port=PORT, debug=True)

templates/index.html

<!DOCTYPE html>
<html>
<head>
<title>Image Classification</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="icon" href="/static/favicon.ico">
</head>
<body>
<!-- NAVBAR -->
<nav class="navbar bg-primary border-bottom border-bottom-dark navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<div class="container">
<a class="navbar-brand" href="/">
<img src="/static/IN-logo.svg" alt="INSTAR Image Classifier" width="150" height="50">
</a>
</div>
<!-- <a class="navbar-brand link-light" href="/">Frontend</a> -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link link-light" aria-current="page" href="/" >Home</a>
<a class="nav-link link-light" href="/about/">About</a>
</div>
</div>
</div>
</nav>
<!-- NAVBAR -->
<!-- UPLOAD -->
<div class="container mt-3">
<h4>Image Classification</h4>
</div>

{% block body %}

{% endblock %}
<!-- UPLOAD -->

<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/jquery-3.7.0.min.js"></script>
</body>
</html>

templates/upload.html

{% extends 'index.html' %}

{% block body %}

{% if extexception %}

<div class="container mt-2">
<div class="card text-light bg-danger mb-3">
<div class="card-header">Upload Failed!</div>
<div class="card-body">
<h5 class="card-title">File Extension Not Allowed: {{ extension }}</h5>
<p class="card-text">Upload image files with <span class="text-info-emphasis">jpg</span>, <span class="text-info-emphasis">png</span> or <span class="text-info-emphasis">jpeg</span> extension</p>
</div>
</div>
</div>

{% endif %}

<div class="container mt-4">
<div class="card text-center">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" id="upload-image" aria-current="true" href="#">Upload Image</a>
</li>
</ul>
</div>
<div class="card-body p-0">
<h5 class="card-title pt-3">Upload image files with <code>jpg</code>, <code>png</code> or <code>jpeg</code> extension</h5>
<form action="#" method="POST" enctype="multipart/form-data">
<p>
<input type="file" name="image" class="btn btn-light" requiered></input>
</p>
<p>
<input type="submit" value="submit" class="btn btn-primary"></input>
</p>
<div class="card-footer">
<p class="card-text">
<small class="text-body-secondary">Available class labels are:
<code>cow,duck,rabbit,monkey,sheep,cat,bear,human,wolf,chicken,eagle,elephant,mouse,pigeon,lion,deer,panda,tiger,dog</code></small>
</p>
</div>
</ul>
</form>
</div>
</div>
</div>

{% if fileupload %}

<div class="container mt-2">
<div class="card mb-3">
<div class="row g-0">
<div class="col-md-4">
<img src="{{ image }}" width="{{ width }}" height="335px" />
</div>
<div class="col-md-8 mt-2">
<div class="card-body">
<h5 class="card-title">Top5 prediction results for the uploaded image:</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Rank</th>
<th scope="col">Label</th>
<th scope="col">Confidence [%]</th>
</tr>
</thead>
<tbody>
{% for name, score in data %}
<tr>
<th scope="row">{{loop.index}}</th>
<td>{{name}}</td>
<td>{{score}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

{% endif %}

{% endblock %}

templates/error.html

{% extends 'index.html' %}

{% block body %}

<div class="container">
<div class="card bg-danger text-light mb-3">
<div class="card-header">ERROR</div>
<div class="card-body">
<h5 class="card-title">{{ title }}</h5>
<p class="card-text">{{ message }}</p>
</div>
</div>
</div>

{% endblock %}

Dockerfile

# base image to use
FROM python:3.11-slim-bookworm
# dir name inside the container used for your app
WORKDIR /opt/python_app
# copy all files into the work dir
COPY . .
# install python dependencies
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
# execute the app when container starts
EXPOSE 5000/tcp
# CMD [ "python3", "-m" , "flask", "--app", "app", "run", "--host=0.0.0.0"]
CMD ["flask", "--app", "app", "run", "--host=0.0.0.0"]