Deploying a Simple Machine Learning Model in a Modern Web Application (Flask, Angular, Docker & Scikit-learn)

Daniel Elsner
7 min readFeb 10, 2018

--

While working at BMW’s Data Science department the last few months, I observed that well working and accurate machine learning (ML) models did not receive the expected (and deserved) attention. A primary reason is the way the models are presented: Jupyter Notebooks are certainly a great platform for developing and tuning models. However, as much as I enjoy working with Python and libraries such as Seaborn or Matplotlib, the notebooks make it difficult to show the work to others -especially non-technicians- to thereby create transparency.

Given my background in web development, I always aim to present my ML models through catchy, interactive web applications. As this approach was relatively successful in my recent work, I decided to create this post and a simple example of a web application. It can serve as a starting point for those who do not want to spend much time on CSS or HTML, but still want to present their work adequately. And as you might have heard at some point, Data Science is software and let’s face it, times where you presented your plain predict.py or predict.R are over.

The repository can be found here and basic knowledge of Flask, Angular 2+ and Docker is recommended. However, the idea is that this sample application abstracts the whole framework overload and demonstrates that it sometimes only requires a few lines of code to create a simple yet nice looking UI.

Nevertheless, if you are still interested go with these tutorials first:

The basis for this application is a simple ML model (more precisely a Support Vector Machine(SVM) Classifier) which will be trained on the popular Iris flower dataset. The result is a web application where you can set a model hyperparameter (C) and trigger the training of the model. The accuracy on the training set will be displayed in a linear gauge. Users are able to send their own iris flower object to the model and receive predicted probabilities (shown in a pie-chart) for the three iris classes: Setosa, Versicolour, and Virginica.

I will use Python with Flask for the backend, Scikit-Learn for the model development, Angular 4 and Typescript for the frontend, and Docker and Docker-Compose for the orchestration of containers.

Architecture Diagram
Final Application

Before we get started, let’s quickly set up the development environment. Ensure that you have Docker installed on your system. Go ahead and clone/fork the repository and navigate into the project directory. Next, run docker-compose up inside the project root directory. This will start a backend (default port 8081) and frontend (default port 4200) service in two docker containers and watch changes in project files. Start your browser and go to http://localhost:4200. You should see the application up and running, please use a modern browser not IE, it’s 2018.

If you want to write the following code yourself, go ahead and remove all the files from frontend/src/app/pages/home and remove the code inside backend/app.py. Note that this will break your running application, but we will add the code step-by-step in the next sections.

Now let’s get started.

Model Development

First, consider a simple ML model trained on the iris data set that then predicts probabilities for a new flower. This will be the model deployed in the application.

## backend/Simple Iris Model.ipynbfrom sklearn import svm
from sklearn import datasets
# read iris dataset
iris = datasets.load_iris()
X, y = iris.data, iris.target
# fit model
clf = svm.SVC(
C=1.0,
probability=True,
random_state=1)
clf.fit(X, y)
print('Training Accuracy: ', clf.score(X, y))print('Prediction results: ', clf.predict_proba([[5.2, 3.5, 2.4, 1.2]]))

Model Deployment, Training

Second, let’s create a simple Flask application and wrap the model training into an API endpoint. It is crucial to store the model for later predictions. A convenient way to do this is the sklearn.externals module which dumps the model into a .pkl-file.

## backend/app.pyfrom flask import Flask, jsonify, request
from sklearn import svm
from sklearn import datasets
from sklearn.externals import joblib
# declare constants
HOST = '0.0.0.0'
PORT = 8081
# initialize flask application
app = Flask(__name__)
@app.route('/api/train', methods=['POST'])
def train():
# get parameters from request
parameters = request.get_json()

# read iris data set
iris = datasets.load_iris()
X, y = iris.data, iris.target

# fit model
clf = svm.SVC(C=float(parameters['C']),
probability=True,
random_state=1)
clf.fit(X, y)
# persist model
joblib.dump(clf, 'model.pkl')
return jsonify({'accuracy': round(clf.score(X, y) * 100, 2)})if __name__ == '__main__':
# run web server
app.run(host=HOST,
debug=True, # automatic reloading enabled
port=PORT)

Now the training can be triggered by calling the endpoint http://localhost:8081/api/train with a HTTP POST request.

Third, we integrate the backend route for model training into our frontend. We create a types.ts file containing the required Typescript types.

## frontend/src/app/pages/home/types.tsexport class SVCParameters {
C: number = 2.0;
}

export class SVCResult {
accuracy: number;
}

Then, we write a simple service iris.service.ts consuming the HTTP endpoint from the backend.

## frontend/src/app/pages/home/iris.service.tsimport {Injectable} from '@angular/core';
import {Http} from "@angular/http";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/map';
import {
SVCParameters,
SVCResult
} from "./types";

const SERVER_URL: string = 'api/';

@Injectable()
export class IrisService {

constructor(private http: Http) {
}

public trainModel(svcParameters: SVCParameters): Observable<SVCResult> {
return this.http.post(`${SERVER_URL}train`, svcParameters).map((res) => res.json());
}
}

The service is injected into our component (i.e. page) home.component.ts, and is then used to send the C model hyperparameter for the SVM to the backend with the function trainModel().

## frontend/src/app/pages/home/home.component.tsimport {Component, OnInit} from '@angular/core';
import {IrisService} from "./iris.service";
import {
SVCParameters,
SVCResult
} from "./types";

