This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Wallaroo Tutorials

Wallaroo Tutorials

Welcome to Wallaroo! Whether you’re using our free Community Edition or the Enterprise Edition, these Quick Start Guides are made to help learn how to deploy your ML Models with Wallaroo. Each of these guides includes a Jupyter Notebook with the documentation, code, and sample data that you can download and run wherever you have Wallaroo installed.

1 - Setting Up Wallaroo for Sample Pipelines and Models

How to get Wallaroo running for your sample models

Welcome to Wallaroo! We’re excited to help you get your ML models deployed as quickly as possible. These Quick Start Guides are meant to show simple but thorough steps in creating new pipelines and models. For full guides on using Wallaroo, see the Wallaroo SDK and the Wallaroo Operations Guide.

This first guide is just how to get Wallaroo ready for your first models and pipelines.

Prerequisites

This guide assumes that you’ve installed Wallaroo in a cluster cloud Kubernetes cluster.

Basic Wallaroo Configuration

The following will install the Wallaroo Quick Start samples into your Wallaroo Jupyter Hub environment.

  1. From the Wallaroo Quick Start Samples repository, visit the Releases page. As of the 2023.1 release, each tutorial can be downloaded as a separate .zip file instead of downloading the entire release.

  2. Download the release that matches your version of Wallaroo, Either a .zip or .tar.gz file can be downloaded. The example below uses the .zip file.

  3. Access your Jupyter Hub from your Wallaroo environment.

  4. Either drag and drop or use the Upload feature to upload the quick start guide .zip file.

  5. From the launcher, select Terminal.

  6. Unzip the files with unzip {ZIP FILE NAME}. For example, if the most recent release is Wallaroo_Tutorials.zip, the command would be:

    unzip Wallaroo_Tutorials.zip
    
  7. Exit the terminal shell when complete.

The quick start samples along with models and data will be ready for use in the unzipped folder.

2 - Model Cookbooks

Sample model deployment tutorials for Wallaroo

Recipes for deploying common models in Wallaroo.

2.1 - Aloha Quick Tutorial

The Aloha Quick Start Guide demonstrates how to use Wallaroo to determine malicious web sites from their URL.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Aloha Demo

In this notebook we will walk through a simple pipeline deployment to inference on a model. For this example we will be using an open source model that uses an Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution.

For our example, we will perform the following:

  • Create a workspace for our work.
  • Upload the Aloha model.
  • Create a pipeline that can ingest our submitted data, submit it to the model, and export the results
  • Run a sample inference through our pipeline by loading a file
  • Run a sample inference through our pipeline’s URL and store the results in a file.

All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.

This is accomplished using the wallaroo.Client() command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.

import wallaroo
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)

# to display dataframe tables
from IPython.display import display
# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Create the Workspace

We will create a workspace to work in and call it the “alohaworkspace”, then set it as current workspace environment. We’ll also create our pipeline in advance as alohapipeline. The model name and the model file will be specified for use in later steps.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

import string
import random

# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'{prefix}alohaworkspace'
pipeline_name = f'{prefix}alohapipeline'
model_name = f'{prefix}alohamodel'
model_file_name = './alohacnnlstm.zip'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
wl.list_workspaces()
Name Created At Users Models Pipelines
john.hummel@wallaroo.ai - Default Workspace 2023-02-27 17:18:34 ['john.hummel@wallaroo.ai'] 0 0
jnhcccfraudworkspace 2023-02-27 17:19:26 ['john.hummel@wallaroo.ai'] 1 1
beticcfraudworkspace 2023-02-27 17:23:26 ['john.hummel@wallaroo.ai'] 1 1
uuzmhotswapworkspace 2023-02-27 17:28:00 ['john.hummel@wallaroo.ai'] 2 1
ggwzhotswapworkspace 2023-02-27 17:33:52 ['john.hummel@wallaroo.ai'] 2 1
azwsedgeworkspaceexample 2023-02-27 17:37:33 ['john.hummel@wallaroo.ai'] 1 1
mlbaedgeworkspaceexample 2023-02-27 17:40:11 ['john.hummel@wallaroo.ai'] 1 1
urldemoworkspace 2023-02-27 17:55:11 ['john.hummel@wallaroo.ai'] 1 1
efxvtagtestworkspace 2023-02-27 18:02:16 ['john.hummel@wallaroo.ai'] 1 1
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

aloha_pipeline = get_pipeline(pipeline_name)
aloha_pipeline
name uxelalohapipeline
created 2023-02-27 18:07:39.988279+00:00
last_updated 2023-02-27 18:07:39.988279+00:00
deployed (none)
tags
versions c7a45891-9797-4522-8ef9-fbd153bfa6c4
steps

We can verify the workspace is created the current default workspace with the get_current_workspace() command.

wl.get_current_workspace()
{'name': 'uxelalohaworkspace', 'id': 16, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T18:07:39.250191+00:00', 'models': [], 'pipelines': [{'name': 'uxelalohapipeline', 'create_time': datetime.datetime(2023, 2, 27, 18, 7, 39, 988279, tzinfo=tzutc()), 'definition': '[]'}]}

Upload the Models

Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow format.

model = wl.upload_model(model_name, model_file_name).configure("tensorflow")

Deploy a model

Now that we have a model that we want to use we will create a deployment for it.

We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.

To do this, we’ll create our pipeline that can ingest the data, pass the data to our Aloha model, and give us a final output. We’ll call our pipeline aloha-test-demo, then deploy it so it’s ready to receive data. The deployment process usually takes about 45 seconds.

  • Note: If you receive an error that the pipeline could not be deployed because there are not enough resources, undeploy any other pipelines and deploy this one again. This command can quickly undeploy all pipelines to regain resources. We recommend not running this command in a production environment since it will cancel any running pipelines:
for p in wl.list_pipelines(): p.undeploy()
aloha_pipeline.add_model_step(model)
name uxelalohapipeline
created 2023-02-27 18:07:39.988279+00:00
last_updated 2023-02-27 18:07:39.988279+00:00
deployed (none)
tags
versions c7a45891-9797-4522-8ef9-fbd153bfa6c4
steps
aloha_pipeline.deploy()
name uxelalohapipeline
created 2023-02-27 18:07:39.988279+00:00
last_updated 2023-02-27 18:07:45.684639+00:00
deployed True
tags
versions 178f6fd4-ee38-4931-adb1-6a4b48f6a473, c7a45891-9797-4522-8ef9-fbd153bfa6c4
steps uxelalohamodel

We can verify that the pipeline is running and list what models are associated with it.

aloha_pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.41',
   'name': 'engine-74fb5d899d-q885n',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'uxelalohapipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'uxelalohamodel',
      'version': '3f354fb2-4220-4873-9418-6debc06ada57',
      'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.2.16',
   'name': 'engine-lb-ddd995646-j5nr2',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Interferences

Infer 1 row

Now that the pipeline is deployed and our Aloha model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file command to load a single encoded URL into the inference engine and print the results back out.

The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 0.

if arrowEnabled is True:
    result = aloha_pipeline.infer_from_file('./data/data_1.df.json')
else:
    result = aloha_pipeline.infer_from_file("./data/data_1.json")
display(result)
time in.text_input out.main check_failures
0 2023-03-03 22:16:48.954 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 16, 32, 23, 29, 32, 30, 19, 26, 17] [0.997564] 0

Batch Inference

Now that our smoke test is successful, let’s really give it some data. We have two inference files we can use:

  • data_1k.json: Contains 10,000 inferences
  • data_25k.json: Contains 25,000 inferences

We’ll pipe the data_25k.json file through the aloha_pipeline deployment URL, and place the results in a file named response.txt. We’ll also display the time this takes. Note that for larger batches of 50,000 inferences or more can be difficult to view in Jupyter Hub because of its size.

  • IMPORTANT NOTE: The _deployment._url() method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection, _deployment._url() returns an external URL. External URL connections requires the authentication be included in the HTTP request, and that Model Endpoints Guide external endpoints are enabled in the Wallaroo configuration options.
inference_url = aloha_pipeline._deployment._url()
inference_url
'https://doc-test.api.example.com/v1/api/pipelines/infer/uxelalohapipeline-13'
connection =wl.mlops().__dict__
token = connection['token']
token
'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrTzQ2VjhoQWZDZTBjWU1ETkZobEZWS25HSC1HZy1xc1JkSlhwTTNQYjBJIn0.eyJleHAiOjE2Nzc1MjEyOTMsImlhdCI6MTY3NzUyMTIzMywiYXV0aF90aW1lIjoxNjc3NTE4MzEyLCJqdGkiOiI4YTNjODgwOC04MDY5LTQxOTItYTIyYi1iYzZlNWQ3ZGQ5YjUiLCJpc3MiOiJodHRwczovL2RvYy10ZXN0LmtleWNsb2FrLndhbGxhcm9vY29tbXVuaXR5Lm5pbmphL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6WyJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6IjQzNWRhOTA1LTMxZTItNGU3NC1iNDIzLTQ1YzM4ZWRiNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6InNkay1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYWNlMWEzMGQtNjZiYy00NGQ5LWJkMGEtYzYyMzc0NzhmZGFhIiwiYWNyIjoiMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsibWFuYWdlLXVzZXJzIiwidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiJhY2UxYTMwZC02NmJjLTQ0ZDktYmQwYS1jNjIzNzQ3OGZkYWEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtdXNlci1pZCI6IjQzNWRhOTA1LTMxZTItNGU3NC1iNDIzLTQ1YzM4ZWRiNTg4OSIsIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInVzZXIiXSwieC1oYXN1cmEtdXNlci1ncm91cHMiOiJ7fSJ9LCJuYW1lIjoiSm9obiBIYW5zYXJpY2siLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLmh1bW1lbEB3YWxsYXJvby5haSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJIYW5zYXJpY2siLCJlbWFpbCI6ImpvaG4uaHVtbWVsQHdhbGxhcm9vLmFpIn0.I3cIyxGf9z-N8ZnuiscyWuT-mV80LJOqXYzARL2EM0JRY_lxqfMr_PO_toOgJCfXDnDI5dTqaltes7kBmrTbyynIAKTDvY566RaUJGR2u0l3wjFm6ImJy6Eu78ck7q0bCLKOJkDNSqBPwDJv9b71uW816GRTYdGYUpmtUULiLYH8y3RBGf3odhIuGeWaTwa3PxG1Affq7rNqGG5LWYHvoRoN4-4eAtu3L5jdfC2wmc2MRh3MNK-UPMx3Fiz3r4GoTiSpsNyH6vmLFNUq1Rd-dKTDWu8UNtOihB-tOqJkmcMr2YINgh5PMKtKEpXMIa2kTNvRpNOtfsik0ZagUMxA9g'
if arrowEnabled is True:
    dataFile="./data/data_25k.df.json"
    contentType="application/json; format=pandas-records"
else:
    dataFile="./data/data_25k.json"
    contentType="application/json"
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data @{dataFile} > curl_response.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 34.4M  100 16.3M  100 18.0M  1161k  1278k  0:00:14  0:00:14 --:--:-- 4077k

Undeploy Pipeline

When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks. Note that if the deployment variable is unchanged aloha_pipeline.deploy() will restart the inference engine in the same configuration as before.

aloha_pipeline.undeploy()
name uxelalohapipeline
created 2023-02-27 18:07:39.988279+00:00
last_updated 2023-02-27 18:07:45.684639+00:00
deployed False
tags
versions 178f6fd4-ee38-4931-adb1-6a4b48f6a473, c7a45891-9797-4522-8ef9-fbd153bfa6c4
steps uxelalohamodel

2.2 - Computer Vision Tutorials

How to use Wallaroo with computer vision models to detect objects in images.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Step 00: Introduction and Setup

This tutorial demonstrates how to use the Wallaroo to detect objects in images through the following models:

  • rnn mobilenet: A single stage object detector that performs fast inferences. Mobilenet is typically good at identifying objects at a distance.
  • resnet50: A dual stage object detector with slower inferencing but but is able to detect objects that are closer to each other.

This tutorial series will demonstrate the following:

  • How to deploy a Wallaroo pipeline with trained rnn mobilenet model and perform sample inferences to detect objects in pictures, then display those objects.
  • How to deploy a Wallaroo pipeline with a trained resnet50 model and perform sample inferences to detect objects in pictures, then display those objects.
  • Use the Wallaroo feature shadow deploy to have both models perform inferences, then select the inference result with the higher confidence and show the objects detected.

This tutorial assumes that users have installed the Wallaroo SDK or are running these tutorials from within their Wallaroo instance’s JupyterHub service.

This demonstration should be run within a Wallaroo JupyterHub instance for best results.

Prerequisites

The included OpenCV class is included in this demonstration as CVDemoUtils.py, and requires the following dependencies:

  • ffmpeg
  • libsm
  • libxext

Internal JupyterHub Service

To install these dependencies in the Wallaroo JupyterHub service, use the following commands from a terminal shell via the following procedure:

  1. Launch the JupyterHub Service within the Wallaroo install.

  2. Select File->New->Terminal.

  3. Enter the following:

    sudo apt-get update
    
    sudo apt-get install ffmpeg libsm6 libxext6  -y
    

External SDK Users

For users using the Wallaroo SDK to connect with a remote Wallaroo instance, the following commands will install the required dependancies:

For Linux users, this can be installed with:

sudo apt-get update
sudo apt-get install ffmpeg libsm6 libxext6  -y

MacOS users can prepare their environments using a package manager such as Brew with the following:

brew install ffmpeg libsm libxext

Libraries and Dependencies

  1. This repository may use large file sizes for the models. If necessary, install Git Large File Storage (LFS) or use the Wallaroo Tutorials Releases to download a .zip file of the most recent computer vision tutorial that includes the models.
  2. Import the following Python libraries into your environment:
    1. torch
    2. wallaroo
    3. torchvision
    4. opencv-python
    5. onnx
    6. onnxruntime
    7. imutils
    8. pytz
    9. ipywidgets

These can be installed by running the command below in the Wallaroo JupyterHub service. Note the use of pip install torch --no-cache-dir for low memory environments.

!pip install torchvision
!pip install torch --no-cache-dir
!pip install opencv-python
!pip install onnx
!pip install onnxruntime
!pip install imutils
!pip install pytz
!pip install ipywidgets

The rest of the tutorials will rely on these libraries and applications, so finish their installation before running the tutorials in this series.

Models for Wallaroo Computer Vision Tutorials

In order for the wallaroo tutorial notebooks to run properly, models must be downloaded to the models directory. As they are too large for Git to contain without extra steps, this tutorial and all models are available as a separate download. These are downloaded via the following procedure:

  1. Go to https://github.com/WallarooLabs/Wallaroo_Tutorials/releases.
  2. Select the most recent release.
  3. Download the file computer_vision.zip.

This contains the entire tutorial, plus the model files. The most current version of this link is there:

https://github.com/WallarooLabs/Wallaroo_Tutorials/releases/download/1.27-2022.4-cv4/computer_vision.zip

2.2.1 - Step 01: Detecting Objects Using mobilenet

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Step 01: Detecting Objects Using mobilenet

The following tutorial demonstrates how to use a trained mobilenet model deployed in Wallaroo to detect objects. This process will use the following steps:

  1. Create a Wallaroo workspace and pipeline.
  2. Upload a trained mobilenet ML model and add it as a pipeline step.
  3. Deploy the pipeline.
  4. Perform an inference on a sample image.
  5. Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
  6. Review our results.

Steps

Import Libraries

The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.

import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo

Now we connect to the Wallaroo instance. If you are connecting from a remote connection, set the wallarooPrefix and wallarooSuffix and use them to connect. If the connection is from within the Wallaroo instance cluster, then just wl = wallaroo.Client() can be used.

# Login through local service

# wl = wallaroo.Client()

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR SUFFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for DataFrame and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Set Variables

The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model.

workspace_name = 'mobilenetworkspacetest'
pipeline_name = 'mobilenetpipeline'
model_name = 'mobilenet'
model_file_name = 'models/mobilenet.pt.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Create Workspace

The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.

workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()
{'name': 'mobilenetworkspacetest', 'id': 9, 'archived': False, 'created_by': 'ca7d7043-8e94-42d5-9f3a-8f55c2e42814', 'created_at': '2023-03-02T19:21:36.309503+00:00', 'models': [{'name': 'mobilenet', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 3, 2, 19, 21, 50, 515472, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 3, 2, 19, 21, 50, 515472, tzinfo=tzutc())}], 'pipelines': [{'name': 'mobilenetpipeline', 'create_time': datetime.datetime(2023, 3, 2, 19, 21, 51, 863552, tzinfo=tzutc()), 'definition': '[]'}]}

Create Pipeline and Upload Model

We will now create or connect to an existing pipeline as named in the variables above.

pipeline = get_pipeline(pipeline_name)
mobilenet_model = wl.upload_model(model_name, model_file_name)

Deploy Pipeline

With the model uploaded, we can add it is as a step in the pipeline, then deploy it. Once deployed, resources from the Wallaroo instance will be reserved and the pipeline will be ready to use the model to perform inference requests.

pipeline.add_model_step(mobilenet_model)

pipeline.deploy()
name mobilenetpipeline
created 2023-03-02 19:21:51.863552+00:00
last_updated 2023-03-02 19:32:02.206502+00:00
deployed True
tags
versions 6682b775-3c04-4071-b643-8d52b3c06e56, baf226fc-bc5e-4c52-9962-d7b87c987ad3, 48280b55-1285-41e3-a1c6-3fbe05ee4d3a
steps mobilenet

Prepare input image

Next we will load a sample image and resize it to the width and height required for the object detector. Once complete, it the image will be converted to a numpy ndim array and added to a dictionary.


# The size the image will be resized to
width = 640
height = 480

# Only objects that have a confidence > confidence_target will be displayed on the image
cvDemo = CVDemo()

imagePath = 'data/images/current/input/example/dairy_bottles.png'

# The image width and height needs to be set to what the model was trained for.  In this case 640x480.
tensor, resizedImage = cvDemo.loadImageAndResize(imagePath, width, height)

# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()

#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor": npArray.tolist()}
# test turning this into a dataframe

dataframedata = pd.DataFrame.from_records(dictData)
#display(dataframedata)

Run Inference

With that done, we can have the model detect the objects on the image by running an inference through the pipeline, and storing the results for the next step.

startTime = time.time()
infResults = pipeline.infer(dictData)
endTime = time.time()

if arrowEnabled is True:
    results = infResults[0]
else:
    results = infResults[0].raw

Draw the Inference Results

With our inference results, we can take them and use the Wallaroo CVDemo class and draw them onto the original image. The bounding boxes and the confidence value will only be drawn on images where the model returned a 90% confidence rate in the object’s identity.

df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None  # default='warn'
pd.options.display.float_format = '{:.2%}'.format

# Points to where all the inference results are
outputs = results['outputs']
boxes = outputs[0]

# reshape this to an array of bounding box coordinates converted to ints
boxList = boxes['Float']['data']
boxA = np.array(boxList)
boxes = boxA.reshape(-1, 4)
boxes = boxes.astype(int)

df[['x', 'y','width','height']] = pd.DataFrame(boxes)

classes = outputs[1]['Int64']['data']
confidences = outputs[2]['Float']['data']

infResults = {
    'model_name' : model_name,
    'pipeline_name' : pipeline_name,
    'width': width,
    'height': height,
    'image' : resizedImage,
    'boxes' : boxes,
    'classes' : classes,
    'confidences' : confidences,
    'confidence-target' : 0.90,
    'inference-time': (endTime-startTime),
    'onnx-time' : int(results['elapsed']) / 1e+9,                
    'color':(255,0,0)
}

image = cvDemo.drawAndDisplayDetectedObjectsWithClassification(infResults)
png

Extract the Inference Information

To show what is going on in the background, we’ll extract the inference results create a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.

idx = 0 
for idx in range(0,len(classes)):
    df['classification'][idx] = cvDemo.CLASSES[classes[idx]] # Classes contains the 80 different COCO classificaitons
    df['confidence'][idx] = confidences[idx]
df
classification confidence x y width height
0 bottle 98.65% 0 210 85 479
1 bottle 90.12% 72 197 151 468
2 bottle 60.78% 211 184 277 420
3 bottle 59.22% 143 203 216 448
4 refrigerator 53.73% 13 41 640 480
5 bottle 45.13% 106 206 159 463
6 bottle 43.73% 278 1 321 93
7 bottle 43.09% 462 104 510 224
8 bottle 40.85% 310 1 352 94
9 bottle 39.19% 528 268 636 475
10 bottle 35.76% 220 0 258 90
11 bottle 31.81% 552 96 600 233
12 bottle 26.45% 349 0 404 98
13 bottle 23.06% 450 264 619 472
14 bottle 20.48% 261 193 307 408
15 bottle 17.46% 509 101 544 235
16 bottle 17.31% 592 100 633 239
17 bottle 16.00% 475 297 551 468
18 bottle 14.91% 368 163 423 362
19 book 13.66% 120 0 175 81
20 book 13.32% 72 0 143 85
21 bottle 12.22% 271 200 305 274
22 book 12.13% 161 0 213 85
23 bottle 11.96% 162 0 214 83
24 bottle 11.53% 310 190 367 397
25 bottle 9.62% 396 166 441 360
26 cake 8.65% 439 256 640 473
27 bottle 7.84% 544 375 636 472
28 vase 7.23% 272 2 306 96
29 bottle 6.28% 453 303 524 463
30 bottle 5.28% 609 94 635 211

Undeploy the Pipeline

With the inference complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.

pipeline.undeploy()
name mobilenetpipeline
created 2023-03-02 19:21:51.863552+00:00
last_updated 2023-03-02 19:32:02.206502+00:00
deployed False
tags
versions 6682b775-3c04-4071-b643-8d52b3c06e56, baf226fc-bc5e-4c52-9962-d7b87c987ad3, 48280b55-1285-41e3-a1c6-3fbe05ee4d3a
steps mobilenet

2.2.2 - Step 02: Detecting Objects Using resnet50

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Step 02: Detecting Objects Using resnet50

The following tutorial demonstrates how to use a trained mobilenet model deployed in Wallaroo to detect objects. This process will use the following steps:

  1. Create a Wallaroo workspace and pipeline.
  2. Upload a trained resnet50 ML model and add it as a pipeline step.
  3. Deploy the pipeline.
  4. Perform an inference on a sample image.
  5. Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
  6. Review our results.

Steps

Import Libraries

The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.

import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo

Arrow Support

As of the 2023.1 release, Wallaroo provides support for DataFrame and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Connect to Wallaroo

Now we connect to the Wallaroo instance. If you are connecting from a remote connection, set the wallarooPrefix and wallarooSuffix and use them to connect. If the connection is from within the Wallaroo instance cluster, then just wl = wallaroo.Client() can be used.

# Login through local service

# wl = wallaroo.Client()

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR SUFFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                auth_type="sso")

Set Variables

The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model. This example has both the resnet model, and a post process script.

workspace_name = 'resnetworkspacetest'
pipeline_name = 'resnetnetpipelinetest'
model_name = 'resnet50'
model_file_name = 'models/frcnn-resnet.pt.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Create Workspace

The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.

workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()

Create Pipeline and Upload Model

We will now create or connect to an existing pipeline as named in the variables above.

pipeline = get_pipeline(pipeline_name)

resnet_model = wl.upload_model(model_name, model_file_name)

Deploy Pipeline

With the model uploaded, we can add it is as a step in the pipeline, then deploy it. Once deployed, resources from the Wallaroo instance will be reserved and the pipeline will be ready to use the model to perform inference requests.

pipeline.add_model_step(resnet_model)

pipeline.deploy()
name resnetnetpipelinetest
created 2023-03-02 19:35:32.620147+00:00
last_updated 2023-03-02 19:36:04.824928+00:00
deployed True
tags
versions 3d79d2d8-369e-4781-b796-d8c0c7144736, 9e290e9a-a735-499e-b5d2-e9e6c4d7fe14
steps resnet50

Test the pipeline by running inference on a sample image

Prepare input image

Next we will load a sample image and resize it to the width and height required for the object detector.

We will convert the image to a numpy ndim array and add it do a dictionary

#The size the image will be resized to
width = 640
height = 480

cvDemo = CVDemo()

imagePath = 'data/images/current/input/example/dairy_bottles.png'

# The image width and height needs to be set to what the model was trained for.  In this case 640x480.
tensor, resizedImage = cvDemo.loadImageAndResize(imagePath, width, height)

# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()

#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor": npArray.tolist()}

Run Inference

With that done, we can have the model detect the objects on the image by running an inference through the pipeline, and storing the results for the next step.

IMPORTANT NOTE: If necessary, add timeout=60 to the infer method if more time is needed to upload the data file for the inference request.

startTime = time.time()
infResults = pipeline.infer(dictData)
endTime = time.time()

if arrowEnabled is True:
    results = infResults[0]
else:
    results = infResults[0].raw

Draw the Inference Results

With our inference results, we can take them and use the Wallaroo CVDemo class and draw them onto the original image. The bounding boxes and the confidence value will only be drawn on images where the model returned a 50% confidence rate in the object’s identity.

df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None  # default='warn'
pd.options.display.float_format = '{:.2%}'.format

# Points to where all the inference results are
outputs = results['outputs']

boxes = outputs[0]

# reshape this to an array of bounding box coordinates converted to ints
boxList = boxes['Float']['data']
boxA = np.array(boxList)
boxes = boxA.reshape(-1, 4)
boxes = boxes.astype(int)

df[['x', 'y','width','height']] = pd.DataFrame(boxes)

classes = outputs[1]['Int64']['data']
confidences = outputs[2]['Float']['data']

infResults = {
    'model_name' : model_name,
    'pipeline_name' : pipeline_name,
    'width': width,
    'height': height,
    'image' : resizedImage,
    'boxes' : boxes,
    'classes' : classes,
    'confidences' : confidences,
    'confidence-target' : 0.50,
    'inference-time': (endTime-startTime),
    'onnx-time' : int(results['elapsed']) / 1e+9,                
    'color':(255,0,0)
}

cvDemo.drawAndDisplayDetectedObjectsWithClassification(infResults)
png

Extract the Inference Information

To show what is going on in the background, we’ll extract the inference results create a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.

idx = 0 
for idx in range(0,len(classes)):
    cocoClasses = cvDemo.getCocoClasses()
    df['classification'][idx] = cocoClasses[classes[idx]] # Classes contains the 80 different COCO classificaitons
    df['confidence'][idx] = confidences[idx]
df
classification confidence x y width height
0 bottle 99.65% 2 193 76 475
1 bottle 98.83% 610 98 639 232
2 bottle 97.00% 544 98 581 230
3 bottle 96.96% 454 113 484 210
4 bottle 96.48% 502 331 551 476
... ... ... ... ... ... ...
95 bottle 5.72% 556 287 580 322
96 refrigerator 5.66% 80 161 638 480
97 bottle 5.60% 455 334 480 349
98 bottle 5.46% 613 267 635 375
99 bottle 5.37% 345 2 395 99

100 rows × 6 columns

Undeploy the Pipeline

With the inference complete, we can undeploy the pipeline and return the resources back to the Wallaroo instance.

pipeline.undeploy()    
name resnetnetpipelinetest
created 2023-03-02 19:35:32.620147+00:00
last_updated 2023-03-02 19:36:04.824928+00:00
deployed False
tags
versions 3d79d2d8-369e-4781-b796-d8c0c7144736, 9e290e9a-a735-499e-b5d2-e9e6c4d7fe14
steps resnet50

2.2.3 - Step 03: mobilenet and resnet50 Shadow Deploy

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Step 03: Detecting Objects Using Shadow Deploy

The following tutorial demonstrates how to use two trained models, one based on the resnet50, the other on mobilenet, deployed in Wallaroo to detect objects. This builds on the previous tutorials in this series, Step 01: Detecting Objects Using mobilenet" and “Step 02: Detecting Objects Using resnet50”.

For this tutorial, the Wallaroo feature Shadow Deploy will be used to submit inference requests to both models at once. The mobilnet object detector is the control and the faster-rcnn object detector is the challenger. The results between the two will be compared for their confidence, and that confidence will be used to draw bounding boxes around identified objects.

This process will use the following steps:

  1. Create a Wallaroo workspace and pipeline.
  2. Upload a trained resnet50 ML model and trained mobilenet model and add them as a shadow deployed step with the mobilenet as the control model.
  3. Deploy the pipeline.
  4. Perform an inference on a sample image.
  5. Based on the
  6. Draw the detected objects, their bounding boxes, their classifications, and the confidence of the classifications on the provided image.
  7. Review our results.

Steps

Import Libraries

The first step will be to import our libraries. Please check with Step 00: Introduction and Setup and verify that the necessary libraries and applications are added to your environment.

import torch
import pickle
import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import numpy as np
import json
import requests
import time
import pandas as pd
from CVDemoUtils import CVDemo

Connect to Wallaroo

Now we connect to the Wallaroo instance. If you are connecting from a remote connection, set the wallarooPrefix and wallarooSuffix and use them to connect. If the connection is from within the Wallaroo instance cluster, then just wl = wallaroo.Client() can be used.

# Login through local service

# wl = wallaroo.Client()

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR SUFFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for DataFrame and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Set Variables

The following variables and methods are used later to create or connect to an existing workspace, pipeline, and model. This example has both the resnet model, and a post process script.

workspace_name = 'shadowimageworkspacetest'
pipeline_name = 'shadowimagepipelinetest'
control_model_name = 'mobilenet'
control_model_file_name = 'models/mobilenet.pt.onnx'
challenger_model_name = 'resnet50'
challenger_model_file_name = 'models/frcnn-resnet.pt.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Create Workspace

The workspace will be created or connected to, and set as the default workspace for this session. Once that is done, then all models and pipelines will be set in that workspace.

workspace = get_workspace(workspace_name)
wl.set_current_workspace(workspace)
wl.get_current_workspace()

Create Pipeline and Upload Model

We will now create or connect to an existing pipeline as named in the variables above, then upload each of the models.

pipeline = get_pipeline(pipeline_name)
control =  wl.upload_model(control_model_name, control_model_file_name)
challenger = wl.upload_model(challenger_model_name, challenger_model_file_name)

Shadow Deploy Pipeline

For this step, rather than deploying each model into a separate step, both will be deployed into a single step as a Shadow Deploy step. This will take the inference input data and process it through both pipelines at the same time. The inference results for the control will be stored in it’s ['outputs'] array, while the results for the challenger are stored the ['shadow_data'] array.

