Skip to content

Commit

Permalink
Dockerize the entier wrapping process for sklearn example
Browse files Browse the repository at this point in the history
- use multi-stage build and Alpine as a base
- use stripped-down model image
- provide Makefile for running docker build command
- declare all Python dependencies in one requirements.txt file
- copy sklearn_iris_deployment.json
  • Loading branch information
errordeveloper committed Jan 19, 2018
1 parent 3b168ad commit 0e0649c
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/models/sklearn_iris_docker/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.dockerignore
Dockerfile
Makefile
sklearn_iris_deployment.json
84 changes: 84 additions & 0 deletions examples/models/sklearn_iris_docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## Use alpine as build time and runtime image
FROM alpine:3.7 as build-alpine

## Install build dependencies
RUN apk add --update \
build-base \
freetype-dev \
gcc \
gfortran \
libc6-compat \
libffi-dev \
libpng-dev \
openblas-dev \
openssl-dev \
py2-pip \
python2 \
python2-dev\
wget \
&& true

## Symlink missing header, so we can compile numpy
RUN ln -s /usr/include/locale.h /usr/include/xlocale.h

## Copy package manager config to staging root tree
RUN mkdir -p /out/etc/apk && cp -r /etc/apk/* /out/etc/apk/
## Install runtime dependencies under staging root tree
RUN apk add --no-cache --initdb --root /out \
alpine-baselayout \
busybox \
ca-certificates \
freetype \
libc6-compat \
libffi \
libpng \
libstdc++ \
musl \
openblas \
openssl \
python2 \
&& true
## Remove package manager residuals
RUN rm -rf /out/etc/apk /out/lib/apk /out/var/cache

## Enter model source tree and install all Python depenendcies
COPY . /src
WORKDIR /src
## TODO this does take a while to build, maybe a good idea to
## put all related build dependencies into a separate public image
RUN pip install --requirement requirements.txt
## Train the model
RUN python train_iris.py

## Copy source code and Python dependencies to the saging root tree
RUN mkdir -p /out/src && cp -r /src/* /out/src/
RUN mkdir -p /out/usr/lib/python2.7/ && cp -r /usr/lib/python2.7/* /out/usr/lib/python2.7/

## Use Seldon Core wrapper image to wrap the model source code
FROM seldonio/core-python-wrapper:0.4 as build-wrapper

ARG MODEL_NAME
ARG IMAGE_VERSION
ARG IMAGE_REPO

## Copy staging diretory here
COPY --from=build-alpine /out /out
## Wrap the Python model
WORKDIR /wrappers/python
RUN python wrap_model.py /out/src $MODEL_NAME $IMAGE_VERSION $IMAGE_REPO --force

## Copy wrapped model source code into staging tree and cleanup what is not neccessary at runtime
RUN mkdir -p /out/microservice && cp -r /out/src/build/* /out/microservice/ && rm -rf /out/src
WORKDIR /out/microservice
RUN rm -f Dockerfile Makefile requirements*.txt build_image.sh push_image.sh
## TODO dockerfile doesn't support build argument interpolation in array notation for ENTRYPOINT & CMD
## to get rid of `/bin/sh` wrapper, it'd help to make $MODEL_NAME an environment variable and let the
## Python script pick it up
RUN printf '#!/bin/sh\nexec python microservice.py %s REST --service-type MODEL --persistence 0' $MODEL_NAME > microservice.sh && chmod +x microservice.sh

## Copy staging root tree onto an empty image
FROM scratch
COPY --from=build-wrapper /out /
WORKDIR /microservice
EXPOSE 5000
ENTRYPOINT ["/microservice/microservice.sh"]
9 changes: 9 additions & 0 deletions examples/models/sklearn_iris_docker/IrisClassifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sklearn.externals import joblib

class IrisClassifier(object):

def __init__(self):
self.model = joblib.load('IrisClassifier.sav')

def predict(self,X,features_names):
return self.model.predict_proba(X)
11 changes: 11 additions & 0 deletions examples/models/sklearn_iris_docker/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
IMAGE_REPO?=seldonio
IMAGE_NAME?=irisclassifier
IMAGE_VERSION?=0.1
MODEL_NAME?=IrisClassifier

container_image:
docker build \
--build-arg IMAGE_REPO=$(IMAGE_REPO) \
--build-arg IMAGE_VERSION=$(IMAGE_VERSION) \
--build-arg MODEL_NAME=$(MODEL_NAME) \
--tag $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_VERSION) .
9 changes: 9 additions & 0 deletions examples/models/sklearn_iris_docker/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
numpy==1.11.2
pandas==0.18.1
grpc==0.3.post19
grpcio==1.8.4
Flask==0.11.1
futures
redis==2.10.5
scipy==0.18.1
scikit-learn==0.19.0
53 changes: 53 additions & 0 deletions examples/models/sklearn_iris_docker/sklearn_iris_deployment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"apiVersion": "machinelearning.seldon.io/v1alpha1",
"kind": "SeldonDeployment",
"metadata": {
"labels": {
"app": "seldon"
},
"name": "seldon-deployment-example"
},
"spec": {
"annotations": {
"project_name": "Iris classification",
"deployment_version": "0.1"
},
"name": "sklearn-iris-deployment",
"oauth_key": "oauth-key",
"oauth_secret": "oauth-secret",
"predictors": [
{
"componentSpec": {
"spec": {
"containers": [
{
"image": "seldonio/irisclassifier:0.1",
"imagePullPolicy": "IfNotPresent",
"name": "sklearn-iris-classifier",
"resources": {
"requests": {
"memory": "1Mi"
}
}
}
],
"terminationGracePeriodSeconds": 20
}
},
"graph": {
"children": [],
"name": "sklearn-iris-classifier",
"endpoint": {
"type" : "REST"
},
"type": "MODEL"
},
"name": "sklearn-iris-predictor",
"replicas": 1,
"annotations": {
"predictor_version" : "0.1"
}
}
]
}
}
25 changes: 25 additions & 0 deletions examples/models/sklearn_iris_docker/train_iris.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import numpy as np
import os
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.externals import joblib
from sklearn import datasets

def main():
clf = LogisticRegression()
p = Pipeline([('clf', clf)])
print 'Training model...'
p.fit(X, y)
print 'Model trained!'

filename_p = 'IrisClassifier.sav'
print 'Saving model in %s' % filename_p
joblib.dump(p, filename_p)
print 'Model saved!'

if __name__ == "__main__":
print 'Loading iris data set...'
iris = datasets.load_iris()
X, y = iris.data, iris.target
print 'Dataset loaded!'
main()

0 comments on commit 0e0649c

Please sign in to comment.