@Component({
selector: 'home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

public svcParameters: SVCParameters = new SVCParameters();
public svcResult: SVCResult;

constructor(private irisService: IrisService) {
}

ngOnInit() {
}

public trainModel() {
this.irisService.trainModel(this.svcParameters).subscribe((svcResult) => {
this.svcResult = svcResult;
});
}
}

Last step for the training is to add the markup in the home.component.html file. I like the Angular Material, Flex-Layout and ngx-charts libraries, but feel free to use your favorite component/style library.

## frontend/src/app/pages/home/home.component.html<div fxLayoutAlign="center center" fxLayout="column">
<h1>Iris Model - Example</h1>

<div class="home__training">
<h3>Train Classification Model (Support Vector Machine)</h3>
<md-card fxLayoutAlign="center center" fxLayout="column">
<h4>Set Parameter for Model Training</h4>
<md-input-container>
<input
mdInput
name="C Parameter"
placeholder="C Parameter"
[(ngModel)]="svcParameters.C"
>
</md-input-container>
<button md-raised-button (click)="trainModel()">
Train model
</button>
</md-card>

<div *ngIf="svcResult"
fxLayoutAlign="center center"
fxLayout="column"
>
<h4>Training Accuracy</h4>
<ngx-charts-linear-gauge
[view]="[300, 100]"
[value]="svcResult.accuracy"
[min]="0"
[max]="100"
[units]="'% accuracy'"
>
</ngx-charts-linear-gauge>
</div>
</div>
</div>

Model Deployment, Prediction of Class Probabilities

Fourth, let’s add the prediction of unseen flowers. The python code is a bit different in the Flask endpoint because we have to load the serialized ML model first. Furthermore, we need to get the details about the iris which we want to classify from the payload of the HTTP POST request.

## backend/app.py...@app.route('/api/predict', methods=['POST'])
def predict():
# get iris object from request
X = request.get_json()
X = [[float(X['sepalLength']), float(X['sepalWidth']), float(X['petalLength']), float(X['petalWidth'])]]

# read model
clf = joblib.load('model.pkl')
probabilities = clf.predict_proba(X)

return jsonify([{'name': 'Iris-Setosa', 'value': round(probabilities[0, 0] * 100, 2)},
{'name': 'Iris-Versicolour', 'value': round(probabilities[0, 1] * 100, 2)},
{'name': 'Iris-Virginica', 'value': round(probabilities[0, 2] * 100, 2)}])
...

As before, we add the required types to types.ts

## frontend/src/app/pages/home/types.ts...export class Iris {
sepalLength: number = 5.0;
sepalWidth: number = 3.5;
petalLength: number = 2.5;
petalWidth: number = 1.2;
}

export class ProbabilityPrediction {
name: string;
value: number;
}
...

and a function in the iris.service.ts which consumes the backend endpoint.

## frontend/src/app/pages/home/iris.service.ts...public predictIris(iris: Iris): Observable<ProbabilityPrediction[]> {
return this.http.post(`${SERVER_URL}predict`, iris).map((res) => res.json());
}
...

Last, we implement four input fields in the home.component.html (one for each feature of an iris flower)

## frontend/src/app/pages/home/home.component.html...<div class="home__prediction"
fxLayoutAlign="center center"
fxLayout="column"
>
<h3>Predict Iris classes</h3>

<md-card fxLayoutAlign="center center" fxLayout="column">
<h4>Define Iris</h4>
<div fxLayout="row">
<md-input-container>
<input mdInput
name="Sepal width"
placeholder="Sepal width"
[(ngModel)]="iris.sepalWidth"
>
</md-input-container>
<md-input-container>
<input mdInput
name="Sepal length"
placeholder="Sepal length"
[(ngModel)]="iris.sepalLength"
>
</md-input-container>
</div>
<div fxLayout="row">
<md-input-container>
<input mdInput
name="Petal width"
placeholder="Petal width"
[(ngModel)]="iris.petalWidth"
>
</md-input-container>
<md-input-container>
<input mdInput
name="Petal length"
placeholder="Petal length"
[(ngModel)]="iris.petalLength"
>
</md-input-container>
</div>
<div>
<button md-raised-button (click)="predictIris()">Predict iris</button>
</div>
</md-card>

<div *ngIf="probabilityPredictions"
fxLayoutAlign="center center"
fxLayout="column"
class="home__chart-container"
>
<h4>Prediction Result</h4>
<ngx-charts-pie-chart
[results]="probabilityPredictions"
[scheme]="colorScheme"
[labels]="true"
[legend]="true"
>
</ngx-charts-pie-chart>
</div>
</div>
...

and bind their values to the respective variables in the home.component.ts. The predictIris() function is used to send the new iris flower object to the backend via the iris.service.ts. The prediction results are displayed in a pie-chart component from ngx-charts.

## frontend/src/app/pages/home/home.component.ts...public iris: Iris = new Iris();
public probabilityPredictions: ProbabilityPrediction[];
...public predictIris() {
this.irisService.predictIris(this.iris).subscribe((probabilityPredictions) => {
this.probabilityPredictions = probabilityPredictions;
});
}
...

This should leave you with the same result at http://localhost:4200 as at the start.

The repository provides further explanation on the setup with Docker. If you are interested in more advanced architectures, e.g. for asynchronous task handling and scheduling of workers, have a look at Airflow and Celery. If you are working behind a proxy (e.g. in a company) see this commit. For production use ng build —-prod for the bundling of the frontend and serve the static files from a NGINX or some other web server (example repo). I would also recommend to look into Kubernetes for orchestration and deployment in production (see this branch).

If you have questions or remarks, please do not hesitate to either contact me directly or comment below.

--

--