pipeline.add_shadow_deploy(control, [challenger])
name shadowimagepipelinetest
created 2023-03-02 19:37:25.349488+00:00
last_updated 2023-03-02 19:37:25.349488+00:00
deployed (none)
tags
versions 474cfb6d-51fc-4e9c-923e-4ca553e73ccd
steps
pipeline.deploy()
name shadowimagepipelinetest
created 2023-03-02 19:37:25.349488+00:00
last_updated 2023-03-02 19:38:07.386270+00:00
deployed True
tags
versions 5b41a12c-f643-47ec-8b9f-842e658fd45c, 474cfb6d-51fc-4e9c-923e-4ca553e73ccd
steps mobilenet
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.13.25',
   'name': 'engine-6775774cb8-ngrz7',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'shadowimagepipelinetest',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'resnet50',
      'version': 'e30e6def-5e32-40d2-bb9f-11896cc36bd9',
      'sha': 'ee606dc9776a1029420b3adf59b6d29395c89d1d9460d75045a1f2f152d288e7',
      'status': 'Running'},
     {'name': 'mobilenet',
      'version': '483465ed-5f41-488e-8539-66a0b028662b',
      'sha': 'f4c7009e53b679f5e44d70d9612e8dc365565cec88c25b5efa11b903b6b7bdc6',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.12.52',
   'name': 'engine-lb-ddd995646-qrj6m',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Prepare input image

Next we will load a sample image and resize it to the width and height required for the object detector.

We will convert the image to a numpy ndim array and add it do a dictionary


imagePath = 'data/images/current/input/example/store-front.png'

# The image width and height needs to be set to what the model was trained for.  In this case 640x480.
cvDemo = CVDemo()

# The size the image will be resized to meet the input requirements of the object detector
width = 640
height = 480
tensor, controlImage = cvDemo.loadImageAndResize(imagePath, width, height)
challengerImage = controlImage.copy()

# get npArray from the tensorFloat
npArray = tensor.cpu().numpy()

#creates a dictionary with the wallaroo "tensor" key and the numpy ndim array representing image as the value.
dictData = {"tensor": npArray.tolist()}

Run Inference using Shadow Deployment

Now lets have the model detect the objects on the image by running inference and extracting the results

startTime = time.time()
infResults = pipeline.infer(dictData, timeout=60)
endTime = time.time()

if arrowEnabled is True:
    results = infResults[0]
else:
    results = infResults[0].raw

Extract Control Inference Results

First we’ll extract the inference result data for the control model and map it onto the image.

df = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None  # default='warn'
pd.options.display.float_format = '{:.2%}'.format

# Points to where all the inference results are
outputs = results['outputs']
shadow_data = results['shadow_data']

controlBoxes = outputs[0]

# reshape this to an array of bounding box coordinates converted to ints
boxList = controlBoxes['Float']['data']
boxA = np.array(boxList)
controlBoxes = boxA.reshape(-1, 4)
controlBoxes = controlBoxes.astype(int)

df[['x', 'y','width','height']] = pd.DataFrame(controlBoxes)

controlClasses = outputs[1]['Int64']['data']
controlConfidences = outputs[2]['Float']['data']

results = {
    'model_name' : control.name(),
    'pipeline_name' : pipeline.name(),
    'width': width,
    'height': height,
    'image' : controlImage,
    'boxes' : controlBoxes,
    'classes' : controlClasses,
    'confidences' : controlConfidences,
    'confidence-target' : 0.9,
    'color':CVDemo.RED, # color to draw bounding boxes and the text in the statistics
    'inference-time': (endTime-startTime),
    'onnx-time' : 0,                
}
cvDemo.drawAndDisplayDetectedObjectsWithClassification(results)
png

Display the Control Results

Here we will use the Wallaroo CVDemo helper class to draw the control model results on the image.

The full results will be displayed in a dataframe with columns representing the classification, confidence, and bounding boxes of the objects identified.

Once extracted from the results we will want to reshape the flattened array into an array with 4 elements (x,y,width,height).

idx = 0 
cocoClasses = cvDemo.getCocoClasses()
for idx in range(0,len(controlClasses)):
    df['classification'][idx] = cocoClasses[controlClasses[idx]] # Classes contains the 80 different COCO classificaitons
    df['confidence'][idx] = controlConfidences[idx]
df
classification confidence x y width height
0 car 99.82% 278 335 494 471
1 person 95.43% 32 303 66 365
2 umbrella 81.33% 117 256 209 322
3 person 72.38% 183 310 203 367
4 umbrella 58.16% 213 273 298 309
5 person 47.49% 155 307 180 365
6 person 45.20% 263 315 303 422
7 person 44.17% 8 304 36 361
8 person 41.89% 608 330 628 375
9 person 40.04% 557 330 582 395
10 potted plant 39.22% 241 193 315 292
11 person 38.94% 547 329 573 397
12 person 38.50% 615 331 634 372
13 person 37.89% 553 321 576 374
14 person 37.04% 147 304 170 366
15 person 36.11% 515 322 537 369
16 person 34.55% 562 317 586 373
17 person 32.37% 531 329 557 399
18 person 32.19% 239 306 279 428
19 person 30.28% 320 308 343 359
20 person 26.50% 289 311 310 380
21 person 23.09% 371 307 394 337
22 person 22.66% 295 300 340 373
23 person 22.23% 1 306 25 362
24 person 21.88% 484 319 506 349
25 person 21.13% 272 327 297 405
26 person 20.15% 136 304 160 363
27 person 19.68% 520 338 543 392
28 person 16.86% 478 317 498 348
29 person 16.55% 365 319 391 344
30 person 16.22% 621 339 639 403
31 potted plant 16.18% 0 361 215 470
32 person 15.13% 279 313 300 387
33 person 10.62% 428 312 444 337
34 umbrella 10.01% 215 252 313 315
35 umbrella 9.10% 295 294 346 357
36 umbrella 7.95% 358 293 402 319
37 umbrella 7.81% 319 307 344 356
38 potted plant 7.18% 166 331 221 439
39 umbrella 6.38% 129 264 200 360
40 person 5.69% 428 318 450 343

Display the Challenger Results

Here we will use the Wallaroo CVDemo helper class to draw the challenger model results on the input image.

challengerBoxes = shadow_data['resnet50'][0]
# reshape this to an array of bounding box coordinates converted to ints
boxList = challengerBoxes['Float']['data']
boxA = np.array(boxList)
challengerBoxes = boxA.reshape(-1, 4)
challengerBoxes = challengerBoxes.astype(int)

challengerDf = pd.DataFrame(columns=['classification','confidence','x','y','width','height'])
pd.options.mode.chained_assignment = None  # default='warn'
pd.options.display.float_format = '{:.2%}'.format

challengerDf[['x', 'y','width','height']] = pd.DataFrame(challengerBoxes)
#pd.options.display.float_format = '{:.2%}'.format
challengerClasses = shadow_data['resnet50'][1]['Int64']['data']
challengerConfidences = shadow_data['resnet50'][2]['Float']['data']

blue = (255, 0, 0)
results = {
    'model_name' : challenger.name(),
    'pipeline_name' : pipeline.name(),
    'width': width,
    'height': height,
    'image' : challengerImage,
    'boxes' : challengerBoxes,
    'classes' : challengerClasses,
    'confidences' : challengerConfidences,
    'confidence-target' : 0.90,
    'color':CVDemo.BLUE, # color to draw bounding boxes and the text in the statistics
    'inference-time': (endTime-startTime),
    'onnx-time' : 0,                
}
cvDemo.drawAndDisplayDetectedObjectsWithClassification(results)
png

Display Challenger Results

The inference results for the objects detected by the challenger model will be displayed including the confidence values. Once extracted from the results we will want to reshape the flattened array into an array with 4 elements (x,y,width,height).

idx = 0 
for idx in range(0,len(challengerClasses)):
    challengerDf['classification'][idx] = cvDemo.CLASSES[challengerClasses[idx]] # Classes contains the 80 different COCO classificaitons
    challengerDf['confidence'][idx] = challengerConfidences[idx]
challengerDf
classification confidence x y width height
0 car 99.91% 274 332 496 472
1 person 99.77% 536 320 563 409
2 person 98.88% 31 305 69 370
3 car 97.02% 617 335 639 424
4 potted plant 96.82% 141 337 164 365
... ... ... ... ... ... ...
81 person 5.61% 312 316 341 371
82 umbrella 5.60% 328 275 418 337
83 person 5.54% 416 320 425 331
84 person 5.52% 406 317 419 331
85 person 5.14% 277 308 292 390

86 rows × 6 columns

pipeline.undeploy()
name shadowimagepipelinetest
created 2023-03-02 19:37:25.349488+00:00
last_updated 2023-03-02 19:38:07.386270+00:00
deployed False
tags
versions 5b41a12c-f643-47ec-8b9f-842e658fd45c, 474cfb6d-51fc-4e9c-923e-4ca553e73ccd
steps mobilenet

Conclusion

Notice the difference in the control confidence and the challenger confidence. Clearly we can see in this example the challenger resnet50 model is performing better than the control mobilenet model. This is likely due to the fact that frcnn resnet50 model is a 2 stage object detector vs the frcnn mobilenet is a single stage detector.

This completes using Wallaroo’s shadow deployment feature to compare different computer vision models.

2.3 - Containerized MLFlow Tutorial

The Containerized MLFlow Tutorial demonstrates how to use Wallaroo with containerized MLFlow models.

Configure Wallaroo with a Private Containerized Model Container Registry

Organizations can configure Wallaroo to use private Containerized Model Container Registry. This allows for the use of containerized models such as MLFlow.

Configure Via Kots

If Wallaroo was installed via kots, use the following procedure to add the private model registry information.

  1. Launch the Wallaroo Administrative Dashboard through a terminal linked to the Kubernetes cluster. Replace the namespace with the one used in your installation.

    kubectl kots admin-console --namespace wallaroo
    
  2. Launch the dashboard, by default at http://localhost:8800.

  3. From the admin dashboard, select Config -> Private Model Container Registry.

  4. Enable Provide private container registry credentials for model images.

  5. Provide the following:

    1. Registery URL: The URL of the Containerized Model Container Registry. Typically in the format host:port. In this example, the registry for GitHub is used. NOTE: When setting the URL for the Containerized Model Container Registry, only the actual service address is needed. For example: with the full URL of the model as ghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-statsmodels-example:2022.4, the URL would be ghcr.io/wallaroolabs.
    2. email: The email address of the user authenticating to the registry service.
    3. username: The username of the user authentication to the registry service.
    4. password: The password of the user authentication to the registry service. In the GitHub example from “MLFlow Creation Tutorial Part 03: Container Registration”, this would be the token.
    <figure>
        <img src="/images/current/wallaroo-configuration/wallaroo-private-model-registry/kots-private-registry.png" width="800"/> 
    </figure>
    
  6. Scroll down and select Save config.

  7. Deploy the new version.

Once complete, the Wallaroo instance will be able to authenticate to the Containerized Model Container Registry and retrieve the images.

Configure via Helm

  1. During either the installation process or updates, set the following in the local-values.yaml file:

    1. privateModelRegistry:

      1. enabled: true
      2. secretName: model-registry-secret
      3. registry: The URL of the private registry.
      4. email: The email address of the user authenticating to the registry service.
      5. username: The username of the user authentication to the registry service.
      6. password: The password of the user authentication to the registry service. In the GitHub example from “MLFlow Creation Tutorial Part 03: Container Registration”, this would be the token.

      For example:

      
      # Other settings - DNS entries, etc.
      
      # The private registry settings
      privateModelRegistry:
        enabled: true
        secretName: model-registry-secret
        registry: "YOUR REGISTRY URL:YOUR REGISTRY PORT"
        email: "YOUR EMAIL ADDRESS"
        username: "YOUR USERNAME"
        password: "Your Password here"
      
  2. Install or update the Wallaroo instance via Helm as per the Wallaroo Helm Install instructions.

Once complete, the Wallaroo instance will be able to authenticate to the registry service and retrieve the images.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

MLFlow Inference with Wallaroo Tutorial

Wallaroo users can register their trained MLFlow ML Models from a containerized model container registry into their Wallaroo instance and perform inferences with it through a Wallaroo pipeline.

As of this time, Wallaroo only supports MLFlow 1.3.0 containerized models. For information on how to containerize an MLFlow model, see the MLFlow Documentation.

This tutorial assumes that you have a Wallaroo instance, and have either your own containerized model or use the one from the reference and are running this Notebook from the Wallaroo Jupyter Hub service.

See the Wallaroo Private Containerized Model Container Registry Guide for details on how to configure a Wallaroo instance with a private model registry.

MLFlow Data Formats

When using containerized MLFlow models with Wallaroo, the inputs and outputs must be named. For example, the following output:

[-12.045839810372835]

Would need to be wrapped with the data values named:

[{"prediction": -12.045839810372835}]

A short sample code for wrapping data may be:

output_df = pd.DataFrame(prediction, columns=["prediction"])
return output_df

MLFlow Models and Wallaroo

MLFlow models are composed of two parts: the model, and the flavors. When submitting a MLFlow model to Wallaroo, both aspects must be part of the ML Model included in the container. For full information about MLFlow model structure, see the MLFlow Documentation.

Wallaroo registers the models from container registries. Organizations will either have to make their containers available in a public or through a private Containerized Model Container Registry service. For examples on setting up a private container registry service, see the Docker Documentation “Deploy a registry server”. For more details on setting up a container registry in a cloud environment, see the related documentation for your preferred cloud provider:

For this example, we will be using the MLFlow containers that was registered in a GitHub container registry service in MLFlow Creation Tutorial Part 03: Container Registration. The address of those containers are:

  • postprocess: ghcr.io/johnhansarickwallaroo/mlflowtests/mlflow-postprocess-example . Used for format data after the statsmodel inferences.
  • statsmodel: ghcr.io/johnhansarickwallaroo/mlflowtests/mlflow-statsmodels-example . The statsmodel generated in MLFlow Creation Tutorial Part 01: Model Creation.

Prerequisites

Before uploading and running an inference with a MLFlow model in Wallaroo the following will be required:

  • MLFlow Input Schema: The input schema with the fields and data types for each MLFlow model type uploaded to Wallaroo. In the examples below, the data types are imported using the pyarrow library.
  • A Wallaroo instance version 2023.1 or later.

IMPORTANT NOTE: Wallaroo supports MLFlow 1.3.0. Please ensure the MLFlow models used in Wallaroo meet this specification.

MLFlow Inference Steps

To register a containerized MLFlow ML Model into Wallaroo, use the following general step:

  • Import Libraries
  • Connect to Wallaroo
  • Set MLFlow Input Schemas
  • Register MLFlow Model
  • Create Pipeline and Add Model Steps
  • Run Inference

Import Libraries

We start by importing the libraries we will need to connect to Wallaroo and use our MLFlow models. This includes the wallaroo libraries, pyarrow for data types, and the json library for handling JSON data.

import uuid
import json

import numpy as np

import wallaroo
from wallaroo.object import EntityNotFoundError

import pyarrow as pa

Connect to Wallaroo

Connect to Wallaroo and store the connection in the variable wl.

The folowing methods are used to create the workspace and pipeline for this tutorial. A workspace is created and set as the current workspace that will contain the registered models and pipelines.

# Login through local service

# wl = wallaroo.Client()

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR SUFFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for DataFrame and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
prefix = 'mlflow'
workspace_name= f"{prefix}statsmodelworkspace"
pipeline_name = f"{prefix}statsmodelpipeline"

mlflowworkspace = get_workspace(workspace_name)
wl.set_current_workspace(mlflowworkspace)

pipeline = get_pipeline(pipeline_name)

Set MLFlow Input Schemas

Set the MLFlow input schemas through the pyarrow library. In the examples below, the input schemas for both the MLFlow model statsmodels-test and the statsmodels-test-postprocess model.

sm_input_schema = pa.schema([
  pa.field('temp', pa.float32()),
  pa.field('holiday', pa.uint8()),
  pa.field('workingday', pa.uint8()),
  pa.field('windspeed', pa.float32())
])

pp_input_schema = pa.schema([
    pa.field('predicted_mean', pa.float32())
])

Register MLFlow Model

Use the register_model_image method to register the Docker container containing the MLFlow models.

statsmodelUrl = "ghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-statsmodels-example:2022.4"
postprocessUrl = "ghcr.io/wallaroolabs/wallaroo_tutorials/mlflow-postprocess-example:2022.4"

sm_model = wl.register_model_image(
    name=f"{prefix}statmodels",
    image=f"{statsmodelUrl}"
).configure("mlflow", input_schema=sm_input_schema, output_schema=pp_input_schema)
pp_model = wl.register_model_image(
    name=f"{prefix}postprocess",
    image=f"{postprocessUrl}"
).configure("mlflow", input_schema=pp_input_schema, output_schema=pp_input_schema)

Create Pipeline and Add Model Steps

With the models registered, we can add the MLFlow models as steps in the pipeline. Once ready, we will deploy the pipeline so it is available for submitting data for running inferences.

pipeline.add_model_step(sm_model)
pipeline.add_model_step(pp_model)
name mlflowstatsmodelpipeline
created 2023-03-02 18:06:43.227930+00:00
last_updated 2023-03-02 18:06:43.227930+00:00
deployed (none)
tags
versions 7e0bdce0-20ce-4b46-ae24-e1ac53a1c6d2
steps
pipeline.deploy()
name mlflowstatsmodelpipeline
created 2023-03-02 18:06:43.227930+00:00
last_updated 2023-03-02 18:06:48.462230+00:00
deployed True
tags
versions 79d6eba9-5eb6-4d70-9676-dc2db997a7ea, 7e0bdce0-20ce-4b46-ae24-e1ac53a1c6d2
steps mlflowstatmodels
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.15.72',
   'name': 'engine-6cf5554547-rvswq',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'mlflowstatsmodelpipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'mlflowstatmodels',
      'version': 'aa17282c-0d17-4a95-bc0a-741ffdbd35c8',
      'sha': '6029a5ba3dd2f7aee588bc285ed97bb7cabb302c190c9b9337f0985614f1ed93',
      'status': 'Running'},
     {'name': 'mlflowpostprocess',
      'version': 'c2f63298-db6f-43c4-91f7-f2f22d9aced0',
      'sha': '3dd892e3bba894455bc6c7031b4dc9ce79e70330c65a1de2689dca00cdec59df',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.16.252',
   'name': 'engine-lb-ddd995646-mlh2q',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': [{'ip': '10.244.16.251',
   'name': 'engine-sidekick-mlflowstatmodels-6-65d85f97fd-kcs4h',
   'status': 'Running',
   'reason': None,
   'details': [],
   'statuses': '\n'},
  {'ip': '10.244.15.71',
   'name': 'engine-sidekick-mlflowpostprocess-7-649797d44b-gwhjl',
   'status': 'Running',
   'reason': None,
   'details': [],
   'statuses': '\n'}]}

Run Inference

Once the pipeline is running, we can submit our data to the pipeline and return our results. Once finished, we will undeploy the pipeline to return the resources back to the cluster.

results = pipeline.infer_from_file('./resources/bike_day_eval_engine.json')
results
model_name model_version pipeline_name outputs elapsed time original_data check_failures shadow_data
0 mlflowpostprocess c2f63298-db6f-43c4-91f7-f2f22d9aced0 mlflowstatsmodelpipeline [{'Float': {'v': 1, 'dim': [7, 1], 'data': [0.... 200 2023-03-02 18:21:26.057 None 0 {}
if arrowEnabled is True:
    display(results.loc[0,'outputs'][0]['Float']['data'])
else:
    display(results[0].data()[0])
[0.2819833755493164,
 0.6588467955589294,
 0.5723681449890137,
 0.6198734641075134,
 -1.2178009748458862,
 -1.8491559028625488,
 0.9338850378990173]
pipeline.undeploy()
name mlflowstatsmodelpipeline
created 2023-03-02 18:06:43.227930+00:00
last_updated 2023-03-02 18:06:48.462230+00:00
deployed False
tags
versions 79d6eba9-5eb6-4d70-9676-dc2db997a7ea, 7e0bdce0-20ce-4b46-ae24-e1ac53a1c6d2
steps mlflowstatmodels

2.4 - Demand Curve Quick Start Guide

The Demand Curve Quick Start Guide demonstrates how to use Wallaroo to chart a demand curve based on submitted data. This example uses a model plus preprocess and postprocessing steps.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Demand Curve Pipeline Tutorial

This worksheet demonstrates a Wallaroo pipeline with data preprocessing, a model, and data postprocessing.

The model is a “demand curve” that predicts the expected number of units of a product that will be sold to a customer as a function of unit price and facts about the customer. Such models can be used for price optimization or sales volume forecasting. This is purely a “toy” demonstration, but is useful for detailing the process of working with models and pipelines.

Data preprocessing is required to create the features used by the model. Simple postprocessing prevents nonsensical estimates (e.g. negative units sold).

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.

This is accomplished using the wallaroo.Client() command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.

The other libraries shown below are used for this example.

import json
import wallaroo
import pandas
import numpy
import conversion
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Now that the Wallaroo client has been initialized, we can create the workspace and call it demandcurveworkspace, then set it as our current workspace. We’ll also create our pipeline so it’s ready when we add our models to it.

We’ll set some variables and methods to create our workspace, pipelines and models. Note that as of the July 2022 release of Wallaroo, workspace names must be unique. Pipelines with the same name will be created as a new version when built.

workspace_name = 'demandcurveworkspace'
pipeline_name = 'demandcurvepipeline'
model_name = 'demandcurvemodel'
model_file_name = './demand_curve_v1.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

demandcurve_pipeline = get_pipeline(pipeline_name)
demandcurve_pipeline
name demandcurvepipeline
created 2023-02-27 18:51:20.074971+00:00
last_updated 2023-02-27 18:51:20.074971+00:00
deployed (none)
tags
versions 0b7583a3-b11b-4f87-975d-2a6aa4306564
steps

With our workspace established, we’ll upload three models:

  • demand_curve_v1.onnx: Our demand_curve model. We’ll store the upload configuration into demand_curve_model.
  • preprocess: Takes the data and prepares it for the demand curve model. We’ll store the upload configuration into module_pre.
  • postprocess: Takes the results from our demand curve model and prepares it for our display. We’ll store the upload configuration into module_post.

Note that the order we upload our models isn’t important - we’ll be establishing the actual process of moving data from one model to the next when we set up our pipeline.

# upload to wallaroo
demand_curve_model = wl.upload_model(model_name, model_file_name).configure()
# load the preprocess module
module_pre = wl.upload_model("preprocess", "./preprocess.py").configure('python')
# load the postprocess module
module_post = wl.upload_model("postprocess", "./postprocess.py").configure('python')

With our models uploaded, we’re going to create our own pipeline and give it three steps:

  • First, start with the preprocess module we called module_pre to prepare the data.
  • Second, we apply the data to our demand_curve_model.
  • And finally, we prepare our data for output with the module_post.
# now make a pipeline
demandcurve_pipeline.add_model_step(module_pre)
demandcurve_pipeline.add_model_step(demand_curve_model)
demandcurve_pipeline.add_model_step(module_post)
name demandcurvepipeline
created 2023-02-27 18:51:20.074971+00:00
last_updated 2023-02-27 18:51:20.074971+00:00
deployed (none)
tags
versions 0b7583a3-b11b-4f87-975d-2a6aa4306564
steps

And with that - let’s deploy our model pipeline. This usually takes about 45 seconds for the deployment to finish.

demandcurve_pipeline.deploy()
name demandcurvepipeline
created 2023-02-27 18:51:20.074971+00:00
last_updated 2023-02-27 18:51:25.156342+00:00
deployed True
tags
versions eb3773df-47a4-4d29-8817-a692ba33fb1c, 0b7583a3-b11b-4f87-975d-2a6aa4306564
steps preprocess

We can check the status of our pipeline to make sure everything was set up correctly:

demandcurve_pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.43',
   'name': 'engine-5c44c969b6-slpc9',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'demandcurvepipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'preprocess',
      'version': 'c5ea27da-9746-4cfe-9907-84bad4c38c29',
      'sha': '1d0090808e807ccb20422e77e59d4d38e3cc39fae5ce115032e68a855a5a62c0',
      'status': 'Running'},
     {'name': 'demandcurvemodel',
      'version': '11eadaf2-485e-4582-a28c-94a4303b3f15',
      'sha': '2820b42c9e778ae259918315f25afc8685ecab9967bad0a3d241e6191b414a0d',
      'status': 'Running'},
     {'name': 'postprocess',
      'version': 'a558e867-c845-4ce4-aa81-7b389bf8578d',
      'sha': '35fbb219462ed5d80f103c920c06f09fd3950c2334cd4c124af5a5c7d2ecbd2f',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.0.42',
   'name': 'engine-lb-ddd995646-ps5sc',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Everything is ready. Let’s feed our pipeline some data. We have some information prepared with the daily_purchasses.csv spreadsheet. We’ll start with just one row to make sure that everything is working correctly.

# read in some purchase data
purchases = pandas.read_csv('daily_purchases.csv')

# start with a one-row data frame for testing
subsamp_raw = purchases.iloc[0:1,: ]
subsamp_raw

# create the input dictionary from the original one-line dataframe
input_dict = conversion.pandas_to_dict(subsamp_raw)
result = demandcurve_pipeline.infer(input_dict)

We can see from the prediction field that the demand curve has a predicted slope of 6.68 from our sample data. We can isolate that by specifying just the data output below.

if arrowEnabled is True:
    display(result[0]['prediction'])
else:
    display(result[0].data())
[6.68025518653071]

Bulk Inference

The initial test went perfectly. Now let’s throw some more data into our pipeline. We’ll draw 10 random rows from our spreadsheet, perform an inference from that, and then display the results and the logs showing the pipeline’s actions.

# Let's do 10 rows at once (drawn randomly)
ix = numpy.random.choice(purchases.shape[0], size=10, replace=False)
output = demandcurve_pipeline.infer(conversion.pandas_to_dict(purchases.iloc[ix,: ]))
if arrowEnabled is True:
    display(output[0]['prediction'])
else:
    display(output[0].data())
[49.73419363867448,
 33.125323160373426,
 9.110871146224234,
 33.125323160373426,
 40.57067889202563,
 33.125323160373426,
 6.771545926800889,
 33.125323160373426,
 33.125323160373426,
 33.125323160373426]

Undeploy the Pipeline

Once we’ve finished with our demand curve demo, we’ll undeploy the pipeline and give the resources back to our Kubernetes cluster.

demandcurve_pipeline.undeploy()
name demandcurvepipeline
created 2023-02-27 18:51:20.074971+00:00
last_updated 2023-02-27 18:51:25.156342+00:00
deployed False
tags
versions eb3773df-47a4-4d29-8817-a692ba33fb1c, 0b7583a3-b11b-4f87-975d-2a6aa4306564
steps preprocess

Thank you for being a part of this demonstration. If you have additional questions, please feel free to contact us at Wallaroo.

2.5 - IMDB Tutorial

The IMDB Tutorial demonstrates how to use Wallaroo to determine if reviews are positive or negative.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

IMDB Sample

The following example demonstrates how to use Wallaroo with chained models. In this example, we will be using information from the IMDB (Internet Movie DataBase) with a sentiment model to detect whether a given review is positive or negative. Imagine using this to automatically scan Tweets regarding your product and finding either customers who need help or have nice things to say about your product.

Note that this example is considered a “toy” model - only the first 100 words in the review were tokenized, and the embedding is very small.

The following example is based on the Large Movie Review Dataset, and sample data can be downloaded from the aclIMDB dataset.

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.

This is accomplished using the wallaroo.Client() command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.

If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client(). If logging in externally, update the wallarooPrefix and wallarooSuffix variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.

import wallaroo
from wallaroo.object import EntityNotFoundError
from IPython.display import display

# used to display dataframe information without truncating
import pandas as pd
pd.set_option('display.max_colwidth', None)
print(wallaroo.__version__)
2023.1.0rc1

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True
# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

To test this model, we will perform the following:

  • Create a workspace for our models.
  • Upload two models:
    • embedder: Takes pre-tokenized text documents (model input: 100 integers/datum; output 800 numbers/datum) and creates an embedding from them.
    • sentiment: The second model classifies the resulting embeddings from 0 to 1, which 0 being an unfavorable review, 1 being a favorable review.
  • Create a pipeline that will take incoming data and pass it to the embedder, which will pass the output to the sentiment model, and then export the final result.
  • To test it, we will use information that has already been tokenized and submit it to our pipeline and gauge the results.

Just for the sake of this tutorial, we’ll use the SDK below to create our workspace , assign as our current workspace, then display all of the workspaces we have at the moment. We’ll also set up for our models and pipelines down the road, so we have one spot to change names to whatever fits your organization’s standards best.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

When we create our new workspace, we’ll save it in the Python variable workspace so we can refer to it as needed.

First we’ll create a workspace for our environment, and call it imdbworkspace. We’ll also set up our pipeline so it’s ready for our models.

import string
import random

# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))
workspace_name = f'{prefix}imdbworkspace'
pipeline_name = f'{prefix}imdbpipeline'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

imdb_pipeline = get_pipeline(pipeline_name)
imdb_pipeline
name vzfximdbpipeline
created 2023-02-27 18:54:24.344787+00:00
last_updated 2023-02-27 18:54:24.344787+00:00
deployed (none)
tags
versions 1f3f3e66-56ec-401b-b550-5f06655f4770
steps

Just to make sure, let’s list our current workspace. If everything is going right, it will show us we’re in the imdb-workspace.

wl.get_current_workspace()
{'name': 'vzfximdbworkspace', 'id': 18, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T18:54:23.351035+00:00', 'models': [], 'pipelines': [{'name': 'vzfximdbpipeline', 'create_time': datetime.datetime(2023, 2, 27, 18, 54, 24, 344787, tzinfo=tzutc()), 'definition': '[]'}]}

Now we’ll upload our two models:

  • embedder.onnx: This will be used to embed the tokenized documents for evaluation.
  • sentiment_model.onnx: This will be used to analyze the review and determine if it is a positive or negative review. The closer to 0, the more likely it is a negative review, while the closer to 1 the more likely it is to be a positive review.
embedder = wl.upload_model(f'{prefix}embedder-o', './embedder.onnx').configure()
smodel = wl.upload_model(f'{prefix}smodel-o', './sentiment_model.onnx').configure(runtime="onnx", tensor_fields=["flatten_1"])

With our models uploaded, now we’ll create our pipeline that will contain two steps:

  • First, it runs the data through the embedder.
  • Second, it applies it to our sentiment model.
# now make a pipeline
imdb_pipeline.add_model_step(embedder)
imdb_pipeline.add_model_step(smodel)
name vzfximdbpipeline
created 2023-02-27 18:54:24.344787+00:00
last_updated 2023-02-27 18:54:24.344787+00:00
deployed (none)
tags
versions 1f3f3e66-56ec-401b-b550-5f06655f4770
steps

Now that we have our pipeline set up with the steps, we can deploy the pipeline.

imdb_pipeline.deploy()
name vzfximdbpipeline
created 2023-02-27 18:54:24.344787+00:00
last_updated 2023-02-27 18:54:30.220884+00:00
deployed True
tags
versions bc2644e2-d780-4515-ab6a-044f4e6fd27e, 1f3f3e66-56ec-401b-b550-5f06655f4770
steps vzfxembedder-o

We’ll check the pipeline status to verify it’s deployed and the models are ready.

imdb_pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.45',
   'name': 'engine-57d4bf6d5d-j77n8',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'vzfximdbpipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'vzfxsmodel-o',
      'version': '69f78f5e-633d-49d8-b46e-d57563669e7b',
      'sha': '3473ea8700fbf1a1a8bfb112554a0dde8aab36758030dcde94a9357a83fd5650',
      'status': 'Running'},
     {'name': 'vzfxembedder-o',
      'version': 'b50749b8-af50-4404-9f95-ffd084ee9416',
      'sha': 'd083fd87fa84451904f71ab8b9adfa88580beb92ca77c046800f79780a20b7e4',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.2.18',
   'name': 'engine-lb-ddd995646-njpqc',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}
print(arrowEnabled)
True

To test this out, we’ll start with a single piece of information from our data directory.

if arrowEnabled is True:
    # using Dataframe JSON
    results = results = imdb_pipeline.infer_from_file('./data/singleton.df.json')
    display(results)
else:
    # pre-arrow Wallaroo JSON
    results = imdb_pipeline.infer_from_file('./data/singleton.json')
    display(results[0].data())
time in.tensor out.dense_1 check_failures
0 2023-02-27 18:54:47.498 [1607.0, 2635.0, 5749.0, 199.0, 49.0, 351.0, 16.0, 2919.0, 159.0, 5092.0, 2457.0, 8.0, 11.0, 1252.0, 507.0, 42.0, 287.0, 316.0, 15.0, 65.0, 136.0, 2.0, 133.0, 16.0, 4311.0, 131.0, 286.0, 153.0, 5.0, 2826.0, 175.0, 54.0, 548.0, 48.0, 1.0, 17.0, 9.0, 183.0, 1.0, 111.0, 15.0, 1.0, 17.0, 284.0, 982.0, 18.0, 28.0, 211.0, 1.0, 1382.0, 8.0, 146.0, 1.0, 19.0, 12.0, 9.0, 13.0, 21.0, 1898.0, 122.0, 14.0, 70.0, 14.0, 9.0, 97.0, 25.0, 74.0, 1.0, 189.0, 12.0, 9.0, 6.0, 31.0, 3.0, 244.0, 2497.0, 3659.0, 2.0, 665.0, 2497.0, 63.0, 180.0, 1.0, 17.0, 6.0, 287.0, 3.0, 646.0, 44.0, 15.0, 161.0, 50.0, 71.0, 438.0, 351.0, 31.0, 5749.0, 2.0, 0.0, 0.0] [0.37142318] 0

Since that works, let’s load up all 50 rows and do a full inference on each of them. Note that Jupyter Hub has a size limitation, so for production systems the outputs should be piped out to a different output.

if arrowEnabled is True:
    # using Dataframe JSON
    results = imdb_pipeline.infer_from_file('./data/test_data.df.json')
    # just display the first row for space
    display(results.loc[0,:])
else:
    # pre-arrow Wallaroo JSON
    results = imdb_pipeline.infer_from_file('./data/test_data.json')
    # just display the first row for space
    display(results[0].data())
time                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                2023-02-27 18:54:48.032000
in.tensor         [1607.0, 2635.0, 5749.0, 199.0, 49.0, 351.0, 16.0, 2919.0, 159.0, 5092.0, 2457.0, 8.0, 11.0, 1252.0, 507.0, 42.0, 287.0, 316.0, 15.0, 65.0, 136.0, 2.0, 133.0, 16.0, 4311.0, 131.0, 286.0, 153.0, 5.0, 2826.0, 175.0, 54.0, 548.0, 48.0, 1.0, 17.0, 9.0, 183.0, 1.0, 111.0, 15.0, 1.0, 17.0, 284.0, 982.0, 18.0, 28.0, 211.0, 1.0, 1382.0, 8.0, 146.0, 1.0, 19.0, 12.0, 9.0, 13.0, 21.0, 1898.0, 122.0, 14.0, 70.0, 14.0, 9.0, 97.0, 25.0, 74.0, 1.0, 189.0, 12.0, 9.0, 6.0, 31.0, 3.0, 244.0, 2497.0, 3659.0, 2.0, 665.0, 2497.0, 63.0, 180.0, 1.0, 17.0, 6.0, 287.0, 3.0, 646.0, 44.0, 15.0, 161.0, 50.0, 71.0, 438.0, 351.0, 31.0, 5749.0, 2.0, 0.0, 0.0]
out.dense_1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       [0.37142318]
check_failures                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               0
Name: 0, dtype: object

Undeploy

With our pipeline’s work done, we’ll undeploy it and give our Kubernetes environment back its resources.

imdb_pipeline.undeploy()
name vzfximdbpipeline
created 2023-02-27 18:54:24.344787+00:00
last_updated 2023-02-27 18:54:30.220884+00:00
deployed False
tags
versions bc2644e2-d780-4515-ab6a-044f4e6fd27e, 1f3f3e66-56ec-401b-b550-5f06655f4770
steps vzfxembedder-o

And there is our example. Please feel free to contact us at Wallaroo for if you have any questions.

3 - Wallaroo Features Tutorials

Tutorials on Wallaroo features.

The following tutorials are created to highlight specific features in Wallaroo.

3.1 - Hot Swap Models Tutorial

How to swap models in a pipeline step while the pipeline is deployed.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Model Hot Swap Tutorial

One of the biggest challenges facing organizations once they have a model trained is deploying the model: Getting all of the resources together, MLOps configured and systems prepared to allow inferences to run.

The next biggest challenge? Replacing the model while keeping the existing production systems running.

This tutorial demonstrates how Wallaroo model hot swap can update a pipeline step with a new model with one command. This lets organizations keep their production systems running while changing a ML model, with the change taking only milliseconds, and any inference requests in that time are processed after the hot swap is completed.

This example and sample data comes from the Machine Learning Group’s demonstration on Credit Card Fraud detection.

This tutorial provides the following:

  • ccfraud.onnx: A pre-trained ML model used to detect potential credit card fraud.
  • xgboost_ccfraud.onnx: A pre-trained ML model used to detect potential credit card fraud originally converted from an XGBoost model. This will be used to swap with the ccfraud.onnx.
  • smoke_test.json: A data file used to verify that the model will return a low possibility of credit card fraud.
  • high_fraud.json: A data file used to verify that the model will return a high possibility of credit card fraud.
  • Sample inference data files: Data files used for inference examples with the following number of records:
    • cc_data_5.json: 5 records.
    • cc_data_1k.json: 1,000 records.
    • cc_data_10k.json: 10,000 records.
    • cc_data_40k.json: Over 40,000 records.

Reference

For more information about Wallaroo and related features, see the Wallaroo Documentation Site.

Steps

The following steps demonstrate the following:

  • Connect to a Wallaroo instance.
  • Create a workspace and pipeline.
  • Upload both models to the workspace.
  • Deploy the pipe with the ccfraud.onnx model as a pipeline step.
  • Perform sample inferences.
  • Hot swap and replace the existing model with the xgboost_ccfraud.onnx while keeping the pipeline deployed.
  • Conduct additional inferences to demonstrate the model hot swap was successful.
  • Undeploy the pipeline and return the resources back to the Wallaroo instance.

Load the Libraries

Load the Python libraries used to connect and interact with the Wallaroo instance.

import wallaroo
from wallaroo.object import EntityNotFoundError

import pandas as pd

# used to display dataframe information without truncating
pd.set_option('display.max_colwidth', None)  

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client.

This is accomplished using the wallaroo.Client(api_endpoint, auth_endpoint, auth_type command) command that connects to the Wallaroo instance services.

The Client method takes the following parameters:

  • api_endpoint (String): The URL to the Wallaroo instance API service.
  • auth_endpoint (String): The URL to the Wallaroo instance Keycloak service.
  • auth_type command (String): The authorization type. In this case, SSO.

The URLs are based on the Wallaroo Prefix and Wallaroo Suffix for the Wallaroo instance. For more information, see the DNS Integration Guide. In the example below, replace “YOUR PREFIX” and “YOUR SUFFIX” with the Wallaroo Prefix and Suffix, respectively.

If connecting from within the Wallaroo instance’s JupyterHub service, then only wl = wallaroo.Client() is required.

Once run, the wallaroo.Client command provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Depending on the configuration of the Wallaroo instance, the user will either be presented with a login request to the Wallaroo instance or be authenticated through a broker such as Google, Github, etc. To use the broker, select it from the list under the username/password login forms. For more information on Wallaroo authentication configurations, see the Wallaroo Authentication Configuration Guides.

# Internal Login

wl = wallaroo.Client()

# Remote Login

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Set the Variables

The following variables are used in the later steps for creating the workspace, pipeline, and uploading the models. Modify them according to your organization’s requirements.

Just for the sake of this tutorial, we’ll use the SDK below to create our workspace , assign as our current workspace, then display all of the workspaces we have at the moment. We’ll also set up for our models and pipelines down the road, so we have one spot to change names to whatever fits your organization’s standards best.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

import string
import random

# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

workspace_name = f'{prefix}hotswapworkspace'
pipeline_name = f'{prefix}hotswappipeline'
original_model_name = f'{prefix}ccfraudoriginal'
original_model_file_name = './ccfraud.onnx'
replacement_model_name = f'{prefix}ccfraudreplacement'
replacement_model_file_name = './xgboost_ccfraud.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Create the Workspace

We will create a workspace based on the variable names set above, and set the new workspace as the current workspace. This workspace is where new pipelines will be created in and store uploaded models for this session.

Once set, the pipeline will be created.

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name ggwzhotswappipeline
created 2023-02-27 17:33:53.541871+00:00
last_updated 2023-02-27 17:33:53.541871+00:00
deployed (none)
tags
versions ad943ff6-1a38-4304-a243-6958ba118df2
steps

Upload Models

We can now upload both of the models. In a later step, only one model will be added as a pipeline step, where the pipeline will submit inference requests to the pipeline.

original_model = wl.upload_model(original_model_name , original_model_file_name)
replacement_model = wl.upload_model(replacement_model_name , replacement_model_file_name)
wl.list_models()
Name # of Versions Owner ID Last Updated Created At
ggwzccfraudreplacement 1 "" 2023-02-27 17:33:55.531013+00:00 2023-02-27 17:33:55.531013+00:00
ggwzccfraudoriginal 1 "" 2023-02-27 17:33:55.147884+00:00 2023-02-27 17:33:55.147884+00:00

Add Model to Pipeline Step

With the models uploaded, we will add the original model as a pipeline step, then deploy the pipeline so it is available for performing inferences.

pipeline.add_model_step(original_model)
pipeline
name ggwzhotswappipeline
created 2023-02-27 17:33:53.541871+00:00
last_updated 2023-02-27 17:33:53.541871+00:00
deployed (none)
tags
versions ad943ff6-1a38-4304-a243-6958ba118df2
steps
pipeline.deploy()
name ggwzhotswappipeline
created 2023-02-27 17:33:53.541871+00:00
last_updated 2023-02-27 17:33:58.263239+00:00
deployed True
tags
versions 3078dffa-4e10-41ef-85bc-e7a0de5afa82, ad943ff6-1a38-4304-a243-6958ba118df2
steps ggwzccfraudoriginal
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.30',
   'name': 'engine-7bb46d756-2v7j7',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'ggwzhotswappipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'ggwzccfraudoriginal',
      'version': '55fe0137-2e0b-4c7d-9cf2-6521b526339e',
      'sha': 'bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.0.29',
   'name': 'engine-lb-ddd995646-wjg4k',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Verify the Model

The pipeline is deployed with our model. The following will verify that the model is operating correctly. The high_fraud.json file contains data that the model should process as a high likelihood of being a fraudulent transaction.

if arrowEnabled is True:
    result = pipeline.infer_from_file('./data/smoke_test.df.json')
else:
    result = pipeline.infer_from_file('./data/smoke_test.json')
display(result)
time out.dense_1 check_failures metadata.last_model
0 1677519249926 [0.0014974177] [] {"model_name":"ggwzccfraudoriginal","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}

Replace the Model

The pipeline is currently deployed and is able to handle inferences. The model will now be replaced without having to undeploy the pipeline. This is done using the pipeline method replace_with_model_step(index, model). Steps start at 0, so the method called below will replace step 0 in our pipeline with the replacement model.

As an exercise, this deployment can be performed while inferences are actively being submitted to the pipeline to show how quickly the swap takes place.

pipeline.replace_with_model_step(0, replacement_model).deploy()
name ggwzhotswappipeline
created 2023-02-27 17:33:53.541871+00:00
last_updated 2023-02-27 17:34:11.049248+00:00
deployed True
tags
versions a620354f-291e-4a98-b5f7-9d8bf165b1df, 3078dffa-4e10-41ef-85bc-e7a0de5afa82, ad943ff6-1a38-4304-a243-6958ba118df2
steps ggwzccfraudoriginal
# Display the pipeline
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.30',
   'name': 'engine-7bb46d756-2v7j7',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'ggwzhotswappipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'ggwzccfraudoriginal',
      'version': '55fe0137-2e0b-4c7d-9cf2-6521b526339e',
      'sha': 'bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.0.29',
   'name': 'engine-lb-ddd995646-wjg4k',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Verify the Swap

To verify the swap, we’ll submit a set of inferences to the pipeline using the new model. We’ll display just the first 5 rows for space reasons.

if arrowEnabled is True:
    result = pipeline.infer_from_file('./data/cc_data_1k.df.json')
    display(result.loc[0:4,:])
else:
    result = pipeline.infer_from_file('./data/cc_data_1k.json')
    display(result)
time out.dense_1 check_failures metadata.last_model
0 1677519255319 [0.99300325] [] {"model_name":"ggwzccfraudreplacement","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}
1 1677519255319 [0.99300325] [] {"model_name":"ggwzccfraudreplacement","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}
2 1677519255319 [0.99300325] [] {"model_name":"ggwzccfraudreplacement","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}
3 1677519255319 [0.99300325] [] {"model_name":"ggwzccfraudreplacement","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}
4 1677519255319 [0.0010916889] [] {"model_name":"ggwzccfraudreplacement","model_sha":"bc85ce596945f876256f41515c7501c399fd97ebcb9ab3dd41bf03f8937b4507"}

Undeploy the Pipeline

With the tutorial complete, the pipeline is undeployed to return the resources back to the Wallaroo instance.

pipeline.undeploy()
name ggwzhotswappipeline
created 2023-02-27 17:33:53.541871+00:00
last_updated 2023-02-27 17:34:11.049248+00:00
deployed False
tags
versions a620354f-291e-4a98-b5f7-9d8bf165b1df, 3078dffa-4e10-41ef-85bc-e7a0de5afa82, ad943ff6-1a38-4304-a243-6958ba118df2
steps ggwzccfraudoriginal

3.2 - Inference URL Tutorial

How to use Internal and External Inference URLs to perform inferences through the Wallaroo SDK on deployed pipelines.

Wallaroo inferences URLs are provided to allow users to submit inference requests through an HTTP API request. Internal URls are enabled by default when a pipeline is deployed. External URLs are enabled by Wallaroo admins through configuration changes through the Wallaroo Administrative Dashboard or other Kubernetes options.

Two tutorials are provided:

  • Internal Pipeline Inference URL Tutorial: Shows how to use the Internal inference URL for a deployed pipeline.
  • External Pipeline Inference URL Tutorial: Demonstrates how to set the configuration options to enable the External Pipeline Inference URl, and how to perform sample API commands through the interface.

3.2.1 - Internal Pipeline Inference URL Tutorial

How to set up a pipeline for deployment and use the internal inference URL.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Internal Pipeline Inference URL Tutorial

Wallaroo provides the ability to perform inferences through deployed pipelines via both internal and external inference URLs. These inference URLs allow inferences to be performed by submitting data to the internal or external URL with the inference results returned in the same format as the InferenceResult Object.

Internal URLs are available only through the internal Kubernetes environment hosting the Wallaroo instance as demonstrated in this tutorial. External URLs are available outside of the Kubernetes environment, such as the public internet. These are demonstrated in the External Pipeline Deployment URL Tutorial.

The following tutorial shows how to set up an environment and demonstrates how to use the Internal Deployment URL. This example provides the following:

  • alohacnnlstm.zip: Aloha model used as part of the Aloha Quick Tutorial.
  • For Arrow enabled instances:
    • data_1.df.json, data_1k.df.json and data_25k.df.json: Sample data used for testing inferences with the sample model.
  • For Arrow distabled instances:
    • data_1.json, data_1k.json and data_25k.json: Sample data used for testing inferences with the sample model.

For our example, we will perform the following:

  • Create a workspace for our work.
  • Upload the Aloha model.
  • Create a pipeline that can ingest our submitted data, submit it to the model, and export the results.
  • Run a sample inference through our pipeline via the SDK to demonstrate the inference is accurate.
  • Run a sample inference through our pipeline’s Internal URL and store the results in a file.

All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.

This is accomplished using the wallaroo.Client() command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.

import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wallarooPrefix = "doc-test"
# wallarooSuffix = "example.com"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Create the Workspace

We will create a workspace to work in and call it the urldemoworkspace, then set it as current workspace environment. We’ll also create our pipeline in advance as urldemopipeline.

The model to be uploaded and used for inference will be labeled as urldemomodel. Modify these to your organizations requirements.

Once complete, the workspace will be created or, if already existing, set to the current workspace to host the pipelines and models.

workspace_name = 'urldemoworkspace'
pipeline_name = 'urldemopipeline'
model_name = 'urldemomodel'
model_file_name = './alohacnnlstm.zip'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name urldemopipeline
created 2023-02-27 17:55:12.813456+00:00
last_updated 2023-02-27 17:55:12.813456+00:00
deployed (none)
tags
versions 54158104-c71d-4980-a6a3-25564c909b44
steps

We can verify the workspace is created the current default workspace with the get_current_workspace() command.

wl.get_current_workspace()
{'name': 'urldemoworkspace', 'id': 14, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T17:55:11.802586+00:00', 'models': [], 'pipelines': [{'name': 'urldemopipeline', 'create_time': datetime.datetime(2023, 2, 27, 17, 55, 12, 813456, tzinfo=tzutc()), 'definition': '[]'}]}

Upload the Models

Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow format.

model = wl.upload_model(model_name, model_file_name).configure("tensorflow")

Deploy The Pipeline

Now that we have a model that we want to use we will create a deployment for it.

We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.

pipeline.add_model_step(model)
pipeline.deploy()
name urldemopipeline
created 2023-02-27 17:55:12.813456+00:00
last_updated 2023-02-27 17:56:25.368424+00:00
deployed True
tags
versions 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44
steps urldemomodel

We can verify that the pipeline is running and list what models are associated with it.

pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.37',
   'name': 'engine-85c895dbbf-tfq4r',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'urldemopipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'urldemomodel',
      'version': 'a4a80d9f-dcfe-419d-becd-dbab31b65904',
      'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.1.12',
   'name': 'engine-lb-ddd995646-p4nb2',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Interferences

Infer 1 row

Now that the pipeline is deployed and our Aloha model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file command to load a single encoded URL into the inference engine and print the results back out.

The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 0.

if arrowEnabled is True:
    result = pipeline.infer_from_file('./data/data_1.df.json')
else:
    result = pipeline.infer_from_file("./data/data_1.json")
display(result)
time in.text_input out.qakbot out.gozi out.cryptolocker out.pykspa out.kraken out.locky out.corebot out.ramdo out.suppobox out.simda out.matsnu out.banjori out.main out.ramnit out.dircrypt check_failures
0 2023-02-27 17:57:33.165 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 16, 32, 23, 29, 32, 30, 19, 26, 17] [0.016155062] [2.0289372e-05] [0.012099565] [0.008038961] [0.0003197726] [0.011029283] [0.9829148] [0.006236233] [1.3889951e-27] [1.793378e-26] [0.010341615] [0.0015195857] [0.997564] [0.0009985751] [4.7591297e-05] 0

Batch Inference

Now that our smoke test is successful, we will retrieve the Internal Deployment URL and perform an inference by submitting our data through a curl command as detailed below.

  • IMPORTANT NOTE: The _deployment._url() method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection, _deployment._url() returns an external URL. External URL connections requires the authentication be included in the HTTP request, and that Model Endpoints Guide external endpoints are enabled in the Wallaroo configuration options.
inference_url = pipeline._deployment._url()
print(inference_url)
connection =wl.mlops().__dict__
token = connection['token']
print(token)
https://doc-test.api.example.com/v1/api/pipelines/infer/urldemopipeline-11
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrTzQ2VjhoQWZDZTBjWU1ETkZobEZWS25HSC1HZy1xc1JkSlhwTTNQYjBJIn0.eyJleHAiOjE2Nzc1MjA3MDIsImlhdCI6MTY3NzUyMDY0MiwiYXV0aF90aW1lIjoxNjc3NTE4MzEyLCJqdGkiOiI1NTdkZDAxYi1jNTVkLTQ0MDQtYTI5ZC01MmRlOWU0MTc2NTciLCJpc3MiOiJodHRwczovL2RvYy10ZXN0LmtleWNsb2FrLndhbGxhcm9vY29tbXVuaXR5Lm5pbmphL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6WyJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6IjQzNWRhOTA1LTMxZTItNGU3NC1iNDIzLTQ1YzM4ZWRiNTg4OSIsInR5cCI6IkJlYXJlciIsImF6cCI6InNkay1jbGllbnQiLCJzZXNzaW9uX3N0YXRlIjoiYWNlMWEzMGQtNjZiYy00NGQ5LWJkMGEtYzYyMzc0NzhmZGFhIiwiYWNyIjoiMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJtYXN0ZXItcmVhbG0iOnsicm9sZXMiOlsibWFuYWdlLXVzZXJzIiwidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiJhY2UxYTMwZC02NmJjLTQ0ZDktYmQwYS1jNjIzNzQ3OGZkYWEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtdXNlci1pZCI6IjQzNWRhOTA1LTMxZTItNGU3NC1iNDIzLTQ1YzM4ZWRiNTg4OSIsIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6InVzZXIiLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbInVzZXIiXSwieC1oYXN1cmEtdXNlci1ncm91cHMiOiJ7fSJ9LCJuYW1lIjoiSm9obiBIYW5zYXJpY2siLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huLmh1bW1lbEB3YWxsYXJvby5haSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJIYW5zYXJpY2siLCJlbWFpbCI6ImpvaG4uaHVtbWVsQHdhbGxhcm9vLmFpIn0.QTTxK6rE-SZIBR7z7hN1aIsGSsPYmBmuI-KxsmwzATjrCtOLO3ObE5YtBye3ITXkG4NAVN3c2llTSzrYLDeBMcsz17_T8UdSpwOVsDeTko-muhzQkcMnUrXGLsJDiOofS3ZT-_S66-IrCfGUD2D1Gj7ufbAnMipyTuE69L1QBEdoszcRfTR-epCqniayB3s6SkhBSgjmgvJcmMSIHxj3zg0siZAjQoxM6_E5GO_o__91p7FiADa0FH3xCmT9iOMM1NcF7FheBNX7xCXBBWekiy9bpB0BQISvMi1IcVCGeMZnTyO1o9ZgFbV5MG-SoKFyZrYUmhBf-JoRjecv1FYgIg
if arrowEnabled is True:
    dataFile="./data/data_25k.df.json"
    contentType="application/json; format=pandas-records"
else:
    dataFile="./data/data_25k.json"
    contentType="application/json"
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data @{dataFile} > curl_response.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 34.3M  100 16.3M  100 18.0M   895k   988k  0:00:18  0:00:18 --:--:--  538k

Undeploy Pipeline

When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks.

IMPORTANT NOTE: For the External Pipeline Deployment URL Tutorial, this pipeline will have to be deployed to make the External Deployment URL available.

pipeline.undeploy()
name urldemopipeline
created 2023-02-27 17:55:12.813456+00:00
last_updated 2023-02-27 17:56:25.368424+00:00
deployed False
tags
versions 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44
steps urldemomodel

3.2.2 - External Inference URL Guide

How to set the admin configurations to enable the external inference URL for deployed Wallaroo pipelines, and perform inferences through them.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

External Pipeline Inference URL Tutorial

Wallaroo provides the ability to perform inferences through deployed pipelines via both internal and external URLs. These URLs allow inferences to be performed by submitting data to the internal or external URL, with the inference results returned in the same format as the InferenceResult Object.

Internal URLs are available only through the internal Kubernetes environment hosting the Wallaroo instance as demonstrated in this tutorial. External URLs are available outside of the Kubernetes environment, such as the public internet. These are demonstrated in the External Pipeline Deployment URL Tutorial.

IMPORTANT NOTE: Before starting this tutorial, the Internal Pipeline Deployment URL Tutorial must be completed to establish the Wallaroo workspace, pipeline and model to be used

The following tutorial shows how to set up an environment and demonstrates how to use the External Deployment URL. This example provides the following:

  • For Arrow enabled instances:
    • data_1.df.json, data_1k.df.json and data_25k.df.json: Sample data used for testing inferences with the sample model.
  • For Arrow distabled instances:
    • data_1.json, data_1k.json and data_25k.json: Sample data used for testing inferences with the sample model.

Prerequisites

  1. Before running this sample notebook, verify that the Internal Pipeline Deployment URL Tutorial has been run. This will create the workspace, pipeline, etc for the below example to run.
  2. Enable external URl inference endpoints through the Wallaroo Administrative Dashboard. This can be accessed through the kots application as detailed in the Wallaroo Install Guildes.

Retrieve Token

There are two methods of retrieving the JWT token used to authenticate to the Wallaroo instance’s API service:

  • Wallaroo SDK. This method requires a Wallaroo based user.
  • API Clent Secret. This is the recommended method as it is user independent. It allows any valid user to make an inference request.

This tutorial will use the Wallaroo SDK method for convenience, with examples on using the API Client Secret method.

API Request Methods

All Wallaroo API endpoints follow the format:

  • https://$URLPREFIX.api.$URLSUFFIX/v1/api$COMMAND

Where $COMMAND is the specific endpoint. For example, for the command to list of workspaces in the Wallaroo instance would use the above format based on these settings:

  • $URLPREFIX: smooth-moose-1617
  • $URLSUFFIX: example.wallaroo.ai
  • $COMMAND: /workspaces/list

This would create the following API endpoint:

  • https://smooth-moose-1617.api.example.wallaroo.ai/v1/api/workspaces/list

Connect to Wallaroo

For this example, a connection to the Wallaroo SDK is used. This will be used to retrieve the JWT token as describe above. Update wallarooPrefix = "YOUR PREFIX" and wallarooSuffix = "YOUR SUFFIX" to match the Wallaroo instance used for this demonstration.

import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd

import requests
from requests.auth import HTTPBasicAuth

import json

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")
APIURL=f"https://{wallarooPrefix}.api.{wallarooSuffix}/v1/api"

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Verify Pipeline Deployed

We’re using the same pipeline from the Internal Pipeline Inference URL Tutorial. We’ll deploy it via the SDK to verify it is running for the later tests.

workspace_name = 'urldemoworkspace'
pipeline_name = 'urldemopipeline'
model_name = 'urldemomodel'
model_file_name = './alohacnnlstm.zip'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline

pipeline.deploy()
name urldemopipeline
created 2023-02-27 17:55:12.813456+00:00
last_updated 2023-02-27 17:59:03.244180+00:00
deployed True
tags
versions 6db21694-9e11-42cb-914c-1528549cedca, 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44
steps urldemomodel

Get External Inference URL

The API command /admin/get_pipeline_external_url retrieves the external inference URL for a specific pipeline in a workspace.

  • Parameters
    • workspace_id (REQUIRED integer): The workspace integer id.
    • pipeline_name (REQUIRED string): The name of the pipeline.

In this example, a list of the workspaces will be retrieved. Based on the setup from the Internal Pipeline Deployment URL Tutorial, the workspace matching urlworkspace will have it’s workspace id stored and used for the /admin/get_pipeline_external_url request with the pipeline urlpipeline.

The External Inference URL will be stored as a variable for the next step.

Modify these values to match the ones used in the Internal Pipeline Deployment URL Tutorial.

# Retrieve the token
connection =wl.mlops().__dict__
token = connection['token']

## Start with the a lists of the workspaces to verify the ID

workspaceName = "urldemoworkspace"
urlPipeline = "urldemopipeline"

# List workspaces

apiRequest = f"{APIURL}/workspaces/list"

data = {
}

headers= {
    'Authorization': 'Bearer ' + token,
    'Content-Type':'application/json'
}

response = requests.post(apiRequest, json=data, headers=headers, verify=True)
workspaces = response.json()
workspaceList = workspaces['workspaces']
#print(workspaceList)
workspaceId = list(filter(lambda x:x["name"]==workspaceName,workspaceList))[0]['id']
# Retrieve the token
connection =wl.mlops().__dict__
token = connection['token']

## Retrieve the pipeline's External Inference URL

apiRequest = f"{APIURL}/admin/get_pipeline_external_url"

data = {
    "workspace_id": workspaceId,
    "pipeline_name": urlPipeline
}

headers= {
    'Authorization': 'Bearer ' + token,
    'Content-Type':'application/json'
}

response = requests.post(apiRequest, json=data, headers=headers, verify=True).json()
externalUrl = response['url']
externalUrl
'https://doc-test.api.example.com/v1/api/pipelines/infer/urldemopipeline-11'

Perform Inference Through External URL

The inference can now be performed through the External Inference URL. This URL will accept the same inference data file that is used with the Wallaroo SDK, or with an Internal Inference URL as used in the Internal Pipeline Inference URL Tutorial.

For this example, the externalUrl retrieved through the Get External Inference URL is used to submit a single inference request through the data file data-1.json.

# Retrieve the token
connection =wl.mlops().__dict__
token = connection['token']

## Inference through external URL

if arrowEnabled is True:
    # retrieve the json data to submit
    data = json.load(open('./data/data_1k.df.json','rb'))
    # set the headers
    headers= {
        'Authorization': 'Bearer ' + token,
        'Content-Type':'application/json; format=pandas-records'
    }
else:
    data = json.load(open('./data/data_1k.json','rb'))
    # set the headers
    headers= {
        'Authorization': 'Bearer ' + token
    }

# submit the request via POST
response = requests.post(externalUrl, json=data, headers=headers)

# Only the first 300 characters will be displayed for brevity
printResponse = json.dumps(response.json())
print(printResponse[0:300])
[{"time": 1677520761514, "in": {"text_input": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 16, 32, 23, 29, 32, 30, 19, 26, 17]}, "out": {"corebot": [0.9829148], "banjori": [0.0015195871], "suppobox": [1.3889898e-27], "ma

Undeploy the Pipeline

With the tutorial complete, we’ll use the SDK to undeploy the pipeline and return the resources back to the Wallaroo instance.

pipeline.undeploy()
name urldemopipeline
created 2023-02-27 17:55:12.813456+00:00
last_updated 2023-02-27 17:59:03.244180+00:00
deployed False
tags
versions 6db21694-9e11-42cb-914c-1528549cedca, 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44
steps urldemomodel

Wallaroo supports the ability to perform inferences through the SDK and through the API for each deployed pipeline. For more information on how to use Wallaroo, see the Wallaroo Documentation Site for full details.

3.3 - Model Insights Tutorial

How to use Model Insights to monitor the environment and know when to retrain the model based on changes to data.

Model Insights

The Model Insights feature lets you monitor how the environment that your model operates within may be changing in ways that affect it’s predictions so that you can intervene (retrain) in an efficient and timely manner. Changes in the inputs, data drift, can occur due to errors in the data processing pipeline or due to changes in the environment such as user preference or behavior.

The validation framework performs per infererence range checks with count frequency based thresholds for alerts and is ideal for catching many errors in input and output data.

In complement to the validation framework model insights focuses on the differences in the distributions of data in a time based window measured against a baseline for a given pipeline and can detect situations where values are still within the expected range but the distribution has shifted. For example, if your model predicts housing prices you might expect the predictions to be between \$200,000 and \$1,000,000 with a distribution centered around \$400,000. If your model suddenly starts predicting prices centered around \$250,000 or \$750,000 the predictions may still be within the expected range but the shift may signal something has changed that should be investigated.

Ideally we’d also monitor the quality of the predictions, concept drift. However this can be difficult as true labels are often not available or are severely delayed in practice. That is there may be a signficant lag between the time the prediction is made and the true (sale price) value is observed.

Consequently, model insights uses data drift detection techniques on both inputs and outputs to detect changes in the distributions of the data.

There are many useful statistical tests for calculating the difference between distributions; however, they typically require assumptions about the underlying distributions or confusing and expensive calculations. We’ve implemented a data drift framework that is easy to understand, fast to compute, runs in an automated fashion and is extensible to many specific use cases.

The methodology currently revolves around calculating the specific percentile-based bins of the baseline distribution and measuring how future distributions fall into these bins. This approach is both visually intuitive and supports an easy to calculate difference score between distributions. Users can tune the scoring mechanism to emphasize different regions of the distribution: for example, you may only care if there is a change in the top 20th percentile of the distribution, compared to the baseline.

You can specify the inputs or outputs that you want to monitor and the data to use for your baselines. You can also specify how often you want to monitor distributions and set parameters to define what constitutes a meaningful change in a distribution for your application.

Once you’ve set up a monitoring task, called an assay, comparisons against your baseline are then run automatically on a scheduled basis. You can be notified if the system notices any abnormally different behavior. The framework also allows you to quickly investigate the cause of any unexpected drifts in your predictions.

The rest of this notebook will shows how to create assays to monitor your pipelines.

NOTE: model insights operates over time and is difficult to demo in a notebook without pre-canned data. We assume you have an active pipeline that has been running and making predictions over time and show you the code you may use to analyze your pipeline.

Workflow

Model Insights has the capability to perform interactive assays so that you can explore the data from a pipeline and learn how the data is behaving. With this information and the knowledge of your particular business use case you can then choose appropriate thresholds for persistent automatic assays as desired.

To get started lets import some libraries we’ll need.

import datetime as dt
from datetime import datetime, timedelta, timezone, tzinfo
import joblib
import pytz
import json
import wallaroo
from wallaroo.object import EntityNotFoundError

import wallaroo.assay
from wallaroo.assay_config import BinMode, Aggregation, Metric

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

import seaborn as sns
import requests
import uuid

import os
import json
from IPython.display import display

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)

plt.rcParams["figure.figsize"] = (12,6)
pd.options.display.float_format = '{:,.2f}'.format
wallaroo.__version__
'2023.1.0rc1'

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Connect to the Wallaroo Instance

This command will be used to set up a connection to the Wallaroo cluster and allow creating and use of Wallaroo inference engines.

# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Connect to Workspace and Pipeline

We will now connect to the existing workspace and pipeline. Update the variables below to match the ones used for past inferences.

workspace_name = 'housepricedrift'
pipeline_name = 'housepricepipe'
model_name = 'housepricemodel'

# Used to generate a unique assay name for each run

import string
import random
# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

assay_name = f"{prefix}example assay"
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name housepricepipe3
created 2023-03-02 16:48:37.592816+00:00
last_updated 2023-03-02 16:48:38.060839+00:00
deployed False
tags
versions 727f0670-e75d-4fee-bf71-657fedf3532b, 6c055424-786a-489d-b146-0771dbea9d3c
steps housepricemodel3

We assume the pipeline has been running for a while and there is a period of time that is free of errors that we’d like to use as the baseline. Lets note the start and end times. For this example we have 30 days of data from Jan 2023 and well use Jan 1 data as our baseline.

import datetime
baseline_start = datetime.datetime.fromisoformat('2023-01-01T00:00:00+00:00')
baseline_end = datetime.datetime.fromisoformat('2023-01-02T00:00:00+00:00')
last_day = datetime.datetime.fromisoformat('2023-02-01T00:00:00+00:00')

Lets create an assay using that pipeline and the model in the pipeline. We also specify the start end end of the baseline.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)

We don’t know much about our baseline data yet so lets examine the data and create a couple of visual representations. First lets get some basic stats on the baseline data.

baseline_run = assay_builder.build().interactive_baseline_run()
baseline_run.baseline_stats()
Baseline
count 182
min 12.00
max 14.97
mean 12.94
median 12.88
std 0.45
start 2023-01-01T00:00:00Z
end 2023-01-02T00:00:00Z

Now lets look at a histogram, kernel density estimate (KDE), and Emperical Cumulative Distribution (ecdf) charts of the baseline data. These will give us insite into the distributions of the predictions and features that the assay is configured for.

assay_builder.baseline_histogram()
png
assay_builder.baseline_kde()
png
assay_builder.baseline_ecdf()
png

Interactive Baseline Runs

We can do an interactive run of just the baseline part to see how the baseline data will be put into bins. This assay uses quintiles so all 5 bins (not counting the outlier bins) have 20% of the predictions. We can see the bin boundaries along the x-axis.

baseline_run.chart()
baseline mean = 12.940910643273655
baseline median = 12.884286880493164
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
png

We can also get a dataframe with the bin/edge information.

baseline_run.baseline_bins()
b_edges b_edge_names b_aggregated_values b_aggregation
0 12.00 left_outlier 0.00 Density
1 12.55 q_20 0.20 Density
2 12.81 q_40 0.20 Density
3 12.98 q_60 0.20 Density
4 13.33 q_80 0.20 Density
5 14.97 q_100 0.20 Density
6 inf right_outlier 0.00 Density

The previous assay used quintiles so all of the bins had the same percentage/count of samples. To get bins that are divided equaly along the range of values we can use BinMode.EQUAL.

equal_bin_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
equal_bin_builder.summarizer_builder.add_bin_mode(BinMode.EQUAL)
equal_baseline = equal_bin_builder.build().interactive_baseline_run()
equal_baseline.chart()
baseline mean = 12.940910643273655
baseline median = 12.884286880493164
bin_mode = Equal
aggregation = Density
metric = PSI
weighted = False
png

We now see very different bin edges and sample percentages per bin.

equal_baseline.baseline_bins()
b_edges b_edge_names b_aggregated_values b_aggregation
0 12.00 left_outlier 0.00 Density
1 12.60 p_1.26e1 0.24 Density
2 13.19 p_1.32e1 0.49 Density
3 13.78 p_1.38e1 0.22 Density
4 14.38 p_1.44e1 0.04 Density
5 14.97 p_1.50e1 0.01 Density
6 inf right_outlier 0.00 Density

Interactive Assay Runs

By default the assay builder creates an assay with some good starting parameters. In particular the assay is configured to run a new analysis for every 24 hours starting at the end of the baseline period. Additionally, it sets the number of bins to 5 so creates quintiles, and sets the target iopath to "outputs 0 0" which means we want to monitor the first column of the first output/prediction.

We can do an interactive run of just the baseline part to see how the baseline data will be put into bins. This assay uses quintiles so all 5 bins (not counting the outlier bins) have 20% of the predictions. We can see the bin boundaries along the x-axis.

We then run it with interactive_run and convert it to a dataframe for easy analysis with to_dataframe.

Now lets do an interactive run of the first assay as it is configured. Interactive runs, don’t save the assay to the database (so they won’t be scheduled in the future) nor do they save the assay results. Instead the results are returned after a short while for further analysis.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_config = assay_builder.add_run_until(last_day).build()
assay_results = assay_config.interactive_run()
assay_df = assay_results.to_dataframe()
assay_df
assay_id name iopath score start min max mean median std warning_threshold alert_threshold status
0 None output dense_2 0 0.00 2023-01-02T00:00:00+00:00 12.05 14.71 12.97 12.90 0.48 None 0.25 Ok
1 None output dense_2 0 0.09 2023-01-03T00:00:00+00:00 12.04 14.65 12.96 12.93 0.41 None 0.25 Ok
2 None output dense_2 0 0.04 2023-01-04T00:00:00+00:00 11.87 14.02 12.98 12.95 0.46 None 0.25 Ok
3 None output dense_2 0 0.06 2023-01-05T00:00:00+00:00 11.92 14.46 12.93 12.87 0.46 None 0.25 Ok
4 None output dense_2 0 0.02 2023-01-06T00:00:00+00:00 12.02 14.15 12.95 12.90 0.43 None 0.25 Ok
5 None output dense_2 0 0.03 2023-01-07T00:00:00+00:00 12.18 14.58 12.96 12.93 0.44 None 0.25 Ok
6 None output dense_2 0 0.02 2023-01-08T00:00:00+00:00 12.01 14.60 12.92 12.90 0.46 None 0.25 Ok
7 None output dense_2 0 0.04 2023-01-09T00:00:00+00:00 12.01 14.40 13.00 12.97 0.45 None 0.25 Ok
8 None output dense_2 0 0.06 2023-01-10T00:00:00+00:00 11.99 14.79 12.94 12.91 0.46 None 0.25 Ok
9 None output dense_2 0 0.02 2023-01-11T00:00:00+00:00 11.90 14.66 12.91 12.88 0.45 None 0.25 Ok
10 None output dense_2 0 0.02 2023-01-12T00:00:00+00:00 11.96 14.82 12.94 12.90 0.46 None 0.25 Ok
11 None output dense_2 0 0.03 2023-01-13T00:00:00+00:00 12.07 14.61 12.96 12.93 0.47 None 0.25 Ok
12 None output dense_2 0 0.15 2023-01-14T00:00:00+00:00 12.00 14.20 13.06 13.03 0.43 None 0.25 Ok
13 None output dense_2 0 2.92 2023-01-15T00:00:00+00:00 12.74 15.62 14.00 14.01 0.57 None 0.25 Alert
14 None output dense_2 0 7.89 2023-01-16T00:00:00+00:00 14.64 17.19 15.91 15.87 0.63 None 0.25 Alert
15 None output dense_2 0 8.87 2023-01-17T00:00:00+00:00 16.60 19.23 17.94 17.94 0.63 None 0.25 Alert
16 None output dense_2 0 8.87 2023-01-18T00:00:00+00:00 18.67 21.29 20.01 20.04 0.64 None 0.25 Alert
17 None output dense_2 0 8.87 2023-01-19T00:00:00+00:00 20.72 23.57 22.17 22.18 0.65 None 0.25 Alert
18 None output dense_2 0 8.87 2023-01-20T00:00:00+00:00 23.04 25.72 24.32 24.33 0.66 None 0.25 Alert
19 None output dense_2 0 8.87 2023-01-21T00:00:00+00:00 25.06 27.67 26.48 26.49 0.63 None 0.25 Alert
20 None output dense_2 0 8.87 2023-01-22T00:00:00+00:00 27.21 29.89 28.63 28.58 0.65 None 0.25 Alert
21 None output dense_2 0 8.87 2023-01-23T00:00:00+00:00 29.36 32.18 30.82 30.80 0.67 None 0.25 Alert
22 None output dense_2 0 8.87 2023-01-24T00:00:00+00:00 31.56 34.35 32.98 32.98 0.65 None 0.25 Alert
23 None output dense_2 0 8.87 2023-01-25T00:00:00+00:00 33.68 36.44 35.14 35.14 0.66 None 0.25 Alert
24 None output dense_2 0 8.87 2023-01-26T00:00:00+00:00 35.93 38.51 37.31 37.33 0.65 None 0.25 Alert
25 None output dense_2 0 3.69 2023-01-27T00:00:00+00:00 12.06 39.91 29.29 38.65 12.66 None 0.25 Alert
26 None output dense_2 0 0.05 2023-01-28T00:00:00+00:00 11.87 13.88 12.92 12.90 0.38 None 0.25 Ok
27 None output dense_2 0 0.10 2023-01-29T00:00:00+00:00 12.02 14.36 12.98 12.96 0.38 None 0.25 Ok
28 None output dense_2 0 0.11 2023-01-30T00:00:00+00:00 11.99 14.44 12.89 12.88 0.37 None 0.25 Ok
29 None output dense_2 0 0.01 2023-01-31T00:00:00+00:00 12.00 14.64 12.92 12.89 0.40 None 0.25 Ok

Basic functionality for creating quick charts is included.

assay_results.chart_scores()
png

We see that the difference scores are low for a while and then jump up to indicate there is an issue. We can examine that particular window to help us decide if that threshold is set correctly or not.

We can generate a quick chart of the results. This chart shows the 5 quantile bins (quintiles) derived from the baseline data plus one for left outliers and one for right outliers. We also see that the data from the window falls within the baseline quintiles but in a different proportion and is skewing higher. Whether this is an issue or not is specific to your use case.

First lets examine a day that is only slightly different than the baseline. We see that we do see some values that fall outside of the range from the baseline values, the left and right outliers, and that the bin values are different but similar.

assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
png

Other days, however are significantly different.

assay_results[12].chart()
baseline mean = 12.940910643273655
window mean = 13.06380216891949
baseline median = 12.884286880493164
window median = 13.027600288391112
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.15060511096978788
scores = [4.6637149189075455e-05, 0.05969428191167242, 0.00806617426854112, 0.008316273402678306, 0.07090885609902021, 0.003572888138686759, 0.0]
index = None
png
assay_results[13].chart()
baseline mean = 12.940910643273655
window mean = 14.004728427908038
baseline median = 12.884286880493164
window median = 14.009637832641602
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 2.9220486095961196
scores = [0.0, 0.7090936334784107, 0.7130482300184766, 0.33500731896676245, 0.12171058214520876, 0.9038825518183468, 0.1393062931689142]
index = None
png

If we want to investigate further, we can run interactive assays on each of the inputs to see if any of them show anything abnormal. In this example we’ll provide the feature labels to create more understandable titles.

The current assay expects continuous data. Sometimes categorical data is encoded as 1 or 0 in a feature and sometimes in a limited number of values such as 1, 2, 3. If one value has high a percentage the analysis emits a warning so that we know the scores for that feature may not behave as we expect.

labels = ['bedrooms', 'bathrooms', 'lat', 'long', 'waterfront', 'sqft_living', 'sqft_lot', 'floors', 'view', 'condition', 'grade', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'sqft_living15', 'sqft_lot15']

topic = wl.get_topic_name(pipeline.id())

all_inferences = wl.get_raw_pipeline_inference_logs(topic, baseline_start, last_day, model_name, limit=1_000_000)

assay_builder = wl.build_assay("Input Assay", pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.window_builder().add_width(hours=4)
assay_config = assay_builder.build()
assay_results = assay_config.interactive_input_run(all_inferences, labels)
iadf = assay_results.to_dataframe()
display(iadf)
column distinct_vals label           largest_pct
     0            17 bedrooms        0.4244 
     1            44 bathrooms       0.2398 
     2          3281 lat             0.0014 
     3           959 long            0.0066 
     4             4 waterfront      0.9156 *** May not be continuous feature
     5          3901 sqft_living     0.0032 
     6          3487 sqft_lot        0.0173 
     7            11 floors          0.4567 
     8            10 view            0.8337 
     9             9 condition       0.5915 
    10            19 grade           0.3943 
    11           745 sqft_above      0.0096 
    12           309 sqft_basement   0.5582 
    13           224 yr_built        0.0239 
    14            77 yr_renovated    0.8889 
    15           649 sqft_living15   0.0093 
    16          3280 sqft_lot15      0.0199 
assay_id name iopath score start min max mean median std warning_threshold alert_threshold status
0 None input tensor 0 0.19 2023-01-02T00:00:00+00:00 -2.54 1.75 0.21 0.68 0.99 None 0.25 Ok
1 None input tensor 0 0.03 2023-01-02T04:00:00+00:00 -1.47 2.82 0.21 -0.40 0.95 None 0.25 Ok
2 None input tensor 0 0.09 2023-01-02T08:00:00+00:00 -2.54 3.89 -0.04 -0.40 1.22 None 0.25 Ok
3 None input tensor 0 0.05 2023-01-02T12:00:00+00:00 -1.47 2.82 -0.12 -0.40 0.94 None 0.25 Ok
4 None input tensor 0 0.08 2023-01-02T16:00:00+00:00 -1.47 1.75 -0.00 -0.40 0.76 None 0.25 Ok
... ... ... ... ... ... ... ... ... ... ... ... ... ...
3055 None input tensor 16 0.08 2023-01-31T04:00:00+00:00 -0.42 4.87 0.25 -0.17 1.13 None 0.25 Ok
3056 None input tensor 16 0.58 2023-01-31T08:00:00+00:00 -0.43 2.01 -0.04 -0.21 0.48 None 0.25 Alert
3057 None input tensor 16 0.13 2023-01-31T12:00:00+00:00 -0.32 7.75 0.30 -0.20 1.57 None 0.25 Ok
3058 None input tensor 16 0.26 2023-01-31T16:00:00+00:00 -0.43 5.88 0.19 -0.18 1.17 None 0.25 Alert
3059 None input tensor 16 0.84 2023-01-31T20:00:00+00:00 -0.40 0.52 -0.17 -0.25 0.18 None 0.25 Alert

3060 rows × 13 columns

We can chart each of the iopaths and do a visual inspection. From the charts we see that if any of the input features had significant differences in the first two days which we can choose to inspect further. Here we choose to show 3 charts just to save space in this notebook.

assay_results.chart_iopaths(labels=labels, selected_labels=['bedrooms', 'lat', 'sqft_living'])
png
png
png

When we are comfortable with what alert threshold should be for our specific purposes we can create and save an assay that will be automatically run on a daily basis.

In this example we’re create an assay that runs everyday against the baseline and has an alert threshold of 0.5.

Once we upload it it will be saved and scheduled for future data as well as run against past data.

alert_threshold = 0.5
import string
import random

prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

assay_name = f"{prefix}example assay"
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_alert_threshold(alert_threshold)
assay_id = assay_builder.upload()

After a short while, we can get the assay results for further analysis.

When we get the assay results, we see that the assays analysis is similar to the interactive run we started with though the analysis for the third day does not exceed the new alert threshold we set. And since we called upload instead of interactive_run the assay was saved to the system and will continue to run automatically on schedule from now on.

Scheduling Assays

By default assays are scheduled to run every 24 hours starting immediately after the baseline period ends.

However, you can control the start time by setting start and the frequency by setting interval on the window.

So to recap:

  • The window width is the size of the window. The default is 24 hours.
  • The interval is how often the analysis is run, how far the window is slid into the future based on the last run. The default is the window width.
  • The window start is when the analysis should start. The default is the end of the baseline period.

For example to run an analysis every 12 hours on the previous 24 hours of data you’d set the window width to 24 (the default) and the interval to 12.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_builder = assay_builder.add_run_until(last_day)

assay_builder.window_builder().add_width(hours=24).add_interval(hours=12)

assay_config = assay_builder.build()

assay_results = assay_config.interactive_run()
print(f"Generated {len(assay_results)} analyses")
Generated 59 analyses
assay_results.chart_scores()
png

To start a weekly analysis of the previous week on a specific day, set the start date (taking care to specify the desired timezone), and the width and interval to 1 week and of course an analysis won’t be generated till a window is complete.

report_start = datetime.datetime.fromisoformat('2022-01-03T00:00:00+00:00')

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end)
assay_builder = assay_builder.add_run_until(last_day)

assay_builder.window_builder().add_width(weeks=1).add_interval(weeks=1).add_start(report_start)

assay_config = assay_builder.build()

assay_results = assay_config.interactive_run()
print(f"Generated {len(assay_results)} analyses")
Generated 5 analyses
assay_results.chart_scores()
png

Advanced Configuration

The assay can be configured in a variety of ways to help customize it to your particular needs. Specifically you can:

  • change the BinMode to evenly spaced, quantile or user provided
  • change the number of bins to use
  • provide weights to use when scoring the bins
  • calculate the score using the sum of differences, maximum difference or population stability index
  • change the value aggregation for the bins to density, cumulative or edges

Lets take a look at these in turn.

Default configuration

First lets look at the default configuration. This is a lot of information but much of it is useful to know where it is available.

We see that the assay is broken up into 4 sections. A top level meta data section, a section for the baseline specification, a section for the window specification and a section that specifies the summarization configuration.

In the meta section we see the name of the assay, that it runs on the first column of the first output "outputs 0 0" and that there is a default threshold of 0.25.

The summarizer section shows us the defaults of Quantile, Density and PSI on 5 bins.

The baseline section shows us that it is configured as a fixed baseline with the specified start and end date times.

And the window tells us what model in the pipeline we are analyzing and how often.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
print(assay_builder.build().to_json())
{
    "name": "dqybexample assay",
    "pipeline_id": 297,
    "pipeline_name": "housepricepipe3",
    "active": true,
    "status": "created",
    "iopath": "output dense_2 0",
    "baseline": {
        "Fixed": {
            "pipeline": "housepricepipe3",
            "model": "housepricemodel3",
            "start_at": "2023-01-01T00:00:00+00:00",
            "end_at": "2023-01-02T00:00:00+00:00"
        }
    },
    "window": {
        "pipeline": "housepricepipe3",
        "model": "housepricemodel3",
        "width": "24 hours",
        "start": null,
        "interval": null
    },
    "summarizer": {
        "type": "UnivariateContinuous",
        "bin_mode": "Quantile",
        "aggregation": "Density",
        "metric": "PSI",
        "num_bins": 5,
        "bin_weights": null,
        "bin_width": null,
        "provided_edges": null,
        "add_outlier_edges": true
    },
    "warning_threshold": null,
    "alert_threshold": 0.25,
    "run_until": "2023-02-01T00:00:00+00:00",
    "workspace_id": 121
}

Defaults

We can run the assay interactively and review the first analysis. The method compare_basic_stats gives us a dataframe with basic stats for the baseline and window data.

assay_results = assay_builder.build().interactive_run()
ar = assay_results[0]

ar.compare_basic_stats()
Baseline Window diff pct_diff
count 182.00 181.00 -1.00 -0.55
min 12.00 12.05 0.04 0.36
max 14.97 14.71 -0.26 -1.71
mean 12.94 12.97 0.03 0.22
median 12.88 12.90 0.01 0.12
std 0.45 0.48 0.03 5.68
start 2023-01-01T00:00:00+00:00 2023-01-02T00:00:00+00:00 NaN NaN
end 2023-01-02T00:00:00+00:00 2023-01-03T00:00:00+00:00 NaN NaN

The method compare_bins gives us a dataframe with the bin information. Such as the number of bins, the right edges, suggested bin/edge names and the values for each bin in the baseline and the window.

ar.compare_bins()
b_edges b_edge_names b_aggregated_values b_aggregation w_edges w_edge_names w_aggregated_values w_aggregation diff_in_pcts
0 12.00 left_outlier 0.00 Density 12.00 left_outlier 0.00 Density 0.00
1 12.55 q_20 0.20 Density 12.55 e_1.26e1 0.19 Density -0.01
2 12.81 q_40 0.20 Density 12.81 e_1.28e1 0.21 Density 0.01
3 12.98 q_60 0.20 Density 12.98 e_1.30e1 0.18 Density -0.02
4 13.33 q_80 0.20 Density 13.33 e_1.33e1 0.21 Density 0.01
5 14.97 q_100 0.20 Density 14.97 e_1.50e1 0.21 Density 0.01
6 NaN right_outlier 0.00 Density NaN right_outlier 0.00 Density 0.00

We can also plot the chart to visualize the values of the bins.

ar.chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
png

Binning Mode

We can change the bin mode algorithm to equal and see that the bins/edges are partitioned at different points and the bins have different values.

prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

assay_name = f"{prefix}example assay"

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.EQUAL)
assay_results = assay_builder.build().interactive_run()
display(display(assay_results[0].compare_bins()))
assay_results[0].chart()
b_edges b_edge_names b_aggregated_values b_aggregation w_edges w_edge_names w_aggregated_values w_aggregation diff_in_pcts
0 12.00 left_outlier 0.00 Density 12.00 left_outlier 0.00 Density 0.00
1 12.60 p_1.26e1 0.24 Density 12.60 e_1.26e1 0.24 Density 0.00
2 13.19 p_1.32e1 0.49 Density 13.19 e_1.32e1 0.48 Density -0.02
3 13.78 p_1.38e1 0.22 Density 13.78 e_1.38e1 0.22 Density -0.00
4 14.38 p_1.44e1 0.04 Density 14.38 e_1.44e1 0.06 Density 0.02
5 14.97 p_1.50e1 0.01 Density 14.97 e_1.50e1 0.01 Density 0.00
6 NaN right_outlier 0.00 Density NaN right_outlier 0.00 Density 0.00
None

baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Equal
aggregation = Density
metric = PSI
weighted = False
score = 0.011074287819376092
scores = [0.0, 7.3591419975306595e-06, 0.000773779195360713, 8.538514991838585e-05, 0.010207597078872246, 1.6725322721660374e-07, 0.0]
index = None
png

User Provided Bin Edges

The values in this dataset run from ~11.6 to ~15.81. And lets say we had a business reason to use specific bin edges. We can specify them with the BinMode.PROVIDED and specifying a list of floats with the right hand / upper edge of each bin and optionally the lower edge of the smallest bin. If the lowest edge is not specified the threshold for left outliers is taken from the smallest value in the baseline dataset.

edges = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0]
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.PROVIDED, edges)
assay_results = assay_builder.build().interactive_run()
display(display(assay_results[0].compare_bins()))
assay_results[0].chart()
b_edges b_edge_names b_aggregated_values b_aggregation w_edges w_edge_names w_aggregated_values w_aggregation diff_in_pcts
0 11.00 left_outlier 0.00 Density 11.00 left_outlier 0.00 Density 0.00
1 12.00 e_1.20e1 0.00 Density 12.00 e_1.20e1 0.00 Density 0.00
2 13.00 e_1.30e1 0.62 Density 13.00 e_1.30e1 0.59 Density -0.03
3 14.00 e_1.40e1 0.36 Density 14.00 e_1.40e1 0.35 Density -0.00
4 15.00 e_1.50e1 0.02 Density 15.00 e_1.50e1 0.06 Density 0.03
5 16.00 e_1.60e1 0.00 Density 16.00 e_1.60e1 0.00 Density 0.00
6 NaN right_outlier 0.00 Density NaN right_outlier 0.00 Density 0.00
None

baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Provided
aggregation = Density
metric = PSI
weighted = False
score = 0.0321620386600679
scores = [0.0, 0.0, 0.0014576920813015586, 3.549754401142936e-05, 0.030668849034754912, 0.0, 0.0]
index = None
png

Number of Bins

We could also choose to a different number of bins, lets say 10, which can be evenly spaced or based on the quantiles (deciles).

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.QUANTILE).add_num_bins(10)
assay_results = assay_builder.build().interactive_run()
display(display(assay_results[1].compare_bins()))
assay_results[1].chart()
b_edges b_edge_names b_aggregated_values b_aggregation w_edges w_edge_names w_aggregated_values w_aggregation diff_in_pcts
0 12.00 left_outlier 0.00 Density 12.00 left_outlier 0.00 Density 0.00
1 12.41 q_10 0.10 Density 12.41 e_1.24e1 0.09 Density -0.00
2 12.55 q_20 0.10 Density 12.55 e_1.26e1 0.04 Density -0.05
3 12.72 q_30 0.10 Density 12.72 e_1.27e1 0.14 Density 0.03
4 12.81 q_40 0.10 Density 12.81 e_1.28e1 0.05 Density -0.05
5 12.88 q_50 0.10 Density 12.88 e_1.29e1 0.12 Density 0.02
6 12.98 q_60 0.10 Density 12.98 e_1.30e1 0.09 Density -0.01
7 13.15 q_70 0.10 Density 13.15 e_1.32e1 0.18 Density 0.08
8 13.33 q_80 0.10 Density 13.33 e_1.33e1 0.14 Density 0.03
9 13.47 q_90 0.10 Density 13.47 e_1.35e1 0.07 Density -0.03
10 14.97 q_100 0.10 Density 14.97 e_1.50e1 0.08 Density -0.02
11 NaN right_outlier 0.00 Density NaN right_outlier 0.00 Density 0.00
None

baseline mean = 12.940910643273655
window mean = 12.956829186961135
baseline median = 12.884286880493164
window median = 12.929338455200195
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.16591076620684958
scores = [0.0, 0.0002571306027792045, 0.044058279699182114, 0.009441459631493015, 0.03381618572319047, 0.0027335446937028877, 0.0011792419836838435, 0.051023062424253904, 0.009441459631493015, 0.008662563542113508, 0.0052978382749576496, 0.0]
index = None

/opt/homebrew/anaconda3/envs/arrowtests/lib/python3.8/site-packages/wallaroo/assay.py:315: UserWarning: FixedFormatter should only be used together with FixedLocator
  ax.set_xticklabels(labels=edge_names, rotation=45)
png

Bin Weights

Now lets say we only care about differences at the higher end of the range. We can use weights to specify that difference in the lower bins should not be counted in the score.

If we stick with 10 bins we can provide 10 a vector of 12 weights. One weight each for the original bins plus one at the front for the left outlier bin and one at the end for the right outlier bin.

Note we still show the values for the bins but the scores for the lower 5 and left outlier are 0 and only the right half is counted and reflected in the score.

weights = [0] * 6
weights.extend([1] * 6)
print("Using weights: ", weights)
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_bin_mode(BinMode.QUANTILE).add_num_bins(10).add_bin_weights(weights)
assay_results = assay_builder.build().interactive_run()
display(display(assay_results[1].compare_bins()))
assay_results[1].chart()
Using weights:  [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]
b_edges b_edge_names b_aggregated_values b_aggregation w_edges w_edge_names w_aggregated_values w_aggregation diff_in_pcts
0 12.00 left_outlier 0.00 Density 12.00 left_outlier 0.00 Density 0.00
1 12.41 q_10 0.10 Density 12.41 e_1.24e1 0.09 Density -0.00
2 12.55 q_20 0.10 Density 12.55 e_1.26e1 0.04 Density -0.05
3 12.72 q_30 0.10 Density 12.72 e_1.27e1 0.14 Density 0.03
4 12.81 q_40 0.10 Density 12.81 e_1.28e1 0.05 Density -0.05
5 12.88 q_50 0.10 Density 12.88 e_1.29e1 0.12 Density 0.02
6 12.98 q_60 0.10 Density 12.98 e_1.30e1 0.09 Density -0.01
7 13.15 q_70 0.10 Density 13.15 e_1.32e1 0.18 Density 0.08
8 13.33 q_80 0.10 Density 13.33 e_1.33e1 0.14 Density 0.03
9 13.47 q_90 0.10 Density 13.47 e_1.35e1 0.07 Density -0.03
10 14.97 q_100 0.10 Density 14.97 e_1.50e1 0.08 Density -0.02
11 NaN right_outlier 0.00 Density NaN right_outlier 0.00 Density 0.00
None

baseline mean = 12.940910643273655
window mean = 12.956829186961135
baseline median = 12.884286880493164
window median = 12.929338455200195
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = True
score = 0.012600694309416988
scores = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.00019654033061397393, 0.00850384373737565, 0.0015735766052488358, 0.0014437605903522511, 0.000882973045826275, 0.0]
index = None

/opt/homebrew/anaconda3/envs/arrowtests/lib/python3.8/site-packages/wallaroo/assay.py:315: UserWarning: FixedFormatter should only be used together with FixedLocator
  ax.set_xticklabels(labels=edge_names, rotation=45)
png

Metrics

The score is a distance or dis-similarity measure. The larger it is the less similar the two distributions are. We currently support summing the differences of each individual bin, taking the maximum difference and a modified Population Stability Index (PSI).

The following three charts use each of the metrics. Note how the scores change. The best one will depend on your particular use case.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
png
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_metric(Metric.SUMDIFF)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = SumDiff
weighted = False
score = 0.025438649748041997
scores = [0.0, 0.009956893934794486, 0.006648048084512165, 0.01548175581324751, 0.006648048084512165, 0.012142553579017668, 0.0]
index = None
png
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_metric(Metric.MAXDIFF)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = MaxDiff
weighted = False
score = 0.01548175581324751
scores = [0.0, 0.009956893934794486, 0.006648048084512165, 0.01548175581324751, 0.006648048084512165, 0.012142553579017668, 0.0]
index = 3
png

Aggregation Options

Also, bin aggregation can be done in histogram Aggregation.DENSITY style (the default) where we count the number/percentage of values that fall in each bin or Empirical Cumulative Density Function style Aggregation.CUMULATIVE where we keep a cumulative count of the values/percentages that fall in each bin.

assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_aggregation(Aggregation.DENSITY)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Density
metric = PSI
weighted = False
score = 0.0029273068646199748
scores = [0.0, 0.000514261205558409, 0.0002139202456922972, 0.0012617897456473992, 0.0002139202456922972, 0.0007234154220295724, 0.0]
index = None
png
assay_builder = wl.build_assay(assay_name, pipeline, model_name, baseline_start, baseline_end).add_run_until(last_day)
assay_builder.summarizer_builder.add_aggregation(Aggregation.CUMULATIVE)
assay_results = assay_builder.build().interactive_run()
assay_results[0].chart()
baseline mean = 12.940910643273655
window mean = 12.969964654406132
baseline median = 12.884286880493164
window median = 12.899214744567873
bin_mode = Quantile
aggregation = Cumulative
metric = PSI
weighted = False
score = 0.04419889502762442
scores = [0.0, 0.009956893934794486, 0.0033088458502823492, 0.01879060166352986, 0.012142553579017725, 0.0, 0.0]
index = None
png

3.4 - Tags Tutorial

How to use create and manage Tags for Wallaroo models and pipelines.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Wallaroo SDK Tag Tutorial

The following tutorial demonstrates how to use Wallaroo Tags. Tags are applied to either model versions or pipelines. This allows organizations to track different versions of models, and search for what pipelines have been used for specific purposes such as testing versus production use.

The following will be demonstrated:

  • List all tags in a Wallaroo instance.
  • List all tags applied to a model.
  • List all tags applied to a pipeline.
  • Apply a tag to a model.
  • Remove a tag from a model.
  • Apply a tag to a pipeline.
  • Remove a tag from a pipeline.
  • Search for a model version by a tag.
  • Search for a pipeline by a tag.

This demonstration provides the following through the Wallaroo Tutorials Github Repository:

Steps

The following steps are performed use to connect to a Wallaroo instance and demonstrate how to use tags with models and pipelines.

Load Libraries

The first step is to load the libraries used to connect and use a Wallaroo instance.

import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo

The following command is used to connect to a Wallaroo instance from within a Wallaroo Jupyter Hub service. For more information on connecting to a Wallaroo instance, see the Wallaroo SDK Guides.

# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Set Variables

The following variables are used to create or connect to existing workspace and pipeline. The model name and model file are set as well. Adjust as required for your organization’s needs.

The methods get_workspace and get_pipeline are used to either create a new workspace and pipeline based on the variables below, or connect to an existing workspace and pipeline with the same name. Once complete, the workspace will be set as the current workspace where pipelines and models are used.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

import string
import random

# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

workspace_name = f'{prefix}tagtestworkspace'
pipeline_name = f'{prefix}tagtestpipeline'
model_name = f'{prefix}tagtestmodel'
model_file_name = './models/ccfraud.onnx'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)
{'name': 'efxvtagtestworkspace', 'id': 15, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T18:02:16.272068+00:00', 'models': [], 'pipelines': []}

Upload Model and Create Pipeline

The tagtest_model and tagtest_pipeline will be created (or connected if already existing) based on the variables set earlier.

tagtest_model = wl.upload_model(model_name, model_file_name).configure()
tagtest_model
{'name': 'efxvtagtestmodel', 'version': '254a2888-0c8b-4172-97c3-c3547bbe6644', 'file_name': 'ccfraud.onnx', 'image_path': None, 'last_update_time': datetime.datetime(2023, 2, 27, 18, 2, 19, 155698, tzinfo=tzutc())}
tagtest_pipeline = get_pipeline(pipeline_name)
tagtest_pipeline
name efxvtagtestpipeline
created 2023-02-27 18:02:20.861896+00:00
last_updated 2023-02-27 18:02:20.861896+00:00
deployed (none)
tags
versions 461dd376-e42e-4479-9de1-7a253c1c5197
steps

List Pipeline and Model Tags

This tutorial assumes that no tags are currently existing, but that can be verified through the Wallaroo client list_pipelines and list_models commands. For this demonstration, it is recommended to use unique tags to verify each example.

wl.list_pipelines()
namecreatedlast_updateddeployedtagsversionssteps
efxvtagtestpipeline2023-27-Feb 18:02:202023-27-Feb 18:02:20(unknown)461dd376-e42e-4479-9de1-7a253c1c5197
urldemopipeline2023-27-Feb 17:55:122023-27-Feb 17:59:03False6db21694-9e11-42cb-914c-1528549cedca, 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44urldemomodel
mlbaedgepipelineexample2023-27-Feb 17:40:152023-27-Feb 17:44:15Falseb97189b0-7782-441a-84b0-2b2ed2fbf36b, 9b46e1e8-a40e-4a2d-a5f1-f2cef2ad57e9, f2aa4340-7495-4b72-b28c-98362eb72399mlbaalohamodel
azwsedgepipelineexample2023-27-Feb 17:37:372023-27-Feb 17:37:38Falsed8e4fce3-590c-46d5-871e-96bb1b0288c6, 93b18cbc-d951-43ba-9228-ef2e1add98ccazwsalohamodel
ggwzhotswappipeline2023-27-Feb 17:33:532023-27-Feb 17:34:11Falsea620354f-291e-4a98-b5f7-9d8bf165b1df, 3078dffa-4e10-41ef-85bc-e7a0de5afa82, ad943ff6-1a38-4304-a243-6958ba118df2ggwzccfraudoriginal
uuzmhotswappipeline2023-27-Feb 17:28:012023-27-Feb 17:32:15False869fd391-c562-4c51-b38a-07003d252e62, d6afc451-404b-4785-8ba0-28f0ba833f0b, 2d14a5bf-9aaf-4020-b385-9b69805f5c3c, 2b61eea9-cb7c-43c3-9ff3-2507cade98a1uuzmccfraudoriginal
beticcfraudpipeline2023-27-Feb 17:23:372023-27-Feb 17:23:38False118aefc4-b71e-4b51-84bd-85e31dbcb44a, 77463125-631b-427d-a60a-bff6d1a09eedbeticcfraudmodel
jnhcccfraudpipeline2023-27-Feb 17:19:342023-27-Feb 17:19:36Falsea5e2db56-5ac5-49b5-9842-60b6dfe2980c, 7d50b378-e093-49dc-9458-74f439c0894djnhcccfraudmodel
wl.list_models()
Name # of Versions Owner ID Last Updated Created At
efxvtagtestmodel 1 "" 2023-02-27 18:02:19.155698+00:00 2023-02-27 18:02:19.155698+00:00

Create Tag

Tags are created with the Wallaroo client command create_tag(String tagname). This creates the tag and makes it available for use.

The tag will be saved to the variable currentTag to be used in the rest of these examples.

# Now we create our tag
currentTag = wl.create_tag("My Great Tag")

List Tags

Tags are listed with the Wallaroo client command list_tags(), which shows all tags and what models and pipelines they have been assigned to. Note that if a tag has not been assigned, it will not be displayed.

# List all tags

wl.list_tags()

(no tags)

Assign Tag to a Model

Tags are assigned to a model through the Wallaroo Tag add_to_model(model_id) command, where model_id is the model’s numerical ID number. The tag is applied to the most current version of the model.

For this example, the currentTag will be applied to the tagtest_model. All tags will then be listed to show it has been assigned to this model.

# add tag to model

currentTag.add_to_model(tagtest_model.id())
{'model_id': 10, 'tag_id': 1}
# list all tags to verify

wl.list_tags()
idtagmodelspipelines
1My Great Tag[('efxvtagtestmodel', ['254a2888-0c8b-4172-97c3-c3547bbe6644'])][]

Search Models by Tag

Model versions can be searched via tags using the Wallaroo Client method search_models(search_term), where search_term is a string value. All models versions containing the tag will be displayed. In this example, we will be using the text from our tag to list all models that have the text from currentTag in them.

# Search models by tag

wl.search_models('My Great Tag')
nameversionfile_nameimage_pathlast_update_time
efxvtagtestmodel 254a2888-0c8b-4172-97c3-c3547bbe6644 ccfraud.onnx None 2023-02-27 18:02:19.155698+00:00

Remove Tag from Model

Tags are removed from models using the Wallaroo Tag remove_from_model(model_id) command.

In this example, the currentTag will be removed from tagtest_model. A list of all tags will be shown with the list_tags command, followed by searching the models for the tag to verify it has been removed.

### remove tag from model

currentTag.remove_from_model(tagtest_model.id())
{'model_id': 10, 'tag_id': 1}
# list all tags to verify it has been removed from `tagtest_model`.

wl.list_tags()

(no tags)

# search models for currentTag to verify it has been removed from `tagtest_model`.

wl.search_models('My Great Tag')

(no model versions)

Add Tag to Pipeline

Tags are added to a pipeline through the Wallaroo Tag add_to_pipeline(pipeline_id) method, where pipeline_id is the pipeline’s integer id.

For this example, we will add currentTag to testtest_pipeline, then verify it has been added through the list_tags command and list_pipelines command.

# add this tag to the pipeline
currentTag.add_to_pipeline(tagtest_pipeline.id())
{'pipeline_pk_id': 20, 'tag_pk_id': 1}
# list tags to verify it was added to tagtest_pipeline

wl.list_tags()
idtagmodelspipelines
1My Great Tag[][('efxvtagtestpipeline', ['461dd376-e42e-4479-9de1-7a253c1c5197'])]
# get all of the pipelines to show the tag was added to tagtest-pipeline

wl.list_pipelines()
namecreatedlast_updateddeployedtagsversionssteps
efxvtagtestpipeline2023-27-Feb 18:02:202023-27-Feb 18:02:20(unknown)My Great Tag461dd376-e42e-4479-9de1-7a253c1c5197
urldemopipeline2023-27-Feb 17:55:122023-27-Feb 17:59:03False6db21694-9e11-42cb-914c-1528549cedca, 930fe54d-9503-4768-8bf9-499f72272098, 54158104-c71d-4980-a6a3-25564c909b44urldemomodel
mlbaedgepipelineexample2023-27-Feb 17:40:152023-27-Feb 17:44:15Falseb97189b0-7782-441a-84b0-2b2ed2fbf36b, 9b46e1e8-a40e-4a2d-a5f1-f2cef2ad57e9, f2aa4340-7495-4b72-b28c-98362eb72399mlbaalohamodel
azwsedgepipelineexample2023-27-Feb 17:37:372023-27-Feb 17:37:38Falsed8e4fce3-590c-46d5-871e-96bb1b0288c6, 93b18cbc-d951-43ba-9228-ef2e1add98ccazwsalohamodel
ggwzhotswappipeline2023-27-Feb 17:33:532023-27-Feb 17:34:11Falsea620354f-291e-4a98-b5f7-9d8bf165b1df, 3078dffa-4e10-41ef-85bc-e7a0de5afa82, ad943ff6-1a38-4304-a243-6958ba118df2ggwzccfraudoriginal
uuzmhotswappipeline2023-27-Feb 17:28:012023-27-Feb 17:32:15False869fd391-c562-4c51-b38a-07003d252e62, d6afc451-404b-4785-8ba0-28f0ba833f0b, 2d14a5bf-9aaf-4020-b385-9b69805f5c3c, 2b61eea9-cb7c-43c3-9ff3-2507cade98a1uuzmccfraudoriginal
beticcfraudpipeline2023-27-Feb 17:23:372023-27-Feb 17:23:38False118aefc4-b71e-4b51-84bd-85e31dbcb44a, 77463125-631b-427d-a60a-bff6d1a09eedbeticcfraudmodel
jnhcccfraudpipeline2023-27-Feb 17:19:342023-27-Feb 17:19:36Falsea5e2db56-5ac5-49b5-9842-60b6dfe2980c, 7d50b378-e093-49dc-9458-74f439c0894djnhcccfraudmodel

Search Pipelines by Tag

Pipelines can be searched through the Wallaroo Client search_pipelines(search_term) method, where search_term is a string value for tags assigned to the pipelines.

In this example, the text “My Great Tag” that corresponds to currentTag will be searched for and displayed.

wl.search_pipelines('My Great Tag')
nameversioncreation_timelast_updated_timedeployedtagssteps
efxvtagtestpipeline461dd376-e42e-4479-9de1-7a253c1c51972023-27-Feb 18:02:202023-27-Feb 18:02:20(unknown)My Great Tag

Remove Tag from Pipeline

Tags are removed from a pipeline with the Wallaroo Tag remove_from_pipeline(pipeline_id) command, where pipeline_id is the integer value of the pipeline’s id.

For this example, currentTag will be removed from tagtest_pipeline. This will be verified through the list_tags and search_pipelines command.

## remove from pipeline
currentTag.remove_from_pipeline(tagtest_pipeline.id())
{'pipeline_pk_id': 20, 'tag_pk_id': 1}
wl.list_tags()

(no tags)

## Verify it was removed
wl.search_pipelines('My Great Tag')

(no pipelines)

3.5 - Simulated Edge Tutorial

The Shadow Deployment Tutorial demonstrates how restrain resources for pipelines to operate in an edge environment.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Simulated Edge Demo

This notebook will explore “Edge ML”, meaning deploying a model intended to be run on “the edge”. What is “the edge”? This is typically defined as a resource (CPU, memory, and/or bandwidth) constrained environment or where a combination of latency requirements and bandwidth available requires the models to run locally.

Wallaroo provides two key capabilities when it comes to deploying models to edge devices:

  1. Since the same engine is used in both environments, the model behavior can often be simulated accurately using Wallaroo in a data center for testing prior to deployment.
  2. Wallaroo makes edge deployments “observable” so the same tools used to monitor model performance can be used in both kinds of deployments.

This notebook closely parallels the Aloha tutorial. The primary difference is instead of provide ample resources to a pipeline to allow high-throughput operation we will specify a resource budget matching what is expected in the final deployment. Then we can apply the expected load to the model and observe how it behaves given the available resources.

This example uses the open source Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution. This could be deployed on a network router to detect suspicious domains in real-time. Of course, it is important to monitor the behavior of the model across all of the deployments so we can see if the detect rate starts to drift over time.

Note that this example is not intended for production use and is meant of an example of running Wallaroo in a restrained environment. The environment is based on the [Wallaroo AWS EC2 Setup guide](https://docs.wallaroo.ai/wallaroo-operations-guide/wallaroo-install-guides/YOUR SUFFIX-install-guides/wallaroo-setup-environment-community/wallaroo-aws-vm-community-setup/).

Full details on how to configure a deployment through the SDK, see the Wallaroo SDK guides.

For our example, we will perform the following:

  • Create a workspace for our work.
  • Upload the Aloha model.
  • Define a resource budget for our inference pipeline.
  • Create a pipeline that can ingest our submitted data, submit it to the model, and export the results
  • Run a sample inference through our pipeline by loading a file
  • Run a batch inference through our pipeline’s URL and store the results in a file and find that the original memory allocation is too small.
  • Redeploy the pipeline with a larger memory budget and attempt sending the same batch of requests through again.

All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.

Open a Connection to Wallaroo

The first step is to connect to Wallaroo through the Wallaroo client. The Python library is included in the Wallaroo install and available through the Jupyter Hub interface provided with your Wallaroo environment.

This is accomplished using the wallaroo.Client() command, which provides a URL to grant the SDK permission to your specific Wallaroo environment. When displayed, enter the URL into a browser and confirm permissions. Store the connection into a variable that can be referenced later.

If logging into the Wallaroo instance through the internal JupyterHub service, use wl = wallaroo.Client(). If logging in externally, update the wallarooPrefix and wallarooSuffix variables with the proper DNS information. For more information on Wallaroo DNS settings, see the Wallaroo DNS Integration Guide.

import wallaroo
from wallaroo.object import EntityNotFoundError
import pandas as pd
import json
from IPython.display import display

# used to display dataframe information without truncating
pd.set_option('display.max_colwidth', None)
# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Useful variables

The following variables and methods are used to create a workspace, the pipeline in the example workspace and upload models into it.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

import string
import random

# make a random 4 character prefix
prefix= ''.join(random.choice(string.ascii_lowercase) for i in range(4))

pipeline_name = f'{prefix}edgepipelineexample'
workspace_name = f'{prefix}edgeworkspaceexample'
model_name = f'{prefix}alohamodel'
model_file_name = './alohacnnlstm.zip'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Create or Set the Workspace

Create the workspace and set it as our default workspace. If a workspace by the same name already exists, then that workspace will be used.

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)
workspace
{'name': 'wjtxedgeworkspaceexample', 'id': 86, 'archived': False, 'created_by': '138bd7e6-4dc8-4dc1-a760-c9e721ef3c37', 'created_at': '2023-02-27T17:46:51.007989+00:00', 'models': [], 'pipelines': []}

Upload the Models

Now we will upload our models. Note that for this example we are applying the model from a .ZIP file. The Aloha model is a protobuf file that has been defined for evaluating web pages, and we will configure it to use data in the tensorflow format.

model = wl.upload_model(model_name, model_file_name).configure("tensorflow")

Define the resource budget

The DeploymentConfig object specifies the resources to allocate for a model pipeline. In this case, we’re going to set a very small budget, one that is too small for this model and then expand it based on testing. To start with, we’ll use 1 CPU and 150 MB of RAM.

deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("150Mi").build()

Deploy a model

Now that we have a model that we want to use we will create a deployment for it using the resource limits defined above.

We will tell the deployment we are using a tensorflow model and give the deployment name and the configuration we want for the deployment.

To do this, we’ll create our pipeline that can ingest the data, pass the data to our Aloha model, and give us a final output. We’ll call our pipeline edgepipeline, then deploy it so it’s ready to receive data. The deployment process usually takes about 45 seconds.

  • Note: If you receive an error that the pipeline could not be deployed because there are not enough resources, undeploy any other pipelines and deploy this one again. This command can quickly undeploy all pipelines to regain resources. We recommend not running this command in a production environment since it will cancel any running pipelines:
for p in wl.list_pipelines(): p.undeploy()
pipeline = get_pipeline(pipeline_name)
pipeline.add_model_step(model)
pipeline.deploy(deployment_config=deployment_config)
name wjtxedgepipelineexample
created 2023-02-27 17:46:53.711612+00:00
last_updated 2023-02-27 17:46:54.419854+00:00
deployed True
tags
versions e33295e1-fa57-4709-a5ee-23cdd2b66131, 1fc9d0e7-6783-4050-9b86-6f0276fb8745
steps wjtxalohamodel

We can verify that the pipeline is running and list what models are associated with it.

pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.48.0.146',
   'name': 'engine-77958c5d4-5zj9x',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'wjtxedgepipelineexample',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'wjtxalohamodel',
      'version': 'f1e3cbc4-b58a-4c3c-95fb-8e46803115b6',
      'sha': 'd71d9ffc61aaac58c2b1ed70a2db13d1416fb9d3f5b891e5e4e2e97180fe22f8',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.48.0.148',
   'name': 'engine-lb-74b4969486-227j2',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Inferences

Infer 1 row

Now that the pipeline is deployed and our model is in place, we’ll perform a smoke test to verify the pipeline is up and running properly. We’ll use the infer_from_file command to load a single encoded URL into the inference engine and print the results back out.

The result should tell us that the tokenized URL is legitimate (0) or fraud (1). This sample data should return close to 0.

if arrowEnabled is True:
    result = pipeline.infer_from_file('./data/data_1.df.json')
else:
    result = pipeline.infer_from_file("./data/data_1.json")
display(result)
time in.text_input out.qakbot out.banjori out.dircrypt out.corebot out.suppobox out.cryptolocker out.simda out.ramnit out.main out.matsnu out.kraken out.ramdo out.gozi out.locky out.pykspa check_failures
0 2023-02-27 17:47:15.407 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 16, 32, 23, 29, 32, 30, 19, 26, 17] [0.016155045] [0.0015195842] [4.7591206e-05] [0.98291475] [1.3889844e-27] [0.012099553] [1.7933435e-26] [0.0009985747] [0.997564] [0.010341614] [0.00031977228] [0.0062362333] [2.0289332e-05] [0.011029261] [0.008038961] 0
  • IMPORTANT NOTE: The _deployment._url() method will return an internal URL when using Python commands from within the Wallaroo instance - for example, the Wallaroo JupyterHub service. When connecting via an external connection, _deployment._url() returns an external URL. External URL connections requires the authentication be included in the HTTP request, and that Model Endpoints Guide external endpoints are enabled in the Wallaroo configuration options.
inference_url = pipeline._deployment._url()
print(inference_url)
connection =wl.mlops().__dict__
token = connection['token']
https://sparkly-apple-3026.api.wallaroo.community/v1/api/pipelines/infer/wjtxedgepipelineexample-106
if arrowEnabled is True:
    dataFile="./data/data_1k.df.json"
    contentType="application/json; format=pandas-records"
else:
    dataFile="./data/data_1k.json"
    contentType="application/json"
!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data @{dataFile} > curl_response.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  735k  100    95  100  735k     52   408k  0:00:01  0:00:01 --:--:--  409k

Redeploy with a little larger budget

If you look in the file curl_response.txt, you will see that the inference failed:

upstream connect error or disconnect/reset before headers. reset reason: connection termination

Even though a single inference passed, submitted a larger batch of work did not. If this is an expected usage case for this model, we need to add more memory. Let’s do that now.

The following DeploymentConfig is the same as the original, but increases the memory from 150MB to 300MB. This sort of budget would be available on some network routers.

pipeline.undeploy()
deployment_config = wallaroo.DeploymentConfigBuilder().replica_count(1).cpus(1).memory("300Mi").build()
pipeline.deploy(deployment_config=deployment_config)
name wjtxedgepipelineexample
created 2023-02-27 17:46:53.711612+00:00
last_updated 2023-02-27 17:47:57.057785+00:00
deployed True
tags
versions 2ebda792-0d70-459b-b1cf-0348abcd6a06, e33295e1-fa57-4709-a5ee-23cdd2b66131, 1fc9d0e7-6783-4050-9b86-6f0276fb8745
steps wjtxalohamodel

Re-run inference

Running the same curl command again should now produce a curl_response.txt file containing the expected results.

!curl -X POST {inference_url} -H "Authorization: Bearer {token}" -H "Content-Type:{contentType}" --data @{dataFile} > curl_response.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1402k  100  666k  100  735k   172k   190k  0:00:03  0:00:03 --:--:--  362k

It is important to note that increasing the memory was necessary to run a batch of 1,000 inferences at once. If this is not a design use case for your system, running with the smaller memory budget may be acceptable. Wallaroo allows you to easily test difference loading patterns to get a sense for what resources are required with sufficient buffer to allow for robust operation of your system while not over-provisioning scarce resources.

Undeploy Pipeline

When finished with our tests, we will undeploy the pipeline so we have the Kubernetes resources back for other tasks. Note that if the deployment variable is unchanged aloha_pipeline.deploy() will restart the inference engine in the same configuration as before.

pipeline.undeploy()
name wjtxedgepipelineexample
created 2023-02-27 17:46:53.711612+00:00
last_updated 2023-02-27 17:47:57.057785+00:00
deployed False
tags
versions 2ebda792-0d70-459b-b1cf-0348abcd6a06, e33295e1-fa57-4709-a5ee-23cdd2b66131, 1fc9d0e7-6783-4050-9b86-6f0276fb8745
steps wjtxalohamodel

4 - Using Jupyter Notebooks in Production

How to go from Jupyter Notebooks to Production Systems

Using Jupyter Notebooks in Production

The following tutorials provide an example of an organization moving from experimentation to deployment in production using Jupyter Notebooks as the basis for code research and use. For this example, we can assume to main actors performing the following tasks.

Number Notebook Sample Task Actor Description
01 01_explore_and_train.ipynb Data Exploration and Model Selection Data Scientist The data scientist evaluates the data and determines the best model to use to solve the proposed problems.
02 02_automated_training_process.ipynd Training Process Automation Setup Data Scientist The data scientist has selected the model and tested how to train it. In this phase, the data scientist tests automating the training process based on a data store.
03 03_deploy_model.ipynb Deploy the Model in Wallaroo MLOps Engineer The MLOps takes the trained model and deploys a Wallaroo pipeline with it to perform inferences on by feeding it data from a data store.
04 04_regular_batch_inferences.ipynb Regular Batch Inference MLOps Engineer With the pipeline deployed, regular inferences can be made and the results reported to a data store.

Each Jupyter Notebook is arranged to demonstrate each step of the process.

Resources

The following resources are provided as part of this tutorial:

  • data
    • data/seattle_housing_col_description.txt: Describes the columns used as part data analysis.
    • data/seattle_housing.csv: Sample data of the Seattle, Washington housing market between 2014 and 2015.
  • code
    • postprocess.py: Formats the data after inference by the model is complete.
    • preprocess.py: Formats the incoming data for the model.
    • simdb.py: A simulated database to demonstrate sending and receiving queries.
    • wallaroo_client.py: Additional methods used with the Wallaroo instance to create workspaces, etc.

4.1 - Data Exploration And Model Selection

For more details on this tutorial’s setup and process, see 00_Introduction.ipynb.

Stage 1: Data Exploration And Model Selection

When starting a project, the data scientist focuses on exploration and experimentation, rather than turning the process into an immediate production system. This notebook presents a simplified view of this stage.

Resources

The following resources are used as part of this tutorial:

  • data
    • data/seattle_housing_col_description.txt: Describes the columns used as part data analysis.
    • data/seattle_housing.csv: Sample data of the Seattle, Washington housing market between 2014 and 2015.
  • code
    • postprocess.py: Formats the data after inference by the model is complete.
    • preprocess.py: Formats the incoming data for the model.
    • simdb.py: A simulated database to demonstrate sending and receiving queries.
    • wallaroo_client.py: Additional methods used with the Wallaroo instance to create workspaces, etc.

Steps

The following steps are part of this process:

Import Libraries

First we’ll import the libraries we’ll be using to evaluate the data and test different models.

import numpy as np
import pandas as pd

import sklearn
import sklearn.ensemble

import xgboost as xgb

import seaborn
import matplotlib
import matplotlib.pyplot as plt

import simdb # module for the purpose of this demo to simulate pulling data from a database

matplotlib.rcParams["figure.figsize"] = (12,6)

Retrieve Training Data

For training, we will use the data on all houses sold in this market with the last two years. As a reminder, this data pulled from a simulated database as an example of how to pull from an existing data store.

conn = simdb.simulate_db_connection()
tablename = simdb.tablename

query = f"select * from {tablename} where date > DATE(DATE(), '-24 month') AND sale_price is not NULL"
print(query)
# read in the data
housing_data = pd.read_sql_query(query, conn)

conn.close()
housing_data
select * from house_listings where date > DATE(DATE(), '-24 month') AND sale_price is not NULL
id date list_price bedrooms bathrooms sqft_living sqft_lot floors waterfront view
0 7129300520 2022-07-11 221900.0 3 1.00 1180 5650 1.0 0 0
1 6414100192 2022-09-06 538000.0 3 2.25 2570 7242 2.0 0 0
2 5631500400 2022-11-23 180000.0 2 1.00 770 10000 1.0 0 0
3 2487200875 2022-09-06 604000.0 4 3.00 1960 5000 1.0 0 0
4 1954400510 2022-11-16 510000.0 3 2.00 1680 8080 1.0 0 0
20518 263000018 2022-02-16 360000.0 3 2.50 1530 1131 3.0 0 0
20519 6600060120 2022-11-21 400000.0 4 2.50 2310 5813 2.0 0 0
20520 1523300141 2022-03-21 402101.0 2 0.75 1020 1350 2.0 0 0
20521 291310100 2022-10-14 400000.0 3 2.50 1600 2388 2.0 0 0
20522 1523300157 2022-07-13 325000.0 2 0.75 1020 1076 2.0 0 0

20523 rows × 22 columns

Data transformations

To improve relative error performance, we will predict on log10 of the sale price.

Predict on log10 price to try to improve relative error performance

housing_data['logprice'] = np.log10(housing_data.sale_price)

From the data, we will create the following features to evaluate:

  • house_age: How old the house is.
  • renovated: Whether the house has been renovated or not.
  • yrs_since_reno: If the house has been renovated, how long has it been.
import datetime

thisyear = datetime.datetime.now().year

housing_data['house_age'] = thisyear - housing_data['yr_built']
housing_data['renovated'] =  np.where((housing_data['yr_renovated'] > 0), 1, 0) 
housing_data['yrs_since_reno'] =  np.where(housing_data['renovated'], housing_data['yr_renovated'] - housing_data['yr_built'], 0)

housing_data.loc[:, ['yr_built', 'yr_renovated', 'house_age', 'renovated', 'yrs_since_reno']]
yr_built yr_renovated house_age renovated yrs_since_reno
0 1955 0 68 0 0
1 1951 1991 72 1 40
2 1933 0 90 0 0
3 1965 0 58 0 0
4 1987 0 36 0 0
... ... ... ... ... ...
20518 2009 0 14 0 0
20519 2014 0 9 0 0
20520 2009 0 14 0 0
20521 2004 0 19 0 0
20522 2008 0 15 0 0

20523 rows × 5 columns

Now we pick variables and split training data into training and holdout (test).

vars = ['bedrooms', 'bathrooms', 'sqft_living', 'sqft_lot', 'floors', 'waterfront', 'view',
'condition', 'grade', 'sqft_above', 'sqft_basement', 'lat', 'long', 'sqft_living15', 'sqft_lot15', 'house_age', 'renovated', 'yrs_since_reno']

outcome = 'logprice'

runif = np.random.default_rng(2206222).uniform(0, 1, housing_data.shape[0])
gp = np.where(runif < 0.2, 'test', 'training')

hd_train = housing_data.loc[gp=='training', :].reset_index(drop=True, inplace=False)
hd_test = housing_data.loc[gp=='test', :].reset_index(drop=True, inplace=False)

# split the training into training and val for xgboost
runif = np.random.default_rng(123).uniform(0, 1, hd_train.shape[0])
xgb_gp = np.where(runif < 0.2, 'val', 'train')
# for xgboost, further split into train and val
train_features = np.array(hd_train.loc[xgb_gp=='train', vars])
train_labels = np.array(hd_train.loc[xgb_gp=='train', outcome])

val_features = np.array(hd_train.loc[xgb_gp=='val', vars])
val_labels = np.array(hd_train.loc[xgb_gp=='val', outcome])

Postprocessing

Since we are fitting a model to predict log10 price, we need to convert predictions back into price units. We also want to round to the nearest dollar.

def postprocess(log10price):
    return np.rint(np.power(10, log10price))

Model testing

For the purposes of this demo, let’s say that we require a mean absolute percent error (MAPE) of 15% or less, and the we want to try a few models to decide which model we want to use.

One could also hyperparameter tune at this stage; for brevity, we’ll omit that in this demo.

XGBoost

First we will test out using a XGBoost model.


xgb_model = xgb.XGBRegressor(
    objective = 'reg:squarederror', 
    max_depth=5, 
    base_score = np.mean(hd_train[outcome])
    )

xgb_model.fit( 
    train_features,
    train_labels,
    eval_set=[(train_features, train_labels), (val_features, val_labels)],
    verbose=False,
    early_stopping_rounds=35
)
print(xgb_model.best_score)
print(xgb_model.best_iteration)
print(xgb_model.best_ntree_limit)
0.07793614689092423
99
100

XGBoost Evaluate on holdout

With the sample model created, we will test it against the holdout data. Note that we are calling the postprocess function on the data.

test_features = np.array(hd_test.loc[:, vars])
test_labels = np.array(hd_test.loc[:, outcome])

pframe = pd.DataFrame({
    'pred' : postprocess(xgb_model.predict(test_features)),
    'actual' : postprocess(test_labels)
})

ax = seaborn.scatterplot(
    data=pframe,
    x='pred',
    y='actual',
    alpha=0.2
)
matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen')
matplotlib.pyplot.title("test")
plt.show()
png
pframe['se'] = (pframe.pred - pframe.actual)**2

pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual
pframe.describe()
pred actual se pct_err
count 4.094000e+03 4.094000e+03 4.094000e+03 4094.000000
mean 5.340824e+05 5.396937e+05 1.657722e+10 12.857674
std 3.413714e+05 3.761666e+05 1.276017e+11 13.512028
min 1.216140e+05 8.200000e+04 1.000000e+00 0.000500
25% 3.167628e+05 3.200000e+05 3.245312e+08 4.252492
50% 4.568700e+05 4.500000e+05 1.602001e+09 9.101485
75% 6.310372e+05 6.355250e+05 6.575385e+09 17.041227
max 5.126706e+06 7.700000e+06 6.637466e+12 252.097895
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)

print(f'rmse = {rmse}, mape = {mape}')
rmse = 128752.54982046234, mape = 12.857674005250548

Random Forest

The next model to test is Random Forest.

model_rf = sklearn.ensemble.RandomForestRegressor(n_estimators=100, max_depth=5, n_jobs=2, max_samples=0.8)

train_features = np.array(hd_train.loc[:, vars])
train_labels = np.array(hd_train.loc[:, outcome])

model_rf.fit(train_features, train_labels)

Random Forest Evaluate on holdout

With the Random Forest sample model created, now we can test it against the holdout data.

pframe = pd.DataFrame({
    'pred' : postprocess(model_rf.predict(test_features)),
    'actual' : postprocess(test_labels)
})

ax = seaborn.scatterplot(
    data=pframe,
    x='pred',
    y='actual',
    alpha=0.2
)
matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen')
matplotlib.pyplot.title("random forest")
plt.show()
png
pframe['se'] = (pframe.pred - pframe.actual)**2

pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual
pframe.describe()
pred actual se pct_err
count 4.094000e+03 4.094000e+03 4.094000e+03 4094.000000
mean 5.196958e+05 5.396937e+05 3.881397e+10 18.206029
std 2.801499e+05 3.761666e+05 4.100412e+11 17.623817
min 2.039910e+05 8.200000e+04 7.290000e+02 0.006353
25% 3.295885e+05 3.200000e+05 6.780296e+08 6.115427
50% 4.644340e+05 4.500000e+05 3.304756e+09 13.227328
75% 5.911275e+05 6.355250e+05 1.380760e+10 24.721135
max 2.854540e+06 7.700000e+06 2.347848e+13 175.680235
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)

print(f'rmse = {rmse}, mape = {mape}')
rmse = 197012.60307486894, mape = 18.206029369592454

Final Decision

At this stage, we decide to go with the xgboost model, with the variables/settings above.

With this stage complete, we can move on to Stage 2: Training Process Automation Setup.

4.2 - From Jupyter to Production

How to go from Jupyter Notebooks to Production Systems

For more details on this tutorial’s setup and process, see 00_Introduction.ipynb.

Stage 2: Training Process Automation Setup

Now that we have decided on the type and structure of the model from Stage 1: Data Exploration And Model Selection, this notebook modularizes the various steps of the process in a structure that is compatible with production and with Wallaroo.

We have pulled the preprocessing and postprocessing steps out of the training notebook into individual scripts that can also be used when the model is deployed.

Assuming no changes are made to the structure of the model, this notebook, or a script based on this notebook, can then be scheduled to run on a regular basis, to refresh the model with more recent training data. We’d expect to run this notebook in conjunction with the Stage 3 notebook, 03_deploy_model.ipynb. For clarity in this demo, we have split the training/upload task into two notebooks, 02_automated_training_process.ipynb and 03_deploy_model.ipynb.

Resources

The following resources are used as part of this tutorial:

  • data
    • data/seattle_housing_col_description.txt: Describes the columns used as part data analysis.
    • data/seattle_housing.csv: Sample data of the Seattle, Washington housing market between 2014 and 2015.
  • code
    • postprocess.py: Formats the data after inference by the model is complete.
    • preprocess.py: Formats the incoming data for the model.
    • simdb.py: A simulated database to demonstrate sending and receiving queries.
    • wallaroo_client.py: Additional methods used with the Wallaroo instance to create workspaces, etc.

Steps

The following steps are part of this process:

Retrieve Training Data

Note that this connection is simulated to demonstrate how data would be retrieved from an existing data store. For training, we will use the data on all houses sold in this market with the last two years.

import numpy as np
import pandas as pd

import sklearn

import xgboost as xgb

import seaborn
import matplotlib
import matplotlib.pyplot as plt

import pickle

import simdb # module for the purpose of this demo to simulate pulling data from a database

from preprocess import create_features  # our custom preprocessing
from postprocess import postprocess    # our custom postprocessing

matplotlib.rcParams["figure.figsize"] = (12,6)
conn = simdb.simulate_db_connection()
tablename = simdb.tablename

query = f"select * from {tablename} where date > DATE(DATE(), '-24 month') AND sale_price is not NULL"
print(query)
# read in the data
housing_data = pd.read_sql_query(query, conn)

conn.close()
housing_data
select * from house_listings where date > DATE(DATE(), '-24 month') AND sale_price is not NULL
id date list_price bedrooms bathrooms sqft_living sqft_lot floors waterfront view
0 7129300520 2022-07-11 221900.0 3 1.00 1180 5650 1.0 0 0
1 6414100192 2022-09-06 538000.0 3 2.25 2570 7242 2.0 0 0
2 5631500400 2022-11-23 180000.0 2 1.00 770 10000 1.0 0 0
3 2487200875 2022-09-06 604000.0 4 3.00 1960 5000 1.0 0 0
4 1954400510 2022-11-16 510000.0 3 2.00 1680 8080 1.0 0 0
20518 263000018 2022-02-16 360000.0 3 2.50 1530 1131 3.0 0 0
20519 6600060120 2022-11-21 400000.0 4 2.50 2310 5813 2.0 0 0
20520 1523300141 2022-03-21 402101.0 2 0.75 1020 1350 2.0 0 0
20521 291310100 2022-10-14 400000.0 3 2.50 1600 2388 2.0 0 0
20522 1523300157 2022-07-13 325000.0 2 0.75 1020 1076 2.0 0 0

20523 rows × 22 columns

Data transformations

To improve relative error performance, we will predict on log10 of the sale price.

Predict on log10 price to try to improve relative error performance

housing_data['logprice'] = np.log10(housing_data.list_price)
# split data into training and test
outcome = 'logprice'

runif = np.random.default_rng(2206222).uniform(0, 1, housing_data.shape[0])
gp = np.where(runif < 0.2, 'test', 'training')

hd_train = housing_data.loc[gp=='training', :].reset_index(drop=True, inplace=False)
hd_test = housing_data.loc[gp=='test', :].reset_index(drop=True, inplace=False)

# split the training into training and val for xgboost
runif = np.random.default_rng(123).uniform(0, 1, hd_train.shape[0])
xgb_gp = np.where(runif < 0.2, 'val', 'train')
# for xgboost
train_features = hd_train.loc[xgb_gp=='train', :].reset_index(drop=True, inplace=False)
train_features = np.array(create_features(train_features))
train_labels = np.array(hd_train.loc[xgb_gp=='train', outcome])

val_features = hd_train.loc[xgb_gp=='val', :].reset_index(drop=True, inplace=False)
val_features = np.array(create_features(val_features))
val_labels = np.array(hd_train.loc[xgb_gp=='val', outcome])

print(f'train_features: {train_features.shape}, train_labels: {len(train_labels)}')
print(f'val_features: {val_features.shape}, val_labels: {len(val_labels)}')
train_features: (13129, 18), train_labels: 13129
val_features: (3300, 18), val_labels: 3300

Generate and Test the Model

Based on the experimentation and testing performed in Stage 1: Data Exploration And Model Selection, XGBoost was selected as the ML model and the variables for training were selected. The model will be generated and tested against sample data.


xgb_model = xgb.XGBRegressor(
    objective = 'reg:squarederror', 
    max_depth=5, 
    base_score = np.mean(hd_train[outcome])
    )

xgb_model.fit( 
    train_features,
    train_labels,
    eval_set=[(train_features, train_labels), (val_features, val_labels)],
    verbose=False,
    early_stopping_rounds=35
)
print(xgb_model.best_score)
print(xgb_model.best_iteration)
print(xgb_model.best_ntree_limit)
0.07793614689092423
99
100
test_features = np.array(create_features(hd_test.copy()))
test_labels = np.array(hd_test.loc[:, outcome])

pframe = pd.DataFrame({
    'pred' : postprocess(xgb_model.predict(test_features)),
    'actual' : postprocess(test_labels)
})

ax = seaborn.scatterplot(
    data=pframe,
    x='pred',
    y='actual',
    alpha=0.2
)
matplotlib.pyplot.plot(pframe.pred, pframe.pred, color='DarkGreen')
matplotlib.pyplot.title("test")
plt.show()
png
pframe['se'] = (pframe.pred - pframe.actual)**2

pframe['pct_err'] = 100*np.abs(pframe.pred - pframe.actual)/pframe.actual
pframe.describe()
pred actual se pct_err
count 4.094000e+03 4.094000e+03 4.094000e+03 4094.000000
mean 5.340824e+05 5.396937e+05 1.657722e+10 12.857674
std 3.413714e+05 3.761666e+05 1.276017e+11 13.512028
min 1.216140e+05 8.200000e+04 1.000000e+00 0.000500
25% 3.167628e+05 3.200000e+05 3.245312e+08 4.252492
50% 4.568700e+05 4.500000e+05 1.602001e+09 9.101485
75% 6.310372e+05 6.355250e+05 6.575385e+09 17.041227
max 5.126706e+06 7.700000e+06 6.637466e+12 252.097895
rmse = np.sqrt(np.mean(pframe.se))
mape = np.mean(pframe.pct_err)

print(f'rmse = {rmse}, mape = {mape}')
rmse = 128752.54982046234, mape = 12.857674005250548

Convert the Model to Onnx

This step converts the model to onnx for easy import into Wallaroo.

# pickle up the model
# with open('housing_model_xgb.pkl', 'wb') as f:
#    pickle.dump(xgb_model, f)
import onnx
from onnxmltools.convert import convert_xgboost

from skl2onnx.common.data_types import FloatTensorType, DoubleTensorType

import preprocess

# set the number of columns
ncols = len(preprocess._vars)

# derive the opset value

from onnx.defs import onnx_opset_version
from onnxconverter_common.onnx_ex import DEFAULT_OPSET_NUMBER
TARGET_OPSET = min(DEFAULT_OPSET_NUMBER, onnx_opset_version())
# Convert the model to onnx

onnx_model_converted = convert_xgboost(xgb_model, 'tree-based classifier',
                             [('input', FloatTensorType([None, ncols]))],
                             target_opset=TARGET_OPSET)

# Save the model

onnx.save_model(onnx_model_converted, "housing_model_xgb.onnx")

With the model trained and ready, we can now go to Stage 3: Deploy the Model in Wallaroo.

4.3 - Deploy the Model in Wallaroo

For more details on this tutorial’s setup and process, see 00_Introduction.ipynb.

Stage 3: Deploy the Model in Wallaroo

In this stage, we upload the trained model and the processing steps to Wallaroo, then set up and deploy the inference pipeline.

Once deployed we can feed the newest batch of data to the pipeline, do the inferences and write the results to a results table.

For clarity in this demo, we have split the training/upload task into two notebooks:

  • 02_automated_training_process.ipynb: Train and pickle ML model.
  • 03_deploy_model.ipynb: Upload the model to Wallaroo and deploy into a pipeline.

Assuming no changes are made to the structure of the model, these two notebooks, or a script based on them, can then be scheduled to run on a regular basis, to refresh the model with more recent training data and update the inference pipeline.

This notebook is expected to run within the Wallaroo instance’s Jupyter Hub service to provide access to all required Wallaroo libraries and functionality.

Resources

The following resources are used as part of this tutorial:

  • data
    • data/seattle_housing_col_description.txt: Describes the columns used as part data analysis.
    • data/seattle_housing.csv: Sample data of the Seattle, Washington housing market between 2014 and 2015.
  • code
    • postprocess.py: Formats the data after inference by the model is complete.
    • preprocess.py: Formats the incoming data for the model.
    • simdb.py: A simulated database to demonstrate sending and receiving queries.
    • wallaroo_client.py: Additional methods used with the Wallaroo instance to create workspaces, etc.
  • models
    • housing_model_xgb.onnx: Model created in Stage 2: Training Process Automation Setup.

Steps

The process of uploading the model to Wallaroo follows these steps:

Connect to Wallaroo

First we import the required libraries to connect to the Wallaroo instance, then connect to the Wallaroo instance.

import json
import pickle
import pandas as pd
import numpy as np

import simdb # module for the purpose of this demo to simulate pulling data from a database

from wallaroo.ModelConversion import ConvertXGBoostArgs, ModelConversionSource, ModelConversionInputType
import wallaroo
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR PREFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace_name = 'housepricing'
model_name = "housepricemodel"
model_file = "./housing_model_xgb.onnx"
pipeline_name = "housing-pipe"

The workspace housepricing will either be created or, if already existing, used and set to the current workspace.

new_workspace = get_workspace(workspace_name)
new_workspace
{'name': 'housepricing', 'id': 23, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T21:00:21.786653+00:00', 'models': [], 'pipelines': []}
_ = wl.set_current_workspace(new_workspace)

Upload The Model

With the connection set and workspace prepared, upload the model created in 02_automated_training_process.ipynb into the current workspace.

hpmodel = wl.upload_model(model_name, model_file).configure()

Upload the Processing Modules

Upload the preprocess.py and postprocess.py modules as models to be added to the pipeline.

# load the preprocess module
module_pre = wl.upload_model("preprocess", "./preprocess.py").configure('python')
# load the postprocess module
module_post = wl.upload_model("postprocess", "./postprocess.py").configure('python')

Create and Deploy the Pipeline

Create the pipeline with the preprocess module, housing model, and postprocess module as pipeline steps, then deploy the newpipeline.

pipeline = (wl.build_pipeline(pipeline_name)
              .add_model_step(module_pre)
              .add_model_step(hpmodel)
              .add_model_step(module_post)
              .deploy()
           )
pipeline
name housing-pipe
created 2023-02-27 21:00:26.107908+00:00
last_updated 2023-02-27 21:00:27.425823+00:00
deployed True
tags
versions d92c7f3d-0b61-44fa-83e2-264d8a045879, b309144d-b5b0-4ca7-a073-4f4ad4145de7
steps preprocess

Test the Pipeline

We will use a single query from the simulated housing_price table and infer. When successful, we will undeploy the pipeline to restore the resources back to the Kubernetes environment.

conn = simdb.simulate_db_connection()

# create the query
query = f"select * from {simdb.tablename} limit 1"
print(query)

# read in the data
singleton = pd.read_sql_query(query, conn)
conn.close()

singleton
select * from house_listings limit 1
id date list_price bedrooms bathrooms sqft_living sqft_lot floors waterfront view ... sqft_above sqft_basement yr_built yr_renovated zipcode lat long sqft_living15 sqft_lot15 sale_price
0 7129300520 2022-07-16 221900.0 3 1.0 1180 5650 1.0 0 0 ... 1180 0 1955 0 98178 47.5112 -122.257 1340 5650 221900.0

1 rows × 22 columns

if arrowEnabled is True:
    result = pipeline.infer({'query': singleton.to_json()})
    display(result)
else:
    result = pipeline.infer({'query': singleton.to_json()})
    # just display the output
    display(result[0].data())
[{'prediction': [224852.0]}]

When finished, we undeploy the pipeline to return the resources back to the environment.

pipeline.undeploy()
name housing-pipe
created 2023-02-27 21:00:26.107908+00:00
last_updated 2023-02-27 21:00:27.425823+00:00
deployed False
tags
versions d92c7f3d-0b61-44fa-83e2-264d8a045879, b309144d-b5b0-4ca7-a073-4f4ad4145de7
steps preprocess

With this stage complete, we can proceed to Stage 4: Regular Batch Inference.

4.4 - Regular Batch Inference

For more details on this tutorial’s setup and process, see 00_Introduction.ipynb.

Stage 4: Regular Batch Inference

In Stage 3: Deploy the Model in Wallaroo, the housing model created and tested in Stage 2: Training Process Automation Setup was uploaded to a Wallaroo instance and added to the pipeline housing-pipe in the workspace housepricing. This pipeline can be deployed at any point and time and used with new inferences.

For the purposes of this demo, let’s say that every month we find the newly entered and still-unsold houses and predict their sale price.

The predictions are entered into a staging table, for further inspection before being joined to the primary housing data table.

We show this as a notebook, but this can also be scripted and scheduled, using CRON or some other process.

Resources

The following resources are used as part of this tutorial:

  • data
    • data/seattle_housing_col_description.txt: Describes the columns used as part data analysis.
    • data/seattle_housing.csv: Sample data of the Seattle, Washington housing market between 2014 and 2015.
  • code
    • postprocess.py: Formats the data after inference by the model is complete.
    • preprocess.py: Formats the incoming data for the model.
    • simdb.py: A simulated database to demonstrate sending and receiving queries.
    • wallaroo_client.py: Additional methods used with the Wallaroo instance to create workspaces, etc.
  • models
    • housing_model_xgb.onnx: Model created in Stage 2: Training Process Automation Setup.

Steps

This process will use the following steps:

Connect to Wallaroo

Connect to the Wallaroo instance and set the housepricing workspace as the current workspace.

import json
import pickle
import wallaroo
import pandas as pd
import numpy as np

import simdb # module for the purpose of this demo to simulate pulling data from a database

from wallaroo_client import get_workspace

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR PREFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
new_workspace = get_workspace("housepricing")
_ = wl.set_current_workspace(new_workspace)

Deploy the Pipeline

Deploy the housing-pipe workspace established in Stage 3: Deploy the Model in Wallaroo (03_deploy_model.ipynb).

pipeline = wl.pipelines_by_name("housing-pipe")[-1]
pipeline.deploy()
name housing-pipe
created 2023-02-27 21:00:26.107908+00:00
last_updated 2023-02-27 21:01:46.967774+00:00
deployed True
tags
versions fbb12aa3-7fa2-4553-9955-3dc7146bcd36, d92c7f3d-0b61-44fa-83e2-264d8a045879, b309144d-b5b0-4ca7-a073-4f4ad4145de7
steps preprocess

Read In New House Listings

From the data store, load the previous month’s house listing and submit them to the deployed pipeline.

conn = simdb.simulate_db_connection()

# create the query
query = f"select * from {simdb.tablename} where date > DATE(DATE(), '-1 month') AND sale_price is NULL"
print(query)

# read in the data
newbatch = pd.read_sql_query(query, conn)
newbatch.shape
select * from house_listings where date > DATE(DATE(), '-1 month') AND sale_price is NULL

(1090, 22)

if arrowEnabled is True:
    query = {'query': newbatch.to_json()}
    result = pipeline.infer(query)
    #display(result)
    predicted_prices = result[0]['prediction']
else:
    query = {'query': newbatch.to_json()}
    result = pipeline.infer(query)[0]
    predicted_prices = result.data()[0]
len(predicted_prices)
1090

Send Predictions to Results Staging Table

Take the predicted prices based on the inference results so they can be joined into the house_listings table.

Once complete, undeploy the pipeline to return the resources back to the Kubernetes environment.

result_table = pd.DataFrame({
    'id': newbatch['id'],
    'saleprice_estimate': predicted_prices,
})

result_table.to_sql('results_table', conn, index=False, if_exists='append')
# Display the top of the table for confirmation
pd.read_sql_query("select * from results_table limit 5", conn)
id saleprice_estimate
0 9215400105 508255.0
1 1695900060 500198.0
2 9545240070 539598.0
3 1432900240 270739.0
4 6131600075 191304.0
conn.close()
pipeline.undeploy()
name housing-pipe
created 2023-02-27 21:00:26.107908+00:00
last_updated 2023-02-27 21:01:46.967774+00:00
deployed False
tags
versions fbb12aa3-7fa2-4553-9955-3dc7146bcd36, d92c7f3d-0b61-44fa-83e2-264d8a045879, b309144d-b5b0-4ca7-a073-4f4ad4145de7
steps preprocess

From here, organizations can automate this process. Other features could be used such as data analysis using Wallaroo assays, or other features such as shadow deployments to test champion and challenger models to find which models provide the best results.

5 - Model Conversion Tutorials

How to convert ML models into a Wallaroo compatible format.

Wallaroo pipelines support the ONNX standard and models converted using the Model autoconversion method.

These sample guides and their machine language models can be downloaded from the Wallaroo Tutorials Repository.

5.1 - Keras Convert and Upload Within Wallaroo

How to convert Keras ML models and upload them to Wallaroo using the Wallaroo auto-conversion method.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Introduction

Machine Learning (ML) models can be converted into a Wallaroo Model object and uploaded into Wallaroo workspace using the Wallaroo Client convert_model(path, source_type, conversion_arguments) method. This conversion process transforms the model into an open format that can be run across different frameworks at compiled C-language speeds.

The following tutorial is a brief example of how to convert a Keras or Tensor ML model to ONNX. This allows organizations that have trained Keras or Tensor models to convert them and use them with Wallaroo.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.

This tutorial demonstrates how to:

  • Convert a keras ML model and upload it into the Wallaroo engine.
  • Run a sample inference on the converted model in a Wallaroo instance.

This tutorial provides the following:

  • simple_sentiment_model.zip: A pre-trained keras sentiment model to be converted. This has 100 columns.

Conversion Steps

To use the Wallaroo autoconverter convert_model(path, source_type, conversion_arguments) method takes 3 parameters. The paramters for keras conversions are:

  • path (STRING): The path to the ML model file.
  • source_type (ModelConversionSource): The type of ML model to be converted. As of this time Wallaroo auto-conversion supports the following source types and their associated ModelConversionSource:
    • sklearn: ModelConversionSource.SKLEARN
    • xgboost: ModelConversionSource.XGBOOST
    • keras: ModelConversionSource.KERAS
  • conversion_arguments: The arguments for the conversion based on the type of model being converted. These are:
    • wallaroo.ModelConversion.ConvertKerasArguments: Used for converting keras type models and takes the following parameters:
      • name: The name of the model being converted.
      • comment: Any comments for the model.
      • input_type: A tensorflow Dtype called in the format ModelConversionInputType.{type}, where {type} is Float, Double, etc depending on the model.
      • dimensions: Corresponds to the keras xtrain in the format List[Union[None, int, float]].

Import Libraries

The first step is to import the libraries needed.

import wallaroo

from wallaroo.ModelConversion import ConvertKerasArguments, ModelConversionSource, ModelConversionInputType
from wallaroo.object import EntityNotFoundError
import pandas as pd

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)

Configuration and Methods

The following will set the workspace, pipeline, model name, the model file name used when uploading and converting the keras model, and the sample data.

The functions get_workspace(name) will either set the current workspace to the requested name, or create it if it does not exist. The function get_pipeline(name) will either set the pipeline used to the name requested, or create it in the current workspace if it does not exist.

workspace_name = 'externalkerasautoconvertworkspace'
pipeline_name = 'externalkerasautoconvertpipeline'
model_name = 'externalsimple-sentiment-model'
model_file_name = 'simple_sentiment_model.zip'
sample_data = 'simple_sentiment_testdata.json'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Connect to Wallaroo

Connect to your Wallaroo instance and store the connection into the variable wl.

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR SUFFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"] == "False":
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Set the Workspace and Pipeline

Set or create the workspace and pipeline based on the names configured earlier.

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name externalkerasautoconvertpipeline
created 2023-02-21 18:16:17.818879+00:00
last_updated 2023-02-21 18:16:17.818879+00:00
deployed (none)
tags
versions d0be28f6-4a0b-4b1c-9196-d809e8e21379
steps

Set the Model Autoconvert Parameters

Set the paramters for converting the simple-sentiment-model. This includes the shape of the model.

model_columns = 100

model_conversion_args = ConvertKerasArguments(
    name=model_name,
    comment="simple keras model",
    input_type=ModelConversionInputType.Float32,
    dimensions=(None, model_columns)
)
model_conversion_type = ModelConversionSource.KERAS

Upload and Convert the Model

Now we can upload the convert the model. Once finished, it will be stored as {unique-file-id}-converted.onnx.

converted model
# converts and uploads model.
model_wl = wl.convert_model('simple_sentiment_model.zip', model_conversion_type, model_conversion_args)
model_wl
{'name': 'externalsimple-sentiment-model', 'version': '7ac5e3a0-62f4-406e-87bb-185dd4f26fb6', 'file_name': '2f0a2876-fe21-44ac-9e1a-ba2462c77692-converted.onnx', 'image_path': None, 'last_update_time': datetime.datetime(2023, 2, 21, 18, 16, 26, 708440, tzinfo=tzutc())}

Test Inference

With the model uploaded and converted, we can run a sample inference.

Add Pipeline Step and Deploy

We will add the model as a step into our pipeline, then deploy it.

pipeline.add_model_step(model_wl).deploy()
name externalkerasautoconvertpipeline
created 2023-02-21 18:16:17.818879+00:00
last_updated 2023-02-21 18:16:30.388009+00:00
deployed True
tags
versions f7141b40-c610-42f7-873c-6fcb2b6e9ada, d0be28f6-4a0b-4b1c-9196-d809e8e21379
steps externalsimple-sentiment-model
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.48.0.50',
   'name': 'engine-579bdcf5bd-9j2mk',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'externalkerasautoconvertpipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'externalsimple-sentiment-model',
      'version': '7ac5e3a0-62f4-406e-87bb-185dd4f26fb6',
      'sha': '88f8118f5e9ea7368dde563413c77738e64b4e3f5856c3c9323b02bcf0dd1fd5',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.48.0.49',
   'name': 'engine-lb-74b4969486-gmwzg',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Run a Test Inference

We can run a test inference from the simple_sentiment_testdata.json file, then display just the results.

if arrowEnabled is True:
    sample_data = 'simple_sentiment_testdata.df.json'
    result = pipeline.infer_from_file(sample_data)
    display(result["out.dense"])
else:
    sample_data = 'simple_sentiment_testdata.json'
    result = pipeline.infer_from_file(sample_data)
    display(result[0].data())
0    [0.094697624]
1       [0.991031]
2     [0.93407357]
3     [0.56030995]
4      [0.9964503]
Name: out.dense, dtype: object

Undeploy the Pipeline

With the tests complete, we will undeploy the pipeline to return the resources back to the Wallaroo instance.

pipeline.undeploy()
name externalkerasautoconvertpipeline
created 2023-02-21 18:16:17.818879+00:00
last_updated 2023-02-21 18:16:30.388009+00:00
deployed False
tags
versions f7141b40-c610-42f7-873c-6fcb2b6e9ada, d0be28f6-4a0b-4b1c-9196-d809e8e21379
steps externalsimple-sentiment-model

5.2 - PyTorch to ONNX Outside Wallaroo

How to convert PyTorch ML models into the ONNX format.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

How to Convert PyTorch to ONNX

The following tutorial is a brief example of how to convert a PyTorth (aka sk-learn) ML model to ONNX. This allows organizations that have trained sk-learn models to convert them and use them with Wallaroo.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service. This sample code is based on the guide Convert your PyTorch model to ONNX.

This tutorial provides the following:

  • pytorchbikeshare.pt: a RandomForestRegressor PyTorch model. This model has a total of 58 inputs, and uses the class BikeShareRegressor.

Conversion Process

Libraries

The first step is to import our libraries we will be using. For this example, the PyTorth torch library will be imported into this kernel.

# the Pytorch libraries
# Import into this kernel

import sys
!{sys.executable} -m pip install torch

import torch
import torch.onnx 

Load the Model

To load a PyTorch model into a variable, the model’s class has to be defined. For out example we are using the BikeShareRegressor class as defined below.

class BikeShareRegressor(torch.nn.Module):
    def __init__(self):
        super(BikeShareRegressor, self).__init__()

        
        self.net = nn.Sequential(nn.Linear(input_size, l1),
                                 torch.nn.ReLU(),
                                 torch.nn.Dropout(p=dropout),
                                 nn.BatchNorm1d(l1),
                                 nn.Linear(l1, l2),
                                 torch.nn.ReLU(),
                                 torch.nn.Dropout(p=dropout),                                
                                 nn.BatchNorm1d(l2),                                                                                                   
                                 nn.Linear(l2, output_size))

    def forward(self, x):
        return self.net(x)

Now we will load the model into the variable pytorch_tobe_converted.

# load the Pytorch model
model = torch.load("./pytorch_bikesharingmodel.pt")

Convert_ONNX Inputs

Now we will define our method Convert_ONNX() which has the following inputs:

  • PyTorchModel: the PyTorch we are converting.

  • modelInputs: the model input or tuple for multiple inputs.

  • onnxPath: The location to save the onnx file.

  • opset_version: The ONNX version to export to.

  • input_names: Array of the model’s input names.

  • output_names: Array of the model’s output names.

  • dynamic_axes: Sets variable length axes in the format, replacing the batch_size as necessary: {'modelInput' : { 0 : 'batch_size'}, 'modelOutput' : {0 : 'batch_size'}}

  • export_params: Whether to store the trained parameter weight inside the model file. Defaults to True.

  • do_constant_folding: Sets whether to execute constant folding for optimization. Defaults to True.

#Function to Convert to ONNX 
def Convert_ONNX(): 

    # set the model to inference mode 
    model.eval() 

    # Export the model   
    torch.onnx.export(model,         # model being run 
         dummy_input,       # model input (or a tuple for multiple inputs) 
         pypath,       # where to save the model  
         export_params=True,  # store the trained parameter weights inside the model file 
         opset_version=15,    # the ONNX version to export the model to 
         do_constant_folding=True,  # whether to execute constant folding for optimization 
         input_names = ['modelInput'],   # the model's input names 
         output_names = ['modelOutput'], # the model's output names 
         dynamic_axes = {'modelInput' : {0 : 'batch_size'}, 'modelOutput' : {0 : 'batch_size'}} # variable length axes 
    ) 
    print(" ") 
    print('Model has been converted to ONNX') 

Convert the Model

We’ll now set our variables and run our conversion. For out example, the input_size is known to be 58, and the device value we’ll derive from torch.cuda. We’ll also set the ONNX version for exporting to 15.

pypath = "pytorchbikeshare.onnx"

input_size = 58

if torch.cuda.is_available():
    device = 'cuda'
else:
    device = 'cpu'

onnx_opset_version = 15

# Set up some dummy input tensor for the model
dummy_input = torch.randn(1, input_size, requires_grad=True).to(device)

Convert_ONNX()
Model has been converted to ONNX

Conclusion

And now our conversion is complete. Please feel free to use this sample code in your own projects.

5.3 - sklearn and XGBoost Regression Auto-Conversion Tutorial Within Wallaroo

How to use the Wallaroo convert_model method with a sklearn model.

Auto-Conversion And Upload Tutorial

Machine Learning (ML) models can be converted into a Wallaroo and uploaded into Wallaroo workspace using the Wallaroo Client convert_model(path, source_type, conversion_arguments) method. This conversion process transforms the model into an open format that can be run across different frameworks at compiled C-language speeds.

The three input parameters are:

  • path (STRING): The path to the ML model file.
  • source_type (ModelConversionSource): The type of ML model to be converted. As of this time Wallaroo auto-conversion supports the following source types and their associated ModelConversionSource:
    • sklearn: ModelConversionSource.SKLEARN
    • xgboost: ModelConversionSource.XGBOOST
  • conversion_arguments: The arguments for the conversion:
    • name: The name of the model being converted.
    • comment: Any comments for the model.
    • number_of_columns: The number of columns the model was trained for.
    • input_type: The ModelConversationInputType, typically Float or Double depending on the model.

The following tutorial demonstrates how to convert a sklearn Linear Model and a XGBoost Regression Model, and upload them into a Wallaroo Workspace. The following is provided for the tutorial:

  • sklearn-linear-model.pickle: A sklearn linear model. An example of training the model is provided in the Jupyter Notebook sklearn-linear-model-example.ipynb. It has 25 columns.
  • xgb_reg.pickle: A XGBoost regression model. An example of training the model is provided in the Jupyter Notebook xgboost-regression-model-example.ipynb. It has 25 columns.

Steps

Prerequisites

Before starting, the following must be available:

  • The model to upload into a workspace.
  • The number of columns the model was trained for.

Wallaroo supports the following model versions:

  • XGBoost: Version 1.6.2
  • SKLearn: 1.1.2

Import Libraries

Import the libraries that will be used for the auto-conversion process.

import pickle
import json

import wallaroo

from wallaroo.ModelConversion import ConvertSKLearnArguments, ConvertXGBoostArgs, ModelConversionSource, ModelConversionInputType
from wallaroo.object import EntityNotFoundError
# Verify the version of XGBoost used to generate the models

import sklearn
import sklearn.datasets

import xgboost as xgb

print(xgb.__version__)
print(sklearn.__version__)
1.6.2
1.1.2

The following code is used to either connect to an existing workspace or to create a new one. For more details on working with workspaces, see the Wallaroo Workspace Management Guide.

Connect to Wallaroo

Connect to your Wallaroo instance.

# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Set the Workspace

We’ll connect or create the workspace testautoconversion and use it for our model testing.

workspace_name = 'testautoconversion'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

wl.get_current_workspace()
{'name': 'testautoconversion', 'id': 20, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T20:05:40.6269+00:00', 'models': [], 'pipelines': []}

Set the Model Conversion Arguments

We’ll create two different configurations, one for each of our models:

  • sklearn_model_conversion_args: Used for our sklearn model.
  • xgboost_model_converstion_args: Used for our XGBoost model.
# The number of columns
NF=25

sklearn_model_conversion_args = ConvertSKLearnArguments(
    name="sklearntest",
    comment="test linear regression",
    number_of_columns=NF,
    input_type=ModelConversionInputType.Double
)
sklearn_model_conversion_type = ModelConversionSource.SKLEARN

xgboost_model_conversion_args = ConvertXGBoostArgs(
    name="xgbtestreg",
    comment="xgboost regression model test",
    number_of_columns=NF,
    input_type=ModelConversionInputType.Float32
)
xgboost_model_conversion_type = ModelConversionSource.XGBOOST

Convert the Models

The convert_model method converts the model using the arguments, and uploads it into the current workspace - in this case, testconversion. Once complete, we can run get_current_workspace to verify that the models were uploaded.

# converts and uploads the sklearn model.
wl.convert_model('sklearn-linear-model.pickle', sklearn_model_conversion_type, sklearn_model_conversion_args)

# converts and uploads the XGBoost model.
wl.convert_model('xgb_reg.pickle', xgboost_model_conversion_type, xgboost_model_conversion_args)
{'name': 'xgbtestreg', 'version': '42779f3a-e874-4f8d-b985-822f2128a954', 'file_name': 'bffa90a2-5a27-4e77-a2f4-cd0f68f99d24-converted.onnx', 'image_path': None, 'last_update_time': datetime.datetime(2023, 2, 27, 20, 11, 36, 906580, tzinfo=tzutc())}
wl.get_current_workspace()
{'name': 'testautoconversion', 'id': 20, 'archived': False, 'created_by': '435da905-31e2-4e74-b423-45c38edb5889', 'created_at': '2023-02-27T20:05:40.6269+00:00', 'models': [{'name': 'sklearntest', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 2, 27, 20, 11, 35, 745445, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 2, 27, 20, 11, 35, 745445, tzinfo=tzutc())}, {'name': 'xgbtestreg', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 2, 27, 20, 11, 36, 906580, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 2, 27, 20, 11, 36, 906580, tzinfo=tzutc())}], 'pipelines': []}

5.4 - sk-learn Regression Model to ONNX Outside Wallaroo

How to convert sk-learn Regression ML models into the ONNX format.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

How to Convert sk-learn Regression Model to ONNX

The following tutorial is a brief example of how to convert a scikit-learn (aka sk-learn) regression ML model to the ONNX.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.

This tutorial provides the following:

  • demand_curve.pickle: a demand curve trained sk-learn model. Once this file is converted to ONNX format, it can be used as part of the Demand Curve Pipeline Tutorial.

    This model contains 3 columns: UnitPrice, cust_known, and UnitPriceXcust_known.

Conversion Process

Libraries

The first step is to import our libraries we will be using.

# Used to load the sk-learn model
import pickle

# Used for the conversion process
import onnx, skl2onnx, onnxmltools
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx.common.data_types import DoubleTensorType

Next let’s define our model_to_onnx method for converting a sk-learn model to ONNX. This has the following inputs:

  • model: The sk-learn model we’re converting.
  • cols: The number of inputs the model expects
  • input_type: Determines how to manage float values, which can either be DoubleTensorType or FloatTensorType.
# convert model to ONNX

def model_to_onnx(model, cols, *, input_type='Double'):
    input_type_lower=input_type.lower()
    # How to manage float values
    if input_type=='Double':
        tensor_type=DoubleTensorType
    elif input_type=='Float':
        tensor_type=FloatTensorType
    else:
        raise ValueError("bad input type")
    tensor_size=cols
    initial_type=[(f'{input_type_lower}_input', tensor_type([None, tensor_size]))]
    onnx_model=onnxmltools.convert_sklearn(model,initial_types=initial_type)
    return onnx_model

With our method defined, now it’s time to convert. Let’s load our sk-learn model and save it into the variable sklearn_model.

# pickle the model, so I can try the Wallaroo converter on it

sklearn_model = pickle.load(open('./demand_curve.pickle', 'rb'))
/opt/homebrew/anaconda3/envs/arrowtests/lib/python3.8/site-packages/sklearn/base.py:329: UserWarning: Trying to unpickle estimator LinearRegression from version 0.24.2 when using version 1.1.2. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to:
https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations
  warnings.warn(

Now we’ll convert our sklearn-model into the variable onnx_model using our model_to_onnx method. Recall that our sklearn-model has 3 columns.

onnx_model_converted = model_to_onnx(sklearn_model, 3)

Now we can save our model to a onnx file.

onnx.save_model(onnx_model_converted, "demand_curve.onnx")

5.5 - Statsmodel Upload to Wallaroo Tutorial

How to upload a Statsmodel ML model into Wallaroo

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Introduction

Organizations can deploy a Machine Learning (ML) model based on the statsmodels directly into Wallaroo through the following process. This conversion process transforms the model into an open format that can be run across different frameworks at compiled C-language speeds.

This example provides the following:

  • train-statsmodel.ipynb: A sample Jupyter Notebook that trains a sample model. The model predicts how many bikes will be rented on each of the next 7 days, based on the previous 7 days’ bike rentals, temperature, and wind speed. Additional files to support this example are:
    • day.csv: Data used to train the sample statsmodel example.
    • infer.py: The inference script that is part of the statsmodel.
  • convert-statsmodel-tutorial.ipynb: A sample Jupyter Notebook that demonstrates how to upload, convert, and deploy the statsmodel example into a Wallaroo instance. Additional files to support this example are:
    • bike_day_model.pkl: A statsmodel ML model trained from the train-statsmodel.ipynb Notebook.

      IMPORTANT NOTE: The statsmodel ML model is composed of two parts that are contained in the .pkl file:

      • The pickled Python runtime expects a dictionary with two keys: model and script:

        • model—the pickled model, which will be automatically loaded into the python runtime with the name ‘model’
        • script—the text of the python script to be run, in a format similar to the existing python script steps (i.e. defining a wallaroo_json method which operates on the data). In this cae, the file infer.py is the script used.
    • bike_day_eval.json: Evaluation data used to test the model’s performance.

Steps

The following steps will perform the following:

  1. Upload the statsmodel ML model bike_day_model.pkl into a Wallaroo.
  2. Deploy the model into a pipeline.
  3. Run a test inference.
  4. Undeploy the pipeline.

Import Libraries

The first step is to import the libraries that we will need.

import json
import os
import datetime

import wallaroo
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Initialize connection

Start a connect to the Wallaroo instance and save the connection into the variable wl.

# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR PREFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Set Configurations

The following will set the workspace, model name, and pipeline that will be used for this example. If the workspace or pipeline already exist, then they will assigned for use in this example. If they do not exist, they will be created based on the names listed below.

workspace_name = 'bikedayevalworkspace'
pipeline_name = 'bikedayevalpipeline'
model_name = 'bikedaymodel'
model_file_name = 'bike_day_model.pkl'

Set the Workspace and Pipeline

This sample code will create or use the existing workspace bike-day-workspace as the current workspace.

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name bikedayevalpipeline
created 2023-02-27 20:15:07.848652+00:00
last_updated 2023-02-27 20:15:07.848652+00:00
deployed (none)
tags
versions 14a140e1-640b-4cf5-9d45-dd27fe00ad80
steps

Upload Pickled Package Statsmodel Model

Upload the statsmodel stored into the pickled package bike_day_model.pkl. See the Notebook train-statsmodel.ipynb for more details on creating this package.

Note that this package is being specified as a python configuration.

file_name = "bike_day_model.pkl"

bike_day_model = wl.upload_model(model_name, model_file_name).configure(runtime="python")

Deploy the Pipeline

We will now add the uploaded model as a step for the pipeline, then deploy it.

pipeline.add_model_step(bike_day_model)
name bikedayevalpipeline
created 2023-02-27 20:15:07.848652+00:00
last_updated 2023-02-27 20:15:07.848652+00:00
deployed (none)
tags
versions 14a140e1-640b-4cf5-9d45-dd27fe00ad80
steps
pipeline.deploy()
name bikedayevalpipeline
created 2023-02-27 20:15:07.848652+00:00
last_updated 2023-02-27 20:16:18.874015+00:00
deployed True
tags
versions 00e53810-6278-463b-b1c7-6a63f25fd1ef, 91b1dd51-2adb-4003-ab68-91a8415210c1, 14a140e1-640b-4cf5-9d45-dd27fe00ad80
steps bikedaymodel
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.244.0.51',
   'name': 'engine-6d5cc888b-v7mhj',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'bikedayevalpipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'bikedaymodel',
      'version': 'd14c4f84-4238-49cd-9a63-ff96d4d28b24',
      'sha': '1bb486598732259efdd131b45d471e165b594d5443928ea9e50fa4c7e0b1b718',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.244.1.13',
   'name': 'engine-lb-ddd995646-49mdq',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Run Inference

Perform an inference from the evaluation data JSON file bike_day_eval.json.

if arrowEnabled is True:
    results = pipeline.infer_from_file('bike_day_eval.json', data_format="custom-json")
else:
    results = pipeline.infer_from_file('bike_day_eval.json')
display(results)
[{'forecast': [1882.378455403016,
   2130.6079157429585,
   2340.840053800859,
   2895.754978555364,
   2163.6575155637433,
   1509.1792126514365,
   2431.183892393437]}]

Undeploy the Pipeline

Undeploy the pipeline and return the resources back to the Wallaroo instance.

pipeline.undeploy()
name bikedayevalpipeline
created 2023-02-27 20:15:07.848652+00:00
last_updated 2023-02-27 20:15:11.565861+00:00
deployed False
tags
versions 91b1dd51-2adb-4003-ab68-91a8415210c1, 14a140e1-640b-4cf5-9d45-dd27fe00ad80
steps bikedaymodel

5.6 - XGBoost Classification Auto-Convert Within Wallaroo

How to convert XGBoost ML Classification models and upload to Wallaroo.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Introduction

The following tutorial is a brief example of how to convert a XGBoost Classification ML model with the convert_model method and upload it into your Wallaroo instance.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.

  • Convert a XGBoost Classification ML model and upload it into the Wallaroo engine.
  • Run a sample inference on the converted model in a Wallaroo instance.

This tutorial provides the following:

  • xgb_class.pickle: A pretrained XGBoost Classification model with 25 columns.
  • xgb_class_eval.json: Test data to perform a sample inference.

Prerequisites

Wallaroo supports the following model versions:

  • XGBoost: Version 1.6.0
  • SKLearn: 1.1.2

Conversion Steps

To use the Wallaroo autoconverter convert_model(path, source_type, conversion_arguments) method takes 3 parameters. The parameters for XGBoost conversions are:

  • path (STRING): The path to the ML model file.
  • source_type (ModelConversionSource): The type of ML model to be converted. As of this time Wallaroo auto-conversion supports the following source types and their associated ModelConversionSource:
    • sklearn: ModelConversionSource.SKLEARN
    • xgboost: ModelConversionSource.XGBOOST
    • keras: ModelConversionSource.KERAS
  • conversion_arguments: The arguments for the conversion based on the type of model being converted. These are:
    • wallaroo.ModelConversion.ConvertXGBoostArgs: Used for XGBoost models and takes the following parameters:
    • name: The name of the model being converted.
    • comment: Any comments for the model.
    • number_of_columns: The number of columns the model was trained for.
    • input_type: A tensorflow Dtype called in the format ModelConversionInputType.{type}, where {type} is Float, Double, etc depending on the model.

Import Libraries

The first step is to import the libraries needed.

import wallaroo

from wallaroo.ModelConversion import ConvertXGBoostArgs, ModelConversionSource, ModelConversionInputType
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo

Connect to your Wallaroo instance and store the connection into the variable wl.

# Login through local Wallaroo instance

# wl = wallaroo.Client()

# SSO login through keycloak

wallarooPrefix = "YOUR PREFIX"
wallarooSuffix = "YOUR PREFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                    auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                    auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Configuration and Methods

The following will set the workspace, pipeline, model name, the model file name used when uploading and converting the keras model, and the sample data.

The functions get_workspace(name) will either set the current workspace to the requested name, or create it if it does not exist. The function get_pipeline(name) will either set the pipeline used to the name requested, or create it in the current workspace if it does not exist.

workspace_name = 'xgboost-classification-autoconvert-workspace'
pipeline_name = 'xgboost-classification-autoconvert-pipeline'
model_name = 'xgb-class-model'
model_file_name = 'xgb_class.pickle'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Set the Workspace and Pipeline

Set or create the workspace and pipeline based on the names configured earlier.

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline

Set the Model Autoconvert Parameters

Set the paramters for converting the xgb-class-model.

#the number of columns
NF = 25

model_conversion_args = ConvertXGBoostArgs(
    name=model_name,
    comment="xgboost classification model test",
    number_of_columns=NF,
    input_type=ModelConversionInputType.Float32
)
model_conversion_type = ModelConversionSource.XGBOOST

Upload and Convert the Model

Now we can upload the convert the model. Once finished, it will be stored as {unique-file-id}-converted.onnx.

# convert and upload
model_wl = wl.convert_model(model_file_name, model_conversion_type, model_conversion_args)

Test Inference

With the model uploaded and converted, we can run a sample inference.

Deploy the Pipeline

Add the uploaded and converted model_wl as a step in the pipeline, then deploy it.

pipeline.add_model_step(model_wl).deploy()

Run the Inference

Use the evaluation data to verify the process completed successfully.

if arrowEnabled is True:
    sample_data = 'xgb_class_eval.df.json'
    result = pipeline.infer_from_file(sample_data)
    display(result)
else:
    sample_data = 'xgb_class_eval.json'
    result = pipeline.infer_from_file(sample_data)
    display(result[0].data())

Undeploy the Pipeline

With the tests complete, we will undeploy the pipeline to return the resources back to the Wallaroo instance.

pipeline.undeploy()

5.7 - XGBoost Regression Auto-Convert Within Wallaroo

How to convert XGBoost ML Regression models and upload to Wallaroo with the Wallaroo convert_model method.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Introduction

The following tutorial is a brief example of how to convert a XGBoost Regression ML model with the convert_model method and upload it into your Wallaroo instance.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.

  • Convert a XGBoost Regression ML model and upload it into the Wallaroo engine.
  • Run a sample inference on the converted model in a Wallaroo instance.

This tutorial provides the following:

  • xgb_reg.pickle: A pretrained XGBoost Regression model with 25 columns.
  • xgb_regression_eval.json: Test data to perform a sample inference.

Conversion Steps

Conversion Steps

To use the Wallaroo autoconverter convert_model(path, source_type, conversion_arguments) method takes 3 parameters. The parameters for XGBoost conversions are:

  • path (STRING): The path to the ML model file.
  • source_type (ModelConversionSource): The type of ML model to be converted. As of this time Wallaroo auto-conversion supports the following source types and their associated ModelConversionSource:
    • sklearn: ModelConversionSource.SKLEARN
    • xgboost: ModelConversionSource.XGBOOST
    • keras: ModelConversionSource.KERAS
  • conversion_arguments: The arguments for the conversion based on the type of model being converted. These are:
    • wallaroo.ModelConversion.ConvertXGBoostArgs: Used for XGBoost models and takes the following parameters:
    • name: The name of the model being converted.
    • comment: Any comments for the model.
    • number_of_columns: The number of columns the model was trained for.
    • input_type: A tensorflow Dtype called in the format ModelConversionInputType.{type}, where {type} is Float, Double, etc depending on the model.

Import Libraries

The first step is to import the libraries needed.

import wallaroo

from wallaroo.ModelConversion import ConvertXGBoostArgs, ModelConversionSource, ModelConversionInputType
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo

Connect to your Wallaroo instance and store the connection into the variable wl.

# Login through local Wallaroo instance

# wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR PREFIX"

wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
                    auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
                    auth_type="sso")

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Configuration and Methods

The following will set the workspace, pipeline, model name, the model file name used when uploading and converting the keras model, and the sample data.

The functions get_workspace(name) will either set the current workspace to the requested name, or create it if it does not exist. The function get_pipeline(name) will either set the pipeline used to the name requested, or create it in the current workspace if it does not exist.

workspace_name = 'xgboost-regression-autoconvert-workspace'
pipeline_name = 'xgboost-regression-autoconvert-pipeline'
model_name = 'xgb-regression-model'
model_file_name = 'xgb_reg.pickle'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

Set the Workspace and Pipeline

Set or create the workspace and pipeline based on the names configured earlier.

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name xgboost-regression-autoconvert-pipeline
created 2023-02-22 17:36:31.143795+00:00
last_updated 2023-02-23 16:29:55.996960+00:00
deployed False
tags
versions 6f6a7cf1-b328-4a67-a37d-4047b1082516, 00bb4dd6-3061-4619-9491-a3b7c307f784, 45081f0c-1991-4ce0-907c-6135ca328084, 1220c119-45e9-4ff4-bdfd-8ff2f95486d5
steps xgb-regression-model

Set the Model Autoconvert Parameters

Set the paramters for converting the xgb-class-model.

#the number of columns
NF = 25

model_conversion_args = ConvertXGBoostArgs(
    name=model_name,
    comment="xgboost regression model test",
    number_of_columns=NF,
    input_type=ModelConversionInputType.Float32
)
model_conversion_type = ModelConversionSource.XGBOOST

Upload and Convert the Model

Now we can upload the convert the model. Once finished, it will be stored as {unique-file-id}-converted.onnx.

# convert and upload
model_wl = wl.convert_model(model_file_name, model_conversion_type, model_conversion_args)

Test Inference

With the model uploaded and converted, we can run a sample inference.

Deploy the Pipeline

Add the uploaded and converted model_wl as a step in the pipeline, then deploy it.

pipeline.add_model_step(model_wl).deploy()
Waiting for deployment - this will take up to 45s ............. ok
name xgboost-regression-autoconvert-pipeline
created 2023-02-22 17:36:31.143795+00:00
last_updated 2023-02-24 16:26:10.105654+00:00
deployed True
tags
versions 73345b3e-e141-497e-bcab-e6145cb8b6ec, 6f6a7cf1-b328-4a67-a37d-4047b1082516, 00bb4dd6-3061-4619-9491-a3b7c307f784, 45081f0c-1991-4ce0-907c-6135ca328084, 1220c119-45e9-4ff4-bdfd-8ff2f95486d5
steps xgb-regression-model
pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.48.1.19',
   'name': 'engine-749bb6f4fd-4qt2x',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'xgboost-regression-autoconvert-pipeline',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'xgb-regression-model',
      'version': '2f30475f-6385-4c5e-bfbb-9d47fa84f8ae',
      'sha': '7414d26cb5495269bc54bcfeebd269d7c74412cbfca07562fc7cb184c55b6f8e',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.48.1.20',
   'name': 'engine-lb-74b4969486-7jzwh',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Run the Inference

Use the test_class_eval.json as set earlier as our sample_data and perform the inference.

if arrowEnabled is True:
    sample_data = 'xgb_regression_eval.df.json'
    result = pipeline.infer_from_file(sample_data)
    display(result)
else:
    sample_data = 'xgb_regression_eval.json'
    result = pipeline.infer_from_file(sample_data)
    result[0].data()
    
time in.tensor out.variable check_failures
0 2023-02-24 16:26:24.157 [-0.0337420814, -0.1876901281, 0.3183056488, 1.1831088244, -0.3047963287, 1.0713634828, 0.4679136198, 1.1382147115, 2.8101110944, -0.9981048796, -0.2543715265, 0.2845195171, -0.6477265924, -1.2198006181, 2.0592129832, -1.586429512, 0.1884164743, -0.3816011585, 1.0781704305, -0.2251253601, 0.6067409459, 0.9659944831, -0.690207203, -0.3849078305, -1.7806555641] [124.11397] 0
1 2023-02-24 16:26:24.157 [-0.6374335428, 0.9713713274, -0.3899847809, -1.8685333445, 0.6264452739, 1.0778638153, -1.1687273967, -1.9366353171, -0.7583260267, -0.1288186991, 2.2018769654, -0.9383105208, -0.0959982166, 0.6889112707, 1.0172067951, -0.1988865499, 1.3461760224, -0.5692275708, 0.0112450486, -1.0244657911, -0.0065034946, -0.888033574, 2.5997682335, -0.6593191496, 0.4554196997] [-238.03009] 0
2 2023-02-24 16:26:24.157 [0.9847406173, -0.6887896553, -0.9483266359, -0.6146245598, 0.395195321, 0.2237676197, -2.1580851068, -0.8124396117, 0.8795326949, 1.0463472648, -0.2343060791, 1.9127900859, -0.0636431887, 2.7055743269, 1.424242505, 0.1486958646, -0.7771892138, -0.6720552548, 0.9127712446, 0.680721406, 1.5207886874, 1.9579334337, -0.9336538468, -0.2942243461, 0.8563934417] [253.06976] 0
3 2023-02-24 16:26:24.157 [-0.0894312686, 2.0916777545, 0.155086745, 0.8335388277, 0.4376497549, -0.2875695352, -1.272466627, -0.8226918076, -0.8637972417, -0.4856051115, -0.978749107, 0.2675108269, 0.5246808262, -0.96869578, 0.8475004997, 1.0027495438, 0.4704188579, 2.6906210825, 1.34454675, -1.4987055653, 0.680752942, -2.6459314502, 0.6274277031, 1.3640818416, -0.8077878088] [141.34639] 0
4 2023-02-24 16:26:24.157 [-0.9200220805, -1.8760634694, -0.8277296049, 0.6511561005, 1.5066237509, -1.1236118386, -0.3776053288, -0.0445487434, -1.4965713379, -0.1756118518, 0.0317408338, 0.2496108303, 1.6857141605, 0.0339106658, -0.3340227553, -0.3428326984, -0.5932644698, -0.4395685475, -0.6870452688, -0.4132149028, -0.7352879532, 0.2080507404, 0.4575261189, -2.0175947284, 1.154633581] [42.79154] 0

Undeploy the Pipeline

With the tests complete, we will undeploy the pipeline to return the resources back to the Wallaroo instance.

pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name xgboost-regression-autoconvert-pipeline
created 2023-02-22 17:36:31.143795+00:00
last_updated 2023-02-24 16:26:10.105654+00:00
deployed False
tags
versions 73345b3e-e141-497e-bcab-e6145cb8b6ec, 6f6a7cf1-b328-4a67-a37d-4047b1082516, 00bb4dd6-3061-4619-9491-a3b7c307f784, 45081f0c-1991-4ce0-907c-6135ca328084, 1220c119-45e9-4ff4-bdfd-8ff2f95486d5
steps xgb-regression-model

5.8 - XGBoost Convert to ONNX

How to convert XGBoost to ONNX using the onnxmltools.convert library

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

How to Convert XGBoost to ONNX

The following tutorial is a brief example of how to convert a XGBoost ML model to the ONNX standard. This allows organizations that have trained XGBoost models to convert them and use them with Wallaroo.

This tutorial assumes that you have a Wallaroo instance and are running this Notebook from the Wallaroo Jupyter Hub service.

This tutorial provides the following:

Conversion Process

Libraries

The first step is to import our libraries we will be using.

import onnx
import pickle
from onnxmltools.convert import convert_xgboost

from skl2onnx.common.data_types import FloatTensorType, DoubleTensorType

Set Variables

The following variables are required to be known before the process can be started:

  • number of columns: The number of columns used by the model.
  • TARGET_OPSET: Verify the TARGET_OPSET value taht will be used in the conversion process matches the current Wallaroo model uploads requirements.
# set the number of columns
ncols = 18
TARGET_OPSET = 15

Load the XGBoost Model

Next we will load our model that has been saved in the pickle format and unpickle it.

# load the xgboost model
with open("housing_model_xgb.pkl", "rb") as f:
    xgboost_model = pickle.load(f)

Conversion Inputs

The convert_xgboost method has the following format and requires the following inputs:

convert_xgboost({XGBoost Model}, 
                {XGBoost Model Type},
                [
                    ('input', 
                    {Tensor Data Type}([None, {ncols}]))
                ],
                target_opset={TARGET_OPSET})
  1. XGBoost Model: The XGBoost Model to convert.
  2. XGBoost Model Type: The type of XGBoost model. In this example is it a tree-based classifier.
  3. Tensor Data Type: Either FloatTensorType or DoubleTensorType from the skl2onnx.common.data_types library.
  4. ncols: Number of columns in the model.
  5. TARGET_OPSET: The target opset which can be derived in code showed below.

Convert the Model

With all of our data in place we can now convert our XBBoost model to ONNX using the convert_xgboost method.

onnx_model_converted = convert_xgboost(xgboost_model, 'tree-based classifier',
                             [('input', FloatTensorType([None, ncols]))],
                             target_opset=TARGET_OPSET)

Save the Model

With the model converted to ONNX, we can now save it and use it in a Wallaroo pipeline.

onnx.save_model(onnx_model_converted, "housing_model_xgb.onnx")

6 - Model Validation and Testing in Wallaroo

Tutorials on using Wallaroo for model testing and validation.

The following tutorials demonstrate how to use Wallaroo to test and validation models in production against their target outcomes.

6.1 - A/B Testing Tutorial

How to use Wallaroo’s A/B Testing feature to evaluate competing ML models.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

A/B Testing

A/B testing is a method that provides the ability to test out ML models for performance, accuracy or other useful benchmarks. A/B testing is contrasted with the Wallaroo Shadow Deployment feature. In both cases, two sets of models are added to a pipeline step:

  • Control or Champion model: The model currently used for inferences.
  • Challenger model(s): One or more models that are to be compared to the champion model.

The two feature are different in this way:

Feature Description
A/B Testing A subset of inferences are submitted to either the champion ML model or a challenger ML model.
Shadow Deploy All inferences are submitted to the champion model and one or more challenger models.

So to repeat: A/B testing submits some of the inference requests to the champion model, some to the challenger model with one set of outputs, while shadow testing submits all of the inference requests to champion and shadow models, and has separate outputs.

This tutorial demonstrate how to conduct A/B testing in Wallaroo. For this example we will be using an open source model that uses an Aloha CNN LSTM model for classifying Domain names as being either legitimate or being used for nefarious purposes such as malware distribution.

For our example, we will perform the following:

  • Create a workspace for our work.
  • Upload the Aloha model and a challenger model.
  • Create a pipeline that can ingest our submitted data with the champion model and the challenger model set into a A/B step.
  • Run a series of sample inferences to display inferences that are run through the champion model versus the challenger model, then determine which is more efficient.

All sample data and models are available through the Wallaroo Quick Start Guide Samples repository.

Steps

Import libraries

Here we will import the libraries needed for this notebook.

import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import pandas as pd
import json
from IPython.display import display

# used to display dataframe information without truncating
from IPython.display import display
pd.set_option('display.max_colwidth', None)
wallaroo.__version__
'2023.1.0+ca09d8a5'

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Connect to the Wallaroo Instance

This command will be used to set up a connection to the Wallaroo cluster and allow creating and use of Wallaroo inference engines.

# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")

Create Workspace

We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace for all other commands.

To allow this tutorial to be run multiple times or by multiple users in the same Wallaroo instance, a random 4 character prefix will be added to the workspace, pipeline, and model.

workspace_name = 'abtesting'
def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)
{'name': 'abtesting', 'id': 137, 'archived': False, 'created_by': '138bd7e6-4dc8-4dc1-a760-c9e721ef3c37', 'created_at': '2023-03-03T18:58:38.546346+00:00', 'models': [{'name': 'aloha-control', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 3, 3, 18, 58, 39, 117617, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 3, 3, 18, 58, 39, 117617, tzinfo=tzutc())}, {'name': 'aloha-challenger', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 3, 3, 18, 58, 39, 431462, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 3, 3, 18, 58, 39, 431462, tzinfo=tzutc())}], 'pipelines': [{'name': 'randomsplitpipeline-demo', 'create_time': datetime.datetime(2023, 3, 3, 18, 58, 39, 517955, tzinfo=tzutc()), 'definition': '[]'}]}

Set Up the Champion and Challenger Models

Now we upload the Champion and Challenger models to our workspace. We will use two models:

  1. aloha-cnn-lstm model.
  2. aloha-cnn-lstm-new (a retrained version)

Set the Champion Model

We upload our champion model, labeled as control.

control =  wl.upload_model("aloha-control",   'models/aloha-cnn-lstm.zip').configure('tensorflow')

Set the Challenger Model

Now we upload the Challenger model, labeled as challenger.

challenger = wl.upload_model("aloha-challenger",   'models/aloha-cnn-lstm-new.zip').configure('tensorflow')

Define The Pipeline

Here we will configure a pipeline with two models and set the control model with a random split chance of receiving 2/3 of the data. Because this is a random split, it is possible for one model or the other to receive more inferences than a strict 2:1 ratio, but the more inferences are run, the more likely it is for the proper ratio split.

pipeline = (wl.build_pipeline("randomsplitpipeline-demo")
            .add_random_split([(2, control), (1, challenger)], "session_id"))

Deploy the pipeline

Now we deploy the pipeline so we can run our inference through it.

experiment_pipeline = pipeline.deploy()
Waiting for deployment - this will take up to 45s .................................. ok
experiment_pipeline.status()
{'status': 'Running',
 'details': [],
 'engines': [{'ip': '10.48.4.199',
   'name': 'engine-5df76d6869-t6nhk',
   'status': 'Running',
   'reason': None,
   'details': [],
   'pipeline_statuses': {'pipelines': [{'id': 'randomsplitpipeline-demo',
      'status': 'Running'}]},
   'model_statuses': {'models': [{'name': 'aloha-challenger',
      'version': '3acd3835-be72-42c4-bcae-84368f416998',
      'sha': '223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3',
      'status': 'Running'},
     {'name': 'aloha-control',
      'version': '89389786-0c17-4214-938c-aa22dd28359f',
      'sha': 'fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520',
      'status': 'Running'}]}}],
 'engine_lbs': [{'ip': '10.48.4.198',
   'name': 'engine-lb-86bc6bd77b-j7pqs',
   'status': 'Running',
   'reason': None,
   'details': []}],
 'sidekicks': []}

Run a single inference

Now we have our deployment set up let’s run a single inference. In the results we will be able to see the inference results as well as which model the inference went to under model_id. We’ll run the inference request 5 times, with the odds are that the challenger model being run at least once.

results = []
if arrowEnabled is True:
    # use dataframe JSON files
    for x in range(5):
        result = experiment_pipeline.infer_from_file("data/data-1.df.json")
        display(result.loc[:,["out._model_split", "out.main"]])    
else:
    # use Wallaroo JSON files
    results.append(experiment_pipeline.infer_from_file("data/data-1.json"))
    results.append(experiment_pipeline.infer_from_file("data/data-1.json"))
    results.append(experiment_pipeline.infer_from_file("data/data-1.json"))
    results.append(experiment_pipeline.infer_from_file("data/data-1.json"))
    results.append(experiment_pipeline.infer_from_file("data/data-1.json"))
    for result in results:
        print(result[0].model())
        print(result[0].data())
out._model_split out.main
0 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.997564]
out._model_split out.main
0 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.997564]
out._model_split out.main
0 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.997564]
out._model_split out.main
0 [{"name":"aloha-challenger","version":"3acd3835-be72-42c4-bcae-84368f416998","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] [0.997564]
out._model_split out.main
0 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.997564]

Run Inference Batch

We will submit 1000 rows of test data through the pipeline, then loop through the responses and display which model each inference was performed in. The results between the control and challenger should be approximately 2:1.

responses = []
if arrowEnabled is True:
    # responses = pd.DataFrame()
    #Read in the test data as one dataframe
    test_data = pd.read_json('data/data-1k.df.json')
    # For each row, submit that row as a separate dataframe
    # Add the results to the responses array
    for index, row in test_data.head(1000).iterrows():
        responses.append(experiment_pipeline.infer(row.to_frame('text_input').reset_index()))
        # display(responses)
    #now get our responses for each row
    # each r is a dataframe, then get the result from out.split into json and get the model name
    # for r in responses:
    #     display(r.loc[0]["out.split"])
    l = [json.loads(r.loc[0]["out._model_split"][0])["name"] for r in responses]
    df = pd.DataFrame({'model': l})
    display(df.model.value_counts())
else:
    l = []
    responses =[]
    from data import test_data
    for nth in range(1000):
        responses.extend(experiment_pipeline.infer(test_data.data[nth]))
    l = [r.raw['model_name'] for r in responses]
    df = pd.DataFrame({'model': l})
    display(df.model.value_counts())
aloha-control       656
aloha-challenger    344
Name: model, dtype: int64

Test Challenger

Now we have run a large amount of data we can compare the results.

For this experiment we are looking for a significant change in the fraction of inferences that predicted a probability of the seventh category being high than 0.5 so we can determine whether our challenger model is more “successful” than the champion model at identifying category 7.

control_count = 0
challenger_count = 0

control_success = 0
challenger_success = 0

if arrowEnabled is True:
    # do nothing
    for r in responses:
        if json.loads(r.loc[0]["out._model_split"][0])["name"] == "aloha-control":
            control_count += 1
            if(r.loc[0]["out.main"][0] > .5):
                control_success += 1
        else:
            challenger_count += 1
            if(r.loc[0]["out.main"][0] > .5):
               challenger_success += 1
else:
    for r in responses:
        if r.raw['model_name'] == "aloha-control":
            control_count += 1
            if(r.raw['outputs'][7]['Float']['data'][0] > .5):
                control_success += 1
        else:
            challenger_count +=1
            if(r.raw['outputs'][7]['Float']['data'][0] > .5):
                challenger_success += 1

print("control class 7 prediction rate: " + str(control_success/control_count))
print("challenger class 7 prediction rate: " + str(challenger_success/challenger_count))
control class 7 prediction rate: 0.9725609756097561
challenger class 7 prediction rate: 0.9854651162790697

Logs

Logs can be viewed with the Pipeline method logs(). For this example, only the first 5 logs will be shown. For Arrow enabled environments, the model type can be found in the column out._model_split.

logs = experiment_pipeline.logs(limit=5)

if arrowEnabled is True:
    display(logs.loc[:,['time', 'out._model_split', 'out.main']])
else:
    display(logs)
time out._model_split out.main
0 2023-03-03 19:08:35.653 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.9999754]
1 2023-03-03 19:08:35.702 [{"name":"aloha-challenger","version":"3acd3835-be72-42c4-bcae-84368f416998","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] [0.9999727]
2 2023-03-03 19:08:35.753 [{"name":"aloha-challenger","version":"3acd3835-be72-42c4-bcae-84368f416998","sha":"223d26869d24976942f53ccb40b432e8b7c39f9ffcf1f719f3929d7595bceaf3"}] [0.6606688]
3 2023-03-03 19:08:35.799 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.9998954]
4 2023-03-03 19:08:35.846 [{"name":"aloha-control","version":"89389786-0c17-4214-938c-aa22dd28359f","sha":"fd998cd5e4964bbbb4f8d29d245a8ac67df81b62be767afbceb96a03d1a01520"}] [0.99999803]

Undeploy Pipeline

With the testing complete, we undeploy the pipeline to return the resources back to the environment.

experiment_pipeline.undeploy()
Waiting for undeployment - this will take up to 45s .................................... ok
name randomsplitpipeline-demo
created 2023-03-03 18:58:39.517955+00:00
last_updated 2023-03-03 19:05:41.670973+00:00
deployed False
tags
versions afa1b6ff-7684-4e58-a513-79295024a442, ff71a8fc-947a-42bd-9a97-b4317ace6a9c, 694eb936-6d94-48e3-887f-d83d9b526d84, a6beb0ac-36c8-4fff-9d45-303790cbf7fe, 66161c1f-df31-4ca1-b4ae-c7f5fb3e73bb, ddca1949-bc85-4b12-9ec8-497b9630244d
steps aloha-control

6.2 - Anomaly Testing Tutorial

How to use Wallaroo’s pipeline validation feature to detect inference anomalies.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Anomaly Detection

Wallaroo provides multiple methods of analytical analysis to verify that the data received and generated during an inference is accurate. This tutorial will demonstrate how to use anomaly detection to track the outputs from a sample model to verify that the model is outputting acceptable results.

Anomaly detection allows organizations to set validation parameters in a pipeline. A validation is added to a pipeline to test data based on an expression, and flag any inferences where the validation failed to the InferenceResult object and the pipeline logs.

This tutorial will follow this process in setting up a validation to a pipeline and examining the results:

  1. Create a workspace and upload the sample model.
  2. Establish a pipeline and add the model as a step.
  3. Add a validation to the pipeline.
  4. Perform inferences and display anomalies through the InferenceResult object and the pipeline log files.

This tutorial provides the following:

  • Housing model: ./models/housing.zip - a pretrained model used to determine standard home prices.
  • Test Data: ./data - sample data.

This demonstration assumes that a Wallaroo instance has been installed.

Steps

Import libraries

The first step is to import the libraries needed for this notebook.

import wallaroo
from wallaroo.object import EntityNotFoundError
import os
import json

from IPython.display import display

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo Instance

The following command will create a connection to the Wallaroo instance and store it in the variable wl.

# Client connection from local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "sparkly-apple-3026"
# wallarooSuffix = "wallaroo.community"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")
import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)
True

Create Workspace

We will create a workspace to manage our pipeline and models. The following variables will set the name of our sample workspace then set it as the current workspace.

workspace_name = 'anomalytesting'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)
{'name': 'anomalyexamples', 'id': 139, 'archived': False, 'created_by': '138bd7e6-4dc8-4dc1-a760-c9e721ef3c37', 'created_at': '2023-03-03T19:11:35.338843+00:00', 'models': [{'name': 'anomaly-housing-model', 'versions': 1, 'owner_id': '""', 'last_update_time': datetime.datetime(2023, 3, 3, 19, 11, 35, 799799, tzinfo=tzutc()), 'created_at': datetime.datetime(2023, 3, 3, 19, 11, 35, 799799, tzinfo=tzutc())}], 'pipelines': [{'name': 'anomalyhousingpipeline', 'create_time': datetime.datetime(2023, 3, 3, 19, 11, 35, 879127, tzinfo=tzutc()), 'definition': '[]'}]}

Upload The Model

The housing model will be uploaded for use in our pipeline.

housing_model = wl.upload_model("anomaly-housing-model", "./models/housing.zip").configure("tensorflow")

Build the Pipeline and Validation

The pipeline anomaly-housing-pipeline will be created and the anomaly-housing-model added as a step. A validation will be created for outputs greater 100.0. This is interpreted as houses with a value greater than $100 million with the add_validation method. When houses greater than this value are detected, the InferenceObject will add it in the check_failures array with the message “price too high”.

Once complete, the pipeline will be deployed and ready for inferences.

p = wl.build_pipeline('anomalyhousing')
p = p.add_model_step(housing_model)
p = p.add_validation('price too high', housing_model.outputs[0][0] < 100.0)
pipeline = p.deploy()
Waiting for deployment - this will take up to 45s ................ ok

Testing

Two data points will be fed used for an inference.

The first, labeled response_normal, will not trigger an anomaly detection. The other, labeled response_trigger, will trigger the anomaly detection, which will be shown in the InferenceResult check_failures array.

Note that multiple validations can be created to allow for multiple anomalies detected.

if arrowEnabled is True:
    test_input = pd.DataFrame.from_records({"dense_16_input":{"0":[0.02675675,0.0,0.02677953,0.0,0.0010046,0.00951931,0.14795322,0.0027145,0.03550877,0.98536841,0.02988655,0.04031725,0.04298041]}})
else:
    test_input = {"dense_16_input":[[0.02675675, 0.0, 0.02677953, 0.0, 0.0010046, 0.00951931, 0.14795322, 0.0027145,  0.03550877, 0.98536841, 0.02988655, 0.04031725, 0.04298041]]}

response_normal = pipeline.infer(test_input)
display(response_normal)
time in.dense_16_input out.dense_19 check_failures
0 2023-03-03 19:19:44.569 [0.02675675, 0.0, 0.02677953, 0.0, 0.0010046, 0.00951931, 0.14795322, 0.0027145, 0.03550877, 0.98536841, 0.02988655, 0.04031725, 0.04298041] [10.349835] 0
if arrowEnabled is True:
    test_input = pd.DataFrame.from_records({"dense_16_input":{"0":[0.02675675,0.0,0.02677953,0.0,0.0010046,0.00951931,0.14795322,0.0027145,2,0.98536841,0.02988655,0.04031725,0.04298041]}})
else:
    test_input = {"dense_16_input":[[0.02675675, 0.0, 0.02677953, 0.0, 0.0010046, 0.00951931, 0.14795322, 0.0027145, 2, 0.98536841, 0.02988655, 0.04031725, 0.04298041]]}

response_trigger = pipeline.infer(test_input)
display(response_trigger)
time in.dense_16_input out.dense_19 check_failures
0 2023-03-03 19:19:44.657 [0.02675675, 0.0, 0.02677953, 0.0, 0.0010046, 0.00951931, 0.14795322, 0.0027145, 2.0, 0.98536841, 0.02988655, 0.04031725, 0.04298041] [350.4699] 1

Multiple Tests

With the initial tests run, we can run the inferences against a larger set of data and identify anomalies that appear versus the expected results. These will be displayed into a graph so we can see where the anomalies occur. In this case with the house that came in at $350 million - outside of our validation range.

Note: Because this is splitting one batch inference into 400 separate inferences for this example, it may take longer to run.

if arrowEnabled is True:
    test_data = pd.read_json('./data/test_data_anomaly_df.json', orient="records")
    responses_anomaly = pd.DataFrame()
    # For the first 400 rows, submit that row as a separate DataFrame
    # Add the results to the responses_anomaly dataframe
    for index, row in test_data.head(400).iterrows():
        responses_anomaly = responses_anomaly.append(pipeline.infer(row.to_frame('dense_16_input').reset_index()))
else:
    responses_anomaly =[]
    from data import test_data_anomaly
    for nth in range(400):
        responses_anomaly.extend(pipeline.infer(test_data_anomaly.data[nth]))
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
if arrowEnabled is True:
    houseprices = pd.DataFrame({'sell_price': responses_anomaly['out.dense_19'].apply(lambda x: x[0])})
else:
    houseprices = pd.DataFrame({'sell_price': [r.raw['outputs'][0]['Float']['data'][0] for  r in responses_anomaly]})

houseprices.hist(column='sell_price', bins=50, grid=False, figsize=(12,8))
plt.axvline(x=100, color='gray', ls='--')
_ = plt.title('Distribution of predicted home sales price')
png

How To Check For Anomalies

There are two primary methods for detecting anomalies with Wallaroo:

  • As demonstrated in the example above, from the InferenceObject check_failures array in the output of each inference to see if anything has happened.
  • The other method is to view pipeline’s logs and see what anomalies have been detected.

View Logs

Anomalies can be displayed through the pipeline logs() method. The parameter valid=False will show any validations that were flagged as False - in this case, houses that were above 100 million in value.

logs = pipeline.logs(valid=False)
display(logs)

Undeploy The Pipeline

With the example complete, we undeploy the pipeline to return the resources back to the Wallaroo instance.

pipeline.undeploy()
Waiting for undeployment - this will take up to 45s ..................................... ok
name anomalyhousingpipeline
created 2023-03-03 19:11:35.879127+00:00
last_updated 2023-03-03 19:19:27.462171+00:00
deployed False
tags
versions 649283cf-6a4c-45b5-a6a7-a7c7dada5d84, 58c17376-838f-4121-91c4-4ff6dcb85728, f05819e7-8019-4f5c-ae07-6f74c02450d0, 09f7b6e3-009f-4e0f-b93a-9225975c8fbd
steps anomaly-housing-model

6.3 - Shadow Deployment Tutorial

The Shadow Deployment Tutorial demonstrates how to use Wallaroo to deploy challenger models to test the performance against champion models.

This tutorial and the assets can be downloaded as part of the Wallaroo Tutorials repository.

Shadow Deployment Tutorial

Wallaroo provides a method of testing the same data against two different models or sets of models at the same time through shadow deployments otherwise known as parallel deployments. This allows data to be submitted to a pipeline with inferences running on two different sets of models. Typically this is performed on a model that is known to provide accurate results - the champion - and a model that is being tested to see if it provides more accurate or faster responses depending on the criteria known as the challengers. Multiple challengers can be tested against a single champion.

As described in the Wallaroo blog post The What, Why, and How of Model A/B Testing:

In data science, A/B tests can also be used to choose between two models in production, by measuring which model performs better in the real world. In this formulation, the control is often an existing model that is currently in production, sometimes called the champion. The treatment is a new model being considered to replace the old one. This new model is sometimes called the challenger….

Keep in mind that in machine learning, the terms experiments and trials also often refer to the process of finding a training configuration that works best for the problem at hand (this is sometimes called hyperparameter optimization).

When a shadow deployment is created, only the inference from the champion is returned in the InferenceResult Object data, while the result data for the shadow deployments is stored in the InferenceResult Object shadow_data.

The following tutorial will demonstrate how:

  • Upload champion and challenger models into a Wallaroo instance.
  • Create a shadow deployment in a Wallaroo pipeline.
  • Perform an inference through a pipeline with a shadow deployment.
  • View the data and shadow_data results from the InferenceResult Object.
  • View the pipeline logs and pipeline shadow logs.

This tutorial provides the following:

  • dev_smoke_test.json: Sample test data used for the inference testing.
  • models/keras_ccfraud.onnx: The champion model.
  • models/modelA.onnx: A challenger model.
  • models/xgboost_ccfraud.onnx: A challenger model.

All models are similar to the ones used for the Wallaroo-101 example included in the Wallaroo Tutorials repository.

Steps

Import libraries

The first step is to import the libraries required.

import wallaroo
from wallaroo.object import EntityNotFoundError

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)

Connect to Wallaroo

Connect to your Wallaroo instance and save the connection as the variable wl.

# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR SUFFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")
<wallaroo.client.Client at 0x7f876c227250>

Arrow Support

As of the 2023.1 release, Wallaroo provides support for dataframe and Arrow for inference inputs. This tutorial allows users to adjust their experience based on whether they have enabled Arrow support in their Wallaroo instance or not.

If Arrow support has been enabled, arrowEnabled=True. If disabled or you’re not sure, set it to arrowEnabled=False

The examples below will be shown in an arrow enabled environment.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
# os.environ["ARROW_ENABLED"]="True"

if "ARROW_ENABLED" not in os.environ or os.environ["ARROW_ENABLED"].casefold() == "False".casefold():
    arrowEnabled = False
else:
    arrowEnabled = True
print(arrowEnabled)

Set Variables

The following variables are used to create or use existing workspaces, pipelines, and upload the models. Adjust them based on your Wallaroo instance and organization requirements.

workspace_name = 'ccfraudcomparisondemo'
pipeline_name = 'ccshadow'
champion_model_name = 'ccfraud-lstm'
champion_model_file = 'models/keras_ccfraud.onnx'
shadow_model_01_name = 'ccfraud-xgb'
shadow_model_01_file = 'models/xgboost_ccfraud.onnx'
shadow_model_02_name = 'ccfraud-rf'
shadow_model_02_file = 'models/modelA.onnx'

Workspace and Pipeline

The following creates or connects to an existing workspace based on the variable workspace_name, and creates or connects to a pipeline based on the variable pipeline_name.

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline
workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

pipeline = get_pipeline(pipeline_name)
pipeline
name ccshadow
created 2023-03-03 19:20:44.224839+00:00
last_updated 2023-03-03 19:20:44.224839+00:00
deployed (none)
tags
versions 32d751ec-f17b-4516-bc26-e3ef914f0938
steps

Load the Models

The models will be uploaded into the current workspace based on the variable names set earlier and listed as the champion, model2 and model3.

champion = wl.upload_model(champion_model_name, champion_model_file).configure()
model2 = wl.upload_model(shadow_model_01_name, shadow_model_01_file).configure()
model3 = wl.upload_model(shadow_model_02_name, shadow_model_02_file).configure()

Create Shadow Deployment

A shadow deployment is created using the add_shadow_deploy(champion, challengers[]) method where:

  • champion: The model that will be primarily used for inferences run through the pipeline. Inference results will be returned through the Inference Object’s data element.
  • challengers[]: An array of models that will be used for inferences iteratively. Inference results will be returned through the Inference Object’s shadow_data element.
pipeline.add_shadow_deploy(champion, [model2, model3])
pipeline.deploy()
Waiting for deployment - this will take up to 45s .............. ok
name ccshadow
created 2023-03-03 19:20:44.224839+00:00
last_updated 2023-03-03 19:20:45.086221+00:00
deployed True
tags
versions c97f777b-72f2-4a94-b2ea-a21bb33c032f, 32d751ec-f17b-4516-bc26-e3ef914f0938
steps ccfraud-lstm

Run Test Inference

Using the data from sample_data_file, a test inference will be made.

For Arrow enabled Wallaroo instances the model outputs are listed by column. The output data is set by the term out, followed by the name of the model. For the default model, this is out.dense_1, while the shadow deployed models are in the format out_{model name}.variable, where {model name} is the name of the shadow deployed model.

For Arrow disabled environments, the output is from the Wallaroo InferenceResult object.

Using the data from sample_data_file, a test inference will be made. As mentioned earlier, the inference results from the champion model will be available in the returned InferenceResult Object’s data element, while inference results from each of the challenger models will be in the returned InferenceResult Object’s shadow_data element.

if arrowEnabled is True:
    sample_data_file = './smoke_test.df.json'
    response = pipeline.infer_from_file(sample_data_file)
else:
    sample_data_file = './smoke_test.json'
    response = pipeline.infer_from_file(sample_data_file)
display(response)
time in.tensor out.dense_1 check_failures out_ccfraud-rf.variable out_ccfraud-xgb.variable
0 2023-03-03 19:21:00.069 [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] [0.0014974177] 0 [1.0] [0.0005066991]

View Pipeline Logs

With the inferences complete, we can retrieve the log data from the pipeline with the pipeline logs method. Note that for each inference request, the logs return one entry per model. For this example, for one inference request three log entries will be created.

pipeline.logs()
time in.tensor out.dense_1 check_failures out_ccfraud-rf.variable out_ccfraud-xgb.variable
0 2023-03-03 19:21:00.069 [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] [0.0014974177] 0 [1.0] [0.0005066991]

View Logs Per Model

Another way of displaying the logs would be to specify the model.

For Arrow enabled Wallaroo instances the model outputs are listed by column. The output data is set by the term out, followed by the name of the model. For the default model, this is out.dense_1, while the shadow deployed models are in the format out_{model name}.variable, where {model name} is the name of the shadow deployed model.

For arrow disabled Wallaroo instances, to view the inputs and results for the shadow deployed models, use the pipeline logs_shadow_deploy() method. The results will be grouped by the inputs.

logs = pipeline.logs()
if arrowEnabled is True:
    display(logs)
else:
    logs = pipeline.logs()
    display([(log.model_name, log.output) for log in logs])
    shadow_logs = pipeline.logs_shadow_deploy()
    display(shadow_logs)
time in.tensor out.dense_1 check_failures out_ccfraud-rf.variable out_ccfraud-xgb.variable
0 2023-03-03 19:21:00.069 [1.0678324729, 0.2177810266, -1.7115145262, 0.682285721, 1.0138553067, -0.4335000013, 0.7395859437, -0.2882839595, -0.447262688, 0.5146124988, 0.3791316964, 0.5190619748, -0.4904593222, 1.1656456469, -0.9776307444, -0.6322198963, -0.6891477694, 0.1783317857, 0.1397992467, -0.3554220649, 0.4394217877, 1.4588397512, -0.3886829615, 0.4353492889, 1.7420053483, -0.4434654615, -0.1515747891, -0.2668451725, -1.4549617756] [0.0014974177] 0 [1.0] [0.0005066991]

Undeploy the Pipeline

With the tutorial complete, we undeploy the pipeline and return the resources back to the system.

pipeline.undeploy()
Waiting for undeployment - this will take up to 45s .................................... ok
name ccshadow
created 2023-03-03 19:20:44.224839+00:00
last_updated 2023-03-03 19:20:45.086221+00:00
deployed False
tags
versions c97f777b-72f2-4a94-b2ea-a21bb33c032f, 32d751ec-f17b-4516-bc26-e3ef914f0938
steps ccfraud-lstm

7 - Wallaroo Tools Tutorials

Tutorials on additional Wallaroo tools.

The following tutorials are provided by Wallaroo to demonstrate different tools and techniques for working with MLModels and data with Wallaroo.

7.1 - Wallaroo JSON Inference Data to DataFrame and Arrow Tutorials

How to convert from inference inputs using Wallaroo proprietary JSON to Pandas DataFrame and Apache Arrow

The following guide on using inference data inputs from Wallaroo proprietary JSON to either Pandas DataFrame or Apache Arrow downloaded as part of the Wallaroo Tutorials repository.

Introduction

The following guide is to help users transition from using Wallaroo Proprietary JSON to Pandas DataFrame and Apache Arrow. The latter two formats allow data scientists to work natively with DataFrames, and when ready convert those into Arrow table files which provides greater file size efficiency and overall speed.

Using Pandas DataFrames for inference inputs requires typecasting into the Wallaroo inference engine. For models that are sensitive to data types, Arrow is the preferred format.

This guide will demonstrate the following:

Prerequisites

The demonstration code assumes a Wallaroo instance with Arrow support enabled and provides the following:

  • ccfraud.onnx: Sample trained ML Model trained to detect credit card fraud transactions.
  • data/high_fraud.json: Sample inference input formatted in the Wallaroo proprietary JSON format.

The following demonstrates how to convert Wallaroo Proprietary JSON to Pandas DataFrame. This example data and models are taken from the Wallaroo 101, which uses the CCFraud model examples.

Initial Connection to Wallaroo

The following establishes a connection to a Wallaroo instance for testing and sample inferences. These steps can be skipped if no sample inferences are required, and are used to demonstrate how inference inputs and output work with Pandas DataFrame and Apache Arrow.

import wallaroo
from wallaroo.object import EntityNotFoundError
from IPython.display import display

# used to display dataframe information without truncating
from IPython.display import display
import pandas as pd
pd.set_option('display.max_colwidth', None)
# Login through local Wallaroo instance

wl = wallaroo.Client()

# SSO login through keycloak

# wallarooPrefix = "YOUR PREFIX"
# wallarooSuffix = "YOUR PREFIX"

# wl = wallaroo.Client(api_endpoint=f"https://{wallarooPrefix}.api.{wallarooSuffix}", 
#                     auth_endpoint=f"https://{wallarooPrefix}.keycloak.{wallarooSuffix}", 
#                     auth_type="sso")
workspace_name = 'inferencedataexamplesworkspace'
pipeline_name = 'inferencedataexamplespipeline'
model_name = 'ccfraud'
model_file_name = './ccfraud.onnx'

def get_workspace(name):
    workspace = None
    for ws in wl.list_workspaces():
        if ws.name() == name:
            workspace= ws
    if(workspace == None):
        workspace = wl.create_workspace(name)
    return workspace

def get_pipeline(name):
    try:
        pipeline = wl.pipelines_by_name(pipeline_name)[0]
    except EntityNotFoundError:
        pipeline = wl.build_pipeline(pipeline_name)
    return pipeline

# Create the workspace

workspace = get_workspace(workspace_name)

wl.set_current_workspace(workspace)

# upload the model
model = wl.upload_model(model_name, model_file_name).configure()

# Create the pipeline then deploy it
pipeline = get_pipeline(pipeline_name)
pipeline.add_model_step(model).deploy()
name inferencedataexamplespipeline
created 2023-02-28 17:42:19.166319+00:00
last_updated 2023-02-28 17:42:19.871068+00:00
deployed True
tags
versions 0ae102ab-082b-4991-9eb2-f43b44d29ed7, 5a90c841-63c6-45c0-b6e6-042e5d213146
steps ccfraud

Enable Arrow SDK Support

The following is only required when the environment "ARROW_ENABLED" has not been set locally. This environment variable is required in the current release of the SDK to enable Arrow support.

import os
# Only set the below to make the OS environment ARROW_ENABLED to TRUE.  Otherwise, leave as is.
os.environ["ARROW_ENABLED"]="True"

Convert Wallaroo Proprietary JSON to Pandas DataFrame

The following demonstrates how to convert Wallaroo Proprietary JSON to Pandas DataFrame.

Load Libraries

The following libraries are used as part of the conversion process.

import pandas as pd
import pyarrow as pa
import json
import datetime
import numpy as np

Load Wallaroo Data

The Wallaroo data will be saved to a variable. This sample input when run through the trained model as an inference returns a high probability of fraud.

# Start with the single example

high_fraud_data = {
    "tensor": [
        [1.0678324729342086,
        18.155556397512136,
        -1.658955105843852,
        5.2111788045436445,
        2.345247064454334,
        10.467083577773014,
        5.0925820522419745,
        12.829515363712181,
        4.953677046849403,
        2.3934736228338225,
        23.912131817957253,
        1.7599568310350207,
        0.8561037518143335,
        1.1656456468728567,
        0.5395988813934498,
        0.7784221343010385,
        6.75806107274245,
        3.927411847659908,
        12.462178276650056,
        12.307538216518655,
        13.787951906620115,
        1.4588397511627804,
        3.681834686805714,
        1.7539143660379741,
        8.484355003656184,
        14.6454097666836,
        26.852377436250144,
        2.716529237720336,
        3.061195706890285]
    ]
}

Convert to DataFrame

The Wallaroo proprietary JSON file will now be converted into Pandas DataFrame.

high_fraud_dataframe =  pd.DataFrame.from_